architect(ET): auto-commit from architect run_id=662
This commit is contained in:
@@ -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<max`→`queued`, иначе `failed`+Telegram). Атомарный reap-claim (guard `status='running'`) совместим со стартовым `requeue_running_jobs`. Тот же поток периодически делает проактивный реклейм stale/dead merge-lease (см. ниже). never-raise; kill-switch `ORCH_REAPER_ENABLED`; снимок в `GET /queue` (блок `reaper`).
|
||||
- **Reconciler** (`src/reconciler.py`, ORCH-053 — реализовано, [adr-0007](adr/adr-0007-reconciler.md)) — фоновый daemon-поток (паттерн `queue_worker`), стартует/останавливается в `main.lifespan` (после `worker.start()` / перед `worker.stop()`). Реконсилирует рассинхрон «источник истины ≠ стадия задачи» при потерянном webhook. F-1 gate-side (продвигает застрявшую стадию по локальной БД через штатный `advance_stage(..., finished_agent=None)`), F-2 plane-side (опрос Plane API → `handle_*` из `plane.py`), F-3 (БД-fallback `sha→branch` в `handle_ci_status`). Источник истины — гейт/Plane, не событие; идемпотентность (active-job guard + atomic-claim + grace); kill-switch `ORCH_RECONCILE_ENABLED`. `analysis` F-1 не трогает (человеческий гейт). F-1 также пропускает escalated (retry≥лимита) и Blocked/Needs-Input задачи (ORCH-060). Наблюдаемость — блок `reconcile` в `GET /queue`.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
work_item: ORCH-109
|
||||
stage: architecture
|
||||
author_agent: architect
|
||||
status: proposed
|
||||
created_at: 2026-06-14
|
||||
model_used: 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.
|
||||
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user