---
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<max → queued`, иначе `failed` + Telegram — `launcher._monitor_agent`/`queue_worker`/
  `job_reaper`) реализует это структурно.
- **Требование:** анализ подтверждает достаточность существующей гарантии; поведение фиксируется
  **регресс-тестом**. Отдельный guard в коде добавляется **только если тест выявит разрыв**.
- **salvage-режим НЕ вводится** (вне объёма) — задача гарантирует не-продвижение, не возобновление.

### FR-6 — Документация конфига (BR-6)
- Комментарий-паспорт в `config.py` (блок ORCH-7, строки ~115–126) расширяется описанием поднятых
  бюджетов `developer`/`reviewer` и ссылкой на reaper-инвариант (NFR-4).
- `.env.example` несёт соответствующие ключи с дефолтами = боевым значениям (канон ORCH-101).
- Сквозная документация (`CLAUDE.md`, `docs/architecture/README.md` — таблица «модель/эффорт по
  ролям») обновляется в том же PR.

## 4. Изменения API

Нет. Ни одного нового/изменённого endpoint'а. `GET /metrics` и `GET /queue` отдают тот же контракт
(`schema_version: 1`) — поле `agents[].model` лишь **начинает заполняться** для running-job
(аддитивное улучшение данных, не контракта; sidecar обязан толерировать, ORCH-099 NFR-6).

## 5. Изменения схемы БД

Нет. Колонка `agent_runs.model` (`TEXT`, NULLABLE) уже существует (`db._ensure_column`, инициализация
`init_db`). Никаких `CREATE`/`ALTER`/новых таблиц. Меняется только **момент** и **частота** записи в
существующую колонку (launch + опциональный постфактум-enrich).

## 6. Требования к новым/изменённым QG checks

Нет. `QG_CHECKS` / `check_*` / `_parse_*` / machine-verdict ключи — не трогаются. Задача целиком вне
слоя Quality Gate (подсистема launch/телеметрия/конфиг). FR-5 опирается на **существующий**
exit-code-контракт продвижения, не на новый гейт.

## 7. Совместимость / регресс

- **Обратная совместимость:** стамп модели аддитивен; при пустом timeout-override поведение
  байт-в-байт прежнее (NFR-1). Никаких kill-switch не требуется — изменение не вводит новых ветвей
  риска (стамп модели всегда безопасен; тайм-аут конфигурируем и fail-safe на глобальный дефолт).
- **Область раската:** стамп модели — все репо/роли (безопасно). Поднятый тайм-аут — только
  `developer`/`reviewer` (все репо, т.к. тайм-аут глобален per-agent); прочие роли неизменны.
- **Обратимость:** вернуть тайм-аут — снять override-конфиг (откат на 1800s). Launch-стамп модели
  отката не требует (чистое улучшение телеметрии).
- **Кросс-каттинг (NFR-4):** при поднятии бюджета выше `reaper_max_running_s − grace` синхронно
  поднять `reaper_max_running_s` (ORCH-065). Sanity-тест конфига стережёт инвариант.
- **never-raise (NFR-2):** обе правки изолированы; сбой не роняет launch и не падает на старте при
  плохом env.
- **Self-hosting (NFR-5):** ни рестарта прода, ни изменения deploy-пути; общий инстанс безопасен.
- **Полный регресс `tests/`** остаётся зелёным; coverage-гейт (ORCH-027) удовлетворён новым
  тест-файлом (изменения в `src/` минимальны и покрыты).
