architect(ET): auto-commit from architect run_id=429
This commit is contained in:
@@ -334,6 +334,42 @@ helper `validated_revision` питает и штамп A, и `EXPECTED_REVISION`
|
||||
Подробнее: [adr-0012](adr/adr-0012-security-gate.md), детально —
|
||||
`docs/work-items/ORCH-022/06-adr/ADR-001-security-gate.md`.
|
||||
|
||||
### Live-трекер: зачистка сирот + эффорт в карточке + честное время (ORCH-087 — design)
|
||||
Скалярный `tasks.tracker_message_id` (только последний `message_id`) при рассинхроне
|
||||
bump-режима (доминанты: гонка двух `update_task_tracker` и delete-fail+send-ok)
|
||||
терял ссылку на прежние карточки → **осиротевшие «замёрзшие»** карточки (скриншот
|
||||
ORCH-082: `📍 To Analyse` на задаче, реально дошедшей до `deploy`). G0-расследование
|
||||
([ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md)):
|
||||
рендер исправен, корень — потеря учёта старых mid. Решение (bump сохраняется как
|
||||
дефолт — фича «карточка внизу» ORCH-042/067):
|
||||
- **G1 — полный учёт mid:** аддитивная таблица-леджер `tracker_messages(task_id,
|
||||
message_id, created_at, deleted_at)` (вариант A1; JSON-массив A2 отклонён —
|
||||
lost-update при гонке). На каждом bump зачищаются ВСЕ незакрытые mid (`deleted_at
|
||||
IS NULL`): успех/«already gone» → `deleted_at`, transient → остаётся для ретрая;
|
||||
новый mid в леджер + `set_tracker_message_id` ТОЛЬКО при `send is not None` (BR-6).
|
||||
Скаляр `tracker_message_id` сохранён (BC). Остаточная гонка самозалечивается за один
|
||||
переход (лок не вводится). Known-limitation: Telegram 48ч (сироты старше неудаляемы).
|
||||
- **G2/G3 — заголовок/deploy-цикл:** после G1 единственная живая карточка несёт
|
||||
заголовок текущей стадии; `_LIVE_BRANCH_LABELS` дополняется ключом `confirm_deploy`
|
||||
(полнота цикла `Awaiting Deploy → Deploying → Confirm Deploy → Monitoring → Done`).
|
||||
- **BR-EFF — эффорт в строке стадии:** новая колонка `agent_runs.effort TEXT`,
|
||||
стамп фактического `resolve_agent_effort` в `launcher._spawn` (CLI эффорт не
|
||||
возвращает); рендер `· {model} · {effort}` (developer=`xhigh`, tester/deployer=
|
||||
`medium`, прочие=`high`); пустой → суффикс опускается.
|
||||
- **BR-G5 — честное время:** done-строка `⏱️ Агенты {agent} · твоё {review~cap} ·
|
||||
общее с ожиданием {wall}` — три независимых подписанных метрики; `agent`=Σ
|
||||
`agent_runs` (главная, точная); «твоё» ограничено порогом
|
||||
`tracker_brd_review_cap_s` (дефолт 2ч, маркер `~` при отсечке аномального застоя);
|
||||
`wall` подписан «с ожиданием», не выдаётся за сумму.
|
||||
- **Инварианты:** `STAGE_TRANSITIONS`/`QG_CHECKS`/стадии — без изменений; миграции
|
||||
аддитивны/идемпотентны (общая прод-БД, enduro не трогается); never-raise,
|
||||
`disable_notification`, `plane_issue_link` (ORCH-067), `disable_web_page_preview`
|
||||
(ORCH-080) — сохранены; разработка поверх свежего `origin/main` (ORCH-86),
|
||||
`reconciler.py` не эродируется.
|
||||
|
||||
Детально — [ADR-001](../work-items/ORCH-087/06-adr/ADR-001-tracker-orphan-cleanup.md),
|
||||
`docs/work-items/ORCH-087/08-data-requirements.md`.
|
||||
|
||||
### Reconciler: реконсиляция потерянных webhook (ORCH-053 — реализовано)
|
||||
Конвейер продвигается только входящими webhook; потерянное событие (502 на ребилде,
|
||||
нет ретраев у Plane/Gitea, неразрезолвленный `sha→branch`) → задача застревает молча
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
# ADR-001: Зачистка осиротевших трекер-карточек (bump + полный учёт message_id), эффорт в строке стадии, честное итоговое время
|
||||
|
||||
## Статус
|
||||
Accepted
|
||||
|
||||
## Контекст
|
||||
|
||||
Каждая задача имеет ОДНУ live-карточку в Telegram (`update_task_tracker`, инвариант
|
||||
«одна карточка на задачу»). Дефолтный режим — `bump` (ORCH-067/042): на каждом
|
||||
обновлении старая карточка удаляется и новая шлётся вниз чата (фича-просьба Славы —
|
||||
«карточка всегда внизу»). Указатель `tasks.tracker_message_id` — **скаляр**, хранит
|
||||
ТОЛЬКО последний `message_id`.
|
||||
|
||||
**Симптом (скриншот Славы, 08.06, ORCH-082):** в чате висела карточка с заголовком
|
||||
`📍 To Analyse`, хотя задача прошла весь конвейер до стадии `deploy`; статусы
|
||||
deploy-цикла не отражены. Карточка — **осиротевшая** старая (`msg 18204`),
|
||||
застрявшая на первом рендере (`To Analyse` = `_DEFAULT_STATUS_LABEL`). Проверено
|
||||
(`deleteMessage → ok:true` и для 18204, и для 18227): бот ИМЕЕТ право удалять — дело
|
||||
не в правах, а в **потере ссылки** на старые `message_id`.
|
||||
|
||||
BRD требует (BR-G0): сначала расследование → ADR, потом фикс. Ниже — ответы на все
|
||||
4 вопроса §4 BRD, рекомендация и принятые архитектурные решения.
|
||||
|
||||
---
|
||||
|
||||
## G0 — Ответы на вопросы расследования (BR-G0, AC-0.1)
|
||||
|
||||
### Вопрос 1 — Сколько РЕАЛЬНО карточек одной задачи висело
|
||||
|
||||
По логам/скриншоту ORCH-082 подтверждено **минимум 2 живых сообщения** одной задачи
|
||||
(`18204` — осиротевшая «замёрзшая» на `To Analyse`; `18227` — актуальная). Скалярный
|
||||
указатель структурно допускает **N>1** сирот: каждый рассинхрон (см. вопрос 2) теряет
|
||||
ровно одну ссылку, а сиротство накопительно — за прогон из ~8 переходов в худшем
|
||||
случае осиротеть может до N−1 карточек. Точное число для конкретного прогона
|
||||
непредсказуемо именно потому, что учёта старых mid НЕТ — это и есть корень бага.
|
||||
|
||||
### Вопрос 2 — В какие МОМЕНТЫ `tracker_message_id` рассинхронизируется
|
||||
|
||||
Текущий код (`update_task_tracker`, ветка `mode == "bump"`):
|
||||
```python
|
||||
if mid is not None:
|
||||
delete_telegram(mid) # best-effort, результат НЕ гейтит send (BR-6)
|
||||
new_mid = send_telegram(text, disable_notification=True)
|
||||
if new_mid is not None:
|
||||
set_tracker_message_id(task_id, new_mid) # перепонт ТОЛЬКО на новый mid
|
||||
```
|
||||
|
||||
| Сценарий | Механика | Рождает сироту? |
|
||||
|----------|----------|-----------------|
|
||||
| (a) `send` → `None` (нет креды / transient) | `new_mid is None` → указатель НЕ перезаписан; но `delete(old)` уже выполнен best-effort. Старая удалена (или осталась, если delete тоже упал — см. e). | Сам по себе — нет; защита BR-6 корректна. |
|
||||
| (b) рестарт орка между `delete` и `send` | `delete(old)` прошёл, процесс упал до `send` → при перезапуске рисуется новая, старая уже удалена. | Обычно нет; но если `delete` вернул False до падения — old жив, ссылка на него только в скаляре, который не менялся → следующий bump его подчистит. |
|
||||
| (c) пересоздание карточки во время CLI-фикса / ручных операций | Ручной `sendMessage` или внешняя правка вне `update_task_tracker` создаёт mid, которого нет в учёте. | Да — учёт о нём не знает. |
|
||||
| (d) **гонка** двух `update_task_tracker` подряд (быстрые стадии) | Оба читают один `mid`, оба `delete` его (один `ok`, второй `already gone`→True), оба `send` → **две** новых карточки; указатель садится на одну → вторая осиротела. | **Да** — частый на быстрых стадиях. |
|
||||
| (e) **`delete` упал (transient/>48ч), но `send` прошёл** | `delete(old)` → False (old жив), `send` → new, указатель `=new` → ссылка на old **навсегда потеряна**. | **Да — доминирующий генератор сирот.** |
|
||||
|
||||
**Вывод:** доминируют (d) гонка и (e) delete-fail+send-ok. Общий первопричинный
|
||||
дефект — **скалярный учёт**: система знает лишь о последнем `message_id`, поэтому при
|
||||
любой потере ссылки старая карточка осиротевает безвозвратно.
|
||||
|
||||
### Вопрос 3 — Почему ИМЕННО заголовок застывает на `To Analyse`
|
||||
|
||||
Это **старый рендер**, а НЕ баг план-лейбла. Код-аудит подтверждает:
|
||||
`render_task_tracker` → `_card_status_label` → `plane_status_label` детерминированно
|
||||
выводит заголовок из `tasks.stage` (`_STAGE_STATUS_LABEL`), и на `deploy` корректно
|
||||
даёт `⏸️ Awaiting Deploy`. Осиротевшая карточка `18204` была отрисована ОДИН раз на
|
||||
самой ранней стадии (`stage` ещё `created`/`analysis` → `To Analyse` =
|
||||
`_DEFAULT_STATUS_LABEL`) и больше не редактировалась/не удалялась (ссылка потеряна).
|
||||
Рендер исправен; «замёрзший» заголовок — следствие сиротства (G1), а не G2.
|
||||
|
||||
**Таблица воспроизведения «стадия → (заголовок в Telegram) vs (stage в БД)»**
|
||||
(аналитическая, выведена из кода `plane_status_label`/`_STAGE_STATUS_LABEL`; подлежит
|
||||
подтверждению живым staging-прогоном TC-18 на 8501, AC-0.2):
|
||||
|
||||
| `tasks.stage` (БД) | Заголовок актуальной карточки (ожидаемо) | Заголовок ОСИРОТЕВШЕЙ (факт ORCH-082) |
|
||||
|--------------------|------------------------------------------|----------------------------------------|
|
||||
| created | `📍 To Analyse` | `📍 To Analyse` |
|
||||
| analysis | `📍 Analysis` (или `⏸️ In Review` при открытом brd-clock) | `📍 To Analyse` (замёрзла) |
|
||||
| architecture | `📍 Architecture` | `📍 To Analyse` |
|
||||
| development | `📍 Development` | `📍 To Analyse` |
|
||||
| review | `📍 Code-Review` | `📍 To Analyse` |
|
||||
| testing | `📍 Testing` | `📍 To Analyse` |
|
||||
| deploy | `📍 ⏸️ Awaiting Deploy — ожидание Confirm Deploy` (+overlay `Deploying`/`Confirm Deploy`/`Monitoring`) | `📍 To Analyse` |
|
||||
| done | `🎉 … ГОТОВО` + `📍 Done` | `📍 To Analyse` |
|
||||
|
||||
Правый столбец — наглядное доказательство: одна карточка отстаёт на `stage` в БД
|
||||
ровно потому, что потеряла ссылку и больше не обновляется.
|
||||
|
||||
### Вопрос 4 — `bump` vs `edit`: что надёжнее против сирот
|
||||
|
||||
| Критерий | `edit` (правка in-place) | `bump` (delete+send вниз) |
|
||||
|----------|--------------------------|----------------------------|
|
||||
| Сироты by design | **Нет** (одно сообщение редактируется) | **Да** при рассинхроне (вопрос 2) |
|
||||
| «Карточка всегда внизу» (фича-просьба ORCH-042) | Теряется (карточка тонет вверх чата) | **Сохраняется** |
|
||||
| Реакция на потерю ссылки | EDIT_GONE → один новый mid, старый и так недоступен | старый mid терялся → сирота |
|
||||
| Поведение при гонке (d) | оба правят один mid (idempotent) | два новых сообщения |
|
||||
|
||||
`edit` строго надёжнее против сирот, но **регрессирует явную фича-просьбу** Славы
|
||||
(«карточка внизу», ради которой bump и сделан дефолтом в ORCH-067). `bump` плодит
|
||||
сирот **только** из-за скалярного учёта — устранимого первопричинного дефекта, а не
|
||||
неотъемлемого свойства режима.
|
||||
|
||||
**Рекомендация (обоснованная данными): сохранить `bump` дефолтом и устранить
|
||||
первопричину — вести ПОЛНЫЙ учёт незакрытых `message_id` (вариант A из R-5).** Это
|
||||
даёт и фичу «карточка внизу», и отсутствие сирот. Переход на `edit` (вариант B) был
|
||||
бы откатом UX-решения ORCH-067 ради лечения симптома, а не причины. `edit` остаётся
|
||||
доступен через `ORCH_TRACKER_MODE=edit` (kill-switch неизменен).
|
||||
|
||||
---
|
||||
|
||||
## Решение
|
||||
|
||||
### Р-1 (G1) — bump + полный учёт message_id через таблицу-леджер `tracker_messages`
|
||||
|
||||
Вводится **аддитивная таблица-леджер** всех незакрытых карточек задачи (вариант A1
|
||||
из R-5; выбран над JSON-массивом A2 — см. «Альтернативы»):
|
||||
|
||||
```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_at TEXT, -- NULL = карточка ещё жива (незакрыта)
|
||||
PRIMARY KEY (task_id, message_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracker_messages_open
|
||||
ON tracker_messages(task_id) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
Скаляр `tasks.tracker_message_id` **сохраняется** (обратная совместимость: остаётся
|
||||
указателем на ТЕКУЩУЮ карточку для прочих читателей `get_tracker_message_id`).
|
||||
Леджер — авторитетный источник для зачистки.
|
||||
|
||||
**Алгоритм `update_task_tracker`, ветка `bump` (соблюдает R-1…R-6):**
|
||||
1. Прочитать ВСЕ незакрытые mid задачи: `SELECT message_id FROM tracker_messages
|
||||
WHERE task_id=? AND deleted_at IS NULL` (R-1).
|
||||
2. Для каждого: `delete_telegram(mid)`:
|
||||
- `True` (удалено ИЛИ `_DELETE_GONE_MARKERS` «already gone», вкл. >48ч) →
|
||||
`UPDATE … SET deleted_at=datetime('now')` (исключить из учёта, R-2);
|
||||
- `False` (transient/сеть/5xx) → оставить незакрытой для повторной попытки на
|
||||
следующем bump (R-2).
|
||||
3. `new_mid = send_telegram(text, disable_notification=True)` — РОВНО один send (R-4).
|
||||
4. Если `new_mid is not None`: `INSERT INTO tracker_messages(task_id, message_id)`
|
||||
**и** `set_tracker_message_id(task_id, new_mid)`. Если `None` — НЕ трогать ни
|
||||
леджер, ни указатель (R-3, сохранена защита BR-6).
|
||||
|
||||
**Инвариант (R после фикса):** после любого `update_task_tracker` все ранее созданные
|
||||
карточки задачи либо удалены, либо помечены `deleted_at`, либо остались незакрытыми
|
||||
для повторной попытки — НИ ОДНА не теряется из учёта (в пределах 48ч-лимита Telegram).
|
||||
|
||||
**Совместимость / миграция:** на первой инициализации существующий
|
||||
`tasks.tracker_message_id` НЕ переносится автоматически в леджер (одноразовый бэкфилл
|
||||
не требуется — старые сироты всё равно за 48ч-окном). Новый поток ведёт леджер с
|
||||
нуля; никаких изменений данных enduro-trails.
|
||||
|
||||
**Зачистка delete ДО send** (как в текущем коде): момент пустоты тих
|
||||
(`disable_notification`), приемлем.
|
||||
|
||||
### Р-2 (G1, остаточный риск гонки) — самозалечивание, без блокировок
|
||||
|
||||
Гонка (d) двух одновременных `update_task_tracker` (вызываются из queue-worker,
|
||||
reconciler, reaper) может на ОДИН цикл оставить лишнюю карточку: оба прочитали тот же
|
||||
открытый набор, оба отправили новую. Обе новые попадают в леджер → **следующий** bump
|
||||
их зачистит. Это строго лучше текущего ПОСТОЯННОГО сиротства и **самозалечивается** за
|
||||
один переход. Кросс-процессную сериализацию (файловый лок/транзакция) НЕ вводим:
|
||||
контракт компонента — best-effort, never-raise, карточка silent; цена лока не
|
||||
оправдана. Остаточный риск задокументирован (AC-1.4, §Последствия).
|
||||
|
||||
### Р-3 (G2) — заголовок текущей стадии
|
||||
|
||||
Отдельного кода не требует: после Р-1 в чате остаётся ОДНА живая карточка, а
|
||||
`render_task_tracker`/`plane_status_label` уже выводят заголовок из `tasks.stage`.
|
||||
Закрепляется регресс-юнитом: `plane_status_label` перебирает все стадии
|
||||
`created…done` и даёт корректный лейбл (TC-06, AC-2.2).
|
||||
|
||||
### Р-4 (G3) — deploy-цикл на карточке
|
||||
|
||||
- `_STAGE_STATUS_LABEL["deploy"] = "⏸️ Awaiting Deploy — ожидание Confirm Deploy"`
|
||||
(offline) — присутствует, покрывает AC-3.1.
|
||||
- live-overlay `_live_plane_branch_override` рисует `Deploying` / `Monitoring after
|
||||
Deploy` через `_LIVE_BRANCH_LABELS` при наличии выделенного Plane-UUID — покрывает
|
||||
AC-3.2.
|
||||
- **Добавить (полнота цикла):** ключ `"confirm_deploy": "⏳ Confirm Deploy —
|
||||
подтвердите прод-деплой"` в `_LIVE_BRANCH_LABELS` (логический ключ `confirm_deploy`
|
||||
уже существует в `plane_sync` с ORCH-059). Без base-alias (это реальный отдельный
|
||||
статус). Контракт never-raise и kill-switch `tracker_live_status` сохранены.
|
||||
- `Done` рендерится из `stage == "done"` (AC-3.3) — без изменений.
|
||||
|
||||
### Р-5 (BR-EFF) — эффорт в строке стадии
|
||||
|
||||
- **Схема:** новая колонка `agent_runs.effort TEXT` через
|
||||
`_ensure_column(conn, "agent_runs", "effort", "TEXT")` рядом с `model` (аддитивно,
|
||||
идемпотентно).
|
||||
- **Стамп в момент запуска** (`launcher._spawn`): сразу после строки
|
||||
`effort = resolve_agent_effort(agent, project_id)` (line 475) выполнить
|
||||
`UPDATE agent_runs SET effort=? WHERE id=run_id` со значением `effort or None`
|
||||
(РЕАЛЬНО ушедшее в `--effort`; пустое → `NULL` → суффикс опускается). Выбран
|
||||
follow-up `UPDATE` (а не расширение `INSERT` на line 449) — минимальный диф, без
|
||||
переноса резолва модели/эффорта выше по коду; значение точно соответствует флагу
|
||||
запуска. CLI не возвращает эффорт в result-JSON, поэтому стамп — единственный
|
||||
надёжный источник (BR §6).
|
||||
- **Рендер** (`render_task_tracker._stage_line`): добавить `effort` в SELECT
|
||||
`agent_runs` и в строку стадии **единым форматом `· {model} · {effort}`**
|
||||
(напр. `✅ Разработка 12м · …↓/…↑ · $… · opus-4-8 · xhigh`). Пустой/неизвестный
|
||||
эффорт → суффикс эффорта опускается (как опускается модель при пустой
|
||||
`short_model_name`) — рендер не падает (AC-E.4). Допустим fallback на
|
||||
`resolve_agent_effort(run["agent"])` для исторических строк без колонки.
|
||||
- **Ожидаемо** (ORCH-41/081): developer=`xhigh`; tester/deployer=`medium`;
|
||||
analyst/architect/reviewer=`high` (AC-E.3).
|
||||
|
||||
### Р-6 (BR-G5) — честное и сходимое итоговое время
|
||||
|
||||
Текущая строка `done` («магическое» раздутое число) заменяется на **три
|
||||
независимых, явно подписанных метрики** — ни одна не выдаётся за сумму других
|
||||
(удовлетворяет T-4 формулировкой «не показывать wall как сумму»):
|
||||
|
||||
```
|
||||
⏱️ Агенты {agent_seconds} · твоё {review_capped} · общее с ожиданием {wall}
|
||||
```
|
||||
|
||||
- **T-1 `agent_seconds`** = `Σ _duration_seconds(started, finished)` по `agent_runs`
|
||||
— **главная метрика**, остаётся точной (без регресса).
|
||||
- **T-2 `review_capped`** — человеческое BRD-время, ограниченное разумным порогом
|
||||
`tracker_brd_review_cap_s` (новый config-флаг, env `ORCH_TRACKER_BRD_REVIEW_CAP_S`,
|
||||
**дефолт 7200с = 2ч**). При `review_seconds > cap` отображается capped-значение с
|
||||
маркером «~» (напр. `~2ч`), сигнализируя об отсечке аномального застоя/рассинхрона
|
||||
(кейс ORCH-087: brd_review болтался открытым из-за In Review→Backlog desync,
|
||||
показывал 392м). Выбран порог (а не «активные окна») — под-оконных данных у нас нет
|
||||
(только `brd_review_started_at`/`ended_at`); порог — допустимый T-2 вариант.
|
||||
Закрывает AC-5.1 (6ч-окно → не ~6ч).
|
||||
- **T-3 `wall`** = `_duration_seconds(created_at, updated_at)` — подписан **«общее с
|
||||
ожиданием»**, НЕ выдаётся за рабочее время. Включает очередь/ожидание/застой.
|
||||
- **T-4** соблюдён: метрики независимы и явно подписаны; wall НЕ представлен как
|
||||
`агенты + твоё` (несведение по незалогированным queue-паузам перестаёт «врать»).
|
||||
- **T-5** `💰`-строка и агрегаты `total_in/out/cost` — без изменений.
|
||||
|
||||
### Р-7 (BR-G6) — свежий main / без эрозии reconciler
|
||||
|
||||
Подтверждено на стадии архитектуры: `git merge-base --is-ancestor origin/main HEAD`
|
||||
→ true (origin/main содержит merge-коммит ORCH-086, #86); в `src/reconciler.py`
|
||||
ветки присутствуют 43 маркера ORCH-086 (`skipped_terminal_total`, `state_uuid`,
|
||||
terminal-skip). **Файлы ORCH-087 (`notifications.py`, `db.py`, `agents/launcher.py`,
|
||||
`usage.py`, тесты) НЕ пересекаются с `reconciler.py`** → правки 86 не эродируются.
|
||||
`CHANGELOG.md` правится под `.gitattributes merge=union`. Явная проверка на
|
||||
merge-gate — AC-6.1/AC-6.2 (TC-19).
|
||||
|
||||
---
|
||||
|
||||
## Инварианты (не нарушаются)
|
||||
|
||||
- never-raise по всему пути нотификаций; карточка всегда silent (`disable_notification`).
|
||||
- «одна карточка на задачу»; ≤1 `send` за вызов `update_task_tracker` (R-4).
|
||||
- Ссылки ORCH-067 (`plane_issue_link`), `disable_web_page_preview` ORCH-080 — сохранены.
|
||||
- `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / стадии конвейера — **без изменений**.
|
||||
- Миграции БД аддитивны и идемпотентны (`CREATE TABLE IF NOT EXISTS` / `_ensure_column`),
|
||||
restart-safe на общей прод-БД; данные enduro-trails не трогаются.
|
||||
|
||||
## Альтернативы (отклонены)
|
||||
|
||||
- **Вариант B (переход дефолта на `edit`)** — устраняет сирот by design, но
|
||||
регрессирует фича-просьбу «карточка внизу» (ORCH-042/067). Лечит симптом, а не
|
||||
причину. Отклонён; `edit` остаётся опцией через kill-switch.
|
||||
- **Вариант A2 (JSON-массив `tasks.tracker_message_ids`)** — компактнее, но
|
||||
read-modify-write блоба сам подвержен lost-update при гонке (d) (два процесса
|
||||
перезапишут JSON друг друга — ровно тот класс багов, что чиним). Строка-на-mid в
|
||||
таблице с раздельными INSERT/UPDATE этого избегает и даёт `deleted_at` для ретрая
|
||||
transient-delete + наблюдаемость. Отклонён в пользу A1.
|
||||
- **Файловый/транзакционный лок против гонки (d)** — избыточен для best-effort
|
||||
silent-карточки; леджер самозалечивается за один переход. Отклонён.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Плюсы:**
|
||||
- Уходит класс багов «замёрзшая сирота» — в чате ровно одна достоверная карточка.
|
||||
- Сохранена фича «карточка всегда внизу» (bump-дефолт).
|
||||
- Эффорт виден рядом с моделью; источник стампа надёжен (момент запуска).
|
||||
- Итоговое время честно и подписано; «магическое» раздутое число устранено.
|
||||
- Все изменения аддитивны/идемпотентны, kill-switch'и сохранены, машина стадий не тронута.
|
||||
|
||||
**Минусы / ограничения:**
|
||||
- **Telegram-лимит 48ч:** сообщения старше 48ч удалить нельзя (`_DELETE_GONE_MARKERS`
|
||||
классифицирует это как «gone» → исключаются из учёта). Совсем старые сироты (до
|
||||
деплоя фикса) могут остаться навсегда — **known limitation** (AC-1.4).
|
||||
- **Остаточная гонка (d):** одна лишняя карточка может прожить один переход до
|
||||
самозалечивания на следующем bump (см. Р-2).
|
||||
- Новая таблица + колонка + один config-флаг — небольшой прирост схемы (оправдан).
|
||||
- Порог `tracker_brd_review_cap_s` — эвристика: легитимный человеческий review длиннее
|
||||
2ч будет отображён как `~2ч`. Порог конфигурируем; компромисс «честность vs точность»
|
||||
в пользу неинтродуцирования аномального застоя в «твоё время».
|
||||
86
docs/work-items/ORCH-087/08-data-requirements.md
Normal file
86
docs/work-items/ORCH-087/08-data-requirements.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Требования к схеме БД — ORCH-087
|
||||
|
||||
Все изменения — **строго аддитивные и идемпотентные** (`CREATE TABLE IF NOT EXISTS`
|
||||
/ `_ensure_column`), restart-safe на живой ОБЩЕЙ прод-БД (SQLite). Данные
|
||||
enduro-trails не трогаются. Существующие колонки/таблицы не ломаются. Точка врезки —
|
||||
`src/db.py::init_db` (рядом с прочими `_ensure_column`/`executescript`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Колонка `agent_runs.effort` (BR-EFF, обязательно)
|
||||
|
||||
```python
|
||||
_ensure_column(conn, "agent_runs", "effort", "TEXT")
|
||||
```
|
||||
|
||||
- Тип `TEXT`, nullable. Хранит РЕАЛЬНО ушедшее в `--effort` значение
|
||||
(`low|medium|high|xhigh|max`) или `NULL`, если флаг не подавался (резолв вернул "").
|
||||
- Заполняется в `launcher._spawn` сразу после `resolve_agent_effort(agent,
|
||||
project_id)` через `UPDATE agent_runs SET effort=? WHERE id=run_id`
|
||||
(`effort or None`).
|
||||
- Читается в `render_task_tracker` (добавить `effort` в SELECT `agent_runs`).
|
||||
- Исторические строки (до миграции) → `effort IS NULL` → суффикс эффорта в карточке
|
||||
опускается; допустим fallback на `resolve_agent_effort(run["agent"])`.
|
||||
- Идемпотентность: `_ensure_column` — no-op при уже существующей колонке (AC-E.1,
|
||||
TC-09).
|
||||
|
||||
## 2. Таблица-леджер `tracker_messages` (BR-G1, вариант A1 ADR-001)
|
||||
|
||||
```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_at TEXT, -- NULL = карточка ещё жива (незакрыта)
|
||||
PRIMARY KEY (task_id, message_id)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_tracker_messages_open
|
||||
ON tracker_messages(task_id) WHERE deleted_at IS NULL;
|
||||
```
|
||||
|
||||
- Авторитетный учёт ВСЕХ созданных карточек задачи; `deleted_at IS NULL` ⇔ карточка
|
||||
считается живой и подлежит зачистке на следующем bump.
|
||||
- Логический FK на `tasks.id` без `REFERENCES` (зеркалит `jobs.task_id`/`job_deps`) —
|
||||
миграция не падает на pre-existing БД.
|
||||
- Частичный индекс `WHERE deleted_at IS NULL` — дешёвая выборка незакрытых mid в
|
||||
горячем пути рендера/зачистки.
|
||||
- `PRIMARY KEY (task_id, message_id)` — идемпотентность INSERT (повторный mid не
|
||||
дублируется); защита от двойного учёта при гонке.
|
||||
|
||||
**Новые геттеры/сеттеры в `src/db.py` (предложение, точная сигнатура — за разработчиком):**
|
||||
|
||||
| Функция | Назначение |
|
||||
|---------|-----------|
|
||||
| `add_tracker_message(task_id, message_id)` | INSERT нового mid (после успешного `send`). `INSERT OR IGNORE` для идемпотентности. |
|
||||
| `get_open_tracker_messages(task_id) -> list[int]` | Все `message_id` с `deleted_at IS NULL`. |
|
||||
| `mark_tracker_message_deleted(task_id, message_id)` | `UPDATE … SET deleted_at=datetime('now')` для успешно удалённых / «already gone». |
|
||||
|
||||
Контракт — как у существующих хелперов БД (never-raise по месту вызова в
|
||||
notifications: ошибка БД не валит конвейер).
|
||||
|
||||
### Сосуществование со скаляром `tasks.tracker_message_id`
|
||||
|
||||
- `tasks.tracker_message_id` **СОХРАНЯЕТСЯ** без изменения семантики — указатель на
|
||||
ТЕКУЩУЮ карточку (читатели `get_tracker_message_id`/`set_tracker_message_id` не
|
||||
трогаются). Обратная совместимость полная.
|
||||
- Леджер `tracker_messages` — НАДмножество: источник истины для зачистки сирот.
|
||||
- Одноразовый бэкфилл скаляра в леджер **не требуется** (старые сироты всё равно за
|
||||
48ч-окном Telegram). Новый поток ведёт леджер с нуля.
|
||||
|
||||
## 3. Что НЕ меняется
|
||||
|
||||
- `tasks` (кроме отсутствия изменений — скаляр сохранён), `jobs`, `events`,
|
||||
`job_deps`, прочие колонки `agent_runs` (`model`, токены, cost, exit_code) — без
|
||||
изменений.
|
||||
- Никаких `DROP`/`ALTER … DROP`/переименований/перетипизаций (SQLite-небезопасно на
|
||||
живой БД).
|
||||
- `STAGE_TRANSITIONS` / `QG_CHECKS` — вне зоны БД, не затрагиваются.
|
||||
|
||||
## 4. Идемпотентность и restart-safety (проверка)
|
||||
|
||||
- Двойной вызов `init_db` → без ошибок (`IF NOT EXISTS` / `_ensure_column` no-op) —
|
||||
TC-09.
|
||||
- Леджер переживает рестарт орка: незакрытые mid читаются из БД → следующий bump
|
||||
подчищает старые карточки (TC-05, AC-1.3).
|
||||
- Миграция на БД с существующими данными enduro: только добавляет колонку/таблицу,
|
||||
данные нетронуты (AC-X.5).
|
||||
29
docs/work-items/ORCH-087/10-tech-risks.md
Normal file
29
docs/work-items/ORCH-087/10-tech-risks.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Технические риски — ORCH-087
|
||||
|
||||
Зона изменений: `src/notifications.py`, `src/db.py`, `src/agents/launcher.py`,
|
||||
`src/usage.py`, тесты. Машина стадий и QG не затрагиваются. Контракт компонента —
|
||||
never-raise, карточка silent.
|
||||
|
||||
| ID | Риск | Вероятность / Влияние | Митигация |
|
||||
|----|------|------------------------|-----------|
|
||||
| R-1 | **Self-hosting:** задача правит инструмент в проде, обслуживающем enduro-trails из общей БД/очереди. Регресс пути нотификаций мог бы испортить наблюдаемость всех проектов. | Низк. / Сред. | never-raise сохранён по всему пути; обязательный `deploy-staging` (8501) гейт перед прод-деплоем; нотификации не на критическом пути конвейера (ошибка не валит стадии). |
|
||||
| R-2 | **Telegram 48ч-лимит:** сироты старше 48ч неудаляемы → могут остаться навсегда. | Сред. / Низк. | Документировано как known-limitation (ADR §Последствия, AC-1.4); `_DELETE_GONE_MARKERS` классифицирует как «gone» → исключает из учёта, не зацикливает ретраи. Касается только сирот ДО деплоя фикса. |
|
||||
| R-3 | **Гонка (d)** двух `update_task_tracker` (queue-worker / reconciler / reaper) → лишняя карточка на один переход. | Сред. / Низк. | Леджер самозалечивается на следующем bump (ADR Р-2); строго лучше текущего постоянного сиротства; кросс-процессный лок сознательно не вводится (цена > выгоды для silent-карточки). |
|
||||
| R-4 | **Миграция на живой общей прод-БД** (SQLite). Неаддитивная правка могла бы тронуть данные enduro. | Низк. / Выс. | Только `CREATE TABLE IF NOT EXISTS` / `_ensure_column` (идемпотентно, no-op при существовании); никаких DROP/ALTER DROP/переименований; логический FK без `REFERENCES` (не падает на pre-existing БД). TC-09 проверяет идемпотентность. |
|
||||
| R-5 | **BR-G6 / merge-gate:** ветка должна жить поверх свежего `origin/main` (ORCH-86); эрозия `reconciler.py` затёрла бы terminal-skip/`state_uuid`-dedup. | Низк. / Выс. | Подтверждено: origin/main — предок HEAD; 43 маркера ORCH-086 на месте; файлы ORCH-087 НЕ пересекают `reconciler.py`. `CHANGELOG.md` под `.gitattributes merge=union`. Явная проверка merge-gate — TC-19 (AC-6.1/6.2). |
|
||||
| R-6 | **Порог `tracker_brd_review_cap_s`** (дефолт 2ч): легитимный человеческий BRD-review длиннее 2ч отобразится как `~2ч` (недо-отчёт). | Сред. / Низк. | Конфигурируем (env); компромисс в пользу неинтродуцирования аномального застоя в «твоё время». Маркер `~` сигнализирует отсечку. Главная метрика (агенты) остаётся точной. |
|
||||
| R-7 | **Стамп эффорта в `_spawn`:** доп. `UPDATE agent_runs` сразу после INSERT мог бы упасть и сорвать запуск агента. | Низк. / Сред. | `UPDATE` по существующему `run_id` в уже открытом соединении; в худшем случае effort=NULL → суффикс опускается (рендер не падает, AC-E.4). Эффорт — наблюдаемость, не функциональность запуска. |
|
||||
| R-8 | **Регресс существующих тестов нотификаций** (новый формат строки стадии с эффортом + новая done-строка времени). | Сред. / Низк. | Обновить ожидания в `tests/test_notifications*.py`; новый формат строго аддитивен (суффикс эффорта/подписи времени). TC-11…TC-15. |
|
||||
| R-9 | **Live-overlay `confirm_deploy`:** новый ключ overlay при отсутствии UUID статуса в проекте мог бы шуметь/падать. | Низк. / Низк. | overlay never-raise, деградирует на offline-label при отсутствии UUID/ошибке; kill-switch `tracker_live_status`; без base-alias (реальный отдельный статус). |
|
||||
|
||||
## Острые точки внимания для разработчика
|
||||
|
||||
1. **Порядок в bump:** зачистка ВСЕХ открытых mid из леджера → `send` → INSERT+repoint
|
||||
ТОЛЬКО при `new_mid is not None` (R-3/BR-6). Ровно один `send` за вызов (R-4).
|
||||
2. **never-raise:** любая ошибка БД-леджера / Telegram внутри `update_task_tracker`
|
||||
гасится (как сейчас) — конвейер не падает (TC-16, AC-X.2).
|
||||
3. **Эффорт = фактический флаг:** хранить `resolve_agent_effort(...)` как ушло в
|
||||
`--effort` (пусто → NULL), а не пересчёт постфактум (CLI не возвращает эффорт).
|
||||
4. **Не трогать** `reconciler.py`/`tests/test_reconciler.py` (BR-G6).
|
||||
5. **Сохранить** `plane_issue_link` (ORCH-067) и `disable_web_page_preview` (ORCH-080)
|
||||
в payload (TC-17, AC-X.4).
|
||||
Reference in New Issue
Block a user