1	//! `spt-msg` — the local-messaging layer for spt-core.
2	//!
3	//! Third layer of the acyclically-layered crate stack (R-ARCH-1): depends only
4	//! on `spt-proto` (wire) and `spt-store` (persistence), one layer down — never
5	//! the reverse. The `spt` binary is the top consumer.
6	//!
7	//! Per ADR-0001 the **stable wire format** (length-prefixed `__REPLY_TO__`
8	//! framing) is copied verbatim from the sister project (`wire`); the delivery
9	//! routing, listener lifecycle, and ready/ring orchestration are clean-room,
10	//! re-expressed against the spt-core `spt-store` API (path-aware spool, perch
11	//! resolver, registry).
12	//!
13	//! M1 is **loopback-only**: every address is `127.0.0.1:<ephemeral>`. Cross-node
14	//! delivery is M4; the consolidated daemon that owns all listeners is M3. M1
15	//! ships the messaging *mechanism* on the proven M0 substrate.
16	// [impl->REQ-ARCH-1]
17
18	// Modules land one per M1 task (T1 wire+listener, T2 deliver, T3 ready, T4 ring).
19	pub mod deliver;
20	pub mod emit;
21	pub mod listener;
22	pub mod ready;
23	pub mod ring;
24	pub mod wire;
25
26	/// Test-only home isolation. The listener/ready/ring surfaces resolve the
27	/// owlery through `spt_store::perch::owlery_dir()`, which honors `$SPT_HOME`.
28	/// Without an override the default home is `%LOCALAPPDATA%\spt-core` — distinct
29	/// from the modern spt runtime's `spt/` home, but still a real shared dir that
30	/// must not collect test debris. Every unit test that touches a perch MUST call
31	/// [`test_home::isolate`] first so it operates in a throwaway temp dir, never
32	/// the real one.
33	#[cfg(test)]
34	pub(crate) mod test_home {
35	    use std::sync::OnceLock;
36	    use tempfile::TempDir;
37
38	    static HOME: OnceLock<TempDir> = OnceLock::new();
39
40	    /// Point `$SPT_HOME` at an isolated temp dir for the whole test process.
41	    /// Idempotent: the first caller wins, every later caller blocks until the
42	    /// env is set, so no test performs a perch op against the default home.
43	    pub(crate) fn isolate() {
44	        HOME.get_or_init(|| {
45	            let dir = TempDir::new().expect("temp SPT_HOME");
46	            std::env::set_var("SPT_HOME", dir.path());
47	            dir
48	        });
49	    }
50	}
51

//! `spt-msg` — the local-messaging layer for spt-core.
//!
//! Third layer of the acyclically-layered crate stack (R-ARCH-1): depends only
//! on `spt-proto` (wire) and `spt-store` (persistence), one layer down — never
//! the reverse. The `spt` binary is the top consumer.
//!
//! Per ADR-0001 the **stable wire format** (length-prefixed `__REPLY_TO__`
//! framing) is copied verbatim from the sister project (`wire`); the delivery
//! routing, listener lifecycle, and ready/ring orchestration are clean-room,
//! re-expressed against the spt-core `spt-store` API (path-aware spool, perch
//! resolver, registry).
//!
//! M1 is **loopback-only**: every address is `127.0.0.1:<ephemeral>`. Cross-node
//! delivery is M4; the consolidated daemon that owns all listeners is M3. M1
//! ships the messaging *mechanism* on the proven M0 substrate.
// [impl->REQ-ARCH-1]

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

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

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

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


Edit

//! Per ADR-0001 the **stable wire format** (length-prefixed `__REPLY_TO__`
//! framing) is copied verbatim from the sister project (`wire`); the delivery
//! routing, listener lifecycle, and ready/ring orchestration are clean-room,
//! re-expressed against the spt-core `spt-store` API (path-aware spool, perch
//! resolver, registry).

//! Per ADR-0001 the only **stable, copy-verbatim wire format** is the `<EVENT>`
//! envelope grammar in `spt-proto::event` (composed at the delivery edge by
//! `emit`); the `wire` TCP frame is an **internal** length-prefixed
//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the
//! mis-elevated `__REPLY_TO__` framing). The delivery routing, listener
//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against
//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).


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

//! Per ADR-0001 the only **stable, copy-verbatim wire format** is the `<EVENT>`
//! envelope grammar in `spt-proto::event` (composed at the delivery edge by
//! `emit`); the `wire` TCP frame is an **internal** length-prefixed
//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the
//! mis-elevated `__REPLY_TO__` framing). The delivery routing, listener
//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against
//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).

//! `spt-msg` — the local-messaging layer for spt-core.
//!
//! Third layer of the acyclically-layered crate stack (R-ARCH-1): depends only
//! on `spt-proto` (wire) and `spt-store` (persistence), one layer down — never
//! the reverse. The `spt` binary is the top consumer.
//!
//! Per ADR-0001 the **stable wire format** (length-prefixed `__REPLY_TO__`
//! framing) is copied verbatim from the sister project (`wire`); the delivery
//! routing, listener lifecycle, and ready/ring orchestration are clean-room,
//! re-expressed against the spt-core `spt-store` API (path-aware spool, perch
//! resolver, registry).
//!
//! M1 is **loopback-only**: every address is `127.0.0.1:<ephemeral>`. Cross-node
//! delivery is M4; the consolidated daemon that owns all listeners is M3. M1
//! ships the messaging *mechanism* on the proven M0 substrate.
// [impl->REQ-ARCH-1]

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

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

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

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

-//! Per ADR-0001 the **stable wire format** (length-prefixed `__REPLY_TO__`

-//! framing) is copied verbatim from the sister project (`wire`); the delivery

-//! routing, listener lifecycle, and ready/ring orchestration are clean-room,

-//! re-expressed against the spt-core `spt-store` API (path-aware spool, perch

+//! Per ADR-0001 the only **stable, copy-verbatim wire format** is the `<EVENT>`

+//! envelope grammar in `spt-proto::event` (composed at the delivery edge by

+//! `emit`); the `wire` TCP frame is an **internal** length-prefixed

+//! `(from, body)` record — not a sister-interop contract (ADR-0020 retired the

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

+//! lifecycle, and ready/ring orchestration are clean-room, re-expressed against

+//! the spt-core `spt-store` API (path-aware spool, perch resolver, registry).


spt-msg layer done (minus killer_quickstart test, deferred to T7). Now T3 — spt api surfaces. cli.rs `emit` + run_listen + cmd_ring.


/// Emit one delivery to the listener stream: render the raw frame into its
/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized
/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.
// [impl->REQ-MSG-4]
fn emit(msg: &str) {
    for line in spt_msg::emit::render_event_lines(msg) {
        println!("{}", line);
    }
    let _ = std::io::stdout().flush();
}

/// Emit one delivery to the listener stream: compose the structural
/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass
/// through; oversized lines chunk into EVENT-PART) and print + flush. One
/// delivery per emit.
// [impl->REQ-MSG-4]
// [impl->REQ-MSG-ENVELOPE]
fn emit(from: &str, body: &str) {
    for line in spt_msg::emit::render_event_lines(from, body) {
        println!("{}", line);
    }
    let _ = std::io::stdout().flush();
}


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

/// Emit one delivery to the listener stream: render the raw frame into its
/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized
/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.
// [impl->REQ-MSG-4]
fn emit(msg: &str) {
    for line in spt_msg::emit::render_event_lines(msg) {
        println!("{}", line);
    }
    let _ = std::io::stdout().flush();
}

/// Emit one delivery to the listener stream: compose the structural
/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass
/// through; oversized lines chunk into EVENT-PART) and print + flush. One
/// delivery per emit.
// [impl->REQ-MSG-4]
// [impl->REQ-MSG-ENVELOPE]
fn emit(from: &str, body: &str) {
    for line in spt_msg::emit::render_event_lines(from, body) {
        println!("{}", line);
    }
    let _ = std::io::stdout().flush();
}

-/// Emit one delivery to the listener stream: render the raw frame into its

-/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized

-/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.

+/// Emit one delivery to the listener stream: compose the structural

+/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass

+/// through; oversized lines chunk into EVENT-PART) and print + flush. One

-    for line in spt_msg::emit::render_event_lines(msg) {

+    for line in spt_msg::emit::render_event_lines(from, body) {


    ok_line(format!("READY:{}", id));
    for msg in backlog {
        emit(&msg);
    }

    if once {
        if let Some(msg) = agent.recv() {
            emit(&msg);
        }
        for msg in agent.drain_spool() {
            emit(&msg);
        }
        // Going offline (the --once listener ends here): drop the perch's

    ok_line(format!("READY:{}", id));
    for (from, body) in backlog {
        emit(&from, &body);
    }

    if once {
        if let Some((from, body)) = agent.recv() {
            emit(&from, &body);
        }
        for (from, body) in agent.drain_spool() {
            emit(&from, &body);
        }
        // Going offline (the --once listener ends here): drop the perch's


    ok_line(format!("READY:{}", id));
    for msg in backlog {
        emit(&msg);
    }

    if once {
        if let Some(msg) = agent.recv() {
            emit(&msg);
        }
        for msg in agent.drain_spool() {
            emit(&msg);
        }
        // Going offline (the --once listener ends here): drop the perch's

    ok_line(format!("READY:{}", id));
    for (from, body) in backlog {
        emit(&from, &body);
    }

    if once {
        if let Some((from, body)) = agent.recv() {
            emit(&from, &body);
        }
        for (from, body) in agent.drain_spool() {
            emit(&from, &body);
        }
        // Going offline (the --once listener ends here): drop the perch's

-        if let Some(msg) = agent.recv() {

+        if let Some((from, body)) = agent.recv() {

-        for msg in agent.drain_spool() {

+        for (from, body) in agent.drain_spool() {

// ready state, then wake the pump so peers see the flip promptly.


    loop {
        if let Some(msg) = agent.recv() {
            emit(&msg);
        }
        for msg in agent.drain_spool() {
            emit(&msg);
        }
    }
}

    loop {
        if let Some((from, body)) = agent.recv() {
            emit(&from, &body);
        }
        for (from, body) in agent.drain_spool() {
            emit(&from, &body);
        }
    }
}


<owl_messages>
<owl_message from="doyle" priority="high">Found REQ-MSG-ENVELOPE multi-crate refactor UNCOMMITTED in my docs-branch working tree (spt-msg: wire/emit/ready/ring/listener/deliver, spt-store: spool/access). traceable-reqs.toml already flipped impl/unit/int → "landed". Was this you? If yes: design-check was the gate before execute — did that resolve? Give me 3 impl-decisions outcome + confirm scope so I can gate. If not you, I'll treat as my own to verify. Either way I'm building + running checks now.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


    loop {
        if let Some(msg) = agent.recv() {
            emit(&msg);
        }
        for msg in agent.drain_spool() {
            emit(&msg);
        }
    }
}

    loop {
        if let Some((from, body)) = agent.recv() {
            emit(&from, &body);
        }
        for (from, body) in agent.drain_spool() {
            emit(&from, &body);
        }
    }
}


**Shared-worktree collision** — doyle sees my mid-flight uncommitted edits and is about to build/gate a BROKEN tree. Stop and warn him immediately.


PowerShell

$msg = @'
STOP — DO NOT build/gate yet. That's MY work, IN FLIGHT, does NOT compile right now. We're in the SAME working tree (docs/msg-envelope-adr-0020) — you're seeing my uncommitted half-refactor. A build now = guaranteed RED (incomplete), and us both editing these files = clobber risk.

YES it's me (todlando). Design-check DID resolve — you BLESSED all 3 earlier this session:
1. TCP wire = structural inner-frame u32 len(from)+from+body (body verbatim) — DONE wire.rs
2. DrainedMsg{from,body} struct — DONE spool.rs
3. anon from="" preserved — DONE
You also reaffirmed gate conditions + 2 re-emphases (adversarial typed-passthrough no-double-wrap; grep-clean incl test fixtures) — both being built.

SCOPE / progress:
DONE: spt-store(spool DrainedMsg + access comment), spt-msg(wire/emit/ready/ring/listener/deliver/lib — structural spine + whole-render + migrated unit tests)
IN PROGRESS now: spt(cli emit/run_listen done; api delivery/startup/worker/reporting next)
NOT STARTED: spt-daemon(notif double-prefix drop/psyrelay/access/shellchan), spt-net(wanmsg comment-only), spt-live(outbound), test sweep(killer_quickstart + new poll/listener ints)

ASK: HOLD — don't touch these files, don't gate. Let me finish + self-verify (full suite + clippy -D + traceable EXIT=0 + xtask) + COMMIT on this branch. I'll ping you the commit SHA when green; gate from the commit, not the live tree. Standby.
'@
$msg | & $env:OWL send doyle
Write-Output "SEND_EXIT=$LASTEXITCODE"

Warn doyle: hold gate, tree mid-refactor


➜ SENT:doyle
SEND_EXIT=0


Warned doyle (hold). Now race to a compiling committed state to close the collision window. cmd_ring reply → render `<EVENT>`.


Edit

        RingOutcome::Replied(reply) => {
            print!("{}", reply);
            let _ = std::io::stdout().flush();
            0
        }

        RingOutcome::Replied { from, body } => {
            // The reply is an arriving message — render the canonical <EVENT>
            // envelope (whole, no chunk), like every other delivery surface.
            println!("{}", spt_msg::emit::render_event_whole(&from, &body));
            let _ = std::io::stdout().flush();
            0
        }


        RingOutcome::Replied(reply) => {
            print!("{}", reply);
            let _ = std::io::stdout().flush();
            0
        }

        RingOutcome::Replied { from, body } => {
            // The reply is an arriving message — render the canonical <EVENT>
            // envelope (whole, no chunk), like every other delivery surface.
            println!("{}", spt_msg::emit::render_event_whole(&from, &body));
            let _ = std::io::stdout().flush();
            0
        }

match ring::ring(&target, &from, body, Duration::from_secs(timeout), &owlery) {

-        RingOutcome::Replied(reply) => {

+        RingOutcome::Replied { from, body } => {

+            // The reply is an arriving message — render the canonical <EVENT>

+            // envelope (whole, no chunk), like every other delivery surface.

+            println!("{}", spt_msg::emit::render_event_whole(&from, &body));


Now cli.rs:7707 check_message. Read around it.


7690	            cmd_emit("mock-shell-0", "bumped", "wall at (3,4)", &token),
7691	            0
7692	        );
7693	        assert_eq!(
7694	            spt_store::spool::pending_count_at(&owner_perch).unwrap(),
7695	            0,
7696	            "a dropped sensory payload never lands in the owner's spool"
7697	        );
7698
7699	        // A live owner receives the typed sensory frame over TCP.
7700	        let rec =
7701	            spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
7702	        spt_store::info::write_info(&owner_perch, &rec).unwrap();
7703	        std::fs::write(perch::resolve_ready_file("doyle", ParentHint::Infer), "").unwrap();
7704	        let listener = spt_msg::listener::PollListener::bind("doyle").expect("owner listener");
7705	        assert_eq!(
7706	            cmd_emit("mock-shell-0", "bumped", "wall at (3,4)", &token),
7707	            0
7708	        );
7709	        let mut got = None;
7710	        for _ in 0..200 {
7711	            if let Some(m) = listener.check_message() {
7712	                got = Some(m);
7713	                break;
7714	            }
7715	            std::thread::sleep(Duration::from_millis(5));
7716	        }
7717	        let got = got.expect("sensory frame delivered live");
7718	        assert!(got.contains("type=\"sensory\""), "{got}");
7719	        assert!(got.contains("sensory-type=\"bumped\""), "{got}");
7720	        assert!(got.contains("wall at (3,4)"), "{got}");
7721	        assert_eq!(
7722	            spt_store::spool::pending_count_at(&owner_perch).unwrap(),
7723	            0,
7724	            "live delivery leaves no spooled copy"
7725	        );
7726	    }
7727
7728	    /// Register a shell adapter named `name` whose `[shell]` section gains
7729	    /// `extra` lines; the spawn template is a cross-platform no-op so the

            cmd_emit("mock-shell-0", "bumped", "wall at (3,4)", &token),
            0
        );
        assert_eq!(
            spt_store::spool::pending_count_at(&owner_perch).unwrap(),
            0,
            "a dropped sensory payload never lands in the owner's spool"
        );

        // A live owner receives the typed sensory frame over TCP.
        let rec =
            spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
        spt_store::info::write_info(&owner_perch, &rec).unwrap();
        std::fs::write(perch::resolve_ready_file("doyle", ParentHint::Infer), "").unwrap();
        let listener = spt_msg::listener::PollListener::bind("doyle").expect("owner listener");
        assert_eq!(
            cmd_emit("mock-shell-0", "bumped", "wall at (3,4)", &token),
            0
        );
        let mut got = None;
        for _ in 0..200 {
            if let Some(m) = listener.check_message() {
                got = Some(m);
                break;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        let got = got.expect("sensory frame delivered live");
        assert!(got.contains("type=\"sensory\""), "{got}");
        assert!(got.contains("sensory-type=\"bumped\""), "{got}");
        assert!(got.contains("wall at (3,4)"), "{got}");
        assert_eq!(
            spt_store::spool::pending_count_at(&owner_perch).unwrap(),
            0,
            "live delivery leaves no spooled copy"
        );
    }

    /// Register a shell adapter named `name` whose `[shell]` section gains
    /// `extra` lines; the spawn template is a cross-platform no-op so the


        let mut got = None;
        for _ in 0..200 {
            if let Some(m) = listener.check_message() {
                got = Some(m);
                break;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        let got = got.expect("sensory frame delivered live");
        assert!(got.contains("type=\"sensory\""), "{got}");
        assert!(got.contains("sensory-type=\"bumped\""), "{got}");
        assert!(got.contains("wall at (3,4)"), "{got}");

        let mut got = None;
        for _ in 0..200 {
            if let Some(m) = listener.check_message() {
                got = Some(m);
                break;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        let (_from, body) = got.expect("sensory frame delivered live");
        assert!(body.contains("type=\"sensory\""), "{body}");
        assert!(body.contains("sensory-type=\"bumped\""), "{body}");
        assert!(body.contains("wall at (3,4)"), "{body}");


        let mut got = None;
        for _ in 0..200 {
            if let Some(m) = listener.check_message() {
                got = Some(m);
                break;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        let got = got.expect("sensory frame delivered live");
        assert!(got.contains("type=\"sensory\""), "{got}");
        assert!(got.contains("sensory-type=\"bumped\""), "{got}");
        assert!(got.contains("wall at (3,4)"), "{got}");

        let mut got = None;
        for _ in 0..200 {
            if let Some(m) = listener.check_message() {
                got = Some(m);
                break;
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        let (_from, body) = got.expect("sensory frame delivered live");
        assert!(body.contains("type=\"sensory\""), "{body}");
        assert!(body.contains("sensory-type=\"bumped\""), "{body}");
        assert!(body.contains("wall at (3,4)"), "{body}");

-        let got = got.expect("sensory frame delivered live");

-        assert!(got.contains("type=\"sensory\""), "{got}");

-        assert!(got.contains("sensory-type=\"bumped\""), "{got}");

-        assert!(got.contains("wall at (3,4)"), "{got}");

+        let (_from, body) = got.expect("sensory frame delivered live");

+        assert!(body.contains("type=\"sensory\""), "{body}");

+        assert!(body.contains("sensory-type=\"bumped\""), "{body}");

+        assert!(body.contains("wall at (3,4)"), "{body}");


Now the api surfaces: delivery.rs, startup.rs, worker.rs. Read them.


150	/// `api poll <shell-id> --link <token>` — the **shell relay drain** (M5-D3c,
151	/// CONTEXT §Shell model: child-relay command receipt). The link token is the
152	/// auth ("owner from the link" — the binary knows nothing else); the drained
153	/// rows are the MAC-stamped command/text/file frames the owner spooled, one
154	/// per line. A token that resolves no instance, or one belonging to a
155	/// different instance than named, refuses.
156	// [impl->REQ-SHELL-1]
157	pub fn cmd_poll_shell(id: &str, link: &str) -> i32 {
158	    let owlery = perch::owlery_dir();
159	    let Some((owner, shell_id)) = spt_daemon::shellhost::find_shell_by_token(&owlery, link) else {
160	        eprintln!("AUTH_REFUSED:{id}: no instance holds this link token");
161	        return EXIT_REFUSED;
162	    };
163	    if shell_id != id {
164	        eprintln!("AUTH_REFUSED:{id}: the link token belongs to {shell_id}");
165	        return EXIT_REFUSED;
166	    }
167	    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);
168	    for frame in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
169	        println!("{frame}");
170	    }
171	    0
172	}
173
174	/// Drain the spool for `id`, returning the messages (oldest first). Separated
175	/// from `cmd_poll` so it is unit-testable without capturing stdout.
176	pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<String> {
177	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
178	    // The resting gate (REQ-INST-6): while the owner's durable rest record
179	    // reads dormant/suspended, deferred rows hold — the hook drain narrows to
180	    // non-deferred. The wake edge persists Active, so the very next drain
181	    // releases them through this same path (exactly once: delivered-marks).
182	    // [impl->REQ-INST-6]
183	    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
184	    let drained = if include_deferred {
185	        spool::drain_all_at(&perch_path)
186	    } else {
187	        spool::drain_non_deferred_at(&perch_path)
188	    };
189	    drained.unwrap_or_default()
190	}
191
192	fn emit(msg: &str) {
193	    use std::io::Write;
194	    print!("{msg}");
195	    let _ = std::io::stdout().flush();
196	}
197
198	#[cfg(test)]
199	mod tests {
200	    use super::*;
201	    use crate::testutil::isolated_home;
202	    use spt_store::info::{self, InfoJson};
203
204	    fn establish(id: &str) {
205	        let path = perch::resolve_perch_path(id, ParentHint::Infer);
206	        std::fs::create_dir_all(&path).unwrap();
207	        info::write_info(
208	            &path,
209	            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),

/// `api poll <shell-id> --link <token>` — the **shell relay drain** (M5-D3c,
/// CONTEXT §Shell model: child-relay command receipt). The link token is the
/// auth ("owner from the link" — the binary knows nothing else); the drained
/// rows are the MAC-stamped command/text/file frames the owner spooled, one
/// per line. A token that resolves no instance, or one belonging to a
/// different instance than named, refuses.
// [impl->REQ-SHELL-1]
pub fn cmd_poll_shell(id: &str, link: &str) -> i32 {
    let owlery = perch::owlery_dir();
    let Some((owner, shell_id)) = spt_daemon::shellhost::find_shell_by_token(&owlery, link) else {
        eprintln!("AUTH_REFUSED:{id}: no instance holds this link token");
        return EXIT_REFUSED;
    };
    if shell_id != id {
        eprintln!("AUTH_REFUSED:{id}: the link token belongs to {shell_id}");
        return EXIT_REFUSED;
    }
    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);
    for frame in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        println!("{frame}");
    }
    0
}

/// Drain the spool for `id`, returning the messages (oldest first). Separated
/// from `cmd_poll` so it is unit-testable without capturing stdout.
pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<String> {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    // The resting gate (REQ-INST-6): while the owner's durable rest record
    // reads dormant/suspended, deferred rows hold — the hook drain narrows to
    // non-deferred. The wake edge persists Active, so the very next drain
    // releases them through this same path (exactly once: delivered-marks).
    // [impl->REQ-INST-6]
    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    drained.unwrap_or_default()
}

fn emit(msg: &str) {
    use std::io::Write;
    print!("{msg}");
    let _ = std::io::stdout().flush();
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;
    use spt_store::info::{self, InfoJson};

    fn establish(id: &str) {
        let path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&path).unwrap();
        info::write_info(
            &path,
            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),


delivery.rs is the key poll surface (T3). Read cmd_poll + how poll_drain/emit are used.


1	//! Delivery + activity: `api state`, `api echo-gate`, `api poll`, and
2	//! inject-method selection.
3	//!
4	//! Activity state lives in the perch as **spt-core-owned sentinels** (CONTEXT
5	//! §activity/idle detection) — the adapter only *calls* the commands; it never
6	//! names or writes the sentinels (MANIFEST §What is NOT in the manifest):
7	//! - `.idle`      — the activity/idle sentinel (`api state busy|idle`).
8	//! - `.more-done` — the echo-commune gate (armed by `api state idle` unless
9	//!   `--no-gate`; togglable directly via `api echo-gate set|clear`). This is the
10	//!   `can_inject = false` fallback path: a hook that can't inject context arms
11	//!   the gate so a later relay/poll surfaces it instead (CONTEXT §hook output).
12	//!
13	//! `api poll` is the **short-lived hook-channel drain** (distinct from
14	//! `api listen`'s long-running relay): it drains the spool and prints each
15	//! message framed to stdout. The inject *method* (hook/relay; PTY is M3) is
16	//! resolved from the manifest's `[inject]` table.
17	//
18	// [impl->REQ-SEAM-ACTIVITY]
19	// [impl->REQ-SEAM-INJECT]
20	// [impl->REQ-API-2]
21
22	use spt_runtime::manifest::InjectMethod;
23	use spt_runtime::Manifest;
24	use spt_store::perch::{self, ParentHint, ECHO_GATE_SENTINEL, IDLE_SENTINEL};
25	use spt_store::spool;
26
27	use super::EXIT_REFUSED;
28
29	fn sentinel_path(id: &str, name: &str) -> std::path::PathBuf {
30	    perch::resolve_perch_path(id, ParentHint::Infer).join(name)
31	}
32
33	/// Set/clear the `.idle` sentinel for an activity transition. Idle additionally
34	/// arms the echo-gate unless `no_gate` (CONTEXT §api state).
35	// [impl->REQ-SEAM-ACTIVITY]
36	pub fn cmd_state(id: &str, state: &str, no_gate: bool) -> i32 {
37	    let idle = sentinel_path(id, IDLE_SENTINEL);
38	    match state {
39	        "idle" => {
40	            if let Err(e) = std::fs::write(&idle, "") {
41	                eprintln!("STATE_FAIL:{id}: {e}");
42	                return EXIT_REFUSED;
43	            }
44	            if !no_gate {
45	                let _ = std::fs::write(sentinel_path(id, ECHO_GATE_SENTINEL), "");
46	            }
47	            eprintln!(
48	                "IDLE:{id}{}",
49	                if no_gate {
50	                    " (no-gate)"
51	                } else {
52	                    " (gate-armed)"
53	                }
54	            );
55	            0
56	        }
57	        "busy" => {
58	            // Removing a non-existent sentinel is fine (idempotent transition).
59	            let _ = std::fs::remove_file(&idle);
60	            eprintln!("BUSY:{id}");
61	            0
62	        }
63	        other => {
64	            eprintln!("USAGE: state must be busy|idle, got {other}");
65	            EXIT_REFUSED
66	        }
67	    }
68	}
69
70	/// `api echo-gate set|clear` — explicit control of the echo-commune gate,
71	/// independent of idle (CONTEXT §api state).
72	// [impl->REQ-SEAM-ACTIVITY]
73	pub fn cmd_echo_gate(id: &str, action: &str) -> i32 {
74	    let gate = sentinel_path(id, ECHO_GATE_SENTINEL);
75	    match action {
76	        "set" => {
77	            if let Err(e) = std::fs::write(&gate, "") {
78	                eprintln!("ECHO_GATE_FAIL:{id}: {e}");
79	                return EXIT_REFUSED;
80	            }
81	            eprintln!("ECHO_GATE_SET:{id}");
82	            0
83	        }
84	        "clear" => {
85	            let _ = std::fs::remove_file(&gate);
86	            eprintln!("ECHO_GATE_CLEAR:{id}");
87	            0
88	        }
89	        other => {
90	            eprintln!("USAGE: echo-gate must be set|clear, got {other}");
91	            EXIT_REFUSED
92	        }
93	    }
94	}
95
96	/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).
97	pub fn is_idle(id: &str) -> bool {
98	    sentinel_path(id, IDLE_SENTINEL).exists()
99	}
100	/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;
101	/// exercised by the activity tests today.
102	#[allow(dead_code)] // consumed by the M2b echo-commune path
103	pub fn echo_gate_armed(id: &str) -> bool {
104	    sentinel_path(id, ECHO_GATE_SENTINEL).exists()
105	}
106
107	/// Resolve the inject methods for an activity state from the manifest's
108	/// `[inject]` table, **dropping `pty`** (PTY injection is M3 — `spt-term`). With
109	/// no manifest or no configured methods, default to `hook` (the universally-
110	/// available channel). REQ-SEAM-INJECT.
111	// [impl->REQ-SEAM-INJECT]
112	pub fn resolve_inject_methods(manifest: Option<&Manifest>, idle: bool) -> Vec<InjectMethod> {
113	    let configured =
114	        manifest.and_then(|m| m.inject.as_ref()).map(
115	            |inj| {
116	                if idle {
117	                    &inj.idle
118	                } else {
119	                    &inj.activity
120	                }
121	            },
122	        );
123	    let mut methods: Vec<InjectMethod> = configured
124	        .map(|v| {
125	            v.iter()
126	                .copied()
127	                .filter(|m| *m != InjectMethod::Pty) // PTY = M3
128	                .collect()
129	        })
130	        .unwrap_or_default();
131	    if methods.is_empty() {
132	        methods.push(InjectMethod::Hook);
133	    }
134	    methods
135	}
136
137	/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
138	/// message framed to stdout. Non-deferred only unless `include_deferred`.
139	// [impl->REQ-API-2]
140	pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
141	    // Inject-method selection is resolved (hook/relay) even though the M2a poll
142	    // channel is the hook drain; PTY delivery lands with the broker in M3.
143	    let _methods = resolve_inject_methods(manifest, is_idle(id));
144	    for msg in poll_drain(id, include_deferred) {
145	        emit(&msg);
146	    }
147	    0
148	}
149
150	/// `api poll <shell-id> --link <token>` — the **shell relay drain** (M5-D3c,

//! Delivery + activity: `api state`, `api echo-gate`, `api poll`, and
//! inject-method selection.
//!
//! Activity state lives in the perch as **spt-core-owned sentinels** (CONTEXT
//! §activity/idle detection) — the adapter only *calls* the commands; it never
//! names or writes the sentinels (MANIFEST §What is NOT in the manifest):
//! - `.idle`      — the activity/idle sentinel (`api state busy|idle`).
//! - `.more-done` — the echo-commune gate (armed by `api state idle` unless
//!   `--no-gate`; togglable directly via `api echo-gate set|clear`). This is the
//!   `can_inject = false` fallback path: a hook that can't inject context arms
//!   the gate so a later relay/poll surfaces it instead (CONTEXT §hook output).
//!
//! `api poll` is the **short-lived hook-channel drain** (distinct from
//! `api listen`'s long-running relay): it drains the spool and prints each
//! message framed to stdout. The inject *method* (hook/relay; PTY is M3) is
//! resolved from the manifest's `[inject]` table.
//
// [impl->REQ-SEAM-ACTIVITY]
// [impl->REQ-SEAM-INJECT]
// [impl->REQ-API-2]

use spt_runtime::manifest::InjectMethod;
use spt_runtime::Manifest;
use spt_store::perch::{self, ParentHint, ECHO_GATE_SENTINEL, IDLE_SENTINEL};
use spt_store::spool;

use super::EXIT_REFUSED;

fn sentinel_path(id: &str, name: &str) -> std::path::PathBuf {
    perch::resolve_perch_path(id, ParentHint::Infer).join(name)
}

/// Set/clear the `.idle` sentinel for an activity transition. Idle additionally
/// arms the echo-gate unless `no_gate` (CONTEXT §api state).
// [impl->REQ-SEAM-ACTIVITY]
pub fn cmd_state(id: &str, state: &str, no_gate: bool) -> i32 {
    let idle = sentinel_path(id, IDLE_SENTINEL);
    match state {
        "idle" => {
            if let Err(e) = std::fs::write(&idle, "") {
                eprintln!("STATE_FAIL:{id}: {e}");
                return EXIT_REFUSED;
            }
            if !no_gate {
                let _ = std::fs::write(sentinel_path(id, ECHO_GATE_SENTINEL), "");
            }
            eprintln!(
                "IDLE:{id}{}",
                if no_gate {
                    " (no-gate)"
                } else {
                    " (gate-armed)"
                }
            );
            0
        }
        "busy" => {
            // Removing a non-existent sentinel is fine (idempotent transition).
            let _ = std::fs::remove_file(&idle);
            eprintln!("BUSY:{id}");
            0
        }
        other => {
            eprintln!("USAGE: state must be busy|idle, got {other}");
            EXIT_REFUSED
        }
    }
}

/// `api echo-gate set|clear` — explicit control of the echo-commune gate,
/// independent of idle (CONTEXT §api state).
// [impl->REQ-SEAM-ACTIVITY]
pub fn cmd_echo_gate(id: &str, action: &str) -> i32 {
    let gate = sentinel_path(id, ECHO_GATE_SENTINEL);
    match action {
        "set" => {
            if let Err(e) = std::fs::write(&gate, "") {
                eprintln!("ECHO_GATE_FAIL:{id}: {e}");
                return EXIT_REFUSED;
            }
            eprintln!("ECHO_GATE_SET:{id}");
            0
        }
        "clear" => {
            let _ = std::fs::remove_file(&gate);
            eprintln!("ECHO_GATE_CLEAR:{id}");
            0
        }
        other => {
            eprintln!("USAGE: echo-gate must be set|clear, got {other}");
            EXIT_REFUSED
        }
    }
}

/// Whether the idle sentinel is set (consumed by `cmd_poll`'s inject selection).
pub fn is_idle(id: &str) -> bool {
    sentinel_path(id, IDLE_SENTINEL).exists()
}
/// Whether the echo-gate is armed. A reader for the M2b echo-commune trigger;
/// exercised by the activity tests today.
#[allow(dead_code)] // consumed by the M2b echo-commune path
pub fn echo_gate_armed(id: &str) -> bool {
    sentinel_path(id, ECHO_GATE_SENTINEL).exists()
}

/// Resolve the inject methods for an activity state from the manifest's
/// `[inject]` table, **dropping `pty`** (PTY injection is M3 — `spt-term`). With
/// no manifest or no configured methods, default to `hook` (the universally-
/// available channel). REQ-SEAM-INJECT.
// [impl->REQ-SEAM-INJECT]
pub fn resolve_inject_methods(manifest: Option<&Manifest>, idle: bool) -> Vec<InjectMethod> {
    let configured =
        manifest.and_then(|m| m.inject.as_ref()).map(
            |inj| {
                if idle {
                    &inj.idle
                } else {
                    &inj.activity
                }
            },
        );
    let mut methods: Vec<InjectMethod> = configured
        .map(|v| {
            v.iter()
                .copied()
                .filter(|m| *m != InjectMethod::Pty) // PTY = M3
                .collect()
        })
        .unwrap_or_default();
    if methods.is_empty() {
        methods.push(InjectMethod::Hook);
    }
    methods
}

/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
/// message framed to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
    // Inject-method selection is resolved (hook/relay) even though the M2a poll
    // channel is the hook drain; PTY delivery lands with the broker in M3.
    let _methods = resolve_inject_methods(manifest, is_idle(id));
    for msg in poll_drain(id, include_deferred) {
        emit(&msg);
    }
    0
}

/// `api poll <shell-id> --link <token>` — the **shell relay drain** (M5-D3c,


delivery.rs — the core poll surface. Compose WHOLE `<EVENT>` per ADR-0020. Rewrite emit + cmd_poll + poll_drain + cmd_poll_shell.


/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
/// message framed to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
    // Inject-method selection is resolved (hook/relay) even though the M2a poll
    // channel is the hook drain; PTY delivery lands with the broker in M3.
    let _methods = resolve_inject_methods(manifest, is_idle(id));
    for msg in poll_drain(id, include_deferred) {
        emit(&msg);
    }
    0
}

/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
/// message as ONE whole `<EVENT>` envelope to stdout (ADR-0020: the canonical
/// arriving-message format at every surface; whole, never `<EVENT-PART>`, so the
/// adapter's hook composer needs no reassembly — self-delimiting, F-002
/// dissolves). Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
// [impl->REQ-MSG-ENVELOPE]
pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
    // Inject-method selection is resolved (hook/relay) even though the M2a poll
    // channel is the hook drain; PTY delivery lands with the broker in M3.
    let _methods = resolve_inject_methods(manifest, is_idle(id));
    for m in poll_drain(id, include_deferred) {
        emit_event(&m.from, &m.body);
    }
    0
}


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

/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
/// message framed to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
    // Inject-method selection is resolved (hook/relay) even though the M2a poll
    // channel is the hook drain; PTY delivery lands with the broker in M3.
    let _methods = resolve_inject_methods(manifest, is_idle(id));
    for msg in poll_drain(id, include_deferred) {
        emit(&msg);
    }
    0
}

/// `api poll <id>` — drain the spool (hook-channel delivery) and print each
/// message as ONE whole `<EVENT>` envelope to stdout (ADR-0020: the canonical
/// arriving-message format at every surface; whole, never `<EVENT-PART>`, so the
/// adapter's hook composer needs no reassembly — self-delimiting, F-002
/// dissolves). Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
// [impl->REQ-MSG-ENVELOPE]
pub fn cmd_poll(id: &str, include_deferred: bool, manifest: Option<&Manifest>) -> i32 {
    // Inject-method selection is resolved (hook/relay) even though the M2a poll
    // channel is the hook drain; PTY delivery lands with the broker in M3.
    let _methods = resolve_inject_methods(manifest, is_idle(id));
    for m in poll_drain(id, include_deferred) {
        emit_event(&m.from, &m.body);
    }
    0
}

-/// message framed to stdout. Non-deferred only unless `include_deferred`.

+/// message as ONE whole `<EVENT>` envelope to stdout (ADR-0020: the canonical

+/// arriving-message format at every surface; whole, never `<EVENT-PART>`, so the

+/// adapter's hook composer needs no reassembly — self-delimiting, F-002

+/// dissolves). Non-deferred only unless `include_deferred`.

-    for msg in poll_drain(id, include_deferred) {

+    for m in poll_drain(id, include_deferred) {


    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);
    for frame in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        println!("{frame}");
    }
    0
}

    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);
    for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        emit_event(&m.from, &m.body);
    }
    0
}


    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);
    for frame in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        println!("{frame}");
    }
    0
}

    let perch_path = perch::resolve_shell_perch_path_in(&owlery, &owner, &shell_id);
    for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        emit_event(&m.from, &m.body);
    }
    0
}

-    for frame in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {

+    for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {


/// Drain the spool for `id`, returning the messages (oldest first). Separated
/// from `cmd_poll` so it is unit-testable without capturing stdout.
pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<String> {

/// Drain the spool for `id`, returning the structural `(from, body)` rows
/// (oldest first). Separated from `cmd_poll` so it is unit-testable without
/// capturing stdout.
pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {


/// Drain the spool for `id`, returning the messages (oldest first). Separated
/// from `cmd_poll` so it is unit-testable without capturing stdout.
pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<String> {

/// Drain the spool for `id`, returning the structural `(from, body)` rows
/// (oldest first). Separated from `cmd_poll` so it is unit-testable without
/// capturing stdout.
pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {

-/// Drain the spool for `id`, returning the messages (oldest first). Separated

-/// from `cmd_poll` so it is unit-testable without capturing stdout.

-pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<String> {

+/// Drain the spool for `id`, returning the structural `(from, body)` rows

+/// (oldest first). Separated from `cmd_poll` so it is unit-testable without

+pub fn poll_drain(id: &str, include_deferred: bool) -> Vec<spool::DrainedMsg> {

let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);

// The resting gate (REQ-INST-6): while the owner's durable rest record

// reads dormant/suspended, deferred rows hold — the hook drain narrows to


fn emit(msg: &str) {
    use std::io::Write;
    print!("{msg}");
    let _ = std::io::stdout().flush();
}

/// Emit one drained row to a hook-channel surface as ONE whole `<EVENT>` line
/// (composed via [`spt_msg::emit::render_event_whole`]: typed bodies pass
/// through verbatim, plain bodies compose `<EVENT type="msg" …>`; never
/// chunked). Self-delimiting by construction.
// [impl->REQ-MSG-ENVELOPE]
fn emit_event(from: &str, body: &str) {
    use std::io::Write;
    println!("{}", spt_msg::emit::render_event_whole(from, body));
    let _ = std::io::stdout().flush();
}


fn emit(msg: &str) {
    use std::io::Write;
    print!("{msg}");
    let _ = std::io::stdout().flush();
}

/// Emit one drained row to a hook-channel surface as ONE whole `<EVENT>` line
/// (composed via [`spt_msg::emit::render_event_whole`]: typed bodies pass
/// through verbatim, plain bodies compose `<EVENT type="msg" …>`; never
/// chunked). Self-delimiting by construction.
// [impl->REQ-MSG-ENVELOPE]
fn emit_event(from: &str, body: &str) {
    use std::io::Write;
    println!("{}", spt_msg::emit::render_event_whole(from, body));
    let _ = std::io::stdout().flush();
}

+/// Emit one drained row to a hook-channel surface as ONE whole `<EVENT>` line

+/// (composed via [`spt_msg::emit::render_event_whole`]: typed bodies pass

+/// through verbatim, plain bodies compose `<EVENT type="msg" …>`; never

+/// chunked). Self-delimiting by construction.

+fn emit_event(from: &str, body: &str) {

+    println!("{}", spt_msg::emit::render_event_whole(from, body));


Now startup.rs. Read 290-420.


290	            Ok(p) => eprintln!("PSYCHE_SPAWNED:{} pid={}", p.id, p.pid),
291	            Err(e) => eprintln!("PSYCHE_SPAWN_FAIL:{id}: {e}"),
292	        }
293	    }
294
295	    eprintln!("READY:{id}");
296
297	    // New-session-start is a reported resurface boundary (ADR-0007/D8b):
298	    // undismissed notifs surface into the spool before the backlog drain, so
299	    // they ride this very first drain — and render endpoint-native when the
300	    // manifest declares [session.notif]. Best-effort — never fails the listen.
301	    super::reporting::resurface_notifs_with(id, manifest);
302
303	    // Deliver one frame to the live agent and record it as an spt context
304	    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the
305	    // agent-facing chokepoint, so every delivered frame becomes a glanceable
306	    // `owl_message` context entry. Best-effort; never disturbs delivery.
307	    let deliver = |msg: &str| {
308	        emit(msg);
309	        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, msg);
310	    };
311
312	    // Drain the offline backlog first (non-deferred only; deferred rows wait
313	    // for the hook channel).
314	    for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
315	        deliver(&msg);
316	    }
317	    // One pulse tick: ingest drops + fire a pending echo (T4/T3/T5).
318	    let pulse = |host: &Option<super::live::LiveHost>| {
319	        if let Some(h) = host {
320	            if let Err(e) = h.pulse_tick(session_id.as_deref()) {
321	                eprintln!("PULSE_FAIL:{id}: {e}");
322	            }
323	        }
324	    };
325	    if once {
326	        if let Some(msg) = listener.check_message_blocking() {
327	            deliver(&msg);
328	        }
329	        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
330	            deliver(&msg);
331	        }
332	        pulse(&live);
333	        return 0;
334	    }
335	    loop {
336	        if let Some(msg) = listener.check_message_blocking() {
337	            deliver(&msg);
338	        }
339	        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
340	            deliver(&msg);
341	        }
342	        pulse(&live);
343	    }
344	}
345
346	/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
347	/// live perch on first contact, or flips an existing skeleton, keyed to the
348	/// session id the binary reports. Auth is intrinsic: an existing *live* perch
349	/// under a different session id is refused (you can only bind your own).
350	// [impl->REQ-SEAM-POSTSPAWN]
351	pub fn cmd_bind(
352	    id: &str,
353	    set_session_id: Option<String>,
354	    proof_session: Option<String>,
355	    adapter: Option<&str>,
356	    subnet: Option<&str>,
357	    endpoint_type: &str,
358	) -> i32 {
359	    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
360	    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
361	        eprintln!("INVALID_ID:{id}: {e}");
362	        return EXIT_REFUSED;
363	    }
364	    let session_id = set_session_id
365	        .or(proof_session)
366	        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
367	        .filter(|s| !s.is_empty());
368	    let Some(session_id) = session_id else {
369	        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
370	        return EXIT_REFUSED;
371	    };
372	    // The session's own anchor: the process that fired this hook (the session).
373	    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
374	    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
375	        Ok(token) => {
376	            eprintln!("BOUND:{id} token={token}");
377	            // A post-spawn bind is a new-session-start — a reported resurface
378	            // boundary (ADR-0007/D8b). Best-effort.
379	            super::reporting::resurface_notifs(id);
380	            0
381	        }
382	        Err(e) => {
383	            report_bind_error(&e);
384	            EXIT_REFUSED
385	        }
386	    }
387	}
388
389	fn report_bind_error(e: &BindError) {
390	    match e {
391	        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
392	        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
393	        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
394	        BindError::Conflict { id, held } => {
395	            eprintln!("CONFLICT:{id} is live under session {held}")
396	        }
397	        BindError::Home(m) => eprintln!("HOME_REFUSED: {m}"),
398	        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
399	    }
400	}
401
402	/// Emit one delivery to the listener stream: render the raw frame into its
403	/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized
404	/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.
405	// [impl->REQ-MSG-4]
406	fn emit(msg: &str) {
407	    use std::io::Write;
408	    for line in spt_msg::emit::render_event_lines(msg) {
409	        println!("{line}");
410	    }
411	    let _ = std::io::stdout().flush();
412	}
413
414	/// Coarse creation timestamp (epoch seconds as a string), matching the
415	/// `info.json` `started` producer-format convention.
416	fn now_stamp() -> String {
417	    std::time::SystemTime::now()
418	        .duration_since(std::time::UNIX_EPOCH)
419	        .map(|d| d.as_secs().to_string())

            Ok(p) => eprintln!("PSYCHE_SPAWNED:{} pid={}", p.id, p.pid),
            Err(e) => eprintln!("PSYCHE_SPAWN_FAIL:{id}: {e}"),
        }
    }

    eprintln!("READY:{id}");

    // New-session-start is a reported resurface boundary (ADR-0007/D8b):
    // undismissed notifs surface into the spool before the backlog drain, so
    // they ride this very first drain — and render endpoint-native when the
    // manifest declares [session.notif]. Best-effort — never fails the listen.
    super::reporting::resurface_notifs_with(id, manifest);

    // Deliver one frame to the live agent and record it as an spt context
    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the
    // agent-facing chokepoint, so every delivered frame becomes a glanceable
    // `owl_message` context entry. Best-effort; never disturbs delivery.
    let deliver = |msg: &str| {
        emit(msg);
        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, msg);
    };

    // Drain the offline backlog first (non-deferred only; deferred rows wait
    // for the hook channel).
    for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        deliver(&msg);
    }
    // One pulse tick: ingest drops + fire a pending echo (T4/T3/T5).
    let pulse = |host: &Option<super::live::LiveHost>| {
        if let Some(h) = host {
            if let Err(e) = h.pulse_tick(session_id.as_deref()) {
                eprintln!("PULSE_FAIL:{id}: {e}");
            }
        }
    };
    if once {
        if let Some(msg) = listener.check_message_blocking() {
            deliver(&msg);
        }
        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&msg);
        }
        pulse(&live);
        return 0;
    }
    loop {
        if let Some(msg) = listener.check_message_blocking() {
            deliver(&msg);
        }
        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&msg);
        }
        pulse(&live);
    }
}

