From b025e1bdf41967f651df198db49759e0795f8ea5 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Sun, 14 Jun 2026 01:02:10 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=662 --- docs/architecture/README.md | 2 +- ...-timeout-budgets-and-launch-model-stamp.md | 85 +++++++ docs/architecture/internals.md | 4 +- ...-timeout-budgets-and-launch-model-stamp.md | 221 ++++++++++++++++++ docs/work-items/ORCH-109/10-tech-risks.md | 42 ++++ 5 files changed, 351 insertions(+), 3 deletions(-) create mode 100644 docs/architecture/adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md create mode 100644 docs/work-items/ORCH-109/06-adr/ADR-001-agent-timeout-budgets-and-launch-model-stamp.md create mode 100644 docs/work-items/ORCH-109/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1a50dfb..57afe88 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -9,7 +9,7 @@ - **Stage Engine** (`src/stage_engine.py`) — исполнение переходов, диспетчеризация QG (`_run_qg`), откаты, синхронизация с Plane. - **Review/Test Parsers** (`src/review_parse.py`, ORCH-046) — defensive-извлечение дословного must-fix текста из артефактов для встраивания в `task_desc` заворота: `extract_review_findings` (P0/P1 из `12-review.md`), `extract_test_failures` (фрагмент тела `13-test-report.md`). Контракт «never raise»: любая ошибка → `""`. - **Quality Gates** (`src/qg/checks.py`) — проверки выхода со стадии, реестр `QG_CHECKS`. -- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`. +- **Agent Launcher** (`src/agents/launcher.py`) — запуск Claude CLI агентов в изолированном git worktree, мониторинг, auto-advance. Модель/эффорт каждого агента резолвятся из config (`resolve_agent_model`/`resolve_agent_effort`, ORCH-41), а не из frontmatter промпта. **ORCH-74:** имя модели валидируется форматом `^claude-…$` (`is_valid_model`) перед `--model`; невалидное → лог + откат на следующий уровень/CLI-дефолт (never-break, как `VALID_EFFORTS` для эффорта). Тот же предикат гардит inline-чтение `--fallback-model`. **ORCH-109 ([adr-0040](adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md)):** (1) резолвенная **модель стампится в `agent_runs.model` в момент launch** (`_spawn`, объединённый `UPDATE … SET model=?, effort=?` рядом со стампом эффорта ORCH-087; пустой резолв → `NULL`; never-raise) → модель видна не-`null` при любом исходе прогона, включая timeout-kill (`exit_code=-9`), и in-flight в `GET /metrics`/`GET /queue` (`get_running_agents` уже отдаёт `model`); постфактум `record_usage` (`model=COALESCE(?, model)`) остаётся **обогащением**, не единственным источником истины. (2) **Per-role wall-clock бюджеты** через выделенные ключи `agent_timeout_developer_s=3600`/`agent_timeout_reviewer_s=3000` (лестница `_resolve_timeout`: `agent_timeout_overrides_json` → выделенный ключ роли → `agent_timeout_seconds=1800`; прочие роли — байт-в-байт; малформный/вне-диапазонный конфиг → дефолт + WARNING). Инвариант reaper ORCH-065 сохранён синхронным поднятием `reaper_max_running_s` 3600→**5400** (`5400 > max(timeout)3600 + grace20`). FR-5 анти-salvage — структурно: продвижение гейтится `if exit_code==0`, timeout-kill → `_finalize_job` (retry/fail), не advance. `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД не тронуты. - **Queue** (`src/queue_worker.py`, ORCH-1) — персистентная очередь задач (SQLite `jobs`), atomic claim, max_concurrency, ретраи, restart-safe. **ORCH-026:** `claim_next_job` гейтит задачи с незавершёнными зависимостями (`job_deps`, `NOT EXISTS`) без занятия слота; декларации/циклы — leaf `src/task_deps.py`. - **Job-reaper** (`src/job_reaper.py`, ORCH-065 — [adr-0011](adr/adr-0011-job-reaper-lease-reclaim.md)) — фоновый daemon-поток (каркас `reconciler`), стартует/останавливается в `main.lifespan` (после `reconciler.start()` / перед `worker.stop()`). Детектирует «мёртвый» `running`-job **без рестарта** процесса (Tier-1 мёртвый `jobs.pid` после `reaper_dead_ticks` тиков; Tier-2 `agent_runs.exit_code` записан, а job ещё `running`; Tier-3 backstop `reaper_max_running_s`) и приводит строку к корректному статусу через те же контракты (`_try_advance_stage`/`_finalize_job`, gate-driven; exit≠0/неизвестно → `attempts`/`agent_effort_`): `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. diff --git a/docs/architecture/internals.md b/docs/architecture/internals.md index ec4efcb..3373331 100644 --- a/docs/architecture/internals.md +++ b/docs/architecture/internals.md @@ -93,7 +93,7 @@ claude.exe --print --system-prompt --allowedTools Read,Write,Edit,Bash Каждый запуск: 1. Записывает run в DB (agent_runs) 2. Запускает subprocess. **stdout/stderr перенаправляются СРАЗУ в файл `/app/data/runs/{id}.log` на уровне ОС** (Popen `stdout=log_fh`). Никакого PIPE в памяти оркестратора → нет PIPE-deadlock, нет потока-читателя, нет зомби (B-2). -3. Стартует **watchdog thread** (timeout 30 мин → SIGKILL по pid) +3. Стартует **watchdog thread** (per-role wall-clock бюджет → SIGTERM→grace→SIGKILL по pid; ORCH-109: developer 60 мин / reviewer 50 мин / прочие 30 мин дефолт, `_resolve_timeout`) 4. Стартует **monitor thread**: `proc.wait()` (гарантированный reap → реальный exit_code в БД) → закрывает log_fh → git commit/push → auto-advance ### 5. Auto-advance (`launcher._try_advance_stage`) @@ -259,7 +259,7 @@ services: | Механизм | Описание | |----------|----------| -| Watchdog | Каждый агент: timeout 30 мин → SIGKILL + exit_code=-9 | +| Watchdog | Per-role wall-clock бюджет (ORCH-109): developer 60 мин / reviewer 50 мин / прочие 30 мин (`_resolve_timeout`) → SIGTERM→grace→SIGKILL + exit_code=-9. Tier-3 backstop `reaper_max_running_s`=90 мин > max(timeout)+grace (ORCH-065) | | safe.directory | git операции работают в любой директории | | Max retries | Developer: max 3 попытки, затем эскалация | | Zombie-free | stdout идёт сразу в файл + monitor `proc.wait()` → процесс всегда reap'нут (B-2) | diff --git a/docs/work-items/ORCH-109/06-adr/ADR-001-agent-timeout-budgets-and-launch-model-stamp.md b/docs/work-items/ORCH-109/06-adr/ADR-001-agent-timeout-budgets-and-launch-model-stamp.md new file mode 100644 index 0000000..c1a10c5 --- /dev/null +++ b/docs/work-items/ORCH-109/06-adr/ADR-001-agent-timeout-budgets-and-launch-model-stamp.md @@ -0,0 +1,221 @@ +--- +work_item: ORCH-109 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-14 +model_used: claude-opus-4-8 +--- + +# ADR-001: Поднятые wall-clock бюджеты developer/reviewer + launch-time стамп модели + +Work Item: **ORCH-109** — timeout budgets + launch-time model telemetry для developer/reviewer +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md`** +(решение кросс-каттинговое: меняет два глобальных per-agent инварианта подсистемы запуска — +бюджеты тайм-аутов всех репо и потолок Tier-3 reaper'а ORCH-065). + +## Статус +Proposed + +## Контекст + +Инцидент **ORCH-104** (runs 658/659/660) вскрыл два независимых дефекта подсистемы запуска агентов +(`src/agents/launcher.py`), верифицированных по коду: + +- **Дефект A — единый тайм-аут для всех ролей.** `_resolve_timeout(agent)` (launcher.py ≈661–679) + возвращает `settings.agent_timeout_seconds = 1800` (config.py:124) для **всех** ролей, если в + `agent_timeout_overrides_json` нет записи (в проде он пуст: `""`, config.py:126). Тяжёлые роли + `developer` (effort `xhigh`, кодирующая) и `reviewer` (effort `high`, читает диф + пишет ревью) + **честно** упираются в 1800s и убиваются watchdog'ом (`_watchdog → stop_process`, exit_code=-9 + через `_record_kill`, launcher.py:778–786). Механические роли (`tester`/`deployer`, effort + `medium`) в этот бюджет укладываются. +- **Дефект B — потеря модели в телеметрии при обрыве.** `agent_runs.model` пишется только + постфактум — из финального usage-JSON в `usage.record_usage` (`model=COALESCE(?, model)`, + usage.py:217). Убитый по тайм-ауту прогон не успевает эмитить финальный JSON → `_extract_model` + даёт `None` → модель остаётся `NULL` ровно тогда, когда она критична для разбора инцидента. + При этом **эффорт уже стампится на launch** (ORCH-087, launcher.py:566–571, `UPDATE agent_runs + SET effort=? WHERE id=?`), потому что CLI его в result-JSON не отдаёт; модель в той же точке + **резолвится** (`model = resolve_agent_model(...)`, launcher.py:559), но в БД на launch **не + пишется**. + +Установленные факты (по коду, не изобретены): +- Колонка `agent_runs.model TEXT` (NULLABLE) уже существует (`db.py:111`, `_ensure_column`) — + **миграции нет**. +- `record_usage` уже использует `model=COALESCE(?, model)` → `None` не затирает ранее проставленное + значение (usage.py:217). Не хватает только записи на launch. +- `db.get_running_agents()` уже отдаёт `r.model AS model` (`db.py` ≈1370–1405) — running-job увидит + модель **сразу** после launch-стампа, без правки SELECT. +- `notifications._stage_line` рендерит `· {model} · {effort}` из строки `agent_runs` — увидит + launch-стампнутую модель даже для `exit_code=-9`, без правки. +- Продвижение стадии гейтится `if exit_code == 0: self._try_advance_stage(...)` (launcher.py:951–952); + иначе → `_finalize_job` (launcher.py:957) → retry/fail. Timeout-kill (-9) **структурно** не + продвигает стадию. +- Кросс-инвариант reaper (ORCH-065): `reaper_max_running_s = 3600` (config.py:497) c зафиксированным + правилом «MUST be > max agent_timeout + grace» (config.py:480–482; `job_reaper.py:43,228`). + Сейчас `3600 > 1800 + 20 = 1820` ✓. **Любое поднятие бюджета обязано пересчитать это неравенство.** +- Sidecar-watchdog (`watchdog/`, ORCH-100) — **наблюдатель**, процессы **не убивает**; сигнал + `agent_hung` (runtime > `agent_hung_min`=20м **И** cpu < 1%) — только Telegram-алерт. Кому + принадлежит kill — исключительно in-process `launcher._watchdog`. + +Почему «как есть» не годится: единый бюджет 1800 системно убивает здоровые тяжёлые прогоны при +пакетном автономном прогоне (эпик ORCH-088), а телеметрия теряет модель именно на этих обрывах. + +## Решение + +### Сводка +Две аддитивные, изолированные правки подсистемы запуска, **без** касания +`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы БД: +(1) стамп резолвенной модели в `agent_runs.model` **в момент launch** рядом со стампом эффорта; +(2) **выделенные типизированные config-ключи** поднятого wall-clock бюджета для `developer`/`reviewer` +с синхронным поднятием `reaper_max_running_s` (сохранение инварианта ORCH-065). +FR-5 (анти-salvage) и FR-4/NFR-6 (видимость при kill / in-flight) — **структурно уже выполнены** +существующим кодом; ORCH-109 добавляет к ним регресс-тесты, а не новые ветви. + +### D1 — Launch-time стамп модели (FR-1, AC-1) +В `launcher._spawn`, в той же открытой `conn`, что и стамп эффорта (ORCH-087), резолвенная +`model = resolve_agent_model(agent, project_id)` (уже вычислена, launcher.py:559) записывается в +`agent_runs.model` текущего `run_id`. Рекомендуется **объединить** в один оператор: +`UPDATE agent_runs SET model=?, effort=? WHERE id=?` с параметрами `(model or None, effort or None, run_id)` +(один commit вместо двух; ровно та же `try/except`-изоляция, что у эффорта). +- Пустой резолв (`model == ""`, CLI-дефолт без `--model`) → пишется `NULL` (симметрично `effort or None`) + → суффикс модели в трекере корректно опускается. +- **Инвариант:** значение присутствует с момента launch и не зависит от исхода прогона (переживает + timeout-kill, виден in-flight). +- **never-raise (NFR-2):** сбой записи изолирован существующим `try/except` + WARNING; launch + продолжается (`model_flag` строится из локальной `model`, а не из БД — стамп лишь телеметрия). + +### D2 — Постфактум-enrich сохраняет launch-стамп (FR-2, AC-2) — без кода +`usage.record_usage` остаётся источником обогащения (токены/стоимость/модель из usage-JSON), но +**перестаёт быть единственным источником истины** о модели. Семантика `model=COALESCE(?, model)` +(usage.py:217) **уже** гарантирует: `usage=None` или `usage["model"]=None` → launch-стамп НЕ +затирается; непустая модель из JSON — допустимо уточняет (полный provider-prefixed id / фактический +fallback). **Код не меняется**; требование — зафиксировать поведение тестом (анти-регресс), не +сломать его будущими правками `record_usage`. + +### D3 — Конфигурируемый поднятый бюджет: выделенные типизированные ключи (FR-3, AC-3/AC-4) +Вводятся два **выделенных** config-ключа (по образцу `agent_model_`/`agent_effort_`, +config.py:133–138/147): + +```python +agent_timeout_developer_s: int = 3600 # env ORCH_AGENT_TIMEOUT_DEVELOPER_S +agent_timeout_reviewer_s: int = 3000 # env ORCH_AGENT_TIMEOUT_REVIEWER_S +``` + +`_resolve_timeout(agent)` получает детерминированную лестницу приоритетов (от высшего): +1. **`agent_timeout_overrides_json[agent]`** — существующий операторский escape-hatch; сохраняется + как высший приоритет (полная BC: сконфигурированный JSON по-прежнему выигрывает для ЛЮБОЙ роли). +2. **выделенный ключ роли** — `developer → agent_timeout_developer_s`, + `reviewer → agent_timeout_reviewer_s`. +3. **`settings.agent_timeout_seconds`** (1800) — для всех прочих ролей (`analyst`/`architect`/ + `tester`/`deployer`) — **байт-в-байт прежнее значение**. + +**never-break (NFR-2, AC-4):** малформный `agent_timeout_overrides_json` → уже игнорируется + WARNING +(launcher.py:677–678). Для выделенных ключей добавляется такой же защитный гард: если резолвенное +значение не положительный int (абсурд/0/отрицательное) → откат на `agent_timeout_seconds` + WARNING +(зеркало защитной валидации disk_monitor, ORCH-063 D7). Прогон/старт не падает. + +**Почему выделенные ключи, а не дефолт `agent_timeout_overrides_json`:** см. «Альтернативы». + +### D4 — Числовые бюджеты + синхронное поднятие reaper (FR-3/NFR-4, AC-5) +| Роль | Бюджет | Обоснование | +|------|--------|-------------| +| `developer` | **3600s (60м)** | бутылочное горло (xhigh, кодирующая); удвоение 1800→3600 — естественная разрядка для тяжёлых задач | +| `reviewer` | **3000s (50м)** | асимметрично легче developer, но тяжелее механических ролей; большой диф + high-reasoning | +| прочие | 1800s (без изменений) | механические/думающие роли укладываются в дефолт | + +`reaper_max_running_s`: **3600 → 5400 (90м)** синхронно (config.py:497). + +**Проверка инварианта ORCH-065** `reaper_max_running_s > max(резолвенный тайм-аут) + agent_kill_grace_seconds`: +`5400 > 3600 + 20 = 3620` ✓ (запас **1780s** — покрывает и окно финализации монитора: +commit/push/PR/usage-comments, Tier-2 `reaper_finalize_grace_s`=300). Дополнительно `5400 < ` +sidecar `stage_stuck_s` (7200s/120м) → легитимный длинный developer-прогон не порождает ложный +`stage_stuck`-алерт. + +Бюджеты — **глобальные per-agent** (не repo-scoped): действуют на все репо, включая enduro-trails. +Это благоприятно/нейтрально (enduro-developer тоже получает воздух; Tier-3 backstop reaper'а +сохраняется как страховка от реально зависшего прогона — R-4). + +### D5 — FR-5 анти-salvage: регресс-тест, без нового кода (AC-8) +Гарантия «timeout-killed прогон не продвигает стадию» **структурна**: `_try_advance_stage` вызывается +только под `if exit_code == 0` (launcher.py:951–952); kill (-9/-15/143) → `_finalize_job` → +`_finalize_transient`/`_finalize_permanent` (retry до `MAX_DEVELOPER_RETRIES`, иначе `failed` + +Telegram). **Новый guard в коде НЕ вводится** (не плодить лишние ветви риска) — добавляется +регресс-тест, фиксирующий, что прогон с `exit_code=-9` не вызывает `advance_stage`. salvage-режим +вне объёма. + +### D6 — Документация и канон дефолтов (FR-6, AC-10) +- `config.py` блок ORCH-7 (≈115–126): паспорт-комментарий расширяется описанием выделенных бюджетов + developer/reviewer + явной ссылкой на reaper-инвариант (NFR-4) с числами `5400 > 3620`. +- `.env.example`: **сейчас агент-тайм-аут ключей нет вовсе** (`ORCH_AGENT_TIMEOUT_SECONDS`/ + `_KILL_GRACE_SECONDS`/`_OVERRIDES_JSON` отсутствуют) → добавляется новый блок «Agent timeout + (ORCH-7/ORCH-109)» с пятью ключами (`SECONDS`/`KILL_GRACE_SECONDS`/`OVERRIDES_JSON`/ + `DEVELOPER_S`/`REVIEWER_S`) **+ обновляется `ORCH_REAPER_MAX_RUNNING_S=3600 → 5400`** (line 377). + Дефолты = боевым значениям (канон ORCH-101): пустой `.env` воспроизводит прод-поведение, в т.ч. + поднятые бюджеты. +- Архитектурная golden source (этот PR, авторство architect): `docs/architecture/README.md` + (бюллет Agent Launcher), `docs/architecture/internals.md` (стр. 96/262 — «timeout 30 мин» + расхардкоживается в per-role). Паспорт `CLAUDE.md` + `CHANGELOG.md` — обновляет developer в том + же PR (правило агентов №2). + +### Согласование BR-3 ↔ NFR-1 (важный нюанс) +NFR-1 требует «при пустом override-конфиге поведение байт-в-байт прежнее», а BR-3 требует «бюджеты +developer/reviewer подняты». Разрешение по канону **ORCH-101** («дефолт каждого параметра = боевому +значению; пустой `.env` ⇒ боевое поведение»): выделенные ключи **дефолтят на поднятый прод-бюджет**, +поэтому пустой `.env` даёт уже исправленное (поднятое) поведение для developer/reviewer — это и есть +намеренная боевая конфигурация. «Байт-в-байт прежнее» строго применяется к **прочим ролям** +(`analyst`/`architect`/`tester`/`deployer` остаются на 1800) — что и есть суть BR-3 (поднять ТОЛЬКО +две роли). Зафиксировано явно, чтобы reviewer не прочитал поднятый дефолт как нарушение NFR-1. + +## Альтернативы +- **Дефолт `agent_timeout_overrides_json = {"developer":3600,"reviewer":3000}`** (вместо выделенных + ключей) — отвергнуто: (1) ломает канон ORCH-101 «пустой = боевой» неочевидным непустым JSON-строковым + дефолтом; (2) JSON-строка хрупка (парс, экранирование) против типизированного int; (3) нельзя + переопределить одну роль одной env-переменной; (4) расходится с конвенцией per-agent скаляров + (`agent_model_`/`agent_effort_`). Выделенные ключи дают типобезопасность, индивидуальный + env-override и сохраняют JSON как чистый escape-hatch. +- **Бюджет developer/reviewer ≤ 3580 без поднятия reaper** (например 3000/2700) — отвергнуто как + доминирующее, но рассмотрено: держит `reaper_max_running_s=3600` нетронутым (меньший blast-radius), + но искусственно урезает самую тяжёлую роль ради статичности backstop-числа — оптимизация не той + переменной. NFR-4 **явно делегирует** архитектору синхронное поднятие reaper. Оставлено как + операторский запасной путь: всё env-override'имо, Owner может занизить бюджеты и вернуть reaper к + 3600 одной правкой `.env` (см. «Откат»). +- **Новый guard-leaf анти-salvage** (FR-5) — отвергнуто: продвижение уже гейтится exit-кодом + (launcher.py:951); новый код = лишняя ветвь риска. Достаточно регресс-теста (D5). +- **Repo-scoped бюджеты (`*_repos`)** — отвергнуто: тайм-аут — свойство launch, не гейт-решение; + глобальность благоприятна enduro и проще; гейт-паттерн `applies(repo)` тут неуместен. +- **Стамп модели через постфактум-парс лога на kill** — отвергнуто: модель известна на launch + детерминированно (`resolve_agent_model`); парсить оборванный лог — хрупко и поздно. + +## Последствия +- **+** Модель видна (не `null`) в трекере, status-комментариях, `/metrics`/`/queue` для **любого** + исхода, включая timeout-kill — ключевой контекст инцидента доступен в момент сбоя (BR-1/BR-4/NFR-6). +- **+** Тяжёлые роли получают реальный бюджет (developer ×2, reviewer +67%) → меньше ложных + timeout-kill на сложных задачах при автономном прогоне (ORCH-088). +- **+** Аддитивно/обратимо: ни схемы, ни гейтов, ни новых компонентов; `agent_runs.model` уже есть. +- **−** `reaper_max_running_s` 60→90м: реально зависший прогон (двойной отказ — watchdog-поток **и** + pid-liveness) держится Tier-3 backstop'ом на 30м дольше. Митигейшн: Tier-1 (pid) и Tier-2 + (finalize-grace) ловят типовые случаи быстрее; watchdog убивает в ≤3600s; double-fault редок. +- **−** Глобальность бюджета поднимает и enduro-роли. Митигейшн: Tier-3 reaper сохранён (R-4); + поднятие благоприятно для качества enduro-прогонов. +- **−** Sidecar `agent_hung_min`=20м теперь заметно ниже бюджета developer (60м) → возможны + Telegram-алерты `agent_hung` для здоровых длинных прогонов с low-CPU фазами. Митигейшн: сигнал — + **alert-only** (не убивает) и конъюнкция с `cpu<1%` гасит большинство ложных; тюнинг + `WATCHDOG_AGENT_HUNG_MIN` — вне объёма (отдельный sidecar-конфиг, alert-only). Детали — `10-tech-risks.md` TR-5. +- **Откат:** занизить бюджеты — снять/уменьшить `ORCH_AGENT_TIMEOUT_DEVELOPER_S`/`_REVIEWER_S` + (или выставить = 1800) и вернуть `ORCH_REAPER_MAX_RUNNING_S=3600`; launch-стамп модели отката не + требует (чистое улучшение телеметрии, COALESCE безопасен). Kill-switch не вводится — изменение не + добавляет рисковых ветвей (стамп всегда безопасен; тайм-аут fail-safe на глобальный дефолт). + +## Ссылки +- BRD: `docs/work-items/ORCH-109/01-brd.md` +- TRZ: `docs/work-items/ORCH-109/02-trz.md` +- Acceptance: `docs/work-items/ORCH-109/03-acceptance-criteria.md` +- Tech-risks: `docs/work-items/ORCH-109/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0040-agent-timeout-budgets-and-launch-model-stamp.md` +- Сверено по коду: `src/agents/launcher.py` (`_spawn` 559–571, `_resolve_timeout` 661–679, + `_watchdog`/`stop_process`/`_record_kill` 681–786, advance-гейт 951–952), `src/usage.py` + (`_extract_model` 95–118, `record_usage` 207–230), `src/config.py` (115–126, 480–497), + `src/db.py` (`agent_runs.model` 111, `get_running_agents` ≈1370–1405), `src/job_reaper.py` + (43, 228), `watchdog/config.py`/`watchdog/signals.py` (agent_hung/stage_stuck) +- Маркер-инвариант: ORCH-065 (reaper Tier-3), ORCH-087 (стамп эффорта), ORCH-101 (канон дефолтов) diff --git a/docs/work-items/ORCH-109/10-tech-risks.md b/docs/work-items/ORCH-109/10-tech-risks.md new file mode 100644 index 0000000..9f141c3 --- /dev/null +++ b/docs/work-items/ORCH-109/10-tech-risks.md @@ -0,0 +1,42 @@ +--- +work_item: ORCH-109 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-14 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-109 — timeout budgets + launch-time model telemetry + +Work Item: **ORCH-109** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Перечисляет риски реализации и их митигейшн. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | Поднятый бюджет developer/reviewer + grace ≥ `reaper_max_running_s` → job-reaper реапает **здоровый** долгий прогон до его watchdog'а (нарушение инварианта ORCH-065) | Низ. | Выс. | reaper синхронно поднят 3600→5400; sanity-тест проверяет `reaper_max_running_s > max(timeout)+grace` для всех ролей (`5400 > 3620`, запас 1780s); число живёт в `config.py` + `.env.example` рядом с инвариантом-комментарием (ADR D4/AC-5) | +| TR-2 | Постфактум-enrich (`record_usage`) затирает корректный launch-стамп при странном/оборванном JSON (`model=None`) | Низ. | Сред. | Семантика `model=COALESCE(?, model)` (usage.py:217) уже сохраняет launch-значение; зафиксировано регресс-тестом (AC-2); `record_usage` не правится | +| TR-3 | Гонка двух писателей `exit_code` (`_record_kill`=-9 и `_monitor_agent`=`proc.wait()`) искажает телеметрию модели | Низ. | Низ. | Модель — отдельная колонка, стампится один раз на launch до обоих писателей exit_code; они трогают только `exit_code`/`finished_at`. Подтверждается тестом (AC-1/AC-6) | +| TR-4 | Глобальность бюджета: поднятый developer-тайм-аут для **enduro** маскирует реально зависший прогон | Низ. | Сред. | Tier-3 backstop reaper'а (`reaper_max_running_s`) сохранён как абсолютный потолок; watchdog по-прежнему убивает в ≤ бюджета; бюджет лишь повышен, не снят | +| TR-5 | Sidecar `agent_hung_min`=20м заметно ниже бюджета developer (60м) → Telegram-алерты `agent_hung` для здоровых длинных прогонов | Сред. | Низ. | Сигнал **alert-only** (sidecar — наблюдатель, не убивает, ORCH-100); конъюнкция с `cpu<1%` гасит активный прогон; тюнинг `WATCHDOG_AGENT_HUNG_MIN` — вне объёма (отдельный sidecar-конфиг). Бюджет 5400s < `stage_stuck_s`=7200s → `stage_stuck` не ложит | +| TR-6 | Сбой записи launch-стампа модели (ошибка БД) роняет launch | Низ. | Выс. | Стамп в существующем `try/except` ORCH-087 + WARNING (never-raise, NFR-2); `model_flag` строится из локальной переменной, не из БД → launch не зависит от стампа (ADR D1) | +| TR-7 | Малформный/невалидный timeout-конфиг (битый JSON, нечисловой/отрицательный ключ) роняет прогон или старт | Низ. | Сред. | Малформный JSON → игнор + WARNING (существующее, launcher.py:677); выделенный ключ вне диапазона → откат на глобальный дефолт + WARNING (защитная валидация по образцу ORCH-063 D7); pydantic ловит нечисловой env на старте (AC-4) | +| TR-8 | Регресс прочих ролей: правка `_resolve_timeout` случайно меняет бюджет `analyst`/`architect`/`tester`/`deployer` | Низ. | Сред. | Лестница приоритетов: dev/reviewer — только по точному имени роли; прочие падают на `agent_timeout_seconds` (1800) без изменений; покрыто тестом per-role (AC-3) | +| TR-9 | Доп. риск контрактов: правка случайно задевает `STAGE_TRANSITIONS`/`QG_CHECKS`/machine-verdict/схему | Низ. | Выс. | Задача целиком вне слоя гейтов; диф-проверка AC-9; колонка `agent_runs.model` уже есть — ни одного `CREATE/ALTER` | + +## Сводный вывод + +Доминирующий класс — **конфигурационные инварианты подсистемы запуска** (TR-1/TR-7/TR-8): все +снимаются детерминированной лестницей `_resolve_timeout`, защитной валидацией (never-break) и +sanity-тестом reaper-неравенства. Остаточный риск для прод-конвейера (self-hosting) — **низкий**: +изменение аддитивно, обратимо через `.env`, не трогает гейты/схему/деплой-путь и не рестартит +прод-контейнер (NFR-5). Единственный наблюдаемый побочный эффект — возможный рост alert-only +`agent_hung`-нотификаций sidecar (TR-5), не влияющий на конвейер. + +**Эскалация:** не требуется на уровне `arch:major-change` (нет новой стадии/компонента/смены БД), но +решение **кросс-каттинговое** (меняет два глобальных per-agent инварианта всех репо + потолок Tier-3 +reaper'а) → зарегистрировано сквозным `docs/architecture/adr/adr-0040-*`. Возврат в анализ не нужен — +ТЗ удовлетворяется без нарушения принципов архитектуры.