# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries its sender id structurally;
  the arriving `<EVENT from="…">` envelope surfaces it, and
  `spt send <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents. To receive that reply,
  a caller who is not already listening gets a temporary perch for the
  duration of the call, which is torn down when the call ends.

  <!-- [doc->REQ-RING-TIMEOUT-MINUTES] -->
  **The wait is counted in minutes.** `--timeout` defaults to **30 minutes**,
  and a bare number is minutes — you are waiting on an agent, which answers on
  agent time, not on request-response time. An explicit suffix sets the unit,
  so `--timeout 90s` and `--timeout 2m` both work and both say what they mean.
  The timeout line answers in the unit you typed. *(Bare numbers meant seconds
  in earlier releases, so a `--timeout 60` carried over from an older script
  now means sixty minutes — write `60s` for the old behavior.)*

  <!-- [doc->REQ-HAZARD-RING-PERCH-ADOPTION] the published half: which side owns the guarantee, stated as behavior (no hazard-history framing) -->
  **A ring only ever tears down a perch it created itself.** This matters
  because a caller may already own a perch that does not currently look
  live — through a busy turn, a session ending softly, or a re-bind in
  progress. In that case the ring does not take the perch over: it delivers
  the message, returns without blocking, and the reply arrives on the
  caller's own listener the next time it listens. The same refusal covers a
  perch directory that exists but is empty, which may be another process
  mid-creation: it is left alone rather than reused or cleaned up, and its
  path is reported so a person can deal with it. The practical guarantee is
  that ringing from a session that already has an identity never costs that
  session its perch or the mail sitting in its spool.

  **This guarantee is carried by the caller — the `spt` command you run —
  not by the daemon, and not by anything a harness supplies.** The command
  claims a temporary perch by creating its directory exclusively, so a
  directory that already exists is a refusal rather than a silent reuse, and
  only a perch proven to be its own is ever removed. An adapter or harness
  neither implements nor can weaken this, and should not add its own
  protection against it. The daemon's role is narrower and separate: for
  perches it hosts, it is the authority on whether a listener is currently
  live — which is what decides whether the reply is waited for here or left
  to arrive on the caller's existing listener.
- **Per-message send control (three orthogonal axes).** Each `spt send`
  carries one value per axis; every axis defaults to unrestricted:
  - **Delivery window** (*when*) — `--active-only` spools for the agent's own
    poll without waking a live listener (it reaches the agent at its next
    natural boundary instead of interrupting now; also held for resting
    dormant/suspended instances and released exactly once on wake). This
    **renames the older `--deferred`**, which still parses as a hidden alias.
    `--idle-only` holds until the target is idle, then delivers (the wake).
    Default delivers in whichever window fires first.
  - **Channel** (*through what*) — `--prefer-native` routes through the
    target's translation binary when one is running, else falls back to the
    standard delivery; `--force-native` uses the binary only (no fallback,
    no reroute — if no binary is live it reports non-delivery rather than
    spooling to another method). Default is unrestricted. The translation
    binary is the adapter's idle-delivery filter; an adapter declares it with
    a `[message-idle-translation-binary].command` (a program token plus args,
    new in v0.16.0 — the bare `path` form is deprecated) and spt-core
    lifecycle-manages it.
  - **Persistence** (*how long*) — `--ephemeral` drops the message if it
    can't be delivered in its accepted window instead of spooling; it is the
    one path allowed to drop silently (everything else spools and reports
    non-delivery). *In this release ephemeral evaporation applies to
    translation-binary delivery and TTL expiry; the harness-relay
    carrier-absence case is not yet wired.*
- **Opaque metadata.** `--json-payload '<json>'` attaches a JSON metadata
  block alongside the body. spt-core carries it verbatim across every rail
  and never interprets it — the **receiving adapter** parses it. It can't
  forge spt-core's own envelope attributes (it rides inside a single `json`
  value), and any sender may attach it.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

<!-- [doc->REQ-DOC-DELIVERY-VOCAB] the send-outcome vocabulary itself — the closed set of SENT/SENT(WAN)/QUEUED/QUEUED(idle-only)/DEFERRED/NO_PERCH + WAN failure tags with their exact conditions; the JSON-consumer view + digest/poll shapes live in reference/json-shapes.md -->

## Send outcomes — the closed set

Every `spt send` reports exactly one outcome line. The line goes to
**stderr** (stdout is reserved for message payloads); classify success by the
**exit code** — `0` for every delivered/spooled outcome, non-zero for every
failure. The token before the first `:` is stable; this set is complete as of
v0.26.0.

**Success (exit 0):**

| Line | Meaning |
|---|---|
| `SENT:<id>` | Delivered live to a listening target on this node. |
| `SENT(WAN):<id>@<node>` | Delivered cross-node, **receiver-confirmed** — printed only when the remote daemon acknowledged. A suffix annotates the confirmed disposition: ` (spooled)` — the remote daemon accepted it into the target's durable spool; ` (duplicate)` — the receiver had already processed this message (a safe dedup'd replay). No suffix = delivered live. |
| `QUEUED:<id>` | The perch exists but nothing is listening — spooled durably, drains on the target's next `ready`. Success, not an error. |
| `QUEUED(idle-only):<id>` | An `--idle-only` send holding for the target's idle window. |
| `DEFERRED:<id>` | An `--active-only` send spooled for the target's own next poll (never interrupts a live listener). |

**Failure (non-zero exit, stderr):**

| Line | Meaning |
|---|---|
| `NO_PERCH:<id> is not listening` | No perch for that id — identity is never invented on someone else's behalf. An `--active-only` send reports this with `(active-only stays local-only)`: the hook channel does not take the cross-node leg. |
| `WAN_NO_PERCH:<id> — no perch on <node>` | The route resolved to a node, but no perch lives there (a stale route — the endpoint may have moved or stopped). |
| `WAN_REFUSED:<id>@<node>` | The receiver denied the message (access gate). |
| `WAN_UNCONFIRMED:<id>@<node>` | No receiver acknowledgment — the peer may be offline or on an old version. The message may or may not have landed; only `SENT(WAN)` means confirmed. |
| `WAN_PEER_SILENT:<id>@<node>` | The node accepted the message and never answered within the reply budget — it is still holding the stream, which points at a wedged or overloaded node rather than an old one. Delivery NOT confirmed; only `SENT(WAN)` means confirmed. <!-- [doc->REQ-WAN-REPLY-BOUND] --> |
| `WAN_FAIL:<id> — <error>` | Cross-node transport failure. |
| `AMBIGUOUS:<id> — <why>` | Several nodes host that id — qualify (`<id>@<node>`). |
| `EMPTY_MSG` | Refused: empty body. |

## The `<EVENT>` wire contract

Every arriving message on an **agent** surface (`spt ready`, `api listen`,
`api poll`, `api worker-poll`) is one `<EVENT …>body</EVENT>` envelope —
never a bare body:

```text
<EVENT type="msg" from="lea">hello</EVENT>
<EVENT type="alarm" target-time="…" current-time="…">check the build</EVENT>
```

A body that is already a fully-formed typed envelope (`echo_commune`,
`notify`, `user-msg`, …) passes through **verbatim** — one envelope, never
re-wrapped, and the body's own `from` wins. On the listener stream an
oversized line splits into `<EVENT-PART seq="K/M" id="…">` chunks the
receiver reassembles; `api poll` / `api worker-poll` always emit one whole
envelope per message, never chunked.

**Escaping — the closed entity set.** Four entities, plus one newline token per
half of the envelope: bodies use `<br>`, attribute values use `&#10;`. There is
no `&#39;`/`&apos;` (single quotes ride literal):

