--- work_item: ORCH-100 stage: analysis author_agent: analyst status: ready-for-review created_at: 2026-06-10 model_used: claude-opus-4-8 --- # 02 — ТЗ (TRZ): ORCH-100 — FND/F1b: sidecar-watchdog (мозг мониторинга, отдельный контейнер) Work Item: **ORCH-100** · Repo: **orchestrator** · Стадия: analysis > ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD (`01-brd.md`) и фактического > кода. Архитектурное обоснование/решения (выбор стека Python/Go, формат хранения порогов, владелец > диск-алерта, точная топология сети sidecar, бюджет памяти/`mem_limit`) — **зона архитектора** > (`06-adr/`). ТЗ фиксирует ТРЕБОВАНИЯ и ограничения, не способ реализации. ## 1. Сводка изменения Добавить **отдельный sidecar-контейнер** `orchestrator-watchdog`, код которого лежит в новой папке `watchdog/` репозитория орка, а рантайм — изолированный контейнер (свой `watchdog/Dockerfile` + сервис в `docker-compose.yml`). Sidecar периодически (тик): (1) тянет `GET /metrics` орка; (2) меряет хост (диск/inode/память/CPU); (3) читает статусы контейнеров через read-only `docker.sock`; (4) пингует Plane/Gitea/Anthropic. По набору **конфигурируемых порогов** через **чистую решающую функцию** (образец `disk_watchdog.decide`) принимает решение `alert | realert | recovery | none` с дедупом/ throttle, и шлёт алерт в **собственный** Telegram-канал (свой токен/chat, независимо от кода орка). Особый сигнал: `/metrics` не отвечает → алерт «орк не отвечает». Всё — never-raise, под kill-switch, строго read-only к наблюдаемому (self-hosting-безопасно). **Орк-сторона (`src/**`) не меняется**: F1b — потребитель уже существующего `GET /metrics` (F1a, ORCH-099). `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / схема БД орка — **не тронуты**. ## 2. Задействованные модули / пути | Путь | Действие | |------|----------| | `watchdog/` | **создать** — корень кода sidecar (новая папка в репо орка) | | `watchdog/Dockerfile` | **создать** — отдельный тонкий образ sidecar (стек — выбор архитектора) | | `watchdog/` | **создать** — демон/цикл сбора+решения+отправки (имя/структура — архитектор) | | `watchdog/` | **создать** — сбор: `/metrics` орка (HTTP), хост (диск/inode/память/CPU), контейнеры (`docker.sock` ro), пинг Plane/Gitea/Anthropic | | `watchdog/` | **создать** — **чистая** решающая функция порога `(value, threshold, prev_state, now, cooldown) → alert\|realert\|recovery\|none` (образец `src/disk_watchdog.py::decide`) | | `watchdog/` | **создать** — независимый Telegram-транспорт sidecar (свой токен/chat; НЕ импорт `src/notifications.py`) | | `watchdog/` | **создать** — чтение порогов/интервалов/токенов/kill-switch из env | | `watchdog/tests/` (или `tests/watchdog/`) | **создать** — pytest на чистые функции (решение/парсинг/детект орк-down); размещение — архитектор | | `docker-compose.yml` | **изменить** — добавить сервис `orchestrator-watchdog` (build `watchdog/`, restart-policy, read-only `docker.sock`, `mem_limit`, env, kill-switch) | | `.env.example` | **изменить** — канон: токен/chat watchdog + пороги + интервалы + kill-switch (без секретов) | | `CHANGELOG.md` | **изменить** — запись о F1b | | `docs/work-items/ORCH-100/07-infra-requirements.md` | **создать (architect)** — разовое инфра-действие: добавить сервис в compose, создать bot/chat watchdog, первый запуск на хосте | > **`src/**` НЕ редактируется.** Если в ходе разработки выяснится нехватка поля в `/metrics` — это > отдельная задача-расширение F1a (ORCH-099), а не правка в рамках F1b (см. BRD §«Вне объёма»). ## 3. Функциональные требования ### FR-1 — Отдельный контейнер sidecar (BR-1, NFR-1) Sidecar собирается из `watchdog/Dockerfile` в отдельный образ и поднимается сервисом `orchestrator-watchdog` в `docker-compose.yml`: отдельный процесс/память/рестарт-политика, **НЕ** внутри процесса орка. `restart: unless-stopped` (или эквивалент) — sidecar самовосстанавливается. ### FR-2 — Сбор сырья орка (BR-2, NFR-6) На каждом тике `GET ` (дефолт-достижимость `http://127.0.0.1:8500/metrics` при host-network; URL конфигурируем). Тело — версионированный конверт F1a: `{schema_version, generated_at, clk_tck, stages[], queue, agents[], cost, enabled}`. Парсинг **толерантен**: неизвестные поля игнорируются, отсутствие опционального — не ошибка, рост `schema_version` логируется (warning), не крэшит. Из конверта извлекаются сигналы для порогов: agent-liveness (cpu_ticks/runtime → «завис»), застрявшая стадия, job-failed, длина очереди. ### FR-3 — Детект «орк не отвечает» (BR-7) — главный сигнал Если `GET /metrics` завершается таймаутом / connection refused / 5xx / нечитаемым телом — это **отдельный сигнал тревоги** `orchestrator_down`. Проходит через ту же машину порога/дедупа/recovery (BR-9): один алерт «орк не отвечает», recovery при восстановлении. Единичный transient не должен немедленно флаппить — порог/таймаут/ретрай подбираются так, чтобы алерт был осмысленным (детали — архитектор/developer; требование: «не флаппить на одиночной сетевой икоте»). ### FR-4 — Сбор хоста (BR-3) Измерять заполнение диска (% и, где доступно, inode), память, CPU по доступным контейнеру хост-путям/интерфейсам (стдлиб-средствами выбранного стека; **без** тяжёлых агентов). Пути/пороги — конфигурируемы. **Диск:** см. FR-9 (анти-дубль с ORCH-063). ### FR-5 — Сбор контейнеров (BR-4, NFR-4) Через `docker.sock`, смонтированный **read-only**, читать состояния контейнеров платформы: различать Up / healthy / restarting / exited / unhealthy. Минимум — статус `orchestrator` (и других ключевых сервисов). **Только чтение** Docker API (list/inspect) — никаких start/stop/restart/exec. ### FR-6 — Пинг внешних зависимостей (BR-5) Периодически проверять доступность Plane / Gitea / Anthropic лёгким запросом (health/ping, короткий таймаут, never-raise). Недоступность → сигнал для порога. Эндпоинты/таймауты — конфигурируемы. ### FR-7 — Пороговый алертинг (BR-6, BR-9) Каждый сигнал проходит через **чистую решающую функцию** (образец `disk_watchdog.decide`): вход `(value/state, threshold, prev_state, now, cooldown)`, выход `alert | realert | recovery | none`. Семантика: - не-alerting & за порогом → **ALERT** (один на пересечение); - alerting & за порогом & cooldown истёк → **REALERT**; - alerting & за порогом & в cooldown → **NONE** (анти-спам); - alerting & вернулось в норму → **RECOVERY**; - не-alerting & в норме → **NONE**. Состояние порога (alerting/last_alert_at) — per-signal, in-memory (best-effort; рестарт sidecar сбрасывает → корректно повторно алертит ещё стоящую проблему, как `disk_watchdog`). Хранилище состояния/порогов (in-memory vs файл/иное) — **решение архитектора**. ### FR-8 — Независимый Telegram-транспорт (BR-8, NFR-4) Отправка через собственный код sidecar (свой ``), читающий **свои** `bot_token`/`chat_id` из env. **Запрещено** импортировать/вызывать `src/notifications.py` или использовать токен/функции орка (иначе падение орка утянет алерт-канал). `disable_web_page_preview`/`parse_mode` — по усмотрению; сообщение содержит суть алерта (сигнал, значение, порог, хост/контейнер). ### FR-9 — Анти-дубль диск-алерта (BR-10) Диск уже алертит `disk_watchdog` (ORCH-063, порог 85%, Telegram орка). F1b **не должен** слать второй диск-алерт на то же событие. **Владельца диск-алерта выбирает архитектор** (варианты: sidecar становится единственным владельцем и внутренний `disk_watchdog` остаётся как fallback на случай down-канала орка; ИЛИ sidecar не дублирует диск, оставляя его за ORCH-063). ТЗ фиксирует инвариант: **на одно событие переполнения диска — не более одного алерта**, решение и его обоснование — в `06-adr/`. ### FR-10 — Управляемость (NFR-5) Kill-switch (env): выключен → sidecar не стартует / инертен, нулевой эффект на орк и конвейер. Пороги (диск, память, agent-завис N мин, длина очереди, и т.п.), интервал тика, таймауты, cooldown — из env (`.env.example` — канон). ### FR-11 — never-raise (NFR-3) Три уровня: per-source (битый источник деградирует один сигнал, прочие собираются), per-tick (внешний try/except цикла), per-send (обёрнутая отправка). Демон не падает от ошибки сбора/сети/парсинга. ## 4. Изменения API **Нет** изменений API орка. Sidecar — **клиент** существующего `GET /metrics` (F1a, ORCH-099). Орк новых эндпоинтов не получает. Sidecar собственного входящего HTTP-API не обязан иметь (опциональный liveness-эндпоинт самого sidecar — на усмотрение архитектора, вне обязательного объёма). ## 5. Изменения схемы БД **Нет.** Sidecar **не пишет** в БД орка (NFR-4) и не имеет своей БД (тонкий стек, C-3). Состояние порогов — in-memory best-effort (FR-7). Журнал уроков (F2, БД орка) — отдельная задача, не F1b. ## 6. Требования к новым/изменённым QG checks **Нет.** F1b живёт **вне** процесса орка и **вне** конвейера Quality Gate. `QG_CHECKS` / `check_*` / `STAGE_TRANSITIONS` — **не тронуты** (по образцу operational-демонов `disk_watchdog`/`reaper`/ `reconciler`, которые тоже не являются Quality Gate). Sidecar — операционный наблюдатель, не гейт. ## 7. Совместимость / регресс - **Обратная совместимость:** изменения **аддитивны** — новая папка `watchdog/`, новый сервис в compose, новые ключи в `.env.example`. Существующий орк-контейнер и его поведение — без изменений. - **Kill-switch:** выключенный sidecar = нулевой эффект (не стартует), полная обратимость (NFR-5). - **Область раската:** только инфраструктура наблюдения; конвейер всех проектов не затронут (self-hosting-безопасно, NFR-4). - **Регресс:** существующий `pytest tests/` остаётся зелёным; новые тесты sidecar добавляются изолированно (FR — чистые функции тестируемы без контейнера/таймера, образец `tests/` для `disk_watchdog.decide`). - **Разовое инфра-предусловие** (не код): добавить сервис в compose + создать bot/chat watchdog + первый запуск на хосте (Слава/Стрим). Зафиксировать в `07-infra-requirements.md`. Отсутствие bot/chat watchdog = sidecar не шлёт (fail-safe, логирует), но не падает.