12 KiB
ADR-001: Режим bump live-трекера через delete+send+repoint, edit как дефолт
Work Item: ORCH-042 · См. 01-brd.md, 02-trz.md, 03-acceptance-criteria.md, 10-tech-risks.md.
Статус
Accepted
Контекст
Live-tracker (src/notifications.py, ветка feat/telegram-live-tracker, Variant B+) держит ОДНУ карточку на задачу и редактирует её на месте (editMessageText) на каждом переходе стадии. Это сознательно убило прежнюю боль — «~15 отдельных карточек/дублей на задачу». Защита от дублей — главный инвариант компонента и не должна регрессировать.
Побочный эффект edit-режима: при активной переписке в чате карточка «тонет» вверху истории — актуальный статус задачи приходится искать скроллом. Слава просит альтернативу: карточка должна всегда быть последней в чате, но без возврата дублей и без звона на каждой стадии.
Дополнительно — косметика текста карточки (смесь EN-меток стадий с RU-текстом, «Ревью БРД», технический хвост deployed). Текстовые правки тривиальны и сами по себе архитектурного решения не требуют; ключевое решение — как реализовать новый режим, не сломав инвариант «одна карточка».
Ограничения окружения (см. CLAUDE.md, docs/operations/INFRA.md):
- Контракт компонента:
update_task_trackerи low-level helpers никогда не бросают (сбой нотификации не должен валить конвейер). - Self-hosting: правка инструмента, который сейчас в проде и обслуживает другие проекты из общей БД/очереди. Прод-рестарт self — только через
deploy-staging(8501). - Telegram Bot API:
deleteMessageне работает для сообщений старше 48 ч и для уже удалённых/недоступных — это нормальный ожидаемый исход, а не ошибка.
Решение
Р-1. Поведение задаётся конфиг-флагом, дефолт edit (нулевая регрессия)
Новое поле Settings.tracker_mode (env ORCH_TRACKER_MODE), значения edit | bump, дефолт edit. Резолюция режима — в notifications, case-insensitive + trim; всё, что не равно "bump" (включая пустое/мусор/None), трактуется как edit. Без явного включения bump поведение неотличимо от текущего → нулевая регрессия и безопасный фолбэк (оркестратор не падает на любом значении флага).
Р-2. Режим bump = delete + send + repoint, инвариант «одна карточка» сохраняется иначе
edit-режим держит одну карточку, редактируя её. bump держит одну карточку, пересоздавая её внизу:
- если сохранён
tracker_message_id— best-effortdelete_telegram(старый_id); send_telegram(text, disable_notification=True)— новая карточка внизу, тихо;- при успехе (
new_mid is not None) —set_tracker_message_idперенаправляется на новый id.
Итог: в чате всегда ровно одна карточка задачи, и она всегда последняя. За один вызов update_task_tracker отправляется не более одного нового сообщения → дублей в пределах вызова нет.
Р-3. delete — best-effort, никогда не блокирует отправку новой карточки
Новый low-level helper delete_telegram(message_id) -> bool с контрактом «never raises». Семантика возврата — «исчезло ли старое сообщение»:
ok:true→True;ok:falseс маркерами «уже нет / нельзя удалить» (message to delete not found,message can't be deleted,message_id_invalid, вынести в константу_DELETE_GONE_MARKERS) →True(не транзиент, сообщение и так недоступно);- прочий
ok:false/ 5xx / исключение (сеть/таймаут) →False+logger.warning; - нет токена/chat_id →
False, HTTP не выполняется.
Результат delete_telegram НЕ влияет на решение отправлять новую карточку — её шлём всегда (BR-6: delete-fail у сообщения >48 ч → всё равно новое). False означает лишь «старое, возможно, ещё живо»; на следующем переходе оно будет удалено повторно (или уже мёртво). Накопления карточек это не даёт, т.к. указатель всегда хранит ровно один id.
Р-4. repoint только при успешном send (анти-затирание указателя)
set_tracker_message_id вызывается только при new_mid is not None. Если send вернул None (нет кредов / транзиент 5xx/таймаут) — id не трогаем (не затираем на None): карточка перерисуется на следующем переходе, дубля нет (≤1 попытка send за вызов). Это симметрично существующему edit-fallback, который тоже не плодит сообщения при транзиенте.
Р-5. bump всегда тихий
Новая карточка отправляется с disable_notification=True — всплывает внизу, но без звука/пинга, как и edit сейчас. Состав отдельных НЕтихих пингов (approve-gate / error / deploy-fail / agent-fail) не меняется (вне scope).
Р-6. Текстовые правки — в одной точке, общие для обоих режимов
Правки (_BRD_LABEL → «Подтверждение BRD»; ✅ вместо ⏸️ после approve-gate; русские display-labels в _TRACKER_STAGES; _done_link → «Внедрено») затрагивают только отображаемые строки. Ключи стадий (analysis, …) и имена агентов (analyst, …) НЕ меняются — они завязаны на _STAGE_ACTIVE_AGENT, last_done, БД. Правка _TRACKER_STAGES в одном месте автоматически русифицирует и «✅ …», и «🔄 … идёт».
Что НЕ меняется (границы решения)
- БД: миграций нет, используется существующая колонка
tasks.tracker_message_idи хелперыget_tracker_message_id/set_tracker_message_id. →08-data-requirements.mdне требуется. - Инфраструктура / топология / порты / контейнеры — без изменений. →
07-infra-requirements.mdне требуется. - State machine (
src/stages.py), реестр QG (src/qg/checks.py), стадии, компоненты — без изменений. → глобальный (cross-cutting) ADR не требуется, решение локально для компонента notifications. - Сигнатуры
send_telegram/edit_telegram/update_task_tracker— без изменений (внешние вызовы изlauncher/stage_engineне трогаются). - Новых зависимостей нет (
httpxуже используется).
Альтернативы
- A1. Только bump, без флага. Отклонено: ломает обратную совместимость и единственного пользователя (Слава может предпочесть edit); рост риска регрессии защиты от дублей. Флаг с дефолтом
editдаёт мгновенный откат. - A2. Pin-сообщение (закрепить карточку). Отклонено: pin не решает «карточка внизу при переписке», шлёт системное уведомление о закреплении (звон), и усложняет API-контракт. Вне духа «тихого» трекера.
- A3. send-then-delete (сначала новое, потом удалить старое). Отклонено как дефолтный порядок: в окне между send и delete в чате видны ДВЕ карточки; при падении на delete остаётся осиротевшая старая → визуальный дубль. delete-then-send гарантирует ≤1 карточку в любой момент при нормальном пути и ≤1 новую отправку за вызов в любом случае.
- A4. Хранить историю/несколько карточек. Вне scope и противоречит исходному инварианту «одна карточка».
Последствия
Плюсы
- Слава получает актуальную карточку всегда внизу чата, одну на задачу, без звона.
- Нулевая регрессия по умолчанию (edit), мгновенный откат флагом.
- Контракт «never raises» и инвариант «одна карточка» сохранены в обоих режимах.
- Изменения локальны (
config.py+notifications.py), без миграций и без рестарта-критичных зависимостей.
Минусы / ограничения
- bump расходует Telegram API на 2 запроса вместо 1 (delete + send) на переход — для одного получателя несущественно (rate-limit Telegram не угрожает).
- При транзиентном delete-fail возможна кратко осиротевшая старая карточка до следующего перехода (она будет вычищена попыткой delete на следующем апдейте) — приемлемо, дублей всё равно не плодит.
- bump теряет визуальную «эволюцию на месте» edit-режима (история чата получает по карточке-замене) — но в чате всегда одна актуальная, что и требуется.
Риски — см. 10-tech-risks.md.
Связи
- BRD/ТЗ/AC:
docs/work-items/ORCH-042/01-brd.md,02-trz.md,03-acceptance-criteria.md; тест-план04-test-plan.yaml. - Компонент: live-tracker (
src/notifications.py),feat/telegram-live-tracker(Variant B+). - Контекст self-hosting / staging-страховка:
CLAUDE.md,docs/operations/INFRA.md,docs/architecture/adr/adr-0003-staging-gate.md. - Обновляемая дока (в том же PR, стадия development):
CHANGELOG.md,docs/architecture/internals.md(секция live-tracker: режимы +ORCH_TRACKER_MODE+delete_telegram),.env.example.