# REQ-MSG-ENVELOPE — `<EVENT>`-everywhere + `__REPLY_TO__` removal — JIT refactor plan

> Implements **ADR-0020** (crystallized via grill-with-docs, 2026-06-15). **Executor: todlando**
> (spt-core executor); **doyle gates** (same model as M12). Dispatched 2026-06-15.

## Goal

Make the canonical `<EVENT type="msg" from="…">body</EVENT>` envelope the SOLE arriving-message
format at every surface (incl `api poll`/`worker-poll`); delete `__REPLY_TO__` from spt-core
entirely; carry `(from, body)` structurally; rebind reply-correlation onto `from`. F-002 dissolves.

## Approach — structural spine, compose at the edge

`(from, body)` is the internal currency. `__REPLY_TO__` (a serialize-then-reparse relic) is removed.
`<EVENT>` is composed once, at each delivery surface, via `spt-proto::event::compose_msg_event`
(typed-passthrough via `is_typed_event_envelope`). Listener composes **and** chunks (Monitor cap);
hook drains compose **whole** `<EVENT>`s (no `<EVENT-PART>`).

## Tasks (land in order — structural spine first, surfaces next, sweep last)

- **T0 — verify + activate.** Confirm callers (done: TCP wire is live — `deliver.rs`→`listener.rs`;
  `format_row` feeds `emit.rs`/`delivery.rs`). Activate REQ-MSG-ENVELOPE `impl,unit,int` in
  `traceable-reqs.toml` (currently `[doc]`).
- **T1 — spt-msg structural spine.** `emit.rs`: drop `parse_frame`; `render_event_lines(from, body)`
  composes directly from the tuple (no `__REPLY_TO__` round-trip). Add/confirm a whole-`<EVENT>`
  compose entry (no chunk) for the hook-drain surface. `wire.rs`: replace the
  `__REPLY_TO__:{from}\n{body}` TCP frame with structural carriage of `(from, body)` (e.g. a small
  framed record carrying both, or compose/parse `<EVENT>` on the wire) — **migrate `deliver.rs`
  (send) + `listener.rs` (recv) together**. `lib.rs`: correct the ADR-0001 framing (the `<EVENT>`
  envelope is the stable format; the TCP frame is internal, not a sister-interop contract).
  `ring.rs`/`ready.rs`/`deliver.rs`: rebind off the `__REPLY_TO__` string onto `(from, body)`.
- **T2 — spt-store spool.** Delete `spool.rs::format_row`. `drain_where`/`drain_all_at`/
  `drain_non_deferred_at`/`drain_one_at` return **structured `(from, body)` rows** (not the joined
  string); callers compose. `access.rs` comment/anchor → `from`.
- **T3 — spt `api poll`/`worker-poll`.** `delivery.rs::cmd_poll`: for each structured drained row,
  emit a **whole `<EVENT>`** (`compose_msg_event`, typed-passthrough) via the T1 whole-compose —
  replacing `emit()`=`print!(raw)`. `cli.rs`: reword `--from` help (no `__REPLY_TO__`).
- **T4 — spt-daemon.** `psyrelay.rs` + `outbound`(spt-live): reply-target sources from the inbound
  `<EVENT>`'s `from` (strip-and-re-stamp anti-spoof unchanged). `notif.rs`: remove the
  `__REPLY_TO__:issuer\n<EVENT…>` wrapping (the typed envelope passes through verbatim — no double
  prefix). `access.rs`: reply-vs-new correlation keys on `from` vs prior outbound. `shellchan.rs`:
  comment.
- **T5 — spt-net comment-only.** `wanmsg.rs` `from` is already structural — fix the
  "(`__REPLY_TO__`)" comments; **no wire change** (N-1 safe by construction).
- **T6 — docs.** KNOWN-HAZARDS Psyche-routing invariant (7.x) → "reply to the inbound `from`".
  (ADR-0020 + ADR-0009/0012 amendments + CONTEXT glossary already landed.)
- **T7 — test sweep.** Migrate every `__REPLY_TO__` assertion (emit/wire/spool/ready/listener/ring/
  psyrelay/deliver/killer_quickstart) to `<EVENT>`/structured. New **int**: `api poll` emits whole
  `<EVENT>`s (single + multi-message, self-delimiting). Full workspace suite + clippy -D + traceable
  EXIT=0 + xtask check.

## Open impl-decisions (resolve at execution)

1. **TCP wire (T1) shape:** carry `(from, body)` as a tiny structural record on the existing
   length-prefixed frame, **or** put the composed `<EVENT>` line on the wire (then the receiver
   parses it). Lean: structural record (keeps composition at the delivery edge, one compose site).
2. **Drain API (T2):** return `Vec<(String, String)>` (from, body) vs a small struct. Lean: a named
   struct (`DrainedMsg { from, body }`) for clarity across the many callers.
3. **Anonymous sender** (`from==""`): renders `<EVENT type="msg" from="">…` (already the
   `render_event_lines` behavior) — confirm adapters treat `from=""` as anonymous (perli does).

## Acceptance (doyle gate)

`__REPLY_TO__` grep-clean across `crates/` (only historical ADR/CHANGELOG mentions remain); `api poll`
emits whole `<EVENT>`s (int-proven, multi-message self-delimiting); listener stream unchanged
(still chunks); reply-correlation + Psyche routing key on `from` (unit); full sweep green; traceable
REQ-MSG-ENVELOPE `[doc,impl,unit,int]` OK. Then ping perli → byte-capture confirms `<EVENT>` → flip
their int.
