← Углубления  ·  Университет

DD · Устойчивость роя: чтобы ночью не сломалось молча

Углублённый модуль поверх курса. Собирает инженерику, которая превращает принципы Модуля 4 («Иммунная система») и Модуля 2 («Оркестрация») в конкретный код, который можно написать на этой неделе. Тон и метки честности — как в основном курсе.

0. Зачем отдельный модуль

Guardrails и budget-gate (Модуль 4) отвечают на вопрос «когда систему нужно ОСТАНОВИТЬ». Этот модуль — про другое: «что делать, когда что-то УЖЕ сломалось в 3 часа ночи» — не остановить, а продолжить правильно, не удвоить ущерб и не заглохнуть молча. Аналогия для всего модуля: не приёмный покой (guardrails решают, кого пускать), а вегетативная нервная система — то, что держит организм живым автоматически, пока хозяин спит: сердце бьётся, давление держится без единого сознательного решения. Рой агентов должен уметь то же — пережить сбой сети, зависший провайдер, конфликт двух писателей в одну память — и к утру либо доехать до цели, либо явно остановиться на понятном шаге, а не зависнуть в неопределённости.

Хорошая новость: у Лучиана это уже частично построено — в коде resource-governor и в шрамах 06-errors-and-gotchas.md. Модуль называет то, что он уже изобрёл полевым методом, настоящими именами и достраивает недостающее.


1. Идемпотентность и безопасный resume: не тупой retry, а «продолжить, не повторить»

По-человечески. Если медсестра не уверена, кололи ли пациенту обезболивающее пять минут назад, она не колет «на всякий случай ещё раз» — передозировка. Она смотрит в карту: там либо стоит отметка «инъекция сделана в 03:14», либо нет. Агентная система в момент сбоя — ровно эта медсестра, которая не помнит, что произошло секунду назад (сессия оборвалась, процесс убит); единственный способ не навредить — внешняя, надёжно записанная отметка, а не повторение действия «для верности».

Как называется по-настоящему. Idempotency (идемпотентность) — свойство операции, при котором повторный вызов с тем же ключом не меняет состояние второй раз. Безопасный resume — продолжение с последней зафиксированной точки, а не пересоздание с нуля («тупой retry» повторяет весь путь заново и рискует продублировать уже случившиеся платные побочные эффекты).

