# Требования к схеме БД — 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).