12 KiB
work_item, stage, author_agent, status, created_at, model_used, track
| work_item | stage | author_agent | status | created_at | model_used | track |
|---|---|---|---|---|---|---|
| ORCH-126 | analysis | analyst | ready-for-review | 2026-06-17 | claude-opus-4-8 | bug |
01 — BRD / Bug-report: ORCH-126 — queued-job хранит протухший run_id/pid и не клеймится даже при выключенном serial-gate
Work Item: ORCH-126 · Repo: orchestrator · Стадия: analysis · Трек: Bug (укороченный маршрут, ORCH-019)
🐞 Багфикс-трек, облегчённый пакет (ORCH-019). Дефект контрол-плейна локализован, причина установлена по коду, корректное поведение однозначно (queued-job не должен нести run-ownership). Правка — точечная гигиена жизненного цикла строки
jobs+ диагностика, по существующим паттернам; ADR/макет не требуются → стадияarchitectureпропускается,escalate: full-cycleНЕ ставится. ⚠️ Трассировка (CLAUDE.md §9): правка затрагивает инварианты ORCH-065 (Tier-1 pid-liveness reaper'а), ORCH-113 (finalizer-liveness), ORCH-114 (recover_on_startup), ORCH-099 (/metricsчитаетpid/run_id) — перед изменением прочитать их06-adr/и не сломать.
1. Бизнес-контекст и проблема
Симптом (наблюдаемое, из инцидента)
Второй дефект контрол-плейна, найденный при попытке провести срочные задачи ORCH-124/125 мимо
serial-gate. Даже при ORCH_SERIAL_GATE_ENABLED=false queued analyst-job'ы зависают и никогда не
переходят в running:
- job 2286 (ORCH-125):
status=queuedприrun_id=759/760иpid=35/42, тогда какstarted_at=NULL— физически невозможное состояние (run-ownership выставлен, но запуск не состоялся). - job 2303 (ORCH-124): при выключенном serial-gate минутами оставался
queued; счётчики очередиqueued=1, running=0— задача не клеймится, хотя слот свободен.
Вывод инцидента: это claim/restart/zombie-state баг, независимый от семантики serial-gate.
Причина симптома (установленный факт — по коду)
Ни один путь возврата job'а в queued не сбрасывает run-ownership (run_id, pid). Эти колонки
выставляются в launcher._spawn (run_id после INSERT в agent_runs, pid после Popen), но при
любом откате в queued остаются «протухшими» от прошлой попытки. Затронуты 5 точек:
| # | Путь | Что чистит | run_id/pid |
|---|---|---|---|
| 1 | db.requeue_running_jobs() (restart-recovery, src/db.py:1475) |
started_at |
НЕ чистит |
| 2 | db.mark_job(status='queued') (src/db.py:1239) |
started_at/finished_at |
НЕ чистит |
| 3 | db.mark_job_transient() (src/db.py:1213) |
started_at/finished_at |
НЕ чистит |
| 4 | db.reap_running_job(status='queued') (src/db.py:1619) |
started_at/finished_at |
НЕ чистит |
| 5 | db.claim_next_job() (src/db.py:1143) |
ставит started_at, attempts++ |
НЕ сбрасывает stale pid |
Это в точности воспроизводит наблюдаемое queued + run_id=759/760 + pid=35/42 + started_at=NULL
(пути 1–4: задача требовала рестарта/ретрая/реапа).
Механизм «никогда не клеймится» (гипотеза к подтверждению в development — взаимодействие с reaper)
claim_next_job сам по себе на run_id/pid не смотрит (SELECT гейтит лишь
status='queued' AND available_at<=now + dep/serial-gate), поэтому stale-метаданные не блокируют
SELECT напрямую. Старвейшн рождается из взаимодействия stale-pid с job-reaper'ом (ORCH-065),
который сканирует status='running' и судит Tier-1 liveness по jobs.pid через merge_gate.pid_alive:
- Окно claim→spawn.
claim_next_jobставитrunning+started_at, ноpidостаётся stale; реальныйpidпишется только в_spawnпослеPopen(launcher.py:711). Между этими шагами (или если_spawnупал наensure_worktree/_materialize_deferred_branchдо строки 711) reaper видитrunningсо старымpid. - pid переиспользован (вероятно после рестарта контейнера) →
pid_alive(stale)=True→ reaper «видит живой процесс» и никогда не реапит действительно застрявшую строку; приmax_concurrency=1(дефолт) этот фантомныйrunningблокирует клейм всей очереди. - pid мёртв → reaper копит dead-тики (
reaper_dead_ticks=2) против чужого pid и может отбросить легитимно-стартующий job обратно вqueued/failed— «выглядит частично стартовавшим, но фактически не запускается».
Таким образом stale run-ownership искажает сигналы liveness/диагностики (reaper + /metrics
get_running_agents), делая клейм/рестарт ненадёжным. Точный путь старвейшна подтверждается
добавляемой авто-санацией/диагностикой (см. 04-test-plan).
2. Объём (scope)
В объёме
- Аудит и фикс гигиены строки
jobsвокруг возврата вqueued:claim_next_job,requeue_running_jobs,mark_job,mark_job_transient,reap_running_job, окно_spawn. - Гарантия: queued-job либо чисто клеймится, либо детерминированно сбрасывается/реквью́ится при рестарте — без удержания stale run-ownership.
- Авто-санация или явная диагностика «невозможного» queued-состояния (
run_id/pidесть, аstarted_atнет). - Тесты на restart/requeue и stale queued-run-метаданные.
Вне объёма
- Семантика serial-gate (ORCH-088/124), dep-gate (ORCH-026) — НЕ трогаем (баг от них независим).
- Изменение
STAGE_TRANSITIONS/QG_CHECKS/check_*/ machine-verdict-ключей / схемы БД (новых колонок не вводим — фикс на существующих). - Переписывание reaper'а (ORCH-065/113) и transition-lease (ORCH-114) — лишь не сломать их инварианты.
3. Заинтересованные стороны
- Оператор (заказчик фикса) — нуждается в надёжном bypass'е: при выключенном serial-gate срочная задача обязана стартовать.
- Self-hosting orchestrator + все проекты на общем инстансе/очереди — фантомный
runningприmax_concurrency=1клинит очередь всех проектов.
4. Бизнес-требования (BR)
- BR-1 — При выключенном serial-gate (
ORCH_SERIAL_GATE_ENABLED=false) валидный queued ORCH-job клеймится и переходит вrunningштатно (без зависания). - BR-2 — Job, возвращённый в
queued(рестарт / ретрай / transient / reap), не несёт stale run-ownership: после возвратаrun_id IS NULLиpid IS NULL. - BR-3 — Свежеклеймленный, ещё не заспавненный job не несёт stale
pid(reaper не судит liveness по чужому процессу). - BR-4 — «Невозможные» queued-состояния (
run_id/pidпри отсутствииstarted_at) авто-санируются или явно сигнализируются (лог + наблюдаемость вGET /queue). - BR-5 — Регресс-тест: до фикса воспроизводит stale-состояние/старвейшн (красный), после — зелёный.
5. Нефункциональные требования (NFR)
- NFR-1 (never-raise / never-wedge): правки в горячем пути клейма не должны ронять или заклинивать очередь всех проектов; ошибка диагностики — изолирована и не влияет на клейм.
- NFR-2 (offline hot-path):
claim_next_jobостаётся offline (только локальная БД), без сети. - NFR-3 (совместимость): схема БД не меняется; поведение для не-stale job'ов байт-в-байт; enduro-trails не затронут.
- NFR-4 (self-hosting safety): правка не рестартит/не роняет прод-контейнер, не трогает
main, без новых процессов; миграция БД не требуется (правки на существующих колонках). - NFR-5 (restart-safe / идемпотентность): санация выдерживает повторный рестарт и гонку
worker↔reaper↔monitor (атомарные guard'ы по
statusсохранены).
6. Допущения и ограничения
- Дефолт
max_concurrency=1(config.py:114) — единственный stuck-runningклинит очередь; поэтому корректность liveness reaper'а критична. run_idдля queued-job — мёртвая ссылка на прошлую попытку (текущего run'а нет), её сброс безопасен; история живёт в таблицеagent_runs, не вjobs.run_id.- Env читается на старте процесса: на self-hosting выключение флага требует управляемого рестарта (вне объёма этого фикса; здесь — гарантия корректного клейма после рестарта).
7. Критерии успеха
Queued ORCH-job при выключенном serial-gate стартует штатно; queued-job'ы никогда не удерживают stale
run-ownership после рестарта/ретрая; невозможные queued-состояния авто-санируются или явно видны;
регресс-тесты покрывают restart/requeue и stale queued-run-метаданные. Полный pytest tests/ -q
зелёный. Детальные PASS/FAIL — 03-acceptance-criteria.md.
8. Риски
- Сброс
run_id/pidв неверной точке мог бы стереть идентичность активного run'а → строго на переходе вqueuedи/или в claim до_spawn(детали — TRZ FR-1..FR-3, решает developer). - Взаимодействие с reaper Tier-1/Tier-3 и transition-lease — см. трассировку выше; детальный
риск-разбор —
10-tech-risks.md(на укороченном маршруте не обязателен).