Recognize a late accept failure after the optimistic teardown

Accept is optimistic: POST /events acknowledging the intent schedules
cleanupAcceptedSession(), which nulls pendingAcceptedSession before
live-accept.mjs has run. When the accept later failed (missing markers,
preview error, receipt conflict, source_locked), the SSE 'error' guard
keyed on pendingAcceptedSession could no longer match its id, so the
tailored recovery never fired: the user got a generic error toast, the
session was gone, and nothing said the variant was never written
(issue #384, analysis by Cursor Bugbot on #381).

Following the issue's fix sketch, an awaitingAcceptResult id is set on
the optimistic success path and deliberately survives the teardown. The
'error' case matches it and tells the user plainly that the variant was
not saved and to pick + generate again (post-teardown the wrapper may
already be gone, so restoring CYCLING is not honestly possible). The
marker is released when the real accept result arrives (complete /
accept / post-accept agent_done) or when a new session supersedes it.

Regression guard covers the set-before-teardown ordering, the error
match, and cleanupAcceptedSession leaving the marker alone; the existing
source contract now also asserts handleGo clears it.

Prepared with AI assistance (Claude Code), directed by @pbakaus.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-25 20:15:05 -07:00
co-authored by Claude Code
parent d272b9bd5d
commit f9ea2f0de0
3 changed files with 67 additions and 3 deletions
+3 -3
View File
@@ -320,7 +320,7 @@ describe('live-browser source contracts', () => {
);
assert.match(
SOURCE,
/case 'complete':\s*case 'accept':\s*if \(maybeCompleteAcceptedSession\(msg\)\) break;/,
/case 'complete':\s*case 'accept':[\s\S]{0,400}?if \(maybeCompleteAcceptedSession\(msg\)\) break;/,
'final accepted DOM cleanup should be driven by explicit complete or harness accept replies',
);
assert.match(
@@ -340,8 +340,8 @@ describe('live-browser source contracts', () => {
assert.match(agentDoneSource, /maybeCompleteAcceptedSession\(msg\)/);
assert.match(
SOURCE,
/function handleGo\(\)[\s\S]{0,900}?pendingAcceptedSession = null;[\s\S]{0,80}?currentSessionId = id8\(\);/,
'starting a new generation should clear any stale accepted-session sentinel first',
/function handleGo\(\)[\s\S]{0,900}?pendingAcceptedSession = null;[\s\S]{0,400}?awaitingAcceptResult = null;[\s\S]{0,120}?currentSessionId = id8\(\);/,
'starting a new generation should clear any stale accepted-session sentinel (and the awaited accept-result marker, #384) first',
);
const handleAcceptStart = SOURCE.indexOf('function handleAccept()');
const maybeCompleteStart = SOURCE.indexOf('function maybeCompleteAcceptedSession', handleAcceptStart);