337	                .await
338	                .map_err(|e| io::Error::other(e.to_string())),
339	            SendHalf::Loopback(w) => w.write_all(bytes).await,
340	        }
341	    }
342
343	    /// Close the send side so the peer's read pump sees a clean end (QUIC
344	    /// `finish` / duplex `shutdown`).
345	    async fn finish(&mut self) -> io::Result<()> {
346	        match self {
347	            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),
348	            SendHalf::Loopback(w) => w.shutdown().await,
349	        }
350	    }
351	}
352
353	/// A stream's read half — fed into the single read pump in [`register_stream`].
354	/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.
355	/// Both append to the same [`StreamLog`], so the subscriber (and everything
356	/// above it) cannot tell the transports apart.
357	enum RecvHalf {
358	    Quic(RecvStream),

                .await
                .map_err(|e| io::Error::other(e.to_string())),
            SendHalf::Loopback(w) => w.write_all(bytes).await,
        }
    }

    /// Close the send side so the peer's read pump sees a clean end (QUIC
    /// `finish` / duplex `shutdown`).
    async fn finish(&mut self) -> io::Result<()> {
        match self {
            SendHalf::Quic(s) => s.finish().map_err(|e| io::Error::other(e.to_string())),
            SendHalf::Loopback(w) => w.shutdown().await,
        }
    }
}

/// A stream's read half — fed into the single read pump in [`register_stream`].
/// QUIC reads via quinn's chunk API; loopback via the read end of the duplex.
/// Both append to the same [`StreamLog`], so the subscriber (and everything
/// above it) cannot tell the transports apart.
enum RecvHalf {
    Quic(RecvStream),


360	}
361
362	/// One live stream the broker holds (QUIC or loopback): the send half (brain
363	/// writes ride it over IPC) + the sequenced read log.
364	struct StreamEntry {
365	    conn_id: u64,
366	    remote_id_hex: String,
367	    /// Whether THIS node opened the stream (`open_stream`) vs the peer
368	    /// (accept loop). The inbound dispatcher (D9-1) claims only
369	    /// peer-initiated streams; without this fact the table is ambiguous —
370	    /// it holds locally-opened outbound streams too (gotcha: a dispatcher
371	    /// that served its own requester stream would deadlock on itself).
372	    initiated_locally: bool,
373	    /// The send half, serialized behind an async mutex (writes happen on the
374	    /// host's runtime via `block_on`).
375	    send: tokio::sync::Mutex<SendHalf>,
376	    log: Arc<Mutex<StreamLog>>,
377	}
378
379	/// Tables shared between the host's sync surface and its runtime tasks
380	/// (accept loop, per-conn stream acceptors, per-stream read pumps).
381	struct NetShared {

}

/// One live stream the broker holds (QUIC or loopback): the send half (brain
/// writes ride it over IPC) + the sequenced read log.
struct StreamEntry {
    conn_id: u64,
    remote_id_hex: String,
    /// Whether THIS node opened the stream (`open_stream`) vs the peer
    /// (accept loop). The inbound dispatcher (D9-1) claims only
    /// peer-initiated streams; without this fact the table is ambiguous —
    /// it holds locally-opened outbound streams too (gotcha: a dispatcher
    /// that served its own requester stream would deadlock on itself).
    initiated_locally: bool,
    /// The send half, serialized behind an async mutex (writes happen on the
    /// host's runtime via `block_on`).
    send: tokio::sync::Mutex<SendHalf>,
    log: Arc<Mutex<StreamLog>>,
}

/// Tables shared between the host's sync surface and its runtime tasks
/// (accept loop, per-conn stream acceptors, per-stream read pumps).
struct NetShared {


Edit

    /// The send half, serialized behind an async mutex (writes happen on the
    /// host's runtime via `block_on`).
    send: tokio::sync::Mutex<SendHalf>,
    log: Arc<Mutex<StreamLog>>,
}

    /// The send half, serialized behind an async mutex (writes happen on the
    /// host's runtime via `block_on`).
    send: tokio::sync::Mutex<SendHalf>,
    log: Arc<Mutex<StreamLog>>,
    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below
    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].
    /// Unused by ordinary (non-retentive) streams.
    room: Arc<tokio::sync::Notify>,
}


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)

    /// The send half, serialized behind an async mutex (writes happen on the
    /// host's runtime via `block_on`).
    send: tokio::sync::Mutex<SendHalf>,
    log: Arc<Mutex<StreamLog>>,
}

    /// The send half, serialized behind an async mutex (writes happen on the
    /// host's runtime via `block_on`).
    send: tokio::sync::Mutex<SendHalf>,
    log: Arc<Mutex<StreamLog>>,
    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below
    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].
    /// Unused by ordinary (non-retentive) streams.
    room: Arc<tokio::sync::Notify>,
}

+    /// Wakes the read pump when a retentive (tunnel) stream's ring drains below

+    /// its backpressure cap (M11-W3, REQ-SHELL-4) — see [`StreamLog::retentive_cap`].

+    /// Unused by ordinary (non-retentive) streams.

/// Tables shared between the host's sync surface and its runtime tasks


Now register_stream — create the `room` notifier, store it, gate both read-pump arms on ring-room:


    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
    shared.streams.lock().unwrap().insert(
        id,
        Arc::new(StreamEntry {
            conn_id,
            remote_id_hex: remote_id_hex.to_string(),
            initiated_locally,
            send: tokio::sync::Mutex::new(send),
            log: Arc::clone(&log),
        }),
    );

    // The read pump — the stream's single producer. A clean end (peer finish)
    // and a torn end (conn lost) both finish the log; the buffered chunks stay
    // replayable either way. The transport differs only at this read leaf
    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the
    // same `StreamLog`, so nothing above the pump can tell them apart.
    tokio::spawn(async move {
        match recv {
            RecvHalf::Quic(mut recv) => loop {
                match recv.read_chunk(STREAM_READ_CHUNK).await {
                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
                    Ok(None) | Err(_) => {
                        log.lock().unwrap().finish();
                        break;
                    }
                }
            },
            RecvHalf::Loopback(mut recv) => {
                let mut buf = vec![0u8; STREAM_READ_CHUNK];
                loop {
                    match recv.read(&mut buf).await {
                        Ok(0) | Err(_) => {
                            log.lock().unwrap().finish();
                            break;
                        }
                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
                    }
                }
            }
        }
    });

    id
}

    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
    let room = Arc::new(tokio::sync::Notify::new());
    shared.streams.lock().unwrap().insert(
        id,
        Arc::new(StreamEntry {
            conn_id,
            remote_id_hex: remote_id_hex.to_string(),
            initiated_locally,
            send: tokio::sync::Mutex::new(send),
            log: Arc::clone(&log),
            room: Arc::clone(&room),
        }),
    );

    // The read pump — the stream's single producer. A clean end (peer finish)
    // and a torn end (conn lost) both finish the log; the buffered chunks stay
    // replayable either way. The transport differs only at this read leaf
    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the
    // same `StreamLog`, so nothing above the pump can tell them apart.
    //
    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,
    // so before pulling the next chunk the pump WAITS while the undrained ring is
    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so
    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the
    // SENDER — "lag, never loss" (CONTEXT:262). A `drain` wakes `room`. Ordinary
    // streams have `retentive_cap == 0`, so this gate is a no-op for them.
    tokio::spawn(async move {
        match recv {
            RecvHalf::Quic(mut recv) => loop {
                await_ring_room(&log, &room).await;
                match recv.read_chunk(STREAM_READ_CHUNK).await {
                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
                    Ok(None) | Err(_) => {
                        log.lock().unwrap().finish();
                        break;
                    }
                }
            },
            RecvHalf::Loopback(mut recv) => {
                let mut buf = vec![0u8; STREAM_READ_CHUNK];
                loop {
                    await_ring_room(&log, &room).await;
                    match recv.read(&mut buf).await {
                        Ok(0) | Err(_) => {
                            log.lock().unwrap().finish();
                            break;
                        }
                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
                    }
                }
            }
        }
    });

    id
}

/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained
/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for
/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.
// [impl->REQ-SHELL-4]
async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {
    loop {
        {
            let l = log.lock().unwrap();
            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {
                return;
            }
        }
        room.notified().await;
    }
}


    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
    shared.streams.lock().unwrap().insert(
        id,
        Arc::new(StreamEntry {
            conn_id,
            remote_id_hex: remote_id_hex.to_string(),
            initiated_locally,
            send: tokio::sync::Mutex::new(send),
            log: Arc::clone(&log),
        }),
    );

    // The read pump — the stream's single producer. A clean end (peer finish)
    // and a torn end (conn lost) both finish the log; the buffered chunks stay
    // replayable either way. The transport differs only at this read leaf
    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the
    // same `StreamLog`, so nothing above the pump can tell them apart.
    tokio::spawn(async move {
        match recv {
            RecvHalf::Quic(mut recv) => loop {
                match recv.read_chunk(STREAM_READ_CHUNK).await {
                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
                    Ok(None) | Err(_) => {
                        log.lock().unwrap().finish();
                        break;
                    }
                }
            },
            RecvHalf::Loopback(mut recv) => {
                let mut buf = vec![0u8; STREAM_READ_CHUNK];
                loop {
                    match recv.read(&mut buf).await {
                        Ok(0) | Err(_) => {
                            log.lock().unwrap().finish();
                            break;
                        }
                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
                    }
                }
            }
        }
    });

    id
}

    let id = shared.next_stream_id.fetch_add(1, Ordering::Relaxed);
    let log = Arc::new(Mutex::new(StreamLog::new(id, shared.ring_cap)));
    let room = Arc::new(tokio::sync::Notify::new());
    shared.streams.lock().unwrap().insert(
        id,
        Arc::new(StreamEntry {
            conn_id,
            remote_id_hex: remote_id_hex.to_string(),
            initiated_locally,
            send: tokio::sync::Mutex::new(send),
            log: Arc::clone(&log),
            room: Arc::clone(&room),
        }),
    );

    // The read pump — the stream's single producer. A clean end (peer finish)
    // and a torn end (conn lost) both finish the log; the buffered chunks stay
    // replayable either way. The transport differs only at this read leaf
    // (QUIC chunk API vs the loopback duplex's `AsyncRead`); both append to the
    // same `StreamLog`, so nothing above the pump can tell them apart.
    //
    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,
    // so before pulling the next chunk the pump WAITS while the undrained ring is
    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so
    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the
    // SENDER — "lag, never loss" (CONTEXT:262). A `drain` wakes `room`. Ordinary
    // streams have `retentive_cap == 0`, so this gate is a no-op for them.
    tokio::spawn(async move {
        match recv {
            RecvHalf::Quic(mut recv) => loop {
                await_ring_room(&log, &room).await;
                match recv.read_chunk(STREAM_READ_CHUNK).await {
                    Ok(Some(chunk)) => log.lock().unwrap().append(&chunk.bytes),
                    Ok(None) | Err(_) => {
                        log.lock().unwrap().finish();
                        break;
                    }
                }
            },
            RecvHalf::Loopback(mut recv) => {
                let mut buf = vec![0u8; STREAM_READ_CHUNK];
                loop {
                    await_ring_room(&log, &room).await;
                    match recv.read(&mut buf).await {
                        Ok(0) | Err(_) => {
                            log.lock().unwrap().finish();
                            break;
                        }
                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
                    }
                }
            }
        }
    });

    id
}

