architect(ET): auto-commit from architect run_id=426
All checks were successful
CI / test (push) Successful in 24s
All checks were successful
CI / test (push) Successful in 24s
This commit is contained in:
@@ -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`.
|
||||
60
docs/work-items/ORCH-087/08-data-requirements.md
Normal file
60
docs/work-items/ORCH-087/08-data-requirements.md
Normal file
@@ -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: всё состояние в БД; рестарт орка не теряет реестр и не плодит сирот по новой схеме.
|
||||
20
docs/work-items/ORCH-087/10-tech-risks.md
Normal file
20
docs/work-items/ORCH-087/10-tech-risks.md
Normal file
@@ -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 не нужен.
|
||||
Reference in New Issue
Block a user