Skip to content

Releases: zach-source/claude-mailbox

v0.7.0 — broadcasts scoped to the sender's project

Choose a tag to compare

@zach-source zach-source released this 03 Aug 02:37
v0.7.0
4dc8208

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

Choose a tag to compare

@zach-source zach-source released this 01 Aug 03:36
v0.6.1
49c90f5

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 flakenix run github:zach-source/claude-mailbox.
  • Homebrew tapbrew install zach-source/claude-mailbox/claude-mailbox.
  • The beads workspace now resolves per-user to ${XDG_DATA_HOME:-~/.local/share}/claude-mailbox (override with MAILBOX_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

Choose a tag to compare

@zach-source zach-source released this 31 Jul 14:45
e7e6486

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-slot leader_hb) move into the bead's description JSON via bd 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.
  • role stays on set-state (genuinely low-cardinality) but is written only on an actual change, instead of spending a bd subprocess 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: bd implements --no-history by demoting the bead to a wisp (the row moves to the wisps table). Verified that bd query label=... AND status=open still returns it with its description intact, so list_sessions/_reap_stale are 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

Choose a tag to compare

@zach-source zach-source released this 25 Jul 07:38
v0.2.2
dbeea31

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 _State singleton is now a per-connection
    _SessionState registry, keyed by FastMCP's Context.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

Choose a tag to compare

@zach-source zach-source released this 25 Jul 06:18
v0.2.0
4e95661

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_TRANSPORTstdio (default, unchanged) or http
  • MAILBOX_HTTP_HOST / MAILBOX_HTTP_PORT — bind address for HTTP mode (default 127.0.0.1:8000)
  • MAILBOX_GLOBAL1/true (default, unchanged: routes bd at the shared beads_global DB via --global) or 0/false/no (omits --global, so bd resolves a plain local project database under WORKSPACE via 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.py covers run_bd's argv with/without MAILBOX_GLOBAL, mocked at the subprocess.run seam
  • New tests/test_transport.py covers main()'s stdio/http dispatch, mocked at the mcp.run seam
  • Manually verified both bd init modes in a scratch directory (shared-server vs. plain embedded local DB), and a live HTTP smoke test: booted the server with MAILBOX_TRANSPORT=http + MAILBOX_GLOBAL=0 against a fresh local db and completed a real MCP initialize handshake over HTTP

v0.1.0 — initial release

Choose a tag to compare

@zach-source zach-source released this 22 Jul 23:25
v0.1.0
4cde57a

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_sessions shows who's working on what, where.
  • Channels & DMsbroadcast/read_channel, send_dm, poll_inbox.
  • Info-requests — blocking request_inforespond_info/check_request (a peer answers your question).
  • Leadership — the session on main is the single leader/orchestrator; convergent smallest-sid election, staleness failover, delegate to secondaries.
  • Claude Code channel push — declares the claude/channel capability 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.
  • CLImailbox 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.