/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained
/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for
/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.
// [impl->REQ-SHELL-4]
async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {
    loop {
        {
            let l = log.lock().unwrap();
            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {
                return;
            }
        }
        room.notified().await;
    }
}

+    let room = Arc::new(tokio::sync::Notify::new());

+    // Backpressure (M11-W3, REQ-SHELL-4): a retentive (tunnel) log never evicts,

+    // so before pulling the next chunk the pump WAITS while the undrained ring is

+    // at its `retentive_cap`. The unread bytes stay in the transport's buffer, so

+    // QUIC stream flow-control (or the loopback duplex's bounded buffer) lags the

+    // SENDER — "lag, never loss" (CONTEXT:262). A `drain` wakes `room`. Ordinary

+    // streams have `retentive_cap == 0`, so this gate is a no-op for them.

+                await_ring_room(&log, &room).await;

+                    await_ring_room(&log, &room).await;

+/// Backpressure gate for a retentive (tunnel) read pump: park until the undrained

+/// ring has room below `retentive_cap`, woken by a `drain` on `room`. A no-op for

+/// an ordinary stream (`retentive_cap == 0`). Holds no lock across the await.

+async fn await_ring_room(log: &Arc<Mutex<StreamLog>>, room: &tokio::sync::Notify) {

+            let l = log.lock().unwrap();

+            if l.retentive_cap == 0 || l.ring.len() < l.retentive_cap {

/// The broker-owned net endpoint + live connection/stream tables (see module


Now add the NetHost methods (`mark_stream_retentive`, `drain_stream`, `open_loopback_pair`). The loopback peer-row discovery needs serialization (concurrent opens break id-arithmetic). Read the NetHost struct + constructor to add a pair-lock:


524	                    }
525	                }
526	            },
527	            RecvHalf::Loopback(mut recv) => {
528	                let mut buf = vec![0u8; STREAM_READ_CHUNK];
529	                loop {
530	                    await_ring_room(&log, &room).await;
531	                    match recv.read(&mut buf).await {
532	                        Ok(0) | Err(_) => {
533	                            log.lock().unwrap().finish();
534	                            break;
535	                        }
536	                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
537	                    }
538	                }
539	            }
540	        }
541	    });
542
543	    id

                    }
                }
            },
            RecvHalf::Loopback(mut recv) => {
                let mut buf = vec![0u8; STREAM_READ_CHUNK];
                loop {
                    await_ring_room(&log, &room).await;
                    match recv.read(&mut buf).await {
                        Ok(0) | Err(_) => {
                            log.lock().unwrap().finish();
                            break;
                        }
                        Ok(n) => log.lock().unwrap().append(&buf[..n]),
                    }
                }
            }
        }
    });

    id


560	}
561
562	/// The broker-owned net endpoint + live connection/stream tables (see module
563	/// docs).
564	pub struct NetHost {
565	    /// The dedicated tokio runtime hosting the endpoint and its accept loop.
566	    /// Dropping the host drops the runtime, stopping every net task with it.
567	    runtime: tokio::runtime::Runtime,
568	    endpoint: Arc<NetEndpoint>,
569	    shared: Arc<NetShared>,
570	    /// `op_id → conn_id` for exactly-once dial replies: a deduped replay of a
571	    /// journaled dial is answered with the connection the first apply opened.
572	    dial_ops: Mutex<HashMap<u64, u64>>,
573	    /// `op_id → stream_id` for exactly-once stream-open replies (D4b), same
574	    /// discipline as `dial_ops`. Shares the one net op-id namespace.
575	    stream_ops: Mutex<HashMap<u64, u64>>,
576	    /// This node's pubkey (the seed-proof's local channel-binding identity).
577	    local_id: PublicKey,
578	    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables
579	    /// the proof (see [`NetConfig::membership`]).
580	    membership: Option<MembershipSource>,
581	    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live
582	    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`
583	    /// so the many hermetic test configs need no change — a roster exchange is a
584	    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +
585	    /// cloned per connect on the net runtime, so the accept loop (spawned in
586	    /// `start`, before the host exists) shares it through this `Arc`.
587	    roster: Arc<Mutex<Option<RosterExchange>>>,
588	    /// The broker's single in-process loopback connection (M12 W1.5, B1),
589	    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
590	    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
591	    /// row lives for the host (no handshake to lose, no closed-watcher).
592	    loopback_conn: Mutex<Option<u64>>,
593	}
594
595	impl NetHost {
596	    /// Bind the broker's endpoint on a fresh dedicated runtime and start the
597	    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the
598	    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and
599	    /// the pre-trust pairing ALPN — routed to the always-on responder

}

/// The broker-owned net endpoint + live connection/stream tables (see module
/// docs).
pub struct NetHost {
    /// The dedicated tokio runtime hosting the endpoint and its accept loop.
    /// Dropping the host drops the runtime, stopping every net task with it.
    runtime: tokio::runtime::Runtime,
    endpoint: Arc<NetEndpoint>,
    shared: Arc<NetShared>,
    /// `op_id → conn_id` for exactly-once dial replies: a deduped replay of a
    /// journaled dial is answered with the connection the first apply opened.
    dial_ops: Mutex<HashMap<u64, u64>>,
    /// `op_id → stream_id` for exactly-once stream-open replies (D4b), same
    /// discipline as `dial_ops`. Shares the one net op-id namespace.
    stream_ops: Mutex<HashMap<u64, u64>>,
    /// This node's pubkey (the seed-proof's local channel-binding identity).
    local_id: PublicKey,
    /// The connect-time seed-proof credential source (Mesh-D2). `None` disables
    /// the proof (see [`NetConfig::membership`]).
    membership: Option<MembershipSource>,
    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live
    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`
    /// so the many hermetic test configs need no change — a roster exchange is a
    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +
    /// cloned per connect on the net runtime, so the accept loop (spawned in
    /// `start`, before the host exists) shares it through this `Arc`.
    roster: Arc<Mutex<Option<RosterExchange>>>,
    /// The broker's single in-process loopback connection (M12 W1.5, B1),
    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
    /// row lives for the host (no handshake to lose, no closed-watcher).
    loopback_conn: Mutex<Option<u64>>,
}

impl NetHost {
    /// Bind the broker's endpoint on a fresh dedicated runtime and start the
    /// accept loop. The endpoint speaks TWO ALPNs (M7 D3, REQ-SUBNET-2): the
    /// trusted [`SPT_NET_ALPN`] (connections join the table as before) and
    /// the pre-trust pairing ALPN — routed to the always-on responder


Bash

grep -n 'loopback_conn: Mutex::new(None)\|roster: Arc::new' crates/spt-daemon/src/nethost.rs | head

Find NetHost constructor field init


719:            loopback_conn: Mutex::new(None),

719:            loopback_conn: Mutex::new(None),


712	            endpoint,
713	            shared,
714	            dial_ops: Mutex::new(HashMap::new()),
715	            stream_ops: Mutex::new(HashMap::new()),
716	            local_id: local_pub,
717	            membership: cfg.membership,
718	            roster,
719	            loopback_conn: Mutex::new(None),
720	        })
721	    }
722
723	    /// Wire the on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2) onto this

            endpoint,
            shared,
            dial_ops: Mutex::new(HashMap::new()),
            stream_ops: Mutex::new(HashMap::new()),
            local_id: local_pub,
            membership: cfg.membership,
            roster,
            loopback_conn: Mutex::new(None),
        })
    }

    /// Wire the on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2) onto this


Edit

    /// The broker's single in-process loopback connection (M12 W1.5, B1),
    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
    /// row lives for the host (no handshake to lose, no closed-watcher).
    loopback_conn: Mutex<Option<u64>>,
}

    /// The broker's single in-process loopback connection (M12 W1.5, B1),
    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
    /// row lives for the host (no handshake to lose, no closed-watcher).
    loopback_conn: Mutex<Option<u64>>,
    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery
    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the
    /// one row this open minted — concurrent pair-opens on the shared loopback conn
    /// would otherwise race the diff.
    loopback_pair_lock: Mutex<()>,
}


    /// The broker's single in-process loopback connection (M12 W1.5, B1),
    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
    /// row lives for the host (no handshake to lose, no closed-watcher).
    loopback_conn: Mutex<Option<u64>>,
}

    /// The broker's single in-process loopback connection (M12 W1.5, B1),
    /// minted lazily on the first [`Self::dial_loopback`] and reused thereafter
    /// — a local `spt rc` needs exactly one. `Some(conn_id)` once minted; the
    /// row lives for the host (no handshake to lose, no closed-watcher).
    loopback_conn: Mutex<Option<u64>>,
    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery
    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the
    /// one row this open minted — concurrent pair-opens on the shared loopback conn
    /// would otherwise race the diff.
    loopback_pair_lock: Mutex<()>,
}

