12 KiB
02-ТЗ — ORCH-087
Техническое задание. Конкретные изменения кода/БД. Архитектурное решение (включая выбор
bump-с-зачисткой vs edit, схему хранения message_id'ов, вывод G0) принимает архитектор и
фиксирует в 06-adr/. Ниже — границы и обязательные требования к реализации.
⚠️ Порядок работ жёсткий: G0 (расследование → ADR) ПЕРВЫМ, фикс G1–G3 и расширение G4 — только после. Фикс вслепую без подтверждённой механики запрещён (см. BRD §3).
0. Задействованные модули src/
src/notifications.py—update_task_tracker(веткаmode == "bump"),render_task_tracker(_stage_line— строка стадии), низкоуровневыеsend_telegram/delete_telegram.src/db.py— хранение message_id'ов задачи (G1) + миграцияagent_runs.effort(G4), геттеры/сеттеры.src/agents/launcher.py—_spawn: стамп фактически применённогоeffortвagent_runs(G4).src/config.py— при необходимости новый kill-switch (напримерtracker_orphan_sweep_enabled).tests/— новые/расширенные тесты (см.04-test-plan.yaml).
1. G0 — расследование (артефакт: 06-adr/)
Не код. Архитектор по данным логов/Telegram/staging-прогона отвечает на R-1…R-4 (BRD §3) и выносит
решение: остаётся bump с гарантированной зачисткой ИЛИ переход на edit по умолчанию. Всё
последующее ТЗ ниже описывает требования, инвариантные к этому выбору, и явно помечает пункты,
зависящие от него.
2. G1 — не оставлять сирот (anti-orphan)
Требование: при bump-обновлении в чате не должно оставаться более одной живой карточки задачи;
старые message_id, не подтверждённые удалёнными, должны подчищаться.
Допустимые подходы (архитектор выбирает один; рекомендация — Б как наиболее устойчивый):
- Подход А (только указатель + строгий swap):
set_tracker_message_id(new_mid)ТОЛЬКО при успешномdeleteстарого ИЛИ при подтверждённом «уже нет» — но это не покрывает рестарт между delete и send. Недостаточно сам по себе. - Подход Б (реестр всех message_id — рекомендуется): хранить ВСЕ созданные message_id задачи и
их состояние. Перед/после успешного
sendнового — подчистить все ранее не удалённые. Реализация:- Новая аддитивная таблица, например
tracker_messages(task_id INTEGER, message_id INTEGER, created_at TEXT, deleted INTEGER DEFAULT 0, PRIMARY KEY(task_id, message_id))черезCREATE TABLE IF NOT EXISTS(идемпотентно, не трогаетtasks/agent_runs/enduro-данные). - При
sendнового — записать его в реестр; при удачномdeleteстарого — пометитьdeleted=1. - Sweep незакрытых: перед отправкой свежей карточки (или фоновым проходом) пытаться удалить все
deleted=0записи задачи, кроме актуальногоtracker_message_id; успешный/«уже-нет» delete →deleted=1. 48ч-«can't be deleted» (_DELETE_GONE_MARKERS) → тоже пометитьdeleted=1(наша проблема исчерпана) + лог. set_tracker_message_idобновляется ТОЛЬКО приnew_mid is not None(как сейчас — не обнулять указатель при transient send-failure).
- Новая аддитивная таблица, например
- Подход В (переход на
edit): сменить дефолтtracker_modeнаedit(правка одного сообщения in-place) — структурно исключает сирот, но теряет «карточка внизу чата» (фича ORCH-042). Допустим ТОЛЬКО если G0 докажет, что выгода против сирот перевешивает (решение — ADR).
Инвариант (любой подход): update_task_tracker остаётся never-raise; ни один сетевой сбой
delete/send не валит конвейер; карточка всегда disable_notification=True.
3. G2 — заголовок отражает текущую стадию
- При живой (не осиротевшей) карточке заголовок/статус-строка строятся
render_task_tracker→_card_status_label→plane_status_label(task_row)изtasks.stageна момент рендера. Эта часть уже корректна (код-аудит). Реальная причина «застывания» — сирота (G1). Требование G2 удовлетворяется устранением сирот (G1): единственная живая карточка по определению отрисована на текущей стадии. - Доп. требование: убедиться (тест R-3), что при ПЕРЕрисовке (bump) свежая карточка всегда несёт
актуальный
plane_status_label— регрессионный тест «stage меняется → статус-строка меняется».
4. G3 — статусы deploy-цикла видимы
- Offline-ядро уже даёт для
stage="deploy"→ «⏸️ Awaiting Deploy — ожидание Confirm Deploy» (_STAGE_STATUS_LABEL), дляdone→ «Done». - Ветки
Deploying/Monitoring after Deploy/Confirm Deployрисуются live-overlay (_live_plane_branch_override, kill-switchtracker_live_status). Требование: проверить тестами, что весь deploy-цикл покрыт (offline-label дляdeploy/done; overlay-маппинг дляdeploying/monitoring). Если обнаружится дыра покрытия — закрыть в_STAGE_STATUS_LABEL/_LIVE_BRANCH_LABELSбез изменения статусной модели ORCH-066. - Изменения
STAGE_TRANSITIONS/plane_syncключей НЕ требуются.
5. G4 — эффорт в строке стадии
5.1 Схема БД (src/db.py)
- Добавить колонку
agent_runs.effort TEXTидемпотентной миграцией:_ensure_column(conn, "agent_runs", "effort", "TEXT")(рядом с..."model"...). NULL для старых строк — рендер деградирует (см. 5.3).
5.2 Стамп фактического эффорта (src/agents/launcher._spawn)
- В
_spawnэффорт уже резолвится:effort = resolve_agent_effort(agent, project_id)(стр. ~475). Записать ФАКТИЧЕСКИ применённое значение вagent_runsстрокой run'а. Варианты:- расширить начальный
INSERT INTO agent_runs (task_id, agent)→(task_id, agent, effort)сeffort(то, что ушло в--effort; пустая строка""при omit → хранить NULL или""); ИЛИ - отдельный
UPDATE agent_runs SET effort=? WHERE id=?сразу после полученияrun_id.
- расширить начальный
- Источник — РОВНО та строка
effort, что формируетeffort_flag(фактически в CLI), НЕ повторныйresolve_agent_effortна рендере. Еслиeffort==""(флаг опущен) — хранить пустое/NULL. - Никогда не валить
_spawnиз-за записи эффорта (контракт launcher).
5.3 Рендер (src/notifications.render_task_tracker._stage_line)
- В
SELECT ... FROM agent_runs(стр. ~322) добавитьeffortв выборку. - В
_stage_lineпослеmodelдобавить эффорт. Формат — компактный, рядом с моделью; рекомендуемый:… · {model} · {effort}при наличии обоих; при пустомeffort— НЕ добавлять суффикс (как уже делаетmodel_suffixдля пустой модели). Точный разделитель (·vs/) — на усмотрение архитектора, но единообразно. - Деградация:
effort is None/""→ строка как сейчас (без эффорт-суффикса), без падения.
6. Изменения API
Нет внешних HTTP API. Внутренние сигнатуры:
- Возможен новый helper в
db.pyдля реестра message_id (G1, подход Б): напримерrecord_tracker_message(task_id, message_id),mark_tracker_message_deleted(task_id, message_id),list_live_tracker_messages(task_id) -> list[int]. Сигнатуры — на усмотрение архитектора. get_tracker_message_id/set_tracker_message_id— сохраняются (обратная совместимость).
7. Изменения схемы БД (сводно, всё аддитивно/идемпотентно)
agent_runs.effort TEXT(G4) —_ensure_column.- (Подход Б)
tracker_messages(...)—CREATE TABLE IF NOT EXISTS. - Никаких изменений существующих колонок; нулевая регрессия для enduro-trails в общей БД.
8. Требования к новым QG checks
Нет. Изменения только в слое нотификаций/БД; QG_CHECKS, гейты, машина стадий не трогаются.
9. Конфигурация (src/config.py)
- При подходе Б — опциональный kill-switch
tracker_orphan_sweep_enabled: bool = True(envORCH_TRACKER_ORPHAN_SWEEP_ENABLED) для безопасного раската/отката зачистки. - При подходе В — менять дефолт
tracker_mode(сейчас"bump"). Решение — ADR.
10. Артефакты pipeline (обновить в ТОМ ЖЕ PR)
CLAUDE.md— секция «Нотификации / Telegram live-tracker»: описать anti-orphan-зачистку и эффорт-в-строке-стадии.docs/architecture/README.md— компонент «Notifications / Live-tracker»: то же.CHANGELOG.md—## [Unreleased](union-merge, ORCH-073).06-adr/ADR-NNN-*.md— вывод G0 + принятое решение (bump-зачистка vs edit) + схема хранения.- При необходимости
08-data-requirements.md(схемаtracker_messages/agent_runs.effort).
11. Инварианты (не нарушать)
- never-raise во всём слое нотификаций; конвейер не блокируется сетевыми сбоями Telegram.
- «Одна карточка на задачу» — после устранения сирот строго одна живая карточка.
- Карточка тихая (
disable_notification=True); пингуют только alert-хелперы. disable_web_page_preview=True(ORCH-080) и кликабельныйplane_issue_link(ORCH-067) сохранены.- Миграции идемпотентны и безопасны на живой общей прод-БД.