03 — Stack Candidates
Candidates designed 2026-07-19 by Fable from the phase-1 evidence; evidence at ./evidence/r1-research-digest.txt. Four candidates for the language/DB/web-overview surface across WorkLedger, Atlas, FleetOps, and Parity Harness under the hard constraint: lowest always-on memory/CPU on one WSL2 host running hundreds of concurrent Claude Code agents. This doc presents candidates only — no recommendation, no scoring matrix; that’s a later synthesis doc.
0. Shared baseline (all four candidates)
WorkLedger and Atlas stay Encore.ts + Drizzle + Postgres on the internal-plane substrate — pinned by plans/002-work-ledger-product/10-architecture.md and plans/003-atlas-intelligence-layer/10-architecture.md, not a footprint-driven choice. Both run as a built binary (encore build), not the dev daemon (encore run + Docker + encored), for always-on use — the audit’s single highest-leverage cut (process-count-audit). They share one tuned local Postgres (~130-150MB, shared_buffers≈32MB) instead of the two per-product instances each architecture doc independently assumed — this consolidation is a footprint decision layered on top of those docs, not something either doc itself weighed (repo-constraints). Parity Harness stays a TypeScript workspace package + CLI outside Encore (plans/002-dual-run-parity-harness/architecture.md binding, architect correction #4 — Cloud Run can’t run pinned-EOL images or tap MBUS); it runs batch/episodic (CI-triggered, not always-on), so it appears in every diagram below as an out-of-band node and is excluded from the always-on RSS budget entirely.
Observability is trimmed the same way in every candidate: no OTel Collector — Claude Code’s OTLP exporter points straight at each backend’s native OTLP endpoint (OTEL_EXPORTER_OTLP_METRICS_ENDPOINT override, documented in Claude Code’s own docs); VictoriaMetrics single-node (vmsingle) replaces Prometheus for metrics, native OTLP ingest, MetricsQL/PromQL-compatible, dashboard-25255-compatible; no Loki — content-free events live in FleetOps’s own SQLite, so there’s no log-shaped data volume to justify a log store; one shared Grafana instance, linked from each product’s own surface, never iframe-embedded.
Shared always-on RSS budget:
| Component | Estimated idle RSS | Evidence status |
|---|---|---|
Shared Postgres 16/17, tuned (shared_buffers≈32MB, small pool) |
~130-150MB | MEASURED — tuned-container benchmark (stevenpg.com) |
| Grafana, one shared instance, linked | ~180-330MB (v12.4.3 vs v13.0.1) | MEASURED — grafana/grafana#123017 |
| vmsingle, native OTLP ingest, low series count (4 products, handful of dashboards) | ~30-70MB | MODELED — downscaled from VM’s own ~850MB-at-1M-series figure; no idle-floor number exists in evidence at this corpus size |
| WorkLedger, Encore built binary | unresolved | UNVERIFIED — no published RSS number exists for encore build output anywhere in evidence (ts-bun-encore); Encore’s Rust core buys throughput, not documented memory savings |
| Atlas, Encore built binary | unresolved | UNVERIFIED, same gap |
| Total (audit’s own bottom line) | ~550-700MB | process-count-audit — the two Encore binaries are the unresolved slack inside this range, not independently summed here |
Measuring real encore build binary RSS is the single highest-value follow-up before committing to any candidate below — it’s the one number in the shared baseline nobody has measured yet.
Candidate A — All TypeScript
1. Summary. FleetOps collapses into one Bun/Hono single-file-compiled process: hook ingest, SSE board (htmx/Datastar), pollers, and the daily digest all run in-process, sharing one event loop and one SQLite (WAL) file. Maximum language coherence — every product in the fleet is TypeScript, sharing types across WorkLedger, Atlas, and FleetOps with zero FFI or subprocess boundary. Bun idle RSS is 35-45MB (ts-bun-encore), the heaviest of the three FleetOps runtime options but still an order of magnitude under a single Claude Code agent process (~156MB measured average, host-empirical). Chief risk: Bun.serve’s default 10-second idle-connection timeout kills quiet SSE streams unless explicitly configured (web-ui), and Bun is the heaviest of the three candidate runtimes for this specific slot.
2. Container diagram
flowchart LR
subgraph FLEET["Claude Code fleet, hundreds of concurrent agents"]
CC["Claude Code sessions"]
end
CC -- "OTLP metrics, direct" --> VM[("vmsingle")]
CC -- "hook events, stripped JSON" --> FO["FleetOps: Bun/Hono, one process<br/>ingest + SSE board + pollers + digest, in-process"]
FO --> SQLITE[("SQLite WAL<br/>HookEvent, Question, WatchItem")]
FO -- "SSE, htmx/Datastar" --> BOARD["Board UI, same process/port"]
WL["WorkLedger<br/>Encore built binary"] --> PG[("Shared Postgres, tuned")]
AT["Atlas<br/>Encore built binary"] --> PG
FO -. "read-only ledger views" .-> PG
HAR["Parity Harness<br/>TS workspace CLI, outside Encore, batch/episodic"] -. "CI-triggered, not always-on" .-> HAR
VM --> GRAF["Grafana, shared, linked"]
PG -. "PG datasource" .-> GRAF
SQLITE -. "SQLite datasource" .-> GRAF
BOARD -. "linked, not embedded" .-> GRAF
3. DDD boundary map
| Bounded context | Owns | Store | Written by | Read by |
|---|---|---|---|---|
| WorkLedger | Unit, attempt, disposition, cost | shared Postgres | WorkLedger service only | Atlas (cost join), FleetOps board (read-only views), Grafana |
| Atlas | Question, Answer, GLOSSARY renderings | shared Postgres | Atlas service only | FleetOps board, Grafana |
| FleetOps | Session, HookEvent, WatchItem, digest state | SQLite (in-process) | Bun/Hono process only | Board UI (same process), Grafana (SQLite datasource) |
| Observability | MetricSample (projection, no SSOT) | vmsingle | Claude Code OTLP export, direct | Grafana |
| Harness | fixture, verdict | own store, out of footprint scope | Harness CLI, CI-triggered | not read by any other context |
4. Test-seam story. TDD order: (a) ingest contract test — given a stripped hook JSON payload, assert the exact HookEvent row shape written to SQLite (Vitest, in-memory SQLite); (b) SSE snapshot test — given a fixed SQLite state, assert the exact frame(s) the Hono SSE handler emits to a connected client, catching the Bun.serve idle-timeout regression class directly; (c) board queue-derivation golden test — given a fixture set of Session+HookEvent+WatchItem rows, assert the derived attention-queue ordering matches a checked-in golden file (shared TS types make this fixture reusable against WorkLedger’s own Unit fixtures for cross-product integration tests later).
5. Evolution path + exit cost. Lowest lock-in of the three runtime candidates for this project because everything is one language family — but the highest rewrite cost if the footprint constraint tightens further, since Bun is already the heaviest of the three. A→B or A→C: the ingest+board logic is ~150-500 LOC (digest’s own sizing for a service this shape, rust-services) — a full rewrite in axum or Go, not a port, since SQLite schema and the hook/SSE contract are language-agnostic and carry over untouched. No DDD boundary changes on migration — only the FleetOps context’s implementation moves.
6. Cost (operator-days). ~1.5-2.5 op-days: smallest effective language surface (one runtime the team already runs everywhere else), no FFI/subprocess seam to design, TypeScript’s agentic-fit grade is mid-pack (B) but irrelevant here since there’s nothing new to learn.
7. Always-on RSS budget
| Component | Estimated idle RSS | Evidence status |
|---|---|---|
| Shared baseline (§0) | ~550-700MB | see §0 |
| FleetOps: Bun/Hono, one process, SQLite embedded | ~35-45MB idle | MEASURED — Bun 1.2 idle RSS, aggregated 2026 benchmarks; narrows toward ~110-180MB only under 1-2k req/s sustained load, not expected at this workload’s 1-5 SSE updates/sec |
| Candidate A total | ~585-745MB idle |
Candidate B — Rust local plane
1. Summary. FleetOps becomes one Rust axum binary: rust-embed serves the UI, rusqlite backs the SQLite store, SSE streams the board, tokio tasks run the pollers. The existing fleetops TUI (6,442 LOC Rust, host-empirical) is pulled in as a library crate, in-process — the only candidate that reuses fleetops’s session-liveness logic without a subprocess or serialization boundary. Real RSS floors for axum are the lowest of any candidate considered: 8.5MB under saturated Sharkbench load, though a separate community report shows a small axum service settling around ~160MB idle post-load under the default glibc allocator (rust-services) — the two numbers bracket a real range rather than converging on one, since neither measures this exact single-tenant, low-traffic workload shape. The named risk is not runtime footprint but two build/maintenance costs: cargo build grabs all CPU cores by default via the jobserver protocol and will compete with concurrent Node agent processes on the same host (mitigate with cargo check in the dev loop, CARGO_BUILD_JOBS caps, workspace split — not a reason to avoid Rust for a service this small); and Rust has the weakest agentic-maintenance evidence of the three languages — real-world repo-level issue resolution sits at 21-28% versus ~70% for Python (agentic-lang-fit), meaning long-term unattended extension by the fleet is the least-proven of the three.
2. Container diagram
flowchart LR
subgraph FLEET["Claude Code fleet, hundreds of concurrent agents"]
CC["Claude Code sessions"]
end
CC -- "OTLP metrics, direct" --> VM[("vmsingle")]
CC -- "hook events, stripped JSON" --> FO["FleetOps: Rust axum, one binary<br/>ingest + SSE board + pollers + digest<br/>fleetops TUI code, in-process library crate"]
FO --> SQLITE[("SQLite WAL, rusqlite<br/>HookEvent, Question, WatchItem")]
FO -- "SSE" --> BOARD["Board UI, rust-embed, same binary/port"]
WL["WorkLedger<br/>Encore built binary"] --> PG[("Shared Postgres, tuned")]
AT["Atlas<br/>Encore built binary"] --> PG
FO -. "read-only ledger views" .-> PG
HAR["Parity Harness<br/>TS workspace CLI, outside Encore, batch/episodic"] -. "CI-triggered, not always-on" .-> HAR
VM --> GRAF["Grafana, shared, linked"]
PG -. "PG datasource" .-> GRAF
SQLITE -. "SQLite datasource" .-> GRAF
BOARD -. "linked, not embedded" .-> GRAF
3. DDD boundary map
| Bounded context | Owns | Store | Written by | Read by |
|---|---|---|---|---|
| WorkLedger | Unit, attempt, disposition, cost | shared Postgres | WorkLedger service only | Atlas, FleetOps board, Grafana |
| Atlas | Question, Answer, GLOSSARY renderings | shared Postgres | Atlas service only | FleetOps board, Grafana |
| FleetOps | Session (now owned in-process, not polled), HookEvent, WatchItem, digest state | SQLite (in-process, rusqlite) | axum binary only | Board UI (same binary), Grafana (SQLite datasource) |
| Observability | MetricSample (projection) | vmsingle | Claude Code OTLP export, direct | Grafana |
| Harness | fixture, verdict | own store, out of footprint scope | Harness CLI, CI-triggered | not read by any other context |
Note the one real boundary shift versus A/C: because the TUI’s session-liveness logic is an in-process library crate rather than a polled subprocess, Session moves from “derived by polling an external tool” to “owned directly inside the FleetOps context” — a tighter coupling that only Rust’s in-process reuse makes possible.
4. Test-seam story. TDD order: (a) ingest contract test — axum’s own test-client (tower::ServiceExt) posts a stripped hook payload, asserts the rusqlite row; (b) library-crate integration test — the fleetops TUI’s own existing test patterns re-run against the in-process crate boundary, catching drift between the TUI’s session-state model and the web board’s expectations of it (the actual integration risk this candidate takes on); (c) SSE snapshot test via axum’s test client, same shape as Candidate A; (d) board queue-derivation golden test, same fixture-and-golden-file pattern as A, ported to Rust’s test harness.
5. Evolution path + exit cost. Highest exit cost of the three runtime candidates if Rust’s agentic-maintenance risk (21-28% resolve rate) materializes in practice — the in-process library-crate reuse is exactly the coupling that makes B→C the most expensive migration path of any pairing here: Go can’t link a Rust crate, so B→C means re-deriving fleetops’s session-liveness logic behind the fleet snapshot subprocess seam Candidate C uses instead, not a mechanical port. B→A is more tractable (drop the library-crate reuse, poll fleet snapshot like C, rewrite the thin service layer in TS) but still throws away the one thing B was chosen for.
6. Cost (operator-days). ~3-5 op-days: the library-crate integration (matching the TUI’s internal types across a crate boundary) is real work beyond the ~150-500 LOC service itself, and Rust’s weaker agentic-fit evidence means more human review time per PR, not just more agent iterations.
7. Always-on RSS budget
| Component | Estimated idle RSS | Evidence status |
|---|---|---|
| Shared baseline (§0) | ~550-700MB | see §0 |
| FleetOps: axum, one binary, rusqlite embedded | ~10-25MB (working estimate for this workload shape) | bracketed by MEASURED points that don’t converge: 8.5MB under saturated Sharkbench load vs ~160MB idle-settling under default glibc allocator in a separate community report (rust-services) — allocator choice (jemalloc/mimalloc) is a real lever if the high end applies here |
| Candidate B total | ~560-725MB idle (up to ~710-860MB if the higher glibc-allocator figure governs) |
Candidate C — Go local plane
1. Summary. FleetOps becomes one Go binary: go:embed serves the UI, mattn/go-sqlite3 (CGO — the box already has a C toolchain since it builds Go itself, and CGO is 1.1-2.1x faster on writes with no real downside, go-services) backs SQLite, net/http handles SSE. Rather than link fleetops in-process, C consumes it via the existing fleet snapshot subprocess/JSON seam — the same seam plans/004-fleet-monitor/06-architecture.md already specifies for the TUI-to-monitor boundary, so this candidate needs zero new integration design on that front. GOMEMLIMIT must be set explicitly since the Go runtime still doesn’t auto-detect cgroup/host memory limits (proposal golang/go#75164 remains unmerged, go-services) — a required manual config step for safe coexistence with hundreds of Node agent processes on the same host. Real single-binary Go+SQLite+embedded-UI precedents exist at meaningfully larger scale than this needs: PocketBase serves 10,000+ realtime connections at 10-150MB RSS on a $6 VPS, and Steve Yegge’s Beads is a directly-comparable AI-agent-fleet tool built in exactly this shape (Go, single binary, SQLite cache, case-studies). Risk: this is the third language in the overall program (TS for WorkLedger/Atlas/Harness, plus whatever B or D contribute), and the subprocess seam to fleetops — while already specified elsewhere — is a serialization boundary Candidate B avoids entirely.
2. Container diagram
flowchart LR
subgraph FLEET["Claude Code fleet, hundreds of concurrent agents"]
CC["Claude Code sessions"]
end
CC -- "OTLP metrics, direct" --> VM[("vmsingle")]
CC -- "hook events, stripped JSON" --> FO["FleetOps: Go, one binary<br/>ingest + SSE board + pollers + digest<br/>net/http, go:embed UI, GOMEMLIMIT set"]
FOTUI["fleetops TUI, Rust, reused as-is"] -- "fleet snapshot, JSON, subprocess" --> FO
FO --> SQLITE[("SQLite WAL, mattn/go-sqlite3<br/>HookEvent, Question, WatchItem")]
FO -- "SSE" --> BOARD["Board UI, go:embed, same binary/port"]
WL["WorkLedger<br/>Encore built binary"] --> PG[("Shared Postgres, tuned")]
AT["Atlas<br/>Encore built binary"] --> PG
FO -. "read-only ledger views" .-> PG
HAR["Parity Harness<br/>TS workspace CLI, outside Encore, batch/episodic"] -. "CI-triggered, not always-on" .-> HAR
VM --> GRAF["Grafana, shared, linked"]
PG -. "PG datasource" .-> GRAF
SQLITE -. "SQLite datasource" .-> GRAF
BOARD -. "linked, not embedded" .-> GRAF
3. DDD boundary map
| Bounded context | Owns | Store | Written by | Read by |
|---|---|---|---|---|
| WorkLedger | Unit, attempt, disposition, cost | shared Postgres | WorkLedger service only | Atlas, FleetOps board, Grafana |
| Atlas | Question, Answer, GLOSSARY renderings | shared Postgres | Atlas service only | FleetOps board, Grafana |
| FleetOps | HookEvent, WatchItem, digest state | SQLite (in-process, mattn/go-sqlite3) | Go binary only | Board UI (same binary), Grafana (SQLite datasource) |
| fleetops TUI (external) | Session (liveness/status), owns its own source (~/.claude/sessions/*.json + /proc) |
none — computed at read time | fleetops binary itself | FleetOps context, via fleet snapshot poll only — never written back to |
| Observability | MetricSample (projection) | vmsingle | Claude Code OTLP export, direct | Grafana |
| Harness | fixture, verdict | own store, out of footprint scope | Harness CLI, CI-triggered | not read by any other context |
Same projection discipline as plans/004-fleet-monitor/06-architecture.md §4: FleetOps never writes back into the TUI’s session state, only polls it.
4. Test-seam story. TDD order: (a) ingest contract test — Go table-driven test against the net/http handler, given a stripped hook payload, asserts the SQLite row (httptest.NewServer + in-memory or temp-file SQLite); (b) fleet snapshot schema/contract test — the actual named risk of this candidate: a fixture JSON matching fleetops’s current snapshot shape is checked in and asserted against on every FleetOps build, so a silent shape change in the TUI’s output is caught at build time, not at runtime (this is the seam Candidate B doesn’t need because it isn’t a subprocess boundary there); (c) SSE snapshot test via httptest, same shape as A/B; (d) board queue-derivation golden test, same pattern, Go idiom.
5. Evolution path + exit cost. Middle exit cost. C→B is expensive in the same direction B→C is (the subprocess seam and the library-crate boundary aren’t mechanically interchangeable — moving to in-process reuse means rewriting the FleetOps binary in Rust, not just changing how it talks to fleetops). C→A is a same-shape rewrite to Bun/Hono of a ~150-500 LOC service; the fleet snapshot JSON contract carries over unchanged since it’s already language-agnostic (it’s how Candidate A would also integrate with fleetops if the team wanted a fleetops-derived Session signal in that candidate too, though A’s diagram above doesn’t include it since the brief scoped A/B’s Session sourcing differently).
6. Cost (operator-days). ~2-3 op-days: Go’s near-instant compile loop and terse, one-idiom style keep agent iteration count low despite being a new-to-this-program language; the subprocess seam is zero-design-cost since it’s already specified in plans/004-fleet-monitor/06-architecture.md, but it does add one integration test class (the schema contract test) the other two candidates don’t need.
7. Always-on RSS budget
| Component | Estimated idle RSS | Evidence status |
|---|---|---|
| Shared baseline (§0) | ~550-700MB | see §0 |
| FleetOps: Go, one binary, mattn/go-sqlite3 embedded | ~15-40MB (working estimate for this workload shape) | MEASURED-adjacent — multiple independent Go-service measurements converge in a 6-64MB band for minimal services at comparable request rates (go-services); GoDoxy’s 150-200MB is a heavier reverse-proxy shape, not representative here |
| Candidate C total | ~565-740MB idle |
Candidate D — Do less: TUI + Grafana only
1. Summary. No new web app at all. The fleetops TUI gains an attention-queue pane (extending the existing 6,442-LOC Rust tool, not a new binary). The hook command writes SQLite directly — no ingest daemon in between — using busy_timeout to absorb write contention. Pollers and the digest script become host cron jobs instead of an always-on process. Grafana is the only web surface: dashboard 25255 plus a SQLite datasource (for HookEvent/WatchItem) and the shared Postgres datasource (for WorkLedger/Atlas). This is the only candidate with zero new always-on services — the marginal RSS cost of the FleetOps slot is exactly zero. The tradeoffs are real, not free: there is no attention-router web UX at all (Grafana panels, not a purpose-built queue view); and hundreds of concurrent Claude Code agents firing hooks means hundreds of processes writing to one SQLite file directly, near the documented envelope where WAL concurrency degrades — errors (“database is locked”) become common past roughly 100 concurrent writers even though throughput itself stays stable into the tens of thousands of ops/sec through 128 threads (db-layer). busy_timeout converts lock contention into retries/latency rather than hard failures, but this candidate is the only one that pushes fleet-scale write concurrency directly at SQLite’s single-writer model with no daemon to serialize or batch it first.
2. Container diagram
flowchart LR
subgraph FLEET["Claude Code fleet, hundreds of concurrent agents"]
CC["Claude Code sessions"]
end
CC -- "OTLP metrics, direct" --> VM[("vmsingle")]
CC -- "hook command, direct write<br/>busy_timeout, no daemon" --> SQLITE[("SQLite WAL<br/>HookEvent, Question, WatchItem")]
TUI["fleetops TUI<br/>+ attention-queue pane"] -- "session json + /proc" --> TUI
TUI -. "reads" .-> SQLITE
CRON["host cron: pollers + digest script"] --> SQLITE
CRON -- "webhook" --> GCHAT[("Google Chat")]
WL["WorkLedger<br/>Encore built binary"] --> PG[("Shared Postgres, tuned")]
AT["Atlas<br/>Encore built binary"] --> PG
HAR["Parity Harness<br/>TS workspace CLI, outside Encore, batch/episodic"] -. "CI-triggered, not always-on" .-> HAR
VM --> GRAF["Grafana — the ONLY web surface<br/>dashboard 25255 + SQLite datasource + PG datasource"]
PG -. "PG datasource" .-> GRAF
SQLITE -. "SQLite datasource" .-> GRAF
3. DDD boundary map
| Bounded context | Owns | Store | Written by | Read by |
|---|---|---|---|---|
| WorkLedger | Unit, attempt, disposition, cost | shared Postgres | WorkLedger service only | Atlas, Grafana |
| Atlas | Question, Answer, GLOSSARY renderings | shared Postgres | Atlas service only | Grafana |
| FleetOps | HookEvent, WatchItem, digest state | SQLite (no owning process — multi-writer: hooks + cron pollers + TUI, each independently) | hook command (direct), cron pollers (direct) | fleetops TUI, Grafana (SQLite datasource) — no board UI context exists in this candidate |
| fleetops TUI (extended) | Session (liveness/status) + attention-queue derivation (new pane) | none — computed at read time from ~/.claude/sessions/*.json, /proc, and SQLite reads |
fleetops binary itself | Robert, interactively |
| Observability | MetricSample (projection) | vmsingle | Claude Code OTLP export, direct | Grafana |
| Harness | fixture, verdict | own store, out of footprint scope | Harness CLI, CI-triggered | not read by any other context |
The structural difference from A/B/C: FleetOps has no owning process. SQLite is written by multiple independent writers (hook command, cron pollers) with no single service serializing access — the tradeoff this candidate makes explicitly to avoid an always-on daemon.
4. Test-seam story. TDD order, and the priority is inverted from A/B/C: (a) direct-write concurrency test first — this is the load-bearing risk, not an afterthought: a test harness spawns N concurrent writers against the real SQLite file with busy_timeout set, asserts zero lost writes and bounds retry latency at a realistic concurrency level before this candidate can be trusted at fleet scale; (b) attention-queue derivation golden test in the TUI’s own Rust test harness — same fixture-and-golden-file shape as the board queue-derivation test in A/B/C, just rendered as a TUI pane instead of a web board; (c) Grafana provisioning smoke test — dashboard JSON and datasource config are checked in as code; a smoke test asserts Grafana starts and both the SQLite and Postgres datasources resolve, since there’s no application-level test surface for “the only web view” otherwise.
5. Evolution path + exit cost. Lowest exit cost of all four candidates. D→any of A/B/C is purely additive: the SQLite schema and hook payload shape already exist and don’t change; the only new thing is introducing an always-on daemon to serialize what direct writes currently do independently, plus a purpose-built board UI. The TUI’s attention-queue pane stays as a secondary consumer afterward — nothing built for D needs to be ripped out to adopt any other candidate later.
6. Cost (operator-days). ~0.5-1 op-day: almost entirely config and wiring (cron jobs, Grafana datasource/dashboard provisioning, one new TUI pane reusing logic the TUI already has for session state) — no new service to design, build, or test-harness from scratch. The concurrency test in §4a is the one piece of real engineering work in this candidate, and it’s a prerequisite, not optional polish.
7. Always-on RSS budget
| Component | Estimated idle RSS | Evidence status |
|---|---|---|
| Shared baseline (§0) | ~550-700MB | see §0 |
| FleetOps: no new always-on service. fleetops TUI is an interactive foreground tool run when Robert has a terminal open, same as today — not part of the always-on daemon budget. | +0MB | N/A — this is the candidate’s defining property |
| Candidate D total | ~550-700MB idle, unchanged from the shared baseline |