Перейти к основному содержимому

ADR 0097 — Прямое чтение схемы engine для "N агентов онлайн" на дашборде

  • Статус: Принят
  • Дата: 2026-08-18
  • Контекст: закрытие пункта backlog "N agentes online no dashboard" (RN-409), унаследованного из итогов dogfooding (FASE 13c)

Контекст​

Дашборду требовалось НАСТОЯЩЕЕ число живости — сколько агентов работают или имеют ожидающий решения пункт прямо сейчас, никогда не размер команды или историческое присутствие (уже принятое продуктовое решение, см. RN-409). Предварительное исследование подтвердило, что эти данные существовали лишь в ДВУХ разных местах, и ни одно не подходило дашборду в существующем виде:

  1. Настоящий живой статус существовал лишь на КЛИЕНТЕ, выводимый из event log (deriveAgentRoster), и только когда проект ОТКРЫТ, — дашборд перечисляет все проекты сразу, не открывая ни одной сессии.
  2. Текущее состояние каждого 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. Три причины:

  1. Свойство RN-090 — о SQL-запросах, а не о сетевых вызовах. HTTP-зависимость внутри DrizzleProjectsSummaryRepository — сегодня ЧИСТОГО репозитория чтения базы — ввела бы сетевой сбой в путь, который сегодня может упасть лишь при недоступном Postgres, а тест подсчёта запросов (pool.query) перестал бы видеть реальную стоимость вызова.
  2. Данные — СОСТОЯНИЕ, а не КОМАНДА. Соглашение о внутреннем HTTP существует для синхронизации ДЕЙСТВИЯ между двумя сторонами (активировать агента, перепроверить инструкцию) — чтение таблицы состояния, которая уже физически существует рядом, — тот же тип чтения, который api уже делает над собой, только через другую схему.
  3. Меньшая новая поверхность. Внутренний 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 не закрывает эту дверь; лишь объявляет, что ОДИН потребитель пакетного чтения пока не оплачивает стоимость её открытия.