     1	test endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches ... === DIAGNOSTIC: run_ok=true online=true harness_pid=Some(147152) alive_after=true rc_connected=true rc_saw_tick=false rc_stdout_bytes=0 ===
     2	DIAG_BECOME_CTRL sid=1 from_seq=0 ring_len=0 next_seq=0 by=None
     3	DIAG_DRAIN sid=1 chunk_bytes=16
     4	DIAG_DRAIN sid=1 chunk_bytes=110
     5	DIAG_DRAIN sid=1 chunk_bytes=47
     6	DIAG_DRAIN sid=1 chunk_bytes=28
     7	DIAG_DRAIN sid=1 chunk_bytes=29
     8	DIAG_DRAIN sid=1 chunk_bytes=29
     9	DIAG_DRAIN sid=1 chunk_bytes=29
    10	DIAG_DRAIN sid=1 chunk_bytes=29
    11	DIAG_DRAIN sid=1 chunk_bytes=29
    12	DIAG_OPR_PUMP_START stream=1
    13	DIAG_OPR_PUMP_START stream=2
    14	DIAG_OPR_PUMP_READ stream=1 n=66
    15	DIAG_SLOG_APPEND stream=1 seq=0 bytes=66 has_sub=false
    16	DIAG_SLOG_ATTACH stream=2 from_seq=0 ring_len=0 next_seq=0
    17	DIAG_OPR_PUMP_READ stream=1 n=39
    18	DIAG_SLOG_APPEND stream=1 seq=1 bytes=39 has_sub=false
    19	DIAG_OPR_PUMP_READ stream=1 n=39
    20	DIAG_SLOG_APPEND stream=1 seq=2 bytes=39 has_sub=false
    21	DIAG_SLOG_ATTACH stream=1 from_seq=0 ring_len=3 next_seq=3
    22	DIAG_SLOG_ATTACH stream=1 from_seq=0 ring_len=3 next_seq=3
    23	DIAG_SERVE_ATTACH session_id=1 from_seq=0 intent=Control origin=398eb0a19aec0fcf927bdf7807922301169978bd1564f692dbe9de33e778361a
    24	DIAG_BECOME_CTRL sid=1 from_seq=0 ring_len=9 next_seq=9 by=Some("398eb0a19aec0fcf927bdf7807922301169978bd1564f692dbe9de33e778361a")
    25	DIAG_REPLAY_WRITE seq=0 ok=true
    26	DIAG_SERVE_FWD seq=0DIAG_REPLAY_WRITE seq=1 bytes=16
    27	DIAG_REPLAY_WRITE seq=2 ok=DIAG_SERVE_FWD_DONE seq=0true
    28	DIAG_REPLAY_WRITE seq=3 ok=trueDIAG_SERVE_FWD seq=1
    29	DIAG_REPLAY_WRITE seq=4 ok=true
    30	DIAG_DRAIN sid=1 chunk_bytes=29
    31	WAVE2-LEAD: rc attached but received 0 bytes, no DUMMY_HARNESS_TICK — endpoint-run harness PTY output not reaching the attach (root-cause pending).


The full ordered log reveals it: serve printed `DIAG_SERVE_FWD_DONE seq=0` but then `DIAG_SERVE_FWD seq=1` with **no DONE** — serve is **blocked for ~10s inside `net_stream_send(seq1)`**. And op_r (stream 2) never reads even seq0's bytes. The known-good test only ever forwards ONE chunk (one echoed marker), so it never hits the 2nd-forward block — that's why it's green. Critical: is `bounded_block_on` spawning onto the 2-worker runtime and blocking a worker? Read it.


crates\spt-daemon\src\nethost.rs:815:    fn bounded_block_on<F, T>(&self, fut: F) -> io::Result<T>
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-816-    where
crates\spt-daemon\src\nethost.rs-817-        F: std::future::Future<Output = io::Result<T>>,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-818-    {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-819-        let dur = self.quic_op_timeout();
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-820-        self.runtime.handle().block_on(async move {
crates\spt-daemon\src\nethost.rs-821-            match tokio::time::timeout(dur, fut).await {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-822-                Ok(inner) => inner,
crates\spt-daemon\src\nethost.rs-823-                Err(_elapsed) => Err(io::Error::other(format!(
crates\spt-daemon\src\nethost.rs-824-                    "broker QUIC op exceeded the {dur:?} bound (peer unresponsive) — \
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-825-                     failing as an ordinary per-peer error"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-826-                ))),
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-827-            }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-828-        })
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-829-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-830-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-831-    /// Run the joiner leg of the pairing ceremony (M7 D3, REQ-SUBNET-2) on
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-832-    /// the host's runtime — the broker IPC `pair-join` arm. The ceremony
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-833-    /// must run HERE because it binds and pins this endpoint's real node
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-834-    /// pubkey. Returns the rendered trust outcome, or the classified failure.
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-835-    // [impl->REQ-SUBNET-2]
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-836-    pub fn pair_join(
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-837-        &self,
crates\spt-daemon\src\nethost.rs-838-        subnet: &str,
crates\spt-daemon\src\nethost.rs-839-        code: &str,
crates\spt-daemon\src\nethost.rs-840-        responder_addr: Option<EndpointAddr>,
crates\spt-daemon\src\nethost.rs-841-    ) -> Result<String, crate::pairhost::JoinFail> {
crates\spt-daemon\src\nethost.rs-842-        let endpoint = Arc::clone(&self.endpoint);
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-843-        self.runtime.handle().block_on(async move {
crates\spt-daemon\src\nethost.rs-844-            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-845-        })
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-846-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-847-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-848-    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-849-    pub fn node_id_hex(&self) -> String {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-850-        self.endpoint.node_id_hex()
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-851-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-852-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-853-    /// The endpoint's dialable address (id + currently-known direct/relay paths).
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-854-    pub fn addr(&self) -> EndpointAddr {
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-855-        self.endpoint.addr()
--
crates\spt-daemon\src\nethost.rs:1520:    fn bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error() {
crates\spt-daemon\src\nethost.rs-1521-        let host = NetHost::start(hermetic(&Identity::generate())).expect("host start");
crates\spt-daemon\src\nethost.rs-1522-        host.set_quic_op_timeout(Duration::from_millis(150));
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1523-
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1524-        // A never-completing await (the black-holed-peer shape) fails at the bound.
crates\spt-daemon\src\nethost.rs-1525-        let started = std::time::Instant::now();
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1526-        let err = host
crates\spt-daemon\src\nethost.rs-1527-            .bounded_block_on(std::future::pending::<io::Result<()>>())
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1528-            .expect_err("a never-completing op must error, never block forever");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1529-        let elapsed = started.elapsed();
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1530-        assert_ne!(
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1531-            err.kind(),
crates\spt-daemon\src\nethost.rs-1532-            io::ErrorKind::TimedOut,
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1533-            "the bound surfaces an ORDINARY error (peer_outcome's per-peer arm), \
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1534-             never the brain's TimedOut poison signal (the A-half restart path)"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1535-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1536-        assert!(
crates\spt-daemon\src\nethost.rs-1537-            elapsed >= Duration::from_millis(150),
crates\spt-daemon\src\nethost.rs-1538-            "it waited at least the bound ({elapsed:?})"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1539-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1540-        assert!(
crates\spt-daemon\src\nethost.rs-1541-            elapsed < Duration::from_secs(5),
crates\spt-daemon\src\nethost.rs-1542-            "and fired promptly, not wildly past it ({elapsed:?})"
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1543-        );
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1544-
crates\spt-daemon\src\nethost.rs-1545-        \ Happy path untouched: a ready op returns its value with no added latency.
crates\spt-daemon\src\nethost.rs-1546-        let v: u64 = host
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1547-            .bounded_block_on(async { Ok(7u64) })
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1548-            .expect("a ready op passes through");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1549-        assert_eq!(v, 7, "the bound is transparent to a completing op");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1550-    }
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs-1551-}


`bounded_block_on` runs the write inline on the broker conn thread (not a worker) — so a 126-byte write into a 64KB duplex should never block on buffer. Yet seq1's write never completes and op_r never reads even seq0. The asymmetry (op_w→peer_r works, peer_w→op_r doesn't) is real. Need to know if `net_stream_send` blocks on a broker reply or fires-and-forgets — that pins whether serve or the broker conn-thread is the stall point. Read it.


939	    pub fn net_stream_send(
940	        &mut self,
941	        stream_id: u64,
942	        bytes: &[u8],
943	        op_id: Option<u64>,
944	        finish: bool,
945	    ) -> io::Result<Option<NetSent>> {
946	        self.send(
947	            KIND_NET_STREAM_SEND,
948	            serde_json::to_value(NetStreamSendReq {
949	                stream_id,
950	                data_b64: encode_bytes(bytes),
951	                op_id,
952	                finish,
953	            })
954	            .expect("NetStreamSendReq serializes"),
955	        )?;
956	        if op_id.is_none() {
957	            return Ok(None);
958	        }
959	        let deadline = self.call_deadline();
960	        loop {
961	            match self.read_event_until(deadline)? {
962	                BrokerEvent::NetSent(ev) if Some(ev.op_id) == op_id => return Ok(Some(ev)),
963	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
964	                _ => continue,
965	            }
966	        }
967	    }
968
969	    /// List the streams the broker holds (D4b) — how a re-attaching brain
970	    /// finds streams (incl. peer-initiated ones) that arrived while it was down.
971	    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {
972	        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;
973	        loop {
974	            match self.read_event()? {
975	                BrokerEvent::NetStreams(ev) => return Ok(ev),
976	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
977	                _ => continue,
978	            }
979	        }
980	    }
981
982	    /// Trigger a **planned brain-process restart** on the broker (ADR-0018 D3-3,
983	    /// Q3): the broker signals its supervisor to cycle the brain child onto the
984	    /// freshly-swapped binary, with `start-reason=update`. `spt update apply`
985	    /// calls this after swapping the binary on disk — the seam that makes a
986	    /// routine update actually run new code without a manual bounce, every
987	    /// broker-held endpoint untouched at the process level.
988	    ///
989	    /// Returns `honored`: whether the broker had a supervisor wired (false on a
990	    /// bare broker with no supervisor, e.g. a test broker — the verb is accepted
991	    /// but nothing respawns). An older broker that predates the verb answers with
992	    /// an error (KH-2.3 forward-compat: surfaced, never a silent no-op of a
993	    /// swapped binary). Readiness-gated promotion / auto-rollback is D6.
994	    // [impl->REQ-UPD-3]
995	    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
996	    pub fn request_brain_restart(&mut self) -> io::Result<bool> {
997	        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;
998	        loop {

    pub fn net_stream_send(
        &mut self,
        stream_id: u64,
        bytes: &[u8],
        op_id: Option<u64>,
        finish: bool,
    ) -> io::Result<Option<NetSent>> {
        self.send(
            KIND_NET_STREAM_SEND,
            serde_json::to_value(NetStreamSendReq {
                stream_id,
                data_b64: encode_bytes(bytes),
                op_id,
                finish,
            })
            .expect("NetStreamSendReq serializes"),
        )?;
        if op_id.is_none() {
            return Ok(None);
        }
        let deadline = self.call_deadline();
        loop {
            match self.read_event_until(deadline)? {
                BrokerEvent::NetSent(ev) if Some(ev.op_id) == op_id => return Ok(Some(ev)),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// List the streams the broker holds (D4b) — how a re-attaching brain
    /// finds streams (incl. peer-initiated ones) that arrived while it was down.
    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {
        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStreams(ev) => return Ok(ev),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// Trigger a **planned brain-process restart** on the broker (ADR-0018 D3-3,
    /// Q3): the broker signals its supervisor to cycle the brain child onto the
    /// freshly-swapped binary, with `start-reason=update`. `spt update apply`
    /// calls this after swapping the binary on disk — the seam that makes a
    /// routine update actually run new code without a manual bounce, every
    /// broker-held endpoint untouched at the process level.
    ///
    /// Returns `honored`: whether the broker had a supervisor wired (false on a
    /// bare broker with no supervisor, e.g. a test broker — the verb is accepted
    /// but nothing respawns). An older broker that predates the verb answers with
    /// an error (KH-2.3 forward-compat: surfaced, never a silent no-op of a
    /// swapped binary). Readiness-gated promotion / auto-rollback is D6.
    // [impl->REQ-UPD-3]
    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
    pub fn request_brain_restart(&mut self) -> io::Result<bool> {
        self.send(KIND_BRAIN_RESTART, serde_json::Value::Null)?;
        loop {


`net_stream_send` with `op_id=None` is fire-and-forget — so `DIAG_SERVE_FWD_DONE seq=0` only means serve queued the IPC frame; the broker processes it async. The ~10s gap before serve even reaches seq1 means the broker's conn-thread is stuck. Pin it: instrument `send_stream` (broker side) before/after `write_all` to prove the loopback `write_all(peer_w)` itself blocks.


Edit

        self.bounded_block_on(async move {
            let mut send = entry.send.lock().await;
            if !bytes.is_empty() {
                send.write_all(&bytes).await?;
            }
            if finish {
                send.finish().await?;
            }
            Ok(())
        })

        let dbg_n = bytes.len();
        self.bounded_block_on(async move {
            eprintln!("DIAG_SEND_STREAM_ENTER stream={stream_id} bytes={dbg_n} finish={finish}");
            let mut send = entry.send.lock().await;
            eprintln!("DIAG_SEND_STREAM_LOCKED stream={stream_id}");
            if !bytes.is_empty() {
                send.write_all(&bytes).await?;
            }
            if finish {
                send.finish().await?;
            }
            eprintln!("DIAG_SEND_STREAM_DONE stream={stream_id}");
            Ok(())
        })


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

        self.bounded_block_on(async move {
            let mut send = entry.send.lock().await;
            if !bytes.is_empty() {
                send.write_all(&bytes).await?;
            }
            if finish {
                send.finish().await?;
            }
            Ok(())
        })

        let dbg_n = bytes.len();
        self.bounded_block_on(async move {
            eprintln!("DIAG_SEND_STREAM_ENTER stream={stream_id} bytes={dbg_n} finish={finish}");
            let mut send = entry.send.lock().await;
            eprintln!("DIAG_SEND_STREAM_LOCKED stream={stream_id}");
            if !bytes.is_empty() {
                send.write_all(&bytes).await?;
            }
            if finish {
                send.finish().await?;
            }
            eprintln!("DIAG_SEND_STREAM_DONE stream={stream_id}");
            Ok(())
        })

// Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): `write_all` / `finish` on a

// black-holed QUIC stream blocks on flow-control the dead peer never grants.

// Loopback (in-process) never hangs, so the bound is a no-op for it.

+            eprintln!("DIAG_SEND_STREAM_ENTER stream={stream_id} bytes={dbg_n} finish={finish}");

+            eprintln!("DIAG_SEND_STREAM_LOCKED stream={stream_id}");

+            eprintln!("DIAG_SEND_STREAM_DONE stream={stream_id}");


Bash

CARGO_TARGET_DIR=target-seam cargo test -p spt --test dummy_harness_e2e -- --nocapture --test-threads=1 > /tmp/dummyrun2.log 2>&1; grep -aoE "DIAG_SEND_STREAM_[A-Z]+ stream=[0-9]+( bytes=[0-9]+ finish=(true|false))?|DIAG_OPR_PUMP_(READ|START|END) stream=[0-9]+( n=[0-9]+)?|DIAG_SERVE_FWD(_DONE)? seq=[0-9]+|DIAG_REPLAY_WRITE seq=[0-9]+ ok=(true|false)|DIAG_SLOG_ATTACH stream=[0-9]+" /tmp/dummyrun2.log | nl | head -80

Capture clean tokenized diagnostic sequence


     1	DIAG_OPR_PUMP_START stream=1
     2	DIAG_OPR_PUMP_START stream=2
     3	DIAG_SEND_STREAM_ENTER stream=2 bytes=66 finish=false
     4	DIAG_SEND_STREAM_LOCKED stream=2
     5	DIAG_SEND_STREAM_DONE stream=2
     6	DIAG_OPR_PUMP_READ stream=1 n=66
     7	DIAG_SLOG_ATTACH stream=2
     8	DIAG_SEND_STREAM_ENTER stream=2 bytes=39 finish=false
     9	DIAG_SEND_STREAM_LOCKED stream=2
    10	DIAG_SEND_STREAM_DONE stream=2
    11	DIAG_OPR_PUMP_READ stream=1 n=39
    12	DIAG_SEND_STREAM_ENTER stream=2 bytes=39 finish=false
    13	DIAG_SEND_STREAM_LOCKED stream=2
    14	DIAG_SEND_STREAM_DONE stream=2
    15	DIAG_OPR_PUMP_READ stream=1 n=39
    16	DIAG_SLOG_ATTACH stream=1
    17	DIAG_SLOG_ATTACH stream=1
    18	DIAG_REPLAY_WRITE seq=0 ok=true
    19	DIAG_SERVE_FWD seq=0
    20	DIAG_REPLAY_WRITE seq=1 ok=true
    21	DIAG_REPLAY_WRITE seq=2 ok=true
    22	DIAG_SERVE_FWD_DONE seq=0
    23	DIAG_REPLAY_WRITE seq=3 ok=true
    24	DIAG_SERVE_FWD seq=1
    25	DIAG_REPLAY_WRITE seq=4 ok=true


**ROOT CAUSE FOUND — decisive.** The tokenized sequence shows: rc's input (stream 2 writes) processed fine, but after `become_controller` starts its 9-chunk ring replay, serve forwards seq0/seq1 yet **`DIAG_SEND_STREAM_ENTER stream=1` NEVER appears** — the broker never processes serve's forwards. Meanwhile `DIAG_REPLAY_WRITE` races ahead (seq0-4).

The mechanism: `serve_attach` **receives** session Output and **sends** its forwards over the **same** broker IPC connection. `become_controller`'s inline multi-frame replay holds the broker's single-threaded conn-handler inside `dispatch_subscribe` (writing broker→serve), so it can't service serve's serve→broker forward frames. Both directions back up → mutual IPC backpressure deadlock → rc gets 0 bytes.

This is **endpoint-run-specific**: the ring is non-empty only when the harness is *already producing* before attach (a live heartbeating endpoint). The known-good echo test attaches to an idle child (empty ring → zero-frame replay → no storm), and the cwd test reads on the spawning conn (no serve-over-IPC) — so neither caught it.

Let me revert all diagnostics, then report to doyle.


git checkout -- crates/spt-daemon/src/broker.rs crates/spt-daemon/src/attach.rs crates/spt-daemon/src/nethost.rs crates/spt/src/rc.rs crates/spt/tests/dummy_harness_e2e.rs && git status --short && echo "---clean---"

Revert all diagnostic instrumentation


?? target-seam/
---clean---


1	//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent
2	//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the
3	//! REAL spt-hosted bringup path end-to-end —
4	//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach
5	//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and
6	//! NOT a mock that exits immediately.
7	//!
8	//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the
9	//! harness contract), prints a stdout heartbeat on an interval, and stays alive
10	//! until killed — so "broker wedged / endpoint died" is cleanly separable from "a
11	//! real harness failed to launch." Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,
12	//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.
13	//!
14	//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the
15	//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the
16	//! live heartbeat output. Bounded; all spawned pids reaped scoped (never
17	//! machine-wide — shared runner).
18
19	use std::io::Read;
20	use std::path::{Path, PathBuf};
21	use std::process::{Child, Command, Output, Stdio};
22	use std::time::{Duration, Instant};
23
24	mod common;
25	use common::CommandNoWindowExt;
26
27	use spt_store::perch::{self, ParentHint};
28
29	fn kill_pid(pid: u32) {
30	    #[cfg(windows)]
31	    let _ = Command::new("taskkill")
32	        .no_window()
33	        .args(["/PID", &pid.to_string(), "/F", "/T"])
34	        .output();
35	    #[cfg(unix)]
36	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
37	}
38
39	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
40	    let (tx, rx) = std::sync::mpsc::channel();
41	    std::thread::spawn(move || {
42	        let _ = tx.send(cmd.output());
43	    });
44	    rx.recv_timeout(deadline)
45	        .expect("captured spt call must complete within its deadline")
46	        .expect("run spt")
47	}
48
49	fn ready_pid(path: &Path) -> Option<u32> {
50	    let s = std::fs::read_to_string(path).ok()?;
51	    serde_json::from_str::<serde_json::Value>(&s)
52	        .ok()?
53	        .get("pid")?
54	        .as_u64()
55	        .map(|p| p as u32)
56	}
57
58	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
59	    let deadline = Instant::now() + budget;
60	    while Instant::now() < deadline {
61	        if let Some(p) = ready_pid(path) {
62	            return Some(p);
63	        }
64	        std::thread::sleep(Duration::from_millis(50));
65	    }
66	    None
67	}
68
69	fn sibling_bin(name: &str) -> PathBuf {
70	    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
71	        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
72	}
73
74	#[test]
75	fn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {
76	    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node
77	    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces
78	    //    as net-stream events, so it requires a net-bound broker — a net-less broker
79	    //    refuses the loopback dial ("net disabled"). Unlike the livehost/reconcile
80	    //    tests (which only inspect perch state), the rc-attach path needs net up. ──
81	    let home = tempfile::tempdir().unwrap();
82	    std::env::set_var("SPT_HOME", home.path());
83
84	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
85	    let exe_suffix = spt_bin
86	        .extension()
87	        .map(|e| format!(".{}", e.to_string_lossy()))
88	        .unwrap_or_default();
89	    let mock_session = sibling_bin("mock-session");
90	    assert!(
91	        mock_session.exists(),
92	        "the dummy-harness program must be built: {} \
93	         (pre-build: cargo build -p mock-adapter --bin mock-session)",
94	        mock_session.display()
95	    );
96
97	    // ── (2) Register the DUMMY-HARNESS adapter: a kind=harness adapter whose
98	    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};
99	    //    the program token is the absolute mock-session path. The dummy shells the
100	    //    same `spt` to bind (via --spt). ──
101	    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
102	    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
103	    let self_cmd = format!(
104	        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
105	         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
106	    );
107	    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
108	    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
109	    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
110	    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
111	    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
112	    // VIEWER-CLOSE / psyche-reap gates need.
113	    let src = perch::spt_home().join("srcs").join("dummyharness");
114	    std::fs::create_dir_all(&src).unwrap();
115	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
116	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
117	    let drops = home.path().join("drops");
118	    std::fs::create_dir_all(&drops).unwrap();
119	    let dir = drops.to_string_lossy().replace('\\', "/");
120	    let manifest_toml = format!(
121	        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
122	         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
123	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
124	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
125	    );
126	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
127	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
128
129	    // ── (3) The REAL daemon broker process. ──
130	    let brain_log = home.path().join("brain.stderr.log");
131	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
132	    let mut broker: Child = Command::new(&spt_bin)
133	        .no_window()
134	        .args(["daemon", "run"])
135	        .env("SPT_HOME", home.path())
136	        .stdout(Stdio::null())
137	        .stderr(Stdio::from(brain_log_file))
138	        .spawn()
139	        .expect("spawn spt daemon run (broker process)");
140	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
141	    {
142	        Some(p) => p,
143	        None => {
144	            let _ = broker.kill();
145	            let _ = broker.wait();
146	            panic!(
147	                "PRECONDITION: brain never came up.\n{}",
148	                std::fs::read_to_string(&brain_log).unwrap_or_default()
149	            );
150	        }
151	    };
152
153	    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
154	    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
155	    let id = "dummy7";
156	    let run = {
157	        let mut cmd = Command::new(&spt_bin);
158	        cmd.no_window()
159	            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
160	            .env("SPT_HOME", home.path());
161	        output_bounded(cmd, Duration::from_secs(45))
162	    };
163
164	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
165	    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
166
167	    // The dummy binds its perch live within a few seconds of the broker PTY spawn.
168	    let mut online = false;
169	    let deadline = Instant::now() + Duration::from_secs(20);
170	    while Instant::now() < deadline {
171	        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
172	            online = true;
173	            break;
174	        }

... [127 lines truncated] ...


1	//! `spt rc <id>` — the operator-side attach pump (M12-W1, REQ-RC-1): attach a
2	//! local terminal to a broker-held PTY session.
3	//!
4	//! **One pump, swappable transport** (the doyle invariant). The attach protocol
5	//! is `serve_attach` on the target side ⇄ this pump on the operator side, both
6	//! speaking `AttachRecord` over a broker net-stream. *Local* attach is the
7	//! degenerate single-node case: the operator dials its OWN node (loopback peer)
8	//! and the broker's inbound-stream dispatch (`dispatch.rs`) runs `serve_attach`
9	//! against the same broker — the exact cross-node accept+serve path, no local
10	//! special-case branch. The one-path invariant lives at this pump, not the
11	//! transport under the stream.
12	//!
13	//! **Detach never kills the session.** The broker owns the PTY; closing our send
14	//! side ends the viewport with the session still running (`serve_attach` →
15	//! `Detached`). PTY ownership stays with the broker (the KNOWN-HAZARDS
16	//! attach-lifecycle invariant).
17	//!
18	//! **Single-Brain pump.** A `Brain` is `&mut`-single-threaded, so the main thread
19	//! owns it: a stdin reader thread only *produces* raw bytes onto an mpsc channel
20	//! (and watches for the detach keybind); the main loop interleaves draining that
21	//! channel into `send_attach_input` with `read_event_until` for inbound `Output`.
22	// [impl->REQ-RC-1]
23
24	use std::io::{Read, Write};
25	use std::sync::atomic::{AtomicU64, Ordering};
26	use std::sync::mpsc;
27	use std::time::{Duration, Instant};
28
29	use spt_daemon::attach::{request_attach, send_attach_input, send_attach_resize};
30	use spt_daemon::brain::{now_ms, Brain, BrokerEvent};
31	use spt_daemon::msg::decode_bytes;
32	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
33
34	/// The detach prefix: ctrl-b (0x02), matching the legacy capsule's prefix so the
35	/// muscle memory carries over. `ctrl-b d` detaches; `ctrl-b ctrl-b` sends one
36	/// literal ctrl-b to the session; any other byte after the prefix is sent
37	/// through verbatim (prefix + byte). Documented in the CLI help.
38	const DETACH_PREFIX: u8 = 0x02; // ctrl-b
39	const DETACH_KEY: u8 = b'd';
40
41	/// How long the main loop blocks for an inbound event before looping back to
42	/// drain stdin — the full-duplex interleave granularity.
43	const POLL_SLICE: Duration = Duration::from_millis(40);
44
45	/// A message from the stdin reader thread to the pump.
46	enum StdinMsg {
47	    /// Raw bytes to forward to the session as `AttachRecord::Input`.
48	    Bytes(Vec<u8>),
49	    /// The operator hit the detach keybind — end the viewport, leave the session.
50	    Detach,
51	}
52
53	/// Mint operator op ids from a random 64-bit base so two `spt rc` sessions never
54	/// collide in the broker's net-op journal (a journaled `send_attach_input` op is
55	/// exactly-once at the PTY — a reused id from a prior session would be deduped,
56	/// dropping the keystroke). Random base + monotonic counter.
57	struct OpMinter(AtomicU64);
58	impl OpMinter {
59	    fn new() -> Self {
60	        let seed = spt_proto::identity::Identity::generate().seed();
61	        let base = u64::from_le_bytes(seed[..8].try_into().expect("8 bytes"));
62	        // Keep it well below u64::MAX so the counter can't wrap into another
63	        // session's base within any realistic session length.
64	        OpMinter(AtomicU64::new(base >> 1))
65	    }
66	    fn next(&self) -> u64 {
67	        self.0.fetch_add(1, Ordering::Relaxed)
68	    }
69	}
70
71	/// Restore the terminal out of raw mode no matter how the pump returns.
72	struct RawGuard(bool);
73	impl RawGuard {
74	    fn enable() -> Self {
75	        RawGuard(crossterm::terminal::enable_raw_mode().is_ok())
76	    }
77	}
78	impl Drop for RawGuard {
79	    fn drop(&mut self) {
80	        if self.0 {
81	            let _ = crossterm::terminal::disable_raw_mode();
82	        }
83	    }
84	}
85
86	/// Resolve a live endpoint id to its broker session id via the broker's hosted
87	/// session table (the spawn's endpoint label, brain.sessions()). `None` ⇒ no
88	/// live session under that endpoint.
89	fn resolve_session(brain: &mut Brain, endpoint_id: &str) -> std::io::Result<Option<u64>> {
90	    let reply = brain.sessions()?;
91	    Ok(reply
92	        .sessions
93	        .into_iter()
94	        .find(|s| s.endpoint == endpoint_id)
95	        .map(|s| s.session_id))
96	}
97
98	/// The node currently driving `endpoint_id`, if any — read off the perch's
99	/// broker-owned `driven_by` marker (REQ-RCVIEW-1). Drives the client-side
100	/// busy-refuse guidance. `None` ⇒ free (or no perch).
101	fn current_driver(endpoint_id: &str) -> Option<String> {
102	    let perch = spt_store::perch::resolve_perch_path(
103	        endpoint_id,
104	        spt_store::perch::ParentHint::Infer,
105	    );
106	    spt_store::info::read_info(&perch).and_then(|i| i.driven_by)
107	}
108
109	/// What one stdin chunk yielded after the detach state machine: the bytes to
110	/// forward to the session, and whether the detach keybind fired.
111	struct DetachParse {
112	    forward: Vec<u8>,
113	    detach: bool,
114	}
115
116	/// Apply the `ctrl-b` detach state machine to one raw stdin chunk. `armed` holds
117	/// the "saw the prefix" state across chunk boundaries (a prefix can end one read
118	/// and its companion key begin the next). `ctrl-b d` → detach; `ctrl-b ctrl-b` →
119	/// one literal `ctrl-b`; `ctrl-b <other>` → prefix + other verbatim; every other
120	/// byte passes through. Pure so it unit-tests without a terminal. On detach the
121	/// bytes BEFORE the keybind are returned to forward; bytes after are dropped (the
122	/// viewport is ending).
123	fn parse_stdin_chunk(armed: &mut bool, input: &[u8]) -> DetachParse {
124	    let mut forward = Vec::with_capacity(input.len());
125	    for &b in input {
126	        if *armed {
127	            *armed = false;
128	            match b {
129	                DETACH_KEY => return DetachParse { forward, detach: true },
130	                DETACH_PREFIX => forward.push(DETACH_PREFIX), // literal ctrl-b
131	                other => {
132	                    forward.push(DETACH_PREFIX);
133	                    forward.push(other);
134	                }
135	            }
136	        } else if b == DETACH_PREFIX {
137	            *armed = true;
138	        } else {
139	            forward.push(b);
140	        }
141	    }
142	    DetachParse {
143	        forward,
144	        detach: false,
145	    }
146	}
147
148	/// Spawn the stdin reader: raw bytes → `StdinMsg::Bytes`, watching for the
149	/// detach keybind ([`parse_stdin_chunk`]). Detaching sends `StdinMsg::Detach`
150	/// and ends the thread; the leaked blocking `read` (if any) dies with the
151	/// process on exit.
152	fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
153	    std::thread::spawn(move || {
154	        let mut stdin = std::io::stdin().lock();
155	        let mut buf = [0u8; 1024];
156	        let mut armed = false; // saw the detach prefix, awaiting the next byte
157	        loop {
158	            let n = match stdin.read(&mut buf) {
159	                Ok(0) => break, // stdin closed
160	                Ok(n) => n,
161	                Err(_) => break,
162	            };
163	            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
164	            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {
165	                break;
166	            }
167	            if parsed.detach {
168	                let _ = tx.send(StdinMsg::Detach);
169	                return;
170	            }
171	        }
172	    });
173	}
174
175	/// Attach a local terminal to the broker-held session for `endpoint_id` under
176	/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is
177	/// busy-refused with guidance on a controlled one), `Viewer` watches read-only,
178	/// `Take` kicks the incumbent controller (loud `Displaced` notice to them) and
179	/// takes control. Returns when the session exits, the operator detaches, or an
180	/// error occurs.
181	// [impl->REQ-RC-1] [impl->REQ-RCVIEW-1] [impl->REQ-KICK-1]

... [469 lines truncated] ...


All diagnostics reverted — tree clean (only `target-seam/` build dir). I have a definitive root cause. This is a broker controller/replay design decision (load-bearing, possibly cross-node too), so per our roles I report to doyle for the design ruling before implementing. Ping him:


$OWL send doyle <<'EOF'
L0 ROOT-CAUSE NAILED (well past STEP-1 isolate). Not the dummy program, not a drain bug, not worker starvation (tried 2->8 workers, no change). It is a BROKER REPLAY-vs-FORWARD IPC DEADLOCK in serve_attach, triggered only by a NON-EMPTY OutputLog ring at attach (i.e. an already-producing harness = exactly endpoint-run).

EVIDENCE (instrumented the real dummy fixture, full ordered broker log):
- drain DOES append; ring fills to 9 chunks. serve_attach attaches to the right session; become_controller replays from seq0.
- serve forwards seq0/seq1 via net_stream_send, BUT broker-side send_stream(stream=1) NEVER runs (no SEND_STREAM_ENTER stream=1) -> the operator-side loopback read pump (op_r) never reads -> rc gets 0 bytes.

MECHANISM: serve_attach RECEIVES session Output (broker->serve) AND SENDS its forwards (serve->broker) on the SAME broker IPC connection. become_controller does a synchronous MULTI-FRAME ring replay inline inside dispatch_subscribe, holding the broker's single-threaded per-conn handler in the broker->serve write loop. While stuck there it cannot read serve's serve->broker forward frames. Both directions back up -> mutual IPC backpressure deadlock. Operator receives nothing.

WHY UNCAUGHT: known-good local_attach_via_loopback E2E attaches to an IDLE echo child (empty ring -> 0-frame replay -> no storm) and only forwards ONE chunk; the cwd E2E reads on the spawning conn (no serve-over-IPC). Neither exercises a full-ring replay + sustained forward on one conn.

LIKELY ALSO CROSS-NODE: same serve_attach shape forwards over QUIC on the same conn -> a busy/pre-producing session attached cross-node should deadlock too (just never tested with a populated ring).

FIX OPTIONS (need your design ruling):
(a) serve_attach forwards on a SEPARATE broker connection from the one it subscribes/receives on -> replay-write and forward-read no longer share the single-threaded conn handler. Most surgical, local to serve_attach + dispatcher. send_stream is keyed by stream_id (conn-agnostic) so this is clean.
(b) make become_controller's controller replay NON-BLOCKING via the dedicated-writer-thread+bounded-channel pattern add_viewer/viewer_writer already use, so dispatch_subscribe returns promptly. More complete (fixes cross-node too) but touches the authoritative cursor-advancing controller path.

My lean: (a) for the surgical v0.12.1 fix, with a note to consider (b)/cross-node hardening. Your call on design + whether to fold the cross-node case now. Then I activate REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT [impl,unit,int] + flip the fixture's rc_saw_tick diagnostic to a hard assert.
EOF

Send root-cause report + fix options to doyle


➜ SENT:doyle


Report sent. While awaiting doyle's design ruling, save the root cause to memory (significant, non-obvious — could be lost on a context clear).


Write

---
name: v0121-l0-attach-deadlock-root
description: v0.12.1 L0 keystone REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT root cause — serve_attach replay-vs-forward IPC deadlock
metadata:
  type: project
---

v0.12.1 Wave 2 **L0 KEYSTONE root-caused** (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT, the operator's "attach shows no output"). Branch v0.12.1-lifecycle @0d8ed2e (doyle's minted REQ). Found via instrumenting the real dummy fixture (`dummy_harness_e2e.rs`), NOT static reading.

**ROOT (definitive):** a broker REPLAY-vs-FORWARD IPC deadlock in `serve_attach`, triggered ONLY by a non-empty `OutputLog` ring at attach = an already-producing harness = exactly endpoint-run. `serve_attach` receives session Output (broker→serve) AND sends its forwards (serve→broker) on the **same** broker IPC connection. `become_controller` (broker.rs ~249) does a synchronous **multi-frame** ring replay inline inside `dispatch_subscribe`, holding the broker's single-threaded per-conn handler in the broker→serve write loop → it can't read serve's serve→broker `net_stream_send` forwards → both directions back up → mutual backpressure deadlock → operator (rc) gets 0 bytes.

**Ruled OUT:** drain works (ring fills to 9), not the dummy program, not worker starvation (tried net runtime 2→8 workers, nethost.rs:653, no change). Smoking gun: serve prints SERVE_FWD seq0/seq1 but broker-side `send_stream(stream=1)` NEVER runs → loopback op_r read pump never reads.

**Why green-but-broken:** known-good `local_attach_via_loopback_conn_rides_the_same_pump` attaches to an IDLE echo child (empty ring → 0-frame replay) and forwards ONE chunk; cwd E2E reads on the spawning conn (no serve-over-IPC). Neither exercises full-ring replay + sustained forward on one conn. **Likely cross-node too** (same serve_attach shape over QUIC).

**Fix options sent to doyle (awaiting design ruling):** (a) serve_attach forwards on a SEPARATE broker conn from the one it subscribes on — surgical, `send_stream` is stream_id-keyed/conn-agnostic; my lean for v0.12.1. (b) make `become_controller` replay non-blocking via the dedicated-writer-thread+bounded-channel pattern `add_viewer`/`viewer_writer` already use — more complete (fixes cross-node) but touches the authoritative cursor-advancing controller path. Then activate REQ stages [impl,unit,int] + flip fixture `rc_saw_tick` diagnostic → hard assert. See [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]].


File created successfully at: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v0121-l0-attach-deadlock-root.md (file state is current in your context — no need to Read it back)

---
name: v0121-l0-attach-deadlock-root
description: v0.12.1 L0 keystone REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT root cause — serve_attach replay-vs-forward IPC deadlock
metadata:
  node_type: memory
  type: project
  originSessionId: 3acfe970-b087-4764-8a3e-c2253b8bd0ae
---

v0.12.1 Wave 2 **L0 KEYSTONE root-caused** (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT, the operator's "attach shows no output"). Branch v0.12.1-lifecycle @0d8ed2e (doyle's minted REQ). Found via instrumenting the real dummy fixture (`dummy_harness_e2e.rs`), NOT static reading.

**ROOT (definitive):** a broker REPLAY-vs-FORWARD IPC deadlock in `serve_attach`, triggered ONLY by a non-empty `OutputLog` ring at attach = an already-producing harness = exactly endpoint-run. `serve_attach` receives session Output (broker→serve) AND sends its forwards (serve→broker) on the **same** broker IPC connection. `become_controller` (broker.rs ~249) does a synchronous **multi-frame** ring replay inline inside `dispatch_subscribe`, holding the broker's single-threaded per-conn handler in the broker→serve write loop → it can't read serve's serve→broker `net_stream_send` forwards → both directions back up → mutual backpressure deadlock → operator (rc) gets 0 bytes.

**Ruled OUT:** drain works (ring fills to 9), not the dummy program, not worker starvation (tried net runtime 2→8 workers, nethost.rs:653, no change). Smoking gun: serve prints SERVE_FWD seq0/seq1 but broker-side `send_stream(stream=1)` NEVER runs → loopback op_r read pump never reads.

**Why green-but-broken:** known-good `local_attach_via_loopback_conn_rides_the_same_pump` attaches to an IDLE echo child (empty ring → 0-frame replay) and forwards ONE chunk; cwd E2E reads on the spawning conn (no serve-over-IPC). Neither exercises full-ring replay + sustained forward on one conn. **Likely cross-node too** (same serve_attach shape over QUIC).

**Fix options sent to doyle (awaiting design ruling):** (a) serve_attach forwards on a SEPARATE broker conn from the one it subscribes on — surgical, `send_stream` is stream_id-keyed/conn-agnostic; my lean for v0.12.1. (b) make `become_controller` replay non-blocking via the dedicated-writer-thread+bounded-channel pattern `add_viewer`/`viewer_writer` already use — more complete (fixes cross-node) but touches the authoritative cursor-advancing controller path. Then activate REQ stages [impl,unit,int] + flip fixture `rc_saw_tick` diagnostic → hard assert. See [[v0121-lifecycle-realharness]] [[v0121-realharness-reopen]].


1	- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave
2	- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
3	- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) — dummy-harness fixture EXECUTION state (todlando). `mock-session --mode dummy` (binds+heartbeats+stays-alive) + dummy_harness_e2e.rs. Bringup PROVEN green (online+alive+psyche). 3 load-bearing fixture findings: psyche_init required for online (startup.rs:464), test must be NET-ENABLED (rc loopback needs net host), rc stdin held-open. OPEN: rc attaches but gets no PTY output — awaiting doyle ruling (Wave1=connect-only vs root-now). On-branch UNCOMMITTED. Full Wave2 keystone + repro data here too
4	- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) — gating a release CHANGELOG: map EVERY commit in `vPREV..vNEW` to a bullet (catch misses) + check each bullet's SCOPE isn't broader than its commit (catch overstatements — a touched feature may predate the release). v0.12.0 had both errors. Post-publish fix = docs-only (CHANGELOG.md on main + gh release edit notes), no retag

- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave
- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable
- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) — dummy-harness fixture EXECUTION state (todlando). `mock-session --mode dummy` (binds+heartbeats+stays-alive) + dummy_harness_e2e.rs. Bringup PROVEN green (online+alive+psyche). 3 load-bearing fixture findings: psyche_init required for online (startup.rs:464), test must be NET-ENABLED (rc loopback needs net host), rc stdin held-open. OPEN: rc attaches but gets no PTY output — awaiting doyle ruling (Wave1=connect-only vs root-now). On-branch UNCOMMITTED. Full Wave2 keystone + repro data here too
- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) — gating a release CHANGELOG: map EVERY commit in `vPREV..vNEW` to a bullet (catch misses) + check each bullet's SCOPE isn't broader than its commit (catch overstatements — a touched feature may predate the release). v0.12.0 had both errors. Post-publish fix = docs-only (CHANGELOG.md on main + gh release edit notes), no retag


Add the index pointer (keeping it short per the size warning):


Edit

- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) —

- [v0.12.1 L0 attach-deadlock root](v0121-l0-attach-deadlock-root.md) — KEYSTONE root-caused: serve_attach receives+forwards on SAME broker IPC conn; become_controller's inline multi-frame ring replay blocks the single-threaded conn handler from reading serve's forwards → backpressure deadlock → rc 0 bytes. Only fires with non-empty ring (already-producing harness = endpoint-run). Awaiting doyle fix ruling: (a) separate forward conn vs (b) non-blocking replay
- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) —


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\MEMORY.md has been updated successfully. (file state is current in your context — no need to Read it back)

- [v0.12.1 L0 attach-deadlock root](v0121-l0-attach-deadlock-root.md) — KEYSTONE root-caused: serve_attach receives+forwards on SAME broker IPC conn; become_controller's inline multi-frame ring replay blocks the single-threaded conn handler from reading serve's forwards → backpressure deadlock → rc 0 bytes. Only fires with non-empty ring (already-producing harness = endpoint-run). Awaiting doyle fix ruling: (a) separate forward conn vs (b) non-blocking replay
- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) —

- [v0.12.0 lifecycle milestone](v012-lifecycle-milestone.md) — SHIPPED 2026-06-18 (counter 25, MINOR). Operator-mandated BLOCKING: status=online latch reconciled vs liveness (controllable-gated, relay exempt) + stop/restart teardown/reap. 9 items all GATE-PASS; CI escape (Linux /bin/sh→dash basename in reap unit) caught+fixed @6de905b; post-publish changelog corrected (resume-picker = ready-only, +un-host-reap bullet). Roots verified-at-code-first; full seam-bar/wave

- [v0.12.1 real-harness reopen](v0121-realharness-reopen.md) — v0.12.0 SHIPPED but lifecycle fixes DON'T work in the real claude-spt harness (gated on mocks/in-proc → green-but-broken). Reopened v0.12.1 (branch+JIT+5 REQs @a4a76c6). Roots: VIEWER-CLOSE-DETACH (missing CREATE_BREAKAWAY_FROM_JOB → terminal Job Object reaps daemon's harness on tab-close) + ATTACH-WEDGE (dead PTY child + undrained pump blocks 2-worker loopback runtime). BINDING: gate every lifecycle fix vs a REAL dummy-harness fixture + real daemon, NO mocks. Invariant "a view is independent from the endpoint" in CONTEXT.md. On legacy spt here = spt-core freely restartable

