1	//! The thin, stateless in-session relay (B3) — the only residue of the interim
2	//! `api listen` runner that stays in the harness-hosted session (topology 1).
3	//!
4	//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,
5	//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What
6	//! remains running inside the agent's own session is just this relay: it forwards
7	//! inbound messages (the offline spool backlog + live poll deliveries) to a sink
8	//! (stdout for the real session) and holds **no state of its own**.
9	//!
10	//! Because it is stateless and every undelivered message is durable in the perch
11	//! spool, the relay is **freely killable and respawnable without losing state**:
12	//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it
13	//! on its next pump. That is the property the daemon needs — a routine that kills
14	//! the in-session relay (e.g. to swap it) never drops a message.
15	// [impl->REQ-DAEMON-1]
16
17	use std::path::{Path, PathBuf};
18	use std::sync::atomic::{AtomicBool, Ordering};
19	use std::time::Duration;
20
21	use spt_msg::listener::PollListener;
22	use spt_store::spool;
23
24	/// Poll window for the relay's blocking accept — bounds each cycle so the loop
25	/// stays responsive to a stop signal (mirrors the interim listener cadence).
26	pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);
27
28	/// A stateless message relay for one perch: a poll listener bound to the id plus
29	/// the perch path it drains. It carries no message buffer — every byte it
30	/// forwards comes straight from the live poll or the durable spool.
31	pub struct Relay {
32	    id: String,
33	    perch_path: PathBuf,
34	    listener: PollListener,
35	}
36
37	impl Relay {
38	    /// Bind a relay for `id` draining `perch_path`, with the default poll window.
39	    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {
40	        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)
41	    }
42
43	    /// Bind a relay with an explicit poll window.
44	    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {
45	        let listener = PollListener::bind_blocking(id, poll)
46	            .map_err(|e| std::io::Error::other(e.to_string()))?;
47	        Ok(Relay {
48	            id: id.to_string(),
49	            perch_path: perch_path.to_path_buf(),
50	            listener,
51	        })
52	    }
53
54	    /// The relayed endpoint id.
55	    pub fn id(&self) -> &str {
56	        &self.id
57	    }
58
59	    /// Drain the durable offline backlog (non-deferred spool rows) to `sink`.
60	    /// Idempotent across respawns: a fresh relay re-drains whatever a killed one
61	    /// left behind. Returns the number of messages forwarded.
62	    pub fn drain_backlog<F: FnMut(&str)>(&self, mut sink: F) -> usize {
63	        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
64	        let n = msgs.len();
65	        for m in &msgs {
66	            sink(m);
67	        }
68	        n
69	    }
70
71	    /// One relay cycle: forward a single live poll delivery (if one arrives in the
72	    /// poll window) then drain the offline backlog. Holds no state between calls.
73	    pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {
74	        if let Some(msg) = self.listener.check_message_blocking() {
75	            sink(&msg);
76	        }
77	        self.drain_backlog(&mut sink);
78	    }
79
80	    /// Run the relay until `stop` is set, forwarding every delivery to `sink`.
81	    /// The blocking poll bounds each iteration, so a kill signal is honored within
82	    /// one poll window. State loss is impossible: the loop buffers nothing.
83	    pub fn run<F: FnMut(&str)>(&self, stop: &AtomicBool, mut sink: F) {
84	        // Drain whatever a predecessor relay left behind before streaming live.
85	        self.drain_backlog(&mut sink);
86	        while !stop.load(Ordering::Relaxed) {
87	            self.pump_once(&mut sink);
88	        }
89	    }
90	}
91
92	#[cfg(test)]
93	mod tests {
94	    use super::*;
95	    use crate::test_home::with_home;
96	    use spt_store::perch::{resolve_perch_path, ParentHint};
97
98	    fn perch_for(id: &str) -> PathBuf {
99	        let p = resolve_perch_path(id, ParentHint::Infer);
100	        std::fs::create_dir_all(&p).unwrap();
101	        p
102	    }
103
104	    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.
105	    fn respawn_loses_no_spooled_message() {
106	        with_home(|_| {
107	            let perch = perch_for("doyle");
108	            spool::spool_message_at(&perch, "alice", "m1").unwrap();
109	            spool::spool_message_at(&perch, "alice", "m2").unwrap();
110
111	            // Relay A binds then is "killed" (dropped) WITHOUT pumping. The two
112	            // messages remain durable in the spool — the relay held no state.
113	            {
114	                let _a = Relay::bind("doyle", &perch).unwrap();
115	            }
116	            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 2);
117
118	            // Relay B (respawn) drains the full backlog — nothing was lost.
119	            let b = Relay::bind("doyle", &perch).unwrap();
120	            let mut got = Vec::new();
121	            let n = b.drain_backlog(|m| got.push(m.to_string()));
122	            assert_eq!(n, 2);
123	            assert!(got.iter().any(|m| m.contains("m1")));
124	            assert!(got.iter().any(|m| m.contains("m2")));
125	            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 0);
126	        });
127	    }
128
129	    #[test] // [unit->REQ-DAEMON-1] run() exits promptly when the stop flag is set.
130	    fn run_honors_stop() {
131	        with_home(|_| {
132	            let perch = perch_for("doyle");
133	            let relay = Relay::bind_with("doyle", &perch, Duration::from_millis(20)).unwrap();
134	            let stop = AtomicBool::new(true); // already stopped: drains once, returns.
135	            let mut got = Vec::new();
136	            spool::spool_message_at(&perch, "alice", "hi").unwrap();
137	            relay.run(&stop, |m| got.push(m.to_string()));
138	            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");
139	        });
140	    }
141	}
142

//! The thin, stateless in-session relay (B3) — the only residue of the interim
//! `api listen` runner that stays in the harness-hosted session (topology 1).
//!
//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,
//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What
//! remains running inside the agent's own session is just this relay: it forwards
//! inbound messages (the offline spool backlog + live poll deliveries) to a sink
//! (stdout for the real session) and holds **no state of its own**.
//!
//! Because it is stateless and every undelivered message is durable in the perch
//! spool, the relay is **freely killable and respawnable without losing state**:
//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it
//! on its next pump. That is the property the daemon needs — a routine that kills
//! the in-session relay (e.g. to swap it) never drops a message.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

use spt_msg::listener::PollListener;
use spt_store::spool;

/// Poll window for the relay's blocking accept — bounds each cycle so the loop
/// stays responsive to a stop signal (mirrors the interim listener cadence).
pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);

/// A stateless message relay for one perch: a poll listener bound to the id plus
/// the perch path it drains. It carries no message buffer — every byte it
/// forwards comes straight from the live poll or the durable spool.
pub struct Relay {
    id: String,
    perch_path: PathBuf,
    listener: PollListener,
}

