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):
- Agent is working (busy).
- Agent finished and is idle.
- Agent stopped on a fatal error (network / provider / auth) — distinct from a transient tool failure where the agent keeps working.
- 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.
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):user_input/ question was asked.Concretely, the gaps:
turn_end/on_errordo not cover it.on_errorfires 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_endfires after every completed turn, not only when the session becomes idle. It carriesstatus/errorin its JSON payload (statusis"completed","failed", or"unknown"), but there is no canonical "the session is now idle / stopped" event.TurnState { Idle, InProgress, Waiting }(crates/tui/src/tui/control_socket.rs:195) is computed insnapshot_from_app(:440) and published only over the control socket — hooks never see it. MoreoverWaitingcurrently maps onlygoal_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, pluspermission.asked/permission.askandtool.execute.beforewhentool === "question". Notification plugins listen to these — e.g. an agent-alert plugin mapssession.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), andpermission.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) andStop(agent finished a turn/stopped).codewhale's
TurnStateshows 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:
session.busy,session.idle,session.error(or asession_statusevent carryingturn_state), fired on transitions of the existingTurnState/runtime_turn_status.user_input/question tool waits for the user. This should also be reflected inTurnState::Waiting(today it covers only goal-continuation waits).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.turn_statein the existingturn_endpayload 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:
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 viaNotification/Stop. codewhale has the internal state (TurnState) but no hook surface for it.Alternatives considered
statusverb) from a sidecar process and alert onturn_statetransitions. Works but is a hack (one watcher per session), andTurnState::Waitingdoes not cover approval/user-input waits anyway.on_error+tool_call_after+ timeouts in a hook to guess "stopped". Fragile and race-prone.notifytool (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 insnapshot_from_app(:440-446) fromapp.is_loading/runtime_turn_status/goal_continuation_waiting.runtime_turn_statusvalues seen in code:"in_progress","completed","failed"(tui/ui/apply.rs:180),None;turn_endpayload carriesstatus+error.session.status/session.idle/session.errorandpermission.asked,tool.execute.before(tool === "question") — see SDK types and community notification plugins.Notification,Stop, plusPreToolUse/PostToolUse/UserPromptSubmit/etc.