Skip to content

fix(server): let Claude abort a stopped v2 turn before closing the CLI - #1008

Merged
rynfar merged 2 commits into
pylonfrom
fix/claude-v2-interrupt-grace
Oct 4, 2026
Merged

rynfar merged 2 commits into
pylonfrom
fix/claude-v2-interrupt-grace

Conversation

@rynfar

@rynfar rynfar commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

Regression

#916 (93ba1cf) made Claude's Stop call query.interrupt() and then wait up to 3s for the turn to finish before closing the process. That gave Claude time to write the prompt to its transcript. The orchestrator v2 adoption (eed7e97) dropped this. ClaudeAdapterV2.interruptTurn now did yield* query.interrupt; query.close with no wait.

User impact

If you stopped a thread's first turn before Claude saved it, the thread kept a resume cursor for a session Claude never stored. The next turn always resumes (providerTurnOrdinal > 1 forces resume in openQuery), so every later message failed with "No conversation found" and the thread couldn't be used again. Separately, if query.interrupt failed, close was never called and the CLI process stayed alive.

Fix

  • ActiveClaudeTurnContext now has a settled Deferred, which finalizeActiveTurn resolves.
  • After the interrupt is acknowledged, interruptTurn waits for settled for at most CLAUDE_INTERRUPT_GRACE (Duration.seconds(3)), then closes. A hung interrupt also counts against the same grace.
  • If the interrupt fails or defects (full cause caught), a warning is logged (orchestration-v2.claude-query-interrupt-failed) and the process is still closed. The existing 10s close timeout and fallback finalization are unchanged.
  • A failed interrupt no longer surfaces as an error, so the effect worker no longer retries the interrupt effect. Closing the process is the intended outcome.

Tests

New tests in ClaudeAdapterV2.test.ts. Each one fails without the fix:

  • interruptTurn lets Claude abort the turn before closing the process: Stop does not close while the turn is unsettled, even though the TestClock is frozen. It closes once Claude's aborted result settles the turn, and the terminal is interrupted.
  • interruptTurn closes the process when Claude never aborts the turn: a hung interrupt closes after TestClock.adjust(CLAUDE_INTERRUPT_GRACE).
  • interruptTurn still closes the process when Claude's interrupt fails / ... defects (the defect case fails if the cause is only caught as a typed error).
  • The existing "drops zero-turn task-notification debris racing interrupt" test now advances the grace, because the debris does not settle the turn.

Replay: turn_interrupt_restart/claudeAgent (first-turn Stop, then a new message that resumes) still passes through OrchestratorV2. That fixture was recorded with a tool call already persisted, so it doesn't reproduce the bug itself. A fixture that reproduces "No conversation found" would need a new live recording.

Validation: the focused suites ClaudeAdapterV2.test.ts, ClaudeReplayFixtures, OrchestratorReplayFixtures and OrchestratorReplayRecovery pass (261 tests). vp run -F t3 typecheck passes, and scoped fmt/lint report nothing new.

Follow-up (not in this PR)

Recovery for threads that are already broken is not included. That would mean: when a resume fails with "No conversation found" after an interrupted first turn, start a fresh native session instead. It needs a session-existence check or error classification in the query runner, plus re-offering the prompt mid-turn, which is too much for this fix. Threads broken before this change stay broken until that lands.

#1005 also edits ClaudeAdapterV2.ts and the wake harness in the test file. Whichever merges second needs a rebase. The harness interrupt option now takes sdkMessages.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:S labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 4.9 KiB 4.9 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.7 KiB 3.7 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 20.4 KiB 20.4 KiB 0 B (0.0%) 29.3 KiB ✅
Codex Live turn messages 2 2 0 (0.0%) 8 ✅
Claude Total thread wire 4.9 KiB 4.9 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.7 KiB 3.7 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 20.8 KiB 20.8 KiB 0 B (0.0%) 29.3 KiB ✅
Claude Live turn messages 2 2 0 (0.0%) 8 ✅

Baseline: 8a831f3 · PR result: 9edd807 · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 106.1 KiB
  • Claude decoded thread snapshot: 106.4 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

rynfar and others added 2 commits October 3, 2026 18:21
Re-port the #916 interrupt grace lost in the orchestrator v2 adoption.
ClaudeAdapterV2.interruptTurn now waits up to CLAUDE_INTERRUPT_GRACE
(3s) after query.interrupt for the active turn to settle, so Claude
records the prompt in its transcript before Stop closes the process.
A failed or hung interrupt no longer skips the close.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Catch the full cause around query.interrupt so a defect, not only a
typed failure, still reaches close.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rynfar
rynfar force-pushed the fix/claude-v2-interrupt-grace branch from 8c7b9c1 to 9edd807 Compare October 4, 2026 00:26
@rynfar
rynfar merged commit a5ba549 into pylon Oct 4, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant