8.2 KiB
type, work_item_id, verdict, version
| type | work_item_id | verdict | version |
|---|---|---|---|
| review | ORCH-065 | REQUEST_CHANGES | 2 |
Review ORCH-065
Summary
Задача закрывает три связанных класса отказов «процесс/поток умер, а ресурс остался
захваченным навсегда»: zombie jobs (A), залипший merge-lease (B), неидемпотентная
финализация merge (C). Реализация в целом качественная: новый daemon-поток
src/job_reaper.py по образцу reconciler (never-raise, kill-switch, снимок в
/queue), трёхуровневая liveness, атомарный reap_running_job(... WHERE status='running'),
проактивный реклейм lease (pid_alive + reclaim_stale_lease), идемпотентный guard
pr_already_merged, колонка jobs.pid через идемпотентный _ensure_column. Инварианты
сохранены: STAGE_TRANSITIONS/QG_CHECKS/check_*/БАГ-8/exit-коды хука не тронуты
(коммиты ORCH-065 не касаются stages.py/qg/checks.py/stage_engine.py); реклейм
lease — только удаление файла, без git-операций. Документация (README, internals, ADR,
глобальный adr-0011, CHANGELOG, .env.example) обновлена в этом же PR. pytest tests/ -q
— 743 passed.
Прошлый блокер v1 (guard pr_already_merged не подключён к пути merge) устранён
коммитом aa46e5d: промпт .openclaw/agents/deployer.md теперь предписывает консультировать
pr_already_merged ПЕРЕД любым (повторным) merge — AC-11 wiring на месте.
Новый блокер: гонка Tier-2 reaper'а с живым monitor-потоком при штатной финализации — порядок «side effects → atomic claim» нарушает собственный контракт ADR-001 Р-1 и может дать дубль-advance / дубль-enqueue следующей стадии (FR-1.2/FR-1.3/AC-3/AC-4).
Findings
P0 — Blocker
- нет
P1 — Must fix
-
Tier-2 реапит живой, штатно финализирующийся job; side-effects идут ДО атомарного claim (нарушение ADR-001 Р-1, риск дубль-advance/дубль-enqueue). В
JobReaper._reap_job(src/job_reaper.py:177) ветка Tier-2 срабатывает на ЛЮБОЙrunning-job, у которого вagent_runsуже записанexit_code— без grace и без какого-либо признака смерти monitor'а. Но именно это состояние («exit_code записан, job ещё running») — нормальное окно финализации живого monitor-потока:_monitor_agentпишетexit_code, затем выполняет git commit/push (+PR), БАГ-8-проверку, сетевые usage-комментарии в Plane (секунды-десятки секунд), и лишь потом_try_advance_stage→_finalize_job. pid агента (jobs.pid) к этому моменту уже мёртв (процесс завершён до записи exit_code), поэтому отличить «monitor умер» от «monitor жив и финализирует» по pid невозможно, а reaper тикает каждыеreaper_interval_s(60с). Доступный для gracefinished_at_runзапрашивается вget_running_jobs, но не используется (помечен «debug only»,src/db.py).Усугубляет дефект порядок в
_reap_known_outcome(src/job_reaper.py:206): для exit0 сначала вызывается_gate_driven_advance(job)(побочные эффекты —_try_advance_stage→advance_stage→enqueue_jobследующей стадии), и только потом атомарныйreap_running_job(..., "done"). Это прямо противоречит ADR-001 Р-1: «Атомарный reap-claim. Перед любым действием с побочными эффектами reaper атомарно «застолбляет» строку тем же приёмом, чтоclaim_next_job». Поскольку claim стоит ПОСЛЕ side-effects, его guardWHERE status='running'не сериализует advance: даже проигравший гонку reaper (rowcount==0) уже успел выполнитьadvance_stage.Последствия в окне гонки (reaper-тик попал в финализацию живого monitor'а):
enqueue_job(src/db.py:419) — обычный INSERT без дедупликации, поэтому параллельныеadvance_stageот reaper и monitor дают двеqueued-строки следующего агента → дублирующий запуск следующей стадии (двойной commit/PR/ комментарии, лишние токены);- либо (если гейт следующего ребра ещё красный, напр. CI не позеленел) reaper уходит
в
_reap_unknown_outcome→reap_running_job(..., "queued")и спихивает в queued job, который вот-вот успешно завершится monitor'ом; приmax_concurrency=1воркер может повторно заклеймить и перезапустить тот же агент.
Это противоречит FR-1.3/AC-3 (живой агент НЕ должен реапиться) и FR-1.2/AC-4 (никакого дубль-advance). Требуется одно из:
- ввести grace для Tier-2 — реапить только если
finished_at_runстарше заведомо-большего, чем максимальное окно финализации (использовать уже запрашиваемыйfinished_at_run), и/или - claim-before-act: атомарно «застолбить» строку (как требует ADR Р-1) ДО любого
advance_stage/enqueue_job, чтобы проигравший гонку reaper не выполнял побочных эффектов; advance — только после выигранного claim.
(Tier-1 этим не страдает: streak
reaper_dead_ticks+ мёртвый pid; ветка_reap_unknown_outcomeбез exit0 тоже безопасна — единственное действие там и есть атомарный flip.)
P2 — Should fix
- нет
P3 — Nice to have
- Битая ссылка в CHANGELOG на глобальный ADR. В записи ORCH-065 указан
docs/architecture/adr/adr-0011-job-reaper-and-lease-reclaim.md, фактический файл —adr-0011-job-reaper-lease-reclaim.md(без-and-). README иsrc/job_reaper.pyссылаются корректно. Поправить путь вCHANGELOG.md.
Документация
Обновлена корректно и в этом же PR (AC-17 PASS по составу): docs/architecture/README.md
(раздел про job-reaper + lease-reclaim, таблицы БД и /queue), docs/architecture/internals.md,
docs/architecture/adr/adr-0011-job-reaper-lease-reclaim.md (+ запись в adr/README.md),
docs/work-items/ORCH-065/06-adr/ADR-001-job-reaper-and-lease-reclaim.md, CHANGELOG.md,
.env.example (флаги ORCH_REAPER_* / ORCH_LEASE_RECLAIM_ENABLED).
Оговорка: ADR-001 Р-1 описывает «claim перед побочными эффектами», но реализация exit0-пути этому не следует (см. P1) — при исправлении кода либо привести код в соответствие с ADR, либо синхронизировать формулировку ADR с фактическим порядком. Битая ссылка на adr-0011 в CHANGELOG — P3.