--- work_item: ORCH-114 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-15 model_used: claude-opus-4-8 escalate: full-cycle --- # 02 — ТЗ (TRZ): ORCH-114 — Ownership-lease для side-effectful переходов стадий + умное восстановление при старте Work Item: **ORCH-114** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **конкретные требования к реализации**, выведенные из BRD (`01-brd.md`) и фактического > кода. **Выбор механизма** (durable lease / heartbeat / transition-epoch / форма хранения, набор > покрываемых рёбер) и архитектурное обоснование — задача архитектора (`06-adr/`). Здесь — *что* > должно быть истинно и *какие модули* затрагиваются, не *как* именно. ## 1. Сводка изменения Вводится **единый инвариант владения** side-effectful переходом стадии: запись стадии и исполнение тяжёлых под-гейтов/финализации защищаются **durable-механизмом владения** (lease/epoch) + **CAS** на запись стадии, так что в любой момент переход конкретной задачи доводит **ровно один** актор, а конкурентный/после-рестартовый повторный вход (reaper / reconciler / webhook / startup-requeue) **откладывается или становится no-op** вместо повторного применения необратимых эффектов (merge_pr / coverage-ratchet / image-rebuild / инициация прод-деплоя / противоречивый rollback↔done). Аддитивно, под kill-switch, never-raise; `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / вердикт-ключи / схемы существующих таблиц — не трогаются (обобщает и делает durable процесс-локальный `finalizer_liveness` ORCH-113). ## 2. Задействованные модули / пути | Путь | Действие | Назначение в ORCH-114 | |------|----------|------------------------| | `src/stage_engine.py` | изменить | `advance_stage`: захват владения на границе side-effectful перехода/финализации; CAS на запись стадии; release в `try/finally`; проигравший — чистый аборт. Покрыть `_handle_merge_verify`, под-гейты `deploy-staging→deploy`, `run_deploy_finalizer` (Phase C), `advance_if_gate_passed` (F-1). | | `src/db.py` | изменить | CAS-вариант записи стадии (запись только при совпадении ожидаемой текущей стадии/эпохи; rowcount-результат). Durable-хелперы владения (acquire / heartbeat-touch / release / reclaim / snapshot) — форму хранилища задаёт архитектор. | | `src/finalizer_liveness.py` | изменить/обобщить | Отправная точка: обобщить process-local реестр до **durable, кросс-путевого** владения (или надстроить durable-слой поверх). Сохранить контракт never-raise + kill-switch. | | `src/job_reaper.py` | изменить | Tier-2/Tier-3 осведомлены о durable-владении на **всех** релевантных путях (не только Tier-2/`deploy-staging`): defer при живом, реклейм мёртвого/устаревшего в ограниченное время (NFR-6). | | `src/queue_worker.py` | изменить | `requeue_running_jobs` / стартовое восстановление сверяется с durable-владением: не пере-исполнять уже применённый необратимый шаг (умное восстановление). `claim_next_job` — не вносить сетевых зависимостей. | | `src/reconciler.py` | изменить | F-1 (`advance_if_gate_passed`) — defer при активном lease перехода (по образцу skip-guard'ов escalated/Blocked/deps). | | `src/webhooks/plane.py` | изменить | Пути продвижения (`_try_advance_stage`, Approved / Confirm Deploy) — defer при активном lease. | | `src/webhooks/gitea.py` | изменить (учесть) | Прямые записи стадии в обход `advance_stage` (`handle_push`/`handle_ci_status`/`handle_pr`) должны попадать под тот же CAS-инвариант либо явно исключаться архитектором (граница в ADR). | | `src/main.py` | изменить | Порядок старта демонов / точка восстановления; read-only блок в `GET /queue`; опц. operator-эндпоинт реклейма. | | `src/config.py` | изменить | Kill-switch(и) + бюджеты/таймауты владения + (опц.) CSV-скоуп репо. | | `tests/test_orch114_transition_ownership.py` | создать | Покрытие FR-1…FR-7 (см. `04-test-plan.yaml`). | ## 3. Функциональные требования ### FR-1 — Единое владение side-effectful переходом (BR-1, BR-6) На границе, где начинается side-effectful финализация/переход (минимум: под-гейты `deploy-staging→deploy`, `_handle_merge_verify` на `deploy→done`, Phase C `run_deploy_finalizer`), актор **захватывает владение** задачей. Пока владение активно, другой актор не исполняет тот же переход. Release — детерминированно в `try/finally` (в т.ч. на исключении/откате). Владение **durable** (NFR-4): переживает рестарт и доступно для проверки другому актору/новому процессу. ### FR-2 — Compare-and-swap / epoch на запись стадии (BR-2) Запись стадии для side-effectful переходов выполняется только если предусловие (ожидаемая текущая стадия и/или эпоха перехода) не изменилось с момента чтения. Реализуется через CAS-вариант `update_task_stage` (`UPDATE … SET stage=?[, epoch=epoch+1] WHERE id=? AND stage=?[ AND epoch=?]`, решение по форме — архитектор). Проигравший гонку писатель получает «lost-race» результат, **не** мутирует стадию и **не** выполняет ни одного побочного эффекта (merge_pr / ratchet / rebuild / deploy-init / enqueue следующего агента). Инвариант распространяется и на пути, пишущие стадию в обход `advance_stage` (gitea-webhook), — либо CAS, либо явное исключение (граница в ADR). ### FR-3 — Reaper, осведомлённый о владении на всех путях (BR-3, NFR-6) Job-reaper перед реклеймом сверяется с durable-владением **не только** в Tier-2 для `deploy-staging` (текущая область ORCH-113), а на всех релевантных тирах/рёбрах: **живой** владелец → **defer** (лог + счётчик, без повторного advance); **мёртвый/устаревший** владелец → реклейм в ограниченное время (Tier-3 backstop `reaper_max_running_s` добивает зависшего; маркер владения backstop не обходит инвариант бюджета). Сохранить атомарный `reap_running_job` rowcount-guard. ### FR-4 — Умное восстановление при старте (BR-4, BR-6, NFR-7) Стартовое восстановление (`requeue_running_jobs` + последующий цикл) использует durable-владение/эпоху, чтобы **детерминированно** различить: (a) финализация не начиналась / безопасно перезапустить → re-drive; (b) необратимый шаг уже применён (мерж в `main` / ratchet / прод-деплой инициирован) → **сойтись к done/консистентному исходу без повторного применения**. Источник истины для «уже применено» — авторитетные durable-факты (SHA-in-main ORCH-071/073, маркер `INITIATED` self-deploy, durable-lease/эпоха), а не in-memory состояние. ### FR-5 — Skip/defer в reconciler и webhook (BR-5) Reconciler F-1 (`advance_if_gate_passed`) и webhook-пути продвижения (`plane._try_advance_stage`, Approved/Confirm Deploy) при **активном** lease перехода для задачи **откладывают** действие (silent skip + наблюдаемость), по образцу существующих skip-guard'ов F-1 (escalated / Blocked / task-deps). Fail-safe: неопределённость состояния lease → консервативный skip (не дублировать). ### FR-6 — Наблюдаемость (BR-7) Аддитивный read-only блок в `GET /queue` (по образцу `serial_gate`/`merge_gate`/`reaper`): держатели lease, возраст владения, defer-счётчики, форсированные/устаревшие реклеймы. Алерт (`send_telegram`, кликабельный номер) на форсированный/устаревший реклейм. Опц. запись в lessons-journal (ORCH-098, `source="auto"`). Опц. operator-эндпоинт ручного реклейма (по образцу `POST /serial-gate/unfreeze`). ### FR-7 — Конфигурация, обратимость, never-raise (BR-9, NFR-1, NFR-2, NFR-8) Все публичные функции владения — never-raise (ошибка → безопасный дефолт + WARNING). Kill-switch возвращает поведение **байт-в-байт** к до-ORCH-114 (lease не пишется/не читается, CAS вырождается в прежний безусловный `update_task_stage`). Hot-path — fail-open; prod-safety — fail-closed. ## 4. Изменения API - **`GET /queue`** — аддитивный read-only блок владения переходом (имя ключа уточнит архитектор, напр. `transition_ownership` / `transition_lease`). Существующие ключи `/queue` — байт-в-байт. - **(Опционально, по решению архитектора)** `POST /transition-lease/release?work_item=` — операторский ручной реклейм застрявшего владения (паттерн `POST /serial-gate/unfreeze`). - `GET /metrics` (ORCH-099) — при необходимости аддитивное поле без бампа `schema_version` (sidecar обязан толерировать незнакомые ключи). Прочие эндпоинты — не трогаются. ## 5. Изменения схемы БД **Требование (форму выбирает архитектор, `06-adr/` + `08-data-requirements.md`):** для durable-владения (NFR-4) и CAS/epoch (FR-2) требуется **аддитивное, идемпотентное** durable-состояние. Кандидаты (не предписание): - доп. аддитивная таблица владения (`CREATE TABLE IF NOT EXISTS`, паттерн `repo_freeze`/ `coverage_baseline`/`lessons`) с `(task_id/job_id, owner, run_id, stage, acquired_at, heartbeat_at, expires_at)`; **либо** - аддитивные колонки на `tasks`/`jobs` (`_ensure_column`, паттерн `tasks.track`/`tasks.cancelled_at`), включая возможную `epoch/version`-колонку для CAS. **Жёсткие ограничения (NFR-3):** только аддитивно/идемпотентно; **схемы существующих таблиц (`tasks`/`jobs`/`agent_runs` и пр.) — байт-в-байт**; никаких изменений существующих столбцов/индексов, ломающих обратную совместимость; restart-safe инициализация в `init_db()`. ## 6. Требования к новым/изменённым QG checks **Нет.** `QG_CHECKS` / `check_*` / `_parse_*` / машинные вердикт-ключи — **не трогаются**. Владение переходом — свойство **движка переходов и фоновых акторов**, а **не** Quality Gate и **не** стадия (аналогично тому, как merge-lease/serial-gate/finalizer-liveness — врезки/leaf'ы, а не QG). Никаких новых стадий/рёбер в `STAGE_TRANSITIONS`. ## 7. Совместимость / регресс - **Kill-switch** (новый флаг в `config.py`, env `ORCH_*`): `False` → CAS вырождается в прежний безусловный `update_task_stage`, lease не пишется/не читается, reaper/reconciler/webhook ведут себя как до ORCH-114 — **байт-в-байт** (включая зелёный существующий `pytest tests/`). - **Область раската:** по образцу leaf-гейтов; durable-lease минимально применяется к self-hosting рёбрам (`deploy-staging`/`deploy→done`, где живут необратимые эффекты); generic-CAS инертен при отсутствии гонки (нулевая стоимость на не-затронутых переходах). Точную область фиксирует архитектор. - **Обратимость:** механизм аддитивен и изолирован; откат = выключить kill-switch (durable-таблица/ колонки остаются инертными). - **never-raise / fail-open / fail-closed:** hot-path claim/guard — fail-open (не клинить общую очередь, AC-8 ORCH-088); prod-safety/необратимость — fail-closed; любой сбой механизма — WARNING + безопасный дефолт. - **Сквозной бюджет:** lease согласован с `reaper_max_running_s`/`reaper_finalize_grace_s` (ORCH-065/ 109/110/113) — не удлиняет финализацию за backstop; устаревший владелец добивается в ограниченное время. - **Маркеры трассировки (ORCH-078):** правки в блоках с маркерами ORCH-065/109/110/111/113 (reaper, finalizer-liveness, merge-gate) сверяются с их ADR перед изменением; новый код помечается `ORCH-114`. - **enduro-trails:** при флаге off / репо вне области — нулевая регрессия.