+    /// Serializes [`Self::open_loopback_pair`] (M11-W3) so the peer-row discovery

+    /// (a before/after diff on the loopback conn's inbound rows) sees exactly the

+    /// one row this open minted — concurrent pair-opens on the shared loopback conn


            roster,
            loopback_conn: Mutex::new(None),
        })
    }

            roster,
            loopback_conn: Mutex::new(None),
            loopback_pair_lock: Mutex::new(()),
        })
    }


            roster,
            loopback_conn: Mutex::new(None),
        })
    }

            roster,
            loopback_conn: Mutex::new(None),
            loopback_pair_lock: Mutex::new(()),
        })
    }

+            loopback_pair_lock: Mutex::new(()),


Now add the three NetHost methods after `stream_infos`. Find its end:


1030	            Ok(())
1031	        })
1032	    }
1033
1034	    /// Every held stream's info — the query a re-attaching brain uses to find
1035	    /// streams (including peer-initiated ones that arrived while it was down).
1036	    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {
1037	        let streams = self.shared.streams.lock().unwrap();
1038	        streams
1039	            .iter()
1040	            .map(|(id, e)| {
1041	                let log = e.log.lock().unwrap();
1042	                NetStreamInfo {
1043	                    stream_id: *id,
1044	                    conn_id: e.conn_id,
1045	                    remote_id_hex: e.remote_id_hex.clone(),
1046	                    next_seq: log.next_seq,
1047	                    finished: log.finished,
1048	                    initiated_locally: e.initiated_locally,
1049	                }
1050	            })
1051	            .collect()
1052	    }
1053
1054	    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`
1055	    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).

            Ok(())
        })
    }

    /// Every held stream's info — the query a re-attaching brain uses to find
    /// streams (including peer-initiated ones that arrived while it was down).
    pub fn stream_infos(&self) -> Vec<NetStreamInfo> {
        let streams = self.shared.streams.lock().unwrap();
        streams
            .iter()
            .map(|(id, e)| {
                let log = e.log.lock().unwrap();
                NetStreamInfo {
                    stream_id: *id,
                    conn_id: e.conn_id,
                    remote_id_hex: e.remote_id_hex.clone(),
                    next_seq: log.next_seq,
                    finished: log.finished,
                    initiated_locally: e.initiated_locally,
                }
            })
            .collect()
    }

    /// Attach `sub` as a stream's read subscriber, replaying from `from_seq`
    /// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).


                    initiated_locally: e.initiated_locally,
                }
            })
            .collect()
    }

                    initiated_locally: e.initiated_locally,
                }
            })
            .collect()
    }

    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,
    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump
    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).
    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.
    /// No-op if the stream is gone.
    // [impl->REQ-SHELL-4]
    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {
        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {
            e.log.lock().unwrap().set_retentive(cap);
        }
    }

    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):
    /// pops the whole ring in order, concatenated, and wakes the read pump (which
    /// may have been lagging the sender at the backpressure cap). Returns
    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).
    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.
    // [impl->REQ-SHELL-4]
    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {
        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);
        let drained = entry.log.lock().unwrap().drain();
        // The drain freed ring room — wake a pump that was backpressure-parked.
        entry.room.notify_one();
        Some(drained)
    }

    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
    /// minted.
    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .iter()
            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)
            .map(|(id, _)| *id)
            .collect()
    }

    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and
    /// return `(owner_stream, shell_stream)` — the operator row the owner side
    /// drives and the cross-wired peer row the shell side drives. The loopback
    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a
    /// write to either send appears on the other's read log). Serialized so the
    /// before/after diff sees exactly the one peer row this open minted.
    // [impl->REQ-SHELL-4]
    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {
        let _guard = self.loopback_pair_lock.lock().unwrap();
        let (conn_id, _) = self.dial_loopback();
        let before = self.inbound_rows_on(conn_id);
        let owner_stream = self.open_stream(conn_id)?;
        let shell_stream = self
            .inbound_rows_on(conn_id)
            .difference(&before)
            .copied()
            .next()
            .ok_or_else(|| io::Error::other("loopback peer row not registered"))?;
        Ok((owner_stream, shell_stream))
    }


                    initiated_locally: e.initiated_locally,
                }
            })
            .collect()
    }

    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,
    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump
    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).
    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.
    /// No-op if the stream is gone.
    // [impl->REQ-SHELL-4]
    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {
        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {
            e.log.lock().unwrap().set_retentive(cap);
        }
    }

    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):
    /// pops the whole ring in order, concatenated, and wakes the read pump (which
    /// may have been lagging the sender at the backpressure cap). Returns
    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).
    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.
    // [impl->REQ-SHELL-4]
    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {
        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);
        let drained = entry.log.lock().unwrap().drain();
        // The drain freed ring room — wake a pump that was backpressure-parked.
        entry.room.notify_one();
        Some(drained)
    }

    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
    /// minted.
    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .iter()
            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)
            .map(|(id, _)| *id)
            .collect()
    }

    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and
    /// return `(owner_stream, shell_stream)` — the operator row the owner side
    /// drives and the cross-wired peer row the shell side drives. The loopback
    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a
    /// write to either send appears on the other's read log). Serialized so the
    /// before/after diff sees exactly the one peer row this open minted.
    // [impl->REQ-SHELL-4]
    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {
        let _guard = self.loopback_pair_lock.lock().unwrap();
        let (conn_id, _) = self.dial_loopback();
        let before = self.inbound_rows_on(conn_id);
        let owner_stream = self.open_stream(conn_id)?;
        let shell_stream = self
            .inbound_rows_on(conn_id)
            .difference(&before)
            .copied()
            .next()
            .ok_or_else(|| io::Error::other("loopback peer row not registered"))?;
        Ok((owner_stream, shell_stream))
    }

+    /// Mark a held stream **retentive** with backpressure `cap` chunks (M11-W3,

+    /// REQ-SHELL-4): its read log stops evicting (no byte loss) and its read pump

+    /// lags the sender once the undrained ring reaches `cap` (no unbounded growth).

+    /// Used for shell-tunnel streams; ordinary streams stay bounded-overwrite.

+    pub fn mark_stream_retentive(&self, stream_id: u64, cap: usize) {

+        if let Some(e) = self.shared.streams.lock().unwrap().get(&stream_id) {

+            e.log.lock().unwrap().set_retentive(cap);

+    /// Drain a retentive stream's undrained bytes (the shell-tunnel recv cursor):

+    /// pops the whole ring in order, concatenated, and wakes the read pump (which

+    /// may have been lagging the sender at the backpressure cap). Returns

+    /// `(bytes, finished)`; `finished` ⇒ the read side ended (the poller closes).

+    /// `None` ⇒ no such stream. Empty bytes + `finished == false` ⇒ nothing pending.

+    pub fn drain_stream(&self, stream_id: u64) -> Option<(Vec<u8>, bool)> {

+        let entry = Arc::clone(self.shared.streams.lock().unwrap().get(&stream_id)?);

+        let drained = entry.log.lock().unwrap().drain();

+        // The drain freed ring room — wake a pump that was backpressure-parked.

+    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the

+    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just

+    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {

+            .filter(|(_, e)| e.conn_id == conn_id && !e.initiated_locally)

+    /// Open a same-node loopback tunnel stream PAIR (M11-W3, REQ-SHELL-4) and

+    /// return `(owner_stream, shell_stream)` — the operator row the owner side

+    /// drives and the cross-wired peer row the shell side drives. The loopback

+    /// `open_stream` mints both rows in-process (one `duplex` wires them, so a

+    /// write to either send appears on the other's read log). Serialized so the

+    /// before/after diff sees exactly the one peer row this open minted.

+    pub fn open_loopback_pair(&self) -> io::Result<(u64, u64)> {

+        let _guard = self.loopback_pair_lock.lock().unwrap();

+        let (conn_id, _) = self.dial_loopback();

+        let before = self.inbound_rows_on(conn_id);

+        let owner_stream = self.open_stream(conn_id)?;

+            .ok_or_else(|| io::Error::other("loopback peer row not registered"))?;

+        Ok((owner_stream, shell_stream))

/// Attach `sub` as a stream's read subscriber, replaying from `from_seq`

/// (gapless resume — the PTY `subscribe` discipline on a QUIC stream).


Build spt-daemon to verify the NetHost retentive/backpressure/drain/pair changes compile:


Bash

cargo build -q -p spt-daemon 2>&1 | tail -20; echo "BUILD=$?"

Build spt-daemon with NetHost changes


Builds clean. Now tests. StreamLog policy (lossless-retain vs bounded-overwrite) is pure-sync unit-testable; the pump backpressure needs the loopback. Add a `ring_len` test accessor + look at the existing nethost test harness:


1230	        );
1231	    }
1232
1233	    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`
1234	    // mints a REUSED singleton whose origin is this node's OWN id, and
1235	    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a
1236	    // peer row (the one the dispatcher serves) — with the operator's writes
1237	    // crossing to the peer row's read pump. No iroh handshake, no QUIC.
1238	    #[test]
1239	    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {
1240	        let id = Identity::generate();
1241	        let host = NetHost::start(hermetic(&id)).expect("host start");
1242
1243	        // Singleton: reused, own-node origin (never a foreign/handshake id).
1244	        let (c1, hex1) = host.dial_loopback();
1245	        let (c2, hex2) = host.dial_loopback();
1246	        assert_eq!(c1, c2, "the loopback conn is a reused singleton");
1247	        assert_eq!(
1248	            hex1,
1249	            id.public_key().to_hex(),
1250	            "loopback origin is THIS node's own id (broker-minted)"
1251	        );
1252	        assert_eq!(hex2, hex1);
1253	        assert_eq!(host.conn_count(), 1, "one loopback conn held");
1254
1255	        // open_stream mints the cross-wired pair: operator row + peer row.
1256	        let op_stream = host.open_stream(c1).expect("open loopback stream");
1257	        let infos = host.stream_infos();
1258	        assert_eq!(infos.len(), 2, "operator + peer rows");
1259	        let op = infos
1260	            .iter()
1261	            .find(|s| s.stream_id == op_stream)
1262	            .expect("operator row present");
1263	        assert!(op.initiated_locally, "the returned id is the operator row");
1264	        let peer = infos
1265	            .iter()
1266	            .find(|s| !s.initiated_locally)
1267	            .expect("peer row present");
1268	        assert_eq!(peer.conn_id, c1);
1269	        assert_eq!(
1270	            peer.remote_id_hex, hex1,
1271	            "the peer row carries the own-node origin (gates same-node)"
1272	        );
1273
1274	        // Cross-wire: a write on the operator side reaches the peer row's read
1275	        // pump (its log advances) — entirely in-process, no QUIC.
1276	        host.send_stream(op_stream, b"ping", false)
1277	            .expect("write operator side");
1278	        let peer_id = peer.stream_id;
1279	        let mut crossed = false;
1280	        for _ in 0..400 {
1281	            if host
1282	                .stream_infos()
1283	                .iter()
1284	                .any(|s| s.stream_id == peer_id && s.next_seq > 0)
1285	            {
1286	                crossed = true;
1287	                break;
1288	            }
1289	            std::thread::sleep(std::time::Duration::from_millis(5));

        );
    }

    // [unit->REQ-RC-1] the broker loopback conn (M12 W1.5, B1): `dial_loopback`
    // mints a REUSED singleton whose origin is this node's OWN id, and
    // `open_stream` on it cross-wires a PAIR in-process — an operator row and a
    // peer row (the one the dispatcher serves) — with the operator's writes
    // crossing to the peer row's read pump. No iroh handshake, no QUIC.
    #[test]
    fn loopback_conn_is_a_reused_singleton_that_cross_wires_a_stream_pair() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");

        // Singleton: reused, own-node origin (never a foreign/handshake id).
        let (c1, hex1) = host.dial_loopback();
        let (c2, hex2) = host.dial_loopback();
        assert_eq!(c1, c2, "the loopback conn is a reused singleton");
        assert_eq!(
            hex1,
            id.public_key().to_hex(),
            "loopback origin is THIS node's own id (broker-minted)"
        );
        assert_eq!(hex2, hex1);
        assert_eq!(host.conn_count(), 1, "one loopback conn held");

        // open_stream mints the cross-wired pair: operator row + peer row.
        let op_stream = host.open_stream(c1).expect("open loopback stream");
        let infos = host.stream_infos();
        assert_eq!(infos.len(), 2, "operator + peer rows");
        let op = infos
            .iter()
            .find(|s| s.stream_id == op_stream)
            .expect("operator row present");
        assert!(op.initiated_locally, "the returned id is the operator row");
        let peer = infos
            .iter()
            .find(|s| !s.initiated_locally)
            .expect("peer row present");
        assert_eq!(peer.conn_id, c1);
        assert_eq!(
            peer.remote_id_hex, hex1,
            "the peer row carries the own-node origin (gates same-node)"
        );

        // Cross-wire: a write on the operator side reaches the peer row's read
        // pump (its log advances) — entirely in-process, no QUIC.
        host.send_stream(op_stream, b"ping", false)
            .expect("write operator side");
        let peer_id = peer.stream_id;
        let mut crossed = false;
        for _ in 0..400 {
            if host
                .stream_infos()
                .iter()
                .any(|s| s.stream_id == peer_id && s.next_seq > 0)
            {
                crossed = true;
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));


