Files
orchestrator/docs/work-items/ORCH-042/06-adr/ADR-001-tracker-bump-mode.md
claude-bot 0ac50b8c73
All checks were successful
CI / test (push) Successful in 12s
architect(ET): auto-commit from architect run_id=168
2026-06-06 10:05:26 +00:00

12 KiB
Raw Blame History

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 держит одну карточку, пересоздавая её внизу:

  1. если сохранён tracker_message_id — best-effort delete_telegram(старый_id);
  2. send_telegram(text, disable_notification=True) — новая карточка внизу, тихо;
  3. при успехе (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:trueTrue;
  • 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.