---
work_item: ORCH-111
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-15
model_used: claude-opus-4-8
escalate: full-cycle
---

# 01 — BRD (бизнес-требования): ORCH-111 — watchdog должен алертить на долго живущие pytest/дочерние процессы, блокирующие конвейер

Work Item: **ORCH-111** · Repo: **orchestrator** · Стадия: analysis · Трек: **Bug → эскалация в полный цикл**

> **Эскалация (`escalate: full-cycle`).** Задача пришла как баг (`BUG:` в заголовке), но **не**
> является дешёвым багфиксом: закрытие пробела требует **архитектурного решения** — у sidecar-watchdog
> сейчас **нет видимости процессов хоста вообще** (`network_mode: host`, но без `pid: host` и без
> bind-mount `/proc`). Выбор механизма наблюдения (расширение привилегий sidecar vs обогащение
> контракта `/metrics` орком) — это развилка с последствиями для безопасности и стабильного контракта
> `/metrics` (schema_version, ORCH-099). Поэтому пакет — **полный** (а не lite-bug), и задача
> помечена `escalate: full-cycle`: нужен прогон стадии `architecture` + ADR (механизм видимости,
> эвристика детекции, привилегии/безопасность). Оператор снимает багфикс-трек эндпоинтом
> `POST /bug-fast-track/escalate?work_item=ORCH-111` (ADR-001 D5, ORCH-019).

## 1. Бизнес-контекст и проблема

### 1.1 Симптом (установленный факт)
На хосте прода были обнаружены старые зависшие процессы `python3 -m pytest tests/test_install_lite_script.py`,
которые жили **более 2 суток**, грузили CPU и мешали локальному merge-gate re-test. Из-за конкуренции
за CPU задача **ORCH-109 несколько раз упиралась** в `re-test timeout after 600s`
(`merge_gate.retest_branch`). Сами эти процессы **не были подняты как отдельный alert** watchdog'а —
оператор узнал о них случайно.

### 1.2 Локализация
- **Мониторинг-мозг** — sidecar-watchdog (ORCH-100, каталог `watchdog/`, сервис `orchestrator-watchdog`).
  Он уже алертит на `stage_stuck` (стадия задачи застряла) и `container_down` (контейнер не в норме),
  а также `agent_hung`, `orch_down`, `host_mem`, `queue_depth`, `job_failed`, `dep_down`.
- **Сигнал `agent_hung`** (`watchdog/signals.py::eval_envelope`) покрывает **только running-агент-джобы**:
  он читает раздел `agents[]` из `GET /metrics`, а тот строится `src/metrics.py::_build_agents` по
  `db.get_running_agents()` — то есть **только по `jobs.pid` активных джобов**.
- **Источник зависших процессов** — субпроцессы pytest, которые орк запускает в worktree:
  `merge_gate.retest_branch` (`python -m pytest <target>`) и `coverage_gate.measure_coverage`
  (`pytest --cov=src`). При `subprocess.TimeoutExpired` Python убивает прямого ребёнка, но
  **внуки/репарентированные процессы переживают**; а если сам агент-процесс убит по таймауту
  (`exit_code=-9`, ORCH-109) — его дочерний pytest **репарентируется на PID 1** и продолжает жить.

### 1.3 Причина (root cause)
Между двумя наблюдателями зияет **слепая зона**: `agent_hung` видит лишь *отслеживаемые* агент-джобы
(по `jobs.pid`), а **осиротевшие/внебюджетные тестовые субпроцессы** (внуки pytest, репарентированные
на PID 1) **не присутствуют ни в `/metrics`, ни в поле зрения sidecar** — у контейнера watchdog нет
доступа к таблице процессов хоста. Поэтому долго живущий pytest, реально блокирующий конвейер через
CPU-голодание merge-gate, **не порождает ни одного сигнала**, пока формально ни одна стадия задачи не
«застряла».

## 2. Объём (scope)

### В объёме
- Новый **отдельный класс алерта** watchdog'а: «долго живущий тестовый/дочерний процесс блокирует
  конвейер» — поднимается, даже если стадия задачи формально не `stuck`.
- Детекция долго живущих процессов тест-класса (pytest и родственные субпроцессы гейтов), переживших
  свой бюджет/осиротевших, на хосте прода.
- Актуализация конфиг-канона watchdog (`.env.watchdog.example` / блок `WATCHDOG_*` в `.env.example`)
  и наблюдаемости.

### Вне объёма
- **Любая реакция на процесс** (kill/SIGTERM/cleanup/reap/перезапуск). Задача — **только мониторинг +
  сигнализация** (явное ограничение заказчика). Автоматический reap осиротевших процессов — **отдельная
  задача**.
