7.6 KiB
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 <sidecarstage_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.