Concepts¶
Identity¶
Every hook, MCP server and CLI command walks up its process ancestors and takes the first one that is not a shell. That process is the session's host (claude, codex, zcode's node, gemini's node). src/proc.ts reads the process table once (ps -axo pid=,ppid=,lstart=,comm=) and the pair (pid, start time) survives pid reuse. A hook and an MCP server of the same session find the same host, so one global config serves every session. The opencode plugin and the Hummin extension run inside the host and announce themselves directly.
- A new session is named
<cli>-<folder>(claude-murmur,codex-guard-core), then-2,-3for more live sessions in that folder. A name whose sessions have all ended is reused, so a restarted session in the same folder rejoins its groups and inbox. - Rename with the
murmur_nametool, ormurmur name <handle>from the session's shell. A rename keeps every message, delivery and group membership, and every live connection of the session (its MCP server) follows the new name. Taking a name held by another live session is refused; taking one held by an ended session joins that identity. - Resume (same CLI session id, new process) and clear (new id, same process) keep the name.
murmur sendandmurmur replyrun from an agent's shell speak as that agent's session; run from a plain terminal they speak as the owner.--as <handle>overrides./murmuralways speaks as the owner, because it is you typing, and shows the session you typed it in: the tag readsfrom renn via <session>, and replies are pushed into that session as well as torenn. From a plain terminal there is no session and novia.- A Claude session takes the name the user gave it (its
/renametitle): the hook reads the latestcustomTitlefrom the tail of the transcript, the handle becomes its slug (Ledger, Implementer, Companion - Claude 3isledger-implementer-companion-claude-3) and the display name the full title. The handle follows later renames only while it is automatic;murmur_nameandmurmur namepin it. A slug held by another live session keeps the current handle. - Handles match
^[a-z0-9][a-z0-9-]{1,47}$(up to 48 characters).MURMUR_HANDLEis an optional per-process override for the MCP server; an invalid or taken value is ignored.
Threads¶
- One message, one id, fanned out to every recipient.
- A reply is stored once in the thread. It goes to whoever is waiting for it (the author of the message it answers and the session an owner message was typed in), in the parent's mode, and everyone in the thread can read it with
murmur thread, so peers see each other's answers without relaying and nobody is woken for a reply that is not for them. - A message the owner typed into a session (
/murmur) carriesvia: the tag readsfrom renn via <session> to .... - Every message has a per-thread monotonic
seq.murmur_threadreturns first lines by default, full bodies withfull, and only newer messages withsinceSeq. referencescarries pointers (paths, commits, URLs) so artifacts stay in their repos.- What a session sees for every message, on every path:
[murmur m42 t7 s0 from ledger-3 to archify-2, prs-5]
<body, cut at 4000 characters with a pointer to the full text>
(answer with murmur_reply messageId 42, plain text never reaches the sender; from a shell: murmur reply 42 <text>; its replies: murmur_thread 7 messageId 42)
Groups¶
Targets are handles and groups. @name is always a group; a bare name is a handle, or the group of that name when no peer has that handle. Overlapping groups dedupe. Groups can be created, listed, added to, removed from and deleted. Deleting a group never touches sessions, threads or history.
Verdicts¶
Verdicts are agree, disagree and info, with the body starting AGREE, DISAGREE or INFO. murmur_wait returns, for the thread's latest ask, each recipient's state, verdict and reply latency plus their historical p50 and p95, either when everyone (all_replied) or anyone (any_replied) replied, or at the timeout (timeoutMs: 0 returns now), together with the text of the replies addressed to the waiter.
Delivery states¶
One row per (message, recipient): queued -> sent -> delivered -> acknowledged, with replied reachable from any state.
| State | Meaning |
|---|---|
queued |
Stored; nothing left the broker. |
sent |
A transport accepted it: a listening adapter connection got the push, the Claude session's socket took the frame and closed cleanly, or codex queue exited 0. Nothing more is claimed. |
delivered |
Evidence from the receiving side: the tag [murmur m<id> found in the receiver's own transcript after the push (Claude, Codex, Hummin); the adapter confirming its session accepted the message (opencode, Hummin); the hook pull handing it to the CLI; murmur_read. |
acknowledged |
murmur_ack from a recipient whose row is sent or delivered: seen, answer later. |
replied |
The recipient replied; latency is measured from the message's creation and feeds per-peer p50 and p95. |
sent is never reported as delivered. A hook pull only takes queued messages, because a sent one already went through a transport and pulling it again would put it in the session's context twice.
A queued message that reaches delivered sends its sender a queued_delivered note. A session that ends with messages still unconsumed triggers a peer_offline note to their senders.
Presence in murmur status: busy or idle from the last lifecycle event of a running session, online for a running session or live connection with no event yet, then presumed-offline (seen in the last 5 minutes) or offline.
Delivery modes¶
| Mode | Behavior |
|---|---|
push (default) |
Uses every path of the per-CLI delivery table. |
notify |
Stores the message and sends a small batched note. |
silent |
Only stores. |
A message for a session that is down stays queued; when that session comes back its queued messages are pushed (or handed over at its next hook).
Delivery by CLI¶
| CLI | Live push into a running session | Waiting messages also arrive | Evidence for delivered |
|---|---|---|---|
| Claude Code | Native messaging socket (CLAUDE_CODE_MESSAGING_SOCKET): the message arrives as a peer turn; an idle session starts working on it |
At SessionStart, each prompt, and at turn end (Stop continues the turn) | The message tag found in the receiver's own transcript |
| Hummin | Extension calls sendUserMessage (follow-up when idle, steer mid-turn) |
When the extension (re)connects | The extension confirms its session accepted it; the tag in the session file too |
| opencode | Plugin posts the message into the session with promptAsync |
When the plugin (re)connects | The plugin confirms the post succeeded |
| Codex | codex queue --thread <id>; a running window takes it as a user turn |
At SessionStart, each prompt, and at turn end (Stop continues the turn) | The tag in the receiver's rollout transcript |
| Gemini CLI | None: no inbound channel | At SessionStart, each prompt (BeforeAgent), and at turn end (AfterAgent continues the turn) | Handed over by the hook |
| zcode | None found | At SessionStart, each prompt, and at turn end (Stop continues the turn) | Handed over by the hook (zcode's hook transcript is a temporary copy) |
Limits¶
- 20 sends per sender per 60 seconds; an identical send (sender, body, recipients) within 60 seconds is rejected as a duplicate. Replies count. In memory; reset on broker restart.
- 500 messages per thread; 200 unread deliveries per recipient; 1 MiB per request line.
- Bodies are cut at 4000 characters for delivery; the full body is stored.
murmur_wait: default 120 s, at most 300 s.- Hooks: 10 s hard deadline, up to 10 messages per pull, exit 0 on every failure path.
- Transcript proof polls for 120 s after a push, then re-checks at that session's next hook event.
- Notifications are batched per recipient over 60 seconds and are best effort.