--- work_item: ORCH-109 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-14 model_used: claude-opus-4-8 --- # 02 — ТЗ (TRZ): ORCH-109 — timeout budgets + launch-time model telemetry для developer/reviewer Work Item: **ORCH-109** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. > Архитектурное обоснование/решения (выбор «выделенные config-ключи vs `agent_timeout_overrides_json`», > точные числовые бюджеты, синхронная правка `reaper_max_running_s`) — задача архитектора (`06-adr`). ## 1. Сводка изменения Две независимые, но связанные правки в подсистеме запуска агентов: 1. **Launch-time стамп модели.** В `launcher._spawn` резолвенная `resolve_agent_model(...)` (уже вычисляется на launch, строка ~559) записывается в `agent_runs.model` в той же DB-сессии, что и стамп эффорта (ORCH-087, строки ~566–571). Постфактум-парс (`usage.record_usage`, `model=COALESCE(?, model)`) сохраняется как **обогащение** и уже не затирает launch-значение `NULL`'ом. Следствие: модель присутствует на строке прогона с момента запуска, переживает timeout-kill и видна in-flight в `GET /metrics`/`GET /queue`. 2. **Конфигурируемый поднятый wall-clock бюджет для `developer`/`reviewer`.** `_resolve_timeout(agent)` должен возвращать поднятый бюджет для `developer` и `reviewer`, конфигурируемый и не затрагивающий прочие роли; механизм покрыт тестом. Сохраняется never-break (малформный конфиг → глобальный дефолт) и кросс-инвариант reaper (`reaper_max_running_s > max(timeout)+grace`). Плюс верификационные требования: телеметрия timeout-killed прогона (модель+эффорт не `null`) и guard анти-salvage (timeout-killed прогон не продвигает стадию). ## 2. Задействованные модули / пути | Путь | Действие | |------|----------| | `src/agents/launcher.py` | изменить — стамп `model` в `_spawn` рядом с `effort` (≈ стр. 559–573); проверка `_resolve_timeout` обслуживает override `developer`/`reviewer` (≈ стр. 661–679) | | `src/config.py` | изменить — config для поднятого тайм-аута `developer`/`reviewer` (выделенные ключи и/или дефолт `agent_timeout_overrides_json`); обновить комментарии-паспорт (≈ стр. 115–126); проверить/при необходимости поднять `reaper_max_running_s` (≈ стр. 494–499) | | `src/usage.py` | проверить/зафиксировать тестом — `record_usage` (`model=COALESCE(?, model)`) НЕ затирает launch-стамп при `model=None` (≈ стр. 207–230); `_extract_model` (≈ стр. 95–118) | | `src/notifications.py` | проверить (правка, вероятно, не нужна) — `_stage_line` рендерит `· {model} · {effort}` из `agent_runs` для строки с `exit_code=-9` (≈ стр. 360–373, 498–542) | | `src/db.py` | НЕ менять схему — `agent_runs.model` TEXT уже есть; проверить, что `get_running_agents` (≈ стр. 1370–1405) отдаёт launch-стампнутую модель для running-job | | `src/stage_engine.py` | проверить — путь продвижения стадии не advance'ит прогон с `exit_code != 0` (guard FR-5); правка только если найден разрыв | | `.env.example` | обновить — задокументировать ключи тайм-аута `developer`/`reviewer` (BR-6) | | `tests/test_orch109_timeout_model.py` (новый) | создать — покрытие FR-1…FR-5 | | `CHANGELOG.md`, `CLAUDE.md` (паспорт), `docs/architecture/README.md` (модель/эффорт-секция) | обновить в том же PR (правило агентов №2) | ## 3. Функциональные требования ### FR-1 — Launch-time стамп модели (BR-1) В `launcher._spawn`, после `model = resolve_agent_model(agent, project_id)`, резолвенное значение записывается в `agent_runs.model` для текущего `run_id` **в момент launch**, по образцу стампа эффорта (ORCH-087): - Запись в той же открытой `conn`, что и стамп эффорта (допустимо объединить в один `UPDATE agent_runs SET model=?, effort=? WHERE id=?` — решение реализации). - Пустой резолв (`model == ""`, CLI-дефолт без `--model`) → пишется `NULL` (как эффорт: `effort or None`), чтобы суффикс модели в трекере корректно опускался. - **Инвариант:** значение `agent_runs.model` присутствует с момента launch и не зависит от исхода прогона. - **never-raise (NFR-2):** сбой записи изолирован `try/except` + WARNING; launch продолжается. ### FR-2 — Постфактум-enrich сохраняет launch-стамп (BR-2) `usage.record_usage` остаётся источником обогащения (токены/стоимость/модель из usage-JSON), но: - При `usage is None` или `usage.get("model") is None` (оборванный/малформный JSON) launch-стамп модели **не затирается** (текущая семантика `model=COALESCE(?, model)` это уже обеспечивает — требование зафиксировать тестом, не регрессировать). - При наличии непустой модели в JSON enrich **уточняет** значение (например, полный provider-prefixed id или фактический fallback-model) — допустимая перезапись непустым на непустое. - Семантика парсинга `_extract_model` (приоритет `modelUsage` → top-level `model`) — без изменений. ### FR-3 — Конфигурируемый поднятый тайм-аут `developer`/`reviewer` (BR-3) - `_resolve_timeout(agent)` возвращает поднятый бюджет для `agent in {"developer","reviewer"}`, конфигурируемый, **детерминированный**, и **не затрагивающий** прочие роли (они продолжают получать глобальный `agent_timeout_seconds`, если для них нет override). - Механизм: либо документированный дефолт `agent_timeout_overrides_json`, либо выделенные ключи (например `agent_timeout_developer_s`/`agent_timeout_reviewer_s`) — выбор архитектора; контракт FR-3 — резолв per-agent поднятого бюджета. - **never-break (NFR-2):** малформный/невалидный конфиг → откат на глобальный дефолт + WARNING (поведение `_resolve_timeout` сохраняется). - **Кросс-инвариант (NFR-4):** итоговый `max(резолвенный тайм-аут)` + `agent_kill_grace_seconds` обязан оставаться `< reaper_max_running_s`; при нарушении — синхронно поднять `reaper_max_running_s`. ### FR-4 — Телеметрия timeout-killed прогона (BR-4) Для прогона с `exit_code != 0` без финального usage-JSON (timeout-kill, `_record_kill` стампит -9): - Строка стадии трекера (`notifications._stage_line`) рендерит `· {short_model} · {effort}` с реальными значениями (модель **не** `null`), т.к. оба стампнуты на launch (FR-1 + ORCH-087). - `db.get_running_agents` (источник `GET /metrics`/`GET /queue`) отдаёт launch-стампнутую модель и для **running**-job (in-flight видимость, NFR-6). - Изменения `notifications.py`, вероятно, не требуются (рендер уже читает `model`); требование — верифицировать тестом, что при стампе на launch значение долетает. ### FR-5 — Guard анти-salvage timeout-killed прогона (BR-5) - Timeout-killed прогон (`exit_code != 0`, в т.ч. -9/-15/143) `developer`/`reviewer` **не продвигает** стадию (`development → review`, `review → testing`) автоматически. - Существующий контракт (advance только при чистом exit-коде + зелёный exit-гейт; иначе `attempts