Sessions, Services & Subagents
xmsg routes messages between three kinds of recipients: instances (interactive AI agent
sessions), services (headless daemons and bridges), and subagents (delegated child turns
spawned under a parent conversation).
Instances and fork claims (X~n)
Section titled “Instances and fork claims (X~n)”An instance is a live or parked conversation in an agent harness:
- Persistent conversation IDs (
claude,agy,codex): The instance ID is the harness’s own conversation UUID. When a conversation closes or pauses between turns, its ID remainsparkedin SQLite for the reply TTL (--reply-ttl, 7 days by default) so inbound messages queue for its next turn. - Process-bound session IDs (
pi,opencode,hermes):xmsgmints a fresh UUID v4 on registration. When a Pi process exits or anopencode/hermessession is deleted/finalized, its address transitions togone(410).
Concurrent processes and fork suffixes (instance_claims)
Section titled “Concurrent processes and fork suffixes (instance_claims)”When multiple live processes present the same Claude conversation ID X (either in the same
~/.claude/sessions directory or across multiple --sessions-dir tenant directories), xmsg
manages them in the persistent SQLite instance_claims table:
- The first observed live process presenting
Xclaims plainX(suffix 1). - Subsequent live processes presenting
Xreceive monotonic fork suffixesX~2,X~3, … in observation order (ties broken by(procStart, pid)within a scan). - Suffixes persist across
xmsg serverestarts and are never promoted while live. - When a node observes a second live process for an already-live ID, it sends a one-time fork
notice to each process informing them of both addresses (
X@nodeandX~2@node). - Once an
X~nfork process exits, its claim is released and sends toX~nreturn410 gone. If plainXis released, the next process presentingXclaims plainX; numeric suffixes~2,~3, … are never reused while records exist.
Services (<name>@<node>)
Section titled “Services (<name>@<node>)”Headless daemons, bots, guards, and bridges register bare service names (such as genie-guard,
matrix-xmsg, or xmsg-gateway) over register.sock:
/// Validates a service name across consumer configuration (`xmsg::client::validate_service_name`),/// service registration (`register.sock` in `src/svc.rs`), subagent names (`validate_subagent_name`),/// and origin checking (`check_origin` in `src/fed.rs`):/// - non-empty and at most 32 bytes;/// - not the reserved `"xmsg"`;/// - no collision with the instance or subagent namespace (`is_instance_ref`, `split_subaddress`, or `'/'`);/// - no forbidden address/delimiter characters (`@`, `·`, `~`, `.`, `:`, `/`) and only ASCII `[A-Za-z0-9_-]`.pub fn validate_service_name(name: &str) -> Result<(), String> { if name.is_empty() { return Err("service name must not be empty".to_string()); } if name.len() > 32 { return Err(format!( "service name '{name}' exceeds maximum length of 32 bytes" )); } if name == "xmsg" { return Err("service name 'xmsg' is reserved".to_string()); } if is_instance_ref(name) || split_subaddress(name).is_some() { return Err(format!( "service name '{name}' is UUID-shaped or collides with the instance/subagent namespace" )); } if name.contains('@') || name.contains('·') || name.contains('~') || name.contains('.') || name.contains(':') || name.contains('/') { return Err(format!( "service name '{name}' contains forbidden character (@, ·, ~, ., : or /)" )); } if !name .bytes() .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') { return Err(format!("invalid service name '{name}'")); } Ok(())}- Executable attestation (
--svc-exe): On normal host nodes,xmsg serverequires--svc-exe <NAME>=<PATH>(orXMSG_SVC_EXE). When a process connects toregister.sockand sends{"harness":"svc","name":"<NAME>"},xmsgchecks the peer UID (peer_uid == server_uid) and verifies that the canonical executable path (/proc/<pid>/exeon Linux,proc_pidpathon macOS) matches<PATH>. Only one live process may hold a service name at a time. - At-least-once long-poll and
ack: Services pull messages with{"action":"poll","waitSecs":30}onregister.sockand acknowledge each delivery with{"action":"ack","messageId":"..."}after committing it to their own durable store. Unacknowledged messages remainqueued(tracked in the connection’sin_flightset) and redeliver in FIFO order if the service disconnects before acknowledging. - Structured
originattestation: Every delivered frame includes an attestedoriginstruct (id,service,node,label) so services make authorization decisions from structured fields rather than parsing display headers:
/// Where a message came from, one shape for every sender (A12): an instance's `id` or a/// service's name, the `node` that attests it, and the sender's own `label`. An anonymous/// sender has neither `id` nor `service`; its `label` is its unattested `from`.#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]#[serde(deny_unknown_fields)]pub struct Origin { #[serde(default, skip_serializing_if = "Option::is_none")] pub id: Option<String>, #[serde(default, skip_serializing_if = "Option::is_none")] pub service: Option<String>, pub node: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub label: Option<String>,}See Daemon Protocol & Rust Client for the wire frames and the
xmsg::client library.
Delegated subagents (<P>/<child>@<node>)
Section titled “Delegated subagents (<P>/<child>@<node>)”When a parent conversation P spawns child subagents, each bound child gets a delegated
sub-address <P>/<child>@<node> where <child> is either the subagent’s bound name or its harness
agent_id / child session ID. Both <P>/<name> and <P>/<agent_id> resolve to the same bound
subagent, and list includes live bound subagents with kind: "subagent".
Turn mailbox (inbox) and parent fallback
Section titled “Turn mailbox (inbox) and parent fallback”- Active turn (
inbox): While a subagent’s turn is active (turn_pidlive with matching start time), messages and replies addressed to<P>/<child>@<node>queue in its turn mailbox (outcome: "queued"). The subagent drains them during its turn by callinginbox(wait?), which returns{address, messages: [{id, from, body, created_at}]}and marks each drained messagedelivered. - Parent fallback (
to=<P>/<child> agent_id=<id>): When the subagent’s turn is not active (between turns or afterSubagentStop/Stop), messages and replies addressed to<P>/<child>@<node>fall back to the parent sessionPwith ato=<P>/<child> agent_id=<agent_id>header prepended to the message body. IfPis currently parked, the message stays queued on<P>/<agent_id>and is either drained by the subagent on its nextinboxcall or delivered toPwith the fallback header whenPreturns.
Per-harness subagent support
Section titled “Per-harness subagent support”| Harness | Sub-address Form | Attribution & Lifecycle |
|---|---|---|
| Claude Code | <P>/<name>@<node> and <P>/<agent_id>@<node> | xmsg register claude --hook on SubagentStart, PostToolUse (Agent/SendMessage), SubagentStop, and PreToolUse (stamping as_agent). |
| Codex | <P>/<X>@<node> (direct children where parent_thread_id == P) | Attributed from runtime params._meta (sessionId, threadId); turn state tracked by SubagentStart, PreModelRequest, and SubagentStop hooks. |
| Antigravity | <P>/<child_uuid>@<node> | Admitted from P’s transcript.jsonl (invoke_subagent) and tracked by xmsg register agy --hook (pre-invocation, pre-tool-use, stop). |
| opencode (1.x & 2.x) | <P>/<child_ses>@<node> | Child sessions walk parentID to root P, bind via subagent (start/stop/end) on register.sock, and pass as_agent on xmsg_send, xmsg_reply, and xmsg_inbox. |
| Hermes | Root <P>@<node> | delegate_task subagents act as their root conversation P and do not bind separate sub-addresses. |
| Pi | None | Single-session process; no subagent sub-addresses. |
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).