# BRD — ORCH-053: Sweeper потерянных webhook (реконсиляция застрявших стадий) Work Item ID: ORCH-053 Стадия: analysis → (architecture) Тип: надёжность конвейера (проектирование + реализация). Self-hosting (ORCH). ## 1. Проблема (бизнес-контекст) Продвижение задач между стадиями конвейера завязано **исключительно** на входящие webhook-события: - **Plane** (`work_item.updated` → статус In Progress / Approved / Rejected) — единственный триггер старта задачи, advance и rollback (`src/webhooks/plane.py`). - **Gitea** (CI-status `success`/`failure`, push, PR reviewed/merged) — триггер development→review, architecture→development, review→testing, deploy→done (`src/webhooks/gitea.py`). Если входящее событие **потеряно** (502 на падающем/ребилдящемся инстансе, Plane/Gitea не повторяют доставку, сетевой сбой, sha→branch не разрезолвился, вебхук был временно выключен) — статус в источнике истины (Plane / зелёный CI) уже изменился, **а задача в оркестраторе не сдвинулась**. Задача висит молча, без какого-либо механизма восстановления. **Живой инцидент (ORCH-044, 06.06):** dev-агент отработал (exit 0, CI позеленел), но Gitea webhook о CI-success не продвинул задачу (не дошёл / не сматчился sha→branch). Задача висела бы на `development` молча навсегда — спасли только ручным дёрганьем гейта `check_ci_green`. Это **системная дыра**, а не разовый сбой; сейчас её ловит ручной heartbeat-watchdog Стрима (костыль). ### Что уже есть и почему недостаточно | Механизм | Что покрывает | Почему не закрывает дыру | |----------|---------------|--------------------------| | `requeue_running_jobs()` (startup) | зависшие **jobs** при рестарте | про jobs, не про застрявший **stage-переход** | | orphan-recovery (`main.py`) | `agent_runs` без `finished_at` | job-уровень, не stage | | ORCH-5 events de-dup (`delivery_id`) | защита от **дублей** webhook | обратной защиты от **потери** нет | | ORCH-045 `ci_poll` в `check_ci_green` | поллит CI 12×10с | только **если гейт уже вызван** webhook'ом; не пришёл webhook → гейт не вызывается | Общий принцип всех существующих механизмов — restart-safe resilience на уровне jobs. **Нет ни одного механизма, реконсилирующего рассинхрон «источник истины ≠ стадия задачи».** ## 2. Цель Задача **не должна застревать молча** из-за потерянного входящего события. Ввести фоновый периодический **sweeper / reconciler**, который сам находит «зависшие» задачи и доигрывает пропущенный переход — через **те же штатные гейты и обработчики**, что и webhook (никакой параллельной логики продвижения). Убрать необходимость в ручном heartbeat-watchdog. ## 3. Заинтересованные стороны - **Owner / Стрим (Слава)** — перестаёт ловить зависания вручную. - **Все проекты на инстансе** (enduro-trails + orchestrator) — конвейер не встаёт молча. - **Self-hosting (ORCH)** — особенно при ребилде прода (ORCH-51): вебхуки, прилетевшие на падающий инстанс, подбираются реконсиляцией после старта. ## 4. Объём (Scope) В объёме — **две взаимодополняющие ветки реконсиляции** (обе обязательны): ### F-1. Gate-side sweeper (реконсиляция застрявшей стадии по локальной БД) Периодический проход по таблице `tasks`: найти задачи, у которых (а) `stage != done`, (б) нет активных job'ов в очереди, (в) с момента `updated_at` прошло больше **per-stage порога** → пере-проверить QG текущей стадии и, если passed — продвинуть **штатным путём** (`stage_engine.advance_stage(..., finished_agent=None)`, тот же путь, что использует webhook). Закрывает потерю Gitea CI/PR-вебхуков (ORCH-044). ### F-2. Plane-side reconciler (реконсиляция потерянного Plane status-webhook) Периодический опрос Plane API по проектам реестра (`projects.py`): issues в статусах, требующих действия (In Progress / Approved / Rejected). Сверить с локальной `tasks` и доиграть **через существующие обработчики `webhooks/plane.py`**: - **In Progress + нет задачи в БД** → создать+запустить (`handle_status_start`/`start_pipeline`); - **Approved + стадия не сдвинута** → advance (`handle_verdict(approved=True)`); - **Rejected + не откатана** → rollback (`handle_verdict(approved=False)`). ### F-3. Усиление sha→branch резолва в Gitea-вебхуке В `handle_ci_status` добавить надёжный fallback (поиск task по БД), чтобы исходный webhook реже терялся из-за неразрезолвленного branch. Sweeper работает от задачи (repo+branch известны из БД) и обходит эту хрупкость по определению. ### F-4. Наблюдаемость Лог (и опц. Telegram) каждый раз, когда sweeper **разблокировал** застрявшую задачу — чтобы видеть частоту срабатывания дыры (метрика потерянных webhook). Опц. вывод счётчика в `/queue` или `/reconcile`. Не спамить, когда всё синхронно. ### Вне объёма - Буфер недоставленных webhook (это ORCH-51; sweeper — резервная сетка к нему). - Изменение состава стадий/гейтов (`STAGE_TRANSITIONS`, `QG_CHECKS`). - Изменение логики самих гейтов и обработчиков (только переиспользование). - Новый исполняемый деплой (ORCH-36). ## 5. Ключевые требования (бизнес-уровень) 1. **Источник истины — гейт/Plane, а не событие.** Sweeper дёргает ровно те же функции продвижения, что и webhook. Параллельной логики продвижения быть не должно. 2. **Идемпотентность (критично).** Задержавшийся или дублированный webhook + sweeper НЕ создают двойную задачу / двойной запуск / двойной advance. Тот же guard, что у webhook: нет активного job + стадия совпадает + atomic claim как в `queue_worker`. 3. **Безопасность активной работы.** Sweeper НЕ трогает задачи с активными (`queued`/`running`) job'ами — они легитимно в работе, не потеряны. 4. **Per-stage grace.** Разные стадии имеют разное нормальное время (analysis ~8–15 мин vs deploy). Порог застревания настраивается, чтобы не дёргать гейт у задачи, где агент законно работает. 5. **Restart-safe.** Sweeper — фоновый поток, стартует с приложением, переживает рестарт (как `queue_worker`). Без потери состояния. 6. **Self-hosting safety.** Sweeper не должен ронять/рестартить прод-контейнер; kill-switch в конфиге для поэтапного раската и аварийного отключения. 7. **Без шума.** Когда всё синхронно — никаких действий и нотификаций. 8. **Документация = golden source.** README/architecture, ADR, CHANGELOG обновляются в том же PR. ## 6. Эффект - Потерянный webhook больше не = молча застрявшая задача. - Ручной heartbeat-watchdog Стрима больше не нужен для ловли зависаний (AC-5 в эпике). - Резервная сетка к ORCH-51 при ребилде прода. ## 7. Связи - **Дополняет ORCH-51** (потеря webhook при рестарте — буфер; sweeper — реконсиляция). - **Дополняет ORCH-36** (если deploy-webhook потеряется — sweeper добьёт deploy→done). - **ORCH-1b** — та же философия resilience: транзиентный сбой не убивает задачу. - Эпик: звено **ORCH-54** (автономное внедрение). Параллельна ORCH-36 (разные файлы), но `max_concurrency=1` → встанет в очередь. ## 8. Риски (кратко; подробно — 10-tech-risks архитектора) - **Гонка sweeper ↔ живой webhook** → двойной запуск. Митигируется atomic claim + active-job guard + grace-период (не конкурировать с задержавшимся webhook). - **Spam нотификаций** при персистентно красном гейте на каждом тике. Митигируется: действие/нотификация только на изменении состояния (advance), не на каждый тик. - **Нагрузка на Plane API** при опросе каждые N сек. Митигируется интервалом + фильтром по статусам + per-project. - **Self-hosting:** sweeper правит инструмент, обслуживающий и другие проекты. Kill-switch обязателен.