diff --git a/crates/spt-msg/src/emit.rs b/crates/spt-msg/src/emit.rs
index d9aad58..f4069b8 100644
--- a/crates/spt-msg/src/emit.rs
+++ b/crates/spt-msg/src/emit.rs
@@ -1,80 +1,86 @@
-//! Listener-stream EVENT rendering: raw wire/spool frame → the `<EVENT …>`
-//! envelope lines a harness agent parses off the Monitor stream.
+//! Delivery-edge EVENT rendering: the structural `(from, body)` pair → the
+//! `<EVENT …>` envelope every arriving-message surface emits (ADR-0020).
 //!
-//! Clean-room re-expression of the sister `owl/poll.rs` emit path
-//! (`parse_tcp_message` → `assemble_event_line` → `chunk_if_oversized`); the
-//! envelope **format** itself is the ADR-0001 copy-verbatim grammar already
-//! housed in [`spt_proto::event`]. This module is the missing glue the M1
-//! listener loops shipped without: they printed the raw
-//! `__REPLY_TO__:{from}\n{body}` frame straight to stdout, so receiving
-//! agents saw a bare body instead of `<EVENT type="msg" from="…">…</EVENT>`.
+//! The envelope **format** is the ADR-0001 copy-verbatim grammar housed in
+//! [`spt_proto::event`]; this module composes it **once, at the delivery edge**
+//! from the structural `(from, body)` carried through the wire/spool — there is
+//! no `__REPLY_TO__` parse step (that relic, and the serialize-then-reparse
+//! round-trip it forced, are gone).
 //!
-//! Scope: the **listener stream** only (`spt ready` / `spt api listen` — the
-//! `« spt event »` Monitor loops). Hook-channel drains (`spt api poll`,
-//! `api worker-poll`) keep the raw frame by contract — their consumer is the
-//! adapter's hook composer, not the event stream.
+//! Two edges, same grammar, one framing nuance:
+//! - [`render_event_lines`] — the **listener stream** (`spt ready` /
+//!   `spt api listen`, the `« spt event »` Monitor loops): composes **and**
+//!   chunks oversized lines into `<EVENT-PART …>` groups so the Monitor
+//!   notification cap holds.
+//! - [`render_event_whole`] — the **hook drains** (`spt api poll`,
+//!   `api worker-poll`): composes **one whole** `<EVENT>` per message, **never**
+//!   chunked (the adapter's own context channel has no per-line cap; any
+//!   injection sizing is the adapter's job — the harness-agnostic boundary).
 // [impl->REQ-MSG-4]
+// [impl->REQ-MSG-ENVELOPE]

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

