Skip to content

Client: honor status seq + re-seed status on reconnect/gap + turn-level working floor (non-text turns) #1637

Description

@jeonghun-jj-lee

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 #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 both session_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).
  • When a status event carries a seq, a status with seq <= 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

  • Reuse-first: extend the existing server-session context 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).
  • 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.
  • Recency-guard the overwrite. Dropping the seed guard and reconciling on every edge risks overwriting a fresh local busy with a stale response; gate the overwrite on the response being newer than the last local mutation (or on seq once Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636 lands).
  • seq honoring 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

  • 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

Source

Notes

Activity

  1. added
    bugSomething isn't working
    afkImplementable without human interaction
    on Sep 28, 2026
  2. jeonghun-jj-lee commented on Sep 28, 2026

    @jeonghun-jj-lee
    ContributorAuthor

    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.

  3. jeonghun-jj-lee commented on Sep 28, 2026

    @jeonghun-jj-lee
    ContributorAuthor

    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.
    • AC that honors seq marked blocked-on-Server: session status has no ordering key + multiple emitters → stale idle while agent runs #1636. No blocking contradiction.
  4. added a commit that references this issue on Sep 28, 2026
  5. jeonghun-jj-lee commented on Sep 28, 2026

    @jeonghun-jj-lee
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkImplementable without human interactionbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions