Skip to content

fix(provider): honest Stop, serialized session starts, message-before-turn ordering - #455

Merged
SergeSerb2 merged 4 commits into
mainfrom
surgecode/provider-lifecycle-fixes
Aug 3, 2026
Merged

fix(provider): honest Stop, serialized session starts, message-before-turn ordering#455
SergeSerb2 merged 4 commits into
mainfrom
surgecode/provider-lifecycle-fixes

Conversation

@SergeSerb2

Copy link
Copy Markdown
Owner

Summary

  • Stop silently does nothing (finding 163): CodexSessionRuntime.interruptTurn returned success when it could not resolve a turn id — the user presses Stop, nothing happens, no error. Now fails typed instead of lying.
  • Orphaned agent processes (finding 159): concurrent startSession calls 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 shared threadLocks.ts (one withThreadLock implementation instead of five per-adapter copies) serializes session start per thread; Codex and Claude now match Kimi/Grok/Cursor.
  • Orphan running turns (finding 155): Claude and Cursor emitted 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.
  • Cursor permanently busy (finding 156): Cursor never set session status "running" on turn open, and never cleared activeTurnId on `turn.completed" — Cursor threads read permanently busy, so the startup stale-sweep could never reclaim them. Now matches the Kimi/Grok lifecycle.
  • Phantom hang / phantom completion (finding 138): a turn-less turn.completed with 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.
  • Finding 151: an unrecognized projection_turns.state value mapped to "running" (the worst guess — nothing will ever settle it); now maps to "interrupted".

Area

  • apps/mac
  • apps/windows
  • apps/mobile
  • apps/server — backend server
  • Shared packages or relay
  • Build, CI, or release tooling
  • Docs

Release size

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

Verification

  • pnpm run verify --all after merging current main: 5/5 steps, exit 0
  • 261 tests across all six touched suites (Claude/Codex/Cursor adapters, CodexSessionRuntime, ProjectionSnapshotQuery, ProviderRuntimeIngestion) pass

🤖 Generated with Claude Code

SergeSerb2 and others added 4 commits August 2, 2026 22:45
…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>
@SergeSerb2 SergeSerb2 added the size:M Normal feature or meaningful behavior change label Aug 3, 2026
@SergeSerb2
SergeSerb2 merged commit 983455a into main Aug 3, 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:M Normal feature or meaningful behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant