# BRD — ORCH-065: zombie jobs + залипший merge-lease Work Item ID: ORCH-065 Тип: BUG (P0) Репозиторий: orchestrator (self-hosting) Эпик: блокер ORCH-54 (полностью автономный self-deploy) ## 1. Контекст и проблема Оркестратор — единый инстанс с **общей БД и общей очередью** (`jobs`, `max_concurrency=1` для self-hosting), обслуживающий несколько проектов. Финальная автономность self-deploy упирается в два связанных класса отказов, оба сводящиеся к «процесс умер/завершился, а состояние осталось захваченным навсегда»: ### Проблема A — zombie jobs (строка `jobs` навсегда `running`) Агент (deployer/developer/reviewer) завершается **или умирает** (краш, OOM, рестарт контейнера в ходе self-deploy, гибель monitor-потока), но строка в таблице `jobs` остаётся в статусе `running`. Финализация статуса job выполняется **только** в `_monitor_agent` → `_finalize_job` внутри того же процесса; если этот поток/процесс не доживает до финализации — job «зомбирован». - Единственная имеющаяся защита — `requeue_running_jobs()` в `main.lifespan`, срабатывающая **исключительно на старте процесса**. Зомби, возникший **без** рестарта (умер дочерний процесс/monitor-поток, а сервис жив), не реанимируется никогда. - При `max_concurrency=1` одна зомби-строка `running` блокирует claim всех последующих job (`count_running_jobs() >= max_concurrency` → claim не происходит) → **встаёт конвейер всех проектов**. ### Проблема B — залипший merge-lease Merge-gate (ORCH-043) берёт файловый lease `/.merge-lease-.json` ПЕРЕД rebase+re-test и держит его до фактического merge PR в `main`. Если процесс умирает на финальном merge **с зажатым lease**: - Реклейм lease реализован **лениво и только по возрасту** (`age >= merge_lock_timeout_s`) и **только в момент `acquire_merge_lease` другой задачей**. Проактивного освобождения (на старте / периодически) нет; **liveness держателя по pid не проверяется** (хотя `pid` в lease пишется). - Пострадавшая задача сама re-drive не получает: merge не финализируется → задача висит, lease мешает чужим merge до истечения TTL. ### Проблема C — неидемпотентная финализация merge Если rebase+re-test прошли зелёно (ветка догнана и проверена), но процесс умер до завершения слияния PR — повторного «докатывания» merge нет. Задача застревает в полу-выполненном состоянии, хотя вся дорогая работа (rebase+re-test) уже сделана. ## 2. Бизнес-последствия - **Это ПОСЛЕДНЯЯ ручная точка автономного деплоя.** Без фикса ни одна self-hosting задача не доезжает до прода без оператора (cancel zombie + ручной merge PR + ручной `--deploy`). - Прямой блокер эпика ORCH-54. - Доказанные инциденты (07.06): ORCH-58/60/61/21 — каждый раз после успешного deployer-прохода job оставался `running`; jobs **236/239/242/254** — зомби, прод-merge/deploy доводились вручную. - Групповой риск: зомби в общей очереди при concurrency=1 останавливает конвейер enduro-trails и всех прочих проектов. ## 3. Цель Сделать так, чтобы **смерть процесса/потока на любой стадии (включая self-restart во время deploy) НЕ оставляла навсегда захваченных ресурсов** — ни строки `jobs` в `running`, ни merge-lease. Конвейер должен самовосстанавливаться без оператора, при этом сохраняя все инварианты self-hosting (не ронять прод-контейнер, не трогать `main`, fail-closed на реальных ошибках). ## 4. Объём (Scope) ### В объёме 1. **Job-reaper** — фоновый watchdog (паттерн `reconciler`/`queue_worker`), детектирующий «мёртвый» `running`-job и приводящий его строку в корректный терминальный/повторный статус (`done`/`failed`/`queued`) детерминированно, без LLM. Restart-safe и работающий **без** рестарта процесса. 2. **Проактивный реклейм stale merge-lease** — освобождение lease, чей держатель мёртв (pid не жив) ИЛИ возраст превысил TTL — на старте и периодически (reaper/ reconciler), а не только лениво при чужом `acquire`. 3. **Идемпотентная финализация merge** — если rebase+re-test зелёные, но merge не состоялся, операция повторяется/докатывается без потери уже сделанной работы. ### Вне объёма - Переход на внешний брокер очередей / смену схемы блокировок merge на БД-lock. - Полный авто-approve деплоя (ORCH-54) — отдельная задача; здесь только снятие технического блокера. - Изменение конвейера стадий (`STAGE_TRANSITIONS`) и реестра гейтов как контрактов. ## 5. Заинтересованные стороны - Owner оркестратора (self-hosting автономность). - Все проекты на общем инстансе (enduro-trails и пр.) — страдают от блокировки общей очереди. ## 6. Допущения и ограничения - `max_concurrency=1` для self-hosting сохраняется. - Self-hosting safety (CLAUDE.md): нельзя ронять/рестартить прод-контейнер в рамках задачи; нельзя пушить/форс-пушить `main`; реклейм lease не должен прерывать легитимно работающий merge. - Никаких ложных реанимаций: живой, но долгий job не должен помечаться зомби (нужен порог/грейс «N тиков» + проверка реальной смерти, а не просто долготы). - Контракт **never-raise** для всей новой фоновой логики (как у reconciler/merge_gate). - Kill-switch на каждый новый механизм (как `reconcile_enabled` / `merge_gate_enabled`). ## 7. Критерий успеха (бизнес-уровень) После фикса воспроизводимый сценарий «успешный deployer-проход + смерть процесса/ self-restart» НЕ оставляет зомби-job и зажатого lease: задача либо корректно доезжает до `done` сама, либо откатывается по штатному контракту — **без участия оператора**. Регресс-тест на jobs-зомби и stale-lease зелёный.