The file C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\outbound.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    /// Relay to the `from` of the inbound being answered.
    Reply { body: String },

//! Psyche outbound intent parser (ADR-0012 / M4-D7.5b) — the parse half of
//! the daemon's parse-and-relay proxy.
//!
//! A sandboxed Psyche's **sole outbound is its stdout** (KNOWN-HAZARDS 7.3):
//! the captured turn output ([`crate::turn::run_psyche_turn`]) is scanned for
//! `<EVENT type="reply">` / `<EVENT type="notify">` envelopes — the only two
//! event types a Psyche may author (`spt_proto::event::is_psyche_authorable_type`).
//! Everything else in the stream — LLM prose, a confused Psyche's `commune`
//! or `msg` envelope, `EVENT-PART` chunk lines — is ignored, never relayed.
//!
//! ## The structural strip (sanitize half 1 of 2)
//! A [`PsycheIntent`] carries the decoded **body only** — the type has no
//! `from`/`to`/attr fields, so a Psyche-supplied `from=`/target is discarded
//! at parse and is *unrepresentable* downstream. The daemon relay
//! (`spt-daemon`, D7.5c) composes the wire envelope itself with the
//! re-stamped `from=<psyche_id>` and constrained routing; the markup is never
//! trusted (ADR-0012: anti-spoof is a conformance invariant).
//
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]

use spt_proto::event::{is_psyche_authorable_type, parse_event, EVENT_TYPE_REPLY};

/// A relayable intent parsed from a Psyche turn's captured stdout. Body only
/// — by construction no Psyche-supplied routing survives the parse.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PsycheIntent {
    /// Relay to the `__REPLY_TO__` sender of the inbound being answered.
    Reply { body: String },
    /// Relay to the agent's own user/subnet only.
    Notify { body: String },
}

impl PsycheIntent {
    /// The decoded body, whichever the intent kind.
    pub fn body(&self) -> &str {
        match self {
            PsycheIntent::Reply { body } | PsycheIntent::Notify { body } => body,
        }
    }
}

/// Scan a captured Psyche stdout for relayable intents, in stream order.
///
/// Line-based: each line is tried as one `<EVENT …>…</EVENT>` envelope
/// (`parse_event` — panic-free, malformed → skipped). Only the two
/// Psyche-authorable types yield intents; every attribute on the envelope is
/// dropped (the structural strip — see module docs). Prose lines and
/// non-authorable envelope types are ignored silently: a chatty Psyche that
/// emitted no envelope relays nothing.
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
pub fn parse_psyche_intents(stdout: &str) -> Vec<PsycheIntent> {
    let mut intents = Vec::new();
    for line in stdout.lines() {
        let Some(ev) = parse_event(line) else {
            continue;
        };
        let Some(t) = ev.event_type.as_deref() else {
            continue;
        };
        if !is_psyche_authorable_type(t) {
            continue;
        }
        let body = ev.body;
        if t == EVENT_TYPE_REPLY {
            intents.push(PsycheIntent::Reply { body });
        } else {
            intents.push(PsycheIntent::Notify { body });
        }
    }
    intents
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] reply + notify parse in stream order, bodies decoded.
    fn parses_reply_and_notify_in_order() {
        let stdout = "I'll answer them now.\n\
            <EVENT type=\"reply\">on it — ETA 5 min</EVENT>\n\
            some more thinking\n\
            <EVENT type=\"notify\">build finished & deployed<br>line two</EVENT>\n";
        let got = parse_psyche_intents(stdout);
        assert_eq!(
            got,
            vec![
                PsycheIntent::Reply {
                    body: "on it — ETA 5 min".to_string()
                },
                PsycheIntent::Notify {
                    body: "build finished & deployed\nline two".to_string()
                },
            ]
        );
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] the structural strip:
            // spoofed from=/to= attrs cannot survive the parse — the intent type has
            // no field to carry them.
    fn spoofed_routing_attrs_are_unrepresentable() {
        let stdout = "<EVENT type=\"reply\" from=\"evil-imposter\" to=\"victim\">payload</EVENT>\n";
        let got = parse_psyche_intents(stdout);
        assert_eq!(
            got,
            vec![PsycheIntent::Reply {
                body: "payload".to_string()
            }]
        );
        // No API on the result exposes the supplied attrs at all.
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] non-authorable envelope
            // types from a confused Psyche are ignored — commune is the Summarizer's
            // inbound, never a Psyche output (ADR-0012).
    fn non_authorable_types_are_ignored() {
        let stdout = "<EVENT type=\"commune\">fake delta</EVENT>\n\
            <EVENT type=\"msg\" from=\"x\">fake msg</EVENT>\n\
            <EVENT type=\"echo_commune\" from=\"x\">fake brief</EVENT>\n\
            <EVENT type=\"alarm\" target-time=\"t\" current-time=\"t\">fake alarm</EVENT>\n";
        assert!(parse_psyche_intents(stdout).is_empty());
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] prose-only stdout (no
            // envelope) yields zero intents — a chatty Psyche relays nothing.
    fn prose_only_yields_nothing() {
        let stdout = "Thought about it.\nNothing to send right now.\n";
        assert!(parse_psyche_intents(stdout).is_empty());
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] malformed envelopes and
            // EVENT-PART chunk lines are skipped panic-free.
    fn malformed_and_chunk_lines_are_skipped() {
        let stdout = "<EVENT type=\"reply\">no closer\n\
            <EVENT-PART seq=\"1/2\" id=\"abcd1234\" type=\"reply\">chunk</EVENT-PART>\n\
            <EVENT unterminated\n\
            <EVENT type=\"reply\">real one</EVENT>\n";
        assert_eq!(
            parse_psyche_intents(stdout),
            vec![PsycheIntent::Reply {
                body: "real one".to_string()
            }]
        );
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] an empty-body envelope
            // still parses to an intent (the relay drops empties with a typed outcome).
    fn empty_body_parses_to_empty_intent() {
        let got = parse_psyche_intents("<EVENT type=\"notify\"></EVENT>\n");
        assert_eq!(
            got,
            vec![PsycheIntent::Notify {
                body: String::new()
            }]
        );
    }
}

-    /// Relay to the `__REPLY_TO__` sender of the inbound being answered.

+    /// Relay to the `from` of the inbound being answered.


Edit

//! the JSON decodes as an ignored unknown field and influences nothing. The
//! `from` field is reply-routing metadata (`__REPLY_TO__`), not an
//! authorization subject.

//! the JSON decodes as an ignored unknown field and influences nothing. The
//! `from` field is reply-routing metadata (the structural sender id), not an
//! authorization subject.


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//! the JSON decodes as an ignored unknown field and influences nothing. The
//! `from` field is reply-routing metadata (the structural sender id), not an
//! authorization subject.

//! WAN message records over broker QUIC streams (M4-D5a) — the REQ-MSG path,
//! now off-node.
//!
//! D4 gave the broker-owned streams a brain can drive across restarts; D3 gave
//! addressing (per-subnet registry + the refuse-ambiguous resolution policy).
//! This module is the thin wire seam between them and the existing local
//! delivery path: a minimal message record, the shared NDJSON framing
//! ([`crate::net::ndjson`]), and a routing helper that decides local-vs-WAN
//! from a [`Resolution`]. The receive-side funnel (op-id dedup → access gate →
//! `spt_msg::deliver`) lives in the daemon, which owns the perch/owlery
//! machinery.
//!
//! ## Origin is transport truth, never payload (REQ-HAZARD-WAN-ORIGIN-AUTH)
//!
//! A [`WanMessage`] deliberately carries **no origin-node field**: the access
//! gate's subject (ADR-0009 origin-node whitelist) is the QUIC connection's
//! handshake-proven remote node id (iroh `EndpointId` == the Ed25519 node
//! pubkey, the REQ-NET-1 identity binding) — read from the broker's stream
//! table, never from record bytes. A forged `origin_node`/`node` field inside
//! the JSON decodes as an ignored unknown field and influences nothing. The
//! `from` field is reply-routing metadata (`__REPLY_TO__`), not an
//! authorization subject.

use serde::{Deserialize, Serialize};

use crate::net::ndjson::{self, NdjsonDecoder};
use crate::net::registry::{Ambiguity, Resolution};

/// One WAN message record: a message for `target` (an endpoint id at the
/// receiving node), as sent by `from` (reply-routing metadata). `op_id` is the
/// sender-minted durable idempotency key — the receiver dedups on it before
/// delivering, so a replayed feed (sender redrive, receiver brain restart
/// resubscribing from an earlier cursor) delivers exactly once
/// (REQ-HAZARD-RESTART-IDEMPOTENT, now wire-fed).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WanMessage {
    /// The endpoint the message is for (resolved at the receiving node).
    pub target: String,
    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =
    /// anonymous). Metadata only; never an authorization subject.
    pub from: String,
    /// The message body.
    pub body: String,
    /// Sender-minted durable idempotency key, unique per logical send (e.g.
    /// `<sender-node-hex>:<epoch>` from the node's own `EpochSource`).
    pub op_id: String,
}

impl WanMessage {
    /// Encode as one NDJSON line (the JSON document + `\n`).
    pub fn encode_line(&self) -> Vec<u8> {
        ndjson::encode_line(self)
    }
}

/// The WAN message feed's NDJSON reassembler — the shared
/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to
/// [`WanMessage`] records. One decoder per stream.
pub type WanDecoder = NdjsonDecoder<WanMessage>;

/// Where a send should go, decided from a registry [`Resolution`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanRoute {
    /// A live instance on this node — the existing local path
    /// (`spt_msg::deliver`), no wire.
    Local,
    /// A live instance on another node — dial and send a [`WanMessage`].
    /// `subnet` is set when resolution crossed a per-subnet registry.
    Remote {
        subnet: Option<String>,
        node: String,
    },
    /// Resolution refused — force qualification (D3c policy: never guess).
    Refused(Ambiguity),
    /// No live (routable) instance anywhere.
    NotFound,
}