impl Relay {
    /// Bind a relay for `id` draining `perch_path`, with the default poll window.
    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {
        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)
    }

    /// Bind a relay with an explicit poll window.
    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {
        let listener = PollListener::bind_blocking(id, poll)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        Ok(Relay {
            id: id.to_string(),
            perch_path: perch_path.to_path_buf(),
            listener,
        })
    }

    /// The relayed endpoint id.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink`.
    /// Idempotent across respawns: a fresh relay re-drains whatever a killed one
    /// left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(m);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {
        if let Some(msg) = self.listener.check_message_blocking() {
            sink(&msg);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink`.
    /// The blocking poll bounds each iteration, so a kill signal is honored within
    /// one poll window. State loss is impossible: the loop buffers nothing.
    pub fn run<F: FnMut(&str)>(&self, stop: &AtomicBool, mut sink: F) {
        // Drain whatever a predecessor relay left behind before streaming live.
        self.drain_backlog(&mut sink);
        while !stop.load(Ordering::Relaxed) {
            self.pump_once(&mut sink);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::perch::{resolve_perch_path, ParentHint};

    fn perch_for(id: &str) -> PathBuf {
        let p = resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&p).unwrap();
        p
    }

    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.
    fn respawn_loses_no_spooled_message() {
        with_home(|_| {
            let perch = perch_for("doyle");
            spool::spool_message_at(&perch, "alice", "m1").unwrap();
            spool::spool_message_at(&perch, "alice", "m2").unwrap();

            // Relay A binds then is "killed" (dropped) WITHOUT pumping. The two
            // messages remain durable in the spool — the relay held no state.
            {
                let _a = Relay::bind("doyle", &perch).unwrap();
            }
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 2);

            // Relay B (respawn) drains the full backlog — nothing was lost.
            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|m| got.push(m.to_string()));
            assert_eq!(n, 2);
            assert!(got.iter().any(|m| m.contains("m1")));
            assert!(got.iter().any(|m| m.contains("m2")));
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 0);
        });
    }

    #[test] // [unit->REQ-DAEMON-1] run() exits promptly when the stop flag is set.
    fn run_honors_stop() {
        with_home(|_| {
            let perch = perch_for("doyle");
            let relay = Relay::bind_with("doyle", &perch, Duration::from_millis(20)).unwrap();
            let stop = AtomicBool::new(true); // already stopped: drains once, returns.
            let mut got = Vec::new();
            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |m| got.push(m.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");
        });
    }
}


relay.rs sink forwards messages. Check who calls Relay to pick the sink shape.


crates\spt-runtime\src\manifest.rs:364:    Relay,
crates\spt-runtime\src\manifest.rs:517:    Relay,
crates\spt-live\src\outbound.rs:27:    /// Relay to the `__REPLY_TO__` sender of the inbound being answered.
crates\spt-live\src\outbound.rs:29:    /// Relay to the agent's own user/subnet only.
crates\spt-daemon\src\lib.rs:33://! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
crates\spt-daemon\src\lib.rs:214:pub use relay::Relay;
crates\spt-net\src\net\endpoint.rs:9://! Relay policy ([`RelayPolicy`]) is the ADR-0002 v1 relay stance made concrete:
crates\spt-daemon\src\psyrelay.rs:89:/// Relay parsed Psyche intents through the sanitize boundary (see module
crates\spt-daemon\src\nethost.rs:63:    /// Relay stance (ADR-0002): n0 default / self-host / disabled.


Relay has no live external sink caller (only tests). Make the sink structural `FnMut(&str, &str)` for ADR-0020 consistency. Update relay.rs.


Edit

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink`.
    /// Idempotent across respawns: a fresh relay re-drains whatever a killed one
    /// left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(m);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {
        if let Some(msg) = self.listener.check_message_blocking() {
            sink(&msg);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink`.
    /// The blocking poll bounds each iteration, so a kill signal is honored within
    /// one poll window. State loss is impossible: the loop buffers nothing.
    pub fn run<F: FnMut(&str)>(&self, stop: &AtomicBool, mut sink: F) {

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as
    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,
    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a
    /// killed one left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(&m.from, &m.body);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {
        if let Some((from, body)) = self.listener.check_message_blocking() {
            sink(&from, &body);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as
    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal
    /// is honored within one poll window. State loss is impossible: the loop
    /// buffers nothing.
    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {


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

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as
    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,
    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a
    /// killed one left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(&m.from, &m.body);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {
        if let Some((from, body)) = self.listener.check_message_blocking() {
            sink(&from, &body);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as
    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal
    /// is honored within one poll window. State loss is impossible: the loop
    /// buffers nothing.
    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {

//! The thin, stateless in-session relay (B3) — the only residue of the interim
//! `api listen` runner that stays in the harness-hosted session (topology 1).
//!
//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,
//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What
//! remains running inside the agent's own session is just this relay: it forwards
//! inbound messages (the offline spool backlog + live poll deliveries) to a sink
//! (stdout for the real session) and holds **no state of its own**.
//!
//! Because it is stateless and every undelivered message is durable in the perch
//! spool, the relay is **freely killable and respawnable without losing state**:
//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it
//! on its next pump. That is the property the daemon needs — a routine that kills
//! the in-session relay (e.g. to swap it) never drops a message.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

use spt_msg::listener::PollListener;
use spt_store::spool;

/// Poll window for the relay's blocking accept — bounds each cycle so the loop
/// stays responsive to a stop signal (mirrors the interim listener cadence).
pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);

/// A stateless message relay for one perch: a poll listener bound to the id plus
/// the perch path it drains. It carries no message buffer — every byte it
/// forwards comes straight from the live poll or the durable spool.
pub struct Relay {
    id: String,
    perch_path: PathBuf,
    listener: PollListener,
}

impl Relay {
    /// Bind a relay for `id` draining `perch_path`, with the default poll window.
    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {
        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)
    }

    /// Bind a relay with an explicit poll window.
    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {
        let listener = PollListener::bind_blocking(id, poll)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        Ok(Relay {
            id: id.to_string(),
            perch_path: perch_path.to_path_buf(),
            listener,
        })
    }

    /// The relayed endpoint id.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink`.
    /// Idempotent across respawns: a fresh relay re-drains whatever a killed one
    /// left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(m);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {
        if let Some(msg) = self.listener.check_message_blocking() {
            sink(&msg);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink`.
    /// The blocking poll bounds each iteration, so a kill signal is honored within
    /// one poll window. State loss is impossible: the loop buffers nothing.
    pub fn run<F: FnMut(&str)>(&self, stop: &AtomicBool, mut sink: F) {
        // Drain whatever a predecessor relay left behind before streaming live.
        self.drain_backlog(&mut sink);
        while !stop.load(Ordering::Relaxed) {
            self.pump_once(&mut sink);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::perch::{resolve_perch_path, ParentHint};

    fn perch_for(id: &str) -> PathBuf {
        let p = resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&p).unwrap();
        p
    }

    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.
    fn respawn_loses_no_spooled_message() {
        with_home(|_| {
            let perch = perch_for("doyle");
            spool::spool_message_at(&perch, "alice", "m1").unwrap();
            spool::spool_message_at(&perch, "alice", "m2").unwrap();

            // Relay A binds then is "killed" (dropped) WITHOUT pumping. The two
            // messages remain durable in the spool — the relay held no state.
            {
                let _a = Relay::bind("doyle", &perch).unwrap();
            }
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 2);

            // Relay B (respawn) drains the full backlog — nothing was lost.
            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|m| got.push(m.to_string()));
            assert_eq!(n, 2);
            assert!(got.iter().any(|m| m.contains("m1")));
            assert!(got.iter().any(|m| m.contains("m2")));
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 0);
        });
    }

    #[test] // [unit->REQ-DAEMON-1] run() exits promptly when the stop flag is set.
    fn run_honors_stop() {
        with_home(|_| {
            let perch = perch_for("doyle");
            let relay = Relay::bind_with("doyle", &perch, Duration::from_millis(20)).unwrap();
            let stop = AtomicBool::new(true); // already stopped: drains once, returns.
            let mut got = Vec::new();
            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |m| got.push(m.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");
        });
    }
}

-    /// Drain the durable offline backlog (non-deferred spool rows) to `sink`.

-    /// Idempotent across respawns: a fresh relay re-drains whatever a killed one

-    /// left behind. Returns the number of messages forwarded.

-    pub fn drain_backlog<F: FnMut(&str)>(&self, mut sink: F) -> usize {

+    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as

+    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,

+    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a

+    /// killed one left behind. Returns the number of messages forwarded.

+    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {

-    pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {

-        if let Some(msg) = self.listener.check_message_blocking() {

+    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {

+        if let Some((from, body)) = self.listener.check_message_blocking() {

-    /// Run the relay until `stop` is set, forwarding every delivery to `sink`.

-    /// The blocking poll bounds each iteration, so a kill signal is honored within

-    /// one poll window. State loss is impossible: the loop buffers nothing.

-    pub fn run<F: FnMut(&str)>(&self, stop: &AtomicBool, mut sink: F) {

+    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as

+    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal

+    /// is honored within one poll window. State loss is impossible: the loop

+    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {


            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|m| got.push(m.to_string()));
            assert_eq!(n, 2);
            assert!(got.iter().any(|m| m.contains("m1")));
            assert!(got.iter().any(|m| m.contains("m2")));

            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));
            assert_eq!(n, 2);
            assert!(got.iter().all(|(f, _)| f == "alice"));
            assert!(got.iter().any(|(_, b)| b.contains("m1")));
            assert!(got.iter().any(|(_, b)| b.contains("m2")));


            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));
            assert_eq!(n, 2);
            assert!(got.iter().all(|(f, _)| f == "alice"));
            assert!(got.iter().any(|(_, b)| b.contains("m1")));
            assert!(got.iter().any(|(_, b)| b.contains("m2")));

//! The thin, stateless in-session relay (B3) — the only residue of the interim
//! `api listen` runner that stays in the harness-hosted session (topology 1).
//!
//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,
//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What
//! remains running inside the agent's own session is just this relay: it forwards
//! inbound messages (the offline spool backlog + live poll deliveries) to a sink
//! (stdout for the real session) and holds **no state of its own**.
//!
//! Because it is stateless and every undelivered message is durable in the perch
//! spool, the relay is **freely killable and respawnable without losing state**:
//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it
//! on its next pump. That is the property the daemon needs — a routine that kills
//! the in-session relay (e.g. to swap it) never drops a message.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

use spt_msg::listener::PollListener;
use spt_store::spool;

/// Poll window for the relay's blocking accept — bounds each cycle so the loop
/// stays responsive to a stop signal (mirrors the interim listener cadence).
pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);

/// A stateless message relay for one perch: a poll listener bound to the id plus
/// the perch path it drains. It carries no message buffer — every byte it
/// forwards comes straight from the live poll or the durable spool.
pub struct Relay {
    id: String,
    perch_path: PathBuf,
    listener: PollListener,
}

impl Relay {
    /// Bind a relay for `id` draining `perch_path`, with the default poll window.
    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {
        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)
    }

    /// Bind a relay with an explicit poll window.
    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {
        let listener = PollListener::bind_blocking(id, poll)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        Ok(Relay {
            id: id.to_string(),
            perch_path: perch_path.to_path_buf(),
            listener,
        })
    }

    /// The relayed endpoint id.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as
    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,
    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a
    /// killed one left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(&m.from, &m.body);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {
        if let Some((from, body)) = self.listener.check_message_blocking() {
            sink(&from, &body);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as
    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal
    /// is honored within one poll window. State loss is impossible: the loop
    /// buffers nothing.
    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {
        // Drain whatever a predecessor relay left behind before streaming live.
        self.drain_backlog(&mut sink);
        while !stop.load(Ordering::Relaxed) {
            self.pump_once(&mut sink);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::perch::{resolve_perch_path, ParentHint};

    fn perch_for(id: &str) -> PathBuf {
        let p = resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&p).unwrap();
        p
    }

    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.
    fn respawn_loses_no_spooled_message() {
        with_home(|_| {
            let perch = perch_for("doyle");
            spool::spool_message_at(&perch, "alice", "m1").unwrap();
            spool::spool_message_at(&perch, "alice", "m2").unwrap();

            // Relay A binds then is "killed" (dropped) WITHOUT pumping. The two
            // messages remain durable in the spool — the relay held no state.
            {
                let _a = Relay::bind("doyle", &perch).unwrap();
            }
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 2);

            // Relay B (respawn) drains the full backlog — nothing was lost.
            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|m| got.push(m.to_string()));
            assert_eq!(n, 2);
            assert!(got.iter().any(|m| m.contains("m1")));
            assert!(got.iter().any(|m| m.contains("m2")));
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 0);
        });
    }

    #[test] // [unit->REQ-DAEMON-1] run() exits promptly when the stop flag is set.
    fn run_honors_stop() {
        with_home(|_| {
            let perch = perch_for("doyle");
            let relay = Relay::bind_with("doyle", &perch, Duration::from_millis(20)).unwrap();
            let stop = AtomicBool::new(true); // already stopped: drains once, returns.
            let mut got = Vec::new();
            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |m| got.push(m.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");
        });
    }
}

-            let n = b.drain_backlog(|m| got.push(m.to_string()));

+            let n = b.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));

-            assert!(got.iter().any(|m| m.contains("m1")));

-            assert!(got.iter().any(|m| m.contains("m2")));

+            assert!(got.iter().all(|(f, _)| f == "alice"));

+            assert!(got.iter().any(|(_, b)| b.contains("m1")));

+            assert!(got.iter().any(|(_, b)| b.contains("m2")));


            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |m| got.push(m.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");

            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |_from, body| got.push(body.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");


            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |_from, body| got.push(body.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");

//! The thin, stateless in-session relay (B3) — the only residue of the interim
//! `api listen` runner that stays in the harness-hosted session (topology 1).
//!
//! Consolidation (REQ-DAEMON-1) moves the *lifecycle* — Psyche spawn, pulse,
//! echo, drop-ingest — into the daemon brain as scheduled in-process loops. What
//! remains running inside the agent's own session is just this relay: it forwards
//! inbound messages (the offline spool backlog + live poll deliveries) to a sink
//! (stdout for the real session) and holds **no state of its own**.
//!
//! Because it is stateless and every undelivered message is durable in the perch
//! spool, the relay is **freely killable and respawnable without losing state**:
//! a killed relay leaves its backlog in the spool, and a fresh relay re-drains it
//! on its next pump. That is the property the daemon needs — a routine that kills
//! the in-session relay (e.g. to swap it) never drops a message.
// [impl->REQ-DAEMON-1]

use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;

use spt_msg::listener::PollListener;
use spt_store::spool;

/// Poll window for the relay's blocking accept — bounds each cycle so the loop
/// stays responsive to a stop signal (mirrors the interim listener cadence).
pub const DEFAULT_RELAY_POLL: Duration = Duration::from_millis(200);

/// A stateless message relay for one perch: a poll listener bound to the id plus
/// the perch path it drains. It carries no message buffer — every byte it
/// forwards comes straight from the live poll or the durable spool.
pub struct Relay {
    id: String,
    perch_path: PathBuf,
    listener: PollListener,
}

impl Relay {
    /// Bind a relay for `id` draining `perch_path`, with the default poll window.
    pub fn bind(id: &str, perch_path: &Path) -> std::io::Result<Self> {
        Self::bind_with(id, perch_path, DEFAULT_RELAY_POLL)
    }

    /// Bind a relay with an explicit poll window.
    pub fn bind_with(id: &str, perch_path: &Path, poll: Duration) -> std::io::Result<Self> {
        let listener = PollListener::bind_blocking(id, poll)
            .map_err(|e| std::io::Error::other(e.to_string()))?;
        Ok(Relay {
            id: id.to_string(),
            perch_path: perch_path.to_path_buf(),
            listener,
        })
    }

    /// The relayed endpoint id.
    pub fn id(&self) -> &str {
        &self.id
    }

    /// Drain the durable offline backlog (non-deferred spool rows) to `sink` as
    /// structural `(from, body)` (the sink composes the `<EVENT>` at its edge,
    /// ADR-0020). Idempotent across respawns: a fresh relay re-drains whatever a
    /// killed one left behind. Returns the number of messages forwarded.
    pub fn drain_backlog<F: FnMut(&str, &str)>(&self, mut sink: F) -> usize {
        let msgs = spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default();
        let n = msgs.len();
        for m in &msgs {
            sink(&m.from, &m.body);
        }
        n
    }

    /// One relay cycle: forward a single live poll delivery (if one arrives in the
    /// poll window) then drain the offline backlog. Holds no state between calls.
    pub fn pump_once<F: FnMut(&str, &str)>(&self, mut sink: F) {
        if let Some((from, body)) = self.listener.check_message_blocking() {
            sink(&from, &body);
        }
        self.drain_backlog(&mut sink);
    }

    /// Run the relay until `stop` is set, forwarding every delivery to `sink` as
    /// `(from, body)`. The blocking poll bounds each iteration, so a kill signal
    /// is honored within one poll window. State loss is impossible: the loop
    /// buffers nothing.
    pub fn run<F: FnMut(&str, &str)>(&self, stop: &AtomicBool, mut sink: F) {
        // Drain whatever a predecessor relay left behind before streaming live.
        self.drain_backlog(&mut sink);
        while !stop.load(Ordering::Relaxed) {
            self.pump_once(&mut sink);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::perch::{resolve_perch_path, ParentHint};

    fn perch_for(id: &str) -> PathBuf {
        let p = resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&p).unwrap();
        p
    }

    #[test] // [unit->REQ-DAEMON-1] a respawned relay re-drains a killed relay's backlog — no loss.
    fn respawn_loses_no_spooled_message() {
        with_home(|_| {
            let perch = perch_for("doyle");
            spool::spool_message_at(&perch, "alice", "m1").unwrap();
            spool::spool_message_at(&perch, "alice", "m2").unwrap();

            // Relay A binds then is "killed" (dropped) WITHOUT pumping. The two
            // messages remain durable in the spool — the relay held no state.
            {
                let _a = Relay::bind("doyle", &perch).unwrap();
            }
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 2);

            // Relay B (respawn) drains the full backlog — nothing was lost.
            let b = Relay::bind("doyle", &perch).unwrap();
            let mut got = Vec::new();
            let n = b.drain_backlog(|from, body| got.push((from.to_string(), body.to_string())));
            assert_eq!(n, 2);
            assert!(got.iter().all(|(f, _)| f == "alice"));
            assert!(got.iter().any(|(_, b)| b.contains("m1")));
            assert!(got.iter().any(|(_, b)| b.contains("m2")));
            assert_eq!(spool::pending_count_non_deferred_at(&perch).unwrap(), 0);
        });
    }

    #[test] // [unit->REQ-DAEMON-1] run() exits promptly when the stop flag is set.
    fn run_honors_stop() {
        with_home(|_| {
            let perch = perch_for("doyle");
            let relay = Relay::bind_with("doyle", &perch, Duration::from_millis(20)).unwrap();
            let stop = AtomicBool::new(true); // already stopped: drains once, returns.
            let mut got = Vec::new();
            spool::spool_message_at(&perch, "alice", "hi").unwrap();
            relay.run(&stop, |m| got.push(m.to_string()));
            assert_eq!(got.len(), 1, "the pre-stream backlog drain still fires");
        });
    }
}

-            relay.run(&stop, |m| got.push(m.to_string()));

+            relay.run(&stop, |_from, body| got.push(body.to_string()));


Now notif.rs — the critical T4 double-prefix drop. Find the `__REPLY_TO__` wrapping + how notifs are spooled.


25-//!
26-\! Delivery is the existing msg substrate: a **daemon-authored** typed
27-//! `<EVENT type="notify" from=… notif_id=… subnet=…>` envelope
28://! (`compose_typed_event`, 4.1 codec — typed envelopes pass through delivery
29-\! verbatim) via `spt_msg::deliver::send` (TCP-first, spool-fallback).
30-// [impl->REQ-NOTIF-1]
31-
32-use std::path::Path;
33-
34-use spt_msg::deliver::{send, SendOutcome};
35:use spt_proto::event::{compose_typed_event, EVENT_TYPE_NOTIFY};
36-use spt_store::epoch::EpochSource;
37-use spt_store::info::read_info;
38-use spt_store::liveness::is_perch_alive;
--
168-/// notif crosses a session boundary in — 4.1 codec, typed, verbatim through
169-/// delivery).
170-fn notify_envelope(row: &NotifRow) -> String {
171:    compose_typed_event(
172-        EVENT_TYPE_NOTIFY,
173-        &[
174-            ("from", &row.from_id),
--
239-    let delivery = send(&local.id, &row.from_id, &notify_envelope(row), owlery);
240-    store
241-        .mark_seen(&row.notif_id, &local.id)
242:        .map_err(|e| format!("mark seen: {e}"))?;
243-    store
244-        .touch_surfaced(&row.notif_id, now_ms)
245:        .map_err(|e| format!("touch surfaced: {e}"))?;
246-    \ D8b: the surface also renders endpoint-native via the winner's shell
247-    // adapters' [session.notif] templates — fire-and-forget.
248-    let _ = render_via_shell_notif_templates(owlery, &local.id, row);
--
271-) -> Result<(NotifRow, FirstFireOutcome), String> {
272-    let row = store
273-        .produce(node_hex, epochs, subnet, kind, from_id, body)
274:        .map_err(|e| format!("notif produce: {e}"))?;
275-    let fired = first_fire(store, &row, policy, owlery, now_ms)?;
276-    Ok((row, fired))
277-}
--
391-            // update worker resolves `product_version` vs the monotonic counter
392-            // so the operator never sees a bare counter (the "update-available
393-            // 6" confusion).
394:            let body = format!(
395-                "An spt-core update {version} is available.\n\
396-                 The changelog can be viewed at: {CONSENT_CHANGELOG_URL}\n\
397-                 Run spt update apply to install."
--
435-    now_ms: u64,
436-) -> Result<(NotifRow, FirstFireOutcome), String> {
437-    // Real newline — the 4.1 codec turns it into `<br>` at compose.
438:    let body = format!(
439-        "update v{quarantine_version} failed to come up — rolled back to v{running_version}, quarantined\n\
440-         the daemon is healthy on the previous version (auto-rollback)"
441-    );
--
489-    for (subnet, _) in &policy.subnets {
490-        let rows = store

[Showing results with pagination = limit: 60]


605-            let rows = spool::drain_all_at(&ling).unwrap();
606-            assert_eq!(
607-                rows,
608-                vec![format!(
609:                    "__REPLY_TO__:issuer\n<EVENT type=\"notify\" from=\"issuer\" \
610-                     notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
611-                    row.notif_id
612-                )],
613-                "daemon-authored typed envelope"


It's a test assertion (the typed-notify-through-spool proof). Read context to migrate.


588	                &mut e,
589	                "home",
590	                NOTIF_KIND_AGENT,
591	                "issuer",
592	                "build done",
593	                &home_policy(),
594	                &owlery,
595	                50_000,
596	            )
597	            .unwrap();
598	            assert_eq!(
599	                fired,
600	                FirstFireOutcome::Fired {
601	                    to: "ling".into(),
602	                    delivery: SendOutcome::Queued
603	                }
604	            );
605	            let rows = spool::drain_all_at(&ling).unwrap();
606	            assert_eq!(
607	                rows,
608	                vec![format!(
609	                    "__REPLY_TO__:issuer\n<EVENT type=\"notify\" from=\"issuer\" \
610	                     notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
611	                    row.notif_id
612	                )],
613	                "daemon-authored typed envelope"
614	            );
615	            let stored = s.get(&row.notif_id).unwrap().unwrap();
616	            assert!(stored.seen.contains("ling"));
617	            assert_eq!(stored.last_surfaced_ms, 50_000);
618	        });
619	    }
620
621	    // [unit->REQ-NOTIF-1] a hidden-in-subnet endpoint is never the first-fire
622	    // target even when most recently active — the next visible one wins.
623	    #[test]
624	    fn hidden_endpoint_is_never_the_target() {
625	        with_home(|home| {
626	            let owlery = perch::owlery_dir();
627	            let doyle = seed_perch(&owlery, "doyle", STATUS_ONLINE, Some(100));

                &mut e,
                "home",
                NOTIF_KIND_AGENT,
                "issuer",
                "build done",
                &home_policy(),
                &owlery,
                50_000,
            )
            .unwrap();
            assert_eq!(
                fired,
                FirstFireOutcome::Fired {
                    to: "ling".into(),
                    delivery: SendOutcome::Queued
                }
            );
            let rows = spool::drain_all_at(&ling).unwrap();
            assert_eq!(
                rows,
                vec![format!(
                    "__REPLY_TO__:issuer\n<EVENT type=\"notify\" from=\"issuer\" \
                     notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
                    row.notif_id
                )],
                "daemon-authored typed envelope"
            );
            let stored = s.get(&row.notif_id).unwrap().unwrap();
            assert!(stored.seen.contains("ling"));
            assert_eq!(stored.last_surfaced_ms, 50_000);
        });
    }

    // [unit->REQ-NOTIF-1] a hidden-in-subnet endpoint is never the first-fire
    // target even when most recently active — the next visible one wins.
    #[test]
    fn hidden_endpoint_is_never_the_target() {
        with_home(|home| {
            let owlery = perch::owlery_dir();
            let doyle = seed_perch(&owlery, "doyle", STATUS_ONLINE, Some(100));


            let rows = spool::drain_all_at(&ling).unwrap();
            assert_eq!(
                rows,
                vec![format!(
                    "__REPLY_TO__:issuer\n<EVENT type=\"notify\" from=\"issuer\" \
                     notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
                    row.notif_id
                )],
                "daemon-authored typed envelope"
            );

            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
            // double-prefix. The delivery edge passes the typed body through.
            let expect_body = format!(
                "<EVENT type=\"notify\" from=\"issuer\" \
                 notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
                row.notif_id
            );
            assert_eq!(
                rows,
                vec![spool::DrainedMsg {
                    from: "issuer".to_string(),
                    body: expect_body.clone(),
                }],
                "daemon-authored typed envelope, carried structurally"
            );
            // Typed-passthrough at the delivery edge: the whole-render emits the
            // body verbatim (its own from wins; the structural from does not
            // double-wrap it).
            assert_eq!(
                spt_msg::emit::render_event_whole(&rows[0].from, &rows[0].body),
                expect_body,
                "typed notify body passes through verbatim — no double-wrap"
            );


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();
            assert_eq!(
                rows,
                vec![format!(
                    "__REPLY_TO__:issuer\n<EVENT type=\"notify\" from=\"issuer\" \
                     notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
                    row.notif_id
                )],
                "daemon-authored typed envelope"
            );

            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
            // double-prefix. The delivery edge passes the typed body through.
            let expect_body = format!(
                "<EVENT type=\"notify\" from=\"issuer\" \
                 notif_id=\"{}\" subnet=\"home\">build done</EVENT>",
                row.notif_id
            );
            assert_eq!(
                rows,
                vec![spool::DrainedMsg {
                    from: "issuer".to_string(),
                    body: expect_body.clone(),
                }],
                "daemon-authored typed envelope, carried structurally"
            );
            // Typed-passthrough at the delivery edge: the whole-render emits the
            // body verbatim (its own from wins; the structural from does not
            // double-wrap it).
            assert_eq!(
                spt_msg::emit::render_event_whole(&rows[0].from, &rows[0].body),
                expect_body,
                "typed notify body passes through verbatim — no double-wrap"
            );

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

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

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

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

+                 notif_id=\"{}\" subnet=\"home\">build done</EVENT>",

-                    "__REPLY_TO__:issuer\n<EVENT type=\"notify\" from=\"issuer\" \

-                     notif_id=\"{}\" subnet=\"home\">build done</EVENT>",

-                "daemon-authored typed envelope"

+                vec![spool::DrainedMsg {

+                    from: "issuer".to_string(),

+                    body: expect_body.clone(),

+                "daemon-authored typed envelope, carried structurally"

+            // Typed-passthrough at the delivery edge: the whole-render emits the

+            // body verbatim (its own from wins; the structural from does not

+                spt_msg::emit::render_event_whole(&rows[0].from, &rows[0].body),

+                "typed notify body passes through verbatim — no double-wrap"

let stored = s.get(&row.notif_id).unwrap().unwrap();


notif.rs done (other drain refs use `.len()`/`.is_empty()`). Now psyrelay.rs — reply-target + tests.


9-//!
10-\! - **re-stamp**: every relayed envelope carries the daemon-known
11-//!   `from=<psyche_id>` — the daemon is the sole trusted author of wire
12-//!   envelopes carrying a real `from=` (ADR-0012);
13://! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`
14-//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —
15-//!   never broadcast, never guessed); a `notify` reaches **only** the agent's
16-\!   own user, through the **notif primitive** (the D8c swap, ADR-0007): the
17-//!   relay produces a `psyche`-kind notif in the agent's own subnet and the
--
49-
50-/// What the relay did with one parsed intent — loud + typed, never silent.
51-#[derive(Debug, Clone, PartialEq, Eq)]
52-pub enum RelayOutcome {
53:    /// A `reply` relayed to the inbound's `__REPLY_TO__` sender.
54-    Replied { to: String, delivery: SendOutcome },
55-    /// A `notify` produced as a notif (ADR-0007) and first-fired to the
56-    /// engine-resolved endpoint (the user's most-recently-active visible
57-    /// surface in the agent's subnet).
--
87-}
88-
89-/// Relay parsed Psyche intents through the sanitize boundary (see module
90-/// docs). `psyche_id` is the daemon-known identity re-stamped as `from=` on
91:/// every relayed envelope; `reply_to` is the `__REPLY_TO__` sender of the
92-/// inbound being answered (`None` for a turn with no inbound sender — e.g.
93-/// alarm-fired); `notif` carries the notify leg's daemon-supplied scope (the
94-/// agent's own subnet — the only place a notify can land).
95-// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
--
300-                    delivery: SendOutcome::Queued
301-                }]
302-            );
303-            let rows = spool::drain_all_at(&alice).unwrap();
304:            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\non it".to_string()]);
305-        });
306-    }
307-
308-    #[test] \ [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] anti-spoof negative:
--
325-                }]
326-            );
327-            // The reply went ONLY to the inbound sender, stamped by the daemon.
328-            let rows = spool::drain_all_at(&alice).unwrap();
329:            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\npayload".to_string()]);
330-            assert!(rows[0].contains("doyle-psyche") && !rows[0].contains("evil-imposter"));
331-            // The Psyche-addressed target received nothing.
332-            assert!(spool::drain_all_at(&victim).unwrap().is_empty());
333-        });
--
358-            let rows = spool::drain_all_at(&doyle).unwrap();
359-            assert_eq!(rows.len(), 1);
360-            assert_eq!(
361-                rows[0],
362:                "__REPLY_TO__:doyle-psyche\n\
363-                 <EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
364-                 subnet=\"home\">build done<br>all green</EVENT>"
365-            );
366-            \ The notif row persists: seen by the fired target, undismissed.