/// `api bind <id>` — post-spawn boot bind (REQ-SEAM-POSTSPAWN). Establishes the
/// live perch on first contact, or flips an existing skeleton, keyed to the
/// session id the binary reports. Auth is intrinsic: an existing *live* perch
/// under a different session id is refused (you can only bind your own).
// [impl->REQ-SEAM-POSTSPAWN]
pub fn cmd_bind(
    id: &str,
    set_session_id: Option<String>,
    proof_session: Option<String>,
    adapter: Option<&str>,
    subnet: Option<&str>,
    endpoint_type: &str,
) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] reject malformed ids at the creation seam.
    if let Err(e) = spt_proto::id::validate_endpoint_id(id) {
        eprintln!("INVALID_ID:{id}: {e}");
        return EXIT_REFUSED;
    }
    let session_id = set_session_id
        .or(proof_session)
        .or_else(|| std::env::var("OWL_SESSION_ID").ok())
        .filter(|s| !s.is_empty());
    let Some(session_id) = session_id else {
        eprintln!("USAGE: bind needs --set-session-id (or $OWL_SESSION_ID)");
        return EXIT_REFUSED;
    };
    // The session's own anchor: the process that fired this hook (the session).
    let parent_pid = proc::parent_pid().unwrap_or_else(std::process::id);
    match establish_perch(id, &session_id, parent_pid, adapter, subnet, endpoint_type) {
        Ok(token) => {
            eprintln!("BOUND:{id} token={token}");
            // A post-spawn bind is a new-session-start — a reported resurface
            // boundary (ADR-0007/D8b). Best-effort.
            super::reporting::resurface_notifs(id);
            0
        }
        Err(e) => {
            report_bind_error(&e);
            EXIT_REFUSED
        }
    }
}

