fix: turn-keyed settling — kill the premature-completed / wrong-thread-state cluster - #442
Merged
Merged
Conversation
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>
14 tasks
14 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.setnow carries an explicit optionalsettledTurn {turnId, state}marker; projections settle exactly that turn. Bareready/idlesettles nothing; only genuinely terminal statuses (error/interrupted/stopped) sweep.latest_turn_idstops flickering to null on turn end — terminal states project immediately, including on non-git threads.turn.abortedis finally ingested (was dropped entirely — Stop left sessions running forever); id-less aborts stop the active turn.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 sessionerror(wasready= fake success).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):
ThreadSessionTurnProjectionmirrors turn-keyed settling and decodessettledTurn.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.isCancellationPendingthreaded into the policy).Wire compatibility:
settledTurnis 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 appapps/windowsapps/mobileapps/server— backend serverpackages/contracts)Release size
size:XSsize:Ssize:Msize:Lsize:XLVerification
pnpm run verify --allpasses (check + typecheck + full TS suite + full Swift suite + lint:mobile), exit 0🤖 Generated with Claude Code