Releases: zach-source/claude-mailbox
Release list
v0.7.0 — broadcasts scoped to the sender's project
Broadcasts are scoped to your project
Every session subscribes to general, <project>, and leader — and broadcast() defaulted to general, so a single broadcast interrupted every session on the machine no matter which repo it was working in.
broadcast(text) now posts to the sender's own <project> channel. Reaching sessions in other repos is the explicit opt-in:
broadcast(text="…", channel="general") # MCP tool
mailbox say "…" -c general # CLI
The server instructions also now tell agents to broadcast sparingly and prefer send_dm when only one session needs to know.
Breaking-ish: if you relied on the old default, add channel="general". Nothing else changed — subscriptions, DMs, requests, delegations, and read_channel are untouched.
v0.6.1 — ~15x less bd traffic, nix + homebrew packaging
First release since v0.4.0 — v0.5.0 and v0.6.0 were tagged but never published, so their changes are folded in here.
Performance
Steady-state bd traffic per session dropped ~15x. The channel poll now issues one bd query per pass instead of four, and backs off from 15s to a 60s ceiling while idle; the heartbeat is down to 3 bd calls per 60s bucket. Measured on an idle registered session: ~3.6 bd calls/min, down from ~65.
Delivery errors are also caught per session rather than around the whole sweep — one slow bd call no longer aborts delivery for every other session in that pass.
Packaging
- Nix flake —
nix run github:zach-source/claude-mailbox. - Homebrew tap —
brew install zach-source/claude-mailbox/claude-mailbox. - The beads workspace now resolves per-user to
${XDG_DATA_HOME:-~/.local/share}/claude-mailbox(override withMAILBOX_WORKSPACE). Earlier builds resolved it to<prefix>/lib/pythonX.Y, which no installed build could write to — installed builds before v0.6.0 do not work. - MIT LICENSE now ships in the release tarball. v0.6.0's tarball was cut before the license landed, which is the only reason this is 0.6.1.
Upgrade
No config changes. If you installed from a pre-0.6.0 tag, initialize the workspace once:
mkdir -p ~/.local/share/claude-mailbox
bd init -C ~/.local/share/claude-mailbox
bd init --global
v0.4.0 — heartbeats stop bloating the beads database
Fix: heartbeats no longer mint a bead + ~4 Dolt commits per beat
beads_global had grown to 2.9 GB / 85,267 Dolt commits, with 21,320 of 21,353 issues being closed State change: hb -> <epoch> event beads (15,698 hb + 5,401 leader-hb).
The obvious suspect — closed-issue volume — was the wrong one. bd admin cleanup --older-than 90 --dry-run deletes zero rows (the DB was days old), and 2.9 GB / 85k commits is ~36 KB/commit: the history is the bloat. That is also why gc only ever reclaimed 2–5% — every commit is reachable from main, so there is nothing unreachable to collect.
Root cause
Both steady-state writers used bd set-state, which by design mints an event bead and rewrites a <dim>:<val> label on every call. Correct for low-cardinality facts (role changed 86 times in the entire history); exactly wrong for a monotonic timestamp that changes every beat.
Measured on the live DB:
| write | Dolt commits | issue rows |
|---|---|---|
set-state on a normal bead |
3.8 | +1 |
bd update -d on a normal bead |
1.0 | 0 |
bd update -d on a --no-history bead |
0.0 | 0 |
set-state on a --no-history bead |
2.0 | +1 (event bead is a new issue) |
Changes
- Heartbeats (session
hb, leader-slotleader_hb) move into the bead's description JSON viabd update -d, folded into the same write that already carried git drift. A timestamp is liveness, not an election decision — it has no audit value. - Session and leader-slot beads are created with
--no-history. rolestays onset-state(genuinely low-cardinality) but is written only on an actual change, instead of spending abdsubprocess every beat to rediscover that nothing changed.
Net: 0 commits and 0 rows per beat for both writers, verified end-to-end.
Also fixed
tests/test_session_isolation.py's fake_bd fixture patched the bd seam in server.py but not in leader.py — and every heartbeat crosses into leader.py. These "unit" tests were spawning real bd against the shared production beads_global. Suite time drops 160–187s → ~6.5s.
Upgrade notes
- Pre-existing leader-slot beads keep costing ~1 commit/beat until migrated by hand:
bd update <slot-id> --no-history. - This stops the bleeding but does not shrink an already-bloated DB. Reclaiming existing space needs
bd flatten --force(squash + GC) with all mailbox daemons stopped. - Caveat:
bdimplements--no-historyby demoting the bead to a wisp (the row moves to thewispstable). Verified thatbd query label=... AND status=openstill returns it with its description intact, solist_sessions/_reap_staleare unaffected — but if presence ever starts dropping sessions that are still heartbeating, wisp TTL compaction is the first thing to suspect.
Full test suite: 64 passed, 1 skipped (opt-in live-bd e2e).
Full Changelog: v0.3.0...v0.4.0
v0.2.2 — per-connection HTTP session isolation
Fixes the identity-collision gap that v0.2.1 (commit 8534c19) documented but
deliberately left unfixed: concurrent HTTP-mode connections used to share one
process-global session (sid, git context, bead_id, objective, captured
channel-push session) — whichever connection registered most recently became
who the server thought every caller was.
What changed
server.py: the module-global_Statesingleton is now a per-connection
_SessionStateregistry, keyed by FastMCP'sContext.session_id— verified
present and stable for every transport (including stdio) in the installed
fastmcp 3.4.4, not just HTTP as the prior investigation assumed.channel.py: the captured channel-push session is per-connection too, so
proactive<channel>push now delivers to every connected client, not just
the first one to register.- Idle HTTP-mode connections (no tool call for 15 minutes) are reaped instead
of heartbeating forever, reusing the existing stale/reap conventions. - stdio mode is unaffected — a stdio process still gets exactly one tracked
connection for its whole life, and the full pre-existing stdio test suite
passes unchanged. - New tests cover concurrent-connection isolation (two live in-process MCP
clients never leak sid/bead_id/objective/status into each other) and the
idle-reap cleanup path.
Residual limitation
Idle-connection cleanup is time-based (no client tool call for 15 minutes),
not a true transport-liveness check — a connection that stays open but is
genuinely idle that long is indistinguishable from an abandoned one and gets
reaped too. Documented in README.md and docs/DESIGN.md. This only applies
under MAILBOX_TRANSPORT=http; stdio is never affected.
Full diff: v0.2.1...v0.2.2
v0.2.0: HTTP transport mode
HTTP transport mode + local-database bd mode
Adds a way to run this MCP server as a standalone, network-reachable service, in addition to its existing stdio mode (which is unchanged by default, zero config required).
New environment variables
MAILBOX_TRANSPORT—stdio(default, unchanged) orhttpMAILBOX_HTTP_HOST/MAILBOX_HTTP_PORT— bind address for HTTP mode (default127.0.0.1:8000)MAILBOX_GLOBAL—1/true (default, unchanged: routesbdat the sharedbeads_globalDB via--global) or0/false/no(omits--global, sobdresolves a plain local project database underWORKSPACEvia its default embedded engine)
Intended use case
Hosting one authoritative mailbox instance in a remote pod, backed by its own dedicated local database rather than the shared machine-wide one, reachable:
- from a Claude Code session on a different machine, as an
http-type MCP server entry - from a plain Python daemon (not a Claude session) as a regular MCP HTTP client
See the README's new "HTTP mode" section for the full env var table and a runnable example.
Known limitation
Proactive <channel> push notifications (peer DMs/requests/broadcasts interrupting a session) are best-effort and single-client only in HTTP mode. FastMCP's streamable-HTTP transport does give each connection its own session, but this server's per-session state (_State in server.py) is still a single process-global instance, so channel push only ever reaches whichever HTTP connection first captures a session — never a specific caller among several concurrent ones. Documented in channel.py and the README; pull-based tools (poll_inbox, read_channel) are unaffected and work correctly for every caller.
Testing
- Existing stdio + global-mode test suite passes unchanged (32 passed, 1 skipped)
- New
tests/test_bd.pycoversrun_bd's argv with/withoutMAILBOX_GLOBAL, mocked at thesubprocess.runseam - New
tests/test_transport.pycoversmain()'s stdio/http dispatch, mocked at themcp.runseam - Manually verified both
bd initmodes in a scratch directory (shared-server vs. plain embedded local DB), and a live HTTP smoke test: booted the server withMAILBOX_TRANSPORT=http+MAILBOX_GLOBAL=0against a fresh local db and completed a real MCPinitializehandshake over HTTP
v0.1.0 — initial release
First release of claude-mailbox — an MCP server that lets concurrent Claude Code sessions discover each other and cross-talk, backed by the bd (beads) shared beads_global Dolt database.
Features
- Presence — sessions register project / worktree / branch / objective;
list_sessionsshows who's working on what, where. - Channels & DMs —
broadcast/read_channel,send_dm,poll_inbox. - Info-requests — blocking
request_info→respond_info/check_request(a peer answers your question). - Leadership — the session on
mainis the single leader/orchestrator; convergent smallest-sid election, staleness failover,delegateto secondaries. - Claude Code channel push — declares the
claude/channelcapability and pushes peer messages into the session as<channel source="mailbox" kind="…" from_sid="…">events (start Claude with--dangerously-load-development-channels server:mailbox). Falls back to pull-based tools without the flag. - CLI —
mailbox who / leader / say / inbox.
Quality
- Reviewed (Fable) → blockers + should-fix + nice-to-haves all addressed.
- 22 unit tests + a live two-session cross-talk test + a raw JSON-RPC wire test proving channel push.
Setup
Requires bd (beads) and a per-machine bd init --global. Run with uv run claude-mailbox. See the README.