Ask the session what is running instead of counting it

A session whose main agent had stopped while a background subagent worked
showed as waiting. The count was kept by hand -- +1 on PreToolUse matched
to ^(Agent|Task)$, -1 on SubagentStop -- and the two halves do not see the
same thing. A launch reaches the hook only at the top level: a subagent
spawning its own subagents does it through events carrying agent_id, which
are dropped. SubagentStop arrives for every subagent at every depth. Each
nested one subtracted from a batch it had never joined.

Measured on the live session that showed it: one background agent, then
fourteen nested stops, the first of which took the count to zero and turned
the chip white with the batch still running. Replaying those recorded
events through the old hook reproduces it exactly, and through the new one
holds busy throughout, rising to two while two background agents ran.

The events carry the answer themselves. Stop and SubagentStop -- the two
that can end a turn, and the only two where it matters -- come with
background_tasks: every running task with its type and status. The count is
now read from there and nothing accumulates, so it cannot drift, and a
subagent that dies without sending SubagentStop no longer leaks a count
that pins the chip at busy. Events without the field leave the stored value
alone, which is what keeps an idle_prompt nudge from freeing a working
session.

A background shell is deliberately not counted. A dev server left running
says nothing about whether the session needs you, and treating it as work
would hold the chip at "working" for as long as it lives.

PreToolUse is no longer registered: counting was the only thing it was for.
It stays listed as a legacy event so that both install and uninstall sweep
it out of settings.json rather than leaving it there to spawn the hook on
every agent launch for nothing.
This commit is contained in:
av
2026-08-23 09:19:30 +03:00
parent f8e85d5703
commit d48a18ae75
4 changed files with 130 additions and 73 deletions
+36 -18
View File
@@ -149,39 +149,57 @@ check "compaction does not reset the clock" "$since_before" "$(field since)"
# The scenario this exists for: you ask for a batch, the main agent launches it
# and ends its turn, and the session waits for the results to consolidate them.
# It is working, not waiting for you, and must not call you over.
#
# What is running is not counted from starts and stops but taken from the list
# the events carry, exactly as measured on a live session: Stop and
# SubagentStop bring "background_tasks", the rest of the events bring nothing.
bg() { # running-subagent-count -> the snapshot as those events carry it
tasks=""
for i in $(seq 0 $(($1 - 1))); do
tasks="$tasks${tasks:+,}{\"id\":\"sub-$i\",\"type\":\"subagent\",\"status\":\"running\"}"
done
echo "\"background_tasks\":[$tasks]"
}
emit "$(ev UserPromptSubmit '"prompt":"launch a batch"')"
check "a new turn resets the count" "0" "$(field agents)"
emit "$(ev PreToolUse '"tool_name":"Agent"')"
emit "$(ev PreToolUse '"tool_name":"Agent"')"
check "two launches counted" "2" "$(field agents)"
emit "$(ev PreToolUse '"tool_name":"TaskCreate"')"
check "a tool that merely starts with Task is not a subagent" "2" "$(field agents)"
emit "$(ev Stop)"
emit "$(ev Stop "$(bg 2)")"
check "main agent stopping does not free a running batch" "busy" "$(field state)"
check "the snapshot is what gets stored" "2" "$(field agents)"
emit "$(ev SubagentStop '"agent_id":"sub-1","agent_type":"general-purpose"')"
emit "$(ev SubagentStop "\"agent_id\":\"sub-0\",\"agent_type\":\"general-purpose\",$(bg 1)")"
check "one down, still working" "busy" "$(field state)"
check "count decremented" "1" "$(field agents)"
check "the count follows the snapshot" "1" "$(field agents)"
emit "$(ev SubagentStop '"agent_id":"sub-2","agent_type":"general-purpose"')"
# The regression this replaced counting for: a subagent's own subagent stops,
# and its SubagentStop reaches this session just like a top-level one -- while
# its *launch* never did, because that event carried agent_id and was dropped.
# Subtracting it freed a session whose batch was still running.
emit "$(ev SubagentStop "\"agent_id\":\"nested-1\",\"agent_type\":\"\",$(bg 1)")"
check "a nested subagent stopping does not free the batch" "busy" "$(field state)"
check "and does not touch the count" "1" "$(field agents)"
emit "$(ev SubagentStop "\"agent_id\":\"sub-1\",$(bg 0)")"
check "last subagent finishing frees the session" "waiting" "$(field state)"
check "count back to zero" "0" "$(field agents)"
# A background shell is a background task too, and must not be mistaken for
# work: a dev server left running would pin the chip at "working" for good.
emit "$(ev UserPromptSubmit '"prompt":"serve"')"
emit "$(ev Stop '"background_tasks":[{"id":"b1","type":"bash","status":"running"}]')"
check "a background shell is not a subagent" "waiting" "$(field state)"
# A batch that finishes while the main agent is still mid-turn must not free it.
emit "$(ev UserPromptSubmit '"prompt":"again"')"
emit "$(ev PreToolUse '"tool_name":"Agent"')"
emit "$(ev SubagentStop '"agent_id":"sub-3"')"
emit "$(ev SubagentStop "\"agent_id\":\"sub-3\",$(bg 0)")"
check "batch done mid-turn leaves the session working" "busy" "$(field state)"
# An idle nudge while a batch runs must not claim the session is free either.
emit "$(ev PreToolUse '"tool_name":"Agent"')"
emit "$(ev Stop)"
# It carries no snapshot of its own, so this also checks the stored one stands.
emit "$(ev Stop "$(bg 1)")"
emit "$(ev Notification '"notification_type":"idle_prompt","message":"still there?"')"
check "idle nudge cannot free a running batch" "busy" "$(field state)"
emit "$(ev SubagentStop '"agent_id":"sub-4"')"
check "an event without a snapshot leaves the count alone" "1" "$(field agents)"
emit "$(ev SubagentStop "\"agent_id\":\"sub-4\",$(bg 0)")"
check "and it frees properly once the batch ends" "waiting" "$(field state)"
# --- concurrency -----------------------------------------------------------