fn report_bind_error(e: &BindError) {
    match e {
        BindError::NoSeed(p) => eprintln!("NO_SEED:{p} (no seed for this parent pid)"),
        BindError::StaleSeed(p) => eprintln!("STALE_SEED:{p} (anchor process is dead)"),
        BindError::EmptySession => eprintln!("EMPTY_SESSION: seed carried no session id"),
        BindError::Conflict { id, held } => {
            eprintln!("CONFLICT:{id} is live under session {held}")
        }
        BindError::Home(m) => eprintln!("HOME_REFUSED: {m}"),
        BindError::Io(m) => eprintln!("BIND_IO: {m}"),
    }
}

/// Emit one delivery to the listener stream: render the raw frame into its
/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized
/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.
// [impl->REQ-MSG-4]
fn emit(msg: &str) {
    use std::io::Write;
    for line in spt_msg::emit::render_event_lines(msg) {
        println!("{line}");
    }
    let _ = std::io::stdout().flush();
}

/// Coarse creation timestamp (epoch seconds as a string), matching the
/// `info.json` `started` producer-format convention.
fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())


startup.rs is the `api listen` listener path (chunks). Update emit, the `deliver` closure (record composed `<EVENT>` for the digest), drains, and check_message sites.


/// Emit one delivery to the listener stream: render the raw frame into its
/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized
/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.
// [impl->REQ-MSG-4]
fn emit(msg: &str) {
    use std::io::Write;
    for line in spt_msg::emit::render_event_lines(msg) {
        println!("{line}");
    }
    let _ = std::io::stdout().flush();
}