1290	        }
1291	        assert!(
1292	            crossed,
1293	            "the operator-side write crossed in-process to the peer row's read pump"
1294	        );
1295	    }
1296	}
1297

        }
        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }
}


grep -n 'fn hermetic\|mod tests\|use super' crates/spt-daemon/src/nethost.rs | head

Find hermetic helper + test mod start


1175:mod tests {
1176:    use super::*;
1178:    fn hermetic(identity: &Identity) -> NetConfig {


1175	mod tests {
1176	    use super::*;
1177
1178	    fn hermetic(identity: &Identity) -> NetConfig {
1179	        NetConfig {
1180	            identity: Identity::from_seed(&identity.seed()),
1181	            relay: RelayPolicy::Disabled,
1182	            discovery: LocalDiscovery::Off,
1183	            scope: BindScope::Loopback,
1184	            membership: None,
1185	        }
1186	    }
1187
1188	    // [unit->REQ-DAEMON-2] the endpoint is constructed broker-side (inside the
1189	    // host's dedicated runtime) with relays disabled, bound to the node's own
1190	    // identity, with an empty conn table.
1191	    #[test]
1192	    fn host_binds_endpoint_to_node_identity() {
1193	        let id = Identity::generate();
1194	        let host = NetHost::start(hermetic(&id)).expect("host start");
1195	        assert_eq!(host.node_id_hex(), id.public_key().to_hex());
1196	        assert_eq!(host.conn_count(), 0);
1197	        assert!(!host.addr_json().is_null(), "dialable address serializes");
1198	    }
1199

mod tests {
    use super::*;

    fn hermetic(identity: &Identity) -> NetConfig {
        NetConfig {
            identity: Identity::from_seed(&identity.seed()),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
            membership: None,
        }
    }

    // [unit->REQ-DAEMON-2] the endpoint is constructed broker-side (inside the
    // host's dedicated runtime) with relays disabled, bound to the node's own
    // identity, with an empty conn table.
    #[test]
    fn host_binds_endpoint_to_node_identity() {
        let id = Identity::generate();
        let host = NetHost::start(hermetic(&id)).expect("host start");
        assert_eq!(host.node_id_hex(), id.public_key().to_hex());
        assert_eq!(host.conn_count(), 0);
        assert!(!host.addr_json().is_null(), "dialable address serializes");
    }


Add a `ring_len` test accessor. First check the NetHost runtime type — backpressure + `block_on` send could deadlock a current-thread runtime:


grep -n 'Runtime::new\|new_multi_thread\|new_current_thread\|runtime::Builder\|enable_all\|worker_threads' crates/spt-daemon/src/nethost.rs | head

Check NetHost runtime flavor


618:        let runtime = tokio::runtime::Builder::new_multi_thread()
619:            .worker_threads(2)
621:            .enable_all()


Multi-thread runtime (2 workers) — `send_stream` block_on won't starve the pump, backpressure is sound. Add `ring_len` accessor:


Edit

    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
    /// minted.
    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {

    /// A held stream's current undrained ring depth in chunks (test introspection
    /// for the retentive backpressure bound). `None` ⇒ no such stream.
    #[cfg(test)]
    fn ring_len(&self, stream_id: u64) -> Option<usize> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .get(&stream_id)
            .map(|e| e.log.lock().unwrap().ring.len())
    }

    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
    /// minted.
    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {


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)

    /// A held stream's current undrained ring depth in chunks (test introspection
    /// for the retentive backpressure bound). `None` ⇒ no such stream.
    #[cfg(test)]
    fn ring_len(&self, stream_id: u64) -> Option<usize> {
        self.shared
            .streams
            .lock()
            .unwrap()
            .get(&stream_id)
            .map(|e| e.log.lock().unwrap().ring.len())
    }

    /// Inbound (peer-initiated) stream rows currently held on `conn_id` — the
    /// candidate set [`Self::open_loopback_pair`] diffs to find the peer it just
    /// minted.
    fn inbound_rows_on(&self, conn_id: u64) -> HashSet<u64> {

+    /// A held stream's current undrained ring depth in chunks (test introspection

+    /// for the retentive backpressure bound). `None` ⇒ no such stream.

+    fn ring_len(&self, stream_id: u64) -> Option<usize> {

+            .map(|e| e.log.lock().unwrap().ring.len())


Now add the tests (StreamLog policy unit tests + loopback backpressure/lossless integration test):


        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }
}

        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }

    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell
    // tunnel) never evicts — `drain` returns every appended byte in order even past
    // the would-be cap (lossless, CONTEXT:262 "cannot drop frames"); an ordinary
    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the
    // broker), evicting the oldest chunks past `cap_chunks`.
    #[test]
    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {
        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns
        // all 8 concatenated in order.
        let mut ret = StreamLog::new(1, 1);
        ret.set_retentive(3);
        for i in 0..8u8 {
            ret.append(&[i]);
        }
        let (bytes, finished) = ret.drain();
        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), "retentive loses nothing, in order");
        assert!(!finished);
        // …and a second drain is empty (the cursor consumed them exactly once).
        assert_eq!(ret.drain().0, Vec::<u8>::new());

        // Ordinary: cap 3, append 8 — the oldest evict, only the last 3 survive
        // (the bounded-overwrite discipline ordinary streams keep).
        let mut ord = StreamLog::new(2, 3);
        for i in 0..8u8 {
            ord.append(&[i]);
        }
        assert_eq!(ord.drain().0, vec![5, 6, 7], "ordinary keeps only the last cap chunks");
    }

    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,
    // doyle gate condition): a retentive shell-side stream that is not drained while
    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the
    // read pump lags the sender at the cap. After the reader catches up, every byte
    // arrives byte-exact and in order. Proves "lag, never loss".
    #[test]
    fn loopback_tunnel_backpressures_without_loss() {
        use std::sync::atomic::{AtomicUsize, Ordering as AtOrd};
        let id = Identity::generate();
        let host = Arc::new(NetHost::start(hermetic(&id)).expect("host start"));
        let (owner, shell) = host.open_loopback_pair().expect("open tunnel pair");
        const CAP: usize = 4;
        host.mark_stream_retentive(owner, CAP);
        host.mark_stream_retentive(shell, CAP);

        // Distinct, read-chunk-sized frames so each lands as its own ring entry
        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.
        const N: usize = 20;
        let frames: Vec<Vec<u8>> = (0..N).map(|i| vec![i as u8; STREAM_READ_CHUNK]).collect();
        let sent = Arc::new(AtomicUsize::new(0));

        let writer = {
            let host = Arc::clone(&host);
            let frames = frames.clone();
            let sent = Arc::clone(&sent);
            std::thread::spawn(move || {
                for f in &frames {
                    host.send_stream(owner, f, false).expect("send");
                    sent.fetch_add(1, AtOrd::Relaxed);
                }
                host.send_stream(owner, &[], true).expect("finish");
            })
        };

        // Let the pump fill the shell ring to the cap and PARK (backpressure).
        let mut peak = 0usize;
        for _ in 0..200 {
            if let Some(d) = host.ring_len(shell) {
                peak = peak.max(d);
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
            if sent.load(AtOrd::Relaxed) >= N {
                break; // (shouldn't reach N while we never drain — backpressure)
            }
        }
        assert!(peak > 0, "the pump delivered some frames to the shell ring");
        assert!(
            host.ring_len(shell).unwrap() <= CAP,
            "the retentive ring never exceeds the backpressure cap (bounded)"
        );
        assert!(
            sent.load(AtOrd::Relaxed) < N,
            "the un-drained consumer lags the sender (backpressure engaged, not all sent)"
        );

        // Now drain to completion — the sender unblocks, every byte arrives in order.
        let mut got: Vec<u8> = Vec::new();
        let mut finished = false;
        for _ in 0..2000 {
            if let Some((mut bytes, fin)) = host.drain_stream(shell) {
                got.append(&mut bytes);
                if fin {
                    finished = true;
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(2));
        }
        writer.join().expect("writer joins");
        assert!(finished, "the tunnel saw EOF after the owner finished");
        let expected: Vec<u8> = frames.concat();
        assert_eq!(got.len(), expected.len(), "no byte lost or duplicated");
        assert_eq!(got, expected, "byte-exact, in order, under backpressure");
    }
}


        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }
}

        assert!(
            crossed,
            "the operator-side write crossed in-process to the peer row's read pump"
        );
    }

    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell
    // tunnel) never evicts — `drain` returns every appended byte in order even past
    // the would-be cap (lossless, CONTEXT:262 "cannot drop frames"); an ordinary
    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the
    // broker), evicting the oldest chunks past `cap_chunks`.
    #[test]
    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {
        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns
        // all 8 concatenated in order.
        let mut ret = StreamLog::new(1, 1);
        ret.set_retentive(3);
        for i in 0..8u8 {
            ret.append(&[i]);
        }
        let (bytes, finished) = ret.drain();
        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), "retentive loses nothing, in order");
        assert!(!finished);
        // …and a second drain is empty (the cursor consumed them exactly once).
        assert_eq!(ret.drain().0, Vec::<u8>::new());

        // Ordinary: cap 3, append 8 — the oldest evict, only the last 3 survive
        // (the bounded-overwrite discipline ordinary streams keep).
        let mut ord = StreamLog::new(2, 3);
        for i in 0..8u8 {
            ord.append(&[i]);
        }
        assert_eq!(ord.drain().0, vec![5, 6, 7], "ordinary keeps only the last cap chunks");
    }

    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,
    // doyle gate condition): a retentive shell-side stream that is not drained while
    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the
    // read pump lags the sender at the cap. After the reader catches up, every byte
    // arrives byte-exact and in order. Proves "lag, never loss".
    #[test]
    fn loopback_tunnel_backpressures_without_loss() {
        use std::sync::atomic::{AtomicUsize, Ordering as AtOrd};
        let id = Identity::generate();
        let host = Arc::new(NetHost::start(hermetic(&id)).expect("host start"));
        let (owner, shell) = host.open_loopback_pair().expect("open tunnel pair");
        const CAP: usize = 4;
        host.mark_stream_retentive(owner, CAP);
        host.mark_stream_retentive(shell, CAP);

        // Distinct, read-chunk-sized frames so each lands as its own ring entry
        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.
        const N: usize = 20;
        let frames: Vec<Vec<u8>> = (0..N).map(|i| vec![i as u8; STREAM_READ_CHUNK]).collect();
        let sent = Arc::new(AtomicUsize::new(0));

        let writer = {
            let host = Arc::clone(&host);
            let frames = frames.clone();
            let sent = Arc::clone(&sent);
            std::thread::spawn(move || {
                for f in &frames {
                    host.send_stream(owner, f, false).expect("send");
                    sent.fetch_add(1, AtOrd::Relaxed);
                }
                host.send_stream(owner, &[], true).expect("finish");
            })
        };

        // Let the pump fill the shell ring to the cap and PARK (backpressure).
        let mut peak = 0usize;
        for _ in 0..200 {
            if let Some(d) = host.ring_len(shell) {
                peak = peak.max(d);
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
            if sent.load(AtOrd::Relaxed) >= N {
                break; // (shouldn't reach N while we never drain — backpressure)
            }
        }
        assert!(peak > 0, "the pump delivered some frames to the shell ring");
        assert!(
            host.ring_len(shell).unwrap() <= CAP,
            "the retentive ring never exceeds the backpressure cap (bounded)"
        );
        assert!(
            sent.load(AtOrd::Relaxed) < N,
            "the un-drained consumer lags the sender (backpressure engaged, not all sent)"
        );

        // Now drain to completion — the sender unblocks, every byte arrives in order.
        let mut got: Vec<u8> = Vec::new();
        let mut finished = false;
        for _ in 0..2000 {
            if let Some((mut bytes, fin)) = host.drain_stream(shell) {
                got.append(&mut bytes);
                if fin {
                    finished = true;
                    break;
                }
            }
            std::thread::sleep(std::time::Duration::from_millis(2));
        }
        writer.join().expect("writer joins");
        assert!(finished, "the tunnel saw EOF after the owner finished");
        let expected: Vec<u8> = frames.concat();
        assert_eq!(got.len(), expected.len(), "no byte lost or duplicated");
        assert_eq!(got, expected, "byte-exact, in order, under backpressure");
    }
}

