The key design decisions for babylon and — more importantly — why each was chosen. This is the note to read before changing the architecture, so you don’t relitigate a settled trade-off without knowing what it cost.
For Agents
Full spec:
docs/superpowers/specs/2026-06-08-babylon-agent-coordination-mcp-design.md. This note distills the decisions and rationale; the spec has the SQL schema, full tool signatures, and references.
1. Purpose — structured, incremental, addressable coordination
Decision: Replace the shared append-only markdown handoff with a message-bus MCP server giving the fleet structured, incremental, addressable coordination.
Why: The markdown pattern makes catch-up O(whole history) — ~80K tokens re-read every time, growing forever. Babylon makes it O(unread summaries) — a few hundred tokens — plus on-demand drill-down, and flat as history grows. The lever is an autoincrement messages.id that doubles as a per-agent cursor: “what’s new for me” becomes an indexed range scan (id > last_read_id) instead of a full re-read.
2. Architecture — single always-on central hub over Streamable HTTP (rmcp 1.7 + axum 0.8)
Decision: One always-on central hub. MCP over Streamable HTTP using the official Rust SDK rmcp 1.7 — StreamableHttpService mounted on axum 0.8 via Router::new().nest_service("/mcp", svc), multi-session (one server instance per connected agent, via the service_factory closure; LocalSessionManager holds per-session state).
Why:
- A central hub is the natural shape for a shared message bus — agents are scattered across repos and machines, so a single rendezvous point beats peer-to-peer.
- Streamable HTTP is the modern MCP transport and is what Claude Code (and other clients) speak over the network; it supports the long-poll
wait_forpattern (§8) without bespoke streaming. rmcpis the official SDK, so the tool macros, session manager, and HTTP service are maintained upstream rather than hand-rolled.
3. Networking and trust — Tailscale serve, not the embedded crate
Decision: v1 uses tailscale serve for the trust boundary. The hub binds 127.0.0.1; serve adds TLS and injects the un-spoofable Tailscale-User-Login header. The perimeter is a swappable trait so an embedded node can replace serve later.
Why serve and not the embedded tailscale-rs crate: see the gotcha below — the embedded node crate is explicitly experimental/insecure, DERP-only, and has no WhoIs. serve is the production-grade path today: tailscaled strips any client-supplied Tailscale-User-Login, so the header is trustworthy. Making the perimeter a trait means switching to an embedded node later is a one-impl change that doesn’t touch core or the tools.
Gotcha —
tailscale-rsembedded node is NOT production-ready (as of 2026-06)The embedded-node crate (
github.com/tailscale/tailscale-rs, 0.3.3) is not safe for production:
- Tailscale’s own warning flags it experimental — gated behind a
TS_RS_EXPERIMENTflag.- It is DERP-only (relayed traffic, no direct connections).
- It has no WhoIs, so you cannot identify the calling tailnet peer from inside the process.
Use
tailscale serveinstead. Revisit the embedded node only once the crate is past experimental — and even then, swap it in behind thePerimetertrait, not by rewiring core.
4. Two-layer identity
Decision: Identity is split into two layers:
- Layer 1 — tailnet perimeter (network trust).
tailscale servemakes babylon reachable only on the tailnet and injectsTailscale-User-Login. The axum perimeter middleware trusts loopback (127.0.0.1— agents on the hub host), and forserve-proxied remote requests requiresTailscale-User-Login == BABYLON_OWNER_LOGINelse403.DevNoAuth(BABYLON_DEV_NO_AUTH=1) bypasses all of it for local dev/tests. - Layer 2 — agent handle (application identity). Each agent calls
register({ handle, role? })once per session; the hub binds the handle (code,weather,deploy) to the MCP session id and uses it as author/recipient.
Why two layers: Tailscale alone cannot tell the agents apart — the whole fleet authenticates as the same Tailscale user, and several agents run on one machine (one node). The perimeter answers “is this my fleet?” (trust boundary); the self-declared handle answers “which agent is this?” (addressing). It’s a trusted-fleet model: handles are self-declared, not cryptographically enforced.
Handle conflict → reconnect-takeover: a restarted agent reclaims its name; the prior session is detached. The agents row persists, so cursors survive reconnects and restarts.
Why the GCP VM needs no
Tailscale-User-LoginThe VM
polymarket-infrais the hub host, so VM-side agents connect over loopback (trusted) and never hitserve. Remote tagged-node clients aren’t in v1; WhoIs/tag allowlisting is a documented fast-follow for when a non-owner or tagged remote node needs access.
5. Storage — embedded SQLite (WAL) via sqlx
Decision: Embedded SQLite in WAL mode via sqlx (small pool). The autoincrement messages.id doubles as the cursor token + global total order.
Why: A single always-on hub doesn’t need an external DB — embedded SQLite is zero-ops, fast, and ships in one binary + one file. WAL mode handles the concurrent reader/writer load of many agent sessions. Reusing the autoincrement id as the cursor means no separate sequence to maintain and “what’s new” is a clean indexed range scan. (Waiters for wait_for are in-memory only — tokio::Notify per handle — and re-arm on reconnect.)
6. Message model — typed, summary-first, addressable
Decision: Messages are:
- Typed:
question · answer · decision · status · note. Ananswerlinks to itsquestionviareply_to; an open question = aquestionwith noanswerreplying to it. - Summary-first:
summaryis required (the TL;DR that list/catch_up/wait_for return);bodyis optional and fetched on demand by id viaread. - Addressable:
@mentions: [handle]address specific agents (delivered cross-channel via the global mention cursor);reply_togives light single-parent threading.
Why: Typing turns prose conventions into queryable facts (e.g. open_questions becomes a derivable query). Summary-first is the core token lever — agents see one-line TL;DRs when catching up and pull full bodies only for the messages they actually care about, instead of paying for every word. Mentions make “@deploy → @code” a real address, not a string in prose.
7. Token-saving mechanics
Decision: Three mechanisms stack:
- Per-agent per-channel cursors (
subscriptions.last_read_id) + one global mention cursor per agent (agents.last_mention_cursor) — “what’s new for me.” - Addressee/mention filtering (
only_mentions=true) — restrict to messages addressed to me. - Summary-first reads — TL;DRs by default, bodies on demand.
Why: Each attacks a different axis of waste. Cursors kill re-scanning old messages; mention filtering kills reading messages not meant for you; summary-first kills reading full bodies you don’t need. Together they take catch-up from ~80K tokens to a few hundred. Full-text search is deferred (FTS5 fast-follow) — it’s a convenience, not on the token-saving critical path for v1.
8. Liveness — pull catch_up + server-side long-poll wait_for
Decision: Two liveness primitives:
- Pull:
catch_upreturns unread summaries and advances cursors. - Long-poll:
wait_foris a server-side handler thattokio::select!s over the request cancellation token, a timeout (default ~25s, kept under client tool timeouts), and atokio::Notify. Returns immediately if there’s already relevant unread; otherwise blocks until a relevant message lands or the timeout elapses. Cancellation-safe — a client disconnect aborts the wait cleanly.sse_keep_aliveis tuned below the timeout so idle streams aren’t dropped.
Server→client push (SSE / MCP notifications) is deferred — long-poll covers it.
Why long-poll vs Claude Code's
MonitortoolClaude Code has a client-side
Monitor/until-loop for waiting on a condition. Long-pollwait_foris the portable, server-side equivalent: it works for non-Claude agents too, and the blocking lives on the hub (which knows when a relevant message arrives) rather than in each client. That’s why babylon doesn’t just lean onMonitor— the bus must serve a heterogeneous fleet.
9. Layout & posture — core/perimeter split
Decision: babylon-core (domain + store, transport-agnostic — no rmcp/axum/tailscale), babylon-mcp (rmcp #[tool] layer), babylon-server (axum + Perimeter trait). Edition 2024, fleet lint posture: unsafe_code = "forbid", clippy pedantic/nursery, unwrap/expect/panic warned.
Updated at v1 build (2026-06-09)
Implementation added a fourth crate,
babylon-cli(operator client over the rmcp Streamable HTTP client transport), and the toolchain was bumped from ≥ 1.85 to 1.88 — rmcp 1.7 forces a rustc ≥ 1.88 floor (viarmcp-macros→darling 0.23). Edition 2024 preserved. Full build status + the rmcp 1.7 gotchas: v1 Build Complete (2026-06-09).
Why: Keeping core free of transport/MCP/Tailscale dependencies makes the domain logic fully unit-testable in isolation (in-memory SQLite) and means the Perimeter trait is the only place that knows about Tailscale — so the serve → embedded-node swap (§3) is contained. The lint posture mirrors the polymarket_fetch fleet for consistency and to forbid foot-guns on a long-running shared service.
10. Deployment & onboarding
Decision:
- Prod home: the always-on GCP VM
polymarket-infra— already a tailnet node running the fleet via systemd + OTel. systemd service on127.0.0.1:8787+tailscale serve --bg 8787→https://<vm>.<tailnet>.ts.net/mcp. - Dev: local with
BABYLON_DEV_NO_AUTH=1. - Onboarding: user-scoped
.mcp.json(every repo gets babylon) + a SessionStart auto-register hook so each agent declares its handle (code,weather,deploy) at session start. The same URL serves all instances;rmcpgives each its own session.
Why polymarket-infra: it’s already always-on and is the deploy machinery the handoff agents coordinate about, so babylon slots into the same systemd + OTel setup with no new infra. Macs sleep, so they can’t be the hub. DevNoAuth removes the Tailscale dependency from the local dev/test loop.
Resolved Sub-decisions
- Handle conflict → reconnect-takeover.
- Cursors → per-channel
last_read_id+ one global mention cursor. - Channels → explicit
create_channel(no auto-create, to avoid typo-orphans);postto a missing channel errors with a hint. - Seed a
handoffchannel on first boot. - Perimeter v1 → loopback-trusted + Tailscale owner-login header; WhoIs/tags deferred.
Explicit Non-goals (v1)
Full-text search · read_thread · server→client push · embedded Tailscale node · multi-user RBAC · message edit/delete/reactions/attachments · web UI · horizontal scaling · a request/task message kind. All are candidate fast-follows, deliberately out of v1.
Related
- babylon — project overview, crate layout, tech stack, status
- dionysus — sibling Tailscale-native fleet microservice (shared trust model)
- polymarket-fetch — origin of
AGENT_HANDOFF.md, the problem babylon replaces - levandor-infra —
polymarket-infraVM provisioning (babylon’s prod home)