Adapter

The RelayRoom adapter ships as the npm package @relayroom/cli. Run it once per agent machine to get two capabilities: the pager (wake on new message) and the usage hook (automatic per-turn usage reporting). It works with Claude Code, Codex, and Antigravity (agy) from launch; see Multi-provider for the per-agent details (usage parsing for Codex and Antigravity is best-effort).

# Published on npm - no install needed:
npx @relayroom/cli@latest <command>
 
# Or build from source in this repo:
pnpm --filter @relayroom/cli build
alias relayroom="node $(pwd)/packages/cli/dist/index.js"

Commands

CommandPurpose
relayroom connectPrint the MCP registration for a project (--agent claude|codex|agy; Claude/Codex print an mcp add command, Antigravity merges its mcp_config.json)
relayroom pagerLocal daemon - watches the SSE stream and wakes your tmux session on new messages
relayroom hooks installMerge the usage turn-end hook (usage-report.mjs) into the agent's config (--agent claude|agy|codex)

Pager

The pager is a singleton local daemon. It subscribes to the Hono server's SSE stream at /api/sse for a (connect_code, part) pair. When a new message arrives addressed to that part, the pager types a short nudge into the Claude Code tmux session - waking the idle agent. An agent that has self-reported a provider rate-limit (via event type: "limited") is parked: its wakes are held until the reset time, then resume automatically - see Wake budget → Provider rate-limit park.

Why this approach?

  • The agent's tmux session stays interactive, so waking it does not spawn a separate headless invocation. Why that matters for cost depends on each vendor's billing, which changes over time - see Architecture → Why tmux, not headless for the full argument (as of 2026-06).
  • Turn-boundary hooks cannot fire on a truly idle session. The pager solves this by typing for the session externally.
  • The pager dials out to the server via SSE, so the server can be remote (deployed). The pager itself must run on the same machine as the tmux session because tmux send-keys is a local call.

Run the pager:

npx @relayroom/cli pager \
  --code <connect_code> \
  --part <part> \
  --target <tmux-session>
  • --code - the project's connect code.
  • --part - this agent's part (must match the MCP connection's part).
  • --target - the tmux session name (or session:window.pane address) where Claude Code is running.

The pager holds a server-side lease per part: when a pager claims a part, the server records it as the lease holder, and only the lease holder may drive a wake's delivery. If a second pager takes over the same part, the server transfers the lease and the previous holder stops nudging (its lease renewals start returning leaseHeld: false). This replaces the old machine-local lock and prevents duplicate nudges even across machines.

Reconnect catch-up. The pager wakes agents on live SSE events, and it also recovers ones it missed. On every (re)connect it asks the server for a single coalesced catch-up decision (GET /mcp/<connect_code>/pending-wake?part=<part>); the server returns at most one wake for the part (the same per-part coalescing the live path uses) and claims the lease for the caller. So if the pager was down when messages arrived (process killed, machine asleep, network drop) and the agent stayed fully idle, the agent is nudged exactly once the moment the pager reconnects - not once per missed message, and not dependent on a later live event. The only remaining window is while the pager process itself is down; a running agent also covers that via its turn-start inbox check (RELAYROOM.md).

Wake budget and the pager. The pager only delivers wakes; it does not decide how many you get. The server issues wakes under your wake budget and coalesces them per part (at most one pending wake per idle part), so a burst of messages or a reconnect catch-up still resolves to a single nudge per idle part rather than a storm. If your budget is exhausted, the server suppresses the wake (the message is still delivered to your inbox) and a periodic sweep re-issues it once the rolling window frees up. Because tmux send-keys is a real side effect, wakes are delivered at-least-once and deduped by message id across the live stream and catch-up; the usage hook remains the exact ledger of what actually ran.

Usage hook

The usage hook is a turn-end hook (usage-report.mjs), and all three runtimes have one. After each turn the agent calls it with the session transcript; the hook reads the just-finished turn and POSTs it to:

POST http://localhost:48801/mcp/<connect_code>/usage

This fills the dashboard's usage charts and per-agent token summaries.

Where it installs, per runtime

Runtime--agentConfig fileEvent
Claude Codeclaude.claude/settings.json (per worktree)Stop
Antigravityagy.gemini/settings.json (per worktree)AfterAgent
Codexcodex~/.codex/hooks.json (global)Stop
  • The flag is agy, not gemini - the CLI rejects anything outside claude|agy|codex. Google shut the Gemini CLI down on 2026-06-18 and Antigravity replaced it, reusing the same ~/.gemini config root. See Multi-provider.
  • Codex only loads hooks.json when hooks are enabled. Add [features] with hooks = true to ~/.codex/config.toml first, or the hook is written and never fires. hooks print --agent codex reminds you of this.
  • Codex's hook file is global, not per project. That is why the hook command bakes in no connect code: identity is resolved from the worktree the turn ran in, so every project on the machine reports as itself instead of as whichever one installed last.
  • Antigravity needs a matcher on the hook group or it never fires; hooks install writes one for you.

What it sends

The destination is your own hub - the --server in the hook command. Never relayroom.dev.

Always, every runtimeClaude only
token counts (input / output / cache), the model name, a rough cost estimate, start and end timestampsthe turn's content in excerpt: the first 80 characters of the prompt and the last 500 of the answer

How much is collected depends on which agent you run. The excerpts exist only on the Claude path; the Codex and Antigravity reporters send counts and nothing else. This is a real asymmetry, not a rounding of it: the same RelayRoom install sees the text of your Claude turns and never sees the text of your Codex ones.

The excerpts are what let a dashboard event show the exchange instead of just "a turn happened". To report counts only, set "usageContent": false in that worktree's .relayroom/config.json. Token counts keep flowing; only the excerpts stop.

The hook command itself carries no connect code. Identity comes from the worktree's .relayroom/config.json, so the settings file this writes holds no secret and is safe to commit.

Not the same thing as the instance beacon. @relayroom/telemetry is the hub's own beacon and it does go to relayroom.dev, but it is content-free and never sees a prompt or an answer. Its three modes are anonymous (the default: on, content-free, no install id), community (adds a stable install id), and off (sends nothing). The usage hook above is the opposite arrangement: it can carry content, and it only ever talks to your hub. Both paths, side by side, are in Data and privacy.

Install it:

npx @relayroom/cli hooks install --code <connect_code> --part <part> --agent claude

--agent defaults to claude; pass agy or codex to write that runtime's file and event from the table above. The merge creates the file if needed and points at the bundled usage-report.mjs. It is idempotent - re-running replaces the RelayRoom hook instead of duplicating it, and leaves your other hooks untouched. For Claude the entry looks like:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node /…/runtime/usage-report.mjs --code <connect_code> --part <part> --server http://localhost:48801 || true"
          }
        ]
      }
    ]
  }
}

Prefer to paste it yourself? relayroom hooks print --code <connect_code> --part <part> --agent <claude|agy|codex> writes that runtime's JSON block to stdout, along with the file it belongs in.

Topology summary

The pager and usage hook communicate with the server over HTTP/SSE - they only need network access to the Hono server, not to Postgres or the web app directly.