From 453c5b7d0426be4a0997c9c9a2f37db5e8d10e39 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 17 Jun 2026 11:07:33 +0300 Subject: [PATCH] analyst(ET): auto-commit from analyst run_id=773 --- docs/work-items/ORCH-126/01-brd.md | 139 ++++++++++++++++++ docs/work-items/ORCH-126/02-trz.md | 97 ++++++++++++ .../ORCH-126/03-acceptance-criteria.md | 107 ++++++++++++++ docs/work-items/ORCH-126/04-test-plan.yaml | 102 +++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 docs/work-items/ORCH-126/01-brd.md create mode 100644 docs/work-items/ORCH-126/02-trz.md create mode 100644 docs/work-items/ORCH-126/03-acceptance-criteria.md create mode 100644 docs/work-items/ORCH-126/04-test-plan.yaml diff --git a/docs/work-items/ORCH-126/01-brd.md b/docs/work-items/ORCH-126/01-brd.md new file mode 100644 index 0000000..022f907 --- /dev/null +++ b/docs/work-items/ORCH-126/01-brd.md @@ -0,0 +1,139 @@ +--- +work_item: ORCH-126 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-17 +model_used: claude-opus-4-8 +track: 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`: + +1. **Окно 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`. +2. **pid переиспользован** (вероятно после рестарта контейнера) → `pid_alive(stale)=True` → reaper + «видит живой процесс» и **никогда не реапит** действительно застрявшую строку; при + `max_concurrency=1` (дефолт) этот фантомный `running` блокирует клейм **всей** очереди. +3. **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` (на укороченном маршруте не обязателен). diff --git a/docs/work-items/ORCH-126/02-trz.md b/docs/work-items/ORCH-126/02-trz.md new file mode 100644 index 0000000..ff139b8 --- /dev/null +++ b/docs/work-items/ORCH-126/02-trz.md @@ -0,0 +1,97 @@ +--- +work_item: ORCH-126 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-17 +model_used: claude-opus-4-8 +track: bug +--- + +# 02 — ТЗ (TRZ): ORCH-126 — гигиена run-ownership queued-job + диагностика невозможных состояний + +Work Item: **ORCH-126** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug** + +> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода. +> «Как именно» (точка сброса, форма диагностики) — в пределах FR; финальный выбор — за developer. +> Архитектурного решения/ADR не требуется (укороченный маршрут ORCH-019). + +## 1. Сводка изменения +Сбрасывать run-ownership строки `jobs` (`run_id`, `pid`) **во всех путях возврата job'а в `queued`** +и/или в момент claim **до** `_spawn`, чтобы (а) queued-job никогда не нёс протухший `run_id`/`pid`, +(б) свежеклеймленный-ещё-не-заспавненный job не нёс stale `pid`, который job-reaper примет за чужой +живой/мёртвый процесс. Плюс — детектор «невозможного» queued-состояния: авто-санация при старте/реапе +**и** наблюдаемость (лог + счётчик в `GET /queue`). Схема БД и контракты гейтов не меняются. + +## 2. Задействованные модули / пути +| Путь | Действие | +|------|----------| +| `src/db.py` | изменить: `requeue_running_jobs` / `mark_job('queued')` / `mark_job_transient` / `reap_running_job('queued')` — добавить сброс `run_id=NULL, pid=NULL`; `claim_next_job` — сброс stale `pid` (и `run_id`) при флипе в `running` | +| `src/agents/launcher.py` | проверить окно `_spawn` (claim→`run_id`/`started_at`→`Popen`→`pid`): убедиться, что при провале `_spawn` до строки 711 job не остаётся со stale `pid` (опирается на сброс в `claim`/`mark_job`) | +| `src/job_reaper.py` | (опц., по выбору developer) Tier-1 анти-false-positive: `pid IS NULL` у свежего `running` уже трактуется как «нет pid → не реапить»; добавить авто-санацию/счётчик невозможных queued-строк, если фикс на стороне reaper | +| `src/main.py` | (опц.) при старте после `requeue_running_jobs` — лог/санация обнаруженных невозможных queued-состояний (наблюдаемость BR-4) | +| `tests/test_orch126_queued_stale_run.py` | создать: регресс + покрытие FR-1..FR-4 | + +## 3. Функциональные требования + +### FR-1 — Сброс run-ownership на всех путях возврата в `queued` (BR-2) +Каждый путь, переводящий job в `queued`, обязан выставить `run_id=NULL` **и** `pid=NULL` той же +UPDATE-транзакцией, что уже чистит `started_at`/`finished_at`: +`db.requeue_running_jobs()` (restart-recovery), `db.mark_job(status='queued')`, +`db.mark_job_transient()`, `db.reap_running_job(status='queued')`. +Инвариант: **`status='queued' ⇒ run_id IS NULL AND pid IS NULL AND started_at IS NULL`**. +Атомарные guard'ы по `status` (`reap_running_job ... WHERE status='running'`) — сохранить байт-в-байт. + +### FR-2 — Чистый claim (BR-1, BR-3) +`db.claim_next_job` при флипе `queued→running` не должен оставлять stale `pid` (и `run_id`) от прошлой +попытки: либо сбросить их в том же UPDATE (`pid=NULL, run_id=NULL`), либо опираться на FR-1 (тогда +queued-job их уже не несёт). Defense-in-depth (оба) — предпочтительно. SELECT-гейт +(`status='queued' AND available_at<=now` + dep/serial-gate) — **не трогать** (NFR-2 offline). +Результат: между claim и стампом `pid` в `_spawn` job имеет `pid IS NULL` (не чужой pid). + +### FR-3 — Безопасность окна `_spawn` (BR-3) +Если `_spawn` падает **до** стампа `pid` (`launcher.py:711`) — `ensure_worktree`/ +`_materialize_deferred_branch`/`_write_task_file`, — обработчик `queue_worker._drain_once` +(`mark_job('queued'|'failed')`) обязан, по FR-1, оставить job без stale `pid`. Проверить, что +повторный claim после такого провала стартует штатно (а не оседает «частично стартовавшим»). + +### FR-4 — Детект и обработка невозможного состояния (BR-4) +«Невозможное» queued-состояние = `status='queued' AND (run_id IS NOT NULL OR pid IS NOT NULL OR +started_at IS NOT NULL)`. Поведение: +- **Авто-санация** при старте (`main.lifespan` после `requeue_running_jobs`) и/или при реап-тике — + привести такие строки к чистому `queued` (FR-1) идемпотентно, never-raise. +- **Наблюдаемость** — структурный WARNING с `job_id`/`run_id`/`pid` + read-only счётчик в блоке + очереди `GET /queue` (например `queue.impossible_queued` или поле в существующем снимке worker'а). + +### FR-5 — Корректность reaper-liveness (BR-1, NFR-5) +После FR-1..FR-3 job-reaper (ORCH-065) на свежеклеймленном `running` видит `pid IS NULL` → Tier-1 +не копит dead-тики против чужого pid и не реапит легитимный старт; фантомный «живой» pid не блокирует +очередь. Инварианты ORCH-065/113/114 (Tier-2 finalize-grace, finalizer-liveness, transition-lease) — +не нарушать. + +## 4. Изменения API +Нет новых эндпоинтов. **Расширение наблюдаемости** read-only снимка `GET /queue` — добавить +счётчик/индикатор обнаруженных и санированных невозможных queued-состояний (BR-4); существующие поля +снимка не переименовывать. + +## 5. Изменения схемы БД +**Нет.** Колонки `jobs.run_id` / `jobs.pid` / `jobs.started_at` уже существуют; фикс — корректное +заполнение/сброс. Никаких `_ensure_column`/новых таблиц/индексов. + +## 6. Требования к новым/изменённым QG checks +**Нет.** `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / machine-verdict-ключи — байт-в-байт. +Дефект — свойство гигиены данных планировщика, не Quality Gate. + +## 7. Совместимость / регресс +- **Обратная совместимость:** для не-stale job'ов поведение байт-в-байт (они и так не несут + `run_id`/`pid` в `queued`); фикс лишь нормализует аномальные строки. +- **Область:** общий планировщик/очередь (не self-hosting-scoped leaf) — затрагивает все проекты, но + семантически нейтрально (приведение к уже-документированному инварианту «queued = без run-ownership»). +- **Kill-switch:** правка — исправление инварианта данных, не новая фича; отдельный флаг не требуется. + Опциональную авто-санацию/диагностику (FR-4) допустимо закрыть под флаг, если developer сочтёт + нужным (дефолт = включено), но базовый сброс FR-1..FR-3 — безусловен. +- **Обратимость:** изменения локальны (UPDATE-наборы в `db.py`); откат — ревертом PR. +- **Миграция:** не требуется; существующие аномальные строки санируются при первом старте (FR-4). +- **Трассировка (CLAUDE.md §9):** перед правкой `pid`/`run_id`-логики прочитать ADR ORCH-065 + (reaper Tier-1), ORCH-113 (finalizer-liveness), ORCH-114 (transition-lease/`recover_on_startup`), + ORCH-099 (`/metrics`); не сломать их инварианты. diff --git a/docs/work-items/ORCH-126/03-acceptance-criteria.md b/docs/work-items/ORCH-126/03-acceptance-criteria.md new file mode 100644 index 0000000..1c0f712 --- /dev/null +++ b/docs/work-items/ORCH-126/03-acceptance-criteria.md @@ -0,0 +1,107 @@ +--- +work_item: ORCH-126 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-17 +model_used: claude-opus-4-8 +track: bug +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-126 — гигиена run-ownership queued-job + +Work Item: **ORCH-126** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug** + +Формат: каждый критерий имеет **PASS** / **FAIL**. Reviewer проверяет буквально по файлам репозитория +и прогону тестов. + +--- + +## AC-1 — Инвариант чистого queued после возврата + +**Условие:** job возвращён в `queued` любым путём — `requeue_running_jobs` (restart), +`mark_job('queued')` (retry), `mark_job_transient` (transient), `reap_running_job('queued')` (reaper). +- **PASS:** после операции строка имеет `run_id IS NULL` **И** `pid IS NULL` **И** `started_at IS NULL`. +- **FAIL:** хотя бы один из `run_id`/`pid`/`started_at` остаётся непустым у `status='queued'`. + +--- + +## AC-2 — Клейм при выключенном serial-gate стартует штатно + +**Условие:** `ORCH_SERIAL_GATE_ENABLED=false`, есть один валидный queued ORCH-job, `running=0`, +`max_concurrency≥1`. +- **PASS:** `claim_next_job` выбирает job, флипает в `running`, `_spawn` стартует; задача не зависает. +- **FAIL:** job остаётся `queued` при свободном слоте (повтор `queued=1, running=0`). + +--- + +## AC-3 — Свежеклеймленный job не несёт чужой pid + +**Условие:** job клеймится из `queued`, который ранее нёс stale `pid`/`run_id`; до выполнения стампа +`pid` в `_spawn`. +- **PASS:** сразу после `claim_next_job` (до `_spawn` Popen-стампа) `jobs.pid IS NULL` (не stale). +- **FAIL:** после claim строка несёт `pid` от прошлой попытки. + +--- + +## AC-4 — Reaper не реапит легитимный старт по чужому pid + +**Условие:** job только что склеймлен (`running`, `started_at=now`, `pid IS NULL`), job-reaper +делает тик. +- **PASS:** reaper не считает строку мёртвой (Tier-1 пропускает `pid IS NULL`); не возвращает её в + `queued`/`failed`; легитимный старт не сбивается. Фантомный «живой» stale-pid больше не блокирует + клейм очереди. +- **FAIL:** reaper реапит стартующий job по чужому pid, либо вечно держит фантомный `running`. + +--- + +## AC-5 — Невозможное queued-состояние авто-санируется / явно видно + +**Условие:** в БД присутствует строка `status='queued'` с непустым `run_id`/`pid`/`started_at` +(например после апгрейда на проблемной БД или гонки). +- **PASS:** состояние приводится к чистому `queued` (авто-санация при старте/реапе, идемпотентно, + never-raise) **и/или** явно сигнализируется (структурный WARNING с `job_id`/`run_id`/`pid` + + счётчик в `GET /queue`). +- **FAIL:** аномальная строка остаётся незамеченной и неисправленной. + +--- + +## AC-6 — Окно `_spawn` устойчиво к провалу до стампа pid + +**Условие:** `_spawn` падает до стампа `pid` (`ensure_worktree`/материализация ветки/запись task-файла); +job уходит назад в `queued` обработчиком `_drain_once`. +- **PASS:** строка в `queued` чистая (AC-1); повторный claim стартует штатно; нет «частично + стартовавшего» зависания. +- **FAIL:** после провала job несёт stale `pid`/`run_id` или не клеймится повторно. + +--- + +## AC-7 — Без регрессов контрактов и схемы + +**Условие:** прогон полного `pytest tests/ -q`; проверка диффа. +- **PASS:** все тесты зелёные; `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / `check_*` / + machine-verdict-ключи / схема БД (`jobs`) — без изменений; новых колонок/таблиц нет; + поведение для не-stale job'ов неизменно; enduro не затронут. +- **FAIL:** падение тестов, изменён контракт гейта/схема БД, или регресс для здоровых job'ов. + +--- + +## AC-8 — Обязательный регресс-тест красный→зелёный + +**Условие:** тест из `04-test-plan.yaml`, фиксирующий stale-состояние и/или старвейшн. +- **PASS:** тест красный на коде ДО фикса, зелёный ПОСЛЕ. +- **FAIL:** тест зелёный до фикса (не воспроизводит баг) или красный после. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-2 / FR-1 | +| AC-2 | BR-1 / FR-2 | +| AC-3 | BR-3 / FR-2 | +| AC-4 | BR-1, BR-3 / FR-5 | +| AC-5 | BR-4 / FR-4 | +| AC-6 | BR-3 / FR-3 | +| AC-7 | NFR-1..NFR-4 | +| AC-8 | BR-5 / FR-1..FR-4 | diff --git a/docs/work-items/ORCH-126/04-test-plan.yaml b/docs/work-items/ORCH-126/04-test-plan.yaml new file mode 100644 index 0000000..c9a583a --- /dev/null +++ b/docs/work-items/ORCH-126/04-test-plan.yaml @@ -0,0 +1,102 @@ +work_item: ORCH-126 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-17 +model_used: claude-opus-4-8 +title: "ORCH-126 — гигиена run-ownership queued-job + диагностика невозможных состояний" +framework: pytest +track: bug +scope: > + Покрывает: сброс run_id/pid на всех путях возврата job в queued (restart/retry/transient/reap), + чистый claim без stale pid, устойчивость окна _spawn, детект/санацию невозможных queued-состояний, + корректность Tier-1 reaper-liveness. Вне покрытия: семантика serial-gate (ORCH-088/124) и dep-gate + (ORCH-026), переписывание reaper/transition-lease, изменение схемы БД. +notes: > + TC-01 — ОБЯЗАТЕЛЬНЫЙ регресс-тест бага: красный на коде ДО фикса, зелёный ПОСЛЕ (BR-5 / AC-8). + Тесты используют изолированную временную SQLite-БД (db.init_db во временной директории), без сети, + без Claude CLI, без реального Popen (spawn мокается/подменяется). Полный регресс pytest tests/ -q + должен оставаться зелёным (AC-7). + +tests: + - id: TC-01 + type: integration + description: > + РЕГРЕСС (red→green): job в running со stale run_id+pid (started_at set) проходит + requeue_running_jobs() (имитация рестарта); ассерт — строка queued с run_id IS NULL, + pid IS NULL, started_at IS NULL. До фикса pid/run_id остаются (красный), после — чисто. + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-02 + type: unit + description: > + mark_job(job_id, 'queued') сбрасывает run_id и pid (а не только started_at/finished_at). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-03 + type: unit + description: > + mark_job_transient(...) на job со stale run_id/pid возвращает чистый queued (run_id/pid NULL), + сохраняя инкремент transient_attempts и backoff available_at. + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-04 + type: unit + description: > + reap_running_job(job_id, 'queued', ...) сбрасывает run_id/pid; атомарный guard + WHERE status='running' сохранён (повторный вызов на уже-queued строке -> rowcount 0). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-05 + type: integration + description: > + claim_next_job() флипает queued->running и не оставляет stale pid: сразу после claim + jobs.pid IS NULL (до _spawn-стампа). Подтверждает чистый старт (AC-3). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-06 + type: integration + description: > + При выключенном serial-gate (ORCH_SERIAL_GATE_ENABLED=false) валидный queued ORCH-job + выбирается claim_next_job при running=0 и не зависает (AC-2). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-07 + type: integration + description: > + Reaper-тик на свежеклеймленном running с pid IS NULL не реапит строку (Tier-1 пропускает + pid IS NULL), легитимный старт сохраняется (AC-4). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-08 + type: integration + description: > + Детект/санация невозможного состояния: предзаписанная строка queued с непустым pid/run_id/ + started_at приводится к чистому queued (авто-санация при старте/реапе) И/ИЛИ фиксируется + счётчиком в снимке очереди; операция идемпотентна и never-raise (AC-5). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-09 + type: integration + description: > + Окно _spawn: при провале launch (launch_job/_spawn бросает до стампа pid) обработчик + _drain_once возвращает job в queued чистым (pid/run_id NULL); повторный claim стартует + штатно (AC-6). + module: tests/test_orch126_queued_stale_run.py + expected: PASS + + - id: TC-10 + type: unit + description: > + Анти-регресс: для здорового job (никогда не стартовал) поведение mark_job/claim байт-в-байт; + терминальные исходы (done/failed/cancelled) и стамп finished_at не затронуты сбросом ownership. + module: tests/test_orch126_queued_stale_run.py + expected: PASS