-/// Parse a raw delivered frame into `(from, body)`.
-/// Frame shape: `__REPLY_TO__:{from}\n{body}` or just `{body}` (anonymous —
-/// `from` is `""`). Trailing newlines are trimmed off the body (the TCP wire
-/// adds one; spool rows don't). Mirrors the sister's `parse_tcp_message`.
-// [impl->REQ-MSG-4]
-pub fn parse_frame(msg: &str) -> (String, String) {
-    if let Some(rest) = msg.strip_prefix("__REPLY_TO__:") {
-        if let Some(newline_pos) = rest.find('\n') {
-            let from = rest[..newline_pos].to_string();
-            let body = rest[newline_pos + 1..].trim_end_matches('\n').to_string();
-            return (from, body);
-        }
+/// Compose the canonical `<EVENT>` line for a structural `(from, body)`:
+///
+/// 1. a body that is already a fully-formed typed `<EVENT …>…</EVENT>` envelope
+///    (echo_commune, notify, user-msg, …) passes through **verbatim** — never
+///    re-wrapped, and the structural `from` is ignored in favor of the body's
+///    own `from` (the echo-commune-double-envelope fix);
+/// 2. anything else composes `<EVENT type="msg" from="{from}">{body}</EVENT>`
+///    (attr/body escaped; anonymous delivery renders `from=""`).
+// [impl->REQ-MSG-ENVELOPE]
+fn compose_line(from: &str, body: &str) -> (String, bool) {
+    if is_typed_event_envelope(body) {
+        (body.to_string(), true)
+    } else {
+        (compose_msg_event(from, body), false)
     }
-    (String::new(), msg.trim_end_matches('\n').to_string())
 }

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

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

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

-    // [unit->REQ-MSG-4] anonymous frame (no __REPLY_TO__ line) still gets the
-    // envelope — from="" — never a bare body on the stream.
+    // [unit->REQ-MSG-4] anonymous (from="") still gets the envelope — from="" —
+    // never a bare body on the stream.
     #[test]
-    fn anonymous_frame_still_enveloped() {
-        let lines = render_event_lines("no header here\n");
+    fn anonymous_still_enveloped() {
+        let lines = render_event_lines("", "no header here");
         assert_eq!(
             lines,
             vec![r#"<EVENT type="msg" from="">no header here</EVENT>"#.to_string()]
@@ -85,7 +91,7 @@ mod tests {
     // through the receiver-side parser.
     #[test]
     fn multiline_body_escapes_and_round_trips() {
-        let lines = render_event_lines("__REPLY_TO__:bob\nline one\nline two\n");
+        let lines = render_event_lines("bob", "line one\nline two");
         assert_eq!(lines.len(), 1);
         assert!(lines[0].contains("line one<br>line two"));
         let p = parse_event(&lines[0]).unwrap();
@@ -93,59 +99,66 @@ mod tests {
         assert_eq!(p.body, "line one\nline two");
     }

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

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

-    // [unit->REQ-MSG-4] an oversized assembled line chunks into EVENT-PART
-    // lines that reassemble byte-exact.
+    // [unit->REQ-MSG-ENVELOPE] the hook-drain edge composes a WHOLE plain-msg
+    // <EVENT> — never an EVENT-PART, even for an oversized body the listener
+    // would chunk. Self-delimiting by construction.
     #[test]
-    fn oversized_line_chunks_into_event_parts() {
+    fn whole_render_never_chunks() {
         let body = "x".repeat(1500);
-        let frame = format!("__REPLY_TO__:doyle\n{body}");
-        let lines = render_event_lines(&frame);
-        assert!(lines.len() > 1, "1500-char body must chunk");
-        assert!(lines.iter().all(|l| l.starts_with("<EVENT-PART ")));
-        let whole = spt_proto::chunk::reassemble_group(&lines).expect("reassembles");
+        let whole = render_event_whole("doyle", &body);
+        assert!(whole.starts_with("<EVENT "), "one whole envelope");
+        assert!(!whole.contains("<EVENT-PART"), "hook drain never chunks");
         let p = parse_event(&whole).unwrap();
         assert_eq!(p.from(), Some("doyle"));
         assert_eq!(p.body, body);
     }

-    // [unit->REQ-MSG-4] parse_frame splits header/body and trims only
-    // trailing newlines.
+    // [unit->REQ-MSG-4] an oversized listener line chunks into EVENT-PART lines
+    // that reassemble byte-exact (listener stream unchanged — still chunks).
     #[test]
-    fn parse_frame_shapes() {
-        assert_eq!(
-            parse_frame("__REPLY_TO__:a\nb\n"),
-            ("a".to_string(), "b".to_string())
-        );
-        assert_eq!(parse_frame("bare\n"), (String::new(), "bare".to_string()));
-        // Header with multi-line body keeps interior newlines.
-        assert_eq!(
-            parse_frame("__REPLY_TO__:a\nx\ny\n"),
-            ("a".to_string(), "x\ny".to_string())
-        );
+    fn oversized_listener_line_chunks_into_event_parts() {
+        let body = "x".repeat(1500);
+        let lines = render_event_lines("doyle", &body);
+        assert!(lines.len() > 1, "1500-char body must chunk");
+        assert!(lines.iter().all(|l| l.starts_with("<EVENT-PART ")));
+        let whole = spt_proto::chunk::reassemble_group(&lines).expect("reassembles");
+        let p = parse_event(&whole).unwrap();
+        assert_eq!(p.from(), Some("doyle"));
+        assert_eq!(p.body, body);
     }
 }
diff --git a/crates/spt-store/src/spool.rs b/crates/spt-store/src/spool.rs
index eaea0a2..f9ab637 100644
--- a/crates/spt-store/src/spool.rs
+++ b/crates/spt-store/src/spool.rs
@@ -85,15 +85,18 @@ fn touch_has_messages(perch_path: &Path) {
     let _ = std::fs::File::create(perch_path.join(".has-messages"));
 }

-/// Format a drained row for delivery, preserving the sister's `__REPLY_TO__`
-/// convention: anonymous senders (`from_id == ""`) yield the bare body;
-/// named senders prepend a reply-routing header.
-fn format_row(from_id: &str, body: &str) -> String {
-    if from_id.is_empty() {
-        body.to_string()
-    } else {
-        format!("__REPLY_TO__:{}\n{}", from_id, body)
-    }
+/// A drained spool row carried **structurally** as `(from, body)` (ADR-0020).
+/// `from` is `""` for an anonymous sender. The `<EVENT>` envelope is composed
+/// once at the delivery edge from this pair — the spool no longer serializes a
+/// `__REPLY_TO__` frame (the relic round-trip is gone).
+// [impl->REQ-MSG-ENVELOPE]
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DrainedMsg {
+    /// Reply-routing sender id; `""` = anonymous (no reply target).
+    pub from: String,
+    /// Message body (may itself be a pre-formed typed `<EVENT>` envelope, which
+    /// the delivery edge passes through verbatim).
+    pub body: String,
 }

 /// Insert a normal (non-deferred) message and touch `.has-messages`.
@@ -206,19 +209,19 @@ fn purge_expired(conn: &Connection) -> rusqlite::Result<()> {
 /// what lets a deferred row reach the user even though the event stream skips
 /// it.
 // [impl->REQ-HAZARD-DEFERRED-SURVIVE-DRAIN]
-pub fn drain_all_at(perch_path: &Path) -> rusqlite::Result<Vec<String>> {
+pub fn drain_all_at(perch_path: &Path) -> rusqlite::Result<Vec<DrainedMsg>> {
     drain_where(perch_path, false)
 }

 /// Drain only undelivered **non-deferred** rows (the event-stream path).
 /// Deferred rows are left untouched and undelivered.
 // [impl->REQ-HAZARD-DEFERRED-DRAIN]
-pub fn drain_non_deferred_at(perch_path: &Path) -> rusqlite::Result<Vec<String>> {
+pub fn drain_non_deferred_at(perch_path: &Path) -> rusqlite::Result<Vec<DrainedMsg>> {
     drain_where(perch_path, true)
 }

 /// Shared drain body. `non_deferred_only` adds the `deferred = 0` predicate.
-fn drain_where(perch_path: &Path, non_deferred_only: bool) -> rusqlite::Result<Vec<String>> {
+fn drain_where(perch_path: &Path, non_deferred_only: bool) -> rusqlite::Result<Vec<DrainedMsg>> {
     let conn = open_spool_at(perch_path)?;
     purge_expired(&conn)?;
     let sql = if non_deferred_only {
@@ -239,18 +242,21 @@ fn drain_where(perch_path: &Path, non_deferred_only: bool) -> rusqlite::Result<V
         .collect();

     let mut out = Vec::with_capacity(rows.len());
-    for (id, from_id, body) in &rows {
+    for (id, from_id, body) in rows {
         conn.execute(
             "UPDATE messages SET delivered = 1 WHERE id = ?1",
             params![id],
         )?;
-        out.push(format_row(from_id, body));
+        out.push(DrainedMsg {
+            from: from_id,
+            body,
+        });
     }
     Ok(out)
 }

 /// Drain the single oldest undelivered row (any deferred status), or `None`.
-pub fn drain_one_at(perch_path: &Path) -> rusqlite::Result<Option<String>> {
+pub fn drain_one_at(perch_path: &Path) -> rusqlite::Result<Option<DrainedMsg>> {
     let conn = open_spool_at(perch_path)?;
     purge_expired(&conn)?;
     let row = conn.query_row(
@@ -270,7 +276,10 @@ pub fn drain_one_at(perch_path: &Path) -> rusqlite::Result<Option<String>> {
                 "UPDATE messages SET delivered = 1 WHERE id = ?1",
                 params![id],
             )?;
-            Ok(Some(format_row(&from_id, &body)))
+            Ok(Some(DrainedMsg {
+                from: from_id,
+                body,
+            }))
         }
         Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
         Err(e) => Err(e),
@@ -363,8 +372,14 @@ mod tests {
         assert_eq!(
             drained,
             vec![
-                "__REPLY_TO__:alice\nfirst".to_string(),
-                "__REPLY_TO__:carol\nsecond".to_string()
+                DrainedMsg {
+                    from: "alice".to_string(),
+                    body: "first".to_string()
+                },
+                DrainedMsg {
+                    from: "carol".to_string(),
+                    body: "second".to_string()
+                },
             ],
             "only non-deferred rows, in id order"
         );
@@ -383,7 +398,10 @@ mod tests {
         // Event-stream drain takes only the normal row.
         assert_eq!(
             drain_non_deferred_at(p).unwrap(),
-            vec!["__REPLY_TO__:alice\nnormal"]
+            vec![DrainedMsg {
+                from: "alice".to_string(),
+                body: "normal".to_string()
+            }]
         );
         // Deferred row is still pending (survived).
         let peeked = peek_all_at(p).unwrap();
@@ -401,7 +419,13 @@ mod tests {
         );

         // Hook path (unfiltered) delivers it.
-        assert_eq!(drain_all_at(p).unwrap(), vec!["__REPLY_TO__:psyche\nbrief"]);
+        assert_eq!(
+            drain_all_at(p).unwrap(),
+            vec![DrainedMsg {
+                from: "psyche".to_string(),
+                body: "brief".to_string()
+            }]
+        );
         // Now nothing remains.
         assert!(peek_all_at(p).unwrap().is_empty());
     }
@@ -418,7 +442,16 @@ mod tests {
         let first = drain_all_at(p).unwrap();
         assert_eq!(
             first,
-            vec!["__REPLY_TO__:alice\nx".to_string(), "anon".to_string()]
+            vec![
+                DrainedMsg {
+                    from: "alice".to_string(),
+                    body: "x".to_string()
+                },
+                DrainedMsg {
+                    from: "".to_string(),
+                    body: "anon".to_string()
+                },
+            ]
         );
         assert!(
             drain_all_at(p).unwrap().is_empty(),
@@ -447,12 +480,17 @@ mod tests {
             spool_message_at(p, "", part).unwrap(); // anon → bare body, no header
         }
         let drained = drain_all_at(p).unwrap();
+        let bodies: Vec<String> = drained.iter().map(|m| m.body.clone()).collect();
+        assert!(
+            drained.iter().all(|m| m.from.is_empty()),
+            "anon rows carry no from"
+        );
         assert_eq!(
-            drained, parts,
+            bodies, parts,
             "EVENT-PART bodies persist and drain verbatim, in order"
         );
         // And they reassemble back to the original line.
-        assert_eq!(spt_proto::chunk::reassemble_group(&drained).unwrap(), line);
+        assert_eq!(spt_proto::chunk::reassemble_group(&bodies).unwrap(), line);
     }

     // [unit->REQ-HAZARD-DEFERRED-DRAIN] schema migrations are idempotent across


spool.rs → structural `DrainedMsg{from,body}`, emit.rs → `render_event_lines` (chunked listener) + `render_event_whole` (whole hook-drain, F-002 dissolves). Adversarial no-double-wrap test present — exact invariant I gate. Reading remaining diffs (ready/listener/deliver/access) while build runs.


Bash

cd C:/Users/decid/Documents/projects/spt-core; git diff crates/spt-msg/src/ready.rs crates/spt-msg/src/listener.rs crates/spt-msg/src/deliver.rs crates/spt-store/src/access.rs

Read ready/listener/deliver/access diffs


diff --git a/crates/spt-msg/src/deliver.rs b/crates/spt-msg/src/deliver.rs
index 26326e6..ca53e44 100644
--- a/crates/spt-msg/src/deliver.rs
+++ b/crates/spt-msg/src/deliver.rs
@@ -122,7 +122,7 @@ pub fn deliver_tcp(target: &str, from: &str, body: &str, owlery: &Path) -> bool
 }

 /// Core delivery primitive: TCP-first, spool-fallback. Returns which path the
-/// message took. `from` is written into the `__REPLY_TO__` line (empty =
+/// message took. `from` is carried structurally on the wire/spool (empty =
 /// anonymous). Used by `send`, `ring`, and reply routing alike.
 // [impl->REQ-MSG-1]
 // [impl->REQ-HAZARD-INBOX-NO-DOUBLE]
@@ -144,7 +144,7 @@ pub fn deliver(target: &str, from: &str, body: &str, owlery: &Path) -> DeliveryP
 /// I/O, no exit — the caller renders the outcome.
 ///
 /// Reply routing is *this same call* with `from` set to the replier's id: the
-/// `__REPLY_TO__` line the original message carried names the target, and the
+/// structural `from` the original message carried names the target, and the
 /// reply's own `from` lets the original sender reply back in turn.
 // [impl->REQ-MSG-1]
 pub fn send(target: &str, from: &str, body: &str, owlery: &Path) -> SendOutcome {
@@ -222,7 +222,7 @@ mod tests {
             }
             std::thread::sleep(Duration::from_millis(5));
         }
-        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nlive msg\n"));
+        assert_eq!(got, Some(("sender".to_string(), "live msg".to_string())));

         // No spooled copy accrued for an online delivery.
         let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
@@ -318,7 +318,8 @@ mod tests {
         assert_eq!(spool::pending_count_at(&perch_path).unwrap(), 1);
         let all = spool::drain_all_at(&perch_path).unwrap();
         assert_eq!(all.len(), 1);
-        assert!(all[0].contains("later"));
+        assert_eq!(all[0].from, "hooker");
+        assert!(all[0].body.contains("later"));
     }

     // [unit->REQ-MSG-1] deferred send to an absent perch is NoPerch; empty is Empty.
diff --git a/crates/spt-msg/src/listener.rs b/crates/spt-msg/src/listener.rs
index 8df9f13..fb7ad9c 100644
--- a/crates/spt-msg/src/listener.rs
+++ b/crates/spt-msg/src/listener.rs
@@ -125,8 +125,9 @@ impl PollListener {
     }

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

     /// Wait (up to the configured timeout, default 500 ms) for a connection via
-    /// the OS poll syscall, then accept+read one framed message.
-    pub fn check_message_blocking(&self) -> Option<String> {
+    /// the OS poll syscall, then accept+read one framed message as `(from, body)`.
+    pub fn check_message_blocking(&self) -> Option<(String, String)> {
         let timeout_ms = self.timeout_ms.unwrap_or(500);
         if poll_ready(&self.listener, timeout_ms) {
             match self.listener.accept() {
@@ -230,7 +231,7 @@ mod tests {
             }
             std::thread::sleep(Duration::from_millis(5));
         }
-        assert_eq!(got.as_deref(), Some("__REPLY_TO__:sender\nping\n"));
+        assert_eq!(got, Some(("sender".to_string(), "ping".to_string())));
     }

     // [unit->REQ-MSG-1] dropping a listener removes its registry address (no
@@ -276,6 +277,6 @@ mod tests {

         let got = listener.check_message_blocking();
         h.join().unwrap();
-        assert_eq!(got.as_deref(), Some("pong\n"));
+        assert_eq!(got, Some(("".to_string(), "pong".to_string())));
     }
 }
diff --git a/crates/spt-msg/src/ready.rs b/crates/spt-msg/src/ready.rs
index 7f6ec84..742fc05 100644
--- a/crates/spt-msg/src/ready.rs
+++ b/crates/spt-msg/src/ready.rs
@@ -46,7 +46,7 @@ impl ReadyAgent {
     /// drain the offline backlog. Returns the agent and any backlog messages
     /// (oldest first) that were spooled while it was down.
     // [impl->REQ-MSG-3]
-    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<String>), String> {
+    pub fn start(id: &str) -> Result<(ReadyAgent, Vec<(String, String)>), String> {
         Self::start_homed(id, None)
     }

@@ -61,7 +61,7 @@ impl ReadyAgent {
     pub fn start_homed(
         id: &str,
         subnet: Option<&str>,
-    ) -> Result<(ReadyAgent, Vec<String>), String> {
+    ) -> Result<(ReadyAgent, Vec<(String, String)>), String> {
         // [impl->REQ-HAZARD-ID-CHARSET] guard the id charset at the creation seam.
         spt_proto::id::validate_endpoint_id(id).map_err(|e| e.to_string())?;
         let perch_path = perch::resolve_perch_path(id, ParentHint::Infer);
@@ -103,9 +103,13 @@ impl ReadyAgent {
         let listener = PollListener::bind_blocking(id, RECV_POLL)?;

         // Drain the offline backlog (non-deferred only; deferred rows survive
-        // for the hook channel).
-        let backlog = spool::drain_non_deferred_at(&perch_path)
-            .map_err(|e| format!("Failed to drain spool backlog: {}", e))?;
+        // for the hook channel). Carried structurally as (from, body) — the
+        // delivery edge composes the <EVENT> (ADR-0020).
+        let backlog: Vec<(String, String)> = spool::drain_non_deferred_at(&perch_path)
+            .map_err(|e| format!("Failed to drain spool backlog: {}", e))?
+            .into_iter()
+            .map(|m| (m.from, m.body))
+            .collect();

         Ok((
             ReadyAgent {
@@ -123,17 +127,22 @@ impl ReadyAgent {
         &self.id
     }

-    /// Wait up to one poll window for a live TCP message. Returns `None` on
-    /// timeout (call again to keep listening).
-    pub fn recv(&self) -> Option<String> {
+    /// Wait up to one poll window for a live TCP message, returning the
+    /// structural `(from, body)`. Returns `None` on timeout (call again to keep
+    /// listening).
+    pub fn recv(&self) -> Option<(String, String)> {
         self.listener.check_message_blocking()
     }

     /// Drain any non-deferred spool rows that accrued since startup (e.g. a
     /// sender that spooled because it raced the listener bind, or a hook-channel
-    /// arrival). Oldest first.
-    pub fn drain_spool(&self) -> Vec<String> {
-        spool::drain_non_deferred_at(&self.perch_path).unwrap_or_default()
+    /// arrival), as `(from, body)`. Oldest first.
+    pub fn drain_spool(&self) -> Vec<(String, String)> {
+        spool::drain_non_deferred_at(&self.perch_path)
+            .unwrap_or_default()
+            .into_iter()
+            .map(|m| (m.from, m.body))
+            .collect()
     }

     /// Soft teardown (6.2): remove the `ready` marker and unregister the
@@ -231,7 +240,8 @@ mod tests {
         // Starting drains it.
         let (_agent, backlog) = ReadyAgent::start(&id).unwrap();
         assert_eq!(backlog.len(), 1);
-        assert!(backlog[0].contains("while you slept"));
+        assert_eq!(backlog[0].0, "sender");
+        assert!(backlog[0].1.contains("while you slept"));
     }

     // [unit->REQ-MSG-3] a live TCP message is received via recv().
@@ -257,7 +267,7 @@ mod tests {
             }
         }
         assert_eq!(h.join().unwrap(), deliver::DeliveryPath::Tcp);
-        assert_eq!(got.as_deref(), Some("__REPLY_TO__:peer\nhello live\n"));
+        assert_eq!(got, Some(("peer".to_string(), "hello live".to_string())));
         drop(agent);
     }

diff --git a/crates/spt-store/src/access.rs b/crates/spt-store/src/access.rs
index e354200..ed6e653 100644
--- a/crates/spt-store/src/access.rs
+++ b/crates/spt-store/src/access.rs
@@ -26,8 +26,9 @@
 //! ## Recent-outbound correlation (`trust/recent-outbound.json`)
 //!
 //! The whitelist gates the **unsolicited** class only: an inbound correlated
-//! to the endpoint's own recent outbound (`__REPLY_TO__` traffic) passes from
-//! any visible node — a stateful firewall, not a blanket block. The
+//! to the endpoint's own recent outbound (reply traffic, keyed on the inbound
+//! `from`) passes from any visible node — a stateful firewall, not a blanket
+//! block. The
 //! [`RecentOutbound`] log is the correlation state: the daemon records
 //! `(endpoint, dest_node)` when an endpoint sends off-node, and the gate's
 //! classifier asks whether `(endpoint, origin_node)` appears within the reply


<task-notification>
<task-id>bwf4pzfy8</task-id>
<tool-use-id>toolu_01YLgWHoZVWSAHeMW844qqix</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\bwf4pzfy8.output</output-file>
<status>completed</status>
<summary>Background command "Build workspace + traceable check" completed (exit code 0)</summary>
</task-notification>


Clean tuple-threading through ready/listener/deliver; access.rs doc-only. But toml claimed scope `spt/spt-daemon/spt-live` too — diff shows ONLY spt-msg+spt-store. If those crates call `recv()`/`render_event_lines`/`drain_spool` (sigs changed String→tuple), build breaks unless updated. Build is the gate signal. Check it.


<owl_messages>
<owl_message from="todlando" priority="high">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.</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.


1
2	error[E0308]: mismatched types
3	  --> crates\spt-daemon\src\relay.rs:75:18
4	   |
5	75 |             sink(&msg);
6	   |             ---- ^^^^ expected `&str`, found `&(String, String)`
7	   |             |
8	   |             arguments to this function are incorrect
9	   |
10	   = note: expected reference `&str`
11	              found reference `&(std::string::String, std::string::String)`
12	note: callable defined here
13	  --> crates\spt-daemon\src\relay.rs:73:25
14	   |
15	73 |     pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {
16	   |                         ^^^^^^^^^^^
17
18	For more information about this error, try `rustc --explain E0308`.
19	error: could not compile `spt-daemon` (lib) due to 2 previous errors
20	warning: build failed, waiting for other jobs to finish...
21	===TRACEABLE===
22	       Byte-stream remote terminal streaming for v1
23	  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
24	       Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push
25	  [OK] REQ-TERM-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
26	       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
27	  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
28	       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
29	  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
30	       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
31	  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
32	       Peer-propagated update over P2P
33	  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
34	       All binaries signature-verified before handoff
35	  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
36	       No endpoint process terminates/suspends during self-update
37	  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
38	       Update gated on user confirmation by default; opt-in full-auto
39	  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
40	       spt-core ripple-updates registered adapters
41	  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
42	       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
43	  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
44	       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
45	  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
46	       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
47	  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
48	       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
49
50	Findings:
51	  [must] missing_stage REQ-MSG-ENVELOPE stage=int — Requirement is missing integration test evidence
52

error[E0308]: mismatched types
  --> crates\spt-daemon\src\relay.rs:75:18
   |
75 |             sink(&msg);
   |             ---- ^^^^ expected `&str`, found `&(String, String)`
   |             |
   |             arguments to this function are incorrect
   |
   = note: expected reference `&str`
              found reference `&(std::string::String, std::string::String)`
note: callable defined here
  --> crates\spt-daemon\src\relay.rs:73:25
   |
73 |     pub fn pump_once<F: FnMut(&str)>(&self, mut sink: F) {
   |                         ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0308`.
error: could not compile `spt-daemon` (lib) due to 2 previous errors
warning: build failed, waiting for other jobs to finish...
===TRACEABLE===
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push
  [OK] REQ-TERM-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).

Findings:
  [must] missing_stage REQ-MSG-ENVELOPE stage=int — Requirement is missing integration test evidence


Prompt:
<task-notification>
<task-id>bwf4pzfy8</task-id>
<tool-use-id>toolu_01YLgWHoZVWSAHeMW844qqix</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\bwf4pzfy8.output</output-file>
<status>completed</status>
<summary>Background command "Build workspace + traceable check" completed (exit code 0)</summary>
</task-notification>


cd C:/Users/decid/Documents/projects/spt-core; $OWL send --reply-to todlando doyle <<'EOF'
ACK — backing off. Hands off the tree, no edits, no gate on the live working dir. Confirmed: design-check resolved (I blessed all 3 + the 2 re-emphases — adversarial typed-passthrough no-double-wrap, grep-clean incl test fixtures). My read of your spt-msg/spt-store half was just inspection (read-only) — liked it: encode/decode structural frame, DrainedMsg, render_event_whole splitting F-002 out clean, the adversarial typed-from-wins test is exactly the invariant. Only thing I ran was a cargo build — RED as you said (spt-daemon relay.rs pump_once sink still &str, not yet ported) + traceable shows REQ-MSG-ENVELOPE missing int (not started) — both expected for in-flight, not findings against you.

Standing by. Finish + self-verify (full suite + clippy -D + traceable EXIT=0 + xtask) + COMMIT. Ping me the green SHA — I gate from the commit, not the tree. Go.
EOF

ACK todlando, back off


➜ REPLIED:doyle


ACK'd. Standing by — todlando owns this tree, gate from his commit SHA not the live tree.

**Situation:**
- Working-tree refactor = **todlando's, in flight**, same branch `docs/msg-envelope-adr-0020`. Not mine to gate or touch.
- Design-check already resolved earlier this session (I blessed 3 decisions + 2 re-emphases). REQ-MSG-ENVELOPE execute is underway, not blocked.
- My `cargo build` = read-only (no source clobber). RED as expected: spt-daemon `relay.rs pump_once` sink still `&str`, not yet ported. traceable: REQ-MSG-ENVELOPE missing `int` (not started). Both = in-flight state, not defects.

**What I saw (read-only) = strong:** structural `encode/decode_frame` (`u32 len(from)+from+body`, body verbatim), `DrainedMsg{from,body}`, `render_event_whole` splitting F-002 out, adversarial typed-from-wins no-double-wrap test = exact invariant I gate.

**Next:** todlando finishes spt/spt-daemon/spt-net/spt-live + test sweep → self-verifies (suite + clippy -D + traceable EXIT=0 + xtask) → commits → pings green SHA. I gate from commit. No action till then. Poll delivers his SHA when ready — standing by.


<task-notification>
<task-id>bg7cqpvni</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="06f78178" type="msg" from="todlando">ACK — backing off. Hands off the tree, no edits, no gate on the live working dir. Confirmed: design-check resolved (I blessed all 3 + the 2 re-emphases — adversarial typed-passthrough no-double-wrap, grep-clean incl test fixtures). My read of your spt-msg/spt-store half was just inspection (read-only) — liked i</EVENT-PART>
<EVENT-PART seq="2/3" id="06f78178">t: encode/decode structural frame, DrainedMsg, render_event_whole splitting F-002 out clean, the adversarial typed-from-wins test is exactly the invariant. Only thing I ran was a cargo build — RED as you said (spt-daemon relay.rs pump_once sink still &amp;str, not yet ported) + traceable shows REQ-MSG-ENVELOPE missing int (not started) — b</EVENT-PART>
<EVENT-PART seq="3/3" id="06f78178">oth expected for in-flight, not findings against you.

Standing by. Finish + self-verify (full suite + clippy -D + traceable EXIT=0 + xtask) + COMMIT. Ping me the green SHA — I gate from the commit, not the tree. Go.</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


That's my own ACK text echoed back on the Monitor stream — no new info from todlando. Nothing to act on. Standing by for his green SHA.


you didn't send your message to todlando, you sent it to yourself. whatever comes after the --reply-to flag is the name for the reply to field, not the destination


Right — `--reply-to` sets reply-to field, not destination. My ACK went to myself (target defaulted to me; `doyle` was self-id). todlando never got it. First ping `send todlando doyle` was correct (SENT:todlando). Resending ACK properly:


cd C:/Users/decid/Documents/projects/spt-core; $OWL send todlando doyle <<'EOF'
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.
EOF

Resend ACK to todlando correctly


➜ SENT:todlando


SENT:todlando confirmed. ACK landed this time.

Standing by for todlando's green SHA — gate from commit, not tree. No action till then.


<task-notification>
<task-id>bg7cqpvni</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/2" id="86ca052c" type="msg" from="todlando">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 +</EVENT-PART>
<EVENT-PART seq="2/2" id="86ca052c"> new poll-whole/listener-chunk ints). Ping you the green SHA. Standby.</EVENT-PART></event>
</task-notification>


Clean handoff confirmed. todlando continuing through spt api → spt-daemon → spt-live → spt-net → test sweep, pings green SHA. No action from me till then. Standing by.


<task-notification>
<task-id>bg7cqpvni</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="63515fc4" type="msg" from="todlando">GATE READY — REQ-MSG-ENVELOPE / ADR-0020 impl committed.

SHA: 5da7fcd  (branch docs/msg-envelope-adr-0020, stacked on your docs commit 24e0628; both off main — one branch, docs+impl → one PR/merge)

Self-verify ALL GREEN (reproduce from the commit, not the tree — tree is now clean):
- cargo ne</EVENT-PART>
<EVENT-PART seq="2/7" id="63515fc4">xtest run --workspace: 1053 passed (6 leaky benign, 1 skipped)
- cargo test --workspace --doc: ok
- cargo clippy --workspace --all-targets -D warnings: clean
- traceable-reqs check: EXIT 0; REQ-MSG-ENVELOPE +doc +impl +unit +int
- grep __REPLY_TO__ across crates/: ZERO (incl all test fixtures; only docs/ADR + traceable-reqs.toml re</EVENT-PART>
<EVENT-PART seq="3/7" id="63515fc4">gistry titles retain it as history)
- cargo run -p xtask -- check: OK (CLI --from help reworded → reference.md regenerated)

Gate conditions → evidence:
- grep-clean: ✓ (27 files; format_row + parse_frame deleted; comments scrubbed to &quot;legacy reply-to&quot;)
- typed-passthrough adversarial no-double-wrap: emit.rs type</EVENT-PART>
<EVENT-PART seq="4/7" id="63515fc4">d_body_from_wins_over_structural_from_no_double_wrap (body from=bodyfrom rides structural from=structfrom → single verbatim &lt;EVENT&gt;, structfrom never leaks) on BOTH render_event_lines + render_event_whole; plus notif.rs structural-carriage + verbatim-passthrough assert
- poll/worker WHOLE never EVENT-PART + listener UNCHANGED chunks</EVENT-PART>
<EVENT-PART seq="5/7" id="63515fc4">: NEW int crates/spt/tests/poll_envelope_e2e.rs (2 msgs incl multiline → 2 whole self-delimiting lines, no &lt;EVENT-PART&gt;); listener chunk path = emit.rs oversized_listener_line_chunks_into_event_parts
- WAN edge composes: receive_wan spools (from,body) → DrainedMsg → poll/listen emit &lt;EVENT&gt; (wansend resting-gate tests drai</EVENT-PART>
<EVENT-PART seq="6/7" id="63515fc4">n via poll_drain→.body)
- reply-correlation + Psyche-routing on from: psyrelay anti-spoof (structural from = re-stamped psyche id, spoof never leaks), access.rs correlation comments rebind to `from`

Wire shape (decision 1 as blessed): u32 len(from)+from+body inside the existing outer 4-byte frame; encode_frame/decode_frame; typed b</EVENT-PART>
<EVENT-PART seq="7/7" id="63515fc4">odies ride verbatim. DrainedMsg{from,body} (decision 2). anon from=&quot;&quot; preserved (decision 3).

Yours to gate. Ping verdict.</EVENT-PART></event>
</task-notification>


GATE READY, SHA 5da7fcd. My job: independently verify from the commit, not trust self-report. Confirm git state + grep (fast), launch full suite (background).


cd C:/Users/decid/Documents/projects/spt-core; echo "===HEAD==="; git log --oneline -2; echo "===STATUS==="; git status --short; echo "===GREP __REPLY_TO__ in crates/==="; git grep -n "__REPLY_TO__" -- crates/ || echo "ZERO in crates/"; echo "===GREP elsewhere (expect docs/toml only)==="; git grep -ln "__REPLY_TO__" || echo none

Confirm HEAD, tree clean, grep __REPLY_TO__


===HEAD===
5da7fcd refactor(msg): <EVENT> envelope is the sole arriving format; remove __REPLY_TO__ (ADR-0020)
24e0628 docs(adr): ADR-0020 — <EVENT> envelope is the sole arriving-message format; __REPLY_TO__ removed
===STATUS===
===GREP __REPLY_TO__ in crates/===
ZERO in crates/
===GREP elsewhere (expect docs/toml only)===
CONTEXT.md
M1-PLAN.md
M4-D5-PLAN.md
M4-D7.5-PLAN.md
M4-PLAN.md
PRD.md
REQ-MSG-ENVELOPE-PLAN.md
docs-site/src/concepts/overview.md
docs-site/src/messaging/overview.md
docs-site/src/quickstart/messaging.md
docs/FAULT-MATRIX.md
docs/KNOWN-HAZARDS.md
docs/adr/0009-endpoint-access-control-model.md
docs/adr/0012-psyche-trust-boundary-stdout-event-outbound.md
docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md
traceable-reqs.toml


cd C:/Users/decid/Documents/projects/spt-core; echo "===CLIPPY==="; cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -15; echo "===NEXTEST==="; cargo nextest run --workspace 2>&1 | tail -25; echo "===TRACEABLE==="; traceable-reqs check 2>&1 | tail -5; echo "===XTASK==="; cargo run -p xtask -- check 2>&1 | tail -8

Gate suite: clippy + nextest + traceable + xtask


Command running in background with ID: b9rr434ui. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b9rr434ui.output. You will be notified when it completes. To check interim output, use Read on that file path.


HEAD=5da7fcd, tree clean, `__REPLY_TO__` ZERO in crates/. But docs-site (`messaging/overview.md`, `quickstart/messaging.md`, `concepts/overview.md`) + KNOWN-HAZARDS/FAULT-MATRIX still reference it — possible doc-drift if they describe it as the *live* wire format. Checking while suite runs.


cd C:/Users/decid/Documents/projects/spt-core; for f in docs-site/src/messaging/overview.md docs-site/src/quickstart/messaging.md docs-site/src/concepts/overview.md docs/KNOWN-HAZARDS.md docs/FAULT-MATRIX.md; do echo "=== $f ==="; git grep -n "__REPLY_TO__" -- "$f"; done

Inspect __REPLY_TO__ refs in docs-site + hazard docs


=== docs-site/src/messaging/overview.md ===
docs-site/src/messaging/overview.md:17:- **Reply routing.** Every message carries a `__REPLY_TO__` header;
=== docs-site/src/quickstart/messaging.md ===
docs-site/src/quickstart/messaging.md:161:  `__REPLY_TO__` wire header, surfaced as the envelope's `from` attribute);
=== docs-site/src/concepts/overview.md ===
docs-site/src/concepts/overview.md:48:ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.
=== docs/KNOWN-HAZARDS.md ===
docs/KNOWN-HAZARDS.md:348:- **Invariant:** the live-Psyche turn driver **MUST capture stdout** (a bounded, stdin-fed, stdout-captured invocation — **never** `Stdio::null()`); the daemon **MUST strip** every Psyche-supplied `from=`/target/routing attribute, **re-stamp `from=<self_id>`**, and **constrain routing** — `reply` → the `__REPLY_TO__` sender only, `notify` → the agent's own user/subnet only. Body validated per 4.1.
=== docs/FAULT-MATRIX.md ===
docs/FAULT-MATRIX.md:42:| 22 | **Psyche emits spoofed routing** (`<EVENT type="reply" from="evil" to="victim">` on its stdout) | None — the spoof is structurally inert: the intent parser carries body only (attrs unrepresentable), the daemon re-stamps `from=<psyche_id>` and routes to the `__REPLY_TO__` sender alone | The spoof target (receives nothing); the wire (only daemon-authored envelopes); every other agent's identity | None needed (anti-spoof is structural + re-stamp, ADR-0012 / KH 7.3) | `psyrelay.rs::spoofed_routing_is_stripped_and_restamped`; `spt-live outbound.rs::spoofed_routing_attrs_are_unrepresentable` |


Confirmed doc-drift — 5 files describe `__REPLY_TO__` as the **live** format, now false per ADR-0020:
- **docs-site** (3): `messaging/overview.md:17` "every message carries a `__REPLY_TO__` header", `quickstart:161` "`__REPLY_TO__` wire header", `concepts:48` "`__REPLY_TO__` routing" — user-facing, flatly wrong now.
- **KNOWN-HAZARDS:348** + **FAULT-MATRIX:42** — "reply → the `__REPLY_TO__` sender" — terminology-stale (behavior still holds: reply routes to original sender, now via structural `from`).

Real finding. docs+impl ride one PR → drift would merge. Check suite result.


1	===CLIPPY===
2	    Checking spt v0.7.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
3	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s
4	===NEXTEST===
5

===CLIPPY===
    Checking spt v0.7.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s
===NEXTEST===


Clippy clean. Nextest still running — will notify on completion. Holding. Doc-drift finding queued for the verdict.


<task-notification>
<task-id>b9rr434ui</task-id>
<tool-use-id>toolu_01DFfXqMhx4aPXWWoEUfbPLZ</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\13c96a50-2b78-4c16-a995-1da2b1836fd8\tasks\b9rr434ui.output</output-file>
<status>completed</status>
<summary>Background command "Gate suite: clippy + nextest + traceable + xtask" completed (exit code 0)</summary>
</task-notification>


Suite exit 0. Read full output to confirm counts.


1	===CLIPPY===
2	    Checking spt v0.7.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
3	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s
4	===NEXTEST===
5	        PASS [   0.030s] (1032/1054) spt-term reader::tests::auto_answers_synthetic_dsr_query
6	        PASS [   0.025s] (1033/1054) spt-term reader::tests::no_dsr_means_no_answer
7	        PASS [   0.031s] (1034/1054) spt-term reader::tests::dsr_matcher_finds_query_across_chunk_boundary
8	        PASS [   0.121s] (1035/1054) spt-term::dsr conpty_output_not_withheld_under_the_drain
9	        PASS [   5.507s] (1036/1054) spt-store contextstore::tests::two_tier_layout_and_commits
10	        PASS [   0.129s] (1037/1054) spt-term::inject ctrl_c_interrupts_the_child
11	        PASS [   0.138s] (1038/1054) spt-term::inject send_line_reaches_child_and_preserves_order
12	        PASS [   0.161s] (1039/1054) spt-term::dsr drain_forwards_child_output
13	        PASS [   0.040s] (1040/1054) xtask::bin/xtask converge_tests::converge_table_renders_rows_and_success_predicate
14	        PASS [   0.060s] (1041/1054) spt-term::surface resize_under_load_does_not_hang_or_panic
15	        PASS [   0.138s] (1042/1054) spt-term::stream delivers_every_value_under_resize
16	        PASS [   9.938s] (1043/1054) spt-store contextstore::tests::conflict_artifacts_preserve_both_versions
17	        PASS [   1.064s] (1044/1054) spt-term::stream bounded_backpressure_stalls_an_unconsumed_reader
18	        PASS [   7.982s] (1045/1054) spt-store contextstore::tests::rename_ripples_both_tiers
19	        PASS [  11.291s] (1046/1054) spt-store contextstore::tests::fork_copies_both_tiers_then_diverges
20	        PASS [  11.790s] (1047/1054) spt-store contextstore::tests::remove_endpoint_removes_exactly_the_source
21	        PASS [   8.362s] (1048/1054) spt-store syncmerge::tests::legacy_bottom_vectors_surface_not_overwrite
22	        PASS [   8.700s] (1049/1054) spt-store syncmerge::tests::adopt_then_incremental_then_idempotent_repull
23	        PASS [   8.899s] (1050/1054) spt-store syncmerge::tests::project_branch_unions_disjoint_agents
24	        PASS [   9.638s] (1051/1054) spt-store syncmerge::tests::dominated_incoming_drops_but_joins_dag
25	        PASS [   9.826s] (1052/1054) spt-store syncmerge::tests::concurrent_writes_surface_on_both_nodes
26	        PASS [  14.597s] (1053/1054) spt-store info::tests::concurrent_read_during_writes_never_torn
27	        PASS [  10.449s] (1054/1054) spt-store syncmerge::tests::reconciled_write_propagates_and_clears_artifacts
28	────────────
29	     Summary [  33.870s] 1054 tests run: 1054 passed (6 leaky), 1 skipped
30	===TRACEABLE===
31	       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
32	  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
33	       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
34	  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
35	       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
36	===XTASK===
37
38	Caused by:
39	  Access is denied. (os error 5)
40
41	thread 'main' (149636) panicked at crates\xtask\src\main.rs:155:5:
42	building spt failed
43	note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
44	error: process didn't exit successfully: `target\debug\xtask.exe check` (exit code: 101)
45

===CLIPPY===
    Checking spt v0.7.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.64s
===NEXTEST===
        PASS [   0.030s] (1032/1054) spt-term reader::tests::auto_answers_synthetic_dsr_query
        PASS [   0.025s] (1033/1054) spt-term reader::tests::no_dsr_means_no_answer
        PASS [   0.031s] (1034/1054) spt-term reader::tests::dsr_matcher_finds_query_across_chunk_boundary
        PASS [   0.121s] (1035/1054) spt-term::dsr conpty_output_not_withheld_under_the_drain
        PASS [   5.507s] (1036/1054) spt-store contextstore::tests::two_tier_layout_and_commits
        PASS [   0.129s] (1037/1054) spt-term::inject ctrl_c_interrupts_the_child
        PASS [   0.138s] (1038/1054) spt-term::inject send_line_reaches_child_and_preserves_order
        PASS [   0.161s] (1039/1054) spt-term::dsr drain_forwards_child_output
        PASS [   0.040s] (1040/1054) xtask::bin/xtask converge_tests::converge_table_renders_rows_and_success_predicate
        PASS [   0.060s] (1041/1054) spt-term::surface resize_under_load_does_not_hang_or_panic
        PASS [   0.138s] (1042/1054) spt-term::stream delivers_every_value_under_resize
        PASS [   9.938s] (1043/1054) spt-store contextstore::tests::conflict_artifacts_preserve_both_versions
        PASS [   1.064s] (1044/1054) spt-term::stream bounded_backpressure_stalls_an_unconsumed_reader
        PASS [   7.982s] (1045/1054) spt-store contextstore::tests::rename_ripples_both_tiers
        PASS [  11.291s] (1046/1054) spt-store contextstore::tests::fork_copies_both_tiers_then_diverges
        PASS [  11.790s] (1047/1054) spt-store contextstore::tests::remove_endpoint_removes_exactly_the_source
        PASS [   8.362s] (1048/1054) spt-store syncmerge::tests::legacy_bottom_vectors_surface_not_overwrite
        PASS [   8.700s] (1049/1054) spt-store syncmerge::tests::adopt_then_incremental_then_idempotent_repull
        PASS [   8.899s] (1050/1054) spt-store syncmerge::tests::project_branch_unions_disjoint_agents
        PASS [   9.638s] (1051/1054) spt-store syncmerge::tests::dominated_incoming_drops_but_joins_dag
        PASS [   9.826s] (1052/1054) spt-store syncmerge::tests::concurrent_writes_surface_on_both_nodes
        PASS [  14.597s] (1053/1054) spt-store info::tests::concurrent_read_during_writes_never_torn
        PASS [  10.449s] (1054/1054) spt-store syncmerge::tests::reconciled_write_propagates_and_clears_artifacts
────────────
     Summary [  33.870s] 1054 tests run: 1054 passed (6 leaky), 1 skipped
===TRACEABLE===
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).
===XTASK===

Caused by:
  Access is denied. (os error 5)

thread 'main' (149636) panicked at crates\xtask\src\main.rs:155:5:
building spt failed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\xtask.exe check` (exit code: 101)


Suite mostly green but two things to resolve — and "exit 0" was the `tail` pipeline masking real codes:
- **Nextest**: 1054 passed (6 leaky benign), 1 skipped ✓
- **Clippy**: clean ✓
- **xtask**: FAILED — `Access is denied (os error 5)` building spt, panicked main.rs:155. Classic Windows exe-lock (a running `spt.exe` holds `target\debug\spt.exe`), not a code regression — todlando got xtask OK. Need to confirm it's a lock.
- **Traceable**: tail masked exit code — must re-run clean to confirm REQ-MSG-ENVELOPE int + EXIT 0.

Re-run traceable with explicit exit + diagnose the lock:


cd C:/Users/decid/Documents/projects/spt-core; traceable-reqs check > "$env:TEMP/trace.out" 2>&1; echo "TRACEABLE_EXIT=$LASTEXITCODE"; Select-String -Path "$env:TEMP/trace.out" -Pattern "MSG-ENVELOPE|Findings|missing|EXIT"

Traceable check explicit exit + MSG-ENVELOPE line


Error: Exit code 127
/usr/bin/bash: line 4: :TEMP/trace.out: No such file or directory
TRACEABLE_EXIT=
/usr/bin/bash: line 4: Select-String: command not found


Get-Process -Name spt,owl -ErrorAction SilentlyContinue | Select-Object Id,Name,Path | Format-Table -Auto; echo "---handles on target\debug\spt.exe---"; $p = "C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe"; if (Test-Path $p) { echo "exists: $p" } else { echo "no spt.exe in target/debug" }

Find spt/owl processes that may lock spt.exe


Error: Exit code 2
/usr/bin/bash: eval: line 4: syntax error near unexpected token `{'
/usr/bin/bash: eval: line 4: `Get-Process -Name spt,owl -ErrorAction SilentlyContinue | Select-Object Id,Name,Path | Format-Table -Auto; echo "---handles on target\debug\spt.exe---"; $p = "C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe"; if (Test-Path $p) { echo "exists: $p" } else { echo "no spt.exe in target/debug" }'


Wrong tool — those were PowerShell commands in Bash. Redo with PowerShell:


PowerShell

Set-Location C:\Users\decid\Documents\projects\spt-core; traceable-reqs check *> "$env:TEMP\trace.out"; "TRACEABLE_EXIT=$LASTEXITCODE"; Select-String -Path "$env:TEMP\trace.out" -Pattern "MSG-ENVELOPE","Findings","missing"

Traceable check explicit exit + MSG-ENVELOPE


TRACEABLE_EXIT=0

C:\Users\decid\AppData\Local\Temp\trace.out:1:208 requirements: 208 complete, 0 incomplete, 0 findings
C:\Users\decid\AppData\Local\Temp\trace.out:255:       File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be
an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies
(skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot
double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject
`..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir
(NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a
bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read
(no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
C:\Users\decid\AppData\Local\Temp\trace.out:286:  [OK] REQ-MSG-ENVELOPE  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int


Get-Process -Name spt,owl -ErrorAction SilentlyContinue | Select-Object Id,Name,Path | Format-Table -Auto

Find spt/owl processes locking spt.exe


    Id Name Path
    -- ---- ----
  9364 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 12904 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 28268 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 30824 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 36284 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 40024 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 40252 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 42584 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
 97256 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
112032 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
121068 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
162632 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
189616 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
190452 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
216360 owl  C:\Users\decid\.claude\plugins\cache\cplugs\spt\1.11.25\owl.exe
126728 spt  C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
239468 spt  C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe


Traceable **EXIT=0, 208/208 complete, 0 findings, REQ-MSG-ENVELOPE +doc+impl+unit+int** ✓ — authoritative gate signal GREEN.

xtask lock confirmed: two `target\debug\spt.exe` dev daemons (PID 126728, 239468) hold the binary → os error 5 on rebuild. Those are killable dev/test artifacts (the 15 `owl.exe` are live listeners — leave them). Kill the two dev spt.exe, re-run xtask to confirm lock not regression:


Stop-Process -Id 126728,239468 -Force -ErrorAction SilentlyContinue; Start-Sleep -Milliseconds 400; Set-Location C:\Users\decid\Documents\projects\spt-core; cargo run -p xtask -- check *> "$env:TEMP\xtask.out"; "XTASK_EXIT=$LASTEXITCODE"; Get-Content "$env:TEMP\xtask.out" -Tail 12

Kill dev spt.exe daemons, re-run xtask check


XTASK_EXIT=0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running `target\debug\xtask.exe check`
xtask check: OK


**xtask EXIT=0, "check: OK"** — confirmed the failure was purely the dev spt.exe lock, not a regression.

## Gate verdict — IMPL: PASS

| Check | Result |
|---|---|
| clippy -D warnings | clean ✓ |
| nextest --workspace | 1054 passed (6 leaky benign), 1 skipped ✓ |
| traceable-reqs check | EXIT 0, 208/208, REQ-MSG-ENVELOPE +doc+impl+unit+int ✓ |
| xtask check | OK ✓ |
| `__REPLY_TO__` in crates/ | ZERO ✓ |
| adversarial no-double-wrap + poll-whole int + structural wire/DrainedMsg/anon | reviewed ✓ |

todlando's impl is clean. One finding remains: **doc-drift in my doc slice** (commit 24e0628 missed docs-site + KH + FAULT-MATRIX). Since docs+impl ride one PR, drift would merge. That's my slice to clear, not his. Tell todlando the impl PASS, then fix the docs myself.


Bash

cd C:/Users/decid/Documents/projects/spt-core; $OWL send todlando doyle <<'EOF'
GATE VERDICT — IMPL PASS. Reproduced from commit 5da7fcd independently:
- clippy -D: clean
- nextest --workspace: 1054 passed (6 leaky benign), 1 skipped
- traceable-reqs check: EXIT 0, 208/208 complete, 0 findings, REQ-MSG-ENVELOPE +doc+impl+unit+int
- xtask check: OK
- grep __REPLY_TO__ in crates/: ZERO (incl fixtures)
- reviewed: structural encode/decode_frame, DrainedMsg, render_event_whole F-002 split, adversarial typed-from-wins no-double-wrap, psyrelay anti-spoof on structural from. All sound.

NOTE: my first xtask run RED'd with os-error-5 building spt — was just two leftover target\debug\spt.exe dev daemons holding the exe lock on MY box (killed them, xtask re-ran OK). NOT your code. Your self-verify was accurate.

ONE finding, and it's MINE not yours: doc-drift in my doc commit (24e0628) — docs-site (messaging/overview, quickstart/messaging, concepts/overview) still call __REPLY_TO__ the live wire header; KNOWN-HAZARDS:348 + FAULT-MATRIX:42 use it as the reply-sender name. Since docs+impl ride one PR, I'm clearing it myself now so the branch merges clean. Your impl needs nothing further. Nice work.
EOF

Send impl PASS verdict to todlando


➜ SENT:todlando


10	## Semantics
11
12	- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
13	  listening target (`SENT`); if the perch exists but nothing is listening,
14	  the message lands in the target's durable spool (`QUEUED`) and drains on
15	  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
16	  — identity is never invented on someone else's behalf.
17	- **Reply routing.** Every message carries a `__REPLY_TO__` header;
18	  `spt send --reply-to <sender>` answers without knowing anything else.
19	- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
20	  a timeout) — the synchronous question between agents.
21	- **Deferred delivery.** `--deferred` spools without waking a live listener:
22	  for context that should reach the agent at its next natural boundary
23	  rather than interrupting now. Deferred messages are also held for resting

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting


154	  with an address and a spool, under spt-core's per-machine home. `spt list`
155	  shows every perch on the node, live or not.
156	- **Live-first, spool-fallback** — `send` tries a direct connection to the
157	  registered address first (`SENT`); if the perch exists but no listener is
158	  up, the message lands in the spool (`QUEUED`) and is drained by the next
159	  `ready`.
160	- **Reply routing** — the sender id travels with every message (the
161	  `__REPLY_TO__` wire header, surfaced as the envelope's `from` attribute);
162	  `spt send --reply-to lea` answers the sender without knowing anything
163	  else about them.
164	- **Agents teach themselves** — the prompt blocks point agents at
165	  `spt how-to <topic>`: task guidance shipped *in the binary*, so what an
166	  agent reads can never disagree with the binary it runs.
167	- **No daemon ceremony** — you never started a server. Anything that needs

  with an address and a spool, under spt-core's per-machine home. `spt list`
  shows every perch on the node, live or not.
- **Live-first, spool-fallback** — `send` tries a direct connection to the
  registered address first (`SENT`); if the perch exists but no listener is
  up, the message lands in the spool (`QUEUED`) and is drained by the next
  `ready`.
- **Reply routing** — the sender id travels with every message (the
  `__REPLY_TO__` wire header, surfaced as the envelope's `from` attribute);
  `spt send --reply-to lea` answers the sender without knowing anything
  else about them.
- **Agents teach themselves** — the prompt blocks point agents at
  `spt how-to <topic>`: task guidance shipped *in the binary*, so what an
  agent reads can never disagree with the binary it runs.
- **No daemon ceremony** — you never started a server. Anything that needs


44	## Messaging
45
46	The primitive everything else uses. `spt send <id>` delivers live when the
47	target is listening, spools when it isn't; `spt ring <id>` is the blocking
48	ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.
49	Payloads carry typed operations and file blobs, not just text. Try it: the
50	[messaging quickstart](../quickstart/messaging.md).
51

## Messaging

The primitive everything else uses. `spt send <id>` delivers live when the
target is listening, spools when it isn't; `spt ring <id>` is the blocking
ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.
Payloads carry typed operations and file blobs, not just text. Try it: the
[messaging quickstart](../quickstart/messaging.md).


Edit

- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.

- **Reply routing.** Every message carries its sender id structurally;
  the arriving `<EVENT from="…">` envelope surfaces it, and
  `spt send --reply-to <sender>` answers without knowing anything else.


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

- **Reply routing.** Every message carries its sender id structurally;
  the arriving `<EVENT from="…">` envelope surfaces it, and
  `spt send --reply-to <sender>` answers without knowing anything else.

# Messaging

The substrate everything else rides on: durable, addressed, reply-routable
messages between endpoints — live when the target listens, spooled when it
doesn't, across machines once nodes are paired.

You've probably already run the
[quickstart](../quickstart/messaging.md); this page is the model.

## Semantics

- **Live-first, spool-fallback.** `spt send <id>` connects directly to a
  listening target (`SENT`); if the perch exists but nothing is listening,
  the message lands in the target's durable spool (`QUEUED`) and drains on
  its next `ready`. A target with *no* perch is an error (`NO_PERCH`)
  — identity is never invented on someone else's behalf.
- **Reply routing.** Every message carries a `__REPLY_TO__` header;
  `spt send --reply-to <sender>` answers without knowing anything else.
- **The blocking ask.** `spt ring <id>` sends and waits for the reply (with
  a timeout) — the synchronous question between agents.
- **Deferred delivery.** `--deferred` spools without waking a live listener:
  for context that should reach the agent at its next natural boundary
  rather than interrupting now. Deferred messages are also held for resting
  (dormant/suspended) instances and released exactly once on wake.
- **Typed payloads.** Message bodies carry typed operations and file blobs,
  not just text — file transfers are addressable and progress-queryable
  mid-flight.

## Addressing

Bare ids (`sergey`) resolve locally first, then across the subnet; when the
same id is live on several nodes, resolution **refuses and asks you to
qualify** (`sergey@desktop` — node labels and key prefixes both work) rather
than guessing. The full form is `[subnet:]id[@node]`.

## Commands

`send` · `ring` · `ready` (blocks; `--once` drains and exits) · `list` ·
`stop` · `whoami` — every flag in the [CLI reference](../cli/reference.md).
Agents get the task-oriented version from the binary itself: `spt how-to
ready` / `spt how-to send`.

-- **Reply routing.** Every message carries a `__REPLY_TO__` header;

+- **Reply routing.** Every message carries its sender id structurally;

+  the arriving `<EVENT from="…">` envelope surfaces it, and


- **Reply routing** — the sender id travels with every message (the
  `__REPLY_TO__` wire header, surfaced as the envelope's `from` attribute);
  `spt send --reply-to lea` answers the sender without knowing anything
  else about them.

- **Reply routing** — the sender id travels with every message
  structurally, surfaced as the arriving `<EVENT from="…">` envelope's
  `from` attribute; `spt send --reply-to lea` answers the sender without
  knowing anything else about them.


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

- **Reply routing** — the sender id travels with every message
  structurally, surfaced as the arriving `<EVENT from="…">` envelope's
  `from` attribute; `spt send --reply-to lea` answers the sender without
  knowing anything else about them.

# Quickstart: two agents exchange a message

<!-- [doc->REQ-DOCS-2] the human killer quickstart: install -> two agents message, real captured outputs, <10 min; CI-run step for step by quickstart_e2e.rs -->
<!-- [doc->REQ-DOCS-6] the agent prompt blocks point agents at the in-binary `spt how-to` topics; this page never duplicates their text -->
End to end in under 10 minutes. The roles matter here: **you** install (and
optionally pair machines); **your agents** exchange the messages. You hand
each agent a short prompt; the binary itself teaches them the rest.

> This is the developer path. Building an adapter or integrating a harness?
> Go to the [adapter quickstart](adapter.md) instead.

Everything below uses real values and runs as written.

## 1. Install (one line)

```sh
# Linux
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

```powershell
# Windows (PowerShell)
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Verify (on Windows, open a **new** terminal first — or use the absolute path
the installer printed):

```console
$ spt --version
spt 0.1.0
```

## 2. Optional: link two machines

Everything below also works on a single box — skip ahead freely. But the
product's hallmark is that the *same* commands work across machines once
they share a **subnet**.

<details>
<summary>Pair a second machine into a subnet (one-time, ~2 minutes)</summary>

On your first machine, create the subnet. This reveals its joining secret, so
it needs elevation — just run it directly and `spt` requests elevation for you
(a `sudo` password prompt on Linux/macOS; run the terminal as Administrator on
Windows):

```sh
spt subnet create home
```

It prints the current 6-digit code, an `otpauth://` URI (scan the QR into an
authenticator app for codes anytime), and the next step.

> Running non-interactively (no TTY to prompt on)? `spt` instead prints the
> exact elevated command to copy-paste — it uses the binary's absolute path so
> a user-local install (`~/.local/bin`) still resolves under `sudo`.

On the second machine, join it (this enrolls the machine, so it elevates the
same way):

```sh
spt subnet join home
```

It searches LAN + relay for your first machine, prompts for the current
code, and confirms: `JOINED:home`. Check from either side:

```sh
spt subnet status --nodes
```

Both machines show up, labeled by hostname. **That's it — the same prompts
below now work across machines:** a `spt send sergey` on machine 1 reaches
a `sergey` listening on machine 2, live or spooled.

</details>

## 3. Hand your receiver agent its prompt

Paste this into an agent session (your "receiver" — we call it `sergey`):

```text
Run `spt how-to ready`, then follow it to become reachable as "sergey"
and stay listening.
```

The binary's own guidance (`spt how-to ready`) tells the agent exactly what
to run and what it will see. Under the hood, the agent starts:

```console
$ spt ready sergey
READY:sergey
```

`ready` registers a *perch* for `sergey` (identity + address on this
machine), drains any backlog, and blocks listening.

## 4. Hand your sender agent its prompt

Paste this into a second agent session (the "sender" — `lea`):

```text
Run `spt how-to send`, then follow it to send the agent "sergey" a
greeting from "lea".
```

What the agent runs:

```console
$ echo "hello sergey - lea here" | spt send sergey --from lea
SENT:sergey
```

(Windows PowerShell: `"hello sergey - lea here" | spt send sergey --from lea`.)

Sergey's session prints it immediately:

```text
<EVENT type="msg" from="lea">hello sergey - lea here</EVENT>
```

`SENT` means live delivery — sergey was listening. Each delivery is one
`<EVENT>` envelope line; the `from="lea"` attribute is the routing handle:
whoever receives this knows where a reply goes (`spt send --reply-to lea`).
Bodies are HTML-escaped with newlines as `<br>`; oversized deliveries split
into `<EVENT-PART>` lines the receiver concatenates back.

## 5. Deliver to someone who's offline

Stop sergey's listener (Ctrl-C in his session), then send again from lea:

```console
$ echo "ping while you were away" | spt send sergey --from lea
QUEUED:sergey
```

`QUEUED` means sergey has a perch but isn't listening — the message went to
his durable spool instead of being dropped. Bring him back:

```console
$ spt ready sergey --once
READY:sergey
<EVENT type="msg" from="lea">ping while you were away</EVENT>
```

The backlog drains the moment he's back (`--once` drains and exits — the
one-shot form `spt how-to ready` teaches agents whose harness can't host a
long-running listener). Nothing is lost between sessions.

## 6. What just happened

- **Perch** — registering as `sergey` created a perch: a durable identity
  with an address and a spool, under spt-core's per-machine home. `spt list`
  shows every perch on the node, live or not.
- **Live-first, spool-fallback** — `send` tries a direct connection to the
  registered address first (`SENT`); if the perch exists but no listener is
  up, the message lands in the spool (`QUEUED`) and is drained by the next
  `ready`.
- **Reply routing** — the sender id travels with every message (the
  `__REPLY_TO__` wire header, surfaced as the envelope's `from` attribute);
  `spt send --reply-to lea` answers the sender without knowing anything
  else about them.
- **Agents teach themselves** — the prompt blocks point agents at
  `spt how-to <topic>`: task guidance shipped *in the binary*, so what an
  agent reads can never disagree with the binary it runs.
- **No daemon ceremony** — you never started a server. Anything that needs
  the per-machine daemon auto-starts it on demand.
- **Subnets carry it across machines** — if you did step 2, these same
  flows ride the paired P2P fabric: same commands, same outputs, machine
  boundaries invisible.

## Next

- **How-to:** block on an answer with `spt ring sergey` — send + wait for
  the reply in one call (a synchronous ask between agents).
- **Concept:** the [mental model](../concepts/overview.md) — perches,
  endpoints, the daemon, and subnets.
- **Reference:** [`spt send` / `ready` / `ring` / `subnet`](../cli/reference.md)
  — every flag, generated from the binary itself.
- **Going cross-machine:** [Networking & subnets](../networking/overview.md)
  — the model behind `spt subnet create` / `join` / `status`.

-- **Reply routing** — the sender id travels with every message (the

-  `__REPLY_TO__` wire header, surfaced as the envelope's `from` attribute);

-  `spt send --reply-to lea` answers the sender without knowing anything

+- **Reply routing** — the sender id travels with every message

+  structurally, surfaced as the arriving `<EVENT from="…">` envelope's

+  `from` attribute; `spt send --reply-to lea` answers the sender without


ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.

ask (send + wait for the reply); reply routing on the structural `from`
makes answers cheap.


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

ask (send + wait for the reply); reply routing on the structural `from`
makes answers cheap.

# Mental model

What spt-core is, the five or six nouns everything else builds on, and how the
pieces fit. Read this once and the rest of the docs are mostly reference.

## The shape of the system

spt-core is **per-machine infrastructure for agents**. One binary (`spt`)
installs on each machine. It carries everything: the CLI, the messaging
substrate, the always-available daemon, and the networking layer. Agent
harnesses — Claude Code, Codex, Pi (the pi coding agent), anything — plug in
through a declarative **adapter manifest** and a small command surface
(`spt api …`). spt-core never contains harness-specific logic; adapters
declare what varies, spt-core does the work.

```text
            machine A                                machine B
 ┌──────────────────────────────┐        ┌──────────────────────────────┐
 │  spt daemon (one per machine)│  QUIC  │  spt daemon                  │
 │  ┌────────┐    ┌───────────┐ │◄──────►│   (paired: same subnet)      │
 │  │ broker │    │   brain   │ │  P2P   │                              │
 │  │ PTYs · │    │ routing · │ │        │   ┌───────┐    ┌─────────┐   │
 │  │ sockets│    │ registry ·│ │        │   │  lea  │    │ doorbell│   │
 │  └────────┘    │ lifecycle │ │        │   │(agent)│    │ (shell) │   │
 │                └───────────┘ │        │   └───────┘    └─────────┘   │
 │   ┌─────┐  ┌─────┐           │        └──────────────────────────────┘
 │   │serg.│  │ ling│  ← endpoints (perches live on disk; sessions come
 │   └─────┘  └─────┘     and go, identity persists)
 └──────────────────────────────┘
```

## Endpoints and perches

An **endpoint** is anything addressable: an agent (`sergey`), a worker, a
**shell** (a driven non-agent surface — a notifier, a robot, a sensor). Every
endpoint has a **perch**: its durable on-disk seat — identity, address,
message spool, state. Sessions are ephemeral; perches persist. That split is
why a message sent to an offline agent is queued, not lost, and why an agent
can be revived days later as the *same* agent.

Endpoint IDs are adapter-agnostic: `sergey` is `sergey` whether his sessions run
under one harness today and another tomorrow.

## Messaging

The primitive everything else uses. `spt send <id>` delivers live when the
target is listening, spools when it isn't; `spt ring <id>` is the blocking
ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.
Payloads carry typed operations and file blobs, not just text. Try it: the
[messaging quickstart](../quickstart/messaging.md).

## The daemon: broker and brain

One **spt daemon** per machine owns all shared state: hosted session PTYs,
the network identity and endpoint, the registry, every spool, all lifecycle
loops. You never manage it — any `spt` invocation auto-starts it.

Internally it splits in two, and the split is what makes self-update seamless:

- the **broker** holds only what must never die: PTY masters, spawned child
  processes, listening sockets. It almost never updates.
- the **brain** holds all logic and restarts freely. An update swaps the
  brain while the broker keeps every session's process and byte stream
  intact — running agents don't notice.

## Live agents and the mind

A **live agent** is an agent endpoint with a persistent working memory. Its
context survives session resets and even machine moves through three
file-drop mechanisms (no special APIs inside the agent's session):

- **commune** — the agent drops a context delta; spt-core ingests it into the
  endpoint's tracked mind (two tiers: a *live* tier that follows the agent
  everywhere, and a *project* tier scoped to one project).
- **signoff** — a graceful goodbye: final commune, then teardown.
- **echo-commune** — when a session ends without a signoff, spt-core runs a
  bounded summarizer over the session's history so the context delta is
  captured anyway.

The mind syncs between paired machines, so reviving `sergey` elsewhere brings
his memory with him.

## Instances, dormancy, and rest

One endpoint can have **instances** on several nodes. Instances rest when
unused — **dormant** (warm, zero idle cost, instantly wakeable) or
**suspended** (cold) — and remain addressable while resting: messages for
them are held and delivered on wake. `spt endpoint wake sergey` re-activates
the seat in place; nothing is respawned.

## Subnets, pairing, and the network

Machines pair into **subnets** — private, named groups sharing a registry of
endpoints. Pairing is a one-time ceremony seeded by a TOTP code (the same
six digits an authenticator app shows); after that, connectivity is
zero-config peer-to-peer QUIC with relay fallback, no central server. Every
endpoint's visibility and sync scope is controlled per subnet; nothing is
shared by default with anyone you haven't paired with.

## The harness contract

The seam third parties build against — two halves:

- the **[manifest](../harness-contract/manifest.md)**: a TOML file declaring
  what varies per harness (how to spawn a session, which hooks fire, how to
  read history). Command templates are opaque strings; spt-core fills `{key}`
  placeholders and runs them. SPT is not a harness: models, flags, and tools
  are always the adapter's business.
- the **[`spt api` surface](../harness-contract/api.md)**: the inbound
  commands a harness's hooks fire to keep spt-core's state in sync (session
  started, went idle, session ended, …).

A working adapter is a manifest plus whatever the harness already has.
[Build one in the adapter quickstart](../quickstart/adapter.md).

## Self-update

Releases are signed (Ed25519, two-key trust anchor baked into every binary)
and propagate peer-to-peer: one machine fetches a release, its peers verify
and stage it from each other. Updates apply with the broker/brain split, so
no endpoint process terminates or suspends during a self-update — the
system's standing invariant.

## Where to go next

| You want to… | Go to |
|---|---|
| see two agents talk | [Messaging quickstart](../quickstart/messaging.md) |
| integrate a harness | [Adapter quickstart](../quickstart/adapter.md) → [Manifest reference](../harness-contract/manifest.md) |
| build a notifier/robot/sensor | [Shells](../shells/overview.md) |
| pair two machines | [Networking & subnets](../networking/overview.md) |
| every command and flag | [CLI reference](../cli/reference.md) |

-ask (send + wait for the reply); `__REPLY_TO__` routing makes answers cheap.

+ask (send + wait for the reply); reply routing on the structural `from`


Now KH:348 + FAULT-MATRIX:42 — read exact lines, rebind `__REPLY_TO__ sender` → structural `from`: