From 21d058e744dd38ef029ed65b2f074401fa65b94d Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Sat, 25 Jul 2026 17:57:29 -0700 Subject: [PATCH] Clear the durable live-session checkpoint on a terminal SSE error reply The documented abort flow in reference/live.md (live-poll.mjs --reply error "...") reset the browser bar to PICKING but left the localStorage checkpoint written for the GENERATING phase in place. Every reload then resurrected a dead session the server no longer knew about, and the page stayed wedged until the user hand-cleared the impeccable-live* keys in the console (issue #362, diagnosed by @yourcodekitten). An agent error reply is terminal for the session it names: when the id matches the current session, run the same markSessionHandled + cleanup teardown as 'discarded' (cleanup includes clearSession); when it matches a stored-but-not-current checkpoint (the error raced a reload), drop that checkpoint too. Errors that name no session keep the existing UI-only reset, and the accept-cleanup and steer branches are untouched. Regression guard added to tests/live-browser-regression.test.mjs. Prepared with AI assistance (Claude Code), directed by @pbakaus. Co-Authored-By: Claude Code --- skill/scripts/live-browser.js | 13 +++++++++++++ tests/live-browser-regression.test.mjs | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/skill/scripts/live-browser.js b/skill/scripts/live-browser.js index 07f09326a..ecfc212c6 100644 --- a/skill/scripts/live-browser.js +++ b/skill/scripts/live-browser.js @@ -6499,6 +6499,19 @@ if (maybeCompleteSteer(msg)) break; console.error('[impeccable] Error:', msg.message); showToast('Error: ' + msg.message, 5000); + // An agent error reply is terminal for the session it names: tear + // it down exactly like 'discarded' (cleanup includes clearSession), + // or the durable localStorage checkpoint survives and every reload + // resurrects a GENERATING bar for a session the server no longer + // knows about (issue #362). + if (msg.id && msg.id === currentSessionId) { + markSessionHandled(); + cleanup(); + break; + } + // A stored-but-not-current checkpoint naming the errored session + // (the error raced a reload) must not resurrect either. + if (msg.id && loadSession()?.id === msg.id) clearSession(); hideBar(); renderEditBadge('hidden'); setLiveState('PICKING'); diff --git a/tests/live-browser-regression.test.mjs b/tests/live-browser-regression.test.mjs index 406e1e97e..c092cc7db 100644 --- a/tests/live-browser-regression.test.mjs +++ b/tests/live-browser-regression.test.mjs @@ -273,6 +273,31 @@ describe('live-browser.js regression guards', () => { ); }); + it('SSE error reply clears the durable session checkpoint like discarded', () => { + // Issue #362: `live-poll.mjs --reply error "..."` is the documented + // abort flow in reference/live.md, and an agent error reply is terminal + // for the session it names. The 'error' case used to reset only the UI + // (hideBar + PICKING) while the localStorage checkpoint written for the + // GENERATING phase survived — so every reload resurrected a dead session + // the server no longer knew about, until the user hand-cleared the + // impeccable-live* keys in the console. The error path must tear down the + // named session exactly like 'discarded' does (markSessionHandled + + // cleanup, which includes clearSession), and drop a stored-but-not- + // current checkpoint that matches the errored id. + const errorCase = SOURCE.match(/case 'error':[\s\S]{0,2500}?setLiveState\('PICKING'\);\s*break;/); + assert.ok(errorCase, 'expected an SSE case \'error\' handler in live-browser.js'); + assert.match( + errorCase[0], + /if \(msg\.id && msg\.id === currentSessionId\) \{[\s\S]{0,160}?markSessionHandled\(\);[\s\S]{0,80}?cleanup\(\);[\s\S]{0,80}?break;/, + 'an error reply naming the current session must run the same markSessionHandled + cleanup teardown as \'discarded\' so the durable checkpoint is cleared', + ); + assert.match( + errorCase[0], + /if \(msg\.id && loadSession\(\)\?\.id === msg\.id\) clearSession\(\);/, + 'an error reply naming a stored-but-not-current session must drop that checkpoint so a reload cannot resurrect it', + ); + }); + it('handleServerLost preserves the current recoverable phase', () => { assert.doesNotMatch( SOURCE,