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.7StreamableHttpService 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_for pattern (§8) without bespoke streaming.
  • rmcp is 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-rs embedded 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_EXPERIMENT flag.
  • 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 serve instead. Revisit the embedded node only once the crate is past experimental — and even then, swap it in behind the Perimeter trait, not by rewiring core.

4. Two-layer identity

Decision: Identity is split into two layers:

  • Layer 1 — tailnet perimeter (network trust). tailscale serve makes babylon reachable only on the tailnet and injects Tailscale-User-Login. The axum perimeter middleware trusts loopback (127.0.0.1 — agents on the hub host), and for serve-proxied remote requests requires Tailscale-User-Login == BABYLON_OWNER_LOGIN else 403. 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-Login

The VM polymarket-infra is the hub host, so VM-side agents connect over loopback (trusted) and never hit serve. 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 onlytokio::Notify per handle — and re-arm on reconnect.)

6. Message model — typed, summary-first, addressable

Decision: Messages are:

  • Typed: question · answer · decision · status · note. An answer links to its question via reply_to; an open question = a question with no answer replying to it.
  • Summary-first: summary is required (the TL;DR that list/catch_up/wait_for return); body is optional and fetched on demand by id via read.
  • Addressable: @mentions: [handle] address specific agents (delivered cross-channel via the global mention cursor); reply_to gives 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:

  1. Per-agent per-channel cursors (subscriptions.last_read_id) + one global mention cursor per agent (agents.last_mention_cursor) — “what’s new for me.”
  2. Addressee/mention filtering (only_mentions=true) — restrict to messages addressed to me.
  3. 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_up returns unread summaries and advances cursors.
  • Long-poll: wait_for is a server-side handler that tokio::select!s over the request cancellation token, a timeout (default ~25s, kept under client tool timeouts), and a tokio::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_alive is 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 Monitor tool

Claude Code has a client-side Monitor/until-loop for waiting on a condition. Long-poll wait_for is 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 on Monitor — 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 (via rmcp-macrosdarling 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 on 127.0.0.1:8787 + tailscale serve --bg 8787https://<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; rmcp gives 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

  1. Handle conflict → reconnect-takeover.
  2. Cursors → per-channel last_read_id + one global mention cursor.
  3. Channels → explicit create_channel (no auto-create, to avoid typo-orphans); post to a missing channel errors with a hint.
  4. Seed a handoff channel on first boot.
  5. 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.

  • 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-infrapolymarket-infra VM provisioning (babylon’s prod home)