From 4b2682e9fa9554cf78014c6562065b5eb59e8dc5 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 28 Sep 2026 20:24:05 -0500 Subject: [PATCH] fix(desktop): prefer the live backlog over the last snapshot fence when appending a catch-up delta syncAgentTerminalSnapshot checked the stale lastSnapshots prefix-delta before the authoritative backlog (body) prefix-delta. Live agent.terminal.output chunks advance the backlog but never lastSnapshots, so once any chunk has streamed since the last snapshot, body is strictly longer than previous and both are prefixes of a catch-up process.list snapshot. The fence-first branch then emitted output.slice(previous.length), re-appending bytes the chunks had already written and duplicating them on screen and in the replay backlog. body is the authoritative on-screen state and is always >= previous, so its delta is the correct, smaller one; check it first. Regression case added to agent-terminal-stream.bound.test.ts (red without the swap). Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com> --- .../agent-terminal-stream.bound.test.ts | 20 +++++++++++++++++++ .../terminal/agent-terminal-stream.ts | 16 +++++++++------ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.bound.test.ts b/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.bound.test.ts index 2b95bf8e34..067d1b86a5 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.bound.test.ts +++ b/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.bound.test.ts @@ -133,6 +133,26 @@ describe('agent terminal retention is bounded', () => { expect(replay('reset-11')).toContain(tail.slice(0, 1000)) }) + it('a catch-up snapshot does not re-print bytes already streamed by live chunks', () => { + // Live `agent.terminal.output` chunks advance the backlog but never the + // lastSnapshots fence. A catch-up snapshot that contains everything shown so + // far matches BOTH prefix checks, and the fence-first order emitted + // output.slice(previous.length) — re-appending bytes the chunks had already + // written, so the screen showed `AAABBBAAABBBCCC` for `AAABBB` + `CCC`. + const proc = 'dup-proc' + let screen = '' + + const stop = registerAgentTerminalWriter(proc, chunk => { + screen = chunk.startsWith('\x1bc') ? chunk.slice(2) : screen + chunk + }) + + writeAgentTerminalChunk(proc, 'AAA') + writeAgentTerminalChunk(proc, 'BBB') + syncAgentTerminalSnapshot(proc, 'AAABBBCCC') + stop() + expect(screen).toBe('AAABBBCCC') + }) + it('an evicted process does not diff against a tail it no longer has', () => { // `lastSnapshots` is the delta fence for `backlog`. If eviction dropped the // backlog but KEPT the fence, the next snapshot takes the `startsWith(previous)` diff --git a/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.ts b/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.ts index cb78f46646..c9b778f6cf 100644 --- a/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.ts +++ b/apps/desktop/src/app/right-sidebar/terminal/agent-terminal-stream.ts @@ -137,18 +137,22 @@ export function syncAgentTerminalSnapshot(procId: string, output: string): void return } - // The fence is updated BEFORE the write so the write's eviction pass counts it: - // after, it would re-create a fence for a process that pass just forgot. - if (output.startsWith(previous)) { + // The live backlog (body) is the authoritative on-screen state and is always at + // least as long as the last snapshot fence, so check its prefix-delta FIRST: the + // fence-first order would re-append bytes already streamed by live chunks and + // print them twice. The fence is updated BEFORE the write so the write's eviction + // pass counts it: after, it would re-create a fence for a process that pass just + // forgot. + if (output.startsWith(body)) { lastSnapshots.set(procId, output) - writeAgentTerminalChunk(procId, output.slice(previous.length)) + writeAgentTerminalChunk(procId, output.slice(body.length)) return } - if (output.startsWith(body)) { + if (output.startsWith(previous)) { lastSnapshots.set(procId, output) - writeAgentTerminalChunk(procId, output.slice(body.length)) + writeAgentTerminalChunk(procId, output.slice(previous.length)) return }