Skip to content

Hooks cannot observe session state (idle / fatal-error / waiting-for-user) — add session-state hook events #6004

Description

@7jrxt42BxFZo4iAnN4CX

Problem

Codewhale exposes 11 hook events (HookEvent, crates/tui/src/hooks/config.rs:27): session_start, session_end, message_submit, tool_call_before, tool_call_after, mode_change, on_error, turn_end, subagent_spawn, subagent_complete, shell_env. They are fine-grained action events, but a hook author cannot observe the high-level session states that matter for out-of-band notification (sound / desktop popup / remote):

  1. Agent is working (busy).
  2. Agent finished and is idle.
  3. Agent stopped on a fatal error (network / provider / auth) — distinct from a transient tool failure where the agent keeps working.
  4. Agent is waiting for user input — an approval prompt is open, or a user_input / question was asked.

Concretely, the gaps:

  • No "waiting for user input" event at all. An approval prompt or a question to the user fires no hook. turn_end/on_error do not cover it.
  • No fatal-vs-transient error distinction. on_error fires for transport/capacity/auth errors and tool failures, with no signal whether the agent stopped or kept going. A hook cannot tell "agent died and is sitting there" from "tool hiccuped, agent continues".
  • turn_end fires after every completed turn, not only when the session becomes idle. It carries status/error in its JSON payload (status is "completed", "failed", or "unknown"), but there is no canonical "the session is now idle / stopped" event.
  • The session state already exists internally but is not exposed to hooks. TurnState { Idle, InProgress, Waiting } (crates/tui/src/tui/control_socket.rs:195) is computed in snapshot_from_app (:440) and published only over the control socket — hooks never see it. Moreover Waiting currently maps only goal_continuation_waiting, not approval/user-input waits.

How other tools do it

opencode emits explicit session-state events (types in the SDK): session.status (busy), session.idle, session.error, plus permission.asked / permission.ask and tool.execute.before when tool === "question". Notification plugins listen to these — e.g. an agent-alert plugin maps session.idle → "finished" sound, session.error → "error" sound with a ~6 s grace period (an error alert fires only if no further activity follows, so transient tool failures are suppressed), and permission.asked/question → "waiting for input" sound. This is the canonical pattern for three distinct "needs attention" signals.

Claude Code has a smaller hook set but includes exactly the two state hooks that matter here: Notification (agent wants attention / needs input) and Stop (agent finished a turn/stopped).

codewhale's TurnState shows the engine already distinguishes busy/idle/waiting internally; it just is not surfaced as hook events.

Proposed solution

Expose session state to the hook system and add the missing triggers, mirroring opencode's model:

  • Add hook events for the three states, e.g. session.busy, session.idle, session.error (or a session_status event carrying turn_state), fired on transitions of the existing TurnState/runtime_turn_status.
  • Add a "waiting for user input" trigger: fire when an approval prompt opens or a user_input/question tool waits for the user. This should also be reflected in TurnState::Waiting (today it covers only goal-continuation waits).
  • For session.error, adopt the opencode grace-period semantic (alert only if the agent actually stopped; suppress when activity resumes) — either engine-side or as documented guidance.
  • Optionally: include turn_state in the existing turn_end payload so current hooks can at least distinguish idle-vs-failed without waiting for new events.

Use case

I run codewhale in a terminal and rely on sound/desktop notifications when the window is not focused:

  • a sound when the agent finishes,
  • a different sound when it stops on a fatal error (network/provider/auth) and just sits there,
  • a different sound when it asks me a question and waits.

Today only the first works (turn_end → idle). The other two are impossible without an external watcher polling the control socket, because the hook events do not exist. In opencode all three work via the plugin events above; in Claude Code via Notification/Stop. codewhale has the internal state (TurnState) but no hook surface for it.

Alternatives considered

  • Poll the control socket (status verb) from a sidecar process and alert on turn_state transitions. Works but is a hack (one watcher per session), and TurnState::Waiting does not cover approval/user-input waits anyway.
  • Combine on_error + tool_call_after + timeouts in a hook to guess "stopped". Fragile and race-prone.
  • A model-callable notify tool (already exists) — not a substitute for state hooks.

Impact

Anyone who wants reliable out-of-band notification (sound/popup/remote/automation) when codewhale needs attention or has stopped silently. This is the prerequisite for notification parity with opencode/Claude Code. The engine already tracks the needed state; the work is exposing it.

Additional context

  • HookEvent (11 events): crates/tui/src/hooks/config.rs:27. Payload/steering contract: docs/HOOKS.md.
  • TurnState { Idle, InProgress, Waiting }: crates/tui/src/tui/control_socket.rs:195; computed in snapshot_from_app (:440-446) from app.is_loading / runtime_turn_status / goal_continuation_waiting.
  • runtime_turn_status values seen in code: "in_progress", "completed", "failed" (tui/ui/apply.rs:180), None; turn_end payload carries status + error.
  • opencode event names: session.status/session.idle/session.error and permission.asked, tool.execute.before (tool === "question") — see SDK types and community notification plugins.
  • Claude Code hooks: Notification, Stop, plus PreToolUse/PostToolUse/UserPromptSubmit/etc.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    • Status
      Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions