# Constellation-style telemetry for Minions — feasibility study

**Status:** Design proposal (not implemented). **Audience:** Contributors.
**Question answered:** Constellation (`~/code/constellation`) has a mature telemetry/usage
stack. Should Minions build something similar, and if so, what shape?

## TL;DR

A **1:1 port is the wrong goal.** Constellation's telemetry is built for a *hosted,
multi-tenant, server-side fleet*: PostgreSQL storage, WebSocket ingestion,
per-constellation/per-device tenancy, and a bearer-authed global-admin dashboard.
Minions is the structural opposite — *single-machine, local-first, zero-deps-beyond-Node,
JSON-file state, loopback dashboard*.

But the **valuable, portable subset is very feasible**, and Minions already has ~60% of it.
The realistic target is a **local-first usage/analytics layer**: a typed append-only event
log + a retention/rollup discipline + a dedicated dashboard "Usage" page, optionally a
`/metrics` Prometheus endpoint. **No database, no new runtime dependencies, no new network
egress** (preserves the "nothing leaves the machine" property — Minions' own code has no
external telemetry today).

## What Constellation does

Database-first, no external SaaS (no Kusto/App Insights/Sentry/Segment/Datadog). Events are
fire-and-forget INSERTs into PostgreSQL on the WebSocket hot path, spread across ~8 tables:

| Table | Retention | Role |
|---|---|---|
| `events` | 7d | Session/device/tool-call/auth lifecycle (`event_type` + JSON `payload`) |
| `sessions` | 30d | Session state + metadata |
| `execution_log` | 30d | Mission/agent runs: `status`, `cost_usd`, `duration_ms`, `error_category` |
| `session_launches` | 30d | Mission-wizard lifecycle + structured error capture (`error_code/category/message/stage`) |
| `app_events` | 30d | Cross-app analytics (e.g. PRism PR review/resolve) |
| `device_connection_log` | 90d | Per-day (device, agent_version) fingerprints |
| `metrics_daily` | **unlimited** | Rolled-up daily aggregates that survive retention |

Key design properties:

- **Rollup-before-delete invariant.** A 6-hourly scheduler (`packages/server/src/utils/retention.ts`
  `startCleanupScheduler`) runs `runMetricsRollup()` (`packages/server/src/utils/metrics-rollup.ts`)
  **before** `runDataCleanup()`. If the rollup throws, retention is skipped that tick — live rows
  are never deleted before they're aggregated. A sentinel watermark row tracks the last fully
  aggregated UTC date; today is excluded (live queries cover it).
- **Structured error taxonomy.** `WIZARD_ERROR_CATEGORIES` in `packages/shared/src/constants.ts`
  (`validation`, `parse_error`, `agent_unavailable`, `agent_error`, `file_write`, `unknown_tool`,
  `timeout`, `unknown`) drives failure dashboards.
- **Emit API.** Direct DB writes (`db.execute('INSERT INTO events ...')`) at ~60–70 call sites;
  a typed `logAuditEvent()` helper (`packages/server/src/utils/audit.ts`) for security events;
  a frontend `recordPrismEvent()` using `navigator.sendBeacon` →
  `POST /api/v1/prism/telemetry` (`packages/server/src/api/prism.ts`).
- **Admin surface.** `GET /api/v1/global-admin/overview?window=24h|3d|7d|30d|90d|all`
  (`packages/server/src/api/global-admin.ts`) returns windowed totals + daily time-series +
  top-failing missions + recent wizard failures; queries live tables inside retention and
  `metrics_daily` beyond it, merging by dimension. Separately, `GET /metrics`
  (`packages/server/src/api/metrics.ts`) exposes Prometheus counters from an in-memory registry
  (`terminal-metrics.ts`) — no Prometheus client library.
- **Privacy.** Path truncation + field redaction (`packages/shared/src/privacy.ts`), configurable
  retention windows, no sampling.
