Files
orchestrator/docs/architecture/adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md

7.6 KiB
Raw Blame History

work_item, stage, author_agent, status, created_at, model_used
work_item stage author_agent status created_at model_used
ORCH-109 architecture architect proposed 2026-06-14 claude-opus-4-8

adr-0040: Per-role wall-clock бюджеты (developer/reviewer) + launch-time стамп модели

  • Статус: proposed
  • Дата: 2026-06-14
  • Задача: ORCH-109
  • Детальный ADR: docs/work-items/ORCH-109/06-adr/ADR-001-agent-timeout-budgets-and-launch-model-stamp.md

Контекст

Инцидент ORCH-104 вскрыл два глобальных дефекта подсистемы запуска агентов (src/agents/launcher.py), затрагивающих все репо общего self-hosting-инстанса (orchestrator + enduro-trails): (A) единый wall-clock тайм-аут agent_timeout_seconds=1800 убивает здоровые тяжёлые роли (developer xhigh, reviewer), т.к. в проде agent_timeout_overrides_json пуст; (B) agent_runs.model пишется только постфактум из usage-JSON (record_usage, COALESCE), а timeout-killed прогон финальный JSON не эмитит → модель остаётся NULL именно в момент инцидента, хотя эффорт уже стампится на launch (ORCH-087). Решение меняет два глобальных per-agent инварианта (бюджеты тайм-аутов + потолок Tier-3 reaper'а ORCH-065), поэтому регистрируется сквозным ADR, а не только work-item ADR.

Решение

Две аддитивные правки launcher'а, без касания STAGE_TRANSITIONS/QG_CHECKS/check_*/ machine-verdict-ключей/схемы БД (колонка agent_runs.model TEXT уже существует — миграции нет):

  • Launch-time стамп модели. В _spawn резолвенная resolve_agent_model(...) пишется в agent_runs.model рядом со стампом эффорта (объединённый UPDATE … SET model=?, effort=?), пустой резолв → NULL. Постфактум record_usage (model=COALESCE(?, model)) остаётся обогащением, перестаёт быть единственным источником истины — launch-стамп переживает kill и виден in-flight (db.get_running_agents уже отдаёт model). never-raise: сбой стампа изолирован, launch не падает.
  • Per-role бюджеты через выделенные типизированные config-ключи (по образцу agent_model_<role>/agent_effort_<role>): agent_timeout_developer_s=3600, agent_timeout_reviewer_s=3000. Лестница _resolve_timeout: agent_timeout_overrides_json[agent] (escape-hatch, высший) → выделенный ключ роли → agent_timeout_seconds=1800 (прочие роли — байт-в-байт). never-break: малформный JSON / вне-диапазонный ключ → откат на глобальный дефолт + WARNING.
  • Синхронное поднятие reaper (инвариант ORCH-065). reaper_max_running_s: 3600 → 5400. Проверка reaper_max_running_s > max(timeout) + agent_kill_grace_seconds: 5400 > 3600 + 20 = 3620 ✓ (запас 1780s, покрывает окно финализации монитора). 5400 < sidecar stage_stuck_s=7200 → легитимный длинный developer-прогон не порождает ложный stage_stuck-алерт.
  • Канон дефолтов (ORCH-101). Дефолт каждого ключа = боевому значению → пустой .env воспроизводит прод-поведение (в т.ч. поднятые бюджеты). «Байт-в-байт прежнее» (NFR-1) строго применяется к ролям вне {developer, reviewer}.
  • FR-5 анти-salvage — структурно, без нового кода. Продвижение стадии гейтится if exit_code == 0: _try_advance_stage(...); timeout-kill (-9) → _finalize_job → retry/fail, никогда не advance. Добавляется регресс-тест, не новая ветвь.

Альтернативы

  • Дефолт agent_timeout_overrides_json={"developer":…} — отвергнуто: ломает канон ORCH-101 непустым JSON-дефолтом, хрупкая строка против типизированного int, нельзя override одной env-роли.
  • Бюджеты ≤ 3580 без поднятия reaper — рассмотрено (меньший blast-radius), отвергнуто как доминирующее: урезает самую тяжёлую роль ради статичности backstop-числа; NFR-4 явно делегирует reaper-поднятие архитектору. Остаётся операторским запасным путём (всё env-override'имо).
  • Repo-scoped бюджеты (*_repos) — отвергнуто: тайм-аут — свойство launch, не гейт-решение; глобальность благоприятна enduro.
  • Новый guard-leaf анти-salvage — отвергнуто: продвижение уже гейтится exit-кодом; новый код = лишняя ветвь риска.

Последствия

  • Модель видна (не null) при любом исходе прогона (трекер / status-комментарии / /metrics / /queue) — ключевой контекст инцидента доступен в момент сбоя; тяжёлые роли получают реальный бюджет (developer ×2, reviewer +67%) → меньше ложных timeout-kill при автономном прогоне (ORCH-088).
  • Аддитивно/обратимо/never-raise; гейты/схема/machine-verdict/деплой-путь не тронуты; прод-контейнер не рестартится (self-hosting безопасность, NFR-5).
  • Плата: Tier-3 backstop 60→90м (реально зависший прогон держится дольше — митигейшн Tier-1/Tier-2 + watchdog ≤ бюджета); глобальность поднимает enduro-роли (благоприятно; reaper-страховка цела); sidecar agent_hung (alert-only) может чаще срабатывать на здоровых длинных прогонах с low-CPU фазами (не влияет на конвейер).
  • Откат: занизить ORCH_AGENT_TIMEOUT_DEVELOPER_S/_REVIEWER_S (= 1800) и вернуть ORCH_REAPER_MAX_RUNNING_S=3600; launch-стамп модели отката не требует. Kill-switch не вводится (нет рисковых ветвей: стамп безопасен, тайм-аут fail-safe на дефолт).

Связи

adr-0011 (job-reaper — Tier-3 backstop reaper_max_running_s, инвариант ORCH-065 правится здесь синхронно), adr-0030 (metrics-endpoint — get_running_agents().model начинает заполняться для running-job), adr-0033 (sidecar-watchdog — agent_hung/stage_stuck пороги, alert-only), adr-0036 (replication foundation — канон «дефолт = боевое значение»). Маркер-инварианты: ORCH-065, ORCH-087, ORCH-101.