/// Map a [`Resolution`] to a [`WanRoute`] against this node's own id. The
/// resolution policy (local-first, refuse-ambiguous, `@node` pin, visibility)
/// already ran inside `resolve_across_visible`; this only splits its hit into
/// the local and wire legs.
// [impl->REQ-NET-1]
pub fn route_wan(resolution: Resolution, local_node: &str) -> WanRoute {
    match resolution {
        Resolution::Resolved { instance, .. } if instance.node == local_node => WanRoute::Local,
        Resolution::Resolved { subnet, instance } => WanRoute::Remote {
            subnet,
            node: instance.node,
        },
        Resolution::Ambiguous(why) => WanRoute::Refused(why),
        Resolution::NotFound => WanRoute::NotFound,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::net::registry::{Instance, Status};

    fn resolved(node: &str) -> Resolution {
        Resolution::Resolved {
            subnet: Some("home".into()),
            instance: Instance {
                node: node.into(),
                status: Status::Active,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        }
    }

    // [unit->REQ-NET-1] a record round-trips through the NDJSON line encoding,
    // and an unknown future field decodes (wire forward-compat).
    #[test]
    fn message_round_trips_and_tolerates_unknown_field() {
        let m = WanMessage {
            target: "ling".into(),
            from: "doyle".into(),
            body: "hello across the wire".into(),
            op_id: "cafe:42".into(),
        };
        let line = m.encode_line();
        assert_eq!(line.last(), Some(&b'\n'));
        let back: WanMessage = serde_json::from_slice(&line[..line.len() - 1]).unwrap();
        assert_eq!(back, m);

        let wire =
            br#"{"target":"ling","from":"doyle","body":"x","op_id":"n:1","added_at_d6":true}"#;
        let future: WanMessage = serde_json::from_slice(wire).unwrap();
        assert_eq!(future.op_id, "n:1");
    }

    // [unit->REQ-HAZARD-WAN-ORIGIN-AUTH] the record has no origin-node field:
    // a forged `origin_node` (or `node`) inside the JSON decodes as an ignored
    // unknown field — nothing in the decoded record can impersonate a node.
    // The gate's subject comes from the QUIC handshake (the broker's stream
    // table), asserted end-to-end in the daemon's wanmsg int test.
    #[test]
    fn forged_origin_field_is_inert() {
        let wire = br#"{"target":"ling","from":"doyle","body":"x","op_id":"n:1","origin_node":"evil","node":"evil"}"#;
        let decoded: WanMessage = serde_json::from_slice(wire).unwrap();
        // Every field the record actually has is accounted for — there is no
        // place for the forged origin to land.
        assert_eq!(
            decoded,
            WanMessage {
                target: "ling".into(),
                from: "doyle".into(),
                body: "x".into(),
                op_id: "n:1".into()
            }
        );
    }

    // [unit->REQ-NET-1] route_wan splits a resolution into the local and wire
    // legs and passes refusals through (never guess past an ambiguity).
    #[test]
    fn route_splits_local_and_remote_and_passes_refusals() {
        assert_eq!(route_wan(resolved("here"), "here"), WanRoute::Local);
        assert_eq!(
            route_wan(resolved("there"), "here"),
            WanRoute::Remote {
                subnet: Some("home".into()),
                node: "there".into()
            }
        );
        let amb = Resolution::Ambiguous(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]));
        assert_eq!(
            route_wan(amb, "here"),
            WanRoute::Refused(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]))
        );
        assert_eq!(route_wan(Resolution::NotFound, "here"), WanRoute::NotFound);
    }
}

-//! `from` field is reply-routing metadata (`__REPLY_TO__`), not an

+//! `from` field is reply-routing metadata (the structural sender id), not an


    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =
    /// anonymous). Metadata only; never an authorization subject.
    pub from: String,

    /// The sender endpoint id — the structural `from` at delivery (empty =
    /// anonymous). Metadata only; never an authorization subject.
    pub from: String,


    /// The sender endpoint id — the structural `from` at delivery (empty =
    /// anonymous). Metadata only; never an authorization subject.
    pub from: String,

//! WAN message records over broker QUIC streams (M4-D5a) — the REQ-MSG path,
//! now off-node.
//!
//! D4 gave the broker-owned streams a brain can drive across restarts; D3 gave
//! addressing (per-subnet registry + the refuse-ambiguous resolution policy).
//! This module is the thin wire seam between them and the existing local
//! delivery path: a minimal message record, the shared NDJSON framing
//! ([`crate::net::ndjson`]), and a routing helper that decides local-vs-WAN
//! from a [`Resolution`]. The receive-side funnel (op-id dedup → access gate →
//! `spt_msg::deliver`) lives in the daemon, which owns the perch/owlery
//! machinery.
//!
//! ## Origin is transport truth, never payload (REQ-HAZARD-WAN-ORIGIN-AUTH)
//!
//! A [`WanMessage`] deliberately carries **no origin-node field**: the access
//! gate's subject (ADR-0009 origin-node whitelist) is the QUIC connection's
//! handshake-proven remote node id (iroh `EndpointId` == the Ed25519 node
//! pubkey, the REQ-NET-1 identity binding) — read from the broker's stream
//! table, never from record bytes. A forged `origin_node`/`node` field inside
//! the JSON decodes as an ignored unknown field and influences nothing. The
//! `from` field is reply-routing metadata (the structural sender id), not an
//! authorization subject.

use serde::{Deserialize, Serialize};

use crate::net::ndjson::{self, NdjsonDecoder};
use crate::net::registry::{Ambiguity, Resolution};

/// One WAN message record: a message for `target` (an endpoint id at the
/// receiving node), as sent by `from` (reply-routing metadata). `op_id` is the
/// sender-minted durable idempotency key — the receiver dedups on it before
/// delivering, so a replayed feed (sender redrive, receiver brain restart
/// resubscribing from an earlier cursor) delivers exactly once
/// (REQ-HAZARD-RESTART-IDEMPOTENT, now wire-fed).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WanMessage {
    /// The endpoint the message is for (resolved at the receiving node).
    pub target: String,
    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =
    /// anonymous). Metadata only; never an authorization subject.
    pub from: String,
    /// The message body.
    pub body: String,
    /// Sender-minted durable idempotency key, unique per logical send (e.g.
    /// `<sender-node-hex>:<epoch>` from the node's own `EpochSource`).
    pub op_id: String,
}

impl WanMessage {
    /// Encode as one NDJSON line (the JSON document + `\n`).
    pub fn encode_line(&self) -> Vec<u8> {
        ndjson::encode_line(self)
    }
}

/// The WAN message feed's NDJSON reassembler — the shared
/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to
/// [`WanMessage`] records. One decoder per stream.
pub type WanDecoder = NdjsonDecoder<WanMessage>;

/// Where a send should go, decided from a registry [`Resolution`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanRoute {
    /// A live instance on this node — the existing local path
    /// (`spt_msg::deliver`), no wire.
    Local,
    /// A live instance on another node — dial and send a [`WanMessage`].
    /// `subnet` is set when resolution crossed a per-subnet registry.
    Remote {
        subnet: Option<String>,
        node: String,
    },
    /// Resolution refused — force qualification (D3c policy: never guess).
    Refused(Ambiguity),
    /// No live (routable) instance anywhere.
    NotFound,
}

/// Map a [`Resolution`] to a [`WanRoute`] against this node's own id. The
/// resolution policy (local-first, refuse-ambiguous, `@node` pin, visibility)
/// already ran inside `resolve_across_visible`; this only splits its hit into
/// the local and wire legs.
// [impl->REQ-NET-1]
pub fn route_wan(resolution: Resolution, local_node: &str) -> WanRoute {
    match resolution {
        Resolution::Resolved { instance, .. } if instance.node == local_node => WanRoute::Local,
        Resolution::Resolved { subnet, instance } => WanRoute::Remote {
            subnet,
            node: instance.node,
        },
        Resolution::Ambiguous(why) => WanRoute::Refused(why),
        Resolution::NotFound => WanRoute::NotFound,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::net::registry::{Instance, Status};

    fn resolved(node: &str) -> Resolution {
        Resolution::Resolved {
            subnet: Some("home".into()),
            instance: Instance {
                node: node.into(),
                status: Status::Active,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        }
    }

    // [unit->REQ-NET-1] a record round-trips through the NDJSON line encoding,
    // and an unknown future field decodes (wire forward-compat).
    #[test]
    fn message_round_trips_and_tolerates_unknown_field() {
        let m = WanMessage {
            target: "ling".into(),
            from: "doyle".into(),
            body: "hello across the wire".into(),
            op_id: "cafe:42".into(),
        };
        let line = m.encode_line();
        assert_eq!(line.last(), Some(&b'\n'));
        let back: WanMessage = serde_json::from_slice(&line[..line.len() - 1]).unwrap();
        assert_eq!(back, m);

        let wire =
            br#"{"target":"ling","from":"doyle","body":"x","op_id":"n:1","added_at_d6":true}"#;
        let future: WanMessage = serde_json::from_slice(wire).unwrap();
        assert_eq!(future.op_id, "n:1");
    }

    // [unit->REQ-HAZARD-WAN-ORIGIN-AUTH] the record has no origin-node field:
    // a forged `origin_node` (or `node`) inside the JSON decodes as an ignored
    // unknown field — nothing in the decoded record can impersonate a node.
    // The gate's subject comes from the QUIC handshake (the broker's stream
    // table), asserted end-to-end in the daemon's wanmsg int test.
    #[test]
    fn forged_origin_field_is_inert() {
        let wire = br#"{"target":"ling","from":"doyle","body":"x","op_id":"n:1","origin_node":"evil","node":"evil"}"#;
        let decoded: WanMessage = serde_json::from_slice(wire).unwrap();
        // Every field the record actually has is accounted for — there is no
        // place for the forged origin to land.
        assert_eq!(
            decoded,
            WanMessage {
                target: "ling".into(),
                from: "doyle".into(),
                body: "x".into(),
                op_id: "n:1".into()
            }
        );
    }

    // [unit->REQ-NET-1] route_wan splits a resolution into the local and wire
    // legs and passes refusals through (never guess past an ambiguity).
    #[test]
    fn route_splits_local_and_remote_and_passes_refusals() {
        assert_eq!(route_wan(resolved("here"), "here"), WanRoute::Local);
        assert_eq!(
            route_wan(resolved("there"), "here"),
            WanRoute::Remote {
                subnet: Some("home".into()),
                node: "there".into()
            }
        );
        let amb = Resolution::Ambiguous(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]));
        assert_eq!(
            route_wan(amb, "here"),
            WanRoute::Refused(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]))
        );
        assert_eq!(route_wan(Resolution::NotFound, "here"), WanRoute::NotFound);
    }
}

-    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =

+    /// The sender endpoint id — the structural `from` at delivery (empty =


Source migration nearly done. Now T7 test sweep. Fresh grep for all remaining `__REPLY_TO__`.