- **Encode (body):** `&` → `&amp;` **first**, then `<` → `&lt;`, `>` → `&gt;`,
  `"` → `&quot;`; then CRLF and lone CR normalize to LF, and LF → `<br>`.
- **Decode (body):** split/replace `<br>` → newline **first**, then
  `&lt;` → `<`, `&gt;` → `>`, `&quot;` → `"`, and `&amp;` → `&` **last**.
  Amp-last is the invariant that keeps an embedded `&amp;lt;` from
  double-decoding into `<`.
<!-- [doc->REQ-HAZARD-ENVELOPE-ATTR-LINESAFE] -->
- **Encode (attribute value):** the same four entities in the same order, then
  the attribute's own linebreak step — CRLF and lone CR normalize to LF, and
  LF → `&#10;` (**not** `<br>`, which belongs to the body).
- **Decode (attribute value):** `&#10;` → newline **first**, then the same
  tag-shaped entities, and `&amp;` → `&` **last**. Amp-last is what makes the
  newline entity safe: an attribute carrying the literal text `&#10;` arrives
  as `&amp;#10;`, which the `&#10;` step cannot match, so it decodes back to
  the literal rather than to a linebreak.
- Attribute values are line-safe because the **encoder makes them so**, not by
  construction. A receiver-composed attribute can carry newlines — the
  `trust-warning` block runs to several — and the envelope is line-framed, so an
  unencoded newline would split one delivery into several lines of which most
  are not envelopes.