+    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell

+    // tunnel) never evicts — `drain` returns every appended byte in order even past

+    // the would-be cap (lossless, CONTEXT:262 "cannot drop frames"); an ordinary

+    // (non-retentive) log keeps bounded-overwrite (a slow brain must not OOM the

+    // broker), evicting the oldest chunks past `cap_chunks`.

+    fn retentive_log_is_lossless_ordered_while_ordinary_log_overwrites() {

+        // Retentive: cap 3, append 8 distinct chunks, NONE evicted — drain returns

+        // all 8 concatenated in order.

+        let mut ret = StreamLog::new(1, 1);

+        let (bytes, finished) = ret.drain();

+        assert_eq!(bytes, (0..8u8).collect::<Vec<_>>(), "retentive loses nothing, in order");

+        // …and a second drain is empty (the cursor consumed them exactly once).

+        assert_eq!(ret.drain().0, Vec::<u8>::new());

+        // Ordinary: cap 3, append 8 — the oldest evict, only the last 3 survive

+        // (the bounded-overwrite discipline ordinary streams keep).

+        let mut ord = StreamLog::new(2, 3);

+        assert_eq!(ord.drain().0, vec![5, 6, 7], "ordinary keeps only the last cap chunks");

+    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,

+    // doyle gate condition): a retentive shell-side stream that is not drained while

+    // the owner keeps writing must NOT lose bytes AND must NOT grow unbounded — the

+    // read pump lags the sender at the cap. After the reader catches up, every byte

+    // arrives byte-exact and in order. Proves "lag, never loss".

+    fn loopback_tunnel_backpressures_without_loss() {

+        use std::sync::atomic::{AtomicUsize, Ordering as AtOrd};

+        let host = Arc::new(NetHost::start(hermetic(&id)).expect("host start"));

+        let (owner, shell) = host.open_loopback_pair().expect("open tunnel pair");

+        host.mark_stream_retentive(owner, CAP);

+        host.mark_stream_retentive(shell, CAP);

+        // Distinct, read-chunk-sized frames so each lands as its own ring entry

+        // (no coalescing) — lets us observe the cap exactly. 20 frames >> CAP.

+        let frames: Vec<Vec<u8>> = (0..N).map(|i| vec![i as u8; STREAM_READ_CHUNK]).collect();

+        let sent = Arc::new(AtomicUsize::new(0));

+            let host = Arc::clone(&host);

+            let frames = frames.clone();

+            let sent = Arc::clone(&sent);

+            std::thread::spawn(move || {

+                    host.send_stream(owner, f, false).expect("send");

+                    sent.fetch_add(1, AtOrd::Relaxed);

+                host.send_stream(owner, &[], true).expect("finish");

+        // Let the pump fill the shell ring to the cap and PARK (backpressure).

+            if let Some(d) = host.ring_len(shell) {

+            std::thread::sleep(std::time::Duration::from_millis(5));

+            if sent.load(AtOrd::Relaxed) >= N {

+                break; // (shouldn't reach N while we never drain — backpressure)

+        assert!(peak > 0, "the pump delivered some frames to the shell ring");

+            host.ring_len(shell).unwrap() <= CAP,

+            "the retentive ring never exceeds the backpressure cap (bounded)"

+            sent.load(AtOrd::Relaxed) < N,

+            "the un-drained consumer lags the sender (backpressure engaged, not all sent)"

+        // Now drain to completion — the sender unblocks, every byte arrives in order.

+        let mut got: Vec<u8> = Vec::new();

+            if let Some((mut bytes, fin)) = host.drain_stream(shell) {

+                got.append(&mut bytes);

+            std::thread::sleep(std::time::Duration::from_millis(2));

+        writer.join().expect("writer joins");

+        assert!(finished, "the tunnel saw EOF after the owner finished");

+        let expected: Vec<u8> = frames.concat();

+        assert_eq!(got.len(), expected.len(), "no byte lost or duplicated");

+        assert_eq!(got, expected, "byte-exact, in order, under backpressure");


Bash

cargo test -q -p spt-daemon --lib 'nethost::tests::retentive_log_is_lossless_ordered_while_ordinary_log_overwrites' 'nethost::tests::loopback_tunnel_backpressures_without_loss' 2>&1 | tail -20

Run new retentive + backpressure tests


error: unexpected argument 'nethost::tests::loopback_tunnel_backpressures_without_loss' found

Usage: cargo.exe test [OPTIONS] [TESTNAME] [-- [ARGS]...]

For more information, try '--help'.


cargo test -q -p spt-daemon --lib nethost::tests:: 2>&1 | tail -20

Run all nethost lib tests


running 5 tests
.....
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 304 filtered out; finished in 1.12s


5/5 nethost tests pass incl. the backpressure test. It's timing-based — verify non-flaky with repeated runs:


for i in 1 2 3; do cargo test -q -p spt-daemon --lib nethost::tests::loopback_tunnel_backpressures_without_loss 2>&1 | grep 'test result'; done

Repeat backpressure test for flakiness


test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 308 filtered out; finished in 1.11s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 308 filtered out; finished in 1.12s
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 308 filtered out; finished in 1.11s


Stable 3/3. The retentive/backpressure substrate is solid. Pre-flight + commit T3.2c-net (the gate-blocking lossless leg):


cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -6; echo "---trace---"; traceable-reqs check >/dev/null 2>&1; echo "TRACEABLE=$?"

Clippy workspace + traceable


    Checking spt-daemon v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.7.1 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.14s
---trace---
TRACEABLE=0


git add -A && git commit -q -m "$(cat <<'EOF'
M11-W3 T3.2c: retentive backpressured tunnel streams on NetHost (REQ-SHELL-4)

The gate-blocking losslessness leg (doyle condition): a shell tunnel must
not drop bytes (CONTEXT:262 "tunneled protocols cannot drop frames, so
congestion surfaces as lag, never loss"). Add a per-stream retentive mode:

- StreamLog gains `retentive_cap` (chunks; 0 = ordinary). A retentive log
  never cap-evicts on append (no loss); the read pump WAITS before pulling
  the next chunk once the undrained ring hits the cap, so the transport's
  native flow-control (QUIC stream / loopback duplex buffer) lags the
  SENDER — "lag, never loss", bounded (no OOM). Ordinary streams keep
  bounded-overwrite untouched (a slow brain must not OOM the broker).
- `drain()` pops the whole ring in order (the recv cursor; exactly-once),
  waking the backpressure-parked pump via a per-stream `room` notifier.
- NetHost: mark_stream_retentive / drain_stream / open_loopback_pair
  (mints the cross-wired same-node pair, serialized peer-row discovery).

Tests: retentive is lossless+ordered past the cap while ordinary
overwrites; a loopback tunnel under a non-draining consumer bounds the
ring at the cap (backpressure engaged, sender lags) yet delivers every
byte byte-exact in order once drained. 5/5 nethost tests green.

Co-authored by: todlando
EOF
)" && git log --oneline -1

Commit T3.2c retentive substrate


7bf91bd M11-W3 T3.2c: retentive backpressured tunnel streams on NetHost (REQ-SHELL-4)


T3.2c committed @7bf91bd — gate-blocking losslessness leg done + tested. Now the bridge wiring: control-channel byte ops (ensure/send/recv) + open-hook + the two surfaces. Read the W2 drive CLI + api patterns to mirror exactly:


640	        #[arg(trailing_var_arg = true)]
641	        op: Vec<String>,
642	        #[arg(long)]
643	        owner: Option<String>,
644	    },
645	    /// Drive the shell with a typed, EPHEMERAL control payload: the owner→shell
646	    /// mirror of sensory. The drive-type is vocabulary-checked against
647	    /// `[shell.drive]`, held in a single latest-wins in-memory slot on the
648	    /// daemon, and drained by the shell's `api drive-poll --link`. NEVER spooled
649	    /// — an offline shell drops the payload with a diagnostic (control is
650	    /// live-or-drop, never replayed).
651	    Drive {
652	        /// Canonical id or alias.
653	        shell_ref: String,
654	        /// The drive payload type (vocabulary-checked against `[shell.drive]`).
655	        #[arg(long = "type")]
656	        drive_type: String,
657	        /// The opaque control payload (descriptive text / encoded blob reference).
658	        payload: String,
659	        #[arg(long)]
660	        owner: Option<String>,
661	    },
662	    /// Send a text and/or file payload down the durable 2-way text+file
663	    /// channel (agent→shell; the shell answers via ordinary `spt send`).
664	    /// File transfers are progress-queryable by xfer id.
665	    Send {
666	        /// Canonical id or alias.
667	        shell_ref: String,
668	        /// The text payload.
669	        text: Option<String>,

        #[arg(trailing_var_arg = true)]
        op: Vec<String>,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Drive the shell with a typed, EPHEMERAL control payload: the owner→shell
    /// mirror of sensory. The drive-type is vocabulary-checked against
    /// `[shell.drive]`, held in a single latest-wins in-memory slot on the
    /// daemon, and drained by the shell's `api drive-poll --link`. NEVER spooled
    /// — an offline shell drops the payload with a diagnostic (control is
    /// live-or-drop, never replayed).
    Drive {
        /// Canonical id or alias.
        shell_ref: String,
        /// The drive payload type (vocabulary-checked against `[shell.drive]`).
        #[arg(long = "type")]
        drive_type: String,
        /// The opaque control payload (descriptive text / encoded blob reference).
        payload: String,
        #[arg(long)]
        owner: Option<String>,
    },
    /// Send a text and/or file payload down the durable 2-way text+file
    /// channel (agent→shell; the shell answers via ordinary `spt send`).
    /// File transfers are progress-queryable by xfer id.
    Send {
        /// Canonical id or alias.
        shell_ref: String,
        /// The text payload.
        text: Option<String>,


5620	                return 1;
5621	            };
5622	            // The cross-node arm (M5-D8c): `<ref>@<node>` ships the verb to
5623	            // the shell's node — commands ride Iroh, the action runs there.
5624	            // [impl->REQ-SHELL-2]
5625	            if let Some((r, node)) = shell_ref.split_once('@') {
5626	                return shell_link_remote(
5627	                    &owner,
5628	                    node,
5629	                    r,
5630	                    spt_net::net::shelllink::SHELL_LINK_CMD,
5631	                    &op,
5632	                );
5633	            }
5634	            let Some((op_name, args)) = op.split_first() else {
5635	                eprintln!("USAGE: shell cmd <ref> <op> [args…]");
5636	                return 2;
5637	            };
5638	            match spt_daemon::drive_shell(&owlery, &owner, &shell_ref, op_name, args) {
5639	                Ok((id, shell)) => {
5640	                    drain_per_receipt(&owlery, &owner, &id, &shell, &format!("op={op_name}"))
5641	                }
5642	                // Per-capability act-gate (M11, REQ-CONSENT-3): the gated op did
5643	                // NOT spool — route the approval prompt (the same plumbing the
5644	                // spawn gate uses) before the command may land.
5645	                Err(spt_daemon::ShellLinkError::NeedsApproval(ask, persist)) => {
5646	                    escalate_act(&owlery, &ask, persist, &format!("op '{op_name}' is gated"))
5647	                }
5648	                Err(e) => render_link_err("SHELL_CMD", &shell_ref, &owner, e),
5649	            }
5650	        }
5651	        ShellCmd::Drive {
5652	            shell_ref,
5653	            drive_type,
5654	            payload,
5655	            owner,
5656	        } => {
5657	            // The REST-only ephemeral drive channel (M11-W2): the owner→shell
5658	            // mirror of the shell's sensory emit. The shared core
5659	            // (`drive_channel_write`) vocabulary-checks the drive-type against
5660	            // [shell.drive], composes the frame, and writes the daemon's single
5661	            // latest-wins in-memory slot — NEVER spooled, NEVER woken. An offline
5662	            // shell drops with a diagnostic (control is live-or-drop; a replayed
5663	            // control frame is actively wrong). Owner-exclusivity is structural:
5664	            // the ref resolves only under this owner's perch.
5665	            // [impl->REQ-SHELL-3]
5666	            let Some(owner) = owner_or_self(owner) else {
5667	                eprintln!("NO_SELF: shell drive needs an owner (--owner <id> or $OWL_SESSION_ID)");
5668	                return 1;
5669	            };
5670	            // The cross-node arm: `<ref>@<node>` ships the drive to the shell's
5671	            // node, where it writes the LOCAL slot through the same core (offline
5672	            // → drop identical to local; never spooled on either leg). Mirrors
5673	            // the cmd @node arm.
5674	            if let Some((r, node)) = shell_ref.split_once('@') {
5675	                return shell_link_remote(
5676	                    &owner,
5677	                    node,
5678	                    r,
5679	                    spt_net::net::shelllink::SHELL_LINK_DRIVE,
5680	                    &[drive_type, payload],
5681	                );
5682	            }
5683	            // The broker (always-up daemon anchor) holds the slot — ensure it is up.
5684	            if let Err(e) = spt_daemon::ensure_running() {
5685	                eprintln!("SHELL_DRIVE_FAIL: daemon start: {e}");
5686	                return 1;
5687	            }
5688	            match spt_daemon::drive_channel_write(&owlery, &owner, &shell_ref, &drive_type, &payload)
5689	            {
5690	                Ok(spt_daemon::DriveDelivery::Driven { id, drive_type }) => {
5691	                    eprintln!("SHELL_DRIVEN:{id} type={drive_type} (latest-wins slot; drains via api drive-poll --link)");
5692	                    0
5693	                }
5694	                Ok(spt_daemon::DriveDelivery::DroppedOffline { id, drive_type }) => {
5695	                    eprintln!("DRIVE_DROPPED:{id} type={drive_type} (shell offline — drive is never spooled)");
5696	                    0
5697	                }
5698	                Err(spt_daemon::ShellLinkError::NoShell) => {
5699	                    eprintln!("NO_SHELL:{shell_ref}: no such instance for {owner}");
5700	                    1
5701	                }
5702	                Err(e) => {
5703	                    eprintln!("SHELL_DRIVE_FAIL:{shell_ref}: {e}");
5704	                    1
5705	                }
5706	            }
5707	        }
5708	        ShellCmd::Send {
5709	            shell_ref,
5710	            text,
5711	            file,
5712	            owner,
5713	        } => {
5714	            // The durable 2-way text+file channel, agent→shell direction

                return 1;
            };
            // The cross-node arm (M5-D8c): `<ref>@<node>` ships the verb to
            // the shell's node — commands ride Iroh, the action runs there.
            // [impl->REQ-SHELL-2]
            if let Some((r, node)) = shell_ref.split_once('@') {
                return shell_link_remote(
                    &owner,
                    node,
                    r,
                    spt_net::net::shelllink::SHELL_LINK_CMD,
                    &op,
                );
            }
            let Some((op_name, args)) = op.split_first() else {
                eprintln!("USAGE: shell cmd <ref> <op> [args…]");
                return 2;
            };
            match spt_daemon::drive_shell(&owlery, &owner, &shell_ref, op_name, args) {
                Ok((id, shell)) => {
                    drain_per_receipt(&owlery, &owner, &id, &shell, &format!("op={op_name}"))
                }
                // Per-capability act-gate (M11, REQ-CONSENT-3): the gated op did
                // NOT spool — route the approval prompt (the same plumbing the
                // spawn gate uses) before the command may land.
                Err(spt_daemon::ShellLinkError::NeedsApproval(ask, persist)) => {
                    escalate_act(&owlery, &ask, persist, &format!("op '{op_name}' is gated"))
                }
                Err(e) => render_link_err("SHELL_CMD", &shell_ref, &owner, e),
            }
        }
        ShellCmd::Drive {
            shell_ref,
            drive_type,
            payload,
            owner,
        } => {
            // The REST-only ephemeral drive channel (M11-W2): the owner→shell
            // mirror of the shell's sensory emit. The shared core
            // (`drive_channel_write`) vocabulary-checks the drive-type against
            // [shell.drive], composes the frame, and writes the daemon's single
            // latest-wins in-memory slot — NEVER spooled, NEVER woken. An offline
            // shell drops with a diagnostic (control is live-or-drop; a replayed
            // control frame is actively wrong). Owner-exclusivity is structural:
            // the ref resolves only under this owner's perch.
            // [impl->REQ-SHELL-3]
            let Some(owner) = owner_or_self(owner) else {
                eprintln!("NO_SELF: shell drive needs an owner (--owner <id> or $OWL_SESSION_ID)");
                return 1;
            };
            // The cross-node arm: `<ref>@<node>` ships the drive to the shell's
            // node, where it writes the LOCAL slot through the same core (offline
            // → drop identical to local; never spooled on either leg). Mirrors
            // the cmd @node arm.
            if let Some((r, node)) = shell_ref.split_once('@') {
                return shell_link_remote(
                    &owner,
                    node,
                    r,
                    spt_net::net::shelllink::SHELL_LINK_DRIVE,
                    &[drive_type, payload],
                );
            }
            // The broker (always-up daemon anchor) holds the slot — ensure it is up.
            if let Err(e) = spt_daemon::ensure_running() {
                eprintln!("SHELL_DRIVE_FAIL: daemon start: {e}");
                return 1;
            }
            match spt_daemon::drive_channel_write(&owlery, &owner, &shell_ref, &drive_type, &payload)
            {
                Ok(spt_daemon::DriveDelivery::Driven { id, drive_type }) => {
                    eprintln!("SHELL_DRIVEN:{id} type={drive_type} (latest-wins slot; drains via api drive-poll --link)");
                    0
                }
                Ok(spt_daemon::DriveDelivery::DroppedOffline { id, drive_type }) => {
                    eprintln!("DRIVE_DROPPED:{id} type={drive_type} (shell offline — drive is never spooled)");
                    0
                }
                Err(spt_daemon::ShellLinkError::NoShell) => {
                    eprintln!("NO_SHELL:{shell_ref}: no such instance for {owner}");
                    1
                }
                Err(e) => {
                    eprintln!("SHELL_DRIVE_FAIL:{shell_ref}: {e}");
                    1
                }
            }
        }
        ShellCmd::Send {
            shell_ref,
            text,
            file,
            owner,
        } => {
            // The durable 2-way text+file channel, agent→shell direction


Substrate done. Now the control-channel byte bridge. Need base64 helpers for binary payloads + the Broker handle. Check both:


grep -rn 'fn encode_bytes\|fn decode_bytes\|pub use.*encode_bytes\|use.*codec' crates/spt-daemon/src/*.rs | grep -i 'bytes\|base64' | head; echo "---broker import in tunnelhub siblings---"; grep -n 'use crate::Broker\|use crate::broker::Broker\|crate::Broker' crates/spt-daemon/src/drivehub.rs crates/spt-daemon/src/digesthub.rs crates/spt-daemon/src/dispatch.rs 2>/dev/null | head

Find base64 helpers + Broker import pattern


crates/spt-daemon/src/msg.rs:631:pub fn encode_bytes(bytes: &[u8]) -> String {
crates/spt-daemon/src/msg.rs:636:pub fn decode_bytes(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
---broker import in tunnelhub siblings---


`encode_bytes`/`decode_bytes` in `msg.rs`. Now refactor tunnelhub: serve gets `Arc<Broker>` + ENSURE/SEND/RECV byte ops. Add imports + cap const:


Edit

use crate::codec::{read_frame, write_frame};
use crate::frame::{Envelope, Role};
use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};

use crate::broker::Broker;
use crate::codec::{read_frame, write_frame};
use crate::frame::{Envelope, Role};
use crate::msg::{decode_bytes, encode_bytes};
use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};

/// The shell tunnel's per-stream retentive backpressure cap, in chunks (M11-W3,
/// REQ-SHELL-4): the undrained ring this many chunks deep lags the sender (no
/// loss, no unbounded growth — see [`crate::nethost::NetHost::mark_stream_retentive`]).
pub const TUNNEL_RING_CHUNKS: usize = 256;


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

use crate::codec::{read_frame, write_frame};
use crate::frame::{Envelope, Role};
use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};

use crate::broker::Broker;
use crate::codec::{read_frame, write_frame};
use crate::frame::{Envelope, Role};
use crate::msg::{decode_bytes, encode_bytes};
use crate::transport::{recv_hello, send_hello, DaemonTransport, LocalSocketTransport};

/// The shell tunnel's per-stream retentive backpressure cap, in chunks (M11-W3,
/// REQ-SHELL-4): the undrained ring this many chunks deep lags the sender (no
/// loss, no unbounded growth — see [`crate::nethost::NetHost::mark_stream_retentive`]).
pub const TUNNEL_RING_CHUNKS: usize = 256;

+/// The shell tunnel's per-stream retentive backpressure cap, in chunks (M11-W3,

+/// REQ-SHELL-4): the undrained ring this many chunks deep lags the sender (no

+/// loss, no unbounded growth — see [`crate::nethost::NetHost::mark_stream_retentive`]).

+pub const TUNNEL_RING_CHUNKS: usize = 256;

/// Daemon→registry: record a shell's freshly-opened tunnel stream pair, stamped

/// with the opening link token. Fired at shell-online (manifest tunnel-enabled).

pub const KIND_TUNNEL_OPEN: &str = "tunnel_open";


Add ENSURE/SEND/RECV wire kinds + payloads:


/// Owner/daemon→registry: clear a shell's tunnel entry unconditionally (the
/// link-break cleanup, fired from `close_shell`; the token stamp is the crash-proof
/// guarantee, this is the prompt cleanup).
pub const KIND_TUNNEL_CLEAR: &str = "tunnel_clear";

/// Owner/daemon→registry: clear a shell's tunnel entry unconditionally (the
/// link-break cleanup, fired from `close_shell`; the token stamp is the crash-proof
/// guarantee, this is the prompt cleanup).
pub const KIND_TUNNEL_CLEAR: &str = "tunnel_clear";
/// Daemon→broker: ensure a shell's tunnel is open (at shell-online, manifest
/// tunnel-enabled) — the broker opens the same-node loopback stream pair, marks
/// both retentive, and registers them under the link token. Idempotent-ish: a
/// second ensure (relink) opens a fresh pair under the new token. Replies
/// [`KIND_TUNNEL_ENSURED`].
pub const KIND_TUNNEL_ENSURE: &str = "tunnel_ensure";
/// Broker→daemon: the ensure reply (ok / the stream ids, or an error string).
pub const KIND_TUNNEL_ENSURED: &str = "tunnel_ensured";
/// Surface→broker: write opaque bytes to one end of the tunnel (the held stream).
/// Fire-and-forget; `finish` closes the send side (the peer sees EOF).
pub const KIND_TUNNEL_SEND: &str = "tunnel_send";
/// Surface→broker: drain opaque bytes from one end of the tunnel (the recv
/// cursor). Replies [`KIND_TUNNEL_RECV_RESULT`].
pub const KIND_TUNNEL_RECV: &str = "tunnel_recv";
/// Broker→surface: the drain reply — the bytes (base64) + whether the stream ended.
pub const KIND_TUNNEL_RECV_RESULT: &str = "tunnel_recv_result";


/// Owner/daemon→registry: clear a shell's tunnel entry unconditionally (the
/// link-break cleanup, fired from `close_shell`; the token stamp is the crash-proof
/// guarantee, this is the prompt cleanup).
pub const KIND_TUNNEL_CLEAR: &str = "tunnel_clear";
/// Daemon→broker: ensure a shell's tunnel is open (at shell-online, manifest
/// tunnel-enabled) — the broker opens the same-node loopback stream pair, marks
/// both retentive, and registers them under the link token. Idempotent-ish: a
/// second ensure (relink) opens a fresh pair under the new token. Replies
/// [`KIND_TUNNEL_ENSURED`].
pub const KIND_TUNNEL_ENSURE: &str = "tunnel_ensure";
/// Broker→daemon: the ensure reply (ok / the stream ids, or an error string).
pub const KIND_TUNNEL_ENSURED: &str = "tunnel_ensured";
/// Surface→broker: write opaque bytes to one end of the tunnel (the held stream).
/// Fire-and-forget; `finish` closes the send side (the peer sees EOF).
pub const KIND_TUNNEL_SEND: &str = "tunnel_send";
/// Surface→broker: drain opaque bytes from one end of the tunnel (the recv
/// cursor). Replies [`KIND_TUNNEL_RECV_RESULT`].
pub const KIND_TUNNEL_RECV: &str = "tunnel_recv";
/// Broker→surface: the drain reply — the bytes (base64) + whether the stream ended.
pub const KIND_TUNNEL_RECV_RESULT: &str = "tunnel_recv_result";

+/// Daemon→broker: ensure a shell's tunnel is open (at shell-online, manifest

+/// tunnel-enabled) — the broker opens the same-node loopback stream pair, marks

+/// both retentive, and registers them under the link token. Idempotent-ish: a

+/// second ensure (relink) opens a fresh pair under the new token. Replies

+pub const KIND_TUNNEL_ENSURE: &str = "tunnel_ensure";

+/// Broker→daemon: the ensure reply (ok / the stream ids, or an error string).

+pub const KIND_TUNNEL_ENSURED: &str = "tunnel_ensured";

+/// Surface→broker: write opaque bytes to one end of the tunnel (the held stream).

+/// Fire-and-forget; `finish` closes the send side (the peer sees EOF).

+pub const KIND_TUNNEL_SEND: &str = "tunnel_send";

+/// Surface→broker: drain opaque bytes from one end of the tunnel (the recv

+/// cursor). Replies [`KIND_TUNNEL_RECV_RESULT`].

+pub const KIND_TUNNEL_RECV: &str = "tunnel_recv";

+/// Broker→surface: the drain reply — the bytes (base64) + whether the stream ended.

+pub const KIND_TUNNEL_RECV_RESULT: &str = "tunnel_recv_result";

/// Which end of the cross-wired stream pair a [`resolve`](KIND_TUNNEL_RESOLVE)

/// asks for: the OWNER end (the operator row `spt shell tunnel` drives) or the


/// `tunnel_result` payload — the resolve reply.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelResult {

/// `tunnel_ensure` payload: the slot key + the opening link token.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelEnsureReq {
    pub owner: String,
    pub shell_id: String,
    pub token: String,
}

/// `tunnel_ensured` payload — the ensure reply.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelEnsured {
    /// The opened stream pair (owner end, shell end), absent on error.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub streams: Option<(u64, u64)>,
    /// An error string if the open failed (net disabled, open error).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// `tunnel_send` payload: the held stream id, the opaque bytes (base64), and
/// whether to close the send side after.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelSendReq {
    pub stream_id: u64,
    pub data_b64: String,
    #[serde(default)]
    pub finish: bool,
}

/// `tunnel_recv` payload: the held stream id to drain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelRecvReq {
    pub stream_id: u64,
}

/// `tunnel_recv_result` payload — the drained opaque bytes + EOF flag.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelRecvResult {
    pub data_b64: String,
    #[serde(default)]
    pub finished: bool,
}

/// `tunnel_result` payload — the resolve reply.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelResult {


/// `tunnel_ensure` payload: the slot key + the opening link token.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelEnsureReq {
    pub owner: String,
    pub shell_id: String,
    pub token: String,
}

/// `tunnel_ensured` payload — the ensure reply.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelEnsured {
    /// The opened stream pair (owner end, shell end), absent on error.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub streams: Option<(u64, u64)>,
    /// An error string if the open failed (net disabled, open error).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub error: Option<String>,
}

/// `tunnel_send` payload: the held stream id, the opaque bytes (base64), and
/// whether to close the send side after.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelSendReq {
    pub stream_id: u64,
    pub data_b64: String,
    #[serde(default)]
    pub finish: bool,
}

/// `tunnel_recv` payload: the held stream id to drain.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TunnelRecvReq {
    pub stream_id: u64,
}

/// `tunnel_recv_result` payload — the drained opaque bytes + EOF flag.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelRecvResult {
    pub data_b64: String,
    #[serde(default)]
    pub finished: bool,
}

/// `tunnel_result` payload — the resolve reply.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TunnelResult {

+/// `tunnel_ensure` payload: the slot key + the opening link token.

+#[derive(Debug, Clone, Serialize, Deserialize)]

+/// `tunnel_ensured` payload — the ensure reply.

+#[derive(Debug, Clone, Default, Serialize, Deserialize)]

+    /// The opened stream pair (owner end, shell end), absent on error.

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    /// An error string if the open failed (net disabled, open error).

+/// `tunnel_send` payload: the held stream id, the opaque bytes (base64), and

+/// whether to close the send side after.

+/// `tunnel_recv` payload: the held stream id to drain.

+/// `tunnel_recv_result` payload — the drained opaque bytes + EOF flag.


Now rewrite `serve_tunnel_control` + `handle_conn` to take `Arc<Broker>` + handle the byte ops:


/// Bind the tunnel-control channel at `name` and serve it forever against `hub`.
/// Thread-per-connection (mirroring the digest / drive control channels).
pub fn serve_tunnel_control(name: &str, hub: Arc<TunnelHub>) -> io::Result<()> {
    let listener = LocalSocketTransport::bind(name)?;
    loop {
        let conn = listener.accept()?;
        let hub = Arc::clone(&hub);
        thread::spawn(move || {
            let _ = handle_conn(conn, &hub);
        });
    }
}

/// Drive one control connection: handshake, then service open / resolve / clear
/// requests until the client disconnects.
fn handle_conn<C: Read + Write>(mut conn: C, hub: &TunnelHub) -> io::Result<()> {
    recv_hello(&mut conn, Role::Brain)?;
    loop {
        let env = match read_frame(&mut conn) {
            Ok(e) => e,
            Err(_) => return Ok(()), // peer hung up
        };
        match env.kind.as_str() {
            KIND_TUNNEL_OPEN => {
                let req: TunnelOpenReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                hub.open(
                    &req.owner,
                    &req.shell_id,
                    &req.token,
                    req.owner_stream,
                    req.shell_stream,
                );
                // No reply — fire-and-forget (the open already succeeded on the wire).
            }
            KIND_TUNNEL_RESOLVE => {
                let req: TunnelResolveReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let stream_id = hub.resolve(&req.owner, &req.shell_id, req.end, &req.token);
                let res = TunnelResult { stream_id };
                write_frame(
                    &mut conn,
                    &Envelope::new(KIND_TUNNEL_RESULT, serde_json::to_value(res).unwrap()),
                )?;
            }
            KIND_TUNNEL_CLEAR => {
                let req: TunnelClearReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                hub.clear(&req.owner, &req.shell_id);
                // No reply.
            }
            // Forward-compat: an unknown control kind is tolerated, not fatal.
            _ => {}
        }
    }
}

/// Bind the tunnel-control channel at `name` and serve it forever against `hub`,
/// using `broker` to reach the broker-owned [`NetHost`](crate::nethost::NetHost)
/// for the opaque byte relay (doyle gate A: the tunnel relay is broker-homed — no
/// dispatch change, no brain serve loop). Thread-per-connection (mirroring the
/// digest / drive control channels).
// [impl->REQ-SHELL-4]
pub fn serve_tunnel_control(name: &str, hub: Arc<TunnelHub>, broker: Arc<Broker>) -> io::Result<()> {
    let listener = LocalSocketTransport::bind(name)?;
    loop {
        let conn = listener.accept()?;
        let hub = Arc::clone(&hub);
        let broker = Arc::clone(&broker);
        thread::spawn(move || {
            let _ = handle_conn(conn, &hub, &broker);
        });
    }
}

/// Drive one control connection: handshake, then service ensure / resolve / send /
/// recv / clear requests until the client disconnects. The byte ops (ensure / send
/// / recv) operate the broker-owned NetHost directly; registry ops (resolve /
/// clear) touch only the hub.
fn handle_conn<C: Read + Write>(mut conn: C, hub: &TunnelHub, broker: &Broker) -> io::Result<()> {
    recv_hello(&mut conn, Role::Brain)?;
    loop {
        let env = match read_frame(&mut conn) {
            Ok(e) => e,
            Err(_) => return Ok(()), // peer hung up
        };
        match env.kind.as_str() {
            KIND_TUNNEL_ENSURE => {
                let req: TunnelEnsureReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let res = match broker.net() {
                    None => TunnelEnsured {
                        streams: None,
                        error: Some("net disabled".into()),
                    },
                    Some(host) => match host.open_loopback_pair() {
                        Ok((owner_stream, shell_stream)) => {
                            host.mark_stream_retentive(owner_stream, TUNNEL_RING_CHUNKS);
                            host.mark_stream_retentive(shell_stream, TUNNEL_RING_CHUNKS);
                            hub.open(
                                &req.owner,
                                &req.shell_id,
                                &req.token,
                                owner_stream,
                                shell_stream,
                            );
                            TunnelEnsured {
                                streams: Some((owner_stream, shell_stream)),
                                error: None,
                            }
                        }
                        Err(e) => TunnelEnsured {
                            streams: None,
                            error: Some(e.to_string()),
                        },
                    },
                };
                write_frame(
                    &mut conn,
                    &Envelope::new(KIND_TUNNEL_ENSURED, serde_json::to_value(res).unwrap()),
                )?;
            }
            KIND_TUNNEL_RESOLVE => {
                let req: TunnelResolveReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let stream_id = hub.resolve(&req.owner, &req.shell_id, req.end, &req.token);
                let res = TunnelResult { stream_id };
                write_frame(
                    &mut conn,
                    &Envelope::new(KIND_TUNNEL_RESULT, serde_json::to_value(res).unwrap()),
                )?;
            }
            KIND_TUNNEL_SEND => {
                let req: TunnelSendReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let bytes = decode_bytes(&req.data_b64)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                if let Some(host) = broker.net() {
                    let _ = host.send_stream(req.stream_id, &bytes, req.finish);
                }
                // No reply — fire-and-forget (the owner gated liveness client-side).
            }
            KIND_TUNNEL_RECV => {
                let req: TunnelRecvReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                let (bytes, finished) = broker
                    .net()
                    .and_then(|h| h.drain_stream(req.stream_id))
                    .unwrap_or_default();
                let res = TunnelRecvResult {
                    data_b64: encode_bytes(&bytes),
                    finished,
                };
                write_frame(
                    &mut conn,
                    &Envelope::new(KIND_TUNNEL_RECV_RESULT, serde_json::to_value(res).unwrap()),
                )?;
            }
            KIND_TUNNEL_CLEAR => {
                let req: TunnelClearReq = serde_json::from_value(env.payload)
                    .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                hub.clear(&req.owner, &req.shell_id);
                // No reply.
            }
            // Forward-compat: an unknown control kind is tolerated, not fatal.
            _ => {}
        }
    }
}