crates\spt-msg\tests\killer_quickstart.rs:56:    if let Some(rest) = frame.strip_prefix("__REPLY_TO__:") {
crates\spt-proto\src\event.rs:48:/// A Psyche-authored reply intent — routed by the daemon to the `__REPLY_TO__`
crates\spt-msg\src\wire.rs:4://! `__REPLY_TO__` serialization (ADR-0020: that string was a mis-elevated
crates\spt-msg\src\wire.rs:24:/// `[u32 BE len(from)][from][body]`. No `__REPLY_TO__`, no newline framing —
crates\spt-msg\src\wire.rs:99:    // no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).
crates\spt-msg\src\lib.rs:11://! mis-elevated `__REPLY_TO__` framing). The delivery routing, listener
crates\spt-msg\src\emit.rs:7://! no `__REPLY_TO__` parse step (that relic, and the serialize-then-reparse
crates\spt-store\src\spool.rs:91:/// `__REPLY_TO__` frame (the relic round-trip is gone).
crates\spt\src\cli.rs:65:        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
crates\spt-daemon\src\notif.rs:607:            // <EVENT notify> envelope verbatim — NO __REPLY_TO__ wrapper, NO
crates\spt-daemon\src\psyrelay.rs:375:            // notify <EVENT> verbatim (no __REPLY_TO__ wrapper, no double-prefix).


Remaining refs are mostly my own explanatory comments. doyle's gate = **ZERO** `__REPLY_TO__` in crates/ (incl comments/tests). Scrub the literal token everywhere in crates/ (docs/ADR keeps it as history). Reword to "legacy reply-to".


//! The frame carries `(from, body)` **structurally** — there is no
//! `__REPLY_TO__` serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in

//! The frame carries `(from, body)` **structurally** — there is no
//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-msg\src\wire.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//! The frame carries `(from, body)` **structurally** — there is no
//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in

//! TCP length-prefixed message framing.
//!
//! The frame carries `(from, body)` **structurally** — there is no
//! `__REPLY_TO__` serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in
//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).
//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`.
//!
//! Frame: a 4-byte big-endian outer length prefix, then that many payload
//! bytes. Payload: `[u32 BE len(from)][from bytes][body bytes]` — the receiver
//! splits it straight back into `(from, body)`. An empty `from` (len 0) is an
//! anonymous delivery (commune / sign-off — the recipient must not reply).
// [impl->REQ-MSG-1]
// [impl->REQ-MSG-ENVELOPE]

use std::io::{self, Read, Write};
use std::net::TcpStream;

/// Maximum framed message size accepted off the wire (16 MiB). Guards a
/// malicious/garbage length prefix from triggering a huge allocation.
const MAX_MESSAGE_BYTES: usize = 16 * 1024 * 1024;

/// Encode `(from, body)` into the structural wire payload:
/// `[u32 BE len(from)][from][body]`. No `__REPLY_TO__`, no newline framing —
/// `from` and `body` are carried as raw byte ranges, so a body that is itself a
/// pre-formed `<EVENT>` envelope rides verbatim.
// [impl->REQ-MSG-ENVELOPE]
pub fn encode_frame(from_id: &str, body: &str) -> Vec<u8> {
    let from_bytes = from_id.as_bytes();
    let body_bytes = body.as_bytes();
    let mut payload = Vec::with_capacity(4 + from_bytes.len() + body_bytes.len());
    payload.extend_from_slice(&(from_bytes.len() as u32).to_be_bytes());
    payload.extend_from_slice(from_bytes);
    payload.extend_from_slice(body_bytes);
    payload
}

/// Decode the structural wire payload back into `(from, body)`. Rejects a
/// truncated/garbage `from` length prefix.
// [impl->REQ-MSG-ENVELOPE]
pub fn decode_frame(payload: &[u8]) -> io::Result<(String, String)> {
    if payload.len() < 4 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "frame shorter than the 4-byte from-length prefix",
        ));
    }
    let from_len = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
    if 4 + from_len > payload.len() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "from-length prefix overruns the frame",
        ));
    }
    let from = String::from_utf8(payload[4..4 + from_len].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    let body = String::from_utf8(payload[4 + from_len..].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok((from, body))
}

/// Write a length-prefixed `(from, body)` message to a TCP stream.
/// Format: 4-byte big-endian outer length prefix, then the structural payload.
pub fn write_message(stream: &mut TcpStream, from_id: &str, body: &str) -> io::Result<()> {
    let payload = encode_frame(from_id, body);
    let len = payload.len() as u32;
    stream.write_all(&len.to_be_bytes())?;
    stream.write_all(&payload)?;
    stream.flush()?;
    Ok(())
}

/// Read a length-prefixed message from a TCP stream, returning `(from, body)`.
/// Reads the 4-byte big-endian outer length, then that many bytes, then splits
/// the structural payload. Rejects an oversized length prefix before allocating.
pub fn read_message(stream: &mut TcpStream) -> io::Result<(String, String)> {
    let mut len_buf = [0u8; 4];
    stream.read_exact(&mut len_buf)?;
    let len = u32::from_be_bytes(len_buf) as usize;

    if len > MAX_MESSAGE_BYTES {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("Message too large: {} bytes", len),
        ));
    }

    let mut buf = vec![0u8; len];
    stream.read_exact(&mut buf)?;
    decode_frame(&buf)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{TcpListener, TcpStream};

    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —
    // no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).
    #[test]
    fn encode_decode_round_trips_structural() {
        let payload = encode_frame("alice", "hi\nbob");
        assert_eq!(decode_frame(&payload).unwrap(), ("alice".to_string(), "hi\nbob".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] empty from => anonymous (len-0 from prefix), body intact.
    #[test]
    fn encode_empty_from_is_anonymous() {
        let payload = encode_frame("", "no reply");
        assert_eq!(decode_frame(&payload).unwrap(), ("".to_string(), "no reply".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] a typed <EVENT> body rides the wire verbatim — the
    // structural frame never reparses or rewraps it.
    #[test]
    fn typed_event_body_rides_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer">done</EVENT>"#;
        let payload = encode_frame("issuer", typed);
        assert_eq!(decode_frame(&payload).unwrap().1, typed);
    }

    // [unit->REQ-MSG-ENVELOPE] a truncated from-length prefix is rejected, not panicked.
    #[test]
    fn decode_rejects_overrunning_from_len() {
        // Claim from_len = 100 on a 6-byte payload.
        let mut bad = (100u32).to_be_bytes().to_vec();
        bad.extend_from_slice(b"ab");
        assert_eq!(
            decode_frame(&bad).unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
    }

    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).
    #[test]
    fn frame_round_trips_over_loopback() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server).unwrap()
        });

        let mut client = TcpStream::connect(addr).unwrap();
        write_message(&mut client, "alice", "hello\nworld").unwrap();
        drop(client);

        let got = handle.join().unwrap();
        assert_eq!(got, ("alice".to_string(), "hello\nworld".to_string()));
    }

    // [unit->REQ-MSG-1] an oversized length prefix is rejected, not allocated.
    #[test]
    fn oversized_length_prefix_rejected() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server)
        });

        let mut client = TcpStream::connect(addr).unwrap();
        // Claim 32 MiB (> MAX_MESSAGE_BYTES) then send nothing more.
        let bogus_len = (32u32 * 1024 * 1024).to_be_bytes();
        client.write_all(&bogus_len).unwrap();
        client.flush().unwrap();
        drop(client);

        let result = handle.join().unwrap();
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
    }
}

-//! `__REPLY_TO__` serialization (ADR-0020: that string was a mis-elevated

+//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated


/// `[u32 BE len(from)][from][body]`. No `__REPLY_TO__`, no newline framing —
/// `from` and `body` are carried as raw byte ranges, so a body that is itself a

/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline
/// framing — `from` and `body` are carried as raw byte ranges, so a body that is


/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline
/// framing — `from` and `body` are carried as raw byte ranges, so a body that is

//! TCP length-prefixed message framing.
//!
//! The frame carries `(from, body)` **structurally** — there is no
//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in
//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).
//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`.
//!
//! Frame: a 4-byte big-endian outer length prefix, then that many payload
//! bytes. Payload: `[u32 BE len(from)][from bytes][body bytes]` — the receiver
//! splits it straight back into `(from, body)`. An empty `from` (len 0) is an
//! anonymous delivery (commune / sign-off — the recipient must not reply).
// [impl->REQ-MSG-1]
// [impl->REQ-MSG-ENVELOPE]

use std::io::{self, Read, Write};
use std::net::TcpStream;

/// Maximum framed message size accepted off the wire (16 MiB). Guards a
/// malicious/garbage length prefix from triggering a huge allocation.
const MAX_MESSAGE_BYTES: usize = 16 * 1024 * 1024;

/// Encode `(from, body)` into the structural wire payload:
/// `[u32 BE len(from)][from][body]`. No `__REPLY_TO__`, no newline framing —
/// `from` and `body` are carried as raw byte ranges, so a body that is itself a
/// pre-formed `<EVENT>` envelope rides verbatim.
// [impl->REQ-MSG-ENVELOPE]
pub fn encode_frame(from_id: &str, body: &str) -> Vec<u8> {
    let from_bytes = from_id.as_bytes();
    let body_bytes = body.as_bytes();
    let mut payload = Vec::with_capacity(4 + from_bytes.len() + body_bytes.len());
    payload.extend_from_slice(&(from_bytes.len() as u32).to_be_bytes());
    payload.extend_from_slice(from_bytes);
    payload.extend_from_slice(body_bytes);
    payload
}

/// Decode the structural wire payload back into `(from, body)`. Rejects a
/// truncated/garbage `from` length prefix.
// [impl->REQ-MSG-ENVELOPE]
pub fn decode_frame(payload: &[u8]) -> io::Result<(String, String)> {
    if payload.len() < 4 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "frame shorter than the 4-byte from-length prefix",
        ));
    }
    let from_len = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
    if 4 + from_len > payload.len() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "from-length prefix overruns the frame",
        ));
    }
    let from = String::from_utf8(payload[4..4 + from_len].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    let body = String::from_utf8(payload[4 + from_len..].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok((from, body))
}

/// Write a length-prefixed `(from, body)` message to a TCP stream.
/// Format: 4-byte big-endian outer length prefix, then the structural payload.
pub fn write_message(stream: &mut TcpStream, from_id: &str, body: &str) -> io::Result<()> {
    let payload = encode_frame(from_id, body);
    let len = payload.len() as u32;
    stream.write_all(&len.to_be_bytes())?;
    stream.write_all(&payload)?;
    stream.flush()?;
    Ok(())
}

/// Read a length-prefixed message from a TCP stream, returning `(from, body)`.
/// Reads the 4-byte big-endian outer length, then that many bytes, then splits
/// the structural payload. Rejects an oversized length prefix before allocating.
pub fn read_message(stream: &mut TcpStream) -> io::Result<(String, String)> {
    let mut len_buf = [0u8; 4];
    stream.read_exact(&mut len_buf)?;
    let len = u32::from_be_bytes(len_buf) as usize;

    if len > MAX_MESSAGE_BYTES {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("Message too large: {} bytes", len),
        ));
    }

    let mut buf = vec![0u8; len];
    stream.read_exact(&mut buf)?;
    decode_frame(&buf)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{TcpListener, TcpStream};

    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —
    // no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).
    #[test]
    fn encode_decode_round_trips_structural() {
        let payload = encode_frame("alice", "hi\nbob");
        assert_eq!(decode_frame(&payload).unwrap(), ("alice".to_string(), "hi\nbob".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] empty from => anonymous (len-0 from prefix), body intact.
    #[test]
    fn encode_empty_from_is_anonymous() {
        let payload = encode_frame("", "no reply");
        assert_eq!(decode_frame(&payload).unwrap(), ("".to_string(), "no reply".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] a typed <EVENT> body rides the wire verbatim — the
    // structural frame never reparses or rewraps it.
    #[test]
    fn typed_event_body_rides_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer">done</EVENT>"#;
        let payload = encode_frame("issuer", typed);
        assert_eq!(decode_frame(&payload).unwrap().1, typed);
    }

    // [unit->REQ-MSG-ENVELOPE] a truncated from-length prefix is rejected, not panicked.
    #[test]
    fn decode_rejects_overrunning_from_len() {
        // Claim from_len = 100 on a 6-byte payload.
        let mut bad = (100u32).to_be_bytes().to_vec();
        bad.extend_from_slice(b"ab");
        assert_eq!(
            decode_frame(&bad).unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
    }

    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).
    #[test]
    fn frame_round_trips_over_loopback() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server).unwrap()
        });

        let mut client = TcpStream::connect(addr).unwrap();
        write_message(&mut client, "alice", "hello\nworld").unwrap();
        drop(client);

        let got = handle.join().unwrap();
        assert_eq!(got, ("alice".to_string(), "hello\nworld".to_string()));
    }

    // [unit->REQ-MSG-1] an oversized length prefix is rejected, not allocated.
    #[test]
    fn oversized_length_prefix_rejected() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server)
        });

        let mut client = TcpStream::connect(addr).unwrap();
        // Claim 32 MiB (> MAX_MESSAGE_BYTES) then send nothing more.
        let bogus_len = (32u32 * 1024 * 1024).to_be_bytes();
        client.write_all(&bogus_len).unwrap();
        client.flush().unwrap();
        drop(client);

        let result = handle.join().unwrap();
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
    }
}

