Sidebar idle / terminal handoff evidence
Failure and scope
The independent /api/sessions response can report idle before the chat SSE's
terminal frame reaches the page. Three sidebar paths were retiring chat state:
idle reconciliation, INFLIGHT purging, and optimistic-row retirement. The idle
path clears the Worklog and active stream ID; the subsequent done is rejected
as stale, so no settled Anchor scene is persisted.
A real-HTTP barrier reproduces this on both published PR head
75c8ff8d2a991f22389e3e4263ee03fb8d8f67a0 and frozen upstream
b09fded7b8c98b549a49aaaece3721015337d307. Applying only the sessions.js
product fix to the frozen upstream also passes the identical oracle: this is a
shared lifecycle defect, not evidence that artifact replay introduced it. The
historical hosted run lacks this instrumentation, so its exact interleaving
cannot be proved retrospectively; its empty-surface/no-persistence signature
matches the controlled reproduction.
Reproduction
LIFECYCLE_SCENARIO=normal LIFECYCLE_TEST_BITE=settle-worklog-frame-proof \
.venv/bin/python tests/browser_conversation_lifecycle.py
./scripts/test.sh tests/test_sidebar_terminal_handoff.py -q
The existing frame proof now uses a test-only server wrapper to hold the actual
done HTTP write. The real worker finishes; the real browser receives an idle
sidebar response through renderSessionList() while its native EventSource
remains open. The test verifies pane, INFLIGHT and registry ownership, then
releases done and retains all existing painted-frame, final-answer, semantic
activity and hard-reload assertions. The fixture always releases/cleans up and
has a bounded wait. Normal production server startup is unchanged.
No renderer, terminal payload or persistence response is fabricated. Runtime events come from the existing deterministic localhost Gateway fixture, not a live model/provider. All state and files are disposable. These are browser viewport checks, not physical-device acceptance.
Before / after
Each screenshot is taken after the idle sidebar response, before the terminal
HTTP frame is released. The exact published source fails at both widths. The
patched source preserves three live activity rows and then passes completion
and hard reload. results.json records the predicate results without machine
paths or credentials.
| Viewport | Before | After |
|---|---|---|
| 1440 x 900 | Empty surface | Owned handoff retained |
| 390 x 844 | Empty surface | Owned handoff retained |
The fix only defers these sidebar cleanups for the current pane's exact OPEN chat transport. Missing, CONNECTING, CLOSED and mismatched sources still take the existing stale recovery path.
Missing terminal frame: bounded recovery
The OPEN-only guard at 85f1c2d34b62b0ade85f51ed2cf8c1cf56f5ab9e could wait
forever when the terminal frame never arrived. The follow-up keeps the live
handoff for 1.5 seconds, then probes the exact stream through
/api/chat/stream/status with an eight-second timeout and no retries.
Repeated sidebar hints cannot restart the deadline or create concurrent
requests. active:true keeps the OPEN owner intact even when the persisted
session fields already look idle; only explicit runtime inactivity permits the
canonical session restore. A failed or malformed runtime probe uses the
existing interrupted-stream path rather than claiming successful settlement.
Normal terminal delivery, transport replacement/close, navigation, and a newer
optimistic turn invalidate the old request. Already-running stream-end recovery
remains the sole recovery owner.
LIFECYCLE_SCENARIO=normal LIFECYCLE_TEST_BITE=sidebar-idle-missing-terminal \
.venv/bin/python tests/browser_conversation_lifecycle.py
./scripts/test.sh tests/test_sidebar_idle_stream_recovery.py -q
The new browser row keeps the real terminal HTTP barrier closed until cleanup;
the client must recover without receiving done or stream_end. Published
85f1c2d34b fails the 11-second completion bound with S.busy still true.
The patched implementation restores the final answer and passes the unchanged
painted-frame, semantic activity, persistence and hard-reload checks before the
barrier is released. The cloud workflow runs both delayed- and missing-terminal
rows. No live provider or production state is used.
A production Gateway regression also blocks post-turn goal evaluation after
the success transcript has already cleared active_stream_id and pending
fields. During that interval the worker and exact entry in STREAMS remain
live and no done event has been emitted, proving why persisted session flags
cannot authorize sidebar settlement on their own.