Ignore subagent work, not subagent notifications
A subagent's tool calls do reach the parent session's hooks: measured, a PostToolUse arrives carrying agent_id and agent_type. Only Stop was guarded against that, and Stop was the case that mattered least. With background subagents the ordering is the harmful one. The main agent ends its turn first, so Stop lands and the session reads "waiting"; the subagents keep working, and their PostToolUse arrives afterwards and puts the session back to "busy". The panel then says a session is working when its input line is free and it is waiting for you -- the precise confusion this indicator exists to prevent, and reported from a live session doing exactly that. Every event carrying agent_id is now ignored. Synchronous subagents lose nothing: the main agent is mid-turn, so its own earlier events already say "busy". Notification is deliberately exempt. It means a human is needed, and that is as true when the agent that got stuck is a subagent -- ignoring it would leave a session silently blocked. Covered both ways in the hook tests, and checked once against a real subagent event captured from a live run rather than a hand-written one.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"')"
|
||||
|
||||
Reference in New Issue
Block a user