- Decode only the **extracted body substring** after parsing the envelope
  framing — never run the entity decode over the full line, or the framing
  tokens themselves unescape.

<!-- [doc->REQ-SEAL-ENVELOPE-ATTR] the published attribute contract: sender-authored class, survives the receiver-composed strip, adapter MUST surface it, citation never authorization -->
**The `seal` attribute — sender-authored, and it must be surfaced.** A
[sealed message](wax-seal.md) arrives carrying its seal token as a `seal="…"`
attribute on the envelope:

```text
<EVENT type="msg" from="reavo" seal="k7mn4wq2vx">Approved: run the migration tonight</EVENT>
```

This is the **envelope author's own field** — like `type`, `from`, or an
alarm's times — not one of the receiver-composed attributes
([`trust-warning`, `mnemonics-json`](../networking/monics.md)) your node
strips off inbound bodies before attaching its own. The seal attribute rides
**end-to-end intact** through that strip: it is the sender's own evidence
citation, and stripping it would delete the citation at every ingress.

**For adapter authors: this attribute must be surfaced** — the same obligation
`trust-warning` carries. A pipeline that re-renders a delivery has to carry
the token through and show it, or the receiving agent loses the pointer to
evidence the sender deliberately attached. And the mirror rule: the attribute
is a **citation, never an authorization subject**. Nothing may branch
authority on its presence or value — only a `BOUND` verdict from
`spt api seal verify` over the delivered body is evidence (a forged attribute
is harmless precisely because verify recomputes the hash).

<!-- [doc->REQ-EVENT-ATTR-PASSTHROUGH] the class rule: open sender-authored attr set, pass-through render obligation, no allowlist, escaping stays in attribute position -->
**The attribute set is open — re-render by pass-through, never by allowlist.**
The sender-authored attributes above are a *class*, not a list: new ones join
the envelope over time (`seal` is only the newest), and they join with their
obligations already in force. So the render rule for any consumer that
re-emits deliveries (an adapter pipeline, a digest, a relay surface):

- **Carry through every attribute you do not yourself consume.** Re-emitting
  from a fixed list of known names silently deletes every attribute added
  after that list was written — and the deletion is invisible at the site
  that wrote the list, because nothing there ever names what it dropped. The
  only closed list in this contract is the receiver-composed **strip** class
  (`trust-warning`, `mnemonics-json`), and it is a strip list, not a render
  list; everything else rides.