psyrelay.rs: reword 3 comments (reply-target → inbound `from`) + migrate 3 test assertions.


//! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`
//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —

//! - **constrained routing**: a `reply` goes **only** to the **`from`** of the
//!   inbound being answered (no sender ⇒ dropped, fail-closed —


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


9	//!
10	//! - **re-stamp**: every relayed envelope carries the daemon-known
11	//!   `from=<psyche_id>` — the daemon is the sole trusted author of wire
12	//!   envelopes carrying a real `from=` (ADR-0012);
13	//! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`
14	//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —
15	//!   never broadcast, never guessed); a `notify` reaches **only** the agent's
16	//!   own user, through the **notif primitive** (the D8c swap, ADR-0007): the
17	//!   relay produces a `psyche`-kind notif in the agent's own subnet and the
18	//!   engine first-fires it — targeting is the engine's visible-in-subnet
19	//!   resolver, never anything the Psyche chose;
20	//! - **body validation**: the 4.1 codec at compose (`\n`→`<br>`, amp-last
21	//!   entities via `spt-proto`) plus a sanity cap bounding a runaway Psyche.
22	//!
23	//! Delivery itself is the existing REQ-MSG substrate ([`spt_msg::deliver`]:
24	//! TCP-first, spool-fallback) — nothing new on the wire.
25	//
26	// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
27
28	use std::collections::BTreeMap;
29	use std::path::Path;
30	use std::time::Duration;
31
32	use spt_live::outbound::{parse_psyche_intents, PsycheIntent};
33	use spt_live::turn::run_psyche_turn;
34	use spt_live::TurnError;
35	use spt_msg::deliver::{send, SendOutcome};
36	use spt_runtime::AgentRuntime;
37	use spt_store::epoch::EpochSource;
38	use spt_store::notif::NotifStore;
39
40	use crate::notif::{
41	    produce_and_first_fire, FirstFireOutcome, NotifSurfacePolicy, NOTIF_KIND_PSYCHE,
42	};
43
44	/// Sanity cap on one relayed intent body — bounds a runaway Psyche that
45	/// dumped its whole context into an envelope. Generous (the T3 chunker
46	/// handles legitimate long bodies at delivery); over-cap intents drop with a
47	/// typed outcome, never silently.
48	pub const PSYCHE_BODY_CAP: usize = 64 * 1024;
49
50	/// What the relay did with one parsed intent — loud + typed, never silent.
51	#[derive(Debug, Clone, PartialEq, Eq)]
52	pub enum RelayOutcome {
53	    /// A `reply` relayed to the inbound's `__REPLY_TO__` sender.
54	    Replied { to: String, delivery: SendOutcome },
55	    /// A `notify` produced as a notif (ADR-0007) and first-fired to the
56	    /// engine-resolved endpoint (the user's most-recently-active visible
57	    /// surface in the agent's subnet).
58	    Notified { to: String, delivery: SendOutcome },
59	    /// A `notify` produced as a notif with no live endpoint to fire at — it
60	    /// sits undismissed in the spool; the next reported boundary surfaces it
61	    /// (fail-open in time, never in targeting).
62	    NotifiedPending { notif_id: String },
63	    /// A `notify` that could not be persisted as a notif — dropped loudly.
64	    DroppedNotifError(String),
65	    /// A `reply` with no inbound sender to answer — dropped, fail-closed.
66	    DroppedNoReplyTarget,
67	    /// An empty/whitespace body — nothing to relay.
68	    DroppedEmptyBody,
69	    /// A body over [`PSYCHE_BODY_CAP`] — refused.
70	    DroppedOversize,
71	}
72
73	/// Everything the `notify` leg needs to produce + first-fire through the
74	/// notif primitive (the D8c swap): the canonical store handles plus this
75	/// agent's own subnet — the **only** subnet a Psyche's notify can land in
76	/// (ADR-0012's "own user/subnet" constraint, now enforced by construction:
77	/// the subnet is daemon-supplied, the Psyche's markup never names one).
78	pub struct PsycheNotifCtx<'a> {
79	    pub store: &'a NotifStore,
80	    pub epochs: &'a mut EpochSource,
81	    /// This node's identity hex — the notif-id mint prefix.
82	    pub node_hex: &'a str,
83	    /// The agent's own subnet (the notify scope).
84	    pub subnet: &'a str,
85	    pub policy: &'a NotifSurfacePolicy,
86	    pub now_ms: u64,
87	}
88
89	/// Relay parsed Psyche intents through the sanitize boundary (see module
90	/// docs). `psyche_id` is the daemon-known identity re-stamped as `from=` on
91	/// every relayed envelope; `reply_to` is the `__REPLY_TO__` sender of the
92	/// inbound being answered (`None` for a turn with no inbound sender — e.g.
93	/// alarm-fired); `notif` carries the notify leg's daemon-supplied scope (the
94	/// agent's own subnet — the only place a notify can land).
95	// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
96	// [impl->REQ-NOTIF-1]