+- [v0.12.1 L0 attach-deadlock root](v0121-l0-attach-deadlock-root.md) — KEYSTONE root-caused: serve_attach receives+forwards on SAME broker IPC conn; become_controller's inline multi-frame ring replay blocks the single-threaded conn handler from reading serve's forwards → backpressure deadlock → rc 0 bytes. Only fires with non-empty ring (already-producing harness = endpoint-run). Awaiting doyle fix ruling: (a) separate forward conn vs (b) non-blocking replay

- [v0.12.1 Wave 1 progress](v0121-lifecycle-realharness.md) — dummy-harness fixture EXECUTION state (todlando). `mock-session --mode dummy` (binds+heartbeats+stays-alive) + dummy_harness_e2e.rs. Bringup PROVEN green (online+alive+psyche). 3 load-bearing fixture findings: psyche_init required for online (startup.rs:464), test must be NET-ENABLED (rc loopback needs net host), rc stdin held-open. OPEN: rc attaches but gets no PTY output — awaiting doyle ruling (Wave1=connect-only vs root-now). On-branch UNCOMMITTED. Full Wave2 keystone + repro data here too

- [changelog scope vs commit range](changelog-scope-vs-commit-range.md) — gating a release CHANGELOG: map EVERY commit in `vPREV..vNEW` to a bullet (catch misses) + check each bullet's SCOPE isn't broader than its commit (catch overstatements — a touched feature may predate the release). v0.12.0 had both errors. Post-publish fix = docs-only (CHANGELOG.md on main + gh release edit notes), no retag

- [pre-flight clippy --workspace](ci-clippy-preflight-workspace.md) — CI denies warnings workspace-wide (`clippy --workspace --all-targets -D warnings`); a `-p <crate>` local check misses sibling-crate lints → CI red round-trip (M11-W1: `-p spt` missed a `useless_vec` in spt-daemon's act-gate test). Pre-flight the exact CI invocation. CI only fires on PR (push branches=main/dev-freeform)


Root cause found, reported to doyle, saved to memory. Tree clean. At a natural checkpoint — the fix is a broker-model design fork that's doyle's call (gater/designer per roles), so awaiting his ruling rather than speculatively building the wrong option.

## Summary

**Proceeded with v0.12.1 Wave 2 L0 (keystone).** Synced branch to doyle's minted REQ @0d8ed2e, then root-caused the operator's "attach shows no output" bug.

**Root cause (definitive, via instrumenting the real dummy fixture):** broker replay-vs-forward IPC deadlock in `serve_attach`. It receives session Output AND sends forwards on the **same** broker IPC connection. `become_controller`'s inline multi-frame ring replay holds the broker's single-threaded per-conn handler, so it can't read serve's forward frames → both directions back up → deadlock → rc gets 0 bytes.

**Trigger = non-empty OutputLog ring at attach** = an already-producing harness = exactly endpoint-run. Ruled out: drain (ring fills to 9), the dummy program, worker starvation (2→8 workers, no change).

**Why green-but-broken:** known-good loopback E2E attaches to an idle child (empty ring → 0-frame replay, 1 chunk forwarded) — never exercises full-ring replay + sustained forward on one conn. Likely affects cross-node attach to a busy session too.

**Sent doyle 2 fix options:** (a) serve_attach forwards on a separate broker conn — surgical, my lean; (b) non-blocking controller replay (reuse `viewer_writer` pattern) — more complete, fixes cross-node, touches the authoritative cursor path.

Awaiting his design ruling, then I activate the REQ stages + flip the fixture's `rc_saw_tick` diagnostic to a hard assert.


<task-notification>
<task-id>bzoq0kglt</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="7f6f95d4" type="msg" from="doyle">RULING: Option (a) for v0.12.1 — serve_attach forwards on a SEPARATE broker conn from the one it subscribes/receives on. It's a structural root fix (decouples the two blocking IPC directions onto independent single-threaded handlers, removing the mutual block), NOT a tape; it does NOT touch the authoritative cursor-ad</EVENT-PART>
<EVENT-PART seq="2/7" id="7f6f95d4">vancing controller path (lowest risk for a PATCH); send_stream being stream_id-keyed makes it clean. Excellent root-cause.

CROSS-NODE: (a) covers it BY CONSTRUCTION — cross-node uses the same single-conn multiplex (forward-over-QUIC on the subscribe conn), so the same conn-separation fixes it. Don't block v0.12.1 on a twohost run: gat</EVENT-PART>
<EVENT-PART seq="3/7" id="7f6f95d4">e the LOCAL case now; if the [twohost] rig is cheap add a populated-ring cross-node rider, else document cross-node as covered-by-construction + leave a note for twohost validation.

REQUIREMENTS for the fix:
- Preserve ORDER: replay (historical) then live, in order, to the operator. (a) keeps serve receiving all output in order on co</EVENT-PART>
<EVENT-PART seq="4/7" id="7f6f95d4">nn-1 and forwarding in order on conn-2 — confirm no interleave.
- Preserve controller cursor semantics (delivered_through advance) unchanged — (a) shouldn't touch it; verify.
- Activate REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT [impl,unit,int], tag in-commit.
- GATE (the gap that hid this): int = attach to a LIVE, ALREADY-PRODUCING (N</EVENT-PART>
<EVENT-PART seq="5/7" id="7f6f95d4">ON-EMPTY ring) endpoint-run harness DELIVERS its DUMMY_HARNESS_TICK within a bounded window (flip your rc_saw_tick diagnostic to a hard assert). Add a unit pinning the deadlock root: a non-empty ring at attach does not wedge the forward path. This non-empty-ring-at-attach case is the regression the suite lacked.

RECORD (don't lose the d</EVENT-PART>
<EVENT-PART seq="6/7" id="7f6f95d4">eeper fix): note in the commit + REQ that the controller path does SYNCHRONOUS inline replay while viewers use the async dedicated-writer-thread+bounded-channel pattern — that divergence is the deeper root. Option (b) = unify the controller replay onto that async pattern; flag it as a principled POST-v0.12.1 follow-up (NOT now — it touches</EVENT-PART>
<EVENT-PART seq="7/7" id="7f6f95d4"> the authoritative cursor path).

Ping me SEAM-GREEN on L0, then L1 (VIEWER-CLOSE-DETACH) + L2 (ATTACH-WEDGE). I re-run the gate independently.</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.


doyle ruled: **Option (a)** — serve_attach forwards on a separate broker conn. Requirements: preserve order (replay→live), don't touch the cursor path, activate REQ [impl,unit,int], int gate = non-empty-ring endpoint-run delivers tick (flip rc_saw_tick to hard assert), unit pins non-empty-ring-no-wedge, record the deeper sync-vs-async divergence + flag (b) as post-v0.12.1. Cross-node covered by construction.

Executing. Read full serve_attach to enumerate every wire send.


1	//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
2	//! onto a local session for an operator on another node.
3	//!
4	//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
5	//! [`AttachRecord::Request`]; this module is the target-side pump between that
6	//! stream and the local broker-hosted session:
7	//!
8	//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
9	//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
10	//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
11	//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
12	//!   journal (a replayed input record cannot double-type);
13	//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
14	//!
15	//! ## Exactly-once lives at the EDGES, not the wire sends
16	//!
17	//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
18	//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
19	//! and *discards* everything interleaved — inside a full-duplex pump loop that
20	//! eats the operator's `Input` records (both sides starve; found the hard way
21	//! in `tests/attach.rs`). Idempotency holds without it because each direction
22	//! dedups at its **effect**: the PTY write is journaled by the operator-minted
23	//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
24	//! session-log `seq` (the render cursor). A post-restart re-serve may
25	//! re-transmit output records on the wire — duplicates are dropped at the
26	//! render cursor, the same discipline the local `OutputLog` replay uses
27	//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
28	//!
29	//! **Compute + files stay here**; the operator is a viewport (CONTEXT
30	//! §Remote-control vs local operation). The access gate runs before anything
31	//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
32	//! [`access_check`] sees the **handshake-proven** origin from the broker's
33	//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
34	//! your own running instance is ungated by default (ADR-0009 default-open);
35	//! the D5d whitelist drops in at this same call.
36	//!
37	//! Restart survival composes from D4/B5 with **no new machinery**: the broker
38	//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
39	//! A restarted target brain re-attaches the session from its output cursor,
40	//! resubscribes the stream from its net cursor, and re-drives journaled sends;
41	//! a restarted operator brain resubscribes and dedups rendered chunks by
42	//! `Output.seq`.
43
44	use std::io;
45
46	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
47	use spt_net::net::ndjson;
48
49	use crate::access::{access_check, AccessDecision, InboundClass};
50	use crate::brain::{Brain, BrokerEvent};
51	use crate::msg::{decode_bytes, encode_bytes};
52
53	/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
54	/// served viewport is a **driver** — attaching wakes the endpoint, the
55	/// viewport ending detaches it (active → dormant; the transition echo is
56	/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
57	/// [`mark_driven_by`]: a session served without a perch has no resting
58	/// record to move.
59	// [impl->REQ-INST-3]
60	fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
61	    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
62	}
63
64	/// Why a serve loop returned.
65	#[derive(Debug, Clone, PartialEq, Eq)]
66	pub enum AttachServeOutcome {
67	    /// The access gate refused the requesting origin, OR a `Control` attach hit
68	    /// an endpoint a DIFFERENT operator already controls (busy-refuse,
69	    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the
70	    /// busy guidance client-side from the endpoint's `driven_by`.
71	    Refused,
72	    /// The session's child exited; the `Exit` record was sent and the stream
73	    /// finished.
74	    Exited(Option<i32>),
75	    /// The operator closed its send side (detached); the viewport ended with
76	    /// the session still running.
77	    Detached,
78	    /// This controller was DISPLACED by a different operator's `--take`/`kick`
79	    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream
80	    /// finished. The endpoint stays driven (by the taker) — no dormant edge.
81	    Displaced,
82	}
83
84	/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]
85	/// arrives — gates wake (controller-only) and PTY input.
86	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87	enum ServeRole {
88	    /// The interactive controller (input + resize; wakes the endpoint).
89	    Controller,
90	    /// A read-only viewer (output only; wake-neutral).
91	    Viewer,
92	}
93
94	/// Serve one attach stream against this brain's broker, blocking until the
95	/// session exits, the operator detaches, or the gate refuses.
96	///
97	/// `origin_node` MUST be the handshake-proven remote id from the broker's
98	/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
99	/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
100	/// is being attached, for the gate. `net_from_seq` is this brain's resume
101	/// cursor on the attach stream (0 cold — also the safe post-restart choice:
102	/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
103	/// journal, and re-pumped output dedups at the operator's render cursor).
104	// [impl->REQ-INST-8]
105	// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
106	pub fn serve_attach(

//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
//! onto a local session for an operator on another node.
//!
//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
//! [`AttachRecord::Request`]; this module is the target-side pump between that
//! stream and the local broker-hosted session:
//!
//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
//!   journal (a replayed input record cannot double-type);
//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
//!
//! ## Exactly-once lives at the EDGES, not the wire sends
//!
//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
//! and *discards* everything interleaved — inside a full-duplex pump loop that
//! eats the operator's `Input` records (both sides starve; found the hard way
//! in `tests/attach.rs`). Idempotency holds without it because each direction
//! dedups at its **effect**: the PTY write is journaled by the operator-minted
//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
//! session-log `seq` (the render cursor). A post-restart re-serve may
//! re-transmit output records on the wire — duplicates are dropped at the
//! render cursor, the same discipline the local `OutputLog` replay uses
//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
//!
//! **Compute + files stay here**; the operator is a viewport (CONTEXT
//! §Remote-control vs local operation). The access gate runs before anything
//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
//! [`access_check`] sees the **handshake-proven** origin from the broker's
//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
//! your own running instance is ungated by default (ADR-0009 default-open);
//! the D5d whitelist drops in at this same call.
//!
//! Restart survival composes from D4/B5 with **no new machinery**: the broker
//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
//! A restarted target brain re-attaches the session from its output cursor,
//! resubscribes the stream from its net cursor, and re-drives journaled sends;
//! a restarted operator brain resubscribes and dedups rendered chunks by
//! `Output.seq`.

use std::io;

use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
use spt_net::net::ndjson;

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};

/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
/// served viewport is a **driver** — attaching wakes the endpoint, the
/// viewport ending detaches it (active → dormant; the transition echo is
/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
/// [`mark_driven_by`]: a session served without a perch has no resting
/// record to move.
// [impl->REQ-INST-3]
fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
}

/// Why a serve loop returned.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AttachServeOutcome {
    /// The access gate refused the requesting origin, OR a `Control` attach hit
    /// an endpoint a DIFFERENT operator already controls (busy-refuse,
    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the
    /// busy guidance client-side from the endpoint's `driven_by`.
    Refused,
    /// The session's child exited; the `Exit` record was sent and the stream
    /// finished.
    Exited(Option<i32>),
    /// The operator closed its send side (detached); the viewport ended with
    /// the session still running.
    Detached,
    /// This controller was DISPLACED by a different operator's `--take`/`kick`
    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream
    /// finished. The endpoint stays driven (by the taker) — no dormant edge.
    Displaced,
}

/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]
/// arrives — gates wake (controller-only) and PTY input.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ServeRole {
    /// The interactive controller (input + resize; wakes the endpoint).
    Controller,
    /// A read-only viewer (output only; wake-neutral).
    Viewer,
}

/// Serve one attach stream against this brain's broker, blocking until the
/// session exits, the operator detaches, or the gate refuses.
///
/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
/// is being attached, for the gate. `net_from_seq` is this brain's resume
/// cursor on the attach stream (0 cold — also the safe post-restart choice:
/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
/// journal, and re-pumped output dedups at the operator's render cursor).
// [impl->REQ-INST-8]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
pub fn serve_attach(


106	pub fn serve_attach(
107	    brain: &mut Brain,
108	    stream_id: u64,
109	    endpoint: &str,
110	    origin_node: &str,
111	    net_from_seq: u64,
112	) -> io::Result<AttachServeOutcome> {
113	    brain.net_stream_subscribe(stream_id, net_from_seq)?;
114	    let mut decoder = AttachDecoder::new();
115	    let mut attached = false;
116	    // The Request's intent — gates wire input (a viewer's keystrokes never reach
117	    // the PTY) independent of the broker's later role reply (no IPC-ordering
118	    // race on the input gate).
119	    let mut intent = AttachIntent::Control;
120	    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:
121	    // controller input that races ahead of the reply is BUFFERED, not dropped
122	    // (the replayed-input-after-restart case) and not leaked to the PTY before
123	    // we know we control the session (the busy-refuse case).
124	    let mut role: Option<ServeRole> = None;
125	    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();
126	    // The attached session — captured from the Request so the terminal paths can
127	    // explicitly release this worker's role (clearing driven_by / viewer_count
128	    // promptly, not only on connection drop).
129	    let mut attached_session: Option<u64> = None;
130
131	    loop {
132	        match brain.read_event()? {
133	            // ── Wire → session: requests, input, resize. ───────────────────
134	            BrokerEvent::NetStreamData {
135	                stream_id: sid,
136	                bytes,
137	                ..
138	            } if sid == stream_id => {
139	                for rec in decoder.push(&bytes) {
140	                    match rec {
141	                        AttachRecord::Request {
142	                            session_id,
143	                            from_seq,
144	                            intent: req_intent,
145	                        } => {
146	                            // Unsolicited wire inbound — the ADR-0009 gate, under
147	                            // the transport-proven origin. Viewing is gated
148	                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:
149	                            // watching reveals full session contents).
150	                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)
151	                                == AccessDecision::Deny
152	                            {
153	                                brain.net_stream_send(stream_id, &[], None, true)?;
154	                                return Ok(AttachServeOutcome::Refused);
155	                            }
156	                            intent = req_intent;
157	                            attached_session = Some(session_id);
158	                            // Role-aware subscribe (REQ-RCVIEW-1): the broker
159	                            // resolves controller / viewer / busy / take by the
160	                            // intent + this handshake-proven origin, and replies a
161	                            // `Subscribed`. driven_by / viewer_count are now
162	                            // BROKER-owned (single writer — resolves the
163	                            // clear-race), so this loop no longer stamps them.
164	                            brain.attach_as(session_id, from_seq, req_intent, Some(origin_node))?;
165	                            attached = true;
166	                        }
167	                        AttachRecord::Input { data_b64, op_id }
168	                            if attached && intent != AttachIntent::Viewer =>
169	                        {
170	                            let bytes = decode_bytes(&data_b64)
171	                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
172	                            match role {
173	                                // Confirmed controller — exactly-once at the PTY
174	                                // (the operator-minted id rides the B5 journal).
175	                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,
176	                                // Outcome not yet known — buffer; flushed on a
177	                                // controller confirmation, discarded on busy/viewer.
178	                                None => pending_input.push((op_id, bytes)),
179	                                // A viewer never reaches here (intent gate).
180	                                Some(ServeRole::Viewer) => {}
181	                            }
182	                        }
183	                        AttachRecord::Resize { rows, cols }
184	                            if attached && intent != AttachIntent::Viewer =>
185	                        {
186	                            // Controller → PTY resize (controller-EXCLUSIVE; the
187	                            // broker rejects it on a viewer's stream). A viewer's
188	                            // Resize is dropped here by the intent gate too.
189	                            let _ = brain.resize(rows, cols);
190	                        }
191	                        // Input/Resize before Request, a viewer's input/resize,
192	                        // or target-direction records echoed back: noise — ignore.
193	                        _ => {}
194	                    }
195	                }
196	            }
197
198	            // ── Broker → loop: the resolved controller/viewer role. ─────────
199	            BrokerEvent::Subscribed { outcome, .. } => {
200	                use crate::msg::SubscribeOutcome as O;
201	                match outcome {
202	                    O::Controller | O::TookControl => {
203	                        role = Some(ServeRole::Controller);
204	                        // A driver attached — the wake edge (controller-only;
205	                        // idempotent on an already-active seat).
206	                        feed_rest(endpoint, crate::resting::RestEvent::Wake);
207	                        // Flush any input that raced ahead of the reply.
208	                        for (op, b) in pending_input.drain(..) {
209	                            brain.send_effect(op, &b)?;
210	                        }
211	                    }
212	                    O::Viewer => {
213	                        role = Some(ServeRole::Viewer);
214	                        // Wake-neutral: a viewer never wakes the endpoint, never
215	                        // keeps it awake (Gap B). Discard any racing input.
216	                        pending_input.clear();
217	                    }
218	                    O::BusyControlled { .. } => {
219	                        // A different operator controls it — refuse (no PTY input
220	                        // ever reached it; the buffer is discarded). The rc
221	                        // renders the busy guidance client-side from driven_by.
222	                        brain.net_stream_send(stream_id, &[], None, true)?;
223	                        return Ok(AttachServeOutcome::Refused);
224	                    }
225	                }
226	            }
227	            // Viewer letterbox feed: the current PTY size (on attach + resize).
228	            BrokerEvent::Size { rows, cols, .. } if attached => {
229	                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });
230	                brain.net_stream_send(stream_id, &line, None, false)?;
231	            }
232	            // This controller was kicked by a different operator's `--take`.
233	            BrokerEvent::Displaced { by, .. } if attached => {
234	                let line = ndjson::encode_line(&AttachRecord::Displaced { by });
235	                brain.net_stream_send(stream_id, &line, None, true)?;
236	                // No dormant edge: the endpoint stays driven by the taker (whose
237	                // own serve loop already fired the wake).
238	                return Ok(AttachServeOutcome::Displaced);
239	            }
240
241	            // The operator closed its send side: the viewport is over.
242	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
243	                brain.net_stream_send(stream_id, &[], None, true)?;
244	                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes
245	                // info.json, carrying the current driven_by through), THEN release
246	                // the role over IPC so the broker's driven_by=None is the LAST
247	                // write — otherwise a concurrent resting RMW restores the stale
248	                // marker (the clear-race the broker-single-writer model averts).
249	                if role == Some(ServeRole::Controller) {
250	                    // The driver detached — the endpoint rests warm (controller-
251	                    // only; a viewer leaving is wake-neutral).
252	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
253	                }
254	                if let Some(sid) = attached_session {
255	                    let _ = brain.detach_session(sid); // release the role (clears markers)
256	                }
257	                return Ok(AttachServeOutcome::Detached);
258	            }
259
260	            // ── Session → wire: output and exit. ────────────────────────────
261	            BrokerEvent::Output { seq, bytes, .. } if attached => {
262	                let line = ndjson::encode_line(&AttachRecord::Output {
263	                    seq,
264	                    data_b64: encode_bytes(&bytes),
265	                });
266	                // Unjournaled by design (see module docs): `seq` rides inside
267	                // the record, and the operator's render cursor is the dedup.
268	                brain.net_stream_send(stream_id, &line, None, false)?;
269	            }
270	            BrokerEvent::Exit { code, .. } if attached => {
271	                let line = ndjson::encode_line(&AttachRecord::Exit { code });
272	                brain.net_stream_send(stream_id, &line, None, true)?;
273	                if role == Some(ServeRole::Controller) {
274	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
275	                }
276	                return Ok(AttachServeOutcome::Exited(code));
277	            }
278
279	            BrokerEvent::Error { message } => {
280	                if role == Some(ServeRole::Controller) {
281	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
282	                }
283	                return Err(io::Error::other(message));
284	            }
285	            // Applied/NetSent acks and unrelated events: not ours to handle.
286	            _ => {}
287	        }
288	    }
289	}
290
291	/// Operator-side: open an attach stream on a dialed connection and send the
292	/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes
293	/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes
294	/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the
295	/// durable id for the exactly-once stream open (safe to block for its ack —
296	/// no attach traffic can be in flight before the stream exists); the Request
297	/// send itself is unjournaled (see module docs — a replayed Request re-gates
298	/// and re-attaches idempotently).
299	// [impl->REQ-INST-8]
300	pub fn request_attach(

pub fn serve_attach(
    brain: &mut Brain,
    stream_id: u64,
    endpoint: &str,
    origin_node: &str,
    net_from_seq: u64,
) -> io::Result<AttachServeOutcome> {
    brain.net_stream_subscribe(stream_id, net_from_seq)?;
    let mut decoder = AttachDecoder::new();
    let mut attached = false;
    // The Request's intent — gates wire input (a viewer's keystrokes never reach
    // the PTY) independent of the broker's later role reply (no IPC-ordering
    // race on the input gate).
    let mut intent = AttachIntent::Control;
    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:
    // controller input that races ahead of the reply is BUFFERED, not dropped
    // (the replayed-input-after-restart case) and not leaked to the PTY before
    // we know we control the session (the busy-refuse case).
    let mut role: Option<ServeRole> = None;
    let mut pending_input: Vec<(u64, Vec<u8>)> = Vec::new();
    // The attached session — captured from the Request so the terminal paths can
    // explicitly release this worker's role (clearing driven_by / viewer_count
    // promptly, not only on connection drop).
    let mut attached_session: Option<u64> = None;

    loop {
        match brain.read_event()? {
            // ── Wire → session: requests, input, resize. ───────────────────
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                for rec in decoder.push(&bytes) {
                    match rec {
                        AttachRecord::Request {
                            session_id,
                            from_seq,
                            intent: req_intent,
                        } => {
                            // Unsolicited wire inbound — the ADR-0009 gate, under
                            // the transport-proven origin. Viewing is gated
                            // IDENTICALLY to driving in v1 (REQ-RCVIEW-1, Gap A:
                            // watching reveals full session contents).
                            if access_check(endpoint, origin_node, InboundClass::Unsolicited)
                                == AccessDecision::Deny
                            {
                                brain.net_stream_send(stream_id, &[], None, true)?;
                                return Ok(AttachServeOutcome::Refused);
                            }
                            intent = req_intent;
                            attached_session = Some(session_id);
                            // Role-aware subscribe (REQ-RCVIEW-1): the broker
                            // resolves controller / viewer / busy / take by the
                            // intent + this handshake-proven origin, and replies a
                            // `Subscribed`. driven_by / viewer_count are now
                            // BROKER-owned (single writer — resolves the
                            // clear-race), so this loop no longer stamps them.
                            brain.attach_as(session_id, from_seq, req_intent, Some(origin_node))?;
                            attached = true;
                        }
                        AttachRecord::Input { data_b64, op_id }
                            if attached && intent != AttachIntent::Viewer =>
                        {
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                            match role {
                                // Confirmed controller — exactly-once at the PTY
                                // (the operator-minted id rides the B5 journal).
                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,
                                // Outcome not yet known — buffer; flushed on a
                                // controller confirmation, discarded on busy/viewer.
                                None => pending_input.push((op_id, bytes)),
                                // A viewer never reaches here (intent gate).
                                Some(ServeRole::Viewer) => {}
                            }
                        }
                        AttachRecord::Resize { rows, cols }
                            if attached && intent != AttachIntent::Viewer =>
                        {
                            // Controller → PTY resize (controller-EXCLUSIVE; the
                            // broker rejects it on a viewer's stream). A viewer's
                            // Resize is dropped here by the intent gate too.
                            let _ = brain.resize(rows, cols);
                        }
                        // Input/Resize before Request, a viewer's input/resize,
                        // or target-direction records echoed back: noise — ignore.
                        _ => {}
                    }
                }
            }

            // ── Broker → loop: the resolved controller/viewer role. ─────────
            BrokerEvent::Subscribed { outcome, .. } => {
                use crate::msg::SubscribeOutcome as O;
                match outcome {
                    O::Controller | O::TookControl => {
                        role = Some(ServeRole::Controller);
                        // A driver attached — the wake edge (controller-only;
                        // idempotent on an already-active seat).
                        feed_rest(endpoint, crate::resting::RestEvent::Wake);
                        // Flush any input that raced ahead of the reply.
                        for (op, b) in pending_input.drain(..) {
                            brain.send_effect(op, &b)?;
                        }
                    }
                    O::Viewer => {
                        role = Some(ServeRole::Viewer);
                        // Wake-neutral: a viewer never wakes the endpoint, never
                        // keeps it awake (Gap B). Discard any racing input.
                        pending_input.clear();
                    }
                    O::BusyControlled { .. } => {
                        // A different operator controls it — refuse (no PTY input
                        // ever reached it; the buffer is discarded). The rc
                        // renders the busy guidance client-side from driven_by.
                        brain.net_stream_send(stream_id, &[], None, true)?;
                        return Ok(AttachServeOutcome::Refused);
                    }
                }
            }
            // Viewer letterbox feed: the current PTY size (on attach + resize).
            BrokerEvent::Size { rows, cols, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });
                brain.net_stream_send(stream_id, &line, None, false)?;
            }
            // This controller was kicked by a different operator's `--take`.
            BrokerEvent::Displaced { by, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Displaced { by });
                brain.net_stream_send(stream_id, &line, None, true)?;
                // No dormant edge: the endpoint stays driven by the taker (whose
                // own serve loop already fired the wake).
                return Ok(AttachServeOutcome::Displaced);
            }

            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                brain.net_stream_send(stream_id, &[], None, true)?;
                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes
                // info.json, carrying the current driven_by through), THEN release
                // the role over IPC so the broker's driven_by=None is the LAST
                // write — otherwise a concurrent resting RMW restores the stale
                // marker (the clear-race the broker-single-writer model averts).
                if role == Some(ServeRole::Controller) {
                    // The driver detached — the endpoint rests warm (controller-
                    // only; a viewer leaving is wake-neutral).
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                if let Some(sid) = attached_session {
                    let _ = brain.detach_session(sid); // release the role (clears markers)
                }
                return Ok(AttachServeOutcome::Detached);
            }

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });
                // Unjournaled by design (see module docs): `seq` rides inside
                // the record, and the operator's render cursor is the dedup.
                brain.net_stream_send(stream_id, &line, None, false)?;
            }
            BrokerEvent::Exit { code, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Exit { code });
                brain.net_stream_send(stream_id, &line, None, true)?;
                if role == Some(ServeRole::Controller) {
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Ok(AttachServeOutcome::Exited(code));
            }

            BrokerEvent::Error { message } => {
                if role == Some(ServeRole::Controller) {
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                return Err(io::Error::other(message));
            }
            // Applied/NetSent acks and unrelated events: not ours to handle.
            _ => {}
        }
    }
}

/// Operator-side: open an attach stream on a dialed connection and send the
/// [`AttachRecord::Request`]. Returns the stream id the caller then consumes
/// ([`AttachRecord::Output`] frames, deduped by `seq`) and writes
/// ([`AttachRecord::Input`] with operator-minted op ids). `open_op` is the
/// durable id for the exactly-once stream open (safe to block for its ack —
/// no attach traffic can be in flight before the stream exists); the Request
/// send itself is unjournaled (see module docs — a replayed Request re-gates
/// and re-attaches idempotently).
// [impl->REQ-INST-8]
pub fn request_attach(


All wire sends enumerated (L153, 222, 230, 235, 243, 268, 272). Fix = route them through a separate `wire` Brain. I'll add a `broker_name` param and cold_start the wire conn internally. Verify `cold_start` signature + how callers have the broker name.


Bash

grep -n "pub fn cold_start\b\|pub fn cold_start_pump\|pub fn now_ms" crates/spt-daemon/src/brain.rs; echo "---callers of serve_attach---"; grep -rn "serve_attach(" crates/spt-daemon/src crates/spt-daemon/tests crates/spt/src

cold_start sig + serve_attach callers


59:pub fn now_ms() -> u64 {
279:    pub fn cold_start(name: &str, now_ms: u64) -> io::Result<Self> {
305:    pub fn cold_start_pump(name: &str, now_ms: u64, io_timeout: Duration) -> io::Result<Self> {
---callers of serve_attach---
crates/spt-daemon/src/attach.rs:106:pub fn serve_attach(
crates/spt-daemon/src/dispatch.rs:350:                Some(endpoint) => match serve_attach(&mut brain, stream_id, &endpoint, origin, 0) {
crates/spt-daemon/tests/attach.rs:158:            serve_attach(&mut target, stream_a, &endpoint_owned, &origin, 0).expect("serve");
crates/spt-daemon/tests/attach.rs:333:        let outcome = serve_attach(&mut target, stream_a, "cc-ep", &origin, 0).expect("serve");
crates/spt-daemon/tests/attach.rs:602:        let outcome = serve_attach(&mut life2, stream_a, "ling", &origin2, 0).expect("re-serve");
crates/spt-daemon/tests/attach.rs:704:            serve_attach(&mut target, stream_a, &endpoint_serve, &origin, 0).expect("serve");


`cold_start(name, now_ms)`. Plan: add `broker_name: &str` param, cold_start a `wire` conn inside serve_attach, route all 7 wire sends through it. Thread the name through callers (dispatch has it; tests have `name`). Implement.


Edit

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{now_ms, Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};


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

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{now_ms, Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};

+use crate::brain::{now_ms, Brain, BrokerEvent};

/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a


// [impl->REQ-INST-8]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
pub fn serve_attach(
    brain: &mut Brain,
    stream_id: u64,
    endpoint: &str,
    origin_node: &str,
    net_from_seq: u64,
) -> io::Result<AttachServeOutcome> {
    brain.net_stream_subscribe(stream_id, net_from_seq)?;

// [impl->REQ-INST-8]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
pub fn serve_attach(
    brain: &mut Brain,
    broker_name: &str,
    stream_id: u64,
    endpoint: &str,
    origin_node: &str,
    net_from_seq: u64,
) -> io::Result<AttachServeOutcome> {
    brain.net_stream_subscribe(stream_id, net_from_seq)?;
    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS
    // (session Output / Size / Exit / Displaced / detach-finish onto the operator
    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES
    // session output (broker→serve) — including the controller's synchronous,
    // inline, multi-frame ring replay (`become_controller`), which holds the
    // broker's single-threaded per-conn handler for the whole replay. If the
    // forwards rode `brain` too, that stuck handler could not read them →
    // serve→broker backs up while broker→serve replay backs up → mutual IPC
    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an
    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The
    // two directions on independent single-threaded handlers cannot block each
    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from
    // a second conn is correct. Order is preserved: this one serve thread reads
    // every Output in seq order on `brain` and forwards it in that same order on
    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a
    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-
    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying
    // the controller onto that async path would remove the inline-replay block at
    // the source, but it touches the authoritative cursor-advancing path.)
    let mut wire = Brain::cold_start(broker_name, now_ms())?;