Fix cross-worktree chat leak: key native-chat transcript cache by resolved file path (#7341)

This commit is contained in:
Neil
2026-07-04 02:40:37 -07:00
committed by GitHub
parent 7c10abc582
commit 533cafdfa9
2 changed files with 75 additions and 11 deletions
+16 -11
View File
@@ -4,13 +4,18 @@ import { resolveSessionFilePath } from './session-file-resolver'
import { readNativeChatTranscript, type ReadTranscriptResult } from './transcript-reader'
// Why: both the desktop IPC handler and the runtime RPC handler read the same
// host-filesystem transcript, so a single process-global cache keyed by
// agent:sessionId maximizes the hit rate across desktop + every paired
// web/mobile client. Keying by connection instead would defeat the multi-client
// case this feature targets and multiply memory by the connection count.
// The cache stores ONE canonical, unwindowed parse; windowing and per-surface
// truncation stay in the callers so the same parse is reused across all `limit`
// values and every client kind.
// host-filesystem transcript, so a single process-global cache keyed by the
// RESOLVED transcript file path maximizes the hit rate across desktop + every
// paired web/mobile client (all clients of one session resolve the same path
// against this runtime's home). Keying by connection instead would defeat the
// multi-client case this feature targets and multiply memory by the connection
// count. The key is the resolved file path, NOT `agent:sessionId`: two panes can
// share one sessionId yet resolve to DIFFERENT files (the same session resumed
// into a second worktree, which writes a new transcript file), and a
// sessionId-only key let one worktree's cached parse be served to another when
// their file mtimes momentarily coincided (#7326). The cache stores ONE
// canonical, unwindowed parse; windowing and per-surface truncation stay in the
// callers so the same parse is reused across all `limit` values and every client kind.
type CachedTranscript = {
result: ReadTranscriptResult
@@ -40,8 +45,8 @@ function setCached(key: string, value: CachedTranscript): void {
}
}
function cacheKey(agent: AgentType, sessionId: string): string {
return `${agent}:${sessionId}`
function cacheKey(agent: AgentType, filePath: string): string {
return `${agent}:${filePath}`
}
async function fileMtimeMs(filePath: string): Promise<number> {
@@ -68,7 +73,7 @@ export async function readNativeChatTranscriptCached(
return { error: `No transcript found for ${agent} session ${sessionId}` }
}
const key = cacheKey(agent, sessionId)
const key = cacheKey(agent, filePath)
const mtimeMs = await fileMtimeMs(filePath)
const cached = cache.get(key)
if (cached && Number.isFinite(mtimeMs) && cached.mtimeMs === mtimeMs) {
@@ -84,7 +89,7 @@ export async function readNativeChatTranscriptCached(
return result
}
/** Test-only: drop the per-session transcript cache between runs. */
/** Test-only: drop the transcript parse cache between runs. */
export function clearNativeChatTranscriptCache(): void {
cache.clear()
}