From 194d6c820e10abdb9fb997811288a19f8e04e8ba Mon Sep 17 00:00:00 2001 From: claude-bot Date: Tue, 9 Jun 2026 08:31:45 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=426 --- ...ADR-001-tracker-orphan-sweep-and-effort.md | 242 ++++++++++++++++++ .../ORCH-087/08-data-requirements.md | 60 +++++ docs/work-items/ORCH-087/10-tech-risks.md | 20 ++ 3 files changed, 322 insertions(+) create mode 100644 docs/work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-sweep-and-effort.md create mode 100644 docs/work-items/ORCH-087/08-data-requirements.md create mode 100644 docs/work-items/ORCH-087/10-tech-risks.md diff --git a/docs/work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-sweep-and-effort.md b/docs/work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-sweep-and-effort.md new file mode 100644 index 0000000..161c0b1 --- /dev/null +++ b/docs/work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-sweep-and-effort.md @@ -0,0 +1,242 @@ +# ADR-001: Anti-orphan bump (реестр message_id + sweep) + эффорт в строке стадии + +## Статус +Accepted + +## Контекст + +Live-трекер ведёт **одну карточку на задачу** (`update_task_tracker`, `src/notifications.py`). +Дефолтный режим — `bump` (ORCH-067): на каждом обновлении карточка пересоздаётся внизу чата +(`delete_telegram(old) → send_telegram(new) → set_tracker_message_id(new)`). Указатель +`tasks.tracker_message_id` хранит **ровно один** message_id — последний. + +**Симптом (Слава, 08.06, ORCH-082):** в чате висела карточка с заголовком «📍 To Analyse», +хотя задача реально была на стадии `deploy` (все стадии ✅ вплоть до «Внедрение»). Параллельно +актуальной карточки с deploy-статусом не было видно. + +BRD §3 требует G0: **установить точную механику бага по данным, до фикса**. Ниже — вывод G0, +заземлённый на разборе фактического кода (`src/notifications.py` ревизии ветки) и топологии +вызовов, а не на предположениях. Этот ADR — вход в реализацию; фикс вслепую запрещён. + +--- + +## G0 — расследование (ответы на R-1…R-4) + +### Метод +Источник истины для механики — **код + топология вызовов**, она детерминирована и проверяема. +Полевые данные из BRD (message_id `18204`, `18227`; `deleteMessage → ok:true` для обоих) служат +подтверждающими отметками: они доказывают, что (1) карточек одной задачи в чате было **минимум +две** и (2) бот **имеет право** удалять — значит причина сирот НЕ в правах, а в **потере ссылки** +на message_id. Точная механика выводится из кода ниже; staging-воспроизведение R-3 — предмет +верификации на стадии тестирования (см. AC-0.1 / тест-план TC-13). + +### R-1. Сколько реально карточек висело +**≥ 2** подтверждено данными BRD (18204 «To Analyse» + 18227). Структурно реестра нет → точное +число сирот, накопленных за жизнь задачи, **не восстановимо постфактум** — это и есть корневой +дефект наблюдаемости (нет учёта всех созданных message_id). Именно поэтому фикс вводит реестр +(`tracker_messages`): он делает число карточек задачи измеримым и управляемым. + +### R-2. В какие моменты `tracker_message_id` рассинхронизируется — по пунктам (a–e) + +Разбор ветки `mode == "bump"` (`update_task_tracker`, стр. 576–586): + +```python +if mid is not None: + delete_telegram(mid) # best-effort; результат НЕ гейтит send +new_mid = send_telegram(text, disable_notification=True) +if new_mid is not None: + set_tracker_message_id(task_id, new_mid) +``` + +| Пункт BRD | Вердикт | Механика | +|---|---|---| +| **(a)** `send` вернул `None` | **НЕ источник сирот** | `set_tracker_message_id` не вызывается → указатель цел (это и есть защита AC-3). `delete(old)` мог уже убрать карточку → временно 0 карточек, перерисовка на следующем апдейте. Сирота не создаётся. | +| **(b)** рестарт между `delete` и `send` | **Частично / не основной** | Если `delete` успел (old удалён), `send` не успел → указатель = old(мёртв). Следующий апдейт: `delete(old)→gone`, `send(new)`. Сирота **не** появляется. **НО** рестарт между `send` (new жив) и `set_tracker_message_id` → указатель остаётся = old; следующий апдейт удаляет old и шлёт ещё одну карточку → **`new` осиротел**. Реальный, но узкий источник. | +| **(c)** ручной CLI/пересоздание | **Источник (операционный)** | Карточки, созданные вне `update_task_tracker`, не попадают в указатель → сироты. Происходит при ручных фиксах. | +| **(d)** гонка двух `update_task_tracker` | **ОСНОВНОЙ** | `update_task_tracker` вызывается из ≥5 независимых потоков: FastAPI-вебхуки (`webhooks/plane.py`, `gitea.py`), `queue_worker`, `reconciler`, `job_reaper`, монитор launcher (`agents/launcher.py`), плюс `stage_engine`/`plane_sync`. Оба потока читают `mid=X`, оба `delete(X)` (один ok, другой `gone`), оба `send` → **две** новые карточки M1, M2; `set(M1)`+`set(M2)` (last-writer-wins) → указатель = M2, **M1 осиротел**. Замок отсутствует. На быстрых стадиях (start→finish→stage-change подряд) вероятность высокая. | +| **(e)** `delete` упал (transient/rate-limit), `send` прошёл | **ОСНОВНОЙ** | `delete_telegram` вернул `False` (network/timeout/5xx/429 — старое сообщение ЖИВО), но `send` успешен → `set(new)`. **Старая карточка жива, указатель ушёл → сирота.** Прямое следствие «результат delete не гейтит send» (намеренное BR-6, чтобы не блокировать карточку). | + +**Вывод R-2:** доминирующие источники сирот — **(d) гонка** и **(e) delete-fail + send-ok**; +вклад дают также **(b′) рестарт между send и set** и **(c) ручные операции**. Пункт **(a) не +порождает сирот** (это лишь временное исчезновение карточки, самозалечивается). Общий корень всех +четырёх — **отсутствие учёта всех созданных message_id**: одиночный указатель структурно не может +подчистить карточку, ссылку на которую он потерял. + +### R-3. Почему заголовок застывает на «To Analyse» +**Это старый рендер осиротевшей карточки, НЕ баг плана-лейбла.** `render_task_tracker` — +**stateless**: строит статус-строку из `tasks.stage` на момент рендера через +`plane_status_label` → `_STAGE_STATUS_LABEL[stage]`. Живая карточка по определению отрисована на +текущей стадии. «To Analyse» = `_DEFAULT_STATUS_LABEL` И `_STAGE_STATUS_LABEL["created"]` — то +есть карточка была создана, когда задача стояла на `created`/раннем `analysis`, после чего +указатель ушёл (источник d/e), и эта карточка больше **никогда не перерисовывалась** — замёрзла на +первом рендере. Код рендера корректен; чинить надо **сирот (G1)**, а не лейбл. Это подтверждает +гипотезу BRD §1. + +Регрессионная страховка G2: тест «stage меняется → статус-строка свежей bump-карточки меняется» +(TC-07) фиксирует, что перерисовка несёт актуальный `plane_status_label`. + +### R-4. bump vs edit — что надёжнее против сирот + +| | `edit` | `bump` (текущий дефолт) | +|---|---|---| +| Сироты | Структурно исключены (правит ОДНО сообщение in-place; новое шлёт только на `EDIT_GONE`) | Плодит при рассинхроне (d/e/b′/c) | +| Карточка внизу чата | **Теряется** (остаётся на месте, тонет в истории) | **Есть** — намеренная фича ORCH-042/067 | +| Гонка | Тоже уязвима, но потолок — один лишний send на `EDIT_GONE`, не на каждый апдейт | Лишний send на каждый рассинхрон | + +**Рекомендация (принята): остаться на `bump` + добавить гарантированную зачистку (Подход Б).** +Обоснование с данными: +- `bump`-вниз-чата — **сознательное продуктовое решение** ORCH-042/067 (карточка не теряется в + истории чата). Переход на `edit` (Подход В) откатил бы фичу ради лечения симптома, который + решается **в рамках** `bump`. +- Корень сирот — **не режим**, а **отсутствие учёта message_id**. Реестр (`tracker_messages`) + закрывает корень при любом режиме: даже `edit` выигрывает от учёта на случай `EDIT_GONE`-fallback. +- `edit` тоже не панацея против гонки (два потока, оба `EDIT_GONE` → два send). Реестр нужен в + обоих случаях; значит правильно сохранить фичу и добавить учёт, а не жертвовать фичей. + +--- + +## Решение + +### Архитектурный выбор: **Подход Б** — реестр всех message_id задачи + sweep незакрытых. +Режим `bump` сохраняется дефолтом; `edit` остаётся доступен (`ORCH_TRACKER_MODE=edit`), +инвариант «одна карточка» теперь обеспечивается **учётом**, а не одиночным указателем. + +#### 1. Новая аддитивная таблица `tracker_messages` (G1) +```sql +CREATE TABLE IF NOT EXISTS tracker_messages ( + task_id INTEGER NOT NULL, + message_id INTEGER NOT NULL, + created_at TEXT DEFAULT (datetime('now')), + deleted INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (task_id, message_id) +); +CREATE INDEX IF NOT EXISTS idx_tracker_messages_live + ON tracker_messages(task_id) WHERE deleted = 0; +``` +`CREATE TABLE/INDEX IF NOT EXISTS` — идемпотентно, не трогает `tasks`/`agent_runs`/`events`/ +enduro-данные в общей прод-БД. Логический FK на `tasks.id` (без `REFERENCES`, как `jobs.task_id`) +— миграция не падает на живой БД. Детали схемы — `08-data-requirements.md`. + +#### 2. Новые helper'ы в `src/db.py` (never-raise, как соседи) +- `record_tracker_message(task_id, message_id)` — `INSERT OR IGNORE` свежий mid (`deleted=0`). +- `mark_tracker_message_deleted(task_id, message_id)` — `UPDATE … SET deleted=1`. +- `list_live_tracker_messages(task_id) -> list[int]` — все `deleted=0` mid задачи (для sweep). +- `get_tracker_message_id` / `set_tracker_message_id` — **сохраняются** (обратная совместимость; + «актуальный» указатель = последняя успешно отправленная карточка). + +#### 3. Новый порядок `bump`-ветки `update_task_tracker` (G1) +``` +text = render_task_tracker(task_id) +new_mid = send_telegram(text, disable_notification=True) # ≤1 send за вызов (AC-4) +if new_mid is not None: + record_tracker_message(task_id, new_mid) # учёт ДО смены указателя + set_tracker_message_id(task_id, new_mid) # указатель только при успехе (AC-3) +sweep_orphans(task_id, keep=new_mid or current_mid) # подчистить всё live, кроме актуальной +# send вернул None → указатель и реестр целы; перерисовка на следующем апдейте +``` +**`sweep_orphans(task_id, keep)`** (новый, в `notifications.py`): +для каждого `mid in list_live_tracker_messages(task_id)` где `mid != keep`: +`delete_telegram(mid)` → +- `True` (удалён ИЛИ `_DELETE_GONE_MARKERS`, вкл. 48ч «can't be deleted») → + `mark_tracker_message_deleted` (наша проблема исчерпана — AC-6); +- `False` (transient) → оставить `deleted=0`, повторить на следующем апдейте/проходе. + +**Ключевое отличие от текущего кода:** sweep чистит **все** ранее созданные живые карточки, а не +только «последний указатель» → закрывает источники **(d)**, **(e)**, **(b′)**, **(c)** разом: +осиротевшую при гонке/упавшем delete карточку видно в реестре и она будет подчищена при следующем +обновлении. `send` по-прежнему **не гейтится** delete'ом (BR-6 сохранён): сначала шлём свежую, +потом метём старые. + +**Идемпотентность гонки:** даже при двух параллельных апдейтах оба `record` своих mid в реестр; +последующий sweep любого из них подчистит лишние (всё, кроме `keep`). Реестр — точка сходимости: +в установившемся состоянии ровно одна `deleted=0` запись (AC-6 / TC-06). Гонку это не **исключает** +(замок не вводим — лишний потоковый лок в never-raise слое опаснее редкой лишней карточки), но +делает её **самозалечивающейся** на следующем тике. + +#### 4. Kill-switch (G1) +`tracker_orphan_sweep_enabled: bool = True` (env `ORCH_TRACKER_ORPHAN_SWEEP_ENABLED`) в +`src/config.py`. `False` → sweep отключён, поведение 1:1 как до ORCH-087 (только смена указателя), +для безопасного раската/отката на общей прод-БД. `record_tracker_message` пишется всегда (дёшево, +аддитивно) — отключается только активная зачистка. + +### G2 — заголовок отражает текущую стадию +Удовлетворяется устранением сирот (G1): единственная живая карточка по построению отрисована +stateless-рендером на текущей стадии. Доп. кода рендера не требуется. Регресс-тест TC-07. + +### G3 — статусы deploy-цикла видимы +Offline-ядро уже даёт: `deploy → «⏸️ Awaiting Deploy — ожидание Confirm Deploy»`, `done → «Done»` +(`_STAGE_STATUS_LABEL`). Ветки `Deploying` / `Confirm Deploy` / `Monitoring after Deploy` рисует +live-overlay (`_live_plane_branch_override`, `_LIVE_BRANCH_LABELS`). Требование — **покрыть +тестами** весь цикл (TC-08); если обнаружится дыра — закрыть в `_STAGE_STATUS_LABEL` / +`_LIVE_BRANCH_LABELS` **без** изменения статусной модели ORCH-066, `STAGE_TRANSITIONS`, `plane_sync`. +Изменений машины стадий не требуется. + +### G4 — эффорт в строке стадии +1. **Схема (`src/db.py`):** `_ensure_column(conn, "agent_runs", "effort", "TEXT")` рядом с + `…"model"…`. NULL для старых строк. +2. **Стамп факта (`src/agents/launcher._spawn`):** записать **ровно ту** строку `effort`, что + формирует `effort_flag` (то, что ушло в `--effort`), а НЕ пересчёт `resolve_agent_effort` на + рендере. Реализация: расширить начальный `INSERT INTO agent_runs (task_id, agent)` → + `(task_id, agent, effort)`, либо `UPDATE agent_runs SET effort=? WHERE id=run_id` сразу после + `run_id = cursor.lastrowid`. **Важно:** `effort` резолвится на стр. ~475, ПОСЛЕ `INSERT` на + стр. ~449 → проще `UPDATE` после резолва (или перенести `INSERT` ниже резолва). `effort==""` + (флаг опущен) → хранить `NULL`/`""`. Запись эффорта **никогда** не валит `_spawn` (контракт + launcher; обернуть try/except или полагаться на существующий). +3. **Рендер (`render_task_tracker._stage_line`):** добавить `effort` в `SELECT … FROM agent_runs` + (стр. ~322–325) и в `_stage_line` после `model`. **Формат:** `… · {model} · {effort}` — + разделитель `·`, единообразно с существующим `model_suffix`. Пустой/NULL `effort` → суффикс + **не добавляется** (как `model_suffix` при пустой модели) — деградация без `None`/падения (AC-9). + Пример: `✅ Разработка 7м · 4.9M↓/32k↑ · $3.97 · opus-4-8 · xhigh`. + +**Уровни (при дефолтном конфиге, ORCH-081):** developer=`xhigh`, tester/deployer=`medium`, +analyst/architect/reviewer=`high`. Значение — из `agent_runs.effort` (факт), не пересчёт (AC-8). + +G4 идёт **в этом же PR** (одна зона: `notifications.py` + `agent_runs`), как разрешает BRD §4. + +--- + +## Последствия + +### Плюсы +- Корень сирот закрыт **учётом** (реестр), а не выбором режима → фича `bump`-вниз-чата ORCH-042/067 + сохранена; `edit` тоже выигрывает (учёт на `EDIT_GONE`). +- Покрыты все реальные источники (d/e/b′/c) одним механизмом; самозалечивание на следующем тике. +- Полностью аддитивно и идемпотентно (`CREATE TABLE IF NOT EXISTS`, `_ensure_column`) → нулевая + регрессия для enduro-trails в общей прод-БД; restart-safe (состояние — в БД, не в памяти). +- Kill-switch для безопасного раската на self-hosting проде. +- Эффорт берётся из факта запуска → не врёт при смене конфига между запуском и рендером. + +### Минусы / ограничения +- **48ч (Telegram):** сообщения старше 48ч удалить нельзя (`message can't be deleted` → + `_DELETE_GONE_MARKERS` → `True`). Такие сироты помечаются `deleted=1` (наша проблема исчерпана) и + **физически остаются в чате**. Документируется как ограничение (AC-6). Новый код не плодит таких + сирот (sweep на каждом апдейте, задолго до 48ч); ограничение касается лишь УЖЕ накопленных старых. +- **Гонка не исключается, а делается самозалечивающейся** — в окне между двумя параллельными + апдейтами кратко возможны 2 карточки; сходится к 1 на следующем тике. Замок намеренно не вводим + (риск дедлока/блокировки в never-raise слое > редкая лишняя карточка). +- **Rate-limit (429)** при массовой зачистке накопленных сирот: `delete_telegram` вернёт `False` + (transient) → не помечаем `deleted`, повторим позже. Зачистка растянется, но не сломается и не + зациклится (each delete — один best-effort вызов за апдейт-проход). +- Рост `tracker_messages`: ~N строк на задачу (N = число апдейтов). Незначительно; при желании — + фоновая обрезка `deleted=1` старше X дней (вне скоупа ORCH-087). + +### Инварианты (не нарушены) +- `update_task_tracker` — **never-raise**; ни delete/send/sweep не валят конвейер (AC-5). +- «Одна карточка на задачу» — после установившегося обновления ровно одна `deleted=0` (AC-4/AC-6). +- Карточка тихая `disable_notification=True`; `disable_web_page_preview=True` (ORCH-080); + `plane_issue_link` кликабелен (ORCH-067) — всё сохранено. +- НЕ трогаются: `STAGE_TRANSITIONS`, `QG_CHECKS`, статусная модель ORCH-066, `plane_sync`-ключи, + поведение для не-self проектов сверх миграции (аддитивна, инертна). + +### Не выбрано (и почему) +- **Подход А (только строгий swap указателя):** не покрывает рестарт между delete и send и гонку + — недостаточен сам по себе (BRD §2). +- **Подход В (дефолт `edit`):** откатывает продуктовую фичу ORCH-042/067 ради симптома, решаемого + внутри `bump`; не устраняет гонку. Отклонён (R-4). +- **Глобальный ADR не заводится:** изменение замкнуто в компоненте Notifications + аддитивная + таблица; нет новой стадии/QG/сквозного компонента. Достаточно per-work-item ADR (канон CLAUDE.md). + +## Артефакты к обновлению в реализации (тот же PR) +`CLAUDE.md` (секция трекера), `docs/architecture/README.md` (компонент Notifications), +`CHANGELOG.md` (`## [Unreleased]`, union-merge ORCH-073), `08-data-requirements.md` (этот PR), +тесты по `04-test-plan.yaml`. diff --git a/docs/work-items/ORCH-087/08-data-requirements.md b/docs/work-items/ORCH-087/08-data-requirements.md new file mode 100644 index 0000000..0e62f6f --- /dev/null +++ b/docs/work-items/ORCH-087/08-data-requirements.md @@ -0,0 +1,60 @@ +# 08 — Требования к схеме БД — ORCH-087 + +Все изменения **аддитивны и идемпотентны**, безопасны на живой ОБЩЕЙ прод-БД (enduro-trails + +orchestrator из одного инстанса). Существующие колонки/данные не трогаются. Решение — ADR-001. + +## 1. Новая таблица `tracker_messages` (G1 — anti-orphan, Подход Б) + +Реестр ВСЕХ Telegram message_id, созданных трекером для задачи, и их состояние. Позволяет +подчищать осиротевшие карточки, ссылку на которые потерял одиночный `tasks.tracker_message_id`. + +```sql +CREATE TABLE IF NOT EXISTS tracker_messages ( + task_id INTEGER NOT NULL, -- логический FK tasks.id (без REFERENCES, как jobs.task_id) + message_id INTEGER NOT NULL, -- Telegram message_id отправленной карточки + created_at TEXT DEFAULT (datetime('now')), + deleted INTEGER NOT NULL DEFAULT 0, -- 0 = живая (надо подчистить), 1 = удалена/исчерпана + PRIMARY KEY (task_id, message_id) +); +CREATE INDEX IF NOT EXISTS idx_tracker_messages_live + ON tracker_messages(task_id) WHERE deleted = 0; +``` + +- Миграция: `conn.executescript(...)` с `CREATE TABLE/INDEX IF NOT EXISTS` в `init_db()` (рядом с + блоком `job_deps`). Повторный `init_db` — no-op. +- `PRIMARY KEY(task_id, message_id)` → `INSERT OR IGNORE` идемпотентен (дубль mid не падает). +- Частичный индекс `WHERE deleted=0` ускоряет sweep (выборка только живых). +- Логический FK (без `REFERENCES tasks(id)`) — миграция не падает на pre-existing БД, как `job_deps`. +- `deleted=1` присваивается при: успешном `delete`, «уже нет» (`message to delete not found`), + «нельзя удалить» (48ч, `message can't be deleted` — `_DELETE_GONE_MARKERS`). Transient-fail + (`False`) → запись остаётся `deleted=0`, повтор на следующем апдейте. + +### Helper'ы доступа (`src/db.py`, never-raise) +| Функция | Назначение | +|---|---| +| `record_tracker_message(task_id, message_id)` | `INSERT OR IGNORE` свежий mid (`deleted=0`) | +| `mark_tracker_message_deleted(task_id, message_id)` | `UPDATE … SET deleted=1` | +| `list_live_tracker_messages(task_id) -> list[int]` | все `deleted=0` mid задачи (для sweep) | + +`get_tracker_message_id` / `set_tracker_message_id` — без изменений (обратная совместимость). + +## 2. Новая колонка `agent_runs.effort` (G4 — эффорт в строке стадии) + +```sql +-- идемпотентная миграция в init_db(), рядом с _ensure_column(... "agent_runs", "model" ...) +ALTER TABLE agent_runs ADD COLUMN effort TEXT; -- через _ensure_column (no-op если есть) +``` + +- Миграция: `_ensure_column(conn, "agent_runs", "effort", "TEXT")`. +- Хранит ФАКТИЧЕСКИ применённый `--effort` запуска (то, что ушло в CLI), стампится в + `launcher._spawn` после `effort = resolve_agent_effort(...)`. Не пересчитывается на рендере. +- `NULL`/`""` для старых строк и для запусков с опущенным флагом (`effort==""`) → рендер опускает + эффорт-суффикс (деградация без падения, AC-9). +- Значения: подмножество `VALID_EFFORTS` (`low|medium|high|xhigh|max`) либо `NULL`. + +## 3. Гарантии совместимости (общая прод-БД) +- Никаких `ALTER`/`DROP` существующих колонок; только новая таблица + новая колонка. +- `CREATE TABLE IF NOT EXISTS` / `_ensure_column` → повторный запуск инертен (AC-10). +- Данные enduro-trails не читаются и не пишутся этими миграциями (таблицы изолированы по `task_id`, + колонка `effort` заполняется только новыми запусками). +- Restart-safe: всё состояние в БД; рестарт орка не теряет реестр и не плодит сирот по новой схеме. diff --git a/docs/work-items/ORCH-087/10-tech-risks.md b/docs/work-items/ORCH-087/10-tech-risks.md new file mode 100644 index 0000000..d6f6b13 --- /dev/null +++ b/docs/work-items/ORCH-087/10-tech-risks.md @@ -0,0 +1,20 @@ +# 10 — Технические риски — ORCH-087 + +| # | Риск | Вероятность / Влияние | Митигация | +|---|------|----------------------|-----------| +| R1 | **Сироты старше 48ч неудаляемы** (Telegram: `message can't be deleted`) — остаются в чате навсегда | Сред / Низ (косметика) | Помечаем `deleted=1` (`_DELETE_GONE_MARKERS`), зачистка не зацикливается; новый код не порождает таких сирот (sweep на каждом апдейте ≪ 48ч). Документировано (AC-6). Старые накопленные — вне автоматики. | +| R2 | **Rate-limit / 429 при массовой зачистке** накопленных сирот | Низ / Низ | `delete_telegram` → `False` (transient) → запись остаётся `deleted=0`, повтор позже. Один best-effort delete на запись за проход; зачистка растягивается, не падает. | +| R3 | **Гонка не исключается** (нет замка) — кратко возможны 2 карточки между параллельными апдейтами | Сред / Низ | Реестр делает гонку **самозалечивающейся**: sweep следующего тика подчистит лишнее (всё ≠ `keep`). Замок намеренно не вводим — лок в never-raise слое опаснее редкой лишней карточки (риск дедлока при `max_concurrency=1`). | +| R4 | **Регрессия миграции на общей прод-БД** (enduro-trails) | Низ / Выс | Строго аддитивно: `CREATE TABLE IF NOT EXISTS` + `_ensure_column` (no-op если есть); логический FK без `REFERENCES`. Idempotent `init_db`. Покрыто TC-09. | +| R5 | **never-raise нарушен** новым sweep/реестром → падение валит конвейер | Низ / Выс | Все helper'ы и `sweep_orphans` обёрнуты как соседи; `update_task_tracker` целиком в `try/except` (AC-5, TC-05). Запись эффорта в `_spawn` не валит launcher. | +| R6 | **Эффорт-стамп врёт** (пересчёт на рендере расходится с CLI при смене конфига) | Сред / Низ | Источник — РОВНО строка `effort`, ушедшая в `--effort` (стамп в `_spawn`), не `resolve_agent_effort` на рендере (AC-8). | +| R7 | **Порядок в `_spawn`:** `INSERT agent_runs` (стр.~449) идёт ДО резолва `effort` (стр.~475) | Сред / Низ | Реализовать `UPDATE agent_runs SET effort=? WHERE id=run_id` после резолва, либо перенести `INSERT` ниже резолва. Зафиксировано в ADR-001 §G4.2. | +| R8 | **Дыра покрытия deploy-цикла** (G3): ветка статуса не отрисована | Низ / Низ | Тесты TC-07/TC-08 проверяют offline-лейблы (`deploy`/`done`) и overlay-маппинг (`deploying`/`monitoring`); дыра закрывается в `_STAGE_STATUS_LABEL`/`_LIVE_BRANCH_LABELS` без изменения ORCH-066. | +| R9 | **Рост `tracker_messages`** (~N строк/задача) | Низ / Низ | Незначительно для SQLite; опц. фоновая обрезка `deleted=1` старше X дней — вне скоупа ORCH-087. | + +## Принятые архитектурные ограничения +- Замок на `update_task_tracker` **не вводится** (R3) — сходимость через реестр предпочтительнее + потокового лока в never-raise слое. +- Дефолтный режим остаётся `bump` (фича ORCH-042/067 сохранена); `edit` доступен через env. +- Изменения замкнуты в компоненте Notifications + аддитивная схема — `STAGE_TRANSITIONS`, + `QG_CHECKS`, статусная модель ORCH-066, `plane_sync`-ключи не трогаются. Глобальный ADR не нужен.