Головна / Статті / Чому агентам 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 тести.

Якщо ви будуєте щось на агентній платформі, свідомо закладайте час на цей цикл. Цікаві баги не у вашому коді. Вони живуть на стику вашого коду й харнесу, поведінку якого ви вивели, а не перевірили. Ці факти про харнес теж варто зберігати в окремому файлі нотаток, окремо від специфікації дизайну, адже вони описують платформу, а не ваш проєкт.