-/// `[u32 BE len(from)][from][body]`. No `__REPLY_TO__`, no newline framing —

-/// `from` and `body` are carried as raw byte ranges, so a body that is itself a

+/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline

+/// framing — `from` and `body` are carried as raw byte ranges, so a body that is


22
23	/// Encode `(from, body)` into the structural wire payload:
24	/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline
25	/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
26	/// pre-formed `<EVENT>` envelope rides verbatim.
27	// [impl->REQ-MSG-ENVELOPE]

/// Encode `(from, body)` into the structural wire payload:
/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline
/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
/// pre-formed `<EVENT>` envelope rides verbatim.
// [impl->REQ-MSG-ENVELOPE]


/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
/// pre-formed `<EVENT>` envelope rides verbatim.

/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
/// itself a pre-formed `<EVENT>` envelope rides verbatim.


/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
/// itself a pre-formed `<EVENT>` envelope rides verbatim.

//! TCP length-prefixed message framing.
//!
//! The frame carries `(from, body)` **structurally** — there is no
//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in
//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).
//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`.
//!
//! Frame: a 4-byte big-endian outer length prefix, then that many payload
//! bytes. Payload: `[u32 BE len(from)][from bytes][body bytes]` — the receiver
//! splits it straight back into `(from, body)`. An empty `from` (len 0) is an
//! anonymous delivery (commune / sign-off — the recipient must not reply).
// [impl->REQ-MSG-1]
// [impl->REQ-MSG-ENVELOPE]

use std::io::{self, Read, Write};
use std::net::TcpStream;

/// Maximum framed message size accepted off the wire (16 MiB). Guards a
/// malicious/garbage length prefix from triggering a huge allocation.
const MAX_MESSAGE_BYTES: usize = 16 * 1024 * 1024;

/// Encode `(from, body)` into the structural wire payload:
/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline
/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
/// pre-formed `<EVENT>` envelope rides verbatim.
// [impl->REQ-MSG-ENVELOPE]
pub fn encode_frame(from_id: &str, body: &str) -> Vec<u8> {
    let from_bytes = from_id.as_bytes();
    let body_bytes = body.as_bytes();
    let mut payload = Vec::with_capacity(4 + from_bytes.len() + body_bytes.len());
    payload.extend_from_slice(&(from_bytes.len() as u32).to_be_bytes());
    payload.extend_from_slice(from_bytes);
    payload.extend_from_slice(body_bytes);
    payload
}

/// Decode the structural wire payload back into `(from, body)`. Rejects a
/// truncated/garbage `from` length prefix.
// [impl->REQ-MSG-ENVELOPE]
pub fn decode_frame(payload: &[u8]) -> io::Result<(String, String)> {
    if payload.len() < 4 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "frame shorter than the 4-byte from-length prefix",
        ));
    }
    let from_len = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
    if 4 + from_len > payload.len() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "from-length prefix overruns the frame",
        ));
    }
    let from = String::from_utf8(payload[4..4 + from_len].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    let body = String::from_utf8(payload[4 + from_len..].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok((from, body))
}

/// Write a length-prefixed `(from, body)` message to a TCP stream.
/// Format: 4-byte big-endian outer length prefix, then the structural payload.
pub fn write_message(stream: &mut TcpStream, from_id: &str, body: &str) -> io::Result<()> {
    let payload = encode_frame(from_id, body);
    let len = payload.len() as u32;
    stream.write_all(&len.to_be_bytes())?;
    stream.write_all(&payload)?;
    stream.flush()?;
    Ok(())
}

/// Read a length-prefixed message from a TCP stream, returning `(from, body)`.
/// Reads the 4-byte big-endian outer length, then that many bytes, then splits
/// the structural payload. Rejects an oversized length prefix before allocating.
pub fn read_message(stream: &mut TcpStream) -> io::Result<(String, String)> {
    let mut len_buf = [0u8; 4];
    stream.read_exact(&mut len_buf)?;
    let len = u32::from_be_bytes(len_buf) as usize;

    if len > MAX_MESSAGE_BYTES {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("Message too large: {} bytes", len),
        ));
    }

    let mut buf = vec![0u8; len];
    stream.read_exact(&mut buf)?;
    decode_frame(&buf)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{TcpListener, TcpStream};

    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —
    // no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).
    #[test]
    fn encode_decode_round_trips_structural() {
        let payload = encode_frame("alice", "hi\nbob");
        assert_eq!(decode_frame(&payload).unwrap(), ("alice".to_string(), "hi\nbob".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] empty from => anonymous (len-0 from prefix), body intact.
    #[test]
    fn encode_empty_from_is_anonymous() {
        let payload = encode_frame("", "no reply");
        assert_eq!(decode_frame(&payload).unwrap(), ("".to_string(), "no reply".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] a typed <EVENT> body rides the wire verbatim — the
    // structural frame never reparses or rewraps it.
    #[test]
    fn typed_event_body_rides_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer">done</EVENT>"#;
        let payload = encode_frame("issuer", typed);
        assert_eq!(decode_frame(&payload).unwrap().1, typed);
    }

    // [unit->REQ-MSG-ENVELOPE] a truncated from-length prefix is rejected, not panicked.
    #[test]
    fn decode_rejects_overrunning_from_len() {
        // Claim from_len = 100 on a 6-byte payload.
        let mut bad = (100u32).to_be_bytes().to_vec();
        bad.extend_from_slice(b"ab");
        assert_eq!(
            decode_frame(&bad).unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
    }

    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).
    #[test]
    fn frame_round_trips_over_loopback() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server).unwrap()
        });

        let mut client = TcpStream::connect(addr).unwrap();
        write_message(&mut client, "alice", "hello\nworld").unwrap();
        drop(client);

        let got = handle.join().unwrap();
        assert_eq!(got, ("alice".to_string(), "hello\nworld".to_string()));
    }

    // [unit->REQ-MSG-1] an oversized length prefix is rejected, not allocated.
    #[test]
    fn oversized_length_prefix_rejected() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server)
        });

        let mut client = TcpStream::connect(addr).unwrap();
        // Claim 32 MiB (> MAX_MESSAGE_BYTES) then send nothing more.
        let bogus_len = (32u32 * 1024 * 1024).to_be_bytes();
        client.write_all(&bogus_len).unwrap();
        client.flush().unwrap();
        drop(client);

        let result = handle.join().unwrap();
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
    }
}

-/// pre-formed `<EVENT>` envelope rides verbatim.

+/// itself a pre-formed `<EVENT>` envelope rides verbatim.


// no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).

// no legacy reply-to string, body byte-ranges preserved (incl interior newlines).


//! TCP length-prefixed message framing.
//!
//! The frame carries `(from, body)` **structurally** — there is no
//! legacy reply-to serialization (ADR-0020: that string was a mis-elevated
//! relic; the only stable, copy-verbatim format is the `<EVENT>` envelope in
//! [`spt_proto::event`], composed once at the delivery edge, never on the wire).
//! spt-core mandates **no** byte-for-byte TCP interop with any sister `spt`.
//!
//! Frame: a 4-byte big-endian outer length prefix, then that many payload
//! bytes. Payload: `[u32 BE len(from)][from bytes][body bytes]` — the receiver
//! splits it straight back into `(from, body)`. An empty `from` (len 0) is an
//! anonymous delivery (commune / sign-off — the recipient must not reply).
// [impl->REQ-MSG-1]
// [impl->REQ-MSG-ENVELOPE]

use std::io::{self, Read, Write};
use std::net::TcpStream;

/// Maximum framed message size accepted off the wire (16 MiB). Guards a
/// malicious/garbage length prefix from triggering a huge allocation.
const MAX_MESSAGE_BYTES: usize = 16 * 1024 * 1024;

/// Encode `(from, body)` into the structural wire payload:
/// `[u32 BE len(from)][from][body]`. No legacy reply-to header, no newline
/// framing — `from` and `body` are carried as raw byte ranges, so a body that is
/// itself a pre-formed `<EVENT>` envelope rides verbatim.
// [impl->REQ-MSG-ENVELOPE]
pub fn encode_frame(from_id: &str, body: &str) -> Vec<u8> {
    let from_bytes = from_id.as_bytes();
    let body_bytes = body.as_bytes();
    let mut payload = Vec::with_capacity(4 + from_bytes.len() + body_bytes.len());
    payload.extend_from_slice(&(from_bytes.len() as u32).to_be_bytes());
    payload.extend_from_slice(from_bytes);
    payload.extend_from_slice(body_bytes);
    payload
}

/// Decode the structural wire payload back into `(from, body)`. Rejects a
/// truncated/garbage `from` length prefix.
// [impl->REQ-MSG-ENVELOPE]
pub fn decode_frame(payload: &[u8]) -> io::Result<(String, String)> {
    if payload.len() < 4 {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "frame shorter than the 4-byte from-length prefix",
        ));
    }
    let from_len = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
    if 4 + from_len > payload.len() {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            "from-length prefix overruns the frame",
        ));
    }
    let from = String::from_utf8(payload[4..4 + from_len].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    let body = String::from_utf8(payload[4 + from_len..].to_vec())
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
    Ok((from, body))
}

/// Write a length-prefixed `(from, body)` message to a TCP stream.
/// Format: 4-byte big-endian outer length prefix, then the structural payload.
pub fn write_message(stream: &mut TcpStream, from_id: &str, body: &str) -> io::Result<()> {
    let payload = encode_frame(from_id, body);
    let len = payload.len() as u32;
    stream.write_all(&len.to_be_bytes())?;
    stream.write_all(&payload)?;
    stream.flush()?;
    Ok(())
}

