ADR 0097 — Прямое чтение схемы engine для "N агентов онлайн" на дашборде
- Статус: Принят
- Дата: 2026-08-18
- Контекст: закрытие пункта backlog "N agentes online no dashboard" (RN-409), унаследованного из итогов dogfooding (FASE 13c)
Контекст
Дашборду требовалось НАСТОЯЩЕЕ число живости — сколько агентов работают или имеют ожидающий решения пункт прямо сейчас, никогда не размер команды или историческое присутствие (уже принятое продуктовое решение, см. RN-409). Предварительное исследование подтвердило, что эти данные существовали лишь в ДВУХ разных местах, и ни одно не подходило дашборду в существующем виде:
- Настоящий живой статус существовал лишь на КЛИЕНТЕ, выводимый из event log
(
deriveAgentRoster), и только когда проект ОТКРЫТ, — дашборд перечисляет все проекты сразу, не открывая ни одной сессии. - Текущее состояние каждого dev agent уже сохраняется —
dev_agent_states, таблица ENGINE, в схеме Postgres"engine"(Ecto), с составным ключом(project_id, agent_id)иstatusодним из пяти значений (working/idle/idle_tripped/awaiting_gate/awaiting_approval, RN-047/ADR 0052).
api и engine разделяют ОДНУ физическую базу (brabo/brabo_test),
с ОДНИМ пользователем/ролью подключения (brabo), разделённые схемой — не
экземпляром и не учётными данными. Объявленное в
CLAUDE.md соглашение о коммуникации ("события через Postgres + внутренний HTTP с service token для
синхронных команд") покрывает ЭФФЕКТЫ — команды, меняющие состояние другой
стороны, — а не чтение отчёта. И уже есть прецедент прямого межсхемного
чтения: apps/api/scripts/medir-execucao.ts (FASE 13b) уже читает
engine.oban_peers сырым SQL тем же путём, чтобы обнаружить перезапуск
engine во время измеряемого выполнения, — только как ручной СКРИПТ, никогда
не покрытый автоматическим тестом.
Read model дашборда (DrizzleProjectsSummaryRepository, RN-090) требует
ОДНОГО агрегированного запроса на ВЕСЬ WORKSPACE, никогда одного на проект, —
это свойство, которое projects-summary.repository.spec.ts доказывает постоянным для
2 и 20 проектов.
Решение
Подсчёт онлайн dev agents читает engine.dev_agent_states НАПРЯМУЮ, через
сырой SQL пакетом по workspace (WHERE project_id IN (...) AND status NOT IN ('idle', 'idle_tripped') GROUP BY project_id), внутри ТОГО ЖЕ
Promise.all, который уже объединяет остальные одиннадцать запросов read model, —
поднимая шаблон medir-execucao.ts от ручного скрипта до протестированного продакшен-
кода (fixture таблицы в test/support/global-setup.ts, под той же
схемой "engine").
Рассмотренная и ОТВЕРГНУТАЯ альтернатива — открыть новый внутренний HTTP-маршрут в
engine (GET /internal/dev-agent-states/online-counts?projectIds=...) и
вызывать его из api. Три причины:
- Свойство RN-090 — о SQL-запросах, а не о сетевых
вызовах. HTTP-зависимость внутри
DrizzleProjectsSummaryRepository— сегодня ЧИСТОГО репозитория чтения базы — ввела бы сетевой сбой в путь, который сегодня может упасть лишь при недоступном Postgres, а тест подсчёта запросов (pool.query) перестал бы видеть реальную стоимость вызова. - Данные — СОСТОЯНИЕ, а не КОМАНДА. Соглашение о внутреннем HTTP существует для синхронизации ДЕЙСТВИЯ между двумя сторонами (активировать агента, перепроверить инструкцию) — чтение таблицы состояния, которая уже физически существует рядом, — тот же тип чтения, который api уже делает над собой, только через другую схему.
- Меньшая новая поверхность. Внутренний HTTP-маршрут потребовал бы аутентификации
через service token, собственного DTO, контрактного теста с обеих сторон — больший
вес, чем ещё один SQL-запрос внутри
Promise.all, который уже насчитывает четырнадцать.
Последствия
DrizzleProjectsSummaryRepositoryначинает зависеть от таблицы, которую api НЕ мигрирует. Принятое допущение: тот, кто эксплуатирует продукт, мигрирует обе стороны вместе (db:migrateИengine:migrate) — это уже неявное допущение всего остального продукта (безengine:migrateни один агент не поднимается, и дашборд в любом случае показал бы ноль истории). Если мигрировала только api, запрос ПАДАЕТ — нет try/catch, скрывающего ошибку; весь дашборд ошибся бы громко и видимо, а не карточка молча без числа.brabo_test(тестовая база vitest api) получила МИНИМАЛЬНУЮ fixture схемы"engine"—CREATE SCHEMA IF NOT EXISTS engine+CREATE TABLE dev_agent_states (project_id, agent_id, status)— вtest/support/global-setup.ts, ОБЪЯВЛЕННУЮ как частичная копия реальной миграции engine (apps/engine/priv/repo/migrations/20260724124356_create_dev_agent_states.exs), а не второй мигратор. Если реальная миграция изменит имя/тип этих двух колонок, эту fixture нужно будет обновить вручную — это объявленная цена этого решения.- Если однажды у engine появится БОЛЬШЕ потребителей состояния dev agent со стороны api (не только подсчёт), давление поднять это до собственного внутреннего HTTP- маршрута — с контрактом и тестом с обеих сторон — вырастет. Этот ADR не закрывает эту дверь; лишь объявляет, что ОДИН потребитель пакетного чтения пока не оплачивает стоимость её открытия.