- Изменение конвейера: `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict / схема БД — **не
  трогаются** (watchdog — наблюдатель вне процесса орка и вне Quality Gates).
- Расширение `agent_hung` на нетреканые процессы (это другой класс сигнала; дубль запрещён — см. NFR-4).
- Снятие первопричины осиротения процессов в самом орке (надёжный reap внуков pytest) — ценно, но это
  **ремедиация**, она вне объёма этой задачи.

## 3. Заинтересованные стороны
- **Заказчик/оператор прода** (Слава) — получает ранний сигнал о CPU-голодании ещё до того, как оно
  завалит merge-gate re-test очередной задачи.
- **Self-hosting конвейер orchestrator** — страдает напрямую (инцидент ORCH-109).
- **Тиражные инсталляции (Lite/Bundled, ORCH-102/103)** — sidecar входит в дефолтный комплект; новый
  сигнал должен укладываться в канон конфига и не ломать тираж.
- **Принимает результат** — reviewer/tester + оператор (smoke на staging-эквиваленте sidecar).

## 4. Бизнес-требования (BR)
- **BR-1** — Watchdog поднимает **отдельный, узнаваемый** alert, когда на хосте обнаружен долго живущий
  процесс тест-класса (pytest и его субпроцессы), возраст которого превышает настраиваемый порог —
  **независимо** от того, застряла ли формально какая-либо стадия задачи.
- **BR-2** — Текст алерта **действенно идентифицирует** виновника: фрагмент командной строки, PID,
  возраст процесса и (при наличии) доля CPU — чтобы оператор мог сразу разобраться и вручную вмешаться.
- **BR-3** — **Только мониторинг + сигнализация.** Watchdog **не убивает / не останавливает / не шлёт
  сигналы** процессу и не выполняет иную ремедиацию (жёсткое ограничение заказчика, рамка C-1
  «наблюдатель строго read-only к наблюдаемому», ORCH-100).
- **BR-4** — **Без ложных срабатываний** на легитимных in-flight прогонах: тестовый процесс,
  принадлежащий **активному отслеживаемому** агенту/гейту в пределах его бюджета, alert поднимать
  **не должен**.
- **BR-5** — Анти-спам и recovery как у прочих сигналов: один alert на пересечение порога, throttled
  re-alert по cooldown, однократный recovery при исчезновении процесса (переиспользовать
  `watchdog/decision.py::decide` + `AlertState`).
- **BR-6** — Сигнал под **kill-switch** и управляется конфигом (порог возраста, cooldown, область).
  Дефолт выбирается так, чтобы включение было **осознанным** и **self-hosting-безопасным** (см. NFR-3).

## 5. Нефункциональные требования (NFR)
- **NFR-1 (надёжность)** — **never-raise** на всех новых путях (per-source / per-tick / per-send), как
  и весь watchdog: сбой коллектора процессов деградирует ОДИН сигнал, а не роняет тик.
- **NFR-2 (read-only)** — строго наблюдение: **ни одного** управляющего действия над процессами/хостом
  (нет `kill`/`signal`/`Popen`/записи). Соответствует C-1 (observer separated from observed).
- **NFR-3 (self-hosting безопасность)** — выкат изменения **не перезапускает** прод-контейнер
  `orchestrator` (встанет конвейер всех проектов): пересобирается/рестартится **только** контейнер
  `orchestrator-watchdog`. Если механизм требует расширения привилегий sidecar (напр. `pid: host`) —
  это привилегия **только наблюдателя**, обоснование и риски — задача архитектора (ADR).
- **NFR-4 (без дубля)** — новый сигнал **не пересекается** с `agent_hung` (тот уже покрывает
  отслеживаемые агент-джобы): новый сигнал закрывает ровно пробел «нетреканый/осиротевший процесс».
- **NFR-5 (канон тиража)** — при изменении compose / ключей `.env.watchdog` обновить в **том же PR**:
  `.env.watchdog.example`, блок `WATCHDOG_*` в `.env.example`, `docs/deployment/LITE_SETUP.md` и
  `docs/architecture/README.md` (норматив сопровождения ORCH-102 NFR-5; key-set-sync тест).
- **NFR-6 (стек)** — sidecar остаётся **stdlib-only** (C-3, ORCH-100): без новых сторонних зависимостей.

## 6. Допущения и ограничения
- **Ключевое архитектурное допущение (для архитектора):** у контейнера `orchestrator-watchdog` сейчас
  **нет** видимости процессов хоста (`network_mode: host`, но без `pid: host` и без mount `/proc`).
  Закрытие пробела требует выбора механизма — **развилка, решаемая ADR**, не аналитиком. Кандидаты
  (перечислены как материал для решения, **без навязывания**): (a) расширение привилегий sidecar —
  `pid: host` либо read-only mount хостового `/proc`, затем stdlib-скан таблицы процессов; (b)
  обогащение `/metrics` орком новым read-only разделом о «бесхозных» тест-субпроцессах (орк видит свой
  PID-namespace), который sidecar лишь читает. У каждого — свои trade-off'ы (привилегии vs контракт
  `/metrics`).
- `/metrics` — **версионированный контракт** (`schema_version`, ORCH-099): если выбран путь (b),
  аддитивные изменения **не бампят** версию (sidecar обязан толерировать).
- Порог возраста для детекции **должен превышать** максимальный легитимный бюджет тест-прогона
  (`merge_retest_timeout_s` ≈ 600s, `coverage_run_timeout_s`), чтобы нормальный прогон **никогда** не
  алертил, а 2-суточный осиротевший pytest — гарантированно (анти-false-positive, материал для ADR).
- enduro-trails не затронут: watchdog наблюдает хост/орк self-hosting; сигнал config-gated.

## 7. Критерии успеха
Watchdog при наличии долго живущего pytest/дочернего процесса, грузящего CPU, **поднимает отдельный
alert** в свой Telegram-канал (с PID/cmd/возрастом), **не трогая** процесс; при отсутствии такого
процесса (или выключенном флаге) — молчит; нормальный тест-прогон под активным джобом **не** триггерит
ложный alert. Детальные PASS/FAIL — `03-acceptance-criteria.md`.

## 8. Риски
- **Ложные срабатывания** на легитимном длинном прогоне → спам в канал (митигируется порогом >
  макс. бюджета + корреляцией с активным джобом).
- **Расширение привилегий sidecar** (если выбран `pid: host`/`/proc`-mount) → увеличение поверхности
  безопасности наблюдателя (требует явного обоснования в ADR; дефолт-off).
- **Дубль с `agent_hung`** при небрежной реализации (NFR-4).
- Детали и владельцы рисков — `10-tech-risks.md` (заполняет архитектор).
