    // [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()));
    }

//! 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-1] format_message shapes the __REPLY_TO__ contract.
    #[test]
    fn format_with_from_prepends_reply_to() {
        assert_eq!(
            format_message("alice", "hi bob"),
            "__REPLY_TO__:alice\nhi bob\n"
        );
    }

    // [unit->REQ-MSG-1] empty from => anonymous (no reply-to line).
    #[test]
    fn format_empty_from_is_anonymous() {
        assert_eq!(format_message("", "no reply"), "no reply\n");
    }

    // [unit->REQ-MSG-1] a framed message round-trips client -> listener byte-exact.
    #[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, "__REPLY_TO__:alice\nhello\nworld\n");
    }

    // [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);
    }
}

-    // [unit->REQ-MSG-1] format_message shapes the __REPLY_TO__ contract.

+    // [unit->REQ-MSG-ENVELOPE] encode/decode carries (from, body) structurally —

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

-    fn format_with_from_prepends_reply_to() {

-            format_message("alice", "hi bob"),

-            "__REPLY_TO__:alice\nhi bob\n"

+    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-1] empty from => anonymous (no reply-to line).

+    // [unit->REQ-MSG-ENVELOPE] empty from => anonymous (len-0 from prefix), body intact.

-    fn format_empty_from_is_anonymous() {

-        assert_eq!(format_message("", "no reply"), "no reply\n");

+    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-1] a framed message round-trips client -> listener byte-exact.

+    // [unit->REQ-MSG-ENVELOPE] a typed <EVENT> body rides the wire verbatim — the

+    // structural frame never reparses or rewraps it.

+    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.

+    fn decode_rejects_overrunning_from_len() {

+        // Claim from_len = 100 on a 6-byte payload.

+        let mut bad = (100u32).to_be_bytes().to_vec();

+            decode_frame(&bad).unwrap_err().kind(),

+    // [unit->REQ-MSG-1] a framed message round-trips client -> listener as (from, body).

-        assert_eq!(got, "__REPLY_TO__:alice\nhello\nworld\n");

+        assert_eq!(got, ("alice".to_string(), "hello\nworld".to_string()));


emit.rs: drop `parse_frame`, `render_event_lines(from, body)`, add whole-compose for hook drains.


Edit

//! Listener-stream EVENT rendering: raw wire/spool frame → the `<EVENT …>`
//! envelope lines a harness agent parses off the Monitor stream.
//!
//! Clean-room re-expression of the sister `owl/poll.rs` emit path
//! (`parse_tcp_message` → `assemble_event_line` → `chunk_if_oversized`); the
//! envelope **format** itself is the ADR-0001 copy-verbatim grammar already
//! housed in [`spt_proto::event`]. This module is the missing glue the M1
//! listener loops shipped without: they printed the raw
//! `__REPLY_TO__:{from}\n{body}` frame straight to stdout, so receiving
//! agents saw a bare body instead of `<EVENT type="msg" from="…">…</EVENT>`.
//!
//! Scope: the **listener stream** only (`spt ready` / `spt api listen` — the
//! `« spt event »` Monitor loops). Hook-channel drains (`spt api poll`,
//! `api worker-poll`) keep the raw frame by contract — their consumer is the
//! adapter's hook composer, not the event stream.
// [impl->REQ-MSG-4]

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

/// Parse a raw delivered frame into `(from, body)`.
/// Frame shape: `__REPLY_TO__:{from}\n{body}` or just `{body}` (anonymous —
/// `from` is `""`). Trailing newlines are trimmed off the body (the TCP wire
/// adds one; spool rows don't). Mirrors the sister's `parse_tcp_message`.
// [impl->REQ-MSG-4]
pub fn parse_frame(msg: &str) -> (String, String) {
    if let Some(rest) = msg.strip_prefix("__REPLY_TO__:") {
        if let Some(newline_pos) = rest.find('\n') {
            let from = rest[..newline_pos].to_string();
            let body = rest[newline_pos + 1..].trim_end_matches('\n').to_string();
            return (from, body);
        }
    }
    (String::new(), msg.trim_end_matches('\n').to_string())
}

/// Render one delivered frame into the stdout line(s) for the listener
/// stream, mirroring the sister's emit discipline:
///
/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>`
///    envelope (echo_commune, notify, …) passes through **verbatim** — never
///    re-wrapped (the sister's echo-commune-double-envelope fix);
/// 2. anything else composes `<EVENT type="msg" from="{from}">{body}</EVENT>`
///    (attr/body escaped; anonymous delivery renders `from=""`);
/// 3. the assembled line chunks into `<EVENT-PART …>` lines past
///    [`EVENT_LINE_THRESHOLD`] so the harness notification cap holds.
// [impl->REQ-MSG-4]
pub fn render_event_lines(msg: &str) -> Vec<String> {
    let (from, body) = parse_frame(msg);
    if is_typed_event_envelope(&body) {
        return vec![body];
    }
    let line = compose_msg_event(&from, &body);
    chunk_if_oversized(&line, EVENT_LINE_THRESHOLD)
}

//! 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
}


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
}

//! Listener-stream EVENT rendering: raw wire/spool frame → the `<EVENT …>`
//! envelope lines a harness agent parses off the Monitor stream.
//!
//! Clean-room re-expression of the sister `owl/poll.rs` emit path
//! (`parse_tcp_message` → `assemble_event_line` → `chunk_if_oversized`); the
//! envelope **format** itself is the ADR-0001 copy-verbatim grammar already
//! housed in [`spt_proto::event`]. This module is the missing glue the M1
//! listener loops shipped without: they printed the raw
//! `__REPLY_TO__:{from}\n{body}` frame straight to stdout, so receiving
//! agents saw a bare body instead of `<EVENT type="msg" from="…">…</EVENT>`.
//!
//! Scope: the **listener stream** only (`spt ready` / `spt api listen` — the
//! `« spt event »` Monitor loops). Hook-channel drains (`spt api poll`,
//! `api worker-poll`) keep the raw frame by contract — their consumer is the
//! adapter's hook composer, not the event stream.
// [impl->REQ-MSG-4]

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

/// Parse a raw delivered frame into `(from, body)`.
/// Frame shape: `__REPLY_TO__:{from}\n{body}` or just `{body}` (anonymous —
/// `from` is `""`). Trailing newlines are trimmed off the body (the TCP wire
/// adds one; spool rows don't). Mirrors the sister's `parse_tcp_message`.
// [impl->REQ-MSG-4]
pub fn parse_frame(msg: &str) -> (String, String) {
    if let Some(rest) = msg.strip_prefix("__REPLY_TO__:") {
        if let Some(newline_pos) = rest.find('\n') {
            let from = rest[..newline_pos].to_string();
            let body = rest[newline_pos + 1..].trim_end_matches('\n').to_string();
            return (from, body);
        }
    }
    (String::new(), msg.trim_end_matches('\n').to_string())
}

/// Render one delivered frame into the stdout line(s) for the listener
/// stream, mirroring the sister's emit discipline:
///
/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>`
///    envelope (echo_commune, notify, …) passes through **verbatim** — never
///    re-wrapped (the sister's echo-commune-double-envelope fix);
/// 2. anything else composes `<EVENT type="msg" from="{from}">{body}</EVENT>`
///    (attr/body escaped; anonymous delivery renders `from=""`);
/// 3. the assembled line chunks into `<EVENT-PART …>` lines past
///    [`EVENT_LINE_THRESHOLD`] so the harness notification cap holds.
// [impl->REQ-MSG-4]
pub fn render_event_lines(msg: &str) -> Vec<String> {
    let (from, body) = parse_frame(msg);
    if is_typed_event_envelope(&body) {
        return vec![body];
    }
    let line = compose_msg_event(&from, &body);
    chunk_if_oversized(&line, EVENT_LINE_THRESHOLD)
}

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

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

    // [unit->REQ-MSG-4] anonymous frame (no __REPLY_TO__ line) still gets the
    // envelope — from="" — never a bare body on the stream.
    #[test]
    fn anonymous_frame_still_enveloped() {
        let lines = render_event_lines("no header here\n");
        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("__REPLY_TO__:bob\nline one\nline two\n");
        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-4] a pre-formed typed envelope body (e.g. the daemon's
    // spooled notify rows) passes through verbatim — no outer msg double-wrap.
    #[test]
    fn typed_envelope_body_passes_through_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer" notif_id="cafe:1">build done</EVENT>"#;
        let frame = format!("__REPLY_TO__:issuer\n{typed}");
        assert_eq!(render_event_lines(&frame), vec![typed.to_string()]);
    }

    // [unit->REQ-MSG-5] the delivery surface RENDERS the user-msg type: a
    // gated user-msg envelope body (the form the local origination / WAN
    // ingress produce) reaches the listener stream verbatim — the receiving
    // agent sees `<EVENT type="user-msg">`, never a re-wrapped plain msg.
    #[test]
    fn user_msg_envelope_renders_verbatim_on_the_stream() {
        let um = spt_proto::event::compose_user_msg_event("playdate-gw", "ship it");
        let frame = format!("__REPLY_TO__:playdate-gw\n{um}");
        let lines = render_event_lines(&frame);
        assert_eq!(lines, vec![um.clone()]);
        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-4] an oversized assembled line chunks into EVENT-PART
    // lines that reassemble byte-exact.
    #[test]
    fn oversized_line_chunks_into_event_parts() {
        let body = "x".repeat(1500);
        let frame = format!("__REPLY_TO__:doyle\n{body}");
        let lines = render_event_lines(&frame);
        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);
    }

    // [unit->REQ-MSG-4] parse_frame splits header/body and trims only
    // trailing newlines.
    #[test]
    fn parse_frame_shapes() {
        assert_eq!(
            parse_frame("__REPLY_TO__:a\nb\n"),
            ("a".to_string(), "b".to_string())
        );
        assert_eq!(parse_frame("bare\n"), (String::new(), "bare".to_string()));
        // Header with multi-line body keeps interior newlines.
        assert_eq!(
            parse_frame("__REPLY_TO__:a\nx\ny\n"),
            ("a".to_string(), "x\ny".to_string())
        );
    }
}

