Главная / Статьи / Почему агентам Claude Code нельзя доверять отчёты о собственном статусе

Почему агентам Claude Code нельзя доверять отчёты о собственном статусе

Stop-хук, журнал событий и бюджеты в долларах, чтобы управлять сессиями Claude Code как оргструктурой, плюс четыре сюрприза харнесса, которые пропустили 74 зелёных теста.

2106 слов

Запустите больше одной сессии Claude Code одновременно, и рано или поздно вы попробуете очевидное: поручить одной сессии управлять остальными. Это работает минут пять. После нескольких проектов с одним и тем же провалом стало ясно, что полезнее перестать считать это проблемой промптов и взглянуть на неё как на проблему архитектуры. Ниже — что сломалось, что в итоге построили и шесть особенностей харнесса, которые не поймал бы ни один юнит-тест; четыре из них проявились только при запуске настоящей сессии.

Не один провал, а три

Симптом, который замечают все, — тишина: подчинённый так и не отчитывается, даже если ему велели сообщить о завершении. Но это самая безобидная из трёх отдельных проблем.

Подчинённые не отчитываются. Инструкция живёт в контексте модели, а контекст сжимается, заканчивается или просто проигрывает самой задаче. К концу работы напоминание об отчёте вытеснено четырьмя сотнями строк вывода тестов. Дело не в дисциплине. Это проблема иерархии памяти, и вежливые формулировки её не решат.

Руководители ничего не видят. Эту проблему легко упустить, она вытекает из того, как устроен Claude Code. Сессия выполняется только пока идёт ход пользователя, а между ходами не делает вообще ничего: не опрашивает, не ждёт, не думает. Поэтому подчинённый, который всё-таки отправил отчёт, не достучится ни до кого. Сообщение лежит в сессии, которая спит, пока человек что-нибудь не напечатает.

Сбои никто не замечает. Представьте две сессии, которые правят один файл и молча затирают друг друга, сессию, у которой посреди задачи закончился контекст и она умерла, или сессию, застрявшую в цикле. Снаружи каждая из них выглядит просто как медленная.

Третья проблема самая дорогая, и наивное решение — попросить сессии отчитываться — её вообще не затрагивает.

Отчёт должен происходить с моделью, а не по её воле

Всё изменил один принцип: отчёт не может зависеть от того, решит ли модель его отправить. Он должен запускаться вокруг модели, а не ею самой.

Если отчёт держится на том, что модель решит его подать, он надёжен ровно настолько, насколько у модели остался контекст. Заберите его из рук модели, и он станет таким же надёжным, как жизненный цикл процесса.

В Claude Code для этого есть подходящий механизм. Хук Stop срабатывает каждый раз, когда сессия завершает ход, и вызывает его харнесс, а не модель. Модель могла забыть, могла остаться без контекста, могла даже упасть — хук всё равно выполнится:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node",
            "args": ["${CLAUDE_PLUGIN_ROOT}/hooks/stop-report.mjs"],
            "timeout": 15
          }
        ]
      }
    ]
  }
}

Это и есть единственная несущая идея. Всё остальное — леса вокруг неё.

Устройство

Получившийся инструмент запускает сессии Claude Code как организацию. Executive общается с вами, Managers отвечают за направления, Staff выполняют работу. Роль определяется положением, а не ярлыком: сотрудник Staff, который набрал собственную команду, становится Manager для этой ветки.

                          you
                           ▲
            results ───────┼─────── objective
                           ▼
                 ┌───────────────────┐
                 │     Executive     │
                 └─────────┬─────────┘
             ┌─────────────┴─────────────┐
             ▼                           ▼
       ┌───────────┐               ┌───────────┐
       │  Manager  │               │  Manager  │
       └─────┬─────┘               └─────┬─────┘
        ┌────┴────┐                 ┌────┴────┐
        ▼         ▼                 ▼         ▼
     ┌─────┐   ┌─────┐           ┌─────┐   ┌─────┐
     │Staff│   │Staff│           │Staff│   │Staff│
     └──┬──┘   └──┬──┘           └──┬──┘   └──┬──┘
        └─ peers ─┘                 └─ peers ─┘

Каждый участник подчиняется ровно одному руководителю, а эскалация идёт по одному уровню за раз. Коллеги одного уровня сами договариваются о пересекающейся работе и зовут Manager, только если не могут договориться.

Три решения стоит объяснить, потому что каждое могло быть другим.

