fix(provider): honest Stop, serialized session starts, message-before-turn ordering - #455
Merged
Merged
Conversation
…arts CodexSessionRuntime.interruptTurn returned success when it could not resolve a turn id, so pressing Stop did nothing with no error. It now fails typed instead of lying. CodexAdapter/ClaudeAdapter startSession raced a plain check-then-replace on the session map: two concurrent starts for one thread both observed "no session", both spawned a runtime, and the loser was orphaned — a live agent process nothing owns or stops. New shared threadLocks.ts (one withThreadLock implementation instead of five adapter-local copies) serializes session start per thread; Codex and Claude now use it like Kimi/Grok/Cursor already did. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r session start Claude and Cursor emitted turn.started (and, for Cursor, activeTurnId) before building/validating the user message and its attachments. A rejected attachment then left an orphan running turn nothing would ever settle — the thread spun forever with no error. The build/validate step now runs first, matching Kimi's existing order. Cursor also never set session status "running" when it opened a turn, and never cleared activeTurnId when turn.completed fired — Cursor threads read permanently busy, so the startup stale-session sweep could never reclaim them. Both now match the Kimi/Grok status lifecycle, and startSession is serialized per thread via the shared threadLocks helper like every other adapter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… turn A turn-less turn.completed with an active tracked turn was rejected outright, so the thread read "running" until the 15-minute stall watchdog swept it — a phantom hang for a turn that actually finished. It now settles the tracked active turn, but only when the provider session confirms there is no other turn it could ambiguously belong to; a genuine conflict (the provider has moved to a different turn) still rejects rather than closing live work. Also: an unrecognized projection_turns.state value (a rolled-back schema, a corrupted write) mapped to "running" — the worst possible guess, since no live turn exists to ever settle it. Maps to "interrupted" instead: settled, and honest that the turn did not finish. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
CodexSessionRuntime.interruptTurnreturned success when it could not resolve a turn id — the user presses Stop, nothing happens, no error. Now fails typed instead of lying.startSessioncalls for one thread raced a plain check-then-replace on the session map (Codex, Claude) — both saw "no session", both spawned a runtime, the loser was orphaned with nothing to stop it. New sharedthreadLocks.ts(onewithThreadLockimplementation instead of five per-adapter copies) serializes session start per thread; Codex and Claude now match Kimi/Grok/Cursor.turn.started(and, for Cursor,activeTurnId) before building/validating the user message and its attachments. A rejected attachment then left a running turn nothing would ever settle. Validation now runs first, matching Kimi's existing order.activeTurnIdon `turn.completed" — Cursor threads read permanently busy, so the startup stale-sweep could never reclaim them. Now matches the Kimi/Grok lifecycle.turn.completedwith an active tracked turn was rejected outright — the thread read "running" until the 15-minute watchdog swept it. Now settles the tracked turn, but only when the provider session confirms there's no other turn it could ambiguously belong to; a genuine conflict still rejects.projection_turns.statevalue mapped to "running" (the worst guess — nothing will ever settle it); now maps to "interrupted".Area
apps/macapps/windowsapps/mobileapps/server— backend serverRelease size
size:XSsize:Ssize:Msize:Lsize:XLVerification
pnpm run verify --allafter merging current main: 5/5 steps, exit 0🤖 Generated with Claude Code