- **Dependencies.** Only telemetry-relevant runtime dep is `pg`. No analytics SDKs anywhere.

## What Minions already has (≈60%)

| Constellation concept | Minions equivalent today |
|---|---|
| `metrics_daily` rollups | `metrics.json._daily.<YYYY-MM-DD>` → `{costUsd, inputTokens, outputTokens, cacheRead, tasks}` |
| Per-entity aggregates | Per-agent counters: `tasksCompleted/Errored`, `prsCreated/Approved/Rejected/Merged`, `reviewsDone`, `totalCostUsd`, `totalInputTokens/OutputTokens/CacheRead`, `totalRuntimeMs`, `model`, `timedTasks` |
| App/engine analytics | `metrics.json._engine`: `command-center`, `agent-dispatch`, `consolidation`, `agent_memory_reconcile` |
| Emit helpers | `llm.trackEngineUsage()`, `trackReviewMetric()`, `updateMetrics()` (`engine/lifecycle.js`, `engine/llm.js`, `engine/shared.js`) |
| Structured error taxonomy | `FAILURE_CLASS` constants (recorded per work-item; not yet aggregated as a metric) |
| Diagnostic stream | `log.json` ring buffer (2500, secret-redacted via `redactSecrets()`) + `engine/dashboard-diagnostics.log` |
| Admin overview query | `getMetrics()` (`engine/queries.js`) surfaced via `/api/status` |

## The gap (the 40% worth building)