Состояние — это журнал событий только на дозапись. До двенадцати процессов ОС пишут в одну директорию одновременно. Блокировок нет: каждое изменение состояния — одна дописанная строка, а ОС гарантирует атомарность такой дозаписи. Хранимой оргструктуры тоже нет, её вычисляют свёрткой журнала при каждом чтении. Убийство сессии ничего не может испортить. В худшем случае останется устаревшая запись working, а её отлов — задача детектора зависаний.

Руководители сами теребят подчинённых. Каждый руководитель на каждом ходу сортирует прямых подчинённых и поднимается по фиксированной лестнице: напомнить, прочитать транскрипт, переназначить. Средняя ступень важнее, чем кажется. Долгий прогон тестов и бесконечный цикл снаружи выглядят одинаково, и различить их можно, только прочитав реальный вывод. Это решение для модели, а не для таймаута.

Бюджеты выстроены цепочкой и считаются в долларах. Коллега возразил, что на подписке доллары кажутся абстракцией, а ощущаются именно токены. Доллары всё равно победили, по двум причинам. --max-budget-usd — единственный лимит, который харнесс действительно соблюдает, а потолок по токенам — просто число в JSON-файле, которое никто не обязан уважать. К тому же токены несравнимы между уровнями моделей: миллион токенов на самой мощной модели может стоить примерно в десять раз дороже, чем на самой дешёвой. Бюджет в токенах не может ограничить иерархию, смысл которой как раз в смешении уровней: менеджер легко уложится в него, умножив счёт на десять. Токены пишутся в лог как показатель. Ограничение — доллары.

Последовательность для одного задания от начала до конца включает альтернативную ветку посередине, и именно она несущая: шаг 5 существует для того, чтобы системе никогда не пришлось полагаться на шаг 4.

Состояние, которое не видят часы

Обнаружение зависаний выглядит как обычная задача на таймауты, и в основном так и есть. Но одно состояние одновременно самое срочное в системе и невидимое для всего, что измеряет тишину.

Возьмём участника, который сообщает blocked. Он только что вышел на связь, поэтому время с последнего контакта равно нулю, и любая проверка простоя считает его здоровым. На деле он перестал работать и ждёт решения, которое может дать только его руководитель. Ни одна проверка его не отметит: он ведь вежливо объявил, что застрял.

Смоук-тест вскрыл это рано, ещё до более сложных багов. Подчинённый отправил blocked: beyond_capability, а в статусе у руководителя он значился как OK. Когда это заметишь, исправление небольшое. Но чтобы заметить, понадобился живой запуск.

А потом всё запустили по-настоящему

К этому моменту было 74 юнит-теста, все зелёные, CI проходил на Linux и Windows на двух версиях Node. Дизайн держался и на бумаге, и в тестах, уверенность была высокой.

Затем одной настоящей фоновой сессии дали пустяковое задание: создать файл со словом «hello». Она провалилась четыре раза подряд по четырём не связанным причинам. Ни одна не была ошибкой в коде. Каждый раз харнесс вёл себя не так, как предполагалось, и каждый раз молча.

1. В фоне ID сессии игнорируются

Идентичность строилась вокруг изящной идеи: родитель заранее назначает ребёнку ID сессии, так что оргструктура записана ещё до появления ребёнка и нет ни мгновения, когда подчинённый работает незарегистрированным.

На практике claude --bg --session-id <uuid> игнорирует переданный UUID, и фоновая сессия придумывает свой. Результат получился почти символичным. Stop-хук искал в оргструктуре ID сессии от харнесса, никого не нашёл и вышел, ничего не записав, — то есть страховка, созданная, чтобы гарантировать отчёт, не сделала ничего.

Решение — передавать выбранный родителем ID ребёнку через переменную окружения и считать его главным, а ID харнесса использовать, только если переменной нет.

2. На запрос разрешения некому ответить

Подчинённый идеально выполнил инструкцию. Прочитал задание, понял протокол и запустил первую команду — проверку входящих — ровно как было сказано.

А потом навсегда застыл на запросе разрешения. Режим acceptEdits одобряет правки файлов, но оставляет shell-команды неодобренными, а к фоновой сессии не подключён человек, который нажмёт «да». Руководитель не может отличить это от зависания, хотя подчинённый всё делает правильно и просто ждёт одобрения, которое не придёт.