Где это УЖЕ есть на заводе, только без имени. Пункт 8 из 06-errors-and-gotchas.md — это учебник по идемпотентности, написанный кровью, а не по книжке: «Восстанавливаешь упавший проект → удалил, сжёг деньги на озвучке». Правило, которое Лучиан вывел сам: voice=doneне удалять, только /resume упавшей стадии после голоса. Это буквально идемпотентный resume — система смотрит на зафиксированный статус («озвучка уже оплачена и лежит на диске») и НЕ повторяет платную операцию. Второй живой пример — медиа-брокер губернатора (POST /media/image//media/video{jobId}): в контракте прямо написано, что брокер «пейсит, дедупит, переживает рестарт» — дедупликация по jobId это и есть идемпотентный ключ на уровне протокола.

Для любопытных. Рецепт для нового платного/мутирующего вызова: (1) стабильный idempotency key, не меняющийся при повторных попытках ("photo:scene-3:gen1", а не новый UUID на каждый retry); (2) явная статус-машина на диске: queued → running → done | failed, не булево «попробовали / не попробовали»; (3) перед retry — сначала прочитать статус по ключу: done → вернуть готовый результат, не пересчитывать; running дольше таймаута — подозрение на зомби (§5), не сигнал «повторить»; (4) DELETE/recreate — крайняя мера, только когда точно известно, что оплаченный побочный эффект ещё не создан.

// пример записи статуса задачи — то, что должно жить в memory-bank/state.json,
// а не только в оперативной памяти процесса
{
  "task_id": "photo:scene-3:gen1",
  "idempotency_key": "photo:scene-3:gen1",
  "status": "done",
  "paid_side_effect": true,
  "artifact_ref": "s3://.../scene-3.png",
  "cost_usd": 0.04,
  "checkpointed_at": "2026-07-12T03:14:00Z"
}
🔧 Тупой retry vs идемпотентный resume — один и тот же сбой, разный итог

Стадия упала посреди ночи. Озвучка (голос) уже была сгенерирована и оплачена. Выбери, как реагирует агент — и посмотри, чем это кончается.

Выбери вариант выше.

2. Task packet / result packet: что агенты кладут друг другу на стол

По-человечески. Пересменка в больнице. Ночной врач не может сказать дневному «разберёшься по ходу» — он заполняет структурированную сдачу дежурства (формат SBAR: Situation — Background — Assessment — Recommendation), потому что следующий должен действовать по фактам, а не гадать по интонации предыдущего.

Как называется по-настоящему. Task packet (пакет задачи) — что оркестратор передаёт исполнителю на вход; result packet — что исполнитель кладёт обратно. Это контракт хендоффа между агентами — то же, что Anthropic/Factory называют «structured handoff» (analysis/orkestratsiya.md, пример Missions): воркер, закончив фичу, не говорит «готово», а заполняет сводку — что сделано, что осталось, какие команды выполнялись и с каким кодом выхода, какие проблемы найдены.

Где это УЖЕ есть на заводе. Контракт POST {BANK_URL}/api/error-bank/report {generator, project_id, stage, error, traceback?, topic?, channel?} из 08-contracts.md + конвенция holder = "<gen>:<job>" — это уже result packet для случая ошибки. Не хватает симметричного task packet на входе и result packet на успехе — сейчас передача идёт неявно через state.json/config.json проекта.

Для любопытных. Минимальная схема, которую стоит формализовать как обязательный контракт для будущего оркестратора (расширение уже существующего error-bank):

// task_packet — то, что оркестратор кладёт агенту/сервису на вход
{
  "task_id": "lightning:script:2026-07-12-01",
  "idempotency_key": "lightning:script:2026-07-12-01",
  "stage": "stage_script",
  "parent_task_id": "lightning:pipeline:2026-07-12",
  "inputs": {"topic_ref": "niche-finder://slug/123"},
  "budget_caps": {"max_usd": 0.50, "max_cache_read_tokens": 250000},
  "deadline_ms": 600000,
  "retry_count": 0,
  "max_retries": 3
}

// result_packet — то, что кладут обратно
{
  "task_id": "lightning:script:2026-07-12-01",
  "status": "done",            // done | failed | needs_retry | escalate
  "artifact_ref": "projects/lightning-01/script.md",
  "cost_usd": 0.11,
  "duration_ms": 4200,
  "next_stage_hint": "stage_script_qc",
  "error": null
}

Ключевая деталь: budget_caps и max_retries едут ВМЕСТЕ с задачей, а не хранятся отдельно в конфиге — это прямая реализация его же правила «>250k cache-read или >$0.50 — стоп» на уровне контракта, а не договорённости с моделью.


3. Чекпоинты и восстановление роя после сбоя посреди ночи

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

Как называется по-настоящему. Checkpointing — периодическая запись состояния во внешнее хранилище; crash recovery — восстановление по последнему чекпоинту, а не по памяти процесса, которой больше нет.

Где это УЖЕ есть на заводе, и где дыра. Пункт 7 из 06-errors-and-gotchas.md — готовый разбор плохого восстановления: «Повторные kill+relaunch+resume+stage-run → 409 Conflict, ничего не бежит, стадия failed навсегда». Правило Лучиана — «чинить вперёд, не с нуля», и /resume асинхронный и медленный («говернер пейсит» — 2-3 минуты). Чекпоинт (state.json по стадиям) уже есть, но дисциплина обращения с ним — нет: паника и повторные ручные вмешательства («thrash») плодят подвешенные корутины, каждая пиннит семафор — сам акт «слишком частого resume» создаёт новые сбои поверх старого.

Для любопытных. Практический рецепт для ночного оркестратора без человека рядом: - Чекпоинт пишется после каждого перехода стадии, а не только на старте/финише прогона — иначе при сбое посередине непонятно, докуда реально дошли. - При рестарте оркестратора (после падения контейнера, перезапуска VPS) — не «запустить всё заново», а сканировать незавершённые задачи по последнему чекпоинту и решать: resume (если побочный эффект зафиксирован как безопасный — см. §1), fail-and-requeue (если чекпоинт слишком старый или в неопределённом состоянии) или escalate (Telegram-уведомление, если это происходит третий раз подряд с одной и той же задачей). - Возраст чекпоинта — сигнал сам по себе: если стадия «висит running» дольше её обычного таймаута × 2, это не «подождать ещё», а автоматический перевод в needs_recovery.

🔧 Оркестратор рестартовал ночью — что делать с зависшей задачей?

Клик по каждому варианту — когда он применяется.

resume чекпоинт свежий fail-and-requeue чекпоинт устарел escalate 3-й раз подряд
Клик по прямоугольнику — условие и что происходит.

4. Политика ретраев: backoff, классификация ошибок, таймаут-бюджеты

По-человечески. Триаж в приёмном покое не лечит каждую жалобу одним способом «подождать и повторить сильнее». Сначала определяют ТИП проблемы — аллергия, передозировка, или просто нужно подождать, пока подействует лекарство — и только потом решают: ждать, повторить процедуру или звать старшего врача.

Как называется по-настоящему. Retry policy с exponential backoff (экспоненциальная задержка между попытками, обычно + jitter — случайный разброс, чтобы клиенты не били по серверу синхронно), классификация ошибок по типу, и timeout budget — явный бюджет времени/попыток, после которого решение принимается автоматически.

Где это УЖЕ есть на заводе — и это зрелая практика. 08-contracts.md предписывает классифицировать ошибки FastGen ПО ТИПУ, с разной паузой: capacity (403 «no accounts available») → ~45-65с, и это не считается в бюджет give-up (не вина завода, а насыщение провайдера); rate (429) → ~8с; server (5xx) → ~5с. Отдельно — false-positive "dangerous content": правило запрещает ретраить тот же промпт, вместо этого реворд до 6 раз, потом блокировка сцены. Самое тонкое: give-up-политика различается по СТАТУСУ задачи, не только по типу ошибки — QUEUED можно смело отменять (возврат кредита), а RUNNING лучше дождаться и забрать коллектором (отмена варящейся генерации токен не вернёт). Это стоит закрепить явным правилом для ВСЕХ провайдеров, а не оставлять знанием одного сервиса.

Trust-badge. При точности 95% на шаг 10-шаговый прогон без чекпоинтов успешен только в ≈60% случаев (0.95^10) (жёлтый — иллюстративная арифметика, не эмпирика). Решение для Loop of Death (агент бесконечно «чинит» ошибку, порождая новую) — smart routing + кеш + лимит попыток (max 3) с эскалацией, даёт 70% экономии и 99.9% надёжности по данным практиков (зелёный — проверено дословно).

Для любопытных. Формула backoff с джиттером: delay = min(cap, base * 2^attempt) * random(0.5, 1.0). Governor уже частично это реализует (PATCH 0002: governor busy-spin backoff — не спамить занятый пул опросами, а ждать паузами).

🔧 Симулятор: точность на шаг → надёжность 10-шагового прогона

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

Точность шага: 95% → вероятность успеха всех 10 шагов подряд = 0.95^10 ≈ 60%

5. Rate-limits провайдера и throttling при параллельном веере

По-человечески. У приёмного покоя ограниченное число коек и аппаратов ИВЛ. Если привезут двадцать человек разом, часть должна подождать в очереди по справедливой сортировке — но не толпиться у одной двери, и не так, чтобы одно отделение забрало все аппараты, пока соседнее стоит пустым.

Как называется по-настоящему. Rate limiting (ограничение частоты запросов) и throttling (придерживание потока ниже лимита), concurrency pools (пулы одновременных слотов), fair scheduling / max-min fairness (честное распределение, не строгий FIFO).

Где это УЖЕ есть на заводе — готовый учебный пример. Таблица пулов resource-governor из 08-contracts.md:

Пул Тип Лимит Что лимитирует
fastgen-images concurrency 20 параллельные картинки
fastgen-video concurrency 5 параллельные видео (провайдер реально варит 4-5, даже при «тарифе 10 потоков»)
fastgen-credits bucket 2000/час часовой бюджет img-кредитов
brain concurrency 3 параллельные claude -p
voice concurrency 1 MOSS TTS

Governor делит слоты между группами (photo, omni, cracker, lightning...) по max-min fairness, не строгому FIFO — освободившийся слот уходит самому обделённому генератору. Два режима отказа, которые стоит различать: fail-wait для платных пулов FastGen — пул занят, ждём, никогда не идём мимо (обход = 429-штормы); fail-open для самого губернатора — недоступен по сети, работаем без слота, а не встаёт весь завод.

Прямое применение к вееру MVP. Если консультант-фабрика запустит 3-10 explorer-агентов параллельно, все они будут драться за общий пул brain (concurrency=3) — систему это не сломает (governor поставит их в очередь), но сильно замедлит ночь, если пул не увеличен под план параллелизма заранее. Второй урок: thumbnail-studio документирует, что максимальный параллелизм (cap=20) контрпродуктивен во время обучения — размер пула не константа «побольше = лучше», а параметр под конкретную задачу.

🔧 Пулы resource-governor — на что дерётся веер агентов

Клик по пулу — что он лимитирует и что будет, если запустить веер параллельно.

Выбери пул выше.

6. Конкурентная запись в общую память: блокировки и сериализация

По-человечески. Если два врача одновременно правят одну бумажную карту пациента, один допишет «давление 120», другой поверх сотрёт это и впишет «давление 140» — запись победившего перезапишет данные другого без единого сообщения об ошибке. Решение — либо карту держит один врач за раз (блокировка), либо у каждого свой лист (изоляция), а не общая страница на всех.

Как называется по-настоящему. Race condition (гонка за ресурс), write conflict, locking / serialization (блокировки, последовательный доступ), optimistic vs pessimistic locking.

Где это УЖЕ есть на заводе как грабля. 06-errors-and-gotchas.md: принудительный стоп активного прогона vidiq оставил data/.run.lock зависшим — файловую блокировку снял только ручной rm, штатный выход её не подхватил. Классический lock-leak — держатель лока умер, не отпустив его, и следующий процесс вечно видит «занято». Второй пример — ghost governor-holders: SIGKILL при пересборке контейнера пропускает release(), пул выглядит полностью занятым, хотя реальных задач нет; лечится ежедневным ночным ресетом пула, не вручную.

Практический рецепт: не блокировать сильнее, а изолировать писателей. Урок из analysis/realnye-keysy.md §7 и Missions-архитектуры (orkestratsiya.md): при параллельном вееро правильный ответ на конкурентную запись — НЕ более жёсткая блокировка общего ресурса, а изоляция: свой git worktree на explorer-а, свой namespace в memory-bank на эксперимент, слияние результатов в одной точке (judge panel). Блокировки нужны только для реально общих счётчиков (дневной бюджет в resource-governor) — и там правильный инструмент не вечный mutex, а лок с TTL: у губернатора уже частично есть ttlMs в теле /acquire — держатель падает, лок сам протухает по времени.


7. Дедлоки между агентами — и почему это НЕ то же самое, что Loop of Death

По-человечески. Представь двух врачей: один ждёт, пока освободится операционная, которую занял второй, а второй ждёт лаборанта, сидящего в очереди на подпись у первого врача. Оба стоят вечно — никто локально не виноват, но круг замкнулся, и без вмешательства извне это не разрешится само. Это принципиально другая проблема, чем врач, зацикленно назначающий одно и то же неверное лекарство (это Loop of Death из Модуля 4 — один актор, повторяющий провальный шаг).

Как называется по-настоящему. Deadlock (взаимная блокировка) — циклическое ожидание ресурсов между двумя и более сторонами без выхода извне. Четыре классических условия (для любопытных): взаимное исключение, удержание-и-ожидание, отсутствие принудительного отбора, циклическое ожидание.

Разница с Loop of Death в одной фразе. Loop of Death — один актор в цикле «ошибка → починка → новая ошибка» (лечится лимитом попыток, §4). Дедлок — несколько акторов, каждый формально прав, но взаимно блокирующих друг друга (лечится не лимитом попыток, а порядком захвата ресурсов и таймаутами на удержание).

Где на заводе есть потенциал для дедлока — и что уже частично защищает. Если два сервиса начнут захватывать слоты resource-governor в разном порядке (сервис А сначала берёт voice, ждёт brain; сервис Б наоборот) — это классический дедлок. Governor уже частично страхует: ttlMs на /acquire превращает бесконечное зависание в таймаут и повторную попытку. Но TTL смягчает последствия, не устраняет причину. Настоящая защита — единый канонический порядок захвата слотов, зафиксированный правилом в 08-contracts.md (например: всегда brainvoicerender, никогда в обратном порядке). Дёшево внедрить сейчас, пока сервисов немного.

🔧 Не путать: Loop of Death vs Дедлок

Похоже выглядит («агент завис»), лечится по-разному. Переключи и сравни.

Выбери вариант выше.

8. Health-checks и каскадный отказ зависимостей

По-человечески. Регулярный чекап измеряет показатели ДО того, как пациент рухнет в обморок — активная проверка, а не «подождём симптомов». А «эффект домино» — это когда отказ одного узла (насоса, качающего кровь) быстро валит другие органы через общую систему, потому что все зависели от одного кровотока.

Как называется по-настоящему. Health check (проверка живости сервиса), circuit breaker (предохранитель, размыкающий цепь к больной зависимости вместо того, чтобы долбить её запросами), cascading failure (каскадный отказ), graceful degradation (осознанная деградация вместо полной остановки).

Где это УЖЕ есть на заводе. Каждый сервис обязан отдавать GET /health → {ok:true} (требование регистрации тулки в 08-contracts.md); pre-flight self-test перед «готово» — проверка resource-governor:4700/health, memory-bank:4600/api/health и своего /health ДО начала работы. Живой пример активного health-check-а — moss_worker_guard.py, cron-скрипт, реконсилящий число GPU-воркеров MOSS с очередью (реальный факт — впустую жгло ~$38/сутки, пока не поставили страж). Каскадный отказ уже разведён на два режима: fail-wait для платных пулов (никогда не обходить лимит — предохранитель держит линию там, где цена ошибки — деньги) и fail-open для самого губернатора как инфраструктуры (сеть моргнула — работаем без слота, а не роняем весь конвейер).

Для любопытных. Health-check «отвечает ли сервис» — минимум. Следующий уровень — карта зависимостей: какие сервисы для стадии жёсткие (без них стадия не выполнится — ждать/эскалировать) и какие мягкие (продолжить в деградированном режиме, залогировав факт). Диск на 91% (внутренний риск, требует свежей проверки актуальности) — пример «тихого убийцы»: пассивный health-check покажет {ok:true} до последней секунды, активный алерт по порогу — нет.


Чек-лист внедрения

  1. Идемпотентный ключ + статус-машина на диске (queued/running/done/failed) для каждого платного вызова; никогда DELETE+пересоздание, если побочный эффект уже оплачен («voice=done → resume» — на все стадии).
  2. Формализовать task_packet/result_packet как обязательную JSON-схему хендоффа — расширение error-bank, с полями budget_caps/max_retries внутри пакета, не в отдельном конфиге.
  3. Чекпоинт после КАЖДОГО перехода стадии; при рестарте оркестратора — сканировать незавершённые задачи и решать resume/requeue/escalate по возрасту чекпоинта.
  4. Классификация ошибок (capacity/rate/server/content-block) с разным backoff и правилом «считать ли в give-up-бюджет» — по образцу таблицы FastGen, распространить на MOSS, YouTube API и остальных провайдеров.
  5. Жёсткий max retry (3-5) с эскалацией + таймаут-бюджет на каждый долгий async-вызов и обязательный cancel() при выходе из поллинга (зомби-фикс FastGen — на все долгие операции).
  6. Пулы concurrency/bucket на каждый платный ресурс, размер — под план параллелизма (веер MVP не должен голодать brain-пул); fail-wait для платных пулов, fail-open только для инфраструктуры.
  7. Изоляция параллельных писателей (worktree на explorer-а, свой memory-bank namespace на эксперимент) вместо более жёсткой блокировки общего ресурса; для реально общих счётчиков — TTL-локи, не вечный mutex.
  8. Единый канонический порядок захвата слотов/локов для всех сервисов — зафиксировать правилом в 08-contracts.md.
  9. /health + активные self-healing cron-guard'ы (по образцу moss_worker_guard.py) на каждый простаивающий платный ресурс; карта «жёсткая/мягкая» зависимость на стадию.
  10. Ночной прогон = серия коротких фаз с промежуточными агентными чекпоинтами, не один непрерывный многочасовой сеанс.

Куда это в курсе

Прямое инженерное углубление Модуля 4 (Урок 4.3 «Loop of Death», Урок 4.1 «Guardrails») — там объясняется ЗАЧЕМ, здесь — КАК устроен код чекпоинтов, ретраев и локов. Связано с Модулем 2, Урок 2.4 (изоляция воркспейсов при вееро MVP) и Модулем 8, Урок 8.2 (очереди задач — throttling и rate-limits в §5 это продолжение темы брокера/resource-governor). Питает этапы Модуля 11 (Roadmap): (4) брокер, (5) orchestrator.mjs с матрицей гейтов, (7) песочница ночного билдера — устойчивость роя это то, что делает их безопасными без человека рядом ночью.