From 76589d3ddc79d7aa0bfb03c252aae5d630ddf875 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Wed, 22 Jul 2026 11:10:57 -0700 Subject: [PATCH] fix(facts): fold visibility tier into hot-memory meta cache key trustedFactReads is re-read from config on every stdio tool call, so the reader's visibility tier can now flip within one server process. The meta-hook cache key (source, session, allow-list hash) didn't capture it, so a mid-session opt-out of facts.trust_local_reads could keep serving a private-inclusive cached payload for the 30s TTL. Compute the visibility filter before the cache lookup and append the tier to the key, restoring the header's cross-tier no-bleed invariant. Co-Authored-By: Claude Fable 5 --- src/core/facts/meta-hook.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/facts/meta-hook.ts b/src/core/facts/meta-hook.ts index 07ba339d2..4fb065285 100644 --- a/src/core/facts/meta-hook.ts +++ b/src/core/facts/meta-hook.ts @@ -51,7 +51,13 @@ export async function getBrainHotMemoryMeta( const sessionId = (ctx as { source_session?: string }).source_session ?? null; const allowListHash = hashAllowList(ctx.takesHoldersAllowList); - const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}`; + // Visibility tier: untrusted remote → world-only; trusted local + + // owner-trusted reads → all rows. Folded into the cache key (the header's + // "cache entries don't bleed across tiers" invariant): trustedFactReads is + // re-read from config per call, so a mid-session opt-out must not keep + // serving a private-inclusive cached payload for the TTL window. + const visibility = readableFactVisibilities(ctx); + const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}::${visibility ? 'world' : 'all'}`; const ttl = Math.max(1000, opts.ttlMs ?? DEFAULT_TTL_MS); const topK = Math.max(1, Math.min(opts.topK ?? DEFAULT_TOP_K, 25)); @@ -62,10 +68,6 @@ export async function getBrainHotMemoryMeta( return cached.payload; } - // Build a fresh payload. Visibility tier: untrusted remote → world-only; - // trusted local + owner-trusted reads → all rows. - const visibility = readableFactVisibilities(ctx); - let rows: FactRow[] = []; if (sessionId) { rows = await ctx.engine.listFactsBySession(sourceId, sessionId, {