-//! Listener-stream EVENT rendering: raw wire/spool frame → the `<EVENT …>`

-//! envelope lines a harness agent parses off the Monitor stream.

+//! Delivery-edge EVENT rendering: the structural `(from, body)` pair → the

+//! `<EVENT …>` envelope every arriving-message surface emits (ADR-0020).

-//! Clean-room re-expression of the sister `owl/poll.rs` emit path

-//! (`parse_tcp_message` → `assemble_event_line` → `chunk_if_oversized`); the

-//! envelope **format** itself is the ADR-0001 copy-verbatim grammar already

-//! housed in [`spt_proto::event`]. This module is the missing glue the M1

-//! listener loops shipped without: they printed the raw

-//! `__REPLY_TO__:{from}\n{body}` frame straight to stdout, so receiving

-//! agents saw a bare body instead of `<EVENT type="msg" from="…">…</EVENT>`.

+//! 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

-//! Scope: the **listener stream** only (`spt ready` / `spt api listen` — the

-//! `« spt event »` Monitor loops). Hook-channel drains (`spt api poll`,

-//! `api worker-poll`) keep the raw frame by contract — their consumer is the

-//! adapter's hook composer, not the event stream.

+//! 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

+//! - [`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).

-/// Parse a raw delivered frame into `(from, body)`.

-/// Frame shape: `__REPLY_TO__:{from}\n{body}` or just `{body}` (anonymous —

-/// `from` is `""`). Trailing newlines are trimmed off the body (the TCP wire

-/// adds one; spool rows don't). Mirrors the sister's `parse_tcp_message`.

