Delivery, Queueing & Replies
Delivery outcomes and the queue
Section titled “Delivery outcomes and the queue”Every send returns an address, a message_id, and an outcome:
delivered: The message was injected into a live Claude Code or Antigravity session or accepted by a live Codex relay.queued: The message is held in SQLite for:- a parked Claude Code, Codex, or Antigravity conversation (not running right now, or between turns);
- a live subagent in an active turn (held in its turn mailbox until it calls
inboxor its turn ends); - a live Pi, opencode, or Hermes session, or a registered service, until the
harness plugin or service explicitly acknowledges (
ack) the delivery.
Once a held message is delivered or acknowledged, its stored record (GET /v1/messages/{id} and
idempotent send replays) updates to delivered.
What queues and for how long
Section titled “What queues and for how long”- What queues: Sends to an instance ID this node has previously observed (
instance_claims, Codex roots, or Antigravity registrations) while it is not running. A Pi ID, a released or closedopencode/hermessession, or an exitedX~nfork returns410 goneand never queues. - Lifetime (
--reply-ttl): Queued messages are held for the reply TTL (604800seconds = 7 days by default) and dropped unread after expiry. A parked ID remains addressable asparkedfor the reply TTL after it was last seen live, or as long as its queue holds a message, whichever is longer; after that, sends to it return404 not_found. - Drain order: When the conversation is next observed live, queued messages drain in FIFO order
with their original send timestamp in the envelope header:
[xmsg] message_id=<id> sent=<RFC 3339 UTC time>.
The queue across a user’s machines
Section titled “The queue across a user’s machines”A user’s nodes share one principal (<user> in <user>.<host>), so a Claude Code conversation
whose ~/.claude directory is synced between machines keeps its bare conversation ID, and its
queued messages follow it automatically:
- The node that gains the conversation asks. When a scan on node newly observes a live
Claude process holding plain ID , node concurrently queries all dialable peers belonging
to the same user over
POST /fed/v2/handoff. - A holder hands over only to the single live listing. A peer holding parked messages for
verifies that is the only live listing of across a complete view of that user’s
nodes. If any node of the user is unreachable (
503 unavailable) or two nodes list live (409 ambiguous), the queue stays put. Nodes belonging to another user are refused (403 denied). - Transfer, then acknowledge. Node transfers a page of messages (up to )
and retains its queue until node acknowledges receipt on
POST /fed/v2/handoff/ack. Node persists its owed acknowledgement in SQLite and retries with backoff (up to 30 s) across restarts until confirms or the messages expire. - Replies route transparently. At acknowledgement, records each handed-over message as forwarded to . When the conversation on replies, the reply travels to , which authorizes it and forwards it to the original sender.
Replies: stored always, pushed by sender kind
Section titled “Replies: stored always, pushed by sender kind”Every envelope delivered to a recipient carries a reply instruction footer:
[xmsg] ref=2f6c1e0a-8d3b-4c7e-9a41-0b5d2e7f9c13@alice.workstation message_id=01J9XYZ... — reply with the xmsg reply tool (tools: send, reply, list, replies)- Symmetric reply authorization: Calling
reply(message_id=m, text=...)answers ‘s other party symmetrically. If the caller is ‘s recipient, the reply goes to ‘s sender; if the caller is ‘s sender, it goes to ‘s recipient as a follow-up in the same exchange (in_reply_to = m). Any caller who is neither party is refused withdenied(not_recipient). - Stored always: Every reply is stored under and can be read via
replies(message_id=m)orGET /v1/messages/{id}/replies?after={seq}&wait={seconds}by anyone holding the message ID. - Pushed when the target party has an inbox: If the party being answered is a Claude Code,
Codex, Antigravity, Pi, opencode, or Hermes session, or a registered service, the reply is also
pushed into their inbox as a new message with its own ULID and
thread_id:[xmsg] reply to message_id=<orig_id> — message_id=<new_id>; reply with the xmsg reply tool - Push outcomes: Recorded on each reply as
pushed,sender_gone,push_failed, orstored(when the origin is a leaf or outbound-linked node that pulls its replies). Replies to anonymous HTTP callers record no push outcome and are read viareplies. - Codex restart custody: a known, nonrevoked root remains queueable within the reply TTL while awaiting fresh writer-lease attestation. It is not automatically live after a daemon restart. An attributed MCP tool call can re-attest only a previously known, nonrevoked root, using the same full writer-lease and rollout checks as resume; unknown roots still require registration. After successful attestation, the call kicks a bounded FIFO relay drain (at most eight envelopes, starting no new attempt after two seconds; each existing relay attempt has its own two-second deadline). Explicit operator revocation still refuses delivery. New replies with definite non-handoff custody retain their stable message ID, return provenance and original queue timestamp; the same root drains them in FIFO order on re-registration. Accepted handoffs are marked
pushed. Unknown or inflight handoffs are never automatically replayed. Historicalsender_goneorpush_failedrows without the matching durable handoff are pull-only throughreplies; no replacement identity or provenance is inferred. - Cross-node replies return to the origin: A reply to a federated message always travels back to
the originating node (
/fed/v2/repliesor reply pull), which decides how to deliver it to the sender.
Idempotency (idempotency_key)
Section titled “Idempotency (idempotency_key)”Providing idempotency_key (1 to 128 characters) on a send or reply over HTTP, agent.sock,
or register.sock (where service frames spell it idempotencyKey) guarantees at-most-once
delivery across matching retries within the configured retention window. The default is
86400 seconds (24 hours), set by --idempotency-ttl / XMSG_IDEMPOTENCY_TTL. After expiry,
reusing the key can create a new message and deliver again:
- Reservation before delivery: The key and minted
message_idare committed to SQLite before delivery starts. - Replay: A retry with the same key and identical target/body after settlement returns the
original result (
message_id,address,outcome) without delivering a second copy. - In-flight wait (
pending): While the first request is still in flight, a concurrent retry waits up to 2 seconds for settlement; if still in flight, it returnsoutcome: "pending"(orpushOutcome: "pending") with the reserved ID. - Failed delivery retry: If delivery failed with an error, the key remains free to retry under
the same
message_id. Peers deduplicate bymessage_id: a peer that already recorded the ID asdeliveredorqueuedreplays that result, while a peer that recorded a failure attempts delivery again. - Conflict (
409 conflict): Reusing the same key with a differentto,message_id, ortextis refused with409 conflict. - Principal scoping: Keys are partitioned by attested caller (
id[~n],<P>/<child>, or servicename), oranon:<from>for anonymous HTTP callers.
How a reply reaches you
Section titled “How a reply reaches you”flowchart TD R["Recipient replies with message ID"] --> S["xmsg stores reply"] S --> L["Local sender with an inbox: push"] S --> D["Dialable remote origin: mTLS reply"] S --> P["Leaf or outbound origin: pull stored replies"] S --> A["Anonymous HTTP sender: read replies by ID"]
Push is an additional delivery attempt, not a replacement for the stored reply. Check the
reported outcome and use replies when the sender has no inbox or a push did not reach it.
The shared reply surface
and key retention
define these paths and the retry window.
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).