Исправление — заранее одобрить CLI при запуске через --allowedTools. Указывайте оба shell-инструмента, потому что выбор модели зависит от платформы: PowerShell на Windows, Bash в остальных случаях.

3. --allowedTools принимает переменное число аргументов и проглатывает промпт

С добавленными разрешениями следующая сессия стартовала и сразу сообщила, что простаивает и ждёт промпт.

--allowedTools <tools...> принимает переменное число аргументов, поэтому продолжает забирать их и после булева флага. Такая команда:

claude --allowedTools "Bash(node:*)" --bg "<the actual prompt>"

затягивает промпт в список инструментов и запускает сессию без инструкций, которая терпеливо ждёт ввода, который никогда не придёт. Опция аккуратно останавливается на флаге, принимающем собственное значение, поэтому такая форма работает:

claude --allowedTools "Bash(node:*)" --model haiku --bg "<the actual prompt>"

Теперь правила разрешений выводятся первыми, прямо перед --session-id, чтобы корректность не зависела от порядка аргументов в остальной части команды.

4. Перед правками нужен worktree

С четвёртой попытки сессия объявила, что перед правкой ей нужно войти в worktree. Она попыталась его создать, не смогла, потому что в черновом репозитории не было коммитов (базовую ветку «HEAD» не удалось разрешить), а затем попробовала git commit, которому нужно было одобрение, которое некому дать.

Отчасти тут виновата тестовая среда. Но сам факт полезно знать: фоновые сессии тяготеют к изоляции через worktree при записи, и эта изоляция ближе к поведению по умолчанию, чем к опции.

Ещё два, найденных прощупыванием

--append-system-prompt-file существует, но не задокументирован. В claude --help его нет, однако при проверке он выдаёт ошибку «не хватает аргумента», а не «неизвестная опция». Это важно, потому что описание роли занимает около тридцати строк с кавычками и переводами строк, а передача его прямо в команде на Windows ломается трижды: cmd.exe не умеет переносить переводы строк в аргументе, экранирование кавычек зависит от оболочки, а длина командной строки ограничена примерно 8191 символом. Запись описания в файл и передача пути обходят все три проблемы.

--plugin-dir не доходит до порождённых сессий. Плагин, запущенный из клона, действует только на эту сессию, поэтому порождённые ею сессии стартуют без хуков и без навыков ролей и игнорируют протокол, которого им никто не дал. У установленного плагина такой проблемы нет, так что рекомендуемый путь — нормальная установка, а для случая с клоном в конфиге есть явная опция проброса.

Зелёных тестов всё равно не хватило

Ещё один случай показывает, почему одних живых тестов тоже недостаточно. CI поймал дефект, который локальные прогоны так и не показали. События в журнале упорядочивались по метке времени в миллисекундах, а ничьи разрешались случайным ID события. Быстрые машины часто пишут пачку событий в пределах одной миллисекунды, поэтому «напомнить, напомнить, отчёт» могло свернуться как «отчёт, напомнить, напомнить», и участник, который только что отчитался, всё ещё считался бы не получившим напоминания. Проекция была недетерминированной и могла читаться по-разному при каждой загрузке.

Локально на Windows все 74 теста проходили. На Linux CI упал дважды.

Решением были не более точные метки времени. Раз дозаписи атомарны, порядок строк в файле уже отражает порядок записей во всех процессах. Если разрешать ничьи по позиции строки, порядок становится и детерминированным, и причинно верным.

Что из этого вынести

Всё это не доказывает, что разработка с ИИ ненадёжна. Код был дешёвой частью, и большая его часть оказалась верной. Это показывает, откуда должна браться уверенность. Зелёный набор тестов доказывает, что система делает то, что вы смоделировали. Он не скажет, верна ли ваша модель платформы, а все описанные сбои сидели именно в этой слепой зоне, потому что тесты несли те же ошибочные допущения, что и реализация.

Сработало другое: сделать опровержение дешёвым и повторять его. Четыре живых запуска, по несколько минут каждый и почти бесплатных на самой маленькой модели, вскрыли четыре архитектурных дефекта, которые одобрили 74 теста.

Если вы строите что-то на агентной платформе, сознательно закладывайте время на этот цикл. Интересные баги не в вашем коде. Они живут на стыке вашего кода и харнесса, поведение которого вы вывели, а не проверили. Эти факты о харнессе тоже стоит хранить в отдельном файле заметок, отдельно от спецификации дизайна, ведь они описывают платформу, а не ваш проект.