/// Read a length-prefixed message from a TCP stream, returning `(from, body)`.
/// Reads the 4-byte big-endian outer length, then that many bytes, then splits
/// the structural payload. Rejects an oversized length prefix before allocating.
pub fn read_message(stream: &mut TcpStream) -> io::Result<(String, String)> {
    let mut len_buf = [0u8; 4];
    stream.read_exact(&mut len_buf)?;
    let len = u32::from_be_bytes(len_buf) as usize;

    if len > MAX_MESSAGE_BYTES {
        return Err(io::Error::new(
            io::ErrorKind::InvalidData,
            format!("Message too large: {} bytes", len),
        ));
    }

    let mut buf = vec![0u8; len];
    stream.read_exact(&mut buf)?;
    decode_frame(&buf)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::{TcpListener, TcpStream};

    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —
    // no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).
    #[test]
    fn encode_decode_round_trips_structural() {
        let payload = encode_frame("alice", "hi\nbob");
        assert_eq!(decode_frame(&payload).unwrap(), ("alice".to_string(), "hi\nbob".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] empty from => anonymous (len-0 from prefix), body intact.
    #[test]
    fn encode_empty_from_is_anonymous() {
        let payload = encode_frame("", "no reply");
        assert_eq!(decode_frame(&payload).unwrap(), ("".to_string(), "no reply".to_string()));
    }

    // [unit->REQ-MSG-ENVELOPE] a typed <EVENT> body rides the wire verbatim — the
    // structural frame never reparses or rewraps it.
    #[test]
    fn typed_event_body_rides_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer">done</EVENT>"#;
        let payload = encode_frame("issuer", typed);
        assert_eq!(decode_frame(&payload).unwrap().1, typed);
    }

    // [unit->REQ-MSG-ENVELOPE] a truncated from-length prefix is rejected, not panicked.
    #[test]
    fn decode_rejects_overrunning_from_len() {
        // Claim from_len = 100 on a 6-byte payload.
        let mut bad = (100u32).to_be_bytes().to_vec();
        bad.extend_from_slice(b"ab");
        assert_eq!(
            decode_frame(&bad).unwrap_err().kind(),
            io::ErrorKind::InvalidData
        );
    }

    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).
    #[test]
    fn frame_round_trips_over_loopback() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server).unwrap()
        });

        let mut client = TcpStream::connect(addr).unwrap();
        write_message(&mut client, "alice", "hello\nworld").unwrap();
        drop(client);

        let got = handle.join().unwrap();
        assert_eq!(got, ("alice".to_string(), "hello\nworld".to_string()));
    }

    // [unit->REQ-MSG-1] an oversized length prefix is rejected, not allocated.
    #[test]
    fn oversized_length_prefix_rejected() {
        let listener = TcpListener::bind("127.0.0.1:0").unwrap();
        let addr = listener.local_addr().unwrap();

        let handle = std::thread::spawn(move || {
            let (mut server, _) = listener.accept().unwrap();
            read_message(&mut server)
        });

        let mut client = TcpStream::connect(addr).unwrap();
        // Claim 32 MiB (> MAX_MESSAGE_BYTES) then send nothing more.
        let bogus_len = (32u32 * 1024 * 1024).to_be_bytes();
        client.write_all(&bogus_len).unwrap();
        client.flush().unwrap();
        drop(client);

        let result = handle.join().unwrap();
        assert!(result.is_err());
        assert_eq!(result.unwrap_err().kind(), io::ErrorKind::InvalidData);
    }
}

-    // no __REPLY_TO__ string, body byte-ranges preserved (incl interior newlines).

+    // no legacy reply-to string, body byte-ranges preserved (incl interior newlines).


//! mis-elevated `__REPLY_TO__` framing). The delivery routing, listener

//! mis-elevated legacy reply-to framing). The delivery routing, listener


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-msg\src\lib.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//! `spt-msg` — the local-messaging layer for spt-core.
//!
//! Third layer of the acyclically-layered crate stack (R-ARCH-1): depends only
//! on `spt-proto` (wire) and `spt-store` (persistence), one layer down — never
//! the reverse. The `spt` binary is the top consumer.
//!
//! Per ADR-0001 the only **stable, copy-verbatim wire format** is the `<EVENT>`
//! envelope grammar in `spt-proto::event` (composed at the delivery edge by
//! `emit`); the `wire` TCP frame is an **internal** length-prefixed
//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the
//! mis-elevated `__REPLY_TO__` framing). The delivery routing, listener
//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against
//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).
//!
//! M1 is **loopback-only**: every address is `127.0.0.1:<ephemeral>`. Cross-node
//! delivery is M4; the consolidated daemon that owns all listeners is M3. M1
//! ships the messaging *mechanism* on the proven M0 substrate.
// [impl->REQ-ARCH-1]

// Modules land one per M1 task (T1 wire+listener, T2 deliver, T3 ready, T4 ring).
pub mod deliver;
pub mod emit;
pub mod listener;
pub mod ready;
pub mod ring;
pub mod wire;

/// Test-only home isolation. The listener/ready/ring surfaces resolve the
/// owlery through `spt_store::perch::owlery_dir()`, which honors `$SPT_HOME`.
/// Without an override the default home is `%LOCALAPPDATA%\spt-core` — distinct
/// from the modern spt runtime's `spt/` home, but still a real shared dir that
/// must not collect test debris. Every unit test that touches a perch MUST call
/// [`test_home::isolate`] first so it operates in a throwaway temp dir, never
/// the real one.
#[cfg(test)]
pub(crate) mod test_home {
    use std::sync::OnceLock;
    use tempfile::TempDir;

    static HOME: OnceLock<TempDir> = OnceLock::new();

    /// Point `$SPT_HOME` at an isolated temp dir for the whole test process.
    /// Idempotent: the first caller wins, every later caller blocks until the
    /// env is set, so no test performs a perch op against the default home.
    pub(crate) fn isolate() {
        HOME.get_or_init(|| {
            let dir = TempDir::new().expect("temp SPT_HOME");
            std::env::set_var("SPT_HOME", dir.path());
            dir
        });
    }
}

-//! mis-elevated `__REPLY_TO__` framing). The delivery routing, listener

+//! mis-elevated legacy reply-to framing). The delivery routing, listener


//! no `__REPLY_TO__` parse step (that relic, and the serialize-then-reparse

//! no legacy reply-to parse step (that relic, and the serialize-then-reparse


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-msg\src\emit.rs has been updated successfully. (file state is current in your context — no need to Read it back)

//! Delivery-edge EVENT rendering: the structural `(from, body)` pair → the
//! `<EVENT …>` envelope every arriving-message surface emits (ADR-0020).
//!
//! The envelope **format** is the ADR-0001 copy-verbatim grammar housed in
//! [`spt_proto::event`]; this module composes it **once, at the delivery edge**
//! from the structural `(from, body)` carried through the wire/spool — there is
//! no `__REPLY_TO__` parse step (that relic, and the serialize-then-reparse
//! round-trip it forced, are gone).
//!
//! Two edges, same grammar, one framing nuance:
//! - [`render_event_lines`] — the **listener stream** (`spt ready` /
//!   `spt api listen`, the `« spt event »` Monitor loops): composes **and**
//!   chunks oversized lines into `<EVENT-PART …>` groups so the Monitor
//!   notification cap holds.
//! - [`render_event_whole`] — the **hook drains** (`spt api poll`,
//!   `api worker-poll`): composes **one whole** `<EVENT>` per message, **never**
//!   chunked (the adapter's own context channel has no per-line cap; any
//!   injection sizing is the adapter's job — the harness-agnostic boundary).
// [impl->REQ-MSG-4]
// [impl->REQ-MSG-ENVELOPE]

use spt_proto::chunk::{chunk_if_oversized, EVENT_LINE_THRESHOLD};
use spt_proto::event::{compose_msg_event, is_typed_event_envelope};

