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