Skip to content

fix: turn-keyed settling — kill the premature-completed / wrong-thread-state cluster - #442

Merged
SergeSerb2 merged 4 commits into
mainfrom
surgecode/status-projection-fixes
Aug 2, 2026
Merged

fix: turn-keyed settling — kill the premature-completed / wrong-thread-state cluster#442
SergeSerb2 merged 4 commits into
mainfrom
surgecode/status-projection-fixes

Conversation

@SergeSerb2

Copy link
Copy Markdown
Owner

Summary

Root-cause fix for the long-reported "threads spam completed / wrong states" cluster (inventory findings 265–270, 153, 157, 158, 228, 134, 135, 45–48, 55; re-verified live on 2026-08-02).

The core defect: a session bouncing through ready/idle — which happens on start, restart, steer, reconnect, and even turn-start failure — settled every running turn as "completed", pinned it there irreversibly, and fired "X finished" notifications while the agent was still working.

Server:

  • thread.session.set now carries an explicit optional settledTurn {turnId, state} marker; projections settle exactly that turn. Bare ready/idle settles nothing; only genuinely terminal statuses (error/interrupted/stopped) sweep.
  • A running session-set naming a settled turn re-opens it (self-heal for premature settles).
  • latest_turn_id stops flickering to null on turn end — terminal states project immediately, including on non-git threads.
  • turn.aborted is finally ingested (was dropped entirely — Stop left sessions running forever); id-less aborts stop the active turn.
  • Interrupted/cancelled outcomes survive end-to-end instead of flattening to "completed"; unknown states map to interrupted, never success.
  • Codex turn/completed{inProgress} ignored in both layers; Claude synthetic (background-chatter) turns close as interrupted; ACP autonomous-turn idle close raised 2s → 30s; turn-start failure marks the session error (was ready = fake success).
  • Startup stale-session sweep delayed 15s + re-probes the provider before interrupting (T3_STALE_SESSION_SWEEP_DELAY_MS); TurnActivityWatchdog emits a terminal recovery on session stop so "stalled" can't strand forever.

macOS (must land with the server change — the client folds the same rule):

  • ThreadSessionTurnProjection mirrors turn-keyed settling and decodes settledTurn.
  • Queued follow-ups drain on running → done (they never drained after a normal completion — drain required .idle, which only interrupted turns reach) and no longer fire on denials/probe blips.
  • "X finished" mirrors the haptics policy: only executing statuses claim finished; denials, dismissed questions, background-subagent churn, and user-requested Stops stay silent (isCancellationPending threaded into the policy).

Wire compatibility: settledTurn is optional on both command and event; older clients ignore it. Mobile/client-runtime still folds the old rule client-side — follow-up noted.

Area

  • apps/mac — native macOS app
  • apps/windows
  • apps/mobile
  • apps/server — backend server
  • Shared packages (packages/contracts)
  • Build, CI, or release tooling
  • Docs

Release size

  • size:XS
  • size:S
  • size:M
  • size:L
  • size:XL

Verification

  • pnpm run verify --all passes (check + typecheck + full TS suite + full Swift suite + lint:mobile), exit 0
  • Targeted suites updated to the new contract with new cases: ProjectionPipeline, projector, ProviderCommandReactor (50/50), TurnActivityWatchdog, Grok/Kimi adapters, ThreadStatusProjectionTests (4 new), AgentNotificationPolicyTests (3 new), LiveBackendShellResumeTests

🤖 Generated with Claude Code

SergeSerb2 and others added 4 commits August 2, 2026 13:46
A session bouncing through ready/idle (start, restart, steer, reconnect,
turn-start failure) settled every running turn as "completed", firing
phantom finished notifications and pinning wrong terminal states. Turn
ends are now explicit:

- thread.session.set carries an optional settledTurn {turnId, state}
  marker; projections settle exactly that turn (turnId null = sweep).
- ready/idle alone settle nothing; only genuinely terminal session
  statuses (error, interrupted, stopped) sweep running turns.
- a running session-set naming a settled turn re-opens it (repair path
  for premature settles), clearing the stale completedAt.
- latest_turn_id no longer flickers to null on turn end: it keeps
  pointing at the turn that just settled, so terminal states project
  immediately (including non-git threads that never get turn-diff).
- turn.aborted is ingested (was dropped entirely): session interrupted +
  settledTurn interrupted; an id-less abort stops the active turn.
- turn.completed preserves interrupted/cancelled outcomes instead of
  flattening them to ready->completed; unknown states are interrupted,
  never completed.
- Codex turn/completed{inProgress} is ignored in both layers instead of
  read as success; unknown Codex statuses map to interrupted.
- Claude synthetic turns (background chatter) close as interrupted, not
  completed, so they stop firing "X finished" as the real turn starts.
- ACP autonomous-turn idle close raised 2s -> 30s to stop silent tools
  producing phantom completions and bogus turn boundaries.
- turn-start failure marks the session error (was ready == fake
  successful completion).
- the startup stale-session sweep is delayed 15s
  (T3_STALE_SESSION_SWEEP_DELAY_MS) and re-probes the provider before
  interrupting, so restarts stop force-interrupting live turns.
- TurnActivityWatchdog emits a terminal recovery on session stop so a
  stalled verdict cannot strand forever in history replays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hantom finished banners

- ThreadSessionTurnProjection settles a running turn only from the
  event's explicit settledTurn marker or a genuinely terminal session
  status; a bare ready/idle no longer manufactures a completed turn
  client-side (mirrors the server projector change in the same branch).
- Queued follow-up messages drain on the running->done edge too: a
  normal completion projects .done, never .idle, so Option+Enter queues
  sat forever after successful turns. Denials, probe blips, and
  error->idle transitions no longer fire queued sends (predecessor must
  be .running).
- "X finished" notifications now mirror the haptics policy: only
  executing statuses (running/reviewing/fixing) claim finished, denied
  approvals and dismissed questions stay silent, background subagent
  churn stays silent, and a user-requested Stop passes
  isCancellationPending so the settle it causes is not announced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The detail-stream ready event now settles the running turn via the
explicit settledTurn marker, matching the server's new turn-keyed
settling; a bare ready no longer projects .done.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SergeSerb2 SergeSerb2 added the size:L Broad feature or substantial cross-package change label Aug 2, 2026
@SergeSerb2
SergeSerb2 merged commit 23d8d22 into main Aug 2, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L Broad feature or substantial cross-package change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant