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
+33 -18
View File
@@ -162,7 +162,8 @@ gnome-extensions enable claude-code-status@git.vakhrushev.me
| `PostToolUse` | `busy` |
| `PreCompact` | `busy` |
| `Notification` | `blocked` или `waiting`, в зависимости от `notification_type` |
| `Stop` | `waiting` |
| `Stop` | `waiting` — если не работают сабагенты (см. ниже) |
| `SubagentStop` | обновляет число работающих сабагентов; последний освобождает сессию |
| `SessionEnd` | файл удаляется |
`PostToolUse` не избыточен: это единственное событие, срабатывающее после выдачи
@@ -208,31 +209,45 @@ Agent SDK и интеграции с редакторами.
Но `Stop` при этом приходит раньше, чем сабагенты закончат. Если верить ему
буквально, сессия покажется свободной ровно тогда, когда в неё лезть бессмысленно.
Поэтому сабагенты считаются явно:
| Событие | Что делает |
|---|---|
| `PreToolUse` с матчером `^(Agent\|Task)$` | +1 к счётчику |
| `SubagentStop` | −1; последний освобождает сессию, если ход уже закончен |
| `UserPromptSubmit` | сбрасывает счётчик в 0 |
Сколько сабагентов ещё работает, хук не считает, а **берёт из самого события**:
`Stop` и `SubagentStop` несут поле `background_tasks` — список фоновых задач с их
`status`. Оттуда и берётся число: задачи с `type: "subagent"` и `status:
"running"`. Остальные события этого поля не несут, и тогда стоит последнее
известное значение.
Пока счётчик больше нуля, состояние `waiting` невозможно: и `Stop`, и напоминание
`idle_prompt` дают `busy`. Освобождает сессию только уход последнего сабагента —
и лишь если основной агент к тому времени остановился.
Пока число больше нуля, состояние `waiting` невозможно: и `Stop`, и напоминание
`idle_prompt` дают `busy`. Освобождает сессию только `SubagentStop`, пришедший в
момент, когда список пуст, — и лишь если основной агент к тому времени
остановился.
Матчер якорный не для красоты: это регулярка, и голое `Task` поймало бы
`TaskCreate`, `TaskUpdate` и прочее. Хук проверяет имя инструмента ещё раз, сам.
Сброс на `UserPromptSubmit` ограничивает ущерб, если сабагент умрёт, не прислав
`SubagentStop`: счётчик не переживёт следующего вашего сообщения.
Фоновая команда (`type: "bash"`) сабагентом не считается намеренно: поднятый
dev-сервер живёт часами и ничего не говорит о том, нужны вы сессии или нет, — а
приравняв его к работе, чип пришлось бы держать «работает» всё это время.
### Почему не счётчик
Сначала счётчик и был: `+1` на `PreToolUse` с матчером `^(Agent|Task)$`, `1` на
`SubagentStop`. Он ошибался в худшую сторону — гасил `busy` у занятой сессии, —
и вот почему. Запуск сабагента виден хуку только на верхнем уровне: когда свой
сабагент разворачивает сабагент, событие приходит с `agent_id` и отбрасывается.
А `SubagentStop` приходит **за каждого сабагента на любой глубине**. Каждый
вложенный вычитал единицу из батча, в который никогда не входил.
Замерено на живой сессии: один фоновый агент, четырнадцать вложенных `SubagentStop`
подряд — счётчик обнулился на первом же, и сессия с работающим батчем показалась
ждущей. Снимок вычитать нечего: он просто говорит, что запущено сейчас, и по той
же причине не течёт, если сабагент умрёт, не прислав `SubagentStop`.
Собственные вызовы инструментов сабагентов игнорируются — они долетают до хуков
родителя как `PostToolUse` с `agent_id`, но счётчик уже всё сказал, а они добавили
бы только записи на диск. `Notification` — исключение: она означает, что нужен
человек, и это одинаково верно, в каком бы агенте ни заклинило.
родителя как `PostToolUse` с `agent_id`, но сессия занята и без них, а записей на
диск от одного сабагента набежали бы сотни. `Notification` — исключение: она
означает, что нужен человек, и это одинаково верно, в каком бы агенте ни заклинило.
В меню число сабагентов показывается строкой «работает · 3 subagents». В панели —
нет: батч из восьми задач остаётся одной строкой «работает 40 мин», и это
правильная строка.
правильная строка. Считаются фоновые: батч, которого основной агент дожидается
сам, и так виден по состоянию `busy`.
## zellij