//!
//! - **re-stamp**: every relayed envelope carries the daemon-known
//!   `from=<psyche_id>` — the daemon is the sole trusted author of wire
//!   envelopes carrying a real `from=` (ADR-0012);
//! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`
//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —
//!   never broadcast, never guessed); a `notify` reaches **only** the agent's
//!   own user, through the **notif primitive** (the D8c swap, ADR-0007): the
//!   relay produces a `psyche`-kind notif in the agent's own subnet and the
//!   engine first-fires it — targeting is the engine's visible-in-subnet
//!   resolver, never anything the Psyche chose;
//! - **body validation**: the 4.1 codec at compose (`\n`→`<br>`, amp-last
//!   entities via `spt-proto`) plus a sanity cap bounding a runaway Psyche.
//!
//! Delivery itself is the existing REQ-MSG substrate ([`spt_msg::deliver`]:
//! TCP-first, spool-fallback) — nothing new on the wire.
//
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]

use std::collections::BTreeMap;
use std::path::Path;
use std::time::Duration;

use spt_live::outbound::{parse_psyche_intents, PsycheIntent};
use spt_live::turn::run_psyche_turn;
use spt_live::TurnError;
use spt_msg::deliver::{send, SendOutcome};
use spt_runtime::AgentRuntime;
use spt_store::epoch::EpochSource;
use spt_store::notif::NotifStore;

