Skip to content

fix(status): cover Claude failure, idle and session-switch hooks; add stuck-state fallbacks - #310

Merged
maxthomas95 merged 2 commits into
mainfrom
fix/status-hook-coverage-20261109
Nov 10, 2026
Merged

maxthomas95 merged 2 commits into
mainfrom
fix/status-hook-coverage-20261109

Conversation

@maxthomas95

Copy link
Copy Markdown
Owner

Summary

Once a hook has been seen, a hook-driven session ignores output cadence entirely, so any turn ending Claude does not report left the session "running" until something else corrected it. This PR closes the endings hooks can report and adds two bounded fallbacks for the ones they cannot. Part 1 of 2 for the status-detection findings in the 2026-11-09 review.

New Claude hooks observed

  • StopFailure — Claude runs it instead of Stop when a turn ends on an API error (rate limit, overload, auth). It completes the turn like Stop. Only the error type is forwarded, never error_details or the message text.
  • SessionEnd / SessionStart, installed with a clear|resume matcher, so the helper is not spawned on ordinary startup, compaction or exit.
  • elicitation_url_dialog notifications, reported as an elicitation dialog.

Following /clear and in-pane /resume

  • When the pane's own conversation ends with reason clear or resume, a session start with a different session id and the matching source is adopted if it arrives within 30 s.
  • Adoption moves the native session id, usage tracking (local tracker, or the remote usage reader), subagent rows, and the id used by restart and workspace restore. It also clears the old conversation's subagent and permission state.
  • Without that pending end, a mismatched session id is dropped exactly as before. That is what keeps a nested claude -p or claude --resume (which inherits the pane's Tether env) from taking over the pane. Adopted ids must be UUID-shaped.

Ghost subagents

  • A parent idle_prompt is now authoritative. Claude only sends it once no background agent is running, so it clears subagents whose SubagentStop never arrived instead of deferring to them forever.

Fallbacks for a stuck "running" (Claude only; Codex is unchanged)

  • Silence watchdog: a hook-confirmed running turn with no PTY output and no hook event for 2 minutes drops to cadence mode and shows waiting. It does not fire while hooks report a running subagent.
  • Interrupt hint: a lone Esc or Ctrl+C typed into a running hook-driven pane drops it to cadence mode until the next hook, because Claude sends no hook on interrupt. Escape sequences (arrows, focus reports, bracketed paste) do not count.
  • A session in either fallback rests at waiting instead of decaying to idle, so the hook that later confirms the pause does not notify a second time.

Overlay

  • Install and scrub cover the new event keys, locally and on SSH hosts (same code path).
  • Scrub now also removes Tether-managed entries from event keys this version does not install, and leaves keys without one untouched.

Boundaries

  • Nothing reads or alters PTY output. The interrupt hint looks at a single input byte and never changes or delays it.
  • The helper prints nothing for the new events (a SessionStart hook's stdout is added to Claude's context); the helper tests assert this.
  • No IPC, schema, dependency or lockfile changes.

Behaviour change to be aware of

  • A confirmed Claude turn no longer stays "running" through unlimited total silence; the test that asserted that now covers Codex, and a new one covers a 30-minute Claude turn that keeps redrawing.

In-app docs updated: src/docs/sessions.md (session states, resume), src/docs/settings.md (CLI hooks).

Test plan

  • npx tsc --noEmit
  • npx eslint on the touched source and test files
  • npx vitest run — 2334 passed, 1 failed: src/main/usage/remote-command.test.ts (real POSIX shell), which fails the same way on the untouched base on this Windows host
  • Rate-limited or otherwise API-failed turn: the session leaves "running" as soon as Claude shows the error
  • Press Esc mid-turn: the session shows waiting a few seconds after output stops, and one notification fires, not two
  • Press Esc while Claude keeps working (for example to close a menu): the session stays running
  • /clear in a hooked pane, then send a prompt: status follows the turn, usage shows the new conversation, and restart reopens it
  • /resume to another conversation in a hooked pane: same checks; then quit and relaunch to confirm restore reopens the conversation you switched to
  • Run a claude -p from a pane's Bash tool: the pane's status and id are unaffected
  • Interrupt a running subagent: the parent leaves "running", and the child row stops showing running within about a minute
  • Long foreground tool run (several minutes, spinner visible): the session stays running throughout
  • SSH environment with remote hooks: /clear moves remote usage collection to the new conversation

Out of scope

  • PermissionRequest (instant permission state) and an input-side "permission resolved" hint
  • agent_needs_input and the quota_auto_resume_* notification types
  • Helper timeout changes
  • A status inspector
  • /branch and other fork session starts: no SessionEnd reason identifies them, so the pane is not followed there

🤖 Generated with Claude Code

maxthomas95 and others added 2 commits November 9, 2026 20:47
… stuck-state fallbacks

Once a hook has been seen, a hook-driven session ignores output cadence
entirely, so every turn ending Claude does not report left the session
"running" until something else corrected it. This closes the endings the
hooks can report and adds two bounded fallbacks for the ones they cannot.

Hooks now observed for Claude:
- StopFailure, which Claude runs instead of Stop when a turn ends on an API
  error (rate limit, overload, auth). It completes the turn like Stop; only
  the error type is forwarded.
- SessionEnd and SessionStart, installed with a `clear|resume` matcher so the
  helper never runs on ordinary startup, compaction or exit. When the pane's
  own conversation ends by /clear or an in-pane /resume, the session start
  that follows within 30 s with the matching source is adopted: the native
  session id, usage tracking (local and the remote reader), subagent rows and
  the id used by restart and workspace restore all move to the new
  conversation. Without that pending end, a mismatched session id is still
  dropped, which is what keeps a nested `claude -p` from driving the pane.
- The browser-URL elicitation notification, reported as an elicitation dialog.

A parent idle_prompt is now authoritative: Claude only sends it once no
background agent is running, so it clears subagents whose SubagentStop never
arrived instead of deferring to them forever.

Fallbacks, Claude only (Codex behaviour is unchanged):
- Silence watchdog: a hook-confirmed running turn with no PTY output and no
  hook event for two minutes drops to cadence mode and shows waiting. Claude's
  TUI redraws while it works, so that much silence means the end signal was
  lost. It does not fire while hooks report a running subagent, because a
  background agent can work behind a still prompt.
- Interrupt hint: a lone Esc or Ctrl+C typed into a running hook-driven pane
  drops it to cadence mode until the next hook. Claude sends no hook on
  interrupt. Only the single key byte counts; escape sequences do not.
Sessions in either fallback rest at waiting rather than decaying to idle, so
the hook that later confirms the pause does not notify a second time.

The overlay scrub now also removes Tether-managed entries from event keys
this version does not install, and leaves keys without one untouched.

Nothing here reads or alters PTY output. The helper stays silent on stdout
for every new event (SessionStart stdout is injected into Claude's context),
which the helper tests assert.

This reverses one previously tested behaviour: a confirmed Claude turn no
longer stays running through unlimited total silence. The Codex version of
that test is kept.

Tests: tsc --noEmit clean; eslint clean on touched files; vitest 2334 passed,
1 failed (src/main/usage/remote-command.test.ts real-POSIX-shell test, which
fails on this Windows host independently of this change).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@maxthomas95
maxthomas95 merged commit 08fd8d0 into main Nov 10, 2026
11 checks passed
@maxthomas95
maxthomas95 deleted the fix/status-hook-coverage-20261109 branch November 10, 2026 03:03
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