fix(sessions): an interrupted turn stops reading "working" (v0.325.0) - #594
Merged
Conversation
Claude Code fires NOTHING on a user interrupt (Esc / Ctrl-C): no Stop, no StopFailure, no SessionEnd. A UserInterrupt event has been requested (anthropics/claude-code#9516) and does not exist. So the turn never ended server-side and the session spun until the 2h wedged-turn ceiling. The signal we DO get is the Notification hook: the TUI parks at its prompt — including "Interrupted · What should Claude do instead?" — and claude raises idle_prompt (159 of them on the live instapods box). notify() now ends the turn as well as ringing the bell, for all three human-blocked kinds (idle_prompt, permission_prompt, agent_needs_input): being blocked on a human is by definition not generating. The session then reads `needs you`, which is the honest state — claude is literally asking what to do instead. The other half of the loop: markTurnBusy (a submitted prompt) retires the open waiting card. Without it an interrupted session would keep reading `needs you` — which outranks `working` — through the entire next turn. An interrupted UNATTENDED run is deliberately left alive rather than reaped: a human stopped it on purpose and now owns it. 8 new assertions in scripts/turn-lifecycle-test.cjs (40 total); the interrupt gap is documented in docs/session-lifecycle-hooks.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ZUfffxY4hKv7M6wMCcaTz
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.
Follow-up to #593. Asked: what happens if I hit Escape and interrupt the session?
Answer, before this PR: it kept spinning for up to 2 hours.
The gap
Claude Code fires nothing on a user interrupt — no
Stop, noStopFailure, noSessionEnd. AUserInterruptevent has been requested (anthropics/claude-code#9516) and does not exist. Sobusy_sincewas never cleared, and the only thing that eventually saved it was the 2h wedged-turn ceiling added in #593.The fix — use the signal we already receive
The TUI parks at its prompt after an interrupt (
Interrupted · What should Claude do instead?) and claude raises aNotificationwithnotification_type: idle_prompt. That hook has been wired for ages — 159idle_promptevents on the live instapods box — but we only used it to ring the bell.notify()now ends the turn as well, for all three human-blocked kinds (idle_prompt,permission_prompt,agent_needs_input). Being blocked on a human is by definition not generating.needs you, which is the honest state — claude is literally asking what to do instead.markTurnBusy(a submitted prompt) retires the open waiting card. Without it an interrupted session would keep readingneeds you— which outranksworking— through the entire next turn.So the cycle is:
working→ Esc →needs you(+ bell) → you type →working.An interrupted unattended run is deliberately left alive rather than reaped: a human stopped it on purpose and now owns it.
Verification
8 new assertions in
scripts/turn-lifecycle-test.cjs(40 total) covering the interrupt path, the card-retire loop, permission prompts, and that a noisenotification_typechanges nothing. Fulltest:governancegreen.The gap and the workaround are written up in
docs/session-lifecycle-hooks.md, so the next person doesn't have to rediscover that there's no interrupt hook.Deploy note: server restart required. No new hooks — this reuses the
Notificationhook already in every launched session, so it works on currently-running sessions too, not just newly launched ones.🤖 Generated with Claude Code
https://claude.ai/code/session_015ZUfffxY4hKv7M6wMCcaTz