## Why Первый читающий маршрут (`GET /api/v1/metrics`) обошёлся дороже, чем выглядел: два прохода ревью измерили 693 мс и +153 МиБ живой кучи на враждебном запросе, а непрерывная запись вместе с четырьмя читающими транзакциями внахлёст дала рост `-wal` около 7 МБ/с без верхней границы (40 МБ за пять секунд). Приём живёт в том же процессе, и обе цены платит он: OOM убивает приём, а доставка, не попавшая в архив, телефоном не переприсылается. Третье проявление той же причины — повтор: спека каталога уже требует побайтового совпадения двух ответов на неизменившейся витрине, то есть ресурс по построению пригоден для условного запроса, а `ETag` не выставляется вовсе. Задача берётся **перед** Read API точек намеренно: тот строится поверх этой же машинерии, и решать один вопрос трижды (каталог, точки, MCP) нельзя. ## What Changes - **Периодический чекпойнт WAL.** Рядом с воркером свёртки живёт горутина, которая раз в минуту выполняет `PRAGMA wal_checkpoint(PASSIVE)` и останавливается дренированием, как воркер. Автоматический чекпойнт SQLite срабатывает только по концу записи, поэтому WAL, раздутый всплеском, остаётся неразобранным до следующей доставки — а ночью телефон молчит часами. - **Наблюдаемость непродвинувшегося чекпойнта.** Пассивный чекпойнт не идёт дальше снимка самого старого активного читателя и **ошибки при этом не возвращает**: измерено — `busy=0`, `log=6256`, `checkpointed=5`. Значит единственный различимый признак — «страниц в журнале много, перенесено меньше», и именно он идёт в `WARN` владельцу. - **Названный предел файла журнала.** `journal_size_limit` в строке подключения: пассивный чекпойнт возвращает страницы в базу, но файл оставляет на пике (измерено: 51 МБ до и после успешного чекпойнта на 12502 страницы). Роста это не ограничивает — усечение делает первая запись после полного чекпойнта, — и так и сказано в спеке. - **Версия витрины и условный запрос.** Хранилище отдаёт версию витрины по `PRAGMA data_version`, каталог выставляет `ETag`, а на `If-None-Match` с непротухшей версией отвечает `304` **не открывая снимок вовсе**. - **Не делается** (отложено): предел размера ответа и собственный дедлайн маршрута — их проектирует Read API точек; потоковое измерение по метрике; кеш ответа; `HEAD` на маршруте каталога. ## Capabilities ### New Capabilities Новых нет: обе части ложатся на существующие домены. ### Modified Capabilities - `storage`: добавляется **версия витрины** (признак изменения «в базу никто не коммитил»; монотонной она не является) и **обслуживание WAL** (чекпойнт по таймеру, признак непродвижения, остановка дренированием). - `catalog`: добавляется **условный запрос** — `ETag` на ответе каталога и `304` на `If-None-Match`, связанный с уже существующим требованием побайтового совпадения двух ответов на неизменившейся витрине. ## Impact - `internal/store` — закреплённое соединение-щуп для `data_version`, метод чекпойнта WAL, `journal_size_limit` в DSN. - `internal/catalog` — снимок каталога уезжает вместе с версией витрины. - `internal/httpapi` — общий помощник условного запроса (им же будут пользоваться точки и MCP), `ETag`/`304` на маршруте каталога. - `cmd/healthlog/serve.go` — горутина чекпойнта и её дренирование в общем бюджете остановки. - Схема базы **не меняется**: миграции нет. - Контракт приёма не меняется. Контракт чтения расширяется совместимо: клиент, не присылающий `If-None-Match`, получает ровно то же, что и сегодня.