/// Compose the canonical `<EVENT>` line for a structural `(from, body)`:
///
/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>` envelope
///    (echo_commune, notify, user-msg, …) passes through **verbatim** — never
///    re-wrapped, and the structural `from` is ignored in favor of the body's
///    own `from` (the echo-commune-double-envelope fix);
/// 2. anything else composes `<EVENT type="msg" from="{from}">{body}</EVENT>`
///    (attr/body escaped; anonymous delivery renders `from=""`).
// [impl->REQ-MSG-ENVELOPE]
fn compose_line(from: &str, body: &str) -> (String, bool) {
    if is_typed_event_envelope(body) {
        (body.to_string(), true)
    } else {
        (compose_msg_event(from, body), false)
    }
}

/// Render a delivered `(from, body)` into the listener-stream stdout line(s):
/// compose the `<EVENT>` (typed bodies pass through verbatim), then chunk an
/// oversized line into `<EVENT-PART …>` lines past [`EVENT_LINE_THRESHOLD`] so
/// the harness notification cap holds.
// [impl->REQ-MSG-4]
// [impl->REQ-MSG-ENVELOPE]
pub fn render_event_lines(from: &str, body: &str) -> Vec<String> {
    let (line, _typed) = compose_line(from, body);
    chunk_if_oversized(&line, EVENT_LINE_THRESHOLD)
}

/// Render a delivered `(from, body)` into **one whole** `<EVENT>` line for the
/// hook drains — composed identically to [`render_event_lines`] (typed bodies
/// pass through verbatim) but **never chunked**, so a hook consumer needs no
/// `id+seq` reassembly. Self-delimiting by construction (one whole envelope per
/// message — finding F-002 dissolves).
// [impl->REQ-MSG-ENVELOPE]
pub fn render_event_whole(from: &str, body: &str) -> String {
    compose_line(from, body).0
}

#[cfg(test)]
mod tests {
    use super::*;
    use spt_proto::event::parse_event;

    // [unit->REQ-MSG-4] a structural (from, body) renders the msg envelope:
    // from attribute + body, exactly one line.
    #[test]
    fn structural_renders_msg_envelope() {
        let lines = render_event_lines("lea", "test message, hi!");
        assert_eq!(
            lines,
            vec![r#"<EVENT type="msg" from="lea">test message, hi!</EVENT>"#.to_string()]
        );
    }

    // [unit->REQ-MSG-4] anonymous (from="") still gets the envelope — from="" —
    // never a bare body on the stream.
    #[test]
    fn anonymous_still_enveloped() {
        let lines = render_event_lines("", "no header here");
        assert_eq!(
            lines,
            vec![r#"<EVENT type="msg" from="">no header here</EVENT>"#.to_string()]
        );
    }

    // [unit->REQ-MSG-4] multi-line bodies escape (`\n` → `<br>`) and round-trip
    // through the receiver-side parser.
    #[test]
    fn multiline_body_escapes_and_round_trips() {
        let lines = render_event_lines("bob", "line one\nline two");
        assert_eq!(lines.len(), 1);
        assert!(lines[0].contains("line one<br>line two"));
        let p = parse_event(&lines[0]).unwrap();
        assert_eq!(p.from(), Some("bob"));
        assert_eq!(p.body, "line one\nline two");
    }

    // [unit->REQ-MSG-ENVELOPE] ADVERSARIAL typed-passthrough: a typed notify
    // body whose OWN from differs from the structural from rides through
    // verbatim — the BODY's from wins, exactly ONE envelope, no double-wrap, the
    // structural from ignored. The subtle no-double-wrap invariant doyle gates.
    #[test]
    fn typed_body_from_wins_over_structural_from_no_double_wrap() {
        let typed =
            r#"<EVENT type="notify" from="bodyfrom" notif_id="cafe:1">build done</EVENT>"#;
        // Structural from deliberately DIFFERS from the body's own from.
        let lines = render_event_lines("structfrom", typed);
        assert_eq!(lines, vec![typed.to_string()], "verbatim, single envelope");
        let whole = render_event_whole("structfrom", typed);
        assert_eq!(whole, typed, "whole-render passes the same typed body verbatim");
        // Parses as ONE notify with the body's from — structfrom nowhere.
        let p = parse_event(&lines[0]).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("notify"));
        assert_eq!(p.from(), Some("bodyfrom"));
        assert!(!lines[0].contains("structfrom"), "structural from must not leak in");
    }

    // [unit->REQ-MSG-5] the delivery surface RENDERS the user-msg type: a
    // gated user-msg envelope body reaches both edges verbatim — the receiving
    // agent sees `<EVENT type="user-msg">`, never a re-wrapped plain msg.
    #[test]
    fn user_msg_envelope_renders_verbatim_on_both_edges() {
        let um = spt_proto::event::compose_user_msg_event("playdate-gw", "ship it");
        let lines = render_event_lines("playdate-gw", &um);
        assert_eq!(lines, vec![um.clone()]);
        assert_eq!(render_event_whole("playdate-gw", &um), um);
        let p = parse_event(&lines[0]).unwrap();
        assert_eq!(p.event_type.as_deref(), Some("user-msg"));
        assert_eq!(p.from(), Some("playdate-gw"));
        assert_eq!(p.body, "ship it");
    }

    // [unit->REQ-MSG-ENVELOPE] the hook-drain edge composes a WHOLE plain-msg
    // <EVENT> — never an EVENT-PART, even for an oversized body the listener
    // would chunk. Self-delimiting by construction.
    #[test]
    fn whole_render_never_chunks() {
        let body = "x".repeat(1500);
        let whole = render_event_whole("doyle", &body);
        assert!(whole.starts_with("<EVENT "), "one whole envelope");
        assert!(!whole.contains("<EVENT-PART"), "hook drain never chunks");
        let p = parse_event(&whole).unwrap();
        assert_eq!(p.from(), Some("doyle"));
        assert_eq!(p.body, body);
    }

    // [unit->REQ-MSG-4] an oversized listener line chunks into EVENT-PART lines
    // that reassemble byte-exact (listener stream unchanged — still chunks).
    #[test]
    fn oversized_listener_line_chunks_into_event_parts() {
        let body = "x".repeat(1500);
        let lines = render_event_lines("doyle", &body);
        assert!(lines.len() > 1, "1500-char body must chunk");
        assert!(lines.iter().all(|l| l.starts_with("<EVENT-PART ")));
        let whole = spt_proto::chunk::reassemble_group(&lines).expect("reassembles");
        let p = parse_event(&whole).unwrap();
        assert_eq!(p.from(), Some("doyle"));
        assert_eq!(p.body, body);
    }
}

-//! no `__REPLY_TO__` parse step (that relic, and the serialize-then-reparse

+//! no legacy reply-to parse step (that relic, and the serialize-then-reparse


/// `__REPLY_TO__` frame (the relic round-trip is gone).

/// legacy reply-to frame (the relic round-trip is gone).


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-store\src\spool.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// A drained spool row carried **structurally** as `(from, body)` (ADR-0020).

/// `from` is `""` for an anonymous sender. The `<EVENT>` envelope is composed

/// once at the delivery edge from this pair — the spool no longer serializes a

-/// `__REPLY_TO__` frame (the relic round-trip is gone).

+/// legacy reply-to frame (the relic round-trip is gone).


// <EVENT notify> envelope verbatim — NO __REPLY_TO__ wrapper, NO

// <EVENT notify> envelope verbatim — NO legacy reply-to wrapper, NO


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\notif.rs has been updated successfully. (file state is current in your context — no need to Read it back)

let rows = spool::drain_all_at(&ling).unwrap();

// Structural carriage (ADR-0020): from="issuer" + body = the typed

-            // <EVENT notify> envelope verbatim — NO __REPLY_TO__ wrapper, NO

+            // <EVENT notify> envelope verbatim — NO legacy reply-to wrapper, NO

// double-prefix. The delivery edge passes the typed body through.

"<EVENT type=\"notify\" from=\"issuer\" \


// notify <EVENT> verbatim (no __REPLY_TO__ wrapper, no double-prefix).

// notify <EVENT> verbatim (no legacy reply-to wrapper, no double-prefix).


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\psyrelay.rs has been updated successfully. (file state is current in your context — no need to Read it back)

let rows = spool::drain_all_at(&doyle).unwrap();

// Structural carriage: from = re-stamped psyche id, body = the typed

-            // notify <EVENT> verbatim (no __REPLY_TO__ wrapper, no double-prefix).

+            // notify <EVENT> verbatim (no legacy reply-to wrapper, no double-prefix).


Now spt-proto event.rs:48 + cli.rs:65 comments.


46	/// An echo-commune brief (Psyche → Self FYI).
47	pub const EVENT_TYPE_ECHO_COMMUNE: &str = "echo_commune";
48	/// A Psyche-authored reply intent — routed by the daemon to the `__REPLY_TO__`
49	/// sender of the inbound being answered, and nowhere else (ADR-0012).
50	pub const EVENT_TYPE_REPLY: &str = "reply";
51	/// A Psyche-authored notify intent — routed by the daemon to the agent's own

/// An echo-commune brief (Psyche → Self FYI).
pub const EVENT_TYPE_ECHO_COMMUNE: &str = "echo_commune";
/// A Psyche-authored reply intent — routed by the daemon to the `__REPLY_TO__`
/// sender of the inbound being answered, and nowhere else (ADR-0012).
pub const EVENT_TYPE_REPLY: &str = "reply";
/// A Psyche-authored notify intent — routed by the daemon to the agent's own


63	        /// Target perch id. May be omitted when --reply-to is given.
64	        target: Option<String>,
65	        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
66	        #[arg(long)]
67	        from: Option<String>,

        /// Target perch id. May be omitted when --reply-to is given.
        target: Option<String>,
        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,


/// A Psyche-authored reply intent — routed by the daemon to the `__REPLY_TO__`
/// sender of the inbound being answered, and nowhere else (ADR-0012).

/// A Psyche-authored reply intent — routed by the daemon to the `from` of the
/// inbound being answered, and nowhere else (ADR-0012).


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-proto\src\event.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// A Psyche-authored reply intent — routed by the daemon to the `__REPLY_TO__`
/// sender of the inbound being answered, and nowhere else (ADR-0012).

/// A Psyche-authored reply intent — routed by the daemon to the `from` of the
/// inbound being answered, and nowhere else (ADR-0012).

pub const EVENT_TYPE_FILE_DROP: &str = "file_drop";

/// An echo-commune brief (Psyche → Self FYI).

pub const EVENT_TYPE_ECHO_COMMUNE: &str = "echo_commune";

-/// A Psyche-authored reply intent — routed by the daemon to the `__REPLY_TO__`

-/// sender of the inbound being answered, and nowhere else (ADR-0012).

+/// A Psyche-authored reply intent — routed by the daemon to the `from` of the

+/// inbound being answered, and nowhere else (ADR-0012).

pub const EVENT_TYPE_REPLY: &str = "reply";

/// A Psyche-authored notify intent — routed by the daemon to the agent's own

/// user/subnet only (ADR-0012; D8's Psyche-producer path).


        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,

        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,

        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).
        #[arg(long)]
        from: Option<String>,

/// Target perch id. May be omitted when --reply-to is given.

-        /// Sender id written into __REPLY_TO__ (auto-detected from session if omitted).

+        /// Sender id carried structurally as the message `from` (auto-detected from session if omitted).

/// Reply to this sender; sets the target and labels output REPLIED.


Now the killer_quickstart.rs integration test — reads the live wire/poll output. Read it.


1	//! **The M1 killer quickstart (local):** two ready agents exchange a message
2	//! end-to-end through the real crate boundaries — the live TCP path *and* the
3	//! offline spool path — on one box.
4	//!
5	//! This is the `int`-stage proof for REQ-MSG-1 (delivery + routing + reply) and
6	//! REQ-MSG-3 (ready lifecycle + backlog drain). It is cross-perch and
7	//! multi-listener: three distinct perches (A, B, C) over a shared owlery, two
8	//! of them holding live listeners simultaneously, with a spool hop in between.
9	// [int->REQ-MSG-1]
10	// [int->REQ-MSG-3]
11
12	use std::sync::OnceLock;
13
14	use spt_msg::deliver::{self, DeliveryPath};
15	use spt_msg::ready::ReadyAgent;
16	use spt_store::info::{self, InfoJson};
17	use spt_store::perch::{self, ParentHint};
18	use spt_store::spool;
19	use tempfile::TempDir;
20
21	/// Hold the temp owlery for the whole test binary so it isn't deleted mid-run.
22	static HOME: OnceLock<TempDir> = OnceLock::new();
23
24	/// Point SPT_HOME at an isolated temp dir (once per test process).
25	fn init_home() {
26	    HOME.get_or_init(|| {
27	        let dir = TempDir::new().expect("temp dir");
28	        std::env::set_var("SPT_HOME", dir.path());
29	        dir
30	    });
31	}
32
33	fn unique(prefix: &str) -> String {
34	    let nanos = std::time::SystemTime::now()
35	        .duration_since(std::time::UNIX_EPOCH)
36	        .map(|d| d.as_nanos())
37	        .unwrap_or(0);
38	    format!("{}-{}-{}", prefix, std::process::id(), nanos)
39	}
40
41	/// Receive one message within a bounded number of poll windows, or fail.
42	fn recv_within(agent: &ReadyAgent, windows: usize) -> String {
43	    for _ in 0..windows {
44	        if let Some(msg) = agent.recv() {
45	            return msg;
46	        }
47	        if let Some(msg) = agent.drain_spool().into_iter().next() {
48	            return msg;
49	        }
50	    }
51	    panic!("no message received within {} windows", windows);
52	}
53
54	/// Split a framed message into (reply_to, body).
55	fn parse_frame(frame: &str) -> (Option<String>, String) {
56	    if let Some(rest) = frame.strip_prefix("__REPLY_TO__:") {
57	        let mut lines = rest.splitn(2, '\n');
58	        let from = lines.next().unwrap_or("").to_string();
59	        let body = lines
60	            .next()
61	            .unwrap_or("")
62	            .trim_end_matches('\n')
63	            .to_string();
64	        (Some(from), body)
65	    } else {
66	        (None, frame.trim_end_matches('\n').to_string())
67	    }
68	}
69
70	// [int->REQ-MSG-1] [int->REQ-MSG-3]
71	#[test]
72	fn two_agents_exchange_message_tcp_and_spool() {
73	    init_home();
74	    let owlery = perch::owlery_dir();
75
76	    let a = unique("alice");
77	    let b = unique("bob");
78	    let c = unique("carol");
79
80	    // --- Live TCP path: A and B both listening; A messages B. ---
81	    let (agent_a, _) = ReadyAgent::start(&a).unwrap();
82	    let (agent_b, _) = ReadyAgent::start(&b).unwrap();
83
84	    assert_eq!(
85	        deliver::deliver(&b, &a, "hello bob", &owlery),
86	        DeliveryPath::Tcp
87	    );
88	    let got = recv_within(&agent_b, 25);
89	    let (from, body) = parse_frame(&got);
90	    assert_eq!(from.as_deref(), Some(a.as_str()), "reply-to routing");
91	    assert_eq!(body, "hello bob");
92
93	    // --- Reply routing: B replies to A using the recovered reply-to. ---
94	    assert_eq!(
95	        deliver::deliver(&a, &b, "hi alice", &owlery),
96	        DeliveryPath::Tcp
97	    );
98	    let reply = recv_within(&agent_a, 25);
99	    let (rfrom, rbody) = parse_frame(&reply);
100	    assert_eq!(rfrom.as_deref(), Some(b.as_str()));
101	    assert_eq!(rbody, "hi alice");
102
103	    // --- Offline spool path: C exists but isn't listening; B messages C. ---
104	    let c_perch = perch::resolve_perch_path(&c, ParentHint::Infer);
105	    std::fs::create_dir_all(&c_perch).unwrap();
106	    info::write_info(
107	        &c_perch,
108	        &InfoJson::new(&c, "now", std::process::id(), "sess", "ready_agent"),
109	    )
110	    .unwrap();
111
112	    assert_eq!(
113	        deliver::deliver(&c, &b, "for carol later", &owlery),
114	        DeliveryPath::Spool
115	    );
116	    assert_eq!(spool::pending_count_at(&c_perch).unwrap(), 1);
117
118	    // --- C starts and drains the backlog on startup. ---
119	    let (_agent_c, backlog) = ReadyAgent::start(&c).unwrap();
120	    assert_eq!(backlog.len(), 1, "C drains the spooled message on startup");
121	    let (cfrom, cbody) = parse_frame(&backlog[0]);
122	    assert_eq!(cfrom.as_deref(), Some(b.as_str()));
123	    assert_eq!(cbody, "for carol later");
124
125	    drop(agent_a);
126	    drop(agent_b);
127	}
128
129	// [int->REQ-MSG-1] a message spooled while the target is down is delivered the
130	// moment the target comes up — the offline->online handoff in isolation.
131	#[test]
132	fn offline_then_online_delivers_backlog() {
133	    init_home();
134	    let owlery = perch::owlery_dir();
135	    let target = unique("sleeper");
136
137	    // Perch exists, no listener: delivery spools.
138	    let perch_path = perch::resolve_perch_path(&target, ParentHint::Infer);
139	    std::fs::create_dir_all(&perch_path).unwrap();
140	    info::write_info(
141	        &perch_path,
142	        &InfoJson::new(&target, "now", std::process::id(), "sess", "ready_agent"),
143	    )
144	    .unwrap();
145
146	    for i in 0..3 {
147	        assert_eq!(
148	            deliver::deliver(&target, "sender", &format!("msg {i}"), &owlery),
149	            DeliveryPath::Spool
150	        );
151	    }
152
153	    // Coming up drains all three, oldest first.
154	    let (_agent, backlog) = ReadyAgent::start(&target).unwrap();
155	    assert_eq!(backlog.len(), 3);
156	    let bodies: Vec<String> = backlog.iter().map(|f| parse_frame(f).1).collect();
157	    assert_eq!(bodies, vec!["msg 0", "msg 1", "msg 2"]);
158	}
159

//! **The M1 killer quickstart (local):** two ready agents exchange a message
//! end-to-end through the real crate boundaries — the live TCP path *and* the
//! offline spool path — on one box.
//!
//! This is the `int`-stage proof for REQ-MSG-1 (delivery + routing + reply) and
//! REQ-MSG-3 (ready lifecycle + backlog drain). It is cross-perch and
//! multi-listener: three distinct perches (A, B, C) over a shared owlery, two
//! of them holding live listeners simultaneously, with a spool hop in between.
// [int->REQ-MSG-1]
// [int->REQ-MSG-3]

use std::sync::OnceLock;

use spt_msg::deliver::{self, DeliveryPath};
use spt_msg::ready::ReadyAgent;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use tempfile::TempDir;

/// Hold the temp owlery for the whole test binary so it isn't deleted mid-run.
static HOME: OnceLock<TempDir> = OnceLock::new();

/// Point SPT_HOME at an isolated temp dir (once per test process).
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp dir");
        std::env::set_var("SPT_HOME", dir.path());
        dir
    });
}

fn unique(prefix: &str) -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("{}-{}-{}", prefix, std::process::id(), nanos)
}

/// Receive one message within a bounded number of poll windows, or fail.
fn recv_within(agent: &ReadyAgent, windows: usize) -> String {
    for _ in 0..windows {
        if let Some(msg) = agent.recv() {
            return msg;
        }
        if let Some(msg) = agent.drain_spool().into_iter().next() {
            return msg;
        }
    }
    panic!("no message received within {} windows", windows);
}

/// Split a framed message into (reply_to, body).
fn parse_frame(frame: &str) -> (Option<String>, String) {
    if let Some(rest) = frame.strip_prefix("__REPLY_TO__:") {
        let mut lines = rest.splitn(2, '\n');
        let from = lines.next().unwrap_or("").to_string();
        let body = lines
            .next()
            .unwrap_or("")
            .trim_end_matches('\n')
            .to_string();
        (Some(from), body)
    } else {
        (None, frame.trim_end_matches('\n').to_string())
    }
}

// [int->REQ-MSG-1] [int->REQ-MSG-3]
#[test]
fn two_agents_exchange_message_tcp_and_spool() {
    init_home();
    let owlery = perch::owlery_dir();

    let a = unique("alice");
    let b = unique("bob");
    let c = unique("carol");

    // --- Live TCP path: A and B both listening; A messages B. ---
    let (agent_a, _) = ReadyAgent::start(&a).unwrap();
    let (agent_b, _) = ReadyAgent::start(&b).unwrap();

    assert_eq!(
        deliver::deliver(&b, &a, "hello bob", &owlery),
        DeliveryPath::Tcp
    );
    let got = recv_within(&agent_b, 25);
    let (from, body) = parse_frame(&got);
    assert_eq!(from.as_deref(), Some(a.as_str()), "reply-to routing");
    assert_eq!(body, "hello bob");

    // --- Reply routing: B replies to A using the recovered reply-to. ---
    assert_eq!(
        deliver::deliver(&a, &b, "hi alice", &owlery),
        DeliveryPath::Tcp
    );
    let reply = recv_within(&agent_a, 25);
    let (rfrom, rbody) = parse_frame(&reply);
    assert_eq!(rfrom.as_deref(), Some(b.as_str()));
    assert_eq!(rbody, "hi alice");

    // --- Offline spool path: C exists but isn't listening; B messages C. ---
    let c_perch = perch::resolve_perch_path(&c, ParentHint::Infer);
    std::fs::create_dir_all(&c_perch).unwrap();
    info::write_info(
        &c_perch,
        &InfoJson::new(&c, "now", std::process::id(), "sess", "ready_agent"),
    )
    .unwrap();

    assert_eq!(
        deliver::deliver(&c, &b, "for carol later", &owlery),
        DeliveryPath::Spool
    );
    assert_eq!(spool::pending_count_at(&c_perch).unwrap(), 1);

    // --- C starts and drains the backlog on startup. ---
    let (_agent_c, backlog) = ReadyAgent::start(&c).unwrap();
    assert_eq!(backlog.len(), 1, "C drains the spooled message on startup");
    let (cfrom, cbody) = parse_frame(&backlog[0]);
    assert_eq!(cfrom.as_deref(), Some(b.as_str()));
    assert_eq!(cbody, "for carol later");

    drop(agent_a);
    drop(agent_b);
}

// [int->REQ-MSG-1] a message spooled while the target is down is delivered the
// moment the target comes up — the offline->online handoff in isolation.
#[test]
fn offline_then_online_delivers_backlog() {
    init_home();
    let owlery = perch::owlery_dir();
    let target = unique("sleeper");

    // Perch exists, no listener: delivery spools.
    let perch_path = perch::resolve_perch_path(&target, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    info::write_info(
        &perch_path,
        &InfoJson::new(&target, "now", std::process::id(), "sess", "ready_agent"),
    )
    .unwrap();

    for i in 0..3 {
        assert_eq!(
            deliver::deliver(&target, "sender", &format!("msg {i}"), &owlery),
            DeliveryPath::Spool
        );
    }

    // Coming up drains all three, oldest first.
    let (_agent, backlog) = ReadyAgent::start(&target).unwrap();
    assert_eq!(backlog.len(), 3);
    let bodies: Vec<String> = backlog.iter().map(|f| parse_frame(f).1).collect();
    assert_eq!(bodies, vec!["msg 0", "msg 1", "msg 2"]);
}


killer_quickstart: recv/drain/backlog now yield `(from,body)` directly — drop `parse_frame`, return the structural pair. Rewrite helpers + assertions.


/// Receive one message within a bounded number of poll windows, or fail.
fn recv_within(agent: &ReadyAgent, windows: usize) -> String {
    for _ in 0..windows {
        if let Some(msg) = agent.recv() {
            return msg;
        }
        if let Some(msg) = agent.drain_spool().into_iter().next() {
            return msg;
        }
    }
    panic!("no message received within {} windows", windows);
}

/// Split a framed message into (reply_to, body).
fn parse_frame(frame: &str) -> (Option<String>, String) {
    if let Some(rest) = frame.strip_prefix("__REPLY_TO__:") {
        let mut lines = rest.splitn(2, '\n');
        let from = lines.next().unwrap_or("").to_string();
        let body = lines
            .next()
            .unwrap_or("")
            .trim_end_matches('\n')
            .to_string();
        (Some(from), body)
    } else {
        (None, frame.trim_end_matches('\n').to_string())
    }
}

/// Receive one message within a bounded number of poll windows, or fail.
/// Returns the structural `(from, body)` pair (ADR-0020 — `from` is `""` when
/// anonymous; the delivery edge composes the `<EVENT>`).
fn recv_within(agent: &ReadyAgent, windows: usize) -> (String, String) {
    for _ in 0..windows {
        if let Some(msg) = agent.recv() {
            return msg;
        }
        if let Some(msg) = agent.drain_spool().into_iter().next() {
            return msg;
        }
    }
    panic!("no message received within {} windows", windows);
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-msg\tests\killer_quickstart.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// Receive one message within a bounded number of poll windows, or fail.
/// Returns the structural `(from, body)` pair (ADR-0020 — `from` is `""` when
/// anonymous; the delivery edge composes the `<EVENT>`).
fn recv_within(agent: &ReadyAgent, windows: usize) -> (String, String) {
    for _ in 0..windows {
        if let Some(msg) = agent.recv() {
            return msg;
        }
        if let Some(msg) = agent.drain_spool().into_iter().next() {
            return msg;
        }
    }
    panic!("no message received within {} windows", windows);
}

//! **The M1 killer quickstart (local):** two ready agents exchange a message
//! end-to-end through the real crate boundaries — the live TCP path *and* the
//! offline spool path — on one box.
//!
//! This is the `int`-stage proof for REQ-MSG-1 (delivery + routing + reply) and
//! REQ-MSG-3 (ready lifecycle + backlog drain). It is cross-perch and
//! multi-listener: three distinct perches (A, B, C) over a shared owlery, two
//! of them holding live listeners simultaneously, with a spool hop in between.
// [int->REQ-MSG-1]
// [int->REQ-MSG-3]

use std::sync::OnceLock;

use spt_msg::deliver::{self, DeliveryPath};
use spt_msg::ready::ReadyAgent;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use tempfile::TempDir;

/// Hold the temp owlery for the whole test binary so it isn't deleted mid-run.
static HOME: OnceLock<TempDir> = OnceLock::new();

/// Point SPT_HOME at an isolated temp dir (once per test process).
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp dir");
        std::env::set_var("SPT_HOME", dir.path());
        dir
    });
}

fn unique(prefix: &str) -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("{}-{}-{}", prefix, std::process::id(), nanos)
}

/// Receive one message within a bounded number of poll windows, or fail.
fn recv_within(agent: &ReadyAgent, windows: usize) -> String {
    for _ in 0..windows {
        if let Some(msg) = agent.recv() {
            return msg;
        }
        if let Some(msg) = agent.drain_spool().into_iter().next() {
            return msg;
        }
    }
    panic!("no message received within {} windows", windows);
}

/// Split a framed message into (reply_to, body).
fn parse_frame(frame: &str) -> (Option<String>, String) {
    if let Some(rest) = frame.strip_prefix("__REPLY_TO__:") {
        let mut lines = rest.splitn(2, '\n');
        let from = lines.next().unwrap_or("").to_string();
        let body = lines
            .next()
            .unwrap_or("")
            .trim_end_matches('\n')
            .to_string();
        (Some(from), body)
    } else {
        (None, frame.trim_end_matches('\n').to_string())
    }
}

// [int->REQ-MSG-1] [int->REQ-MSG-3]
#[test]
fn two_agents_exchange_message_tcp_and_spool() {
    init_home();
    let owlery = perch::owlery_dir();

    let a = unique("alice");
    let b = unique("bob");
    let c = unique("carol");

    // --- Live TCP path: A and B both listening; A messages B. ---
    let (agent_a, _) = ReadyAgent::start(&a).unwrap();
    let (agent_b, _) = ReadyAgent::start(&b).unwrap();

    assert_eq!(
        deliver::deliver(&b, &a, "hello bob", &owlery),
        DeliveryPath::Tcp
    );
    let got = recv_within(&agent_b, 25);
    let (from, body) = parse_frame(&got);
    assert_eq!(from.as_deref(), Some(a.as_str()), "reply-to routing");
    assert_eq!(body, "hello bob");

    // --- Reply routing: B replies to A using the recovered reply-to. ---
    assert_eq!(
        deliver::deliver(&a, &b, "hi alice", &owlery),
        DeliveryPath::Tcp
    );
    let reply = recv_within(&agent_a, 25);
    let (rfrom, rbody) = parse_frame(&reply);
    assert_eq!(rfrom.as_deref(), Some(b.as_str()));
    assert_eq!(rbody, "hi alice");

    // --- Offline spool path: C exists but isn't listening; B messages C. ---
    let c_perch = perch::resolve_perch_path(&c, ParentHint::Infer);
    std::fs::create_dir_all(&c_perch).unwrap();
    info::write_info(
        &c_perch,
        &InfoJson::new(&c, "now", std::process::id(), "sess", "ready_agent"),
    )
    .unwrap();

    assert_eq!(
        deliver::deliver(&c, &b, "for carol later", &owlery),
        DeliveryPath::Spool
    );
    assert_eq!(spool::pending_count_at(&c_perch).unwrap(), 1);

    // --- C starts and drains the backlog on startup. ---
    let (_agent_c, backlog) = ReadyAgent::start(&c).unwrap();
    assert_eq!(backlog.len(), 1, "C drains the spooled message on startup");
    let (cfrom, cbody) = parse_frame(&backlog[0]);
    assert_eq!(cfrom.as_deref(), Some(b.as_str()));
    assert_eq!(cbody, "for carol later");

    drop(agent_a);
    drop(agent_b);
}

// [int->REQ-MSG-1] a message spooled while the target is down is delivered the
// moment the target comes up — the offline->online handoff in isolation.
#[test]
fn offline_then_online_delivers_backlog() {
    init_home();
    let owlery = perch::owlery_dir();
    let target = unique("sleeper");

    // Perch exists, no listener: delivery spools.
    let perch_path = perch::resolve_perch_path(&target, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    info::write_info(
        &perch_path,
        &InfoJson::new(&target, "now", std::process::id(), "sess", "ready_agent"),
    )
    .unwrap();

    for i in 0..3 {
        assert_eq!(
            deliver::deliver(&target, "sender", &format!("msg {i}"), &owlery),
            DeliveryPath::Spool
        );
    }

    // Coming up drains all three, oldest first.
    let (_agent, backlog) = ReadyAgent::start(&target).unwrap();
    assert_eq!(backlog.len(), 3);
    let bodies: Vec<String> = backlog.iter().map(|f| parse_frame(f).1).collect();
    assert_eq!(bodies, vec!["msg 0", "msg 1", "msg 2"]);
}

-fn recv_within(agent: &ReadyAgent, windows: usize) -> String {

+/// Returns the structural `(from, body)` pair (ADR-0020 — `from` is `""` when

+/// anonymous; the delivery edge composes the `<EVENT>`).

+fn recv_within(agent: &ReadyAgent, windows: usize) -> (String, String) {

-/// Split a framed message into (reply_to, body).

-fn parse_frame(frame: &str) -> (Option<String>, String) {

-    if let Some(rest) = frame.strip_prefix("__REPLY_TO__:") {

-        let mut lines = rest.splitn(2, '\n');

-        let from = lines.next().unwrap_or("").to_string();

-        (None, frame.trim_end_matches('\n').to_string())


    let got = recv_within(&agent_b, 25);
    let (from, body) = parse_frame(&got);
    assert_eq!(from.as_deref(), Some(a.as_str()), "reply-to routing");
    assert_eq!(body, "hello bob");

    let (from, body) = recv_within(&agent_b, 25);
    assert_eq!(from, a, "reply-to routing (structural from)");
    assert_eq!(body, "hello bob");


    let (from, body) = recv_within(&agent_b, 25);
    assert_eq!(from, a, "reply-to routing (structural from)");
    assert_eq!(body, "hello bob");

//! **The M1 killer quickstart (local):** two ready agents exchange a message
//! end-to-end through the real crate boundaries — the live TCP path *and* the
//! offline spool path — on one box.
//!
//! This is the `int`-stage proof for REQ-MSG-1 (delivery + routing + reply) and
//! REQ-MSG-3 (ready lifecycle + backlog drain). It is cross-perch and
//! multi-listener: three distinct perches (A, B, C) over a shared owlery, two
//! of them holding live listeners simultaneously, with a spool hop in between.
// [int->REQ-MSG-1]
// [int->REQ-MSG-3]

use std::sync::OnceLock;

use spt_msg::deliver::{self, DeliveryPath};
use spt_msg::ready::ReadyAgent;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;
use tempfile::TempDir;

/// Hold the temp owlery for the whole test binary so it isn't deleted mid-run.
static HOME: OnceLock<TempDir> = OnceLock::new();

/// Point SPT_HOME at an isolated temp dir (once per test process).
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp dir");
        std::env::set_var("SPT_HOME", dir.path());
        dir
    });
}

fn unique(prefix: &str) -> String {
    let nanos = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("{}-{}-{}", prefix, std::process::id(), nanos)
}

/// Receive one message within a bounded number of poll windows, or fail.
/// Returns the structural `(from, body)` pair (ADR-0020 — `from` is `""` when
/// anonymous; the delivery edge composes the `<EVENT>`).
fn recv_within(agent: &ReadyAgent, windows: usize) -> (String, String) {
    for _ in 0..windows {
        if let Some(msg) = agent.recv() {
            return msg;
        }
        if let Some(msg) = agent.drain_spool().into_iter().next() {
            return msg;
        }
    }
    panic!("no message received within {} windows", windows);
}

// [int->REQ-MSG-1] [int->REQ-MSG-3]
#[test]
fn two_agents_exchange_message_tcp_and_spool() {
    init_home();
    let owlery = perch::owlery_dir();

    let a = unique("alice");
    let b = unique("bob");
    let c = unique("carol");

    // --- Live TCP path: A and B both listening; A messages B. ---
    let (agent_a, _) = ReadyAgent::start(&a).unwrap();
    let (agent_b, _) = ReadyAgent::start(&b).unwrap();

    assert_eq!(
        deliver::deliver(&b, &a, "hello bob", &owlery),
        DeliveryPath::Tcp
    );
    let got = recv_within(&agent_b, 25);
    let (from, body) = parse_frame(&got);
    assert_eq!(from.as_deref(), Some(a.as_str()), "reply-to routing");
    assert_eq!(body, "hello bob");

    // --- Reply routing: B replies to A using the recovered reply-to. ---
    assert_eq!(
        deliver::deliver(&a, &b, "hi alice", &owlery),
        DeliveryPath::Tcp
    );
    let reply = recv_within(&agent_a, 25);
    let (rfrom, rbody) = parse_frame(&reply);
    assert_eq!(rfrom.as_deref(), Some(b.as_str()));
    assert_eq!(rbody, "hi alice");

    // --- Offline spool path: C exists but isn't listening; B messages C. ---
    let c_perch = perch::resolve_perch_path(&c, ParentHint::Infer);
    std::fs::create_dir_all(&c_perch).unwrap();
    info::write_info(
        &c_perch,
        &InfoJson::new(&c, "now", std::process::id(), "sess", "ready_agent"),
    )
    .unwrap();

    assert_eq!(
        deliver::deliver(&c, &b, "for carol later", &owlery),
        DeliveryPath::Spool
    );
    assert_eq!(spool::pending_count_at(&c_perch).unwrap(), 1);

    // --- C starts and drains the backlog on startup. ---
    let (_agent_c, backlog) = ReadyAgent::start(&c).unwrap();
    assert_eq!(backlog.len(), 1, "C drains the spooled message on startup");
    let (cfrom, cbody) = parse_frame(&backlog[0]);
    assert_eq!(cfrom.as_deref(), Some(b.as_str()));
    assert_eq!(cbody, "for carol later");

    drop(agent_a);
    drop(agent_b);
}

// [int->REQ-MSG-1] a message spooled while the target is down is delivered the
// moment the target comes up — the offline->online handoff in isolation.
#[test]
fn offline_then_online_delivers_backlog() {
    init_home();
    let owlery = perch::owlery_dir();
    let target = unique("sleeper");

    // Perch exists, no listener: delivery spools.
    let perch_path = perch::resolve_perch_path(&target, ParentHint::Infer);
    std::fs::create_dir_all(&perch_path).unwrap();
    info::write_info(
        &perch_path,
        &InfoJson::new(&target, "now", std::process::id(), "sess", "ready_agent"),
    )
    .unwrap();

    for i in 0..3 {
        assert_eq!(
            deliver::deliver(&target, "sender", &format!("msg {i}"), &owlery),
            DeliveryPath::Spool
        );
    }

    // Coming up drains all three, oldest first.
    let (_agent, backlog) = ReadyAgent::start(&target).unwrap();
    assert_eq!(backlog.len(), 3);
    let bodies: Vec<String> = backlog.iter().map(|f| parse_frame(f).1).collect();
    assert_eq!(bodies, vec!["msg 0", "msg 1", "msg 2"]);
}

-    let got = recv_within(&agent_b, 25);

-    let (from, body) = parse_frame(&got);

-    assert_eq!(from.as_deref(), Some(a.as_str()), "reply-to routing");

+    let (from, body) = recv_within(&agent_b, 25);

+    assert_eq!(from, a, "reply-to routing (structural from)");


    let reply = recv_within(&agent_a, 25);
    let (rfrom, rbody) = parse_frame(&reply);
    assert_eq!(rfrom.as_deref(), Some(b.as_str()));
    assert_eq!(rbody, "hi alice");

    let (rfrom, rbody) = recv_within(&agent_a, 25);
    assert_eq!(rfrom, b);
    assert_eq!(rbody, "hi alice");