-pub fn parse_frame(msg: &str) -> (String, String) {

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

-        if let Some(newline_pos) = rest.find('\n') {

-            let from = rest[..newline_pos].to_string();

-            let body = rest[newline_pos + 1..].trim_end_matches('\n').to_string();

+/// 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=""`).

+fn compose_line(from: &str, body: &str) -> (String, bool) {

+        (compose_msg_event(from, body), false)

-    (String::new(), msg.trim_end_matches('\n').to_string())

-/// Render one delivered frame into the stdout line(s) for the listener

-/// stream, mirroring the sister's emit discipline:

-/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>`

-///    envelope (echo_commune, notify, …) passes through **verbatim** — never

-///    re-wrapped (the sister's echo-commune-double-envelope fix);

-/// 2. anything else composes `<EVENT type="msg" from="{from}">{body}</EVENT>`

-///    (attr/body escaped; anonymous delivery renders `from=""`);

-/// 3. the assembled line chunks into `<EVENT-PART …>` lines past

-///    [`EVENT_LINE_THRESHOLD`] so the harness notification cap holds.

+/// 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.

-pub fn render_event_lines(msg: &str) -> Vec<String> {

-    let (from, body) = parse_frame(msg);

-    if is_typed_event_envelope(&body) {

-    let line = compose_msg_event(&from, &body);

+pub fn render_event_lines(from: &str, body: &str) -> Vec<String> {

+    let (line, _typed) = compose_line(from, body);

+/// 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).

+pub fn render_event_whole(from: &str, body: &str) -> String {


Migrate emit.rs tests to the `(from, body)` signature + add whole-render + adversarial typed-passthrough coverage.


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

    // [unit->REQ-MSG-4] anonymous frame (no __REPLY_TO__ line) still gets the
    // envelope — from="" — never a bare body on the stream.
    #[test]
    fn anonymous_frame_still_enveloped() {
        let lines = render_event_lines("no header here\n");
        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("__REPLY_TO__:bob\nline one\nline two\n");
        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-4] a pre-formed typed envelope body (e.g. the daemon's
    // spooled notify rows) passes through verbatim — no outer msg double-wrap.
    #[test]
    fn typed_envelope_body_passes_through_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer" notif_id="cafe:1">build done</EVENT>"#;
        let frame = format!("__REPLY_TO__:issuer\n{typed}");
        assert_eq!(render_event_lines(&frame), vec![typed.to_string()]);
    }

    // [unit->REQ-MSG-5] the delivery surface RENDERS the user-msg type: a
    // gated user-msg envelope body (the form the local origination / WAN
    // ingress produce) reaches the listener stream verbatim — the receiving
    // agent sees `<EVENT type="user-msg">`, never a re-wrapped plain msg.
    #[test]
    fn user_msg_envelope_renders_verbatim_on_the_stream() {
        let um = spt_proto::event::compose_user_msg_event("playdate-gw", "ship it");
        let frame = format!("__REPLY_TO__:playdate-gw\n{um}");
        let lines = render_event_lines(&frame);
        assert_eq!(lines, vec![um.clone()]);
        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-4] an oversized assembled line chunks into EVENT-PART
    // lines that reassemble byte-exact.
    #[test]
    fn oversized_line_chunks_into_event_parts() {
        let body = "x".repeat(1500);
        let frame = format!("__REPLY_TO__:doyle\n{body}");
        let lines = render_event_lines(&frame);
        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);
    }

    // [unit->REQ-MSG-4] parse_frame splits header/body and trims only
    // trailing newlines.
    #[test]
    fn parse_frame_shapes() {
        assert_eq!(
            parse_frame("__REPLY_TO__:a\nb\n"),
            ("a".to_string(), "b".to_string())
        );
        assert_eq!(parse_frame("bare\n"), (String::new(), "bare".to_string()));
        // Header with multi-line body keeps interior newlines.
        assert_eq!(
            parse_frame("__REPLY_TO__:a\nx\ny\n"),
            ("a".to_string(), "x\ny".to_string())
        );
    }
}

    // [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);
    }
}


    // [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);
    }
}

//! 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] the regular frame renders the sister msg envelope:
    // from attribute + body, exactly one line.
    #[test]
    fn frame_renders_msg_envelope() {
        let lines = render_event_lines("__REPLY_TO__:lea\ntest message, hi!\n");
        assert_eq!(
            lines,
            vec![r#"<EVENT type="msg" from="lea">test message, hi!</EVENT>"#.to_string()]
        );
    }

    // [unit->REQ-MSG-4] anonymous frame (no __REPLY_TO__ line) still gets the
    // envelope — from="" — never a bare body on the stream.
    #[test]
    fn anonymous_frame_still_enveloped() {
        let lines = render_event_lines("no header here\n");
        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("__REPLY_TO__:bob\nline one\nline two\n");
        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-4] a pre-formed typed envelope body (e.g. the daemon's
    // spooled notify rows) passes through verbatim — no outer msg double-wrap.
    #[test]
    fn typed_envelope_body_passes_through_verbatim() {
        let typed = r#"<EVENT type="notify" from="issuer" notif_id="cafe:1">build done</EVENT>"#;
        let frame = format!("__REPLY_TO__:issuer\n{typed}");
        assert_eq!(render_event_lines(&frame), vec![typed.to_string()]);
    }

    // [unit->REQ-MSG-5] the delivery surface RENDERS the user-msg type: a
    // gated user-msg envelope body (the form the local origination / WAN
    // ingress produce) reaches the listener stream verbatim — the receiving
    // agent sees `<EVENT type="user-msg">`, never a re-wrapped plain msg.
    #[test]
    fn user_msg_envelope_renders_verbatim_on_the_stream() {
        let um = spt_proto::event::compose_user_msg_event("playdate-gw", "ship it");
        let frame = format!("__REPLY_TO__:playdate-gw\n{um}");
        let lines = render_event_lines(&frame);
        assert_eq!(lines, vec![um.clone()]);
        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-4] an oversized assembled line chunks into EVENT-PART
    // lines that reassemble byte-exact.
    #[test]
    fn oversized_line_chunks_into_event_parts() {
        let body = "x".repeat(1500);
        let frame = format!("__REPLY_TO__:doyle\n{body}");
        let lines = render_event_lines(&frame);
        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);
    }

    // [unit->REQ-MSG-4] parse_frame splits header/body and trims only
    // trailing newlines.
    #[test]
    fn parse_frame_shapes() {
        assert_eq!(
            parse_frame("__REPLY_TO__:a\nb\n"),
            ("a".to_string(), "b".to_string())
        );
        assert_eq!(parse_frame("bare\n"), (String::new(), "bare".to_string()));
        // Header with multi-line body keeps interior newlines.
        assert_eq!(
            parse_frame("__REPLY_TO__:a\nx\ny\n"),
            ("a".to_string(), "x\ny".to_string())
        );
    }
}

-    // [unit->REQ-MSG-4] the regular frame renders the sister msg envelope:

+    // [unit->REQ-MSG-4] a structural (from, body) renders the msg envelope:

// from attribute + body, exactly one line.

-        let lines = render_event_lines("__REPLY_TO__:lea\ntest message, hi!\n");

+    fn structural_renders_msg_envelope() {

+        let lines = render_event_lines("lea", "test message, hi!");

-    // [unit->REQ-MSG-4] anonymous frame (no __REPLY_TO__ line) still gets the

-    // envelope — from="" — never a bare body on the stream.

+    // [unit->REQ-MSG-4] anonymous (from="") still gets the envelope — from="" —

+    // never a bare body on the stream.

-    fn anonymous_frame_still_enveloped() {

-        let lines = render_event_lines("no header here\n");

+        let lines = render_event_lines("", "no header here");

-        let lines = render_event_lines("__REPLY_TO__:bob\nline one\nline two\n");

+        let lines = render_event_lines("bob", "line one\nline two");

let p = parse_event(&lines[0]).unwrap();

assert_eq!(p.body, "line one\nline two");

-    // [unit->REQ-MSG-4] a pre-formed typed envelope body (e.g. the daemon's

-    // spooled notify rows) passes through verbatim — no outer msg double-wrap.

+    // [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.

-    fn typed_envelope_body_passes_through_verbatim() {

-        let typed = r#"<EVENT type="notify" from="issuer" notif_id="cafe:1">build done</EVENT>"#;

-        let frame = format!("__REPLY_TO__:issuer\n{typed}");

-        assert_eq!(render_event_lines(&frame), vec![typed.to_string()]);

+    fn typed_body_from_wins_over_structural_from_no_double_wrap() {

+            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");

-    // gated user-msg envelope body (the form the local origination / WAN

-    // ingress produce) reaches the listener stream verbatim — the receiving

+    // gated user-msg envelope body reaches both edges verbatim — the receiving

-    fn user_msg_envelope_renders_verbatim_on_the_stream() {

+    fn user_msg_envelope_renders_verbatim_on_both_edges() {

-        let frame = format!("__REPLY_TO__:playdate-gw\n{um}");

-        let lines = render_event_lines(&frame);

+        let lines = render_event_lines("playdate-gw", &um);

+        assert_eq!(render_event_whole("playdate-gw", &um), um);

let p = parse_event(&lines[0]).unwrap();

-    // [unit->REQ-MSG-4] an oversized assembled line chunks into EVENT-PART

-    // lines that reassemble byte-exact.

+    // [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.

-    fn oversized_line_chunks_into_event_parts() {

-        let frame = format!("__REPLY_TO__:doyle\n{body}");

+        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"));

+    // [unit->REQ-MSG-4] an oversized listener line chunks into EVENT-PART lines

+    // that reassemble byte-exact (listener stream unchanged — still chunks).

+    fn oversized_listener_line_chunks_into_event_parts() {

+        let lines = render_event_lines("doyle", &body);

-    // [unit->REQ-MSG-4] parse_frame splits header/body and trims only

-            parse_frame("__REPLY_TO__:a\nb\n"),

-            ("a".to_string(), "b".to_string())

-        assert_eq!(parse_frame("bare\n"), (String::new(), "bare".to_string()));

-        // Header with multi-line body keeps interior newlines.

-            parse_frame("__REPLY_TO__:a\nx\ny\n"),

-            ("a".to_string(), "x\ny".to_string())


spt-msg core done. Now fix consumers: deliver.rs, listener.rs, ready.rs, ring.rs. Read them.


1	//! `PollListener` — a loopback TCP accept listener with registry lifecycle.
2	//!
3	//! Clean-room re-expression of the sister `common/listener.rs` against the
4	//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing
5	//! and the cross-platform `poll_ready` syscall wrapper are mechanical and match
6	//! the sister byte-for-byte in behavior; the registry/info.json lifecycle is
7	//! re-pointed at `spt_store`.
8	//!
9	//! Lifecycle: [`PollListener::bind`] grabs an ephemeral `127.0.0.1:0` port and
10	//! writes the perch's address into the registry under the owning pid. On drop
11	//! (or [`PollListener::close`]) the registry entry is removed so no stale
12	//! address survives a crashed listener — the recycled-pid guard
13	//! ([`spt_store::registry::resolve_address`]) is the second line of defense.
14	//!
15	//! **Interim model (M1):** one standalone listener per ready agent. M3's
16	//! `spt-daemon` consolidates all listeners into one supervisor; the surface is
17	//! kept thin so that consolidation is a move, not a rewrite.
18	// [impl->REQ-MSG-1]
19	// [impl->REQ-MSG-3]
20
21	use spt_store::info::{self, PidValue};
22	use spt_store::perch::{self, ParentHint};
23	use spt_store::registry;
24	use std::io::ErrorKind;
25	use std::net::{SocketAddr, TcpListener};
26	use std::path::PathBuf;
27	use std::time::Duration;
28
29	use crate::wire;
30
31	/// A loopback TCP listener that owns its registry registration.
32	pub struct PollListener {
33	    listener: TcpListener,
34	    id: String,
35	    owlery: PathBuf,
36	    perch_path: PathBuf,
37	    closed: bool,
38	    /// Poll timeout for the blocking accept path; `None` => default 500 ms.
39	    timeout_ms: Option<i32>,
40	}
41
42	// poll_ready: wait for the listener socket to have a pending connection via the
43	// OS poll syscall. Returns true if readable, false on timeout/error.
44
45	#[cfg(unix)]
46	fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {
47	    use std::os::unix::io::AsRawFd;
48	    let mut pfd = libc::pollfd {
49	        fd: listener.as_raw_fd(),
50	        events: libc::POLLIN,
51	        revents: 0,
52	    };
53	    loop {
54	        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
55	        if ret == -1 {
56	            let err = std::io::Error::last_os_error();
57	            if err.kind() == ErrorKind::Interrupted {
58	                continue; // EINTR — retry
59	            }
60	            return false;
61	        }
62	        return ret > 0 && (pfd.revents & libc::POLLIN) != 0;
63	    }
64	}
65
66	#[cfg(windows)]
67	fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {
68	    use std::os::windows::io::AsRawSocket;
69	    use windows_sys::Win32::Networking::WinSock::{WSAPoll, POLLRDNORM, WSAPOLLFD};
70	    let mut pfd = WSAPOLLFD {
71	        fd: listener.as_raw_socket() as usize,
72	        events: POLLRDNORM,
73	        revents: 0,
74	    };
75	    let ret = unsafe { WSAPoll(&mut pfd, 1, timeout_ms) };
76	    ret > 0 && (pfd.revents & POLLRDNORM) != 0
77	}
78
79	impl PollListener {
80	    /// Bind an ephemeral loopback port in **non-blocking** mode and register the
81	    /// resulting address under this perch id (owning pid = current process).
82	    pub fn bind(id: &str) -> Result<Self, String> {
83	        Self::bind_inner(id, None)
84	    }
85
86	    /// Bind an ephemeral loopback port in **blocking** mode; the accept path
87	    /// uses an OS poll with the given timeout (see [`Self::check_message_blocking`]).
88	    pub fn bind_blocking(id: &str, timeout: Duration) -> Result<Self, String> {
89	        let ms = timeout.as_millis().min(i32::MAX as u128) as i32;
90	        Self::bind_inner(id, Some(ms))
91	    }
92
93	    fn bind_inner(id: &str, timeout_ms: Option<i32>) -> Result<Self, String> {
94	        let owlery = perch::owlery_dir();
95	        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
96
97	        let listener = TcpListener::bind("127.0.0.1:0")
98	            .map_err(|e| format!("Failed to bind TCP listener: {}", e))?;
99	        // Non-blocking listeners poll explicitly; blocking listeners keep the
100	        // OS default and gate accept behind poll_ready with a timeout.
101	        if timeout_ms.is_none() {
102	            listener
103	                .set_nonblocking(true)
104	                .map_err(|e| format!("Failed to set non-blocking: {}", e))?;
105	        }
106	        let addr = listener
107	            .local_addr()
108	            .map_err(|e| format!("Failed to get local address: {}", e))?;
109	        registry::register_address(id, &addr, &owlery)
110	            .map_err(|e| format!("Failed to register address: {}", e))?;
111
112	        Ok(Self {
113	            listener,
114	            id: id.to_string(),
115	            owlery,
116	            perch_path,
117	            closed: false,
118	            timeout_ms,
119	        })
120	    }
121
122	    /// The bound local address (`127.0.0.1:<ephemeral>`).
123	    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
124	        self.listener.local_addr()
125	    }
126
127	    /// Try to accept one pending connection and read its framed message.
128	    /// Returns `Some(msg)` if one was waiting, `None` otherwise (no panic).
129	    pub fn check_message(&self) -> Option<String> {
130	        match self.listener.accept() {
131	            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
132	            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,
133	            Err(_) => None,
134	        }
135	    }
136
137	    /// Wait (up to the configured timeout, default 500 ms) for a connection via
138	    /// the OS poll syscall, then accept+read one framed message.
139	    pub fn check_message_blocking(&self) -> Option<String> {
140	        let timeout_ms = self.timeout_ms.unwrap_or(500);
141	        if poll_ready(&self.listener, timeout_ms) {
142	            match self.listener.accept() {
143	                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
144	                Err(_) => None,
145	            }
146	        } else {
147	            None
148	        }
149	    }
150
151	    /// Unregister this perch's address from the registry. Idempotent.
152	    pub fn close(&mut self) {
153	        let _ = registry::unregister_address(&self.id, &self.owlery);
154	        self.closed = true;
155	    }
156
157	    /// Unregister and stamp `info.json` with the `BUSY` pid sentinel — the
158	    /// "listener alive but between polls" state a sender may observe. Used when
159	    /// the perch persists but the socket is torn down mid-cycle.
160	    pub fn close_busy(&mut self) {
161	        let _ = registry::unregister_address(&self.id, &self.owlery);
162	        write_busy(&self.perch_path);
163	        self.closed = true;
164	    }
165	}
166
167	impl Drop for PollListener {
168	    fn drop(&mut self) {
169	        if !self.closed {
170	            // Default teardown: drop the registry address so no stale entry
171	            // outlives the socket. info.json/spool are left untouched
172	            // (soft-cleanup semantics live in the ready lifecycle, T3).
173	            let _ = registry::unregister_address(&self.id, &self.owlery);
174	        }
175	    }
176	}
177
178	/// Rewrite `<perch>/info.json` with the `BUSY` pid sentinel, preserving every
179	/// other field. Best-effort: a missing/unparseable info.json is a no-op.
180	pub(crate) fn write_busy(perch_path: &std::path::Path) {
181	    if let Some(mut info) = info::read_info(perch_path) {
182	        info.pid = PidValue::Busy("BUSY".to_string());
183	        let _ = info::write_info(perch_path, &info);
184	    }
185	}
186
187	#[cfg(test)]
188	mod tests {
189	    use super::*;
190	    use std::net::TcpStream;
191	    use std::time::Instant;
192
193	    /// Unique perch id per invocation (pid + nanos) so concurrent tests sharing
194	    /// the process-global SPT_HOME registry never collide.
195	    fn unique_id(prefix: &str) -> String {
196	        let nanos = std::time::SystemTime::now()
197	            .duration_since(std::time::UNIX_EPOCH)
198	            .map(|d| d.as_nanos())
199	            .unwrap_or(0);
200	        format!("{}-{}-{}", prefix, std::process::id(), nanos)
201	    }
202
203	    // [unit->REQ-MSG-1] a non-blocking listener binds a loopback port, registers
204	    // its address, and a framed envelope round-trips client -> listener.
205	    #[test]
206	    fn bind_registers_and_round_trips() {
207	        crate::test_home::isolate();
208	        let id = unique_id("test-bind");
209	        let listener = PollListener::bind(&id).unwrap();
210	        let addr = listener.local_addr().unwrap();
211
212	        // Registry holds the bound address.
213	        let owlery = perch::owlery_dir();
214	        assert_eq!(
215	            registry::lookup_address(&id, &owlery),
216	            Some(addr.to_string())
217	        );
218
219	        // Deliver one framed message.
220	        let mut client = TcpStream::connect(addr).unwrap();
221	        wire::write_message(&mut client, "sender", "ping").unwrap();
222	        drop(client);
223
224	        // Non-blocking accept may briefly WouldBlock before the connection lands.
225	        let mut got = None;
226	        for _ in 0..200 {
227	            if let Some(m) = listener.check_message() {
228	                got = Some(m);
229	                break;
230	            }
231	            std::thread::sleep(Duration::from_millis(5));
232	        }
233	        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nping\n"));
234	    }
235
236	    // [unit->REQ-MSG-1] dropping a listener removes its registry address (no
237	    // stale entry survives a dead listener).
238	    #[test]
239	    fn drop_unregisters_address() {
240	        crate::test_home::isolate();
241	        let id = unique_id("test-drop");
242	        let owlery = perch::owlery_dir();
243	        {
244	            let _l = PollListener::bind(&id).unwrap();
245	            assert!(registry::lookup_address(&id, &owlery).is_some());
246	        }
247	        assert!(registry::lookup_address(&id, &owlery).is_none());
248	    }
249
250	    // [unit->REQ-MSG-1] a blocking listener returns None within ~timeout when
251	    // no connection arrives (bounded wait, not a hang).
252	    #[test]
253	    fn blocking_times_out_when_idle() {
254	        crate::test_home::isolate();
255	        let id = unique_id("test-block-idle");
256	        let listener = PollListener::bind_blocking(&id, Duration::from_millis(100)).unwrap();
257	        let start = Instant::now();
258	        assert!(listener.check_message_blocking().is_none());
259	        assert!(start.elapsed() < Duration::from_millis(400));
260	    }
261
262	    // [unit->REQ-MSG-1] a blocking listener accepts a message that arrives
263	    // within the poll window.
264	    #[test]
265	    fn blocking_accepts_within_window() {
266	        crate::test_home::isolate();
267	        let id = unique_id("test-block-recv");
268	        let listener = PollListener::bind_blocking(&id, Duration::from_millis(500)).unwrap();
269	        let addr = listener.local_addr().unwrap();
270
271	        let h = std::thread::spawn(move || {
272	            std::thread::sleep(Duration::from_millis(30));
273	            let mut c = TcpStream::connect(addr).unwrap();
274	            wire::write_message(&mut c, "", "pong").unwrap();
275	        });
276
277	        let got = listener.check_message_blocking();
278	        h.join().unwrap();
279	        assert_eq!(got.as_deref(), Some("pong\n"));
280	    }
281	}
282

//! `PollListener` — a loopback TCP accept listener with registry lifecycle.
//!
//! Clean-room re-expression of the sister `common/listener.rs` against the
//! spt-core `spt-store` API (perch resolver + registry). The socket plumbing
//! and the cross-platform `poll_ready` syscall wrapper are mechanical and match
//! the sister byte-for-byte in behavior; the registry/info.json lifecycle is
//! re-pointed at `spt_store`.
//!
//! Lifecycle: [`PollListener::bind`] grabs an ephemeral `127.0.0.1:0` port and
//! writes the perch's address into the registry under the owning pid. On drop
//! (or [`PollListener::close`]) the registry entry is removed so no stale
//! address survives a crashed listener — the recycled-pid guard
//! ([`spt_store::registry::resolve_address`]) is the second line of defense.
//!
//! **Interim model (M1):** one standalone listener per ready agent. M3's
//! `spt-daemon` consolidates all listeners into one supervisor; the surface is
//! kept thin so that consolidation is a move, not a rewrite.
// [impl->REQ-MSG-1]
// [impl->REQ-MSG-3]

use spt_store::info::{self, PidValue};
use spt_store::perch::{self, ParentHint};
use spt_store::registry;
use std::io::ErrorKind;
use std::net::{SocketAddr, TcpListener};
use std::path::PathBuf;
use std::time::Duration;

use crate::wire;

/// A loopback TCP listener that owns its registry registration.
pub struct PollListener {
    listener: TcpListener,
    id: String,
    owlery: PathBuf,
    perch_path: PathBuf,
    closed: bool,
    /// Poll timeout for the blocking accept path; `None` => default 500 ms.
    timeout_ms: Option<i32>,
}

// poll_ready: wait for the listener socket to have a pending connection via the
// OS poll syscall. Returns true if readable, false on timeout/error.

#[cfg(unix)]
fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {
    use std::os::unix::io::AsRawFd;
    let mut pfd = libc::pollfd {
        fd: listener.as_raw_fd(),
        events: libc::POLLIN,
        revents: 0,
    };
    loop {
        let ret = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
        if ret == -1 {
            let err = std::io::Error::last_os_error();
            if err.kind() == ErrorKind::Interrupted {
                continue; // EINTR — retry
            }
            return false;
        }
        return ret > 0 && (pfd.revents & libc::POLLIN) != 0;
    }
}

#[cfg(windows)]
fn poll_ready(listener: &TcpListener, timeout_ms: i32) -> bool {
    use std::os::windows::io::AsRawSocket;
    use windows_sys::Win32::Networking::WinSock::{WSAPoll, POLLRDNORM, WSAPOLLFD};
    let mut pfd = WSAPOLLFD {
        fd: listener.as_raw_socket() as usize,
        events: POLLRDNORM,
        revents: 0,
    };
    let ret = unsafe { WSAPoll(&mut pfd, 1, timeout_ms) };
    ret > 0 && (pfd.revents & POLLRDNORM) != 0
}

impl PollListener {
    /// Bind an ephemeral loopback port in **non-blocking** mode and register the
    /// resulting address under this perch id (owning pid = current process).
    pub fn bind(id: &str) -> Result<Self, String> {
        Self::bind_inner(id, None)
    }

    /// Bind an ephemeral loopback port in **blocking** mode; the accept path
    /// uses an OS poll with the given timeout (see [`Self::check_message_blocking`]).
    pub fn bind_blocking(id: &str, timeout: Duration) -> Result<Self, String> {
        let ms = timeout.as_millis().min(i32::MAX as u128) as i32;
        Self::bind_inner(id, Some(ms))
    }

    fn bind_inner(id: &str, timeout_ms: Option<i32>) -> Result<Self, String> {
        let owlery = perch::owlery_dir();
        let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

        let listener = TcpListener::bind("127.0.0.1:0")
            .map_err(|e| format!("Failed to bind TCP listener: {}", e))?;
        // Non-blocking listeners poll explicitly; blocking listeners keep the
        // OS default and gate accept behind poll_ready with a timeout.
        if timeout_ms.is_none() {
            listener
                .set_nonblocking(true)
                .map_err(|e| format!("Failed to set non-blocking: {}", e))?;
        }
        let addr = listener
            .local_addr()
            .map_err(|e| format!("Failed to get local address: {}", e))?;
        registry::register_address(id, &addr, &owlery)
            .map_err(|e| format!("Failed to register address: {}", e))?;

        Ok(Self {
            listener,
            id: id.to_string(),
            owlery,
            perch_path,
            closed: false,
            timeout_ms,
        })
    }

    /// The bound local address (`127.0.0.1:<ephemeral>`).
    pub fn local_addr(&self) -> std::io::Result<SocketAddr> {
        self.listener.local_addr()
    }

    /// Try to accept one pending connection and read its framed message.
    /// Returns `Some(msg)` if one was waiting, `None` otherwise (no panic).
    pub fn check_message(&self) -> Option<String> {
        match self.listener.accept() {
            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,
            Err(_) => None,
        }
    }

    /// Wait (up to the configured timeout, default 500 ms) for a connection via
    /// the OS poll syscall, then accept+read one framed message.
    pub fn check_message_blocking(&self) -> Option<String> {
        let timeout_ms = self.timeout_ms.unwrap_or(500);
        if poll_ready(&self.listener, timeout_ms) {
            match self.listener.accept() {
                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
                Err(_) => None,
            }
        } else {
            None
        }
    }

    /// Unregister this perch's address from the registry. Idempotent.
    pub fn close(&mut self) {
        let _ = registry::unregister_address(&self.id, &self.owlery);
        self.closed = true;
    }

    /// Unregister and stamp `info.json` with the `BUSY` pid sentinel — the
    /// "listener alive but between polls" state a sender may observe. Used when
    /// the perch persists but the socket is torn down mid-cycle.
    pub fn close_busy(&mut self) {
        let _ = registry::unregister_address(&self.id, &self.owlery);
        write_busy(&self.perch_path);
        self.closed = true;
    }
}

impl Drop for PollListener {
    fn drop(&mut self) {
        if !self.closed {
            // Default teardown: drop the registry address so no stale entry
            // outlives the socket. info.json/spool are left untouched
            // (soft-cleanup semantics live in the ready lifecycle, T3).
            let _ = registry::unregister_address(&self.id, &self.owlery);
        }
    }
}

/// Rewrite `<perch>/info.json` with the `BUSY` pid sentinel, preserving every
/// other field. Best-effort: a missing/unparseable info.json is a no-op.
pub(crate) fn write_busy(perch_path: &std::path::Path) {
    if let Some(mut info) = info::read_info(perch_path) {
        info.pid = PidValue::Busy("BUSY".to_string());
        let _ = info::write_info(perch_path, &info);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::net::TcpStream;
    use std::time::Instant;

    /// Unique perch id per invocation (pid + nanos) so concurrent tests sharing
    /// the process-global SPT_HOME registry never collide.
    fn unique_id(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)
    }

    // [unit->REQ-MSG-1] a non-blocking listener binds a loopback port, registers
    // its address, and a framed envelope round-trips client -> listener.
    #[test]
    fn bind_registers_and_round_trips() {
        crate::test_home::isolate();
        let id = unique_id("test-bind");
        let listener = PollListener::bind(&id).unwrap();
        let addr = listener.local_addr().unwrap();

        // Registry holds the bound address.
        let owlery = perch::owlery_dir();
        assert_eq!(
            registry::lookup_address(&id, &owlery),
            Some(addr.to_string())
        );

        // Deliver one framed message.
        let mut client = TcpStream::connect(addr).unwrap();
        wire::write_message(&mut client, "sender", "ping").unwrap();
        drop(client);

        // Non-blocking accept may briefly WouldBlock before the connection lands.
        let mut got = None;
        for _ in 0..200 {
            if let Some(m) = listener.check_message() {
                got = Some(m);
                break;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nping\n"));
    }

    // [unit->REQ-MSG-1] dropping a listener removes its registry address (no
    // stale entry survives a dead listener).
    #[test]
    fn drop_unregisters_address() {
        crate::test_home::isolate();
        let id = unique_id("test-drop");
        let owlery = perch::owlery_dir();
        {
            let _l = PollListener::bind(&id).unwrap();
            assert!(registry::lookup_address(&id, &owlery).is_some());
        }
        assert!(registry::lookup_address(&id, &owlery).is_none());
    }

    // [unit->REQ-MSG-1] a blocking listener returns None within ~timeout when
    // no connection arrives (bounded wait, not a hang).
    #[test]
    fn blocking_times_out_when_idle() {
        crate::test_home::isolate();
        let id = unique_id("test-block-idle");
        let listener = PollListener::bind_blocking(&id, Duration::from_millis(100)).unwrap();
        let start = Instant::now();
        assert!(listener.check_message_blocking().is_none());
        assert!(start.elapsed() < Duration::from_millis(400));
    }

    // [unit->REQ-MSG-1] a blocking listener accepts a message that arrives
    // within the poll window.
    #[test]
    fn blocking_accepts_within_window() {
        crate::test_home::isolate();
        let id = unique_id("test-block-recv");
        let listener = PollListener::bind_blocking(&id, Duration::from_millis(500)).unwrap();
        let addr = listener.local_addr().unwrap();

        let h = std::thread::spawn(move || {
            std::thread::sleep(Duration::from_millis(30));
            let mut c = TcpStream::connect(addr).unwrap();
            wire::write_message(&mut c, "", "pong").unwrap();
        });

        let got = listener.check_message_blocking();
        h.join().unwrap();
        assert_eq!(got.as_deref(), Some("pong\n"));
    }
}


listener.rs: `check_message` returns the raw frame string. Change to structural `Option<(String,String)>`. Find its callers first.


crates\spt-msg\src\ring.rs:101:        if let Some(msg) = reply_listener.check_message_blocking() {
crates\spt-msg\src\deliver.rs:219:            if let Some(m) = listener.check_message() {
crates\spt-msg\src\ready.rs:129:        self.listener.check_message_blocking()
crates\spt-msg\src\listener.rs:87:    /// uses an OS poll with the given timeout (see [`Self::check_message_blocking`]).
crates\spt-msg\src\listener.rs:129:    pub fn check_message(&self) -> Option<String> {
crates\spt-msg\src\listener.rs:139:    pub fn check_message_blocking(&self) -> Option<String> {
crates\spt-msg\src\listener.rs:227:            if let Some(m) = listener.check_message() {
crates\spt-msg\src\listener.rs:258:        assert!(listener.check_message_blocking().is_none());
crates\spt-msg\src\listener.rs:277:        let got = listener.check_message_blocking();
crates\spt\tests\shell_e2e.rs:235:        if let Some(m) = listener.check_message() {
crates\spt\src\cli.rs:7707:            if let Some(m) = listener.check_message() {
crates\spt-daemon\src\relay.rs:74:        if let Some(msg) = self.listener.check_message_blocking() {
crates\spt\src\api\startup.rs:326:        if let Some(msg) = listener.check_message_blocking() {
crates\spt\src\api\startup.rs:336:        if let Some(msg) = listener.check_message_blocking() {


check_message → `Option<(String,String)>`. Update listener.rs + its tests.


    /// Try to accept one pending connection and read its framed message.
    /// Returns `Some(msg)` if one was waiting, `None` otherwise (no panic).
    pub fn check_message(&self) -> Option<String> {
        match self.listener.accept() {
            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,
            Err(_) => None,
        }
    }

    /// Wait (up to the configured timeout, default 500 ms) for a connection via
    /// the OS poll syscall, then accept+read one framed message.
    pub fn check_message_blocking(&self) -> Option<String> {
        let timeout_ms = self.timeout_ms.unwrap_or(500);
        if poll_ready(&self.listener, timeout_ms) {
            match self.listener.accept() {
                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
                Err(_) => None,
            }
        } else {
            None
        }
    }

    /// Try to accept one pending connection and read its framed message.
    /// Returns `Some((from, body))` if one was waiting, `None` otherwise (no
    /// panic). The structural pair — the delivery edge composes the `<EVENT>`.
    pub fn check_message(&self) -> Option<(String, String)> {
        match self.listener.accept() {
            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,
            Err(_) => None,
        }
    }

    /// Wait (up to the configured timeout, default 500 ms) for a connection via
    /// the OS poll syscall, then accept+read one framed message as `(from, body)`.
    pub fn check_message_blocking(&self) -> Option<(String, String)> {
        let timeout_ms = self.timeout_ms.unwrap_or(500);
        if poll_ready(&self.listener, timeout_ms) {
            match self.listener.accept() {
                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
                Err(_) => None,
            }
        } else {
            None
        }
    }


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

    /// Try to accept one pending connection and read its framed message.
    /// Returns `Some(msg)` if one was waiting, `None` otherwise (no panic).
    pub fn check_message(&self) -> Option<String> {
        match self.listener.accept() {
            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,
            Err(_) => None,
        }
    }

    /// Wait (up to the configured timeout, default 500 ms) for a connection via
    /// the OS poll syscall, then accept+read one framed message.
    pub fn check_message_blocking(&self) -> Option<String> {
        let timeout_ms = self.timeout_ms.unwrap_or(500);
        if poll_ready(&self.listener, timeout_ms) {
            match self.listener.accept() {
                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
                Err(_) => None,
            }
        } else {
            None
        }
    }

    /// Try to accept one pending connection and read its framed message.
    /// Returns `Some((from, body))` if one was waiting, `None` otherwise (no
    /// panic). The structural pair — the delivery edge composes the `<EVENT>`.
    pub fn check_message(&self) -> Option<(String, String)> {
        match self.listener.accept() {
            Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
            Err(ref e) if e.kind() == ErrorKind::WouldBlock => None,
            Err(_) => None,
        }
    }

    /// Wait (up to the configured timeout, default 500 ms) for a connection via
    /// the OS poll syscall, then accept+read one framed message as `(from, body)`.
    pub fn check_message_blocking(&self) -> Option<(String, String)> {
        let timeout_ms = self.timeout_ms.unwrap_or(500);
        if poll_ready(&self.listener, timeout_ms) {
            match self.listener.accept() {
                Ok((mut stream, _)) => wire::read_message(&mut stream).ok(),
                Err(_) => None,
            }
        } else {
            None
        }
    }

-    /// Returns `Some(msg)` if one was waiting, `None` otherwise (no panic).

-    pub fn check_message(&self) -> Option<String> {

+    /// Returns `Some((from, body))` if one was waiting, `None` otherwise (no

+    /// panic). The structural pair — the delivery edge composes the `<EVENT>`.

+    pub fn check_message(&self) -> Option<(String, String)> {

-    /// the OS poll syscall, then accept+read one framed message.

-    pub fn check_message_blocking(&self) -> Option<String> {

+    /// the OS poll syscall, then accept+read one framed message as `(from, body)`.

+    pub fn check_message_blocking(&self) -> Option<(String, String)> {


        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nping\n"));
    }

        assert_eq!(got, Some(("sender".to_string(), "ping".to_string())));
    }


        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nping\n"));
    }

        assert_eq!(got, Some(("sender".to_string(), "ping".to_string())));
    }

-        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nping\n"));

+        assert_eq!(got, Some(("sender".to_string(), "ping".to_string())));

// [unit->REQ-MSG-1] dropping a listener removes its registry address (no


        let got = listener.check_message_blocking();
        h.join().unwrap();
        assert_eq!(got.as_deref(), Some("pong\n"));
    }

        let got = listener.check_message_blocking();
        h.join().unwrap();
        assert_eq!(got, Some(("".to_string(), "pong".to_string())));
    }


        let got = listener.check_message_blocking();
        h.join().unwrap();
        assert_eq!(got.as_deref(), Some("pong\n"));
    }

        let got = listener.check_message_blocking();
        h.join().unwrap();
        assert_eq!(got, Some(("".to_string(), "pong".to_string())));
    }

-        assert_eq!(got.as_deref(), Some("pong\n"));

+        assert_eq!(got, Some(("".to_string(), "pong".to_string())));


Now ready.rs, ring.rs, deliver.rs. Read them.