Peer sessions

peersd lets agent sessions find and message each other over the Bus. An agent such as Claude Code finds its peers only through local sockets, so a session inside a desk container cannot hear one on the host, and two sessions about to push the same branch never learn of each other. peersd carries that over the Bus instead:

  • a registry of the live sessions: name, node, working directory, and whether each is busy or idle;
  • direct messages, with an inbox per session, and messages to everyone;
  • a notice board every session reads, such as a hold on a branch or a queue of sessions waiting to land.

Sessions join through their agent's hooks; for Claude Code that is claude_code_hook.mix, below. What a session reads from peers is framed as data from another session, never as its user's words: a peer can inform or ask, but never approve anything on the user's behalf.

Running it

peersd is a Mix Bus citizen. The user unit runs it in your own session:

$ systemctl --user enable --now mixos-peersd.service

The unit is installed as <prefix>/share/peersd/units/peersd.service; copy or link it into ~/.config/systemd/user/ as mixos-peersd.service. State lives in ~/.local/state/mixos/peers/<host>/peers.db, one per host, because a desk container may share its home with the host.

Across the mesh. Each node's peersd holds that node's sessions, and mix peers reaches the others: list and msg all visit every node noded knows (noded.peers), skipping a node that runs no peersd and naming one that does not answer. A message to a session on another node carries the sender's name@node as its claimed name, beside the receiving broker's stamp. Notices are copied to every node: each carries the time it was set or cleared at its origin, the newest wins, and a clear leaves a tombstone so a late copy cannot bring it back. mix peers notice list also brings any node that missed a set or clear up to date. The copying is done by mix peers: a notice set by calling peers.notice.set directly reaches other nodes only at the next mix peers notice list.

One registry for several nodes. A group of nodes can share one registry instead (a host and the desks that share its home, say): run peersd on one of them and name it as the hub in ~/.config/mixos/peers.conf.mix:

{hub: "host1"}

Every node with that conf then reaches peers.host1.bus only.

Command line

mix peers is the command line (installed at <prefix>/lib/peersd/peers_cli.mix). Run inside an agent session it acts as that session; otherwise as the Bus caller it is.

CommandWhat it does
mix peers listthe live sessions; a session silent for two hours is marked stale
mix peers msg NAME[@NODE] TEXT...a direct message; a bare NAME is looked for on this node, then on the others (PEER_AMBIGUOUS if two have it)
mix peers msg all TEXT...a message to every other live session
mix peers notice set KEY TEXT... [--ttl S]pin a notice every session reads once
mix peers notice clear KEYremove it; every live session is told
mix peers notice listthe notices in force
mix peers inbox [--peek]this session's new messages
mix peers name NEWrename this session

Exit codes: 0 done, 1 refused or unreachable, 64 usage.

Claude Code

Register the hook for five events in ~/.claude/settings.json (merge it into any hooks you already have). The paths below are for the default prefix, /opt/mixos; use your own <prefix>/lib/peersd/claude_code_hook.mix if MixOS is installed elsewhere:

{
  "hooks": {
    "SessionStart": [{"hooks": [{"type": "command", "command": "mix /opt/mixos/lib/peersd/claude_code_hook.mix"}]}],
    "UserPromptSubmit": [{"hooks": [{"type": "command", "command": "mix /opt/mixos/lib/peersd/claude_code_hook.mix"}]}],
    "PostToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "mix /opt/mixos/lib/peersd/claude_code_hook.mix"}]}],
    "Stop": [{"hooks": [{"type": "command", "command": "mix /opt/mixos/lib/peersd/claude_code_hook.mix"}]}],
    "SessionEnd": [{"hooks": [{"type": "command", "command": "mix /opt/mixos/lib/peersd/claude_code_hook.mix"}]}]
  }
}
EventWhat the hook does
SessionStartclaims a name (the session's title when free), shows notices and messages
UserPromptSubmitmarks the session busy, shows anything new
PostToolUse (Bash)shows anything new between commands, so a busy session hears a hold before its next push
Stopwith mail waiting, holds the session for one more turn to read it (never twice); otherwise marks it idle
SessionEndleaves the registry

The hook never gets in a session's way: any Bus fault is silent, and after a timeout that session's hook stays quiet for a minute. Subagents are skipped (they would read their parent's inbox), and PEERS_HOOK=off in the environment turns it off for headless sessions. Only what the hook shows is marked read; the rest waits for the next event.

An idle session whose user is away does not wake for a message; it reads it on its next prompt.

Bus interface

The peers service. Bodies are JSON.

CommandBodyAnswer
peers.claim{claude_session, node, cwd, name?}{name, node}; a known session asking for another free name is renamed
peers.state{claude_session, state: busy|idle}
peers.leave{claude_session}
peers.list{sessions: [{name, node, cwd, state, age_s, stale}]}
peers.send{to: NAME or "*", text, from_session?, from?}{delivered, stored, ids, from}
peers.inbox{claude_session, peek?, limit?}{name, node, messages, notices, more}
peers.ack{claude_session, upto?, notices_upto?}marks read what a reader showed
peers.notice.set{key, text, ttl_s?}{key, ts, expires}
peers.notice.clear{key}every live session gets a notice-cleared message
peers.notice.list{notices}
peers.notice.put{key, version, origin, text? | cleared?}a set or clear forwarded from another node; applied only if newer
peers.notice.all{node, notices, tombs}, for reconciling
peers.whoami{claude_session}{name, node}

Topics: peers.message {id, to, from} on every delivery, peers.notice on every set and clear.

Each message records two senders: from, the name the sender claims, and sender, the broker's stamp of the caller (its local name, or service@node over the mesh). Text is at most 4096 bytes; an inbox keeps the newest 200 messages; ended sessions and their mail go after a week, or as soon as a new session takes the ended one's name (it starts with a clean inbox). A session is honoured only from where it was claimed: this node, or the mesh node it came from (PEER_ORIGIN otherwise).

This page as Markdown · This page describes the current main branch of MixOS.