Files
claudemesh/apps/broker
Alejandro Gutiérrez 2b88784005
Some checks failed
CI / Lint (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Broker tests (Postgres) (push) Has been cancelled
CI / Docker build (linux/amd64) (push) Has been cancelled
feat(cli,broker): stable session identity — fix ghost peers + lost DMs (1.35.0)
Session identity is now anchored on Claude Code's session UUID instead of a
fresh random keypair per launch. The ed25519 session keypair is generated
once per (mesh, session UUID) and persisted under
~/.claudemesh/sessions/<mesh>/<uuid>.json, so relaunching or --resume-ing the
same session reuses the same sessionPubkey.

Why: a DM is sealed (crypto_box) to the recipient's sessionPubkey. With
ephemeral per-launch keys, the pubkey rotated on every relaunch, so queued
messages became undecryptable AND the old presence lingered as a same-name
ghost that won queued-DM claim races. Reconnecting could not recover the
peer because it minted yet another key. On --resume the CLI also registered
a throwaway random id unrelated to the resumed session, so the broker never
recognized the returning peer.

CLI (launch.ts):
- resolve the stable UUID for all paths: fresh mints + forces via
  --session-id; --resume V registers V; --continue resolves the most-recent
  session UUID from ~/.claude/projects/<cwd>.
- use loadOrCreateSessionKeypair(mesh, uuid) instead of generateKeypair().

CLI (daemon/run.ts):
- onRegister closes any prior SessionBrokerClient holding the same pubkey
  under a different token (the leaked-WS ghost).

Broker (handleSessionHello):
- reattach by sessionPubkey regardless of lease state (online or grace),
  closing the stale socket — enforces one live presence per session pubkey,
  killing the duplicate and draining queued DMs on return.

Trade-off: session secret keys now persist on disk (the member key already
does); SPEC.md updated to reflect the stable-identity model. Older CLIs
remain compatible (they keep using ephemeral keys).

New: keypair-store.ts + 7 unit tests. Full CLI suite: 114/114 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 12:59:36 +01:00
..

@claudemesh/broker

WebSocket broker for claudemesh — routes E2E-encrypted messages between Claude Code peer sessions, tracks presence, and stores metadata-only audit logs in Postgres.

What it is

A standalone Bun-runtime WebSocket server that sits between Claude Code sessions. Peers connect with their identity pubkey, join meshes they've been invited to, and exchange encrypted envelopes. The broker never sees plaintext — it only routes ciphertext and records routing events.

Running locally

# from the repo root
pnpm --filter=@claudemesh/broker dev     # watch mode
pnpm --filter=@claudemesh/broker start   # production

Required env vars

Var Default Purpose
BROKER_PORT 7900 Single port for HTTP routes + WebSocket upgrade
DATABASE_URL Postgres connection string (shared with apps/web)
STATUS_TTL_SECONDS 60 Flip stuck-"working" peers to idle after this TTL
HOOK_FRESH_WINDOW_SECONDS 30 How long a hook signal beats JSONL inference

Routes (single port)

Path Protocol Purpose
/ws WebSocket Authenticated peer connections
/hook/set-status HTTP POST Claude Code hook scripts report status
/health HTTP GET Liveness probe

Depends on

  • @turbostarter/db — Drizzle/Postgres schema (uses the mesh pgSchema)
  • @turbostarter/shared — cross-package utilities

Deployment

Runs as a separate process (not inside Next.js). Intended deployment targets: Fly.io, Railway, or Coolify on the surfquant VPS. WebSocket server must be reachable at ic.claudemesh.com.

Status

Scaffold only. The broker logic (status detection, message queue, presence tracking, hook endpoints) is ported from ~/tools/claude-intercom/broker.ts in a follow-up step.