use crate::notif::{
    produce_and_first_fire, FirstFireOutcome, NotifSurfacePolicy, NOTIF_KIND_PSYCHE,
};

/// Sanity cap on one relayed intent body — bounds a runaway Psyche that
/// dumped its whole context into an envelope. Generous (the T3 chunker
/// handles legitimate long bodies at delivery); over-cap intents drop with a
/// typed outcome, never silently.
pub const PSYCHE_BODY_CAP: usize = 64 * 1024;

/// What the relay did with one parsed intent — loud + typed, never silent.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelayOutcome {
    /// A `reply` relayed to the inbound's `__REPLY_TO__` sender.
    Replied { to: String, delivery: SendOutcome },
    /// A `notify` produced as a notif (ADR-0007) and first-fired to the
    /// engine-resolved endpoint (the user's most-recently-active visible
    /// surface in the agent's subnet).
    Notified { to: String, delivery: SendOutcome },
    /// A `notify` produced as a notif with no live endpoint to fire at — it
    /// sits undismissed in the spool; the next reported boundary surfaces it
    /// (fail-open in time, never in targeting).
    NotifiedPending { notif_id: String },
    /// A `notify` that could not be persisted as a notif — dropped loudly.
    DroppedNotifError(String),
    /// A `reply` with no inbound sender to answer — dropped, fail-closed.
    DroppedNoReplyTarget,
    /// An empty/whitespace body — nothing to relay.
    DroppedEmptyBody,
    /// A body over [`PSYCHE_BODY_CAP`] — refused.
    DroppedOversize,
}

/// Everything the `notify` leg needs to produce + first-fire through the
/// notif primitive (the D8c swap): the canonical store handles plus this
/// agent's own subnet — the **only** subnet a Psyche's notify can land in
/// (ADR-0012's "own user/subnet" constraint, now enforced by construction:
/// the subnet is daemon-supplied, the Psyche's markup never names one).
pub struct PsycheNotifCtx<'a> {
    pub store: &'a NotifStore,
    pub epochs: &'a mut EpochSource,
    /// This node's identity hex — the notif-id mint prefix.
    pub node_hex: &'a str,
    /// The agent's own subnet (the notify scope).
    pub subnet: &'a str,
    pub policy: &'a NotifSurfacePolicy,
    pub now_ms: u64,
}