1. **No typed, append-only *event log*.** Minions has aggregate *counters* and a generic
   *diagnostic* log, but nothing like Constellation's `events`/`execution_log`/`session_launches`:
   domain events with `event_type` + structured `payload` + timestamp that can be **sliced over
   time** ("dispatches by type per day", "`failure_class` trend", "plan→PRD failures with full
   error text"). This is the single biggest capability difference.
2. **No dedicated usage/analytics *view*.** Dashboard pages are home/work/prs/plans/inbox/tools/
   schedule/watches/pipelines/meetings + qa/engine. There is no cost trend, failure-category
   breakdown, or throughput chart. `getMetrics()` data is collected but barely visualized.
3. **No retention discipline on a structured event stream.** The `_daily` rollup exists, but there
   is no domain-event log to retain/prune under the rollup-before-delete invariant.

## Portability matrix

| Constellation component | Verdict | Rationale |
|---|---|---|
| Typed event taxonomy + error categories | **Port (adapt)** | Reuse existing `WORK_TYPE` / `FAILURE_CLASS`; high value, low cost |
| Append-only event log | **Port → JSONL** | `engine/events.jsonl` (one event/line, atomic append); matches zero-dep ethos, not a DB table |
| Daily rollup + **rollup-before-delete** | **Adopt the discipline** | Generalize existing `_daily` to also fold event counts / failure categories; prune JSONL after rollup |
| Retention windows | **Port (config knob)** | `engine.telemetryRetentionDays`; reuse an existing `cleanup` tick phase |
| Usage/analytics dashboard page | **Port (scaled down)** | New `dashboard/pages/usage.html` + renderer over `getMetrics()` + rollups; inline SVG, no chart lib |
| `GET /metrics` Prometheus endpoint | **Optional, easy** | ~40 lines, in-memory text exposition |
| PostgreSQL backend | **Drop** | Violates "zero deps beyond Node built-ins"; JSON/JSONL suffice at single-machine volume. (See `design-state-storage.md` — if a DB ever lands, `node:sqlite` is the candidate, and this log would migrate with it.) |
| Multi-tenant constellation/device model | **Drop** | Minions is one fleet on one machine |
| WebSocket ingestion + `sendBeacon` | **Drop / reuse existing** | Engine already observes agents directly; dashboard already POSTs diagnostics |
| Bearer-auth global-admin | **Drop** | Local loopback dashboard, no tenancy |

## Recommended shape (phased, local-first)

### Phase 1 — Event log + emit helper (small, ~½ day)
Add `engine/telemetry.js` exporting `recordEvent(type, payload)` that appends a single redacted
JSON line to `engine/events.jsonl`. Event types live in a `TELEMETRY_EVENT` constant set (no magic
strings, per repo convention). Reuse `shared.redactSecrets()`. Wire ~10–15 high-value sites already
on the tick path: dispatch start/done/fail (+`failure_class`), PR poll outcomes, plan→PRD,
review verdicts, CC turns. Gitignored runtime file (add to the State Files list in `CLAUDE.md`).

### Phase 2 — Rollup + retention (small, ~½ day)
Extend the existing daily-rollup path so it also folds event counts + failure-category tallies into
`metrics.json._daily`. Prune `events.jsonl` lines older than `telemetryRetentionDays` **after** the
rollup write succeeds (Constellation's invariant). Slots into an existing periodic tick phase.
All `metrics.json` writes stay inside `mutateJsonFileLocked()`.

### Phase 3 — Usage dashboard page (medium, ~1–1.5 days)
New sidebar page reading `getMetrics()` + `_daily`: cost/token trend, dispatches-by-type,
`failure_class` breakdown, per-agent throughput, CC usage. Inline SVG sparklines — **no chart
dependency**. Follow the fragment-assembly model (`dashboard/pages/usage.html` + a
`dashboard/js/render-usage.js`) and bump `RENDER_VERSIONS` per the render-cache rules. Add a
Settings toggle if gated behind a feature flag.

### Phase 4 (optional) — `GET /metrics` (~2 hrs)
Prometheus text exposition of the same counters for external scrapers. Strict-CSP-exempt route like
the other HTML entry points; counters from an in-memory registry, no client library.

## Effort & risk

- **Effort:** Phase 1 ~½ day · Phase 2 ~½ day · Phase 3 ~1–1.5 days · Phase 4 ~2 hrs.
  **Total ≈ 2.5–3 days** for the full local-first system; **Phases 1–2 alone deliver the core
  capability in ~1 day.**
- **Risk: low.** No new deps, no schema migrations, no network surface, no new external egress.
  Watch-items: (a) JSONL append volume — bounded by retention + rollup; (b) `mutateJsonFileLocked`
  concurrency rules for the rollup write; (c) source-inspection tests for any new dashboard page;
  (d) CSP/route placement if Phase 4 ships.
- **Cross-repo:** lands on `yemi33` origin and auto-mirrors to `opg`; nothing here touches
  enterprise-only files.

## Honest caveats

- Constellation's *real* power is the **central warehouse aggregating many machines** — a SaaS
  capability Minions structurally cannot replicate without a server + DB + tenancy, which would
  break its design philosophy. If cross-machine fleet analytics is ever the actual goal, that is a
  separate, much larger project (a hosted collector), not a port. (`constellation-bridge.md`
  already defines a read-only cross-repo bridge; a telemetry collector would be a different beast.)
- Much of Constellation's table sprawl (`sessions`, `devices`, `app_events`) maps to things Minions
  either doesn't have or already tracks in JSON. **Don't recreate tables 1:1** — port the *event-log
  + rollup + view* pattern, not the schema.

## References

- Constellation rollup: `packages/server/src/utils/metrics-rollup.ts`,
  `packages/server/src/utils/retention.ts`
- Constellation emit/admin: `packages/server/src/utils/audit.ts`,
  `packages/server/src/api/global-admin.ts`, `packages/server/src/api/metrics.ts`,
  `packages/server/src/api/prism.ts`, `packages/dashboard/src/prism/prism-telemetry.ts`
- Constellation taxonomy/privacy: `packages/shared/src/constants.ts`,
  `packages/shared/src/privacy.ts`
- Minions today: `engine/metrics.json`, `getMetrics()` (`engine/queries.js`),
  `trackEngineUsage()` (`engine/llm.js`), `FAILURE_CLASS` (`engine/shared.js`),
  related design doc `design-state-storage.md`.
