From a88581d8d58c1948c5f8f8fbe7a64722d1eb9219 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:05:06 -0700 Subject: [PATCH] perf(claude): look up retained prompts' turns in one journal pass during catch-up (#36127) Catch-up scanned the whole chat journal once per retained prompt. Index prompt-to-turn matches once per reader, the same way the reader already indexes local prompts, keeping every match as a commit dependency in journal order. Tracked in STA-12049. --- .../claude/claude-transcript-catch-up.test.ts | 5 ++- .../claude/claude-transcript-item-reader.ts | 32 ++++++++++++------- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/main/claude/claude-transcript-catch-up.test.ts b/src/main/claude/claude-transcript-catch-up.test.ts index d7027f1cf1d..ecfbd806a9f 100644 --- a/src/main/claude/claude-transcript-catch-up.test.ts +++ b/src/main/claude/claude-transcript-catch-up.test.ts @@ -477,7 +477,7 @@ describe('Claude chat catch-up', () => { expect(c.warn).not.toHaveBeenCalled() }) - // P saved aliases plus P unacknowledged queued prompts: P×J scope work and one J-item index. + // P saved aliases plus P unacknowledged queued prompts: two J-item indexes, not P×J scans. it.each([10, 100].flatMap((prompts) => [1_000, 10_000].map((items) => ({ prompts, items }))))( 'bounds retained-leaf work for $prompts aliases and local misses over $items journal items', async ({ prompts, items }) => { @@ -569,8 +569,7 @@ describe('Claude chat catch-up', () => { expect(c.journal.canonicalItemId(alias)).toBe(canonical) expect(committed).not.toHaveBeenCalled() expect(c.warn).not.toHaveBeenCalled() - expect(associationVisits).toBeGreaterThan(prompts * itemCount) - expect(associationVisits).toBeLessThanOrEqual((prompts + 1) * itemCount) + expect(associationVisits).toBeLessThanOrEqual(2 * itemCount) expect(viewVisits).toBeLessThanOrEqual(2 * itemCount) console.info('retained-leaf operation counts', { prompts, diff --git a/src/main/claude/claude-transcript-item-reader.ts b/src/main/claude/claude-transcript-item-reader.ts index 93bb10caa63..3fbdcf55a35 100644 --- a/src/main/claude/claude-transcript-item-reader.ts +++ b/src/main/claude/claude-transcript-item-reader.ts @@ -15,6 +15,7 @@ export function createClaudeTranscriptItemReader( ) { const dependencies = new Map() let localPrompts: Map | undefined + let promptTurns: Map | undefined const saved = (key: string) => { const held = dependencies.get(key) if (held) { @@ -63,19 +64,28 @@ export function createClaudeTranscriptItemReader( const scope = scopeOf(item) return scope.kind === 'turn' ? scope : undefined } - let scope: AgentJournalTurnScope | undefined - journal.visitItems((itemId, _sequence, body) => { - if ( - body.kind === 'turn' && - body.userItemId && - journal.canonicalItemId(body.userItemId) === item.itemId - ) { - const turn = saved(itemId) - if (turn) { - scope = scopeOf({ ...item, turnScope: { kind: 'turn', turnItemId: itemId } }) + // One pass, like localPrompts: a stale entry cannot commit past the catch-up guard. + if (!promptTurns) { + const turns = new Map() + journal.visitItems((itemId, _sequence, body) => { + if (body.kind === 'turn' && body.userItemId) { + const user = journal.canonicalItemId(body.userItemId) + const held = turns.get(user) + if (held) { + held.push(itemId) + } else { + turns.set(user, [itemId]) + } } + }) + promptTurns = turns + } + let scope: AgentJournalTurnScope | undefined + for (const turnItemId of promptTurns.get(item.itemId) ?? []) { + if (saved(turnItemId)) { + scope = scopeOf({ ...item, turnScope: { kind: 'turn', turnItemId } }) } - }) + } return scope } return { saved, localPrompt, scopeOf, promptScope, dependencies }