/// Relay parsed Psyche intents through the sanitize boundary (see module
/// docs). `psyche_id` is the daemon-known identity re-stamped as `from=` on
/// every relayed envelope; `reply_to` is the `__REPLY_TO__` sender of the
/// inbound being answered (`None` for a turn with no inbound sender — e.g.
/// alarm-fired); `notif` carries the notify leg's daemon-supplied scope (the
/// agent's own subnet — the only place a notify can land).
// [impl->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY]
// [impl->REQ-NOTIF-1]


//! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`
//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —

//! - **constrained routing**: a `reply` goes **only** to the **`from`** of the
//!   inbound being answered (no sender ⇒ dropped, fail-closed —


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)

//! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`
//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —

//! - **constrained routing**: a `reply` goes **only** to the **`from`** of the
//!   inbound being answered (no sender ⇒ dropped, fail-closed —

//! - **re-stamp**: every relayed envelope carries the daemon-known

//!   `from=<psyche_id>` — the daemon is the sole trusted author of wire

//!   envelopes carrying a real `from=` (ADR-0012);

-//! - **constrained routing**: a `reply` goes **only** to the `__REPLY_TO__`

-//!   sender of the inbound being answered (no sender ⇒ dropped, fail-closed —

+//! - **constrained routing**: a `reply` goes **only** to the **`from`** of the

+//!   inbound being answered (no sender ⇒ dropped, fail-closed —

//!   never broadcast, never guessed); a `notify` reaches **only** the agent's

//!   own user, through the **notif primitive** (the D8c swap, ADR-0007): the

//!   relay produces a `psyche`-kind notif in the agent's own subnet and the


    /// A `reply` relayed to the inbound's `__REPLY_TO__` sender.
    Replied { to: String, delivery: SendOutcome },

    /// A `reply` relayed to the inbound's `from`.
    Replied { to: String, delivery: SendOutcome },


    /// A `reply` relayed to the inbound's `__REPLY_TO__` sender.
    Replied { to: String, delivery: SendOutcome },

    /// A `reply` relayed to the inbound's `from`.
    Replied { to: String, delivery: SendOutcome },

/// What the relay did with one parsed intent — loud + typed, never silent.

-    /// A `reply` relayed to the inbound's `__REPLY_TO__` sender.

+    /// A `reply` relayed to the inbound's `from`.

/// A `notify` produced as a notif (ADR-0007) and first-fired to the

/// engine-resolved endpoint (the user's most-recently-active visible


/// every relayed envelope; `reply_to` is the `__REPLY_TO__` sender of the
/// inbound being answered (`None` for a turn with no inbound sender — e.g.

/// every relayed envelope; `reply_to` is the `from` of the
/// inbound being answered (`None` for a turn with no inbound sender — e.g.


/// every relayed envelope; `reply_to` is the `__REPLY_TO__` sender of the
/// inbound being answered (`None` for a turn with no inbound sender — e.g.

/// every relayed envelope; `reply_to` is the `from` of the
/// inbound being answered (`None` for a turn with no inbound sender — e.g.

/// Relay parsed Psyche intents through the sanitize boundary (see module

/// docs). `psyche_id` is the daemon-known identity re-stamped as `from=` on

-/// every relayed envelope; `reply_to` is the `__REPLY_TO__` sender of the

+/// every relayed envelope; `reply_to` is the `from` of the

/// alarm-fired); `notif` carries the notify leg's daemon-supplied scope (the

/// agent's own subnet — the only place a notify can land).


Now psyrelay.rs test assertions. Read 295-370.


295	            let got = drive("<EVENT type=\"reply\">on it</EVENT>\n", Some("alice"));
296	            assert_eq!(
297	                got,
298	                vec![RelayOutcome::Replied {
299	                    to: "alice".to_string(),
300	                    delivery: SendOutcome::Queued
301	                }]
302	            );
303	            let rows = spool::drain_all_at(&alice).unwrap();
304	            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\non it".to_string()]);
305	        });
306	    }
307
308	    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] anti-spoof negative:
309	            // Psyche-supplied from=/to= never survive to the wire — the relayed
310	            // message carries the daemon-stamped from, and the spoof target gets
311	            // nothing.
312	    fn spoofed_routing_is_stripped_and_restamped() {
313	        with_home(|_| {
314	            let alice = seed_perch("alice");
315	            let victim = seed_perch("victim");
316	            let got = drive(
317	                "<EVENT type=\"reply\" from=\"evil-imposter\" to=\"victim\">payload</EVENT>\n",
318	                Some("alice"),
319	            );
320	            assert_eq!(
321	                got,
322	                vec![RelayOutcome::Replied {
323	                    to: "alice".to_string(),
324	                    delivery: SendOutcome::Queued
325	                }]
326	            );
327	            // The reply went ONLY to the inbound sender, stamped by the daemon.
328	            let rows = spool::drain_all_at(&alice).unwrap();
329	            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\npayload".to_string()]);
330	            assert!(rows[0].contains("doyle-psyche") && !rows[0].contains("evil-imposter"));
331	            // The Psyche-addressed target received nothing.
332	            assert!(spool::drain_all_at(&victim).unwrap().is_empty());
333	        });
334	    }
335
336	    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] the notify leg rides
337	            // the notif primitive (D8c): a daemon-minted notif in the agent's OWN
338	            // subnet, first-fired to the engine-resolved live endpoint as the typed
339	            // envelope with the re-stamped from — and the row persists (replicable,
340	            // resurfaceable) with the target marked seen.
341	            // [unit->REQ-NOTIF-1]
342	    fn notify_reaches_own_user_only() {
343	        with_home(|_| {
344	            let doyle = seed_live_perch("doyle", 500);
345	            let mut h = NotifHarness::new();
346	            let got = drive_with(
347	                &mut h,
348	                "<EVENT type=\"notify\">build done
all green</EVENT>\n",
349	                None,
350	            );
351	            assert_eq!(
352	                got,
353	                vec![RelayOutcome::Notified {
354	                    to: "doyle".to_string(),
355	                    delivery: SendOutcome::Queued
356	                }]
357	            );
358	            let rows = spool::drain_all_at(&doyle).unwrap();
359	            assert_eq!(rows.len(), 1);
360	            assert_eq!(
361	                rows[0],
362	                "__REPLY_TO__:doyle-psyche\n\
363	                 <EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
364	                 subnet=\"home\">build done
all green</EVENT>"
365	            );
366	            // The notif row persists: seen by the fired target, undismissed.
367	            let row = h.store.get("cafe:1").unwrap().unwrap();
368	            assert_eq!(row.kind, NOTIF_KIND_PSYCHE);
369	            assert!(row.seen.contains("doyle") && !row.dismissed);

let got = drive("<EVENT type=\"reply\">on it</EVENT>\n", Some("alice"));
            assert_eq!(
                got,
                vec![RelayOutcome::Replied {
                    to: "alice".to_string(),
                    delivery: SendOutcome::Queued
                }]
            );
            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\non it".to_string()]);
        });
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] anti-spoof negative:
            // Psyche-supplied from=/to= never survive to the wire — the relayed
            // message carries the daemon-stamped from, and the spoof target gets
            // nothing.
    fn spoofed_routing_is_stripped_and_restamped() {
        with_home(|_| {
            let alice = seed_perch("alice");
            let victim = seed_perch("victim");
            let got = drive(
                "<EVENT type=\"reply\" from=\"evil-imposter\" to=\"victim\">payload</EVENT>\n",
                Some("alice"),
            );
            assert_eq!(
                got,
                vec![RelayOutcome::Replied {
                    to: "alice".to_string(),
                    delivery: SendOutcome::Queued
                }]
            );
            // The reply went ONLY to the inbound sender, stamped by the daemon.
            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\npayload".to_string()]);
            assert!(rows[0].contains("doyle-psyche") && !rows[0].contains("evil-imposter"));
            // The Psyche-addressed target received nothing.
            assert!(spool::drain_all_at(&victim).unwrap().is_empty());
        });
    }

    #[test] // [unit->REQ-HAZARD-PSYCHE-OUTBOUND-PROXY] the notify leg rides
            // the notif primitive (D8c): a daemon-minted notif in the agent's OWN
            // subnet, first-fired to the engine-resolved live endpoint as the typed
            // envelope with the re-stamped from — and the row persists (replicable,
            // resurfaceable) with the target marked seen.
            // [unit->REQ-NOTIF-1]
    fn notify_reaches_own_user_only() {
        with_home(|_| {
            let doyle = seed_live_perch("doyle", 500);
            let mut h = NotifHarness::new();
            let got = drive_with(
                &mut h,
                "<EVENT type=\"notify\">build done
all green</EVENT>\n",
                None,
            );
            assert_eq!(
                got,
                vec![RelayOutcome::Notified {
                    to: "doyle".to_string(),
                    delivery: SendOutcome::Queued
                }]
            );
            let rows = spool::drain_all_at(&doyle).unwrap();
            assert_eq!(rows.len(), 1);
            assert_eq!(
                rows[0],
                "__REPLY_TO__:doyle-psyche\n\
                 <EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
                 subnet=\"home\">build done
all green</EVENT>"
            );
            // The notif row persists: seen by the fired target, undismissed.
            let row = h.store.get("cafe:1").unwrap().unwrap();
            assert_eq!(row.kind, NOTIF_KIND_PSYCHE);
            assert!(row.seen.contains("doyle") && !row.dismissed);


            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\non it".to_string()]);
        });
    }

            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(
                rows,
                vec![spool::DrainedMsg {
                    from: "doyle-psyche".to_string(),
                    body: "on it".to_string()
                }]
            );
        });
    }


            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\non it".to_string()]);
        });
    }

            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(
                rows,
                vec![spool::DrainedMsg {
                    from: "doyle-psyche".to_string(),
                    body: "on it".to_string()
                }]
            );
        });
    }

