Skip to content

fix(sessions): an interrupted turn stops reading "working" (v0.325.0) - #594

Merged
vikasprogrammer merged 1 commit into
mainfrom
feat/interrupt-idle
Aug 8, 2026
Merged

fix(sessions): an interrupted turn stops reading "working" (v0.325.0)#594
vikasprogrammer merged 1 commit into
mainfrom
feat/interrupt-idle

Conversation

@vikasprogrammer

Copy link
Copy Markdown
Owner

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, no StopFailure, no SessionEnd. A UserInterrupt event has been requested (anthropics/claude-code#9516) and does not exist. So busy_since was 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 a Notification with notification_type: idle_prompt. That hook has been wired for ages — 159 idle_prompt events 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.
  • 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.

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 noise notification_type changes nothing. Full test:governance green.

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 Notification hook 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

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
@vikasprogrammer
vikasprogrammer merged commit 02a1def into main Aug 8, 2026
1 check passed
@vikasprogrammer
vikasprogrammer deleted the feat/interrupt-idle branch August 8, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant