You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Client: honor status seq + re-seed status on reconnect/gap + turn-level working floor (non-text turns) #1637
Problem — A client can be left showing a stale idle / blank rail while the agent is still running, and it never self-corrects, because of client-side gaps the #1617 fixes do not cover. (1) The #1617 streaming floor rises only on message.part.delta, but the engine streams most turns via message.part.updated, and some turns produce no parts at all (refusal, immediate provider error, empty completion, abort-before-first-token) — so the floor never rises and a stale idle blanks the rail. (2) The transcript indicator and a context-tree "busy" memo read raw status, bypassing session_working. (3) On reconnect/gap the client never re-fetches session status from source of truth, so a status left stale during an outage stays stale until reload. A second session_working (global-sync child store) has no floor at all. Approach — Replace the fragile per-delta floor with a turn-active flag keyed on the turn bracket the server already emits, add fallback clears so it can never wedge, and reconcile from a source of truth that carries idle. (1) Set the flag on session.execution.started (which brackets every turn, part-bearing or not — not "first part," which no-part turns never send) and feed it into bothsession_working implementations. (2) Clear it on the terminal execution.{succeeded,failed,interrupted}and on the safety fallbacks the current floor already has — session.error, session eviction/session.deleted, a session.status:{type:"idle"} frame, and a bounded staleness timeout — so a swallowed terminal event cannot leave the rail stuck "working" forever. (3) Migrate the raw-status display readers onto session_working, while exempting the readers that intentionally trigger on the real idle edge. (4) On every reconnect edge and gap frame, reconcile session_status from /session/status (the full tri-state StatusMap — not/session/active, which lists running sessions only and cannot carry the idle needed to downgrade a stale busy), guarded so a stale response cannot downgrade a live busy. Honor seq when present (depends on #1636). Scope — in: the turn-active flag keyed on execution.started + its fallback clears, feeding both session_working impls (server-session store and global-sync child store, incl. adding the flag's event plumbing to the child-store reducer); migrating the transcript indicator and the context-tab busy memo onto session_working; the reconnect/gap status reconcile from /session/status with a recency guard; the watchdog reconnect bumping the resync token; honoring seq when present. out: the server status emission and seq definition (#1636 — this issue only consumes seq); the fan-in relay reconnect cursor (#1638); the relay backpressure core (#1617, fixed); the diff-refetch and todo-refetch readers that must keep reading the raw idle edge (explicitly exempted, not migrated). Assumptions — session.execution.started reliably brackets the start of every turn; /session/status returns per-session tri-state status for all sessions (verified endpoint, distinct from /session/active); a swallowed terminal event is possible, so the flag needs fallback clears; honoring seq is optional-when-absent so the flag + reconcile halves can land before #1636.
Acceptance Criteria
A turn that produces no parts (refusal, immediate error, empty completion, abort-before-first-token) keeps session_working true for its duration and clears exactly once when it ends — the no-part gap is closed and no stuck-working state results.
A turn that streams only tool or reasoning parts keeps session_working true across a stray idle.
The flag is set on session.execution.started and cleared on the terminal execution events or any fallback (session.error, eviction/session.deleted, an idle status frame, or the bounded timeout) — enumerate each clear path in a test; no terminal path leaves the flag set indefinitely.
The transcript per-turn indicator and the context-tab busy memo reflect session_working, not raw status type; a stray idle during a live turn does not flip either to done.
The diff-refetch and todo-refetch readers still fire on the genuine idle edge (they are NOT migrated).
Both session_working implementations (server-session and global-sync child store) honor the same turn-active floor.
On a reconnect edge and on a gap frame, session_status is reconciled from /session/status (tri-state), correcting a stale idle and a stale busy without a reload.
The reconcile does not downgrade a live busy to idle when the fetched response is older than the last local status mutation for that session (recency-guarded).
The watchdog/silent-socket reconnect bumps the resync token (it currently does not).
New tests: a no-part turn keeps working then clears once; a swallowed-terminal turn clears via each fallback (error / eviction / idle-frame / timeout) and never wedges; the transcript + context-tab readers honor the floor; diff/todo readers still fire on real idle; a reconnect/gap reconcile from a faked /session/status corrects both a stale idle and a stale busy; an out-of-order reconcile does not downgrade a live busy (recency guard); the child-store session_working honors the flag.
The rail self-heal predicate stays a pure in-place unit test.
Key Decisions
Key the flag on execution.started, not "first part." No-part turns never produce a first part; the execution bracket is the only signal guaranteed for every turn shape. This makes the flag a faithful client mirror of the server's busy/idle bracket.
The flag must keep the current floor's self-clearing safety. Clearing only on the three terminal execution events introduces a stuck-working regression if any terminal path (error exits, /session/abort) skips them; the fallback clears (error / eviction / idle-frame / timeout) are required. The AC is "not cleared by a normal mid-turn part.updated," not "cleared exclusively by the three terminals."
Reconcile from /session/status, not /session/active./session/active is running-only and cannot carry the idle needed to correct a stale busy; /session/status carries the full tri-state.
Migrate display readers, exempt trigger readers. The transcript indicator and context-tab busy memo are display and must honor the floor; the diff/todo refetch readers intentionally key on the real idle transition and must keep raw status, or those refetches break.
A genuinely-working session never reads session_working() === false for any turn shape (text, tool-only, reasoning-only, no-part).
The flag never remains set after a turn has genuinely ended (no stuck-working), guaranteed by the fallback clears.
Reconciling never downgrades a live session to idle on a stale/out-of-order response.
No new dependency on message.part.delta for correctness.
Prior Art
The server-session context (session_status writers incl. execution.*, session_working, the Fan-in relay never re-arms backpressure (flowing stuck false) → session transcript + rail go stale until reload #1617streamActiveParts floor + its self-clears, part lifecycle, eviction); the global-sync child store's second session_working and its reducer (writes status only from session.status frames — needs flag plumbing); the transcript turn indicator; the context-tab busy memo; the diff/todo refetch readers (to exempt); the gap-frame interception + forced bootstrap; /session/status (tri-state) vs /session/active (running-only); the active-sessions seed and its guard; the rail self-heal predicate; the liveness watchdog and its forceReconnect (no resync bump today).
Full-path event-delivery sweep + adversarial deliberation on feature/free-tier-fleet. The deliberation moved the flag trigger to execution.started, added the fallback clears (fixing a stuck-working regression), corrected the endpoint to /session/status, named the unlisted context-tab reader, and exempted the diff/todo trigger readers.
Blocked by #1636 for the seq-honoring criterion (the turn-active flag and reconnect re-seed are independent and can land first). Sibling of #1638. Part of the #1617 staleness family.
Deliberated (adversarial review, by hand — tooling unavailable; weaker claim than a tooled review). Coverage-lens critic reading only this issue and the raw client code. Body revised to close its findings:
No-part-turn gap + trigger fix: keying the flag on 'first part' misses turns that produce no parts (refusal, immediate error, empty completion, abort-before-token). The flag is now keyed on session.execution.started, which brackets every turn.
Stuck-working regression (the opposite bug): clearing only on the three terminal execution events wedges the rail 'working' forever if any terminal path (error exits, /session/abort) skips them. Added fallback clears (session.error, eviction, idle status frame, bounded timeout) — the safety the current per-delta floor already has. Softened the AC accordingly.
Wrong endpoint (schema mismatch):/session/active is running-only {type:"running"} and cannot carry idle, so it cannot downgrade a stale busy. Reconcile now from /session/status (full tri-state).
Unnamed reader + exemptions: named the context-tab busy memo (same bug) to migrate; explicitly exempted the diff-refetch and todo-refetch readers, which must keep reading the real idle edge.
Downgrade race: added a recency guard so an out-of-order reconcile cannot overwrite a live busy with a stale idle.
Landed via #1642 (merge commit f4518fe) into feature/free-tier-fleet. Director-run gates + CI all green (engine-tests, fast, build-binary, schema-roundtrip, boot-smoke ×3, vsix-gate). Not auto-closed because the PR targets feature/free-tier-fleet, not the default branch.
Important
Problem — A client can be left showing a stale idle / blank rail while the agent is still running, and it never self-corrects, because of client-side gaps the #1617 fixes do not cover. (1) The
#1617streaming floor rises only onmessage.part.delta, but the engine streams most turns viamessage.part.updated, and some turns produce no parts at all (refusal, immediate provider error, empty completion, abort-before-first-token) — so the floor never rises and a stale idle blanks the rail. (2) The transcript indicator and a context-tree "busy" memo read raw status, bypassingsession_working. (3) On reconnect/gap the client never re-fetches session status from source of truth, so a status left stale during an outage stays stale until reload. A secondsession_working(global-sync child store) has no floor at all.Approach — Replace the fragile per-delta floor with a turn-active flag keyed on the turn bracket the server already emits, add fallback clears so it can never wedge, and reconcile from a source of truth that carries idle. (1) Set the flag on
session.execution.started(which brackets every turn, part-bearing or not — not "first part," which no-part turns never send) and feed it into bothsession_workingimplementations. (2) Clear it on the terminalexecution.{succeeded,failed,interrupted}and on the safety fallbacks the current floor already has —session.error, session eviction/session.deleted, asession.status:{type:"idle"}frame, and a bounded staleness timeout — so a swallowed terminal event cannot leave the rail stuck "working" forever. (3) Migrate the raw-status display readers ontosession_working, while exempting the readers that intentionally trigger on the real idle edge. (4) On every reconnect edge and gap frame, reconcilesession_statusfrom/session/status(the full tri-state StatusMap — not/session/active, which lists running sessions only and cannot carry the idle needed to downgrade a stale busy), guarded so a stale response cannot downgrade a live busy. Honorseqwhen present (depends on #1636).Scope — in: the turn-active flag keyed on
execution.started+ its fallback clears, feeding bothsession_workingimpls (server-session store and global-sync child store, incl. adding the flag's event plumbing to the child-store reducer); migrating the transcript indicator and the context-tab busy memo ontosession_working; the reconnect/gap status reconcile from/session/statuswith a recency guard; the watchdog reconnect bumping the resync token; honoringseqwhen present. out: the server status emission andseqdefinition (#1636 — this issue only consumesseq); the fan-in relay reconnect cursor (#1638); the relay backpressure core (#1617, fixed); the diff-refetch and todo-refetch readers that must keep reading the raw idle edge (explicitly exempted, not migrated).Assumptions —
session.execution.startedreliably brackets the start of every turn;/session/statusreturns per-session tri-state status for all sessions (verified endpoint, distinct from/session/active); a swallowed terminal event is possible, so the flag needs fallback clears; honoringseqis optional-when-absent so the flag + reconcile halves can land before #1636.Acceptance Criteria
session_workingtrue for its duration and clears exactly once when it ends — the no-part gap is closed and no stuck-working state results.session_workingtrue across a stray idle.session.execution.startedand cleared on the terminal execution events or any fallback (session.error, eviction/session.deleted, an idle status frame, or the bounded timeout) — enumerate each clear path in a test; no terminal path leaves the flag set indefinitely.session_working, not raw status type; a stray idle during a live turn does not flip either to done.session_workingimplementations (server-session and global-sync child store) honor the same turn-active floor.session_statusis reconciled from/session/status(tri-state), correcting a stale idle and a stale busy without a reload.seq, a status withseq <=the last seen for that session is discarded (exercised once Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636 lands; marked blocked-on-Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636 until then).Testing Decisions
server-sessioncontext suite (owns the status writers,session_working, and the Fan-in relay never re-arms backpressure (flowing stuck false) → session transcript + rail go stale until reload #1617 floor tests)./session/statuscorrects both a stale idle and a stale busy; an out-of-order reconcile does not downgrade a live busy (recency guard); the child-storesession_workinghonors the flag.Key Decisions
execution.started, not "first part." No-part turns never produce a first part; the execution bracket is the only signal guaranteed for every turn shape. This makes the flag a faithful client mirror of the server's busy/idle bracket./session/abort) skips them; the fallback clears (error / eviction / idle-frame / timeout) are required. The AC is "not cleared by a normal mid-turnpart.updated," not "cleared exclusively by the three terminals."/session/status, not/session/active./session/activeis running-only and cannot carry the idle needed to correct a stale busy;/session/statuscarries the full tri-state.seqonce Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636 lands).seqhonoring is optional-when-absent so the flag + reconcile land without hard-blocking on Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636; strongest paired with it.Constraints & Invariants
session_working() === falsefor any turn shape (text, tool-only, reasoning-only, no-part).message.part.deltafor correctness.Prior Art
server-sessioncontext (session_statuswriters incl.execution.*,session_working, the Fan-in relay never re-arms backpressure (flowing stuck false) → session transcript + rail go stale until reload #1617streamActivePartsfloor + its self-clears, part lifecycle, eviction); the global-sync child store's secondsession_workingand its reducer (writes status only fromsession.statusframes — needs flag plumbing); the transcript turn indicator; the context-tab busy memo; the diff/todo refetch readers (to exempt); the gap-frame interception + forced bootstrap;/session/status(tri-state) vs/session/active(running-only); the active-sessions seed and its guard; the rail self-heal predicate; the liveness watchdog and itsforceReconnect(no resync bump today).seqthis issue honors); [A3] Add status reconciliation fallback for remote prompts #1567 (status reconcile); Webview: client-side SSE liveness watchdog + wake re-arm (server-sdk) #1584/Webview: entity rail self-heals /amicode/problem on stream reconnect #1585 (watchdog + rail self-heal this broadens); Fan-in relay resumes re-opened arm from connect-time cursor + no gap frame on reconnect → silent event loss #1638 (relay sibling).Source
feature/free-tier-fleet. The deliberation moved the flag trigger toexecution.started, added the fallback clears (fixing a stuck-working regression), corrected the endpoint to/session/status, named the unlisted context-tab reader, and exempted the diff/todo trigger readers.Notes
seq-honoring criterion depends on Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636; the turn-active flag and the/session/statusreconcile are independent and can land first.