-            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\non it".to_string()]);

+                vec![spool::DrainedMsg {

+                    from: "doyle-psyche".to_string(),

+                    body: "on it".to_string()


            // The reply went ONLY to the inbound sender, stamped by the daemon.
            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\npayload".to_string()]);
            assert!(rows[0].contains("doyle-psyche") && !rows[0].contains("evil-imposter"));

            // The reply went ONLY to the inbound sender, stamped by the daemon:
            // the structural `from` is the re-stamped psyche id, never the spoof.
            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(
                rows,
                vec![spool::DrainedMsg {
                    from: "doyle-psyche".to_string(),
                    body: "payload".to_string()
                }]
            );
            assert_eq!(rows[0].from, "doyle-psyche");
            assert!(!rows[0].from.contains("evil-imposter") && !rows[0].body.contains("evil-imposter"));


            // The reply went ONLY to the inbound sender, stamped by the daemon.
            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\npayload".to_string()]);
            assert!(rows[0].contains("doyle-psyche") && !rows[0].contains("evil-imposter"));

            // The reply went ONLY to the inbound sender, stamped by the daemon:
            // the structural `from` is the re-stamped psyche id, never the spoof.
            let rows = spool::drain_all_at(&alice).unwrap();
            assert_eq!(
                rows,
                vec![spool::DrainedMsg {
                    from: "doyle-psyche".to_string(),
                    body: "payload".to_string()
                }]
            );
            assert_eq!(rows[0].from, "doyle-psyche");
            assert!(!rows[0].from.contains("evil-imposter") && !rows[0].body.contains("evil-imposter"));

-            // The reply went ONLY to the inbound sender, stamped by the daemon.

+            // The reply went ONLY to the inbound sender, stamped by the daemon:

+            // the structural `from` is the re-stamped psyche id, never the spoof.

-            assert_eq!(rows, vec!["__REPLY_TO__:doyle-psyche\npayload".to_string()]);

-            assert!(rows[0].contains("doyle-psyche") && !rows[0].contains("evil-imposter"));

+                    body: "payload".to_string()

+            assert_eq!(rows[0].from, "doyle-psyche");

+            assert!(!rows[0].from.contains("evil-imposter") && !rows[0].body.contains("evil-imposter"));

// The Psyche-addressed target received nothing.


            let rows = spool::drain_all_at(&doyle).unwrap();
            assert_eq!(rows.len(), 1);
            assert_eq!(
                rows[0],
                "__REPLY_TO__:doyle-psyche\n\
                 <EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
                 subnet=\"home\">build done<br>all green</EVENT>"
            );

            let rows = spool::drain_all_at(&doyle).unwrap();
            assert_eq!(rows.len(), 1);
            // Structural carriage: from = re-stamped psyche id, body = the typed
            // notify <EVENT> verbatim (no __REPLY_TO__ wrapper, no double-prefix).
            assert_eq!(
                rows[0],
                spool::DrainedMsg {
                    from: "doyle-psyche".to_string(),
                    body: "<EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
                           subnet=\"home\">build done<br>all green</EVENT>"
                        .to_string()
                }
            );


            let rows = spool::drain_all_at(&doyle).unwrap();
            assert_eq!(rows.len(), 1);
            assert_eq!(
                rows[0],
                "__REPLY_TO__:doyle-psyche\n\
                 <EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
                 subnet=\"home\">build done<br>all green</EVENT>"
            );

            let rows = spool::drain_all_at(&doyle).unwrap();
            assert_eq!(rows.len(), 1);
            // Structural carriage: from = re-stamped psyche id, body = the typed
            // notify <EVENT> verbatim (no __REPLY_TO__ wrapper, no double-prefix).
            assert_eq!(
                rows[0],
                spool::DrainedMsg {
                    from: "doyle-psyche".to_string(),
                    body: "<EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \
                           subnet=\"home\">build done<br>all green</EVENT>"
                        .to_string()
                }
            );

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

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

-                "__REPLY_TO__:doyle-psyche\n\

-                 <EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \

-                 subnet=\"home\">build done<br>all green</EVENT>"

+                    from: "doyle-psyche".to_string(),

+                    body: "<EVENT type=\"notify\" from=\"doyle-psyche\" notif_id=\"cafe:1\" \

+                           subnet=\"home\">build done<br>all green</EVENT>"

// The notif row persists: seen by the fired target, undismissed.

let row = h.store.get("cafe:1").unwrap().unwrap();


psyrelay done. Now daemon access.rs (3) + shellchan.rs (1).


crates\spt-daemon\src\access.rs-18-\! - **Outbound** → any visible node: the whitelist never restricts who an
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-19-//!   endpoint talks *to*.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-20-//! - **Inbound reply** correlated to the endpoint's own prior outbound
crates\spt-daemon\src\access.rs:21://!   (`__REPLY_TO__`) → allowed from any visible node ("established/related").
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-22-//!   Classification reads the durable [`RecentOutbound`] log
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-23-//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-24-//! - **Unsolicited/direct inbound** → only from a whitelisted node.
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-36-//! small-file read is noise.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-37-//!
crates\spt-daemon\src\access.rs-38-\! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
crates\spt-daemon\src\access.rs:39://! reply-vs-unsolicited — a `__REPLY_TO__` conversation is message traffic.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-40-//! Control surfaces (terminal attach, file transfer) are never replies; their
crates\spt-daemon\src\access.rs-41-\! serves always gate as [`InboundClass::Unsolicited`].
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-42-
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-49-pub enum InboundClass {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-50-    /// A new/direct control or message — gated by the whitelist.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-51-    Unsolicited,
crates\spt-daemon\src\access.rs:52:    /// Correlated to the target endpoint's own prior outbound (`__REPLY_TO__`)
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-53-    /// — allowed from any visible node.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-54-    Reply,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\access.rs-55-}
--
crates\spt-daemon\src\shellchan.rs-150-    stamped: &str,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\shellchan.rs-151-) -> Result<(), String> {
crates\spt-daemon\src\shellchan.rs-152-    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
crates\spt-daemon\src\shellchan.rs:153:    // from_id = "" — the stamped frame is the whole payload; a __REPLY_TO__
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\shellchan.rs-154-    // header would corrupt the MAC'd line.
crates\spt-daemon\src\shellchan.rs-155-    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\shellchan.rs-156-}


All comments. Read + reword access.rs (daemon) + shellchan.rs.


17	//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).
18	//! - **Outbound** → any visible node: the whitelist never restricts who an
19	//!   endpoint talks *to*.
20	//! - **Inbound reply** correlated to the endpoint's own prior outbound
21	//!   (`__REPLY_TO__`) → allowed from any visible node ("established/related").
22	//!   Classification reads the durable [`RecentOutbound`] log
23	//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].
24	//! - **Unsolicited/direct inbound** → only from a whitelisted node.
25	//! - **Same-node operation is always allowed** — the gate sits on the
26	//!   net-inbound path only, so the local delivery path never sees it; an
27	//!   origin that *is* this node (a loopback self-dial) short-circuits Allow.
28	//! - **Default absent = open**; setting a whitelist restricts (an empty entry
29	//!   = locked down). Node-tier ships M4 (Ed25519 node pubkeys); the user tier
30	//!   is an inert schema reservation until the cross-user model (ADR-0006).
31	//!
32	//! The store is [`spt_store::access::AccessStore`] (`trust/access.json` —
33	//! security material next to the trust store). The gate re-reads it per check:
34	//! an `spt access` edit takes effect on the next inbound without a daemon
35	//! restart, and the checks run at message/stream-establishment rate where a
36	//! small-file read is noise.
37	//!
38	//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
39	//! reply-vs-unsolicited — a `__REPLY_TO__` conversation is message traffic.
40	//! Control surfaces (terminal attach, file transfer) are never replies; their
41	//! serves always gate as [`InboundClass::Unsolicited`].
42
43	use spt_store::access::{AccessStore, RecentOutbound, REPLY_WINDOW_MS};
44	use spt_store::perch::node_key_file;
45
46	/// How an inbound interaction reached this node, for the stateful-firewall
47	/// split.
48	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49	pub enum InboundClass {
50	    /// A new/direct control or message — gated by the whitelist.
51	    Unsolicited,
52	    /// Correlated to the target endpoint's own prior outbound (`__REPLY_TO__`)
53	    /// — allowed from any visible node.
54	    Reply,
55	}
56

//!   never a payload field (REQ-HAZARD-WAN-ORIGIN-AUTH).
//! - **Outbound** → any visible node: the whitelist never restricts who an
//!   endpoint talks *to*.
//! - **Inbound reply** correlated to the endpoint's own prior outbound
//!   (`__REPLY_TO__`) → allowed from any visible node ("established/related").
//!   Classification reads the durable [`RecentOutbound`] log
//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].
//! - **Unsolicited/direct inbound** → only from a whitelisted node.
//! - **Same-node operation is always allowed** — the gate sits on the
//!   net-inbound path only, so the local delivery path never sees it; an
//!   origin that *is* this node (a loopback self-dial) short-circuits Allow.
//! - **Default absent = open**; setting a whitelist restricts (an empty entry
//!   = locked down). Node-tier ships M4 (Ed25519 node pubkeys); the user tier
//!   is an inert schema reservation until the cross-user model (ADR-0006).
//!
//! The store is [`spt_store::access::AccessStore`] (`trust/access.json` —
//! security material next to the trust store). The gate re-reads it per check:
//! an `spt access` edit takes effect on the next inbound without a daemon
//! restart, and the checks run at message/stream-establishment rate where a
//! small-file read is noise.
//!
//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
//! reply-vs-unsolicited — a `__REPLY_TO__` conversation is message traffic.
//! Control surfaces (terminal attach, file transfer) are never replies; their
//! serves always gate as [`InboundClass::Unsolicited`].

use spt_store::access::{AccessStore, RecentOutbound, REPLY_WINDOW_MS};
use spt_store::perch::node_key_file;

/// How an inbound interaction reached this node, for the stateful-firewall
/// split.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InboundClass {
    /// A new/direct control or message — gated by the whitelist.
    Unsolicited,
    /// Correlated to the target endpoint's own prior outbound (`__REPLY_TO__`)
    /// — allowed from any visible node.
    Reply,
}


//! - **Inbound reply** correlated to the endpoint's own prior outbound
//!   (`__REPLY_TO__`) → allowed from any visible node ("established/related").

//! - **Inbound reply** correlated to the endpoint's own prior outbound
//!   (keyed on the inbound `from`) → allowed from any visible node
//!   ("established/related").


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

//! - **Inbound reply** correlated to the endpoint's own prior outbound
//!   (`__REPLY_TO__`) → allowed from any visible node ("established/related").

//! - **Inbound reply** correlated to the endpoint's own prior outbound
//!   (keyed on the inbound `from`) → allowed from any visible node
//!   ("established/related").

//! - **Outbound** → any visible node: the whitelist never restricts who an

-//!   (`__REPLY_TO__`) → allowed from any visible node ("established/related").

+//!   (keyed on the inbound `from`) → allowed from any visible node

//!   Classification reads the durable [`RecentOutbound`] log

//!   ([`classify_inbound`]); senders feed it via [`note_outbound`].

//! - **Unsolicited/direct inbound** → only from a whitelisted node.


//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
//! reply-vs-unsolicited — a `__REPLY_TO__` conversation is message traffic.

//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
//! reply-vs-unsolicited — a reply conversation (correlated on `from`) is
//! message traffic.


//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
//! reply-vs-unsolicited — a `__REPLY_TO__` conversation is message traffic.

//! **Class policy:** only the *message* funnel ([`crate::wan`]) classifies
//! reply-vs-unsolicited — a reply conversation (correlated on `from`) is
//! message traffic.

-//! reply-vs-unsolicited — a `__REPLY_TO__` conversation is message traffic.

+//! reply-vs-unsolicited — a reply conversation (correlated on `from`) is

//! Control surfaces (terminal attach, file transfer) are never replies; their

//! serves always gate as [`InboundClass::Unsolicited`].


    /// Correlated to the target endpoint's own prior outbound (`__REPLY_TO__`)
    /// — allowed from any visible node.
    Reply,

    /// Correlated to the target endpoint's own prior outbound (keyed on the
    /// inbound `from`) — allowed from any visible node.
    Reply,


    /// Correlated to the target endpoint's own prior outbound (`__REPLY_TO__`)
    /// — allowed from any visible node.
    Reply,

    /// Correlated to the target endpoint's own prior outbound (keyed on the
    /// inbound `from`) — allowed from any visible node.
    Reply,

/// A new/direct control or message — gated by the whitelist.

-    /// Correlated to the target endpoint's own prior outbound (`__REPLY_TO__`)

-    /// — allowed from any visible node.

+    /// Correlated to the target endpoint's own prior outbound (keyed on the

+    /// inbound `from`) — allowed from any visible node.


148	    owner: &str,
149	    shell_id: &str,
150	    stamped: &str,
151	) -> Result<(), String> {
152	    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
153	    // from_id = "" — the stamped frame is the whole payload; a __REPLY_TO__
154	    // header would corrupt the MAC'd line.
155	    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))
156	}
157

    owner: &str,
    shell_id: &str,
    stamped: &str,
) -> Result<(), String> {
    let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);
    // from_id = "" — the stamped frame is the whole payload; a __REPLY_TO__
    // header would corrupt the MAC'd line.
    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))
}


    // from_id = "" — the stamped frame is the whole payload; a __REPLY_TO__
    // header would corrupt the MAC'd line.
    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))

    // from_id = "" — anonymous structural carriage; the stamped frame is the
    // whole payload (no reply header to corrupt the MAC'd line).
    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))


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

    // from_id = "" — the stamped frame is the whole payload; a __REPLY_TO__
    // header would corrupt the MAC'd line.
    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))

    // from_id = "" — anonymous structural carriage; the stamped frame is the
    // whole payload (no reply header to corrupt the MAC'd line).
    spt_store::spool::spool_message_at(&perch, "", stamped).map_err(|e| format!("spool: {e}"))