- **"Consume" means the attribute's distinction is re-expressed, not merely
  read.** A pipeline consumes an attribute when its handling puts the value's
  meaning back in front of the agent — content re-rendered into the
  pipeline's own surface, or a dispatch whose *agent-visible outcome* differs
  per value (routing on `type` consumes it only while distinct types yield
  distinguishable deliveries). Reading a value and then emitting output that
  two sender-distinct deliveries would share is peeking, not consuming. The
  test is the drop test: if omitting the attribute makes two deliveries the
  sender distinguished indistinguishable at the agent surface, it must ride.
- **Attribute names are tokens, not text.** The envelope's name grammar is
  `[a-z0-9_-]+`. Names are hostile-reachable — envelopes arrive from peer
  *nodes*, not only from this binary — so a pass-through re-emits a name only
  when it matches that grammar; a tag-position span that does not is framing
  damage, not an attribute. Refuse or drop it **loudly**, never re-emit it.
- **Keep the value in attribute position with its wire escaping intact.**
  An attribute value is attr-escaped for exactly one context. Unescaping it
  into a body or frame context hands a hostile sender a forgery seam — a
  crafted value could close your re-rendered tag and land text at your frame
  level, indistinguishable from a real delivery. Pass the raw attr-escaped
  span through; decode only where the final consumer parses attributes.

**MAC-stamped frames are a different surface.** The shell relay drain
(`api poll <shell-id> --link <token>`) emits raw stamped frames of the form
`<mac> <frame>` — a 64-hex-char HMAC-SHA256 over the frame bytes, one space,
then the frame — and is deliberately **not** `<EVENT>`-wrapped (the shell
child verifies the MAC and parses its own vocabulary). Agent-perch surfaces
never emit stamped frames; a parser of agent traffic only ever sees
`<EVENT>` / `<EVENT-PART>` lines.

<!-- [doc->REQ-HAZARD-EVENTPART-REASSEMBLY] -->
### EVENT-PART reassembly (listener stream)

When the **listener stream** (`spt ready`, `spt api listen`) would emit an
`<EVENT>` line longer than its per-line cap, it splits that **one** envelope
across N `<EVENT-PART>` lines. `api poll` / `api worker-poll` never chunk, so
only a consumer of the listener stream needs this — and such a consumer **must**
implement reassembly, or a single oversized delivery wedges its parser. The
rules (a receiver that follows them reconstructs the original envelope
byte-for-byte):

- **`<EVENT-PART>` is a distinct tag — do not prefix-match `<EVENT`.** Each part
  is a whole line `<EVENT-PART seq="K/M" id="…">FRAGMENT</EVENT-PART>` with its
  **own** closing tag `</EVENT-PART>`. A substring test for `<EVENT` matches
  **both** tags, and `</EVENT>` never closes a part — a scanner that keys on
  `<EVENT` / `</EVENT>` will treat a part as an unterminated `<EVENT>` and stall.
  Dispatch on the full tag name.
- **`seq="K/M"`** is **1-indexed**: `K` runs `1..M`; `M` is the total part count,
  identical on every part of the group. Never zero-padded.
- **`id`** is an **opaque grouping token**, unique per split envelope. Group
  parts by `id` **equality only** — never assume its charset, length, or
  structure (it is an implementation-chosen nonce, not a contract value).
- **Attributes ride on the `seq="1/M"` head part only.** The original envelope's
  attrs (`type`, `from`, `notif_id`, …) appear on the head; continuation parts
  carry only `seq` and `id`. Reassembly takes the attrs from the head.
- **Each FRAGMENT is a raw byte-slice of the already-escaped envelope.** A split
  point may fall **inside** an entity (`&amp;`) or a `<br>` token. So:
  **concatenate the fragments in `seq` order first** to recover the whole
  `<EVENT …>…</EVENT>` line, **then** parse the framing and decode the body
  **once** (per the escaping rules above). **Never** decode a fragment on its
  own. `concat(FRAGMENT_1..M)` equals the original envelope's inner content
  verbatim.
