diff --git a/README.md b/README.md index ddc13d2..c899fbc 100644 --- a/README.md +++ b/README.md @@ -142,8 +142,20 @@ gnome-extensions enable claude-code-status@git.vakhrushev.me всё ещё позволяет хуку, прочитавшему старое состояние до `Stop`, записать своё устаревшее решение после него. -Сабагенты не показываются. Батч из восьми задач — это одна строка «работает -40 мин», и это правильная строка: пятый воркер из восьми ни о чём не просит. +Сабагенты не показываются, и это не про экономию строк. Их вызовы инструментов +**долетают** до хуков родительской сессии — как `PostToolUse` с полями +`agent_id` и `agent_type` (проверено запуском). Учитывать их нельзя: при фоновых +сабагентах основной агент заканчивает ход первым (`Stop`, то есть `waiting`), а +сабагенты продолжают работать, и их события перебили бы состояние обратно в +`busy`. Панель показывала бы «работает» у сессии, которая на самом деле ждёт +вас, — ровно та подмена, ради предотвращения которой всё и затевалось. + +Поэтому любое событие с `agent_id` игнорируется. Кроме `Notification`: она +означает, что нужен человек, и это одинаково верно, в каком бы агенте ни +заклинило. + +Батч из восьми задач остаётся одной строкой «работает 40 мин», и это правильная +строка: пятый воркер из восьми ни о чём не просит. ## zellij diff --git a/hooks/claude-status-hook.py b/hooks/claude-status-hook.py index e2dfd84..0e45512 100755 --- a/hooks/claude-status-hook.py +++ b/hooks/claude-status-hook.py @@ -83,12 +83,22 @@ def derive_state(event): name = event.get("hook_event_name") if name == "SessionEnd": return "end" + # A notification is honoured whoever raised it: it means a human is needed, + # and that is just as true when the agent that got stuck is a subagent. if name == "Notification": return NOTIFICATION_STATES.get(event.get("notification_type")) - # A subagent finishing its own turn is not the session becoming free; the - # main agent is still working. SubagentStop is a distinct event and is not - # registered, but Stop carries agent_id when raised inside an agent. - if name == "Stop" and event.get("agent_id"): + # Everything else describes work, and work done by a subagent is not the + # main agent's state. Measured: a subagent's tool call does reach the + # parent session's hooks, as PostToolUse carrying agent_id and agent_type. + # + # This matters most for background subagents. There the main agent ends its + # turn first -- Stop, so "waiting" -- and the subagents keep going, so their + # PostToolUse arrives afterwards and would flip the session back to "busy". + # The panel would then read "working" for a session whose input line is free + # and which is waiting for you, which is the exact confusion it exists to + # prevent. Synchronous subagents need no special handling either way: the + # main agent is mid-turn, so its own earlier events already say "busy". + if event.get("agent_id"): return None return EVENT_STATES.get(name) diff --git a/tests/test-hook.sh b/tests/test-hook.sh index ffd5208..47ec511 100755 --- a/tests/test-hook.sh +++ b/tests/test-hook.sh @@ -60,9 +60,24 @@ before=$(field event_ts) emit "$(ev Notification '"notification_type":"idle_prompt","message":"waiting for input"')" check "idle_prompt while waiting does not rewrite" "$before" "$(field event_ts)" +# Subagent activity must not touch the state. The background case is the one +# that bites: the main agent has already stopped, so a subagent's tool call +# arriving afterwards would claim the session is working when it is waiting. emit "$(ev Stop '"agent_id":"sub-1"')" check "subagent Stop ignored" "waiting" "$(field state)" +emit "$(ev PostToolUse '"tool_name":"Bash","agent_id":"sub-1","agent_type":"general-purpose"')" +check "background subagent does not un-wait the session" "waiting" "$(field state)" + +# ...but a subagent that gets stuck still needs a human, so its notification +# must come through. +emit "$(ev Notification '"notification_type":"permission_prompt","message":"x","agent_id":"sub-1"')" +check "subagent permission prompt still blocks" "blocked" "$(field state)" +emit "$(ev PostToolUse '"tool_name":"Bash"')" +check "main agent tool call clears it again" "busy" "$(field state)" +emit "$(ev Stop)" +check "back to waiting" "waiting" "$(field state)" + # Deliberately checked from "busy": compaction raises SessionStart mid-turn, # and taking it at face value would drop a working session back to waiting. emit "$(ev UserPromptSubmit '"prompt":"go"')"