let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, owner, shell_id);

-    // from_id = "" — the stamped frame is the whole payload; a __REPLY_TO__

-    // header would corrupt the MAC'd line.

+    // from_id = "" — anonymous structural carriage; the stamped frame is the

+    // whole payload (no reply header to corrupt the MAC'd line).


spt-daemon done. Now spt-live outbound.rs + spt-net wanmsg.rs.


C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\outbound.rs-24-/// — by construction no Psyche-supplied routing survives the parse.
C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\outbound.rs-25-#[derive(Debug, Clone, PartialEq, Eq)]
C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\outbound.rs-26-pub enum PsycheIntent {
crates\spt-live\src\outbound.rs:27:    /// Relay to the `__REPLY_TO__` sender of the inbound being answered.
crates\spt-live\src\outbound.rs-28-    Reply { body: String },
C:\Users\decid\Documents\projects\spt-core\crates\spt-live\src\outbound.rs-29-    /// Relay to the agent's own user/subnet only.
crates\spt-live\src\outbound.rs-30-    Notify { body: String },
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-18-//! pubkey, the REQ-NET-1 identity binding) — read from the broker's stream
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-19-//! table, never from record bytes. A forged `origin_node`/`node` field inside
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-20-//! the JSON decodes as an ignored unknown field and influences nothing. The
crates\spt-net\src\net\wanmsg.rs:21://! `from` field is reply-routing metadata (`__REPLY_TO__`), not an
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-22-//! authorization subject.
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-23-
crates\spt-net\src\net\wanmsg.rs-24-use serde::{Deserialize, Serialize};
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-38-pub struct WanMessage {
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-39-    /// The endpoint the message is for (resolved at the receiving node).
crates\spt-net\src\net\wanmsg.rs-40-    pub target: String,
crates\spt-net\src\net\wanmsg.rs:41:    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-42-    /// anonymous). Metadata only; never an authorization subject.
crates\spt-net\src\net\wanmsg.rs-43-    pub from: String,
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\wanmsg.rs-44-    /// The message body.


All comments. Reword outbound.rs + wanmsg.rs.


26	pub enum PsycheIntent {
27	    /// Relay to the `__REPLY_TO__` sender of the inbound being answered.
28	    Reply { body: String },
29	    /// Relay to the agent's own user/subnet only.

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.


18	//! pubkey, the REQ-NET-1 identity binding) — read from the broker's stream
19	//! table, never from record bytes. A forged `origin_node`/`node` field inside
20	//! the JSON decodes as an ignored unknown field and influences nothing. The
21	//! `from` field is reply-routing metadata (`__REPLY_TO__`), not an
22	//! authorization subject.
23
24	use serde::{Deserialize, Serialize};
25
26	use crate::net::ndjson::{self, NdjsonDecoder};
27	use crate::net::registry::{Ambiguity, Resolution};
28
29	/// One WAN message record: a message for `target` (an endpoint id at the
30	/// receiving node), as sent by `from` (reply-routing metadata). `op_id` is the
31	/// sender-minted durable idempotency key — the receiver dedups on it before
32	/// delivering, so a replayed feed (sender redrive, receiver brain restart
33	/// resubscribing from an earlier cursor) delivers exactly once
34	/// (REQ-HAZARD-RESTART-IDEMPOTENT, now wire-fed).
35	// [impl->REQ-NET-1]
36	// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
37	#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
38	pub struct WanMessage {
39	    /// The endpoint the message is for (resolved at the receiving node).
40	    pub target: String,
41	    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =
42	    /// anonymous). Metadata only; never an authorization subject.
43	    pub from: String,

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


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

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