/// Emit one delivery to the listener stream: compose the structural
/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass
/// through; oversized lines chunk into EVENT-PART) and print + flush. One
/// delivery per emit.
// [impl->REQ-MSG-4]
// [impl->REQ-MSG-ENVELOPE]
fn emit(from: &str, body: &str) {
    use std::io::Write;
    for line in spt_msg::emit::render_event_lines(from, body) {
        println!("{line}");
    }
    let _ = std::io::stdout().flush();
}


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

/// Emit one delivery to the listener stream: render the raw frame into its
/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized
/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.
// [impl->REQ-MSG-4]
fn emit(msg: &str) {
    use std::io::Write;
    for line in spt_msg::emit::render_event_lines(msg) {
        println!("{line}");
    }
    let _ = std::io::stdout().flush();
}

/// Emit one delivery to the listener stream: compose the structural
/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass
/// through; oversized lines chunk into EVENT-PART) and print + flush. One
/// delivery per emit.
// [impl->REQ-MSG-4]
// [impl->REQ-MSG-ENVELOPE]
fn emit(from: &str, body: &str) {
    use std::io::Write;
    for line in spt_msg::emit::render_event_lines(from, body) {
        println!("{line}");
    }
    let _ = std::io::stdout().flush();
}

-/// Emit one delivery to the listener stream: render the raw frame into its

-/// `<EVENT …>` envelope line(s) (typed envelopes pass through; oversized

-/// lines chunk into EVENT-PART) and print + flush. One delivery per emit.

+/// Emit one delivery to the listener stream: compose the structural

+/// `(from, body)` into its `<EVENT …>` envelope line(s) (typed envelopes pass

+/// through; oversized lines chunk into EVENT-PART) and print + flush. One

-    for line in spt_msg::emit::render_event_lines(msg) {

+    for line in spt_msg::emit::render_event_lines(from, body) {


    // Deliver one frame to the live agent and record it as an spt context
    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the
    // agent-facing chokepoint, so every delivered frame becomes a glanceable
    // `owl_message` context entry. Best-effort; never disturbs delivery.
    let deliver = |msg: &str| {
        emit(msg);
        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, msg);
    };

    // Drain the offline backlog first (non-deferred only; deferred rows wait
    // for the hook channel).
    for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        deliver(&msg);
    }

    // Deliver one message to the live agent and record it as an spt context
    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the
    // agent-facing chokepoint, so every delivered message becomes a glanceable
    // `owl_message` context entry. The recorded form is the whole composed
    // `<EVENT>` (what the agent sees), not the structural pair. Best-effort;
    // never disturbs delivery.
    let deliver = |from: &str, body: &str| {
        emit(from, body);
        let event = spt_msg::emit::render_event_whole(from, body);
        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, &event);
    };

    // Drain the offline backlog first (non-deferred only; deferred rows wait
    // for the hook channel).
    for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        deliver(&m.from, &m.body);
    }


    // Deliver one frame to the live agent and record it as an spt context
    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the
    // agent-facing chokepoint, so every delivered frame becomes a glanceable
    // `owl_message` context entry. Best-effort; never disturbs delivery.
    let deliver = |msg: &str| {
        emit(msg);
        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, msg);
    };

    // Drain the offline backlog first (non-deferred only; deferred rows wait
    // for the hook channel).
    for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        deliver(&msg);
    }

    // Deliver one message to the live agent and record it as an spt context
    // injection for the two-origin digest (REQ-TERM-7) — `emit` is the
    // agent-facing chokepoint, so every delivered message becomes a glanceable
    // `owl_message` context entry. The recorded form is the whole composed
    // `<EVENT>` (what the agent sees), not the structural pair. Best-effort;
    // never disturbs delivery.
    let deliver = |from: &str, body: &str| {
        emit(from, body);
        let event = spt_msg::emit::render_event_whole(from, body);
        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, &event);
    };

    // Drain the offline backlog first (non-deferred only; deferred rows wait
    // for the hook channel).
    for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
        deliver(&m.from, &m.body);
    }

// manifest declares [session.notif]. Best-effort — never fails the listen.

-    // Deliver one frame to the live agent and record it as an spt context

+    // Deliver one message to the live agent and record it as an spt context

-    // agent-facing chokepoint, so every delivered frame becomes a glanceable

-    // `owl_message` context entry. Best-effort; never disturbs delivery.

-        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, msg);

+    // agent-facing chokepoint, so every delivered message becomes a glanceable

+    // `owl_message` context entry. The recorded form is the whole composed

+    // `<EVENT>` (what the agent sees), not the structural pair. Best-effort;

+    let deliver = |from: &str, body: &str| {

+        let event = spt_msg::emit::render_event_whole(from, body);

+        spt_live::record_context_injection(id, spt_live::KIND_OWL_MESSAGE, &event);

-    for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {

+    for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {

// One pulse tick: ingest drops + fire a pending echo (T4/T3/T5).

let pulse = |host: &Option<super::live::LiveHost>| {


    if once {
        if let Some(msg) = listener.check_message_blocking() {
            deliver(&msg);
        }
        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&msg);
        }
        pulse(&live);
        return 0;
    }
    loop {
        if let Some(msg) = listener.check_message_blocking() {
            deliver(&msg);
        }
        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&msg);
        }
        pulse(&live);
    }
}

    if once {
        if let Some((from, body)) = listener.check_message_blocking() {
            deliver(&from, &body);
        }
        for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&m.from, &m.body);
        }
        pulse(&live);
        return 0;
    }
    loop {
        if let Some((from, body)) = listener.check_message_blocking() {
            deliver(&from, &body);
        }
        for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&m.from, &m.body);
        }
        pulse(&live);
    }
}


<owl_messages>
<owl_message from="doyle" priority="high">ACK (resend — prior went to myself, --reply-to bug). Backing off: hands off the tree, no edits, no gate on live working dir. Design-check confirmed resolved (3 blessed + 2 re-emphases). Read-only inspection of your spt-msg/spt-store half = strong (structural encode/decode, DrainedMsg, render_event_whole F-002 split, adversarial typed-from-wins no-double-wrap test = exact invariant). My cargo build was RED as you predicted (spt-daemon relay.rs not ported) — in-flight, not a finding. Finish + self-verify (suite + clippy -D + traceable EXIT=0 + xtask) + COMMIT. Ping green SHA, I gate from commit not tree. Go.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


    if once {
        if let Some(msg) = listener.check_message_blocking() {
            deliver(&msg);
        }
        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&msg);
        }
        pulse(&live);
        return 0;
    }
    loop {
        if let Some(msg) = listener.check_message_blocking() {
            deliver(&msg);
        }
        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&msg);
        }
        pulse(&live);
    }
}

    if once {
        if let Some((from, body)) = listener.check_message_blocking() {
            deliver(&from, &body);
        }
        for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&m.from, &m.body);
        }
        pulse(&live);
        return 0;
    }
    loop {
        if let Some((from, body)) = listener.check_message_blocking() {
            deliver(&from, &body);
        }
        for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {
            deliver(&m.from, &m.body);
        }
        pulse(&live);
    }
}

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

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

-        for msg in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {

+        for m in spool::drain_non_deferred_at(&perch_path).unwrap_or_default() {


doyle backing off, gate-from-commit confirmed, his RED was the expected in-flight relay.rs. Collision resolved. Quick ack + keep building (worker.rs next).


PowerShell

$msg = @'
Ack — clean handoff, you're hands-off, I gate-from-commit. Correct: relay.rs (spt-daemon) is exactly the not-yet-ported surface. Continuing: spt api (worker next) → spt-daemon (relay/notif double-prefix/psyrelay/access/shellchan) → spt-live outbound → spt-net wanmsg comment → test sweep (killer_quickstart + new poll-whole/listener-chunk ints). Ping you the green SHA. Standby.
'@
$msg | & $env:OWL send doyle
Write-Output "SEND_EXIT=$LASTEXITCODE"

Ack doyle handoff


➜ SENT:doyle
SEND_EXIT=0


60	/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):
61	/// remove its `ready` marker so it no longer counts as a live child; the
62	/// `info.json` + spool are preserved. Idempotent.
63	// [impl->REQ-API-2]
64	pub fn cmd_worker_stop(id: &str) -> i32 {
65	    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();
66	    if removed {
67	        eprintln!("WORKER_STOPPED:{id}");
68	    } else {
69	        eprintln!("WORKER_STOPPED:{id} (no ready marker)");
70	    }
71	    0
72	}
73
74	/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
75	/// to stdout. Non-deferred only unless `include_deferred`.
76	// [impl->REQ-API-2]
77	pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
78	    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
79	    // The resting gate (REQ-INST-6): a resting worker's deferred rows hold,
80	    // exactly as `poll_drain`'s — the KH 1.4 "all sites agree" discipline.
81	    // [impl->REQ-INST-6]
82	    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
83	    let drained = if include_deferred {
84	        spool::drain_all_at(&perch_path)
85	    } else {
86	        spool::drain_non_deferred_at(&perch_path)
87	    };
88	    for msg in drained.unwrap_or_default() {
89	        use std::io::Write;
90	        print!("{msg}");
91	        let _ = std::io::stdout().flush();
92	    }
93	    0
94	}
95
96	/// The paths of `parent_id`'s **live** nested children (those still carrying a
97	/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.
98	// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
99	pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {
100	    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join("nested");
101	    let mut live = Vec::new();
102	    let Ok(entries) = std::fs::read_dir(&nested) else {
103	        return live;
104	    };
105	    for entry in entries.flatten() {
106	        let child = entry.path();
107	        if !child.is_dir() {
108	            continue;
109	        }
110	        // A child is live while its ready marker exists (info.json present =
111	        // it's a real perch; ready = not yet soft-cleaned).
112	        if info::read_info(&child).is_some() && child.join("ready").exists() {
113	            live.push(child);
114	        }
115	    }
116	    live
117	}
118
119	fn now_stamp() -> String {
120	    std::time::SystemTime::now()
121	        .duration_since(std::time::UNIX_EPOCH)
122	        .map(|d| d.as_secs().to_string())
123	        .unwrap_or_else(|_| "0".to_string())
124	}
125
126	#[cfg(test)]
127	mod tests {
128	    use super::*;
129	    use crate::testutil::isolated_home;
130
131	    fn establish_parent(id: &str) {
132	        let path = perch::resolve_perch_path(id, ParentHint::Infer);
133	        std::fs::create_dir_all(&path).unwrap();
134	        info::write_info(
135	            &path,
136	            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
137	        )
138	        .unwrap();
139	    }
140
141	    #[test] // [unit->REQ-API-2] a worker nests under its parent and is addressable.
142	    fn worker_start_nests_under_parent() {
143	        let _h = isolated_home();
144	        establish_parent("alice");
145	        assert_eq!(cmd_worker_start("alice", "alice-w1"), 0);
146
147	        let expected = perch::resolve_perch_path("alice", ParentHint::Infer)
148	            .join("nested")
149	            .join("alice-w1");
150	        assert!(expected.join("info.json").exists());
151	        assert!(expected.join("ready").exists());
152	        // Inferred resolution agrees with the explicit nesting.
153	        assert_eq!(
154	            perch::resolve_perch_path("alice-w1", ParentHint::Infer),
155	            expected
156	        );
157	    }
158
159	    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] a parent with a live worker
160	            // reports children; stopping the worker clears the guard.
161	    fn cascade_guard_tracks_live_children() {
162	        let _h = isolated_home();
163	        establish_parent("alice");
164	        assert!(live_child_paths("alice").is_empty(), "no children yet");
165
166	        cmd_worker_start("alice", "alice-w1");
167	        assert!(
168	            !live_child_paths("alice").is_empty(),
169	            "live worker must be seen"
170	        );
171	        assert_eq!(live_child_paths("alice").len(), 1);
172
173	        // Soft-stop the worker → ready marker gone → no longer a live child.
174	        assert_eq!(cmd_worker_stop("alice-w1"), 0);
175	        assert!(
176	            live_child_paths("alice").is_empty(),
177	            "soft-stopped worker is not live"
178	        );
179	        // The record is preserved (soft cleanup), only the ready marker removed.
180	        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
181	        assert!(child.join("info.json").exists());
182	        assert!(!child.join("ready").exists());
183	    }
184
185	    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.
186	    fn worker_poll_drains_spool() {
187	        let _h = isolated_home();
188	        establish_parent("alice");
189	        cmd_worker_start("alice", "alice-w1");
190	        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
191	        spool::spool_message_at(&child, "alice", "task result").unwrap();
192
193	        let perch_path = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
194	        let got = spool::drain_non_deferred_at(&perch_path).unwrap();
195	        assert_eq!(got.len(), 1);
196	        // And the command path returns 0.
197	        cmd_worker_start("alice", "alice-w2");
198	        assert_eq!(cmd_worker_poll("alice-w2", false), 0);
199	    }

/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):
/// remove its `ready` marker so it no longer counts as a live child; the
/// `info.json` + spool are preserved. Idempotent.
// [impl->REQ-API-2]
pub fn cmd_worker_stop(id: &str) -> i32 {
    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();
    if removed {
        eprintln!("WORKER_STOPPED:{id}");
    } else {
        eprintln!("WORKER_STOPPED:{id} (no ready marker)");
    }
    0
}

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
/// to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    // The resting gate (REQ-INST-6): a resting worker's deferred rows hold,
    // exactly as `poll_drain`'s — the KH 1.4 "all sites agree" discipline.
    // [impl->REQ-INST-6]
    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for msg in drained.unwrap_or_default() {
        use std::io::Write;
        print!("{msg}");
        let _ = std::io::stdout().flush();
    }
    0
}

/// The paths of `parent_id`'s **live** nested children (those still carrying a
/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {
    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join("nested");
    let mut live = Vec::new();
    let Ok(entries) = std::fs::read_dir(&nested) else {
        return live;
    };
    for entry in entries.flatten() {
        let child = entry.path();
        if !child.is_dir() {
            continue;
        }
        // A child is live while its ready marker exists (info.json present =
        // it's a real perch; ready = not yet soft-cleaned).
        if info::read_info(&child).is_some() && child.join("ready").exists() {
            live.push(child);
        }
    }
    live
}

fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

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

    fn establish_parent(id: &str) {
        let path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&path).unwrap();
        info::write_info(
            &path,
            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
        )
        .unwrap();
    }

    #[test] // [unit->REQ-API-2] a worker nests under its parent and is addressable.
    fn worker_start_nests_under_parent() {
        let _h = isolated_home();
        establish_parent("alice");
        assert_eq!(cmd_worker_start("alice", "alice-w1"), 0);

        let expected = perch::resolve_perch_path("alice", ParentHint::Infer)
            .join("nested")
            .join("alice-w1");
        assert!(expected.join("info.json").exists());
        assert!(expected.join("ready").exists());
        // Inferred resolution agrees with the explicit nesting.
        assert_eq!(
            perch::resolve_perch_path("alice-w1", ParentHint::Infer),
            expected
        );
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] a parent with a live worker
            // reports children; stopping the worker clears the guard.
    fn cascade_guard_tracks_live_children() {
        let _h = isolated_home();
        establish_parent("alice");
        assert!(live_child_paths("alice").is_empty(), "no children yet");

        cmd_worker_start("alice", "alice-w1");
        assert!(
            !live_child_paths("alice").is_empty(),
            "live worker must be seen"
        );
        assert_eq!(live_child_paths("alice").len(), 1);

        // Soft-stop the worker → ready marker gone → no longer a live child.
        assert_eq!(cmd_worker_stop("alice-w1"), 0);
        assert!(
            live_child_paths("alice").is_empty(),
            "soft-stopped worker is not live"
        );
        // The record is preserved (soft cleanup), only the ready marker removed.
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        assert!(child.join("info.json").exists());
        assert!(!child.join("ready").exists());
    }

    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.
    fn worker_poll_drains_spool() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        spool::spool_message_at(&child, "alice", "task result").unwrap();

        let perch_path = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        let got = spool::drain_non_deferred_at(&perch_path).unwrap();
        assert_eq!(got.len(), 1);
        // And the command path returns 0.
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(cmd_worker_poll("alice-w2", false), 0);
    }


worker-poll → whole `<EVENT>`.


Edit

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
/// to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    // The resting gate (REQ-INST-6): a resting worker's deferred rows hold,
    // exactly as `poll_drain`'s — the KH 1.4 "all sites agree" discipline.
    // [impl->REQ-INST-6]
    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for msg in drained.unwrap_or_default() {
        use std::io::Write;
        print!("{msg}");
        let _ = std::io::stdout().flush();
    }
    0
}

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), each
/// message emitted as ONE whole `<EVENT>` envelope to stdout (ADR-0020; never
/// `<EVENT-PART>`). Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
// [impl->REQ-MSG-ENVELOPE]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    // The resting gate (REQ-INST-6): a resting worker's deferred rows hold,
    // exactly as `poll_drain`'s — the KH 1.4 "all sites agree" discipline.
    // [impl->REQ-INST-6]
    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for m in drained.unwrap_or_default() {
        use std::io::Write;
        println!("{}", spt_msg::emit::render_event_whole(&m.from, &m.body));
        let _ = std::io::stdout().flush();
    }
    0
}


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

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), each
/// message emitted as ONE whole `<EVENT>` envelope to stdout (ADR-0020; never
/// `<EVENT-PART>`). Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
// [impl->REQ-MSG-ENVELOPE]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    // The resting gate (REQ-INST-6): a resting worker's deferred rows hold,
    // exactly as `poll_drain`'s — the KH 1.4 "all sites agree" discipline.
    // [impl->REQ-INST-6]
    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for m in drained.unwrap_or_default() {
        use std::io::Write;
        println!("{}", spt_msg::emit::render_event_whole(&m.from, &m.body));
        let _ = std::io::stdout().flush();
    }
    0
}