- **Parts may arrive out of order and are not guaranteed contiguous.** Key
  partial state by `id`, sort by `seq` `K`, and reassemble only once all `M`
  parts of that `id` are held.
- **Drop orphan groups silently — and keep the receiver alive.** If the
  `seq="1/M"` head is absent (earlier parts lost across a session boundary) or
  any `seq` in `1..M` is missing or never arrives, drop the whole group — never
  emit a partial envelope. "Silently" is about the **stream** (no partial
  envelope reaches it), not the receiver's own diagnostics: a receiver may — and
  for the invisible-failure class, should — surface the drop in its own log or a
  counter. An incomplete group is an expected boundary condition (its canonical
  cause is a mid-stream restart), so it must **not** tear down the receiver — a
  restart cannot recover a group whose head is already gone and only re-creates
  the condition. Bound the pending partial-state (evict the oldest group under a
  cap rather than buffer without limit — an unbounded reassembly buffer is the
  failure mode this whole section exists to prevent), reset it on receiver
  restart, and lose at most the one oversized message, never the stream.

<!-- [doc->REQ-MSG-SHORT-ID] -->
## Message short-IDs and replies

Every message gets an eight-character short-ID when it is committed, minted from
the message's own content hash and scoped to the node that holds it. Nothing
mints a second identifier later: the same token appears in the delivery envelope
(`msg-id`), in the `MSG_IN` and `MSG_OUT` io-event rows, and in the URL
`/<node>/m/<short-id>` — so what you say in a chat line is what you click, and
two readers cannot disagree about what a message is called.

The alphabet is base32 without `0`, `1` and `8`, so `O`/`0` and `I`/`1` are
never two spellings of one id. Read one back with:

```console
$ spt msg show BCDFGH23
BCDFGH23 from doyle to todlando at 1788750167374ms [spool]
attachment: report.md http://localhost:5474/kitsubito/f/report.md (2481 bytes)
---
the report you asked for
```

`--json` is the machine twin, and `/<node>/m/<id>` renders the same message in a
browser (`?json` there too). Treat the id as **opaque**: a collision on one node
is detected when it is minted and resolved by lengthening that one id to nine
characters, then ten, which is safe precisely because no reader parses it.

`spt send <target> --reply-to <id>` carries the parent's id in the envelope so
an adapter *may* render a thread. It is a label on the message, not a way to
address one: `target` stays required, and an **unknown parent is carried, not
refused** — the parent may live on a node we cannot ask, and refusing would make
a thread across a partition impossible rather than merely unrendered.

Files ride along the same way: see
[Attachments and `spt fetch`](../serving/attachments.md).

## Addressing

Bare ids (`sergey`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`sergey@desktop` — node labels and key prefixes both work) rather
than guessing. The full form is `[subnet:]id[@node]`.

## Sending from inside a turn

An agent whose adapter declares [`[io] compliance`](../harness-contract/manifest.md#io--io-funnel-compliance)
can send by writing a **shortform tag** in its own output — `@<doyle,perri the
build is green @>` — instead of shelling out to `spt send`. It is not a special
class of message: dispatch goes through the ordinary send path, with the same
admission, sealing, spooling and refusal behaviour. Two things differ, and both
are deliberate: a tag inside backticks or a fenced code block is a **quotation**
that sends nothing, and the outcome comes back only through the now-signal's
`DISPATCH_RESULTS` category — a dispatch never echoes or replies to confirm
itself. The grammar, the suppression rules, and the manifest gate are specified
in [the frame contract](../shells/frames.md#shortform-sending-from-inside-a-turn).

A message that **arrives** carrying a tag is text: spt-core never parses an
inbound body for shortform, so nothing you receive can make you send.

## Commands

`send` · `ring` · `ready` (blocks; `--once` drains and exits) · `list` ·
`stop` · `whoami` · `msg show` · `fetch` — every flag in the [CLI reference](../cli/reference.md).
Agents get the task-oriented version from the binary itself: `spt how-to
ready` / `spt how-to send`.