//! Worker perches: `api worker-start` / `worker-stop` / `worker-poll`, plus the
//! **cascade-wipe guard** (KNOWN-HAZARDS 6.3).
//!
//! A worker is a nested child perch under a parent agent
//! (`owlery/<parent>/nested/<worker>/`), addressable like any perch and routed
//! through the *single* path resolver (REQ-HAZARD-SINGLE-PATH-SOURCE / WORKER-
//! PATH — no divergent sibling layouts).
//!
//! ## Cascade-wipe guard (6.3, REQ-HAZARD-CASCADE-WIPE-GUARD)
//! A destructive parent teardown must never hard-delete a parent that still
//! hosts **live** children. "Live" here = the child still carries its `ready`
//! marker (the soft-cleanup signal; `worker-stop` removes it). When live
//! children exist, the caller soft-cleans and surfaces their paths instead of
//! recursively deleting in-flight work. [`live_child_paths`] is the guard
//! primitive T7's `session-end` consults.
//
// [impl->REQ-API-2]
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]

use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::spool;

use super::auth;
use super::EXIT_REFUSED;

/// `api worker-start <parent> <id>` — create a nested worker perch under
/// `parent`. Resolves with an Explicit parent so the layout is unambiguous even
/// if the id's own suffix would infer a different one.
// [impl->REQ-API-2]
pub fn cmd_worker_start(parent: &str, id: &str) -> i32 {
    // [impl->REQ-HAZARD-ID-CHARSET] both parent and child ids must be well-formed.
    for name in [parent, id] {
        if let Err(e) = spt_proto::id::validate_endpoint_id(name) {
            eprintln!("INVALID_ID:{name}: {e}");
            return EXIT_REFUSED;
        }
    }
    let perch_path = perch::resolve_perch_path(id, ParentHint::Explicit(parent));
    if let Err(e) = std::fs::create_dir_all(&perch_path) {
        eprintln!("WORKER_START_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    let rec = InfoJson::new(id, &now_stamp(), std::process::id(), "", "worker");
    if let Err(e) = info::write_info(&perch_path, &rec) {
        eprintln!("WORKER_START_FAIL:{id}: {e}");
        return EXIT_REFUSED;
    }
    // ready marker = the live-child signal the cascade-wipe guard checks.
    let _ = std::fs::write(
        perch::resolve_ready_file(id, ParentHint::Explicit(parent)),
        "",
    );
    let token = auth::mint_token();
    let _ = auth::write_token(&perch_path, &token);
    eprintln!("WORKER_STARTED:{id} under {parent} token={token}");
    0
}

/// `api worker-stop <id>` — soft-clean a worker (REQ-HAZARD-SOFT-CLEANUP):
/// remove its `ready` marker so it no longer counts as a live child; the
/// `info.json` + spool are preserved. Idempotent.
// [impl->REQ-API-2]
pub fn cmd_worker_stop(id: &str) -> i32 {
    let removed = std::fs::remove_file(perch::resolve_ready_file(id, ParentHint::Infer)).is_ok();
    if removed {
        eprintln!("WORKER_STOPPED:{id}");
    } else {
        eprintln!("WORKER_STOPPED:{id} (no ready marker)");
    }
    0
}

/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed
/// to stdout. Non-deferred only unless `include_deferred`.
// [impl->REQ-API-2]
pub fn cmd_worker_poll(id: &str, include_deferred: bool) -> i32 {
    let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
    // The resting gate (REQ-INST-6): a resting worker's deferred rows hold,
    // exactly as `poll_drain`'s — the KH 1.4 "all sites agree" discipline.
    // [impl->REQ-INST-6]
    let include_deferred = include_deferred && !spt_daemon::resting::deferred_held(&perch_path);
    let drained = if include_deferred {
        spool::drain_all_at(&perch_path)
    } else {
        spool::drain_non_deferred_at(&perch_path)
    };
    for msg in drained.unwrap_or_default() {
        use std::io::Write;
        print!("{msg}");
        let _ = std::io::stdout().flush();
    }
    0
}

/// The paths of `parent_id`'s **live** nested children (those still carrying a
/// `ready` marker). Empty ⇒ the parent is safe to hard-delete.
// [impl->REQ-HAZARD-CASCADE-WIPE-GUARD]
pub fn live_child_paths(parent_id: &str) -> Vec<std::path::PathBuf> {
    let nested = perch::resolve_perch_path(parent_id, ParentHint::Infer).join("nested");
    let mut live = Vec::new();
    let Ok(entries) = std::fs::read_dir(&nested) else {
        return live;
    };
    for entry in entries.flatten() {
        let child = entry.path();
        if !child.is_dir() {
            continue;
        }
        // A child is live while its ready marker exists (info.json present =
        // it's a real perch; ready = not yet soft-cleaned).
        if info::read_info(&child).is_some() && child.join("ready").exists() {
            live.push(child);
        }
    }
    live
}

fn now_stamp() -> String {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".to_string())
}

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

    fn establish_parent(id: &str) {
        let path = perch::resolve_perch_path(id, ParentHint::Infer);
        std::fs::create_dir_all(&path).unwrap();
        info::write_info(
            &path,
            &InfoJson::new(id, "0", std::process::id(), "sid", "live_agent"),
        )
        .unwrap();
    }

    #[test] // [unit->REQ-API-2] a worker nests under its parent and is addressable.
    fn worker_start_nests_under_parent() {
        let _h = isolated_home();
        establish_parent("alice");
        assert_eq!(cmd_worker_start("alice", "alice-w1"), 0);

        let expected = perch::resolve_perch_path("alice", ParentHint::Infer)
            .join("nested")
            .join("alice-w1");
        assert!(expected.join("info.json").exists());
        assert!(expected.join("ready").exists());
        // Inferred resolution agrees with the explicit nesting.
        assert_eq!(
            perch::resolve_perch_path("alice-w1", ParentHint::Infer),
            expected
        );
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] a parent with a live worker
            // reports children; stopping the worker clears the guard.
    fn cascade_guard_tracks_live_children() {
        let _h = isolated_home();
        establish_parent("alice");
        assert!(live_child_paths("alice").is_empty(), "no children yet");

        cmd_worker_start("alice", "alice-w1");
        assert!(
            !live_child_paths("alice").is_empty(),
            "live worker must be seen"
        );
        assert_eq!(live_child_paths("alice").len(), 1);

        // Soft-stop the worker → ready marker gone → no longer a live child.
        assert_eq!(cmd_worker_stop("alice-w1"), 0);
        assert!(
            live_child_paths("alice").is_empty(),
            "soft-stopped worker is not live"
        );
        // The record is preserved (soft cleanup), only the ready marker removed.
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        assert!(child.join("info.json").exists());
        assert!(!child.join("ready").exists());
    }

    #[test] // [unit->REQ-API-2] worker-poll drains the worker's own spool.
    fn worker_poll_drains_spool() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        spool::spool_message_at(&child, "alice", "task result").unwrap();

        let perch_path = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        let got = spool::drain_non_deferred_at(&perch_path).unwrap();
        assert_eq!(got.len(), 1);
        // And the command path returns 0.
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(cmd_worker_poll("alice-w2", false), 0);
    }

    // [unit->REQ-INST-6] the resting gate agrees at the worker drain (the
    // KH 1.4 "all sites agree" discipline): a resting worker's deferred row
    // survives an include-deferred poll undelivered; the wake edge releases
    // it through the same command.
    #[test]
    fn worker_poll_holds_deferred_while_resting() {
        use spt_daemon::resting::{write_rest, RestState};
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        let child = perch::resolve_perch_path("alice-w1", ParentHint::Infer);
        spool::spool_message_deferred_at(&child, "alice", "deferred task").unwrap();

        write_rest(&child, RestState::Suspended, 1_000).unwrap();
        assert_eq!(cmd_worker_poll("alice-w1", true), 0);
        assert_eq!(
            spool::pending_count_at(&child).unwrap(),
            1,
            "deferred row held through a resting include-deferred poll"
        );

        write_rest(&child, RestState::Active, 2_000).unwrap();
        assert_eq!(cmd_worker_poll("alice-w1", true), 0);
        assert_eq!(
            spool::pending_count_at(&child).unwrap(),
            0,
            "wake released it"
        );
    }

    #[test] // [unit->REQ-HAZARD-CASCADE-WIPE-GUARD] two workers: guard holds until
            // the last live child is stopped.
    fn guard_clears_only_when_all_children_stopped() {
        let _h = isolated_home();
        establish_parent("alice");
        cmd_worker_start("alice", "alice-w1");
        cmd_worker_start("alice", "alice-w2");
        assert_eq!(live_child_paths("alice").len(), 2);
        cmd_worker_stop("alice-w1");
        assert!(
            !live_child_paths("alice").is_empty(),
            "one child still live"
        );
        cmd_worker_stop("alice-w2");
        assert!(live_child_paths("alice").is_empty());
    }
}

-/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), framed

-/// to stdout. Non-deferred only unless `include_deferred`.

+/// `api worker-poll <id>` — drain a worker perch's spool (oldest first), each

+/// message emitted as ONE whole `<EVENT>` envelope to stdout (ADR-0020; never

+/// `<EVENT-PART>`). Non-deferred only unless `include_deferred`.

-    for msg in drained.unwrap_or_default() {

+    for m in drained.unwrap_or_default() {

+        println!("{}", spt_msg::emit::render_event_whole(&m.from, &m.body));


spt T3 done. Now spt-daemon. Read relay.rs (drain + check_message consumer).