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

/// A hermetic config that REQUIRES the mutual seed-proof (`membership: Some`):
/// a dial then runs `prove_membership`, which a `membership: None` peer never
/// answers — the documented mixed-pair deadlock (the dialer opens a proof
/// control stream and awaits a reply that never comes). The credential content
/// is irrelevant; its PRESENCE is what makes the dialer open the stream and
/// block, giving the deterministic black hole the broker-side bound must cut.
fn proving(identity: Identity) -> NetConfig {
    let mut cfg = hermetic(identity);
    cfg.membership = Some(std::sync::Arc::new(|| {
        vec![spt_daemon::seedproofx::SubnetCred {
            name: "blackhole".to_string(),
            seed: vec![7u8; 32],
            epoch: 1,
            prev: None,
        }]
    }));
    cfg
}

/// Bind a broker (serving on a background thread) that owns a hermetic endpoint
/// for `identity`, with its effect journal in `dir`.
fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {

//! D4a regression — the broker owns the node's iroh endpoint + live QUIC
//! connections (ADR-0004 §B), driven by the brain over the versioned IPC.
//!
//! Hermetic throughout: relays disabled, no discovery, loopback QUIC only —
//! no network is reached. The shapes proven here:
//!
//! - the endpoint is constructed **broker-side** and reported to the brain via
//!   the `net-status` frame (node id == the identity the broker bound);
//! - a brain dials a **second in-process broker's endpoint** through the
//!   `net-dial` frame; both brokers register the connection in their tables;
//! - an `op_id`-carrying dial is **exactly-once across a brain restart**: the
//!   replayed dial from the next brain life is deduped at the broker (no second
//!   connection) and answered with the original connection;
//! - an unknown `net-*` frame is tolerated (reported, connection survives) —
//!   the 2.3 forward-compat invariant carries into the net vocabulary;
//! - a net-less broker answers `net-status` with `enabled: false` and a dial
//!   with a typed error (capability probing, not a wedge).

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use spt_daemon::brain::Brain;
use spt_daemon::nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique_name() -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-d4a-{}-{}.sock", std::process::id(), n)
}

/// A hermetic net config: relays disabled, no discovery, loopback-only sockets
/// (no real interface touched; no per-test-binary Windows firewall prompt).
fn hermetic(identity: Identity) -> NetConfig {
    NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    }
}

/// Bind a broker (serving on a background thread) that owns a hermetic endpoint
/// for `identity`, with its effect journal in `dir`.
fn net_broker(name: &str, identity: Identity, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(hermetic(identity)).expect("net host start");
    let broker =
        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}

fn connect_retry(name: &str) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain could not connect");
}

// [int->REQ-DAEMON-2] the endpoint is constructed broker-side and surfaced to
// the brain over the `net-status` frame: the reported node id is exactly the
// identity the broker bound (EndpointId == node pubkey), with a dialable
// address and an empty conn table.
#[test]
fn broker_owns_endpoint_and_reports_status() {
    let id = Identity::generate();
    let expect_hex = id.public_key().to_hex();
    let dir = tempfile::tempdir().expect("tempdir");
    let name = unique_name();
    let _broker = net_broker(&name, id, dir.path());

    let mut brain = connect_retry(&name);
    let status = brain.net_status().expect("net-status");
    assert!(status.enabled, "broker hosts an endpoint");
    assert_eq!(status.node_id_hex.as_deref(), Some(expect_hex.as_str()));
    assert!(!status.addr.is_null(), "dialable address rides the reply");
    assert_eq!(status.conn_count, 0);
}

// [int->REQ-DAEMON-2] a brain dials a second in-process broker's endpoint over
// hermetic loopback QUIC through the `net-dial` frame: the dialer's broker
// registers the connection in its table, and the second broker's accept loop
// registers the inbound side — both visible over their own IPC status frames.
#[test]
fn brain_dials_second_broker_over_loopback() {
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let id_b = Identity::generate();
    let b_hex = id_b.public_key().to_hex();
    let _broker_a = net_broker(&name_a, Identity::generate(), &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, id_b, &dir.path().join("b"));

    let mut brain_a = connect_retry(&name_a);
    let mut brain_b = connect_retry(&name_b);

    // Hand B's dialable address to A's brain (as a ticket exchange would).
    let addr = brain_b.net_status().expect("b status").addr;
    let dialed = brain_a.net_dial(addr, None).expect("dial");
    assert_eq!(
        dialed.remote_id_hex, b_hex,
        "QUIC handshake proves the peer"
    );
    assert!(dialed.applied_now);

    assert_eq!(brain_a.net_status().expect("a status").conn_count, 1);
    // B's accept loop registers the inbound connection asynchronously.
    let mut b_count = 0;
    for _ in 0..400 {
        b_count = brain_b.net_status().expect("b status").conn_count;
        if b_count == 1 {
            break;
        }
        thread::sleep(Duration::from_millis(5));
    }
    assert_eq!(b_count, 1, "acceptor broker registered the inbound conn");
}

// [int->REQ-HAZARD-RESTART-IDEMPOTENT] an `op_id`-carrying dial is exactly-once
// across a brain restart: a fresh brain life replaying the same durable id is
// deduped at the broker (no second QUIC connection — the conn table stays at
// one) and answered with the connection the first life opened. The broker-owned
// journal holds the net-namespaced key.
#[test]
fn replayed_dial_op_is_deduped_across_brain_restart() {
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let broker_a = net_broker(&name_a, Identity::generate(), &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join("b"));

    let mut brain_b = connect_retry(&name_b);
    let addr = brain_b.net_status().expect("b status").addr;

    const OP: u64 = 42;
    // Life 1: journaled dial, then "crash" (connection drop).
    let mut life1 = connect_retry(&name_a);
    let first = life1.net_dial(addr.clone(), Some(OP)).expect("dial");
    assert!(first.applied_now, "first delivery runs the dial");
    drop(life1);

    // Life 2: re-drives the same durable op (the brain never learned it landed).
    let mut life2 = connect_retry(&name_a);
    let replay = life2.net_dial(addr, Some(OP)).expect("replayed dial");
    assert!(!replay.applied_now, "replay is deduped, not re-dialed");
    assert_eq!(
        replay.conn_id, first.conn_id,
        "answered with the original connection"
    );
    assert_eq!(replay.remote_id_hex, first.remote_id_hex);

    // Exactly-once at the resource: one connection, one journaled key.
    assert_eq!(life2.net_status().expect("a status").conn_count, 1);
    assert!(broker_a.journal().is_applied((NET_EFFECT_SESSION, OP)));
    assert_eq!(broker_a.journal().applied_count(), 1);
}

// [int->REQ-DAEMON-2] an unknown net frame kind is tolerated: the broker
// reports it and the connection keeps serving (the 2.3 unknown-kind invariant
// holds for the D4a vocabulary too).
#[test]
fn unknown_net_frame_is_reported_not_fatal() {
    let dir = tempfile::tempdir().expect("tempdir");
    let name = unique_name();
    let _broker = net_broker(&name, Identity::generate(), dir.path());

    let mut brain = connect_retry(&name);
    // A future net frame this broker has never heard of, sent raw.
    brain
        .net_dial(serde_json::json!("not even an address"), None)
        .expect_err("malformed addr is a reported error, not a panic");
    // The connection survived: status still answers.
    let status = brain.net_status().expect("net-status after error");
    assert!(status.enabled);

    // And a genuinely unknown KIND surfaces as an error event, connection alive.
    // (Drive the raw frame through the brain's transport via a second client.)
    use spt_daemon::codec::{read_frame, write_frame};
    use spt_daemon::frame::{Envelope, Role};
    use spt_daemon::transport::{send_hello, LocalSocketTransport};
    let mut conn = LocalSocketTransport::connect(&name).expect("connect raw");
    send_hello(&mut conn, Role::Brain).expect("hello");
    write_frame(
        &mut conn,
        &Envelope::new("net-frame-from-the-future", serde_json::Value::Null),
    )
    .expect("send unknown kind");
    let reply = read_frame(&mut conn).expect("connection still serving");
    assert_eq!(reply.kind, "error", "unknown kind reported, not fatal");
    // Still serving after the error: a status round-trips on the same connection.
    write_frame(
        &mut conn,
        &Envelope::new("net-status", serde_json::Value::Null),
    )
    .expect("send status");
    let reply = read_frame(&mut conn).expect("still alive");
    assert_eq!(reply.kind, "net-status-reply");
}

// [int->REQ-DAEMON-2] a broker bound without a network host answers capability
// probes gracefully: `net-status` says disabled, a dial is a typed error.
#[test]
fn netless_broker_probes_gracefully() {
    let dir = tempfile::tempdir().expect("tempdir");
    let name = unique_name();
    let broker = Broker::bind_in(&name, dir.path().join("effects.log")).expect("bind");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });

    let mut brain = connect_retry(&name);
    let status = brain.net_status().expect("net-status");
    assert!(!status.enabled, "no endpoint hosted");
    assert!(status.node_id_hex.is_none());
    assert_eq!(status.conn_count, 0);

    let err = brain
        .net_dial(serde_json::json!({}), None)
        .expect_err("dial on a net-less broker errors");
    assert!(
        err.to_string().contains("net disabled"),
        "typed, operator-facing: {err}"
    );
}

+/// A hermetic config that REQUIRES the mutual seed-proof (`membership: Some`):

+/// a dial then runs `prove_membership`, which a `membership: None` peer never

+/// answers — the documented mixed-pair deadlock (the dialer opens a proof

+/// control stream and awaits a reply that never comes). The credential content

+/// is irrelevant; its PRESENCE is what makes the dialer open the stream and

+/// block, giving the deterministic black hole the broker-side bound must cut.

+fn proving(identity: Identity) -> NetConfig {

+    cfg.membership = Some(std::sync::Arc::new(|| {

+        vec![spt_daemon::seedproofx::SubnetCred {

+            name: "blackhole".to_string(),


Edit

// [int->REQ-DAEMON-2] a broker bound without a network host answers capability
// probes gracefully: `net-status` says disabled, a dial is a typed error.
#[test]
fn netless_broker_probes_gracefully() {

// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the broker-side QUIC-op bound (the
// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but
// black-holes the seed-proof (a membership-mismatched/dead peer — the exact
// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker
// error reply within the bound, NEVER an unbounded await. Exactly-once is
// preserved on timeout: the journaled dial records NO phantom conn — the journal
// key stays un-applied and the conn table stays empty — so a fresh op re-dials
// cleanly (no dedupe into a dead conn). And the broker keeps serving after: it
// REPLIED an error, it did not wedge.
#[test]
fn dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error() {
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());

    // A: a dialer that REQUIRES the seed-proof, with a SHORT QUIC-op bound so the
    // dead-peer await fails fast instead of pinning the test ~10s.
    let a_host = NetHost::start(proving(Identity::generate())).expect("a host");
    a_host.set_quic_op_timeout(Duration::from_millis(800));
    let broker_a =
        Broker::bind_in_with_net(&name_a, dir.path().join("a-effects.log"), Some(a_host))
            .expect("bind a");
    {
        let serve = Arc::clone(&broker_a);
        thread::spawn(move || {
            let _ = serve.serve();
        });
    }

    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it
    // registers the conn) but never runs the proof responder, so A's
    // `prove_membership` awaits a control-stream reply that never comes.
    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join("b"));

    let mut brain_a = connect_retry(&name_a);
    let mut brain_b = connect_retry(&name_b);
    let b_addr = brain_b.net_status().expect("b status").addr;

    const OP: u64 = 7;
    let started = std::time::Instant::now();
    let err = brain_a
        .net_dial(b_addr, Some(OP))
        .expect_err("a black-holed dial must error, never block forever");
    let elapsed = started.elapsed();

    // The broker REPLIED an ordinary error within the bound — not a wedge, and
    // nowhere near iroh's native ~30s handshake timeout.
    assert!(
        err.to_string().contains("bound") || err.to_string().contains("unresponsive"),
        "the broker-side bound's error reply surfaced: {err}"
    );
    assert!(
        elapsed < Duration::from_secs(8),
        "the broker bound fired ({elapsed:?}), not an unbounded await"
    );

    // Exactly-once-on-timeout: no phantom conn recorded — the journal key is
    // un-applied and A's conn table is empty, so a fresh op re-dials cleanly.
    assert!(
        !broker_a.journal().is_applied((NET_EFFECT_SESSION, OP)),
        "a timed-out dial records NO applied effect (no phantom conn_id)"
    );
    assert_eq!(broker_a.journal().applied_count(), 0, "nothing journaled");
    assert_eq!(
        brain_a.net_status().expect("a status").conn_count,
        0,
        "no half-open phantom connection survives the timeout"
    );

    // The broker is not wedged: it keeps serving on the same connection.
    assert!(
        brain_a.net_status().expect("a still serves").enabled,
        "the broker replied and kept serving — it did not wedge"
    );

    // Re-driving the SAME durable op still ATTEMPTS (errors again) rather than
    // dedup-succeeding into a phantom — the un-applied key means a clean retry.
    brain_a
        .net_dial(brain_b.net_status().expect("b status").addr, Some(OP))
        .expect_err("the un-applied op re-dials (re-times-out), never a phantom dedup");
}

// [int->REQ-DAEMON-2] a broker bound without a network host answers capability
// probes gracefully: `net-status` says disabled, a dial is a typed error.
#[test]
fn netless_broker_probes_gracefully() {


// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the broker-side QUIC-op bound (the
// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but
// black-holes the seed-proof (a membership-mismatched/dead peer — the exact
// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker
// error reply within the bound, NEVER an unbounded await. Exactly-once is
// preserved on timeout: the journaled dial records NO phantom conn — the journal
// key stays un-applied and the conn table stays empty — so a fresh op re-dials
// cleanly (no dedupe into a dead conn). And the broker keeps serving after: it
// REPLIED an error, it did not wedge.
#[test]
fn dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error() {
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());

    // A: a dialer that REQUIRES the seed-proof, with a SHORT QUIC-op bound so the
    // dead-peer await fails fast instead of pinning the test ~10s.
    let a_host = NetHost::start(proving(Identity::generate())).expect("a host");
    a_host.set_quic_op_timeout(Duration::from_millis(800));
    let broker_a =
        Broker::bind_in_with_net(&name_a, dir.path().join("a-effects.log"), Some(a_host))
            .expect("bind a");
    {
        let serve = Arc::clone(&broker_a);
        thread::spawn(move || {
            let _ = serve.serve();
        });
    }

    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it
    // registers the conn) but never runs the proof responder, so A's
    // `prove_membership` awaits a control-stream reply that never comes.
    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join("b"));

    let mut brain_a = connect_retry(&name_a);
    let mut brain_b = connect_retry(&name_b);
    let b_addr = brain_b.net_status().expect("b status").addr;

    const OP: u64 = 7;
    let started = std::time::Instant::now();
    let err = brain_a
        .net_dial(b_addr, Some(OP))
        .expect_err("a black-holed dial must error, never block forever");
    let elapsed = started.elapsed();

    // The broker REPLIED an ordinary error within the bound — not a wedge, and
    // nowhere near iroh's native ~30s handshake timeout.
    assert!(
        err.to_string().contains("bound") || err.to_string().contains("unresponsive"),
        "the broker-side bound's error reply surfaced: {err}"
    );
    assert!(
        elapsed < Duration::from_secs(8),
        "the broker bound fired ({elapsed:?}), not an unbounded await"
    );

    // Exactly-once-on-timeout: no phantom conn recorded — the journal key is
    // un-applied and A's conn table is empty, so a fresh op re-dials cleanly.
    assert!(
        !broker_a.journal().is_applied((NET_EFFECT_SESSION, OP)),
        "a timed-out dial records NO applied effect (no phantom conn_id)"
    );
    assert_eq!(broker_a.journal().applied_count(), 0, "nothing journaled");
    assert_eq!(
        brain_a.net_status().expect("a status").conn_count,
        0,
        "no half-open phantom connection survives the timeout"
    );

    // The broker is not wedged: it keeps serving on the same connection.
    assert!(
        brain_a.net_status().expect("a still serves").enabled,
        "the broker replied and kept serving — it did not wedge"
    );

    // Re-driving the SAME durable op still ATTEMPTS (errors again) rather than
    // dedup-succeeding into a phantom — the un-applied key means a clean retry.
    brain_a
        .net_dial(brain_b.net_status().expect("b status").addr, Some(OP))
        .expect_err("the un-applied op re-dials (re-times-out), never a phantom dedup");
}

// [int->REQ-DAEMON-2] a broker bound without a network host answers capability
// probes gracefully: `net-status` says disabled, a dial is a typed error.
#[test]
fn netless_broker_probes_gracefully() {

+// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the broker-side QUIC-op bound (the

+// pump-IPC-deadline B-half): a dial to a peer that ACCEPTS the connection but

+// black-holes the seed-proof (a membership-mismatched/dead peer — the exact

+// shape of the 2.2h hfenduleam wedge) must fail PROMPTLY with an ORDINARY broker

+// error reply within the bound, NEVER an unbounded await. Exactly-once is

+// preserved on timeout: the journaled dial records NO phantom conn — the journal

+// key stays un-applied and the conn table stays empty — so a fresh op re-dials

+// cleanly (no dedupe into a dead conn). And the broker keeps serving after: it

+    let dir = tempfile::tempdir().expect("tempdir");

+    let (name_a, name_b) = (unique_name(), unique_name());

+    // A: a dialer that REQUIRES the seed-proof, with a SHORT QUIC-op bound so the

+    // dead-peer await fails fast instead of pinning the test ~10s.

+    let a_host = NetHost::start(proving(Identity::generate())).expect("a host");

+    a_host.set_quic_op_timeout(Duration::from_millis(800));

+        Broker::bind_in_with_net(&name_a, dir.path().join("a-effects.log"), Some(a_host))

+        let serve = Arc::clone(&broker_a);

+    // B: the black hole — accepts the QUIC handshake (`membership: None` ⇒ it

+    // registers the conn) but never runs the proof responder, so A's

+    // `prove_membership` awaits a control-stream reply that never comes.

+    let _broker_b = net_broker(&name_b, Identity::generate(), &dir.path().join("b"));

+    let mut brain_a = connect_retry(&name_a);

+    let mut brain_b = connect_retry(&name_b);

+    let b_addr = brain_b.net_status().expect("b status").addr;

+    let started = std::time::Instant::now();

+        .expect_err("a black-holed dial must error, never block forever");

+    // The broker REPLIED an ordinary error within the bound — not a wedge, and

+    // nowhere near iroh's native ~30s handshake timeout.

+        err.to_string().contains("bound") || err.to_string().contains("unresponsive"),

+        "the broker-side bound's error reply surfaced: {err}"

+        elapsed < Duration::from_secs(8),

+        "the broker bound fired ({elapsed:?}), not an unbounded await"

+    // Exactly-once-on-timeout: no phantom conn recorded — the journal key is

+    // un-applied and A's conn table is empty, so a fresh op re-dials cleanly.

+        !broker_a.journal().is_applied((NET_EFFECT_SESSION, OP)),

+        "a timed-out dial records NO applied effect (no phantom conn_id)"

+    assert_eq!(broker_a.journal().applied_count(), 0, "nothing journaled");

+        brain_a.net_status().expect("a status").conn_count,

+        "no half-open phantom connection survives the timeout"

+    // The broker is not wedged: it keeps serving on the same connection.

+        brain_a.net_status().expect("a still serves").enabled,

+        "the broker replied and kept serving — it did not wedge"

+    // Re-driving the SAME durable op still ATTEMPTS (errors again) rather than

+    // dedup-succeeding into a phantom — the un-applied key means a clean retry.

+        .net_dial(brain_b.net_status().expect("b status").addr, Some(OP))

+        .expect_err("the un-applied op re-dials (re-times-out), never a phantom dedup");


Build + run the netbroker int to validate the black-hole mechanism before writing the pump test.


Bash

cargo test -p spt-daemon --test netbroker dial_to_a_black_holing_peer 2>&1 | tail -25

Run netbroker black-hole int test


   Compiling spt-daemon v0.8.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 12.51s
     Running tests\netbroker.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\netbroker-06288c72f971fd61.exe)

running 1 test
test dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 5 filtered out; finished in 1.69s


netbroker int passes (black hole fires at ~800ms, broker replies ordinary error, no phantom, still serving). Now the pump end-to-end regression. Add helpers + test to pump.rs.


Edit

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

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

/// A hermetic config that REQUIRES the mutual seed-proof (`membership: Some`): a
/// dial runs `prove_membership`, which a `membership: None` peer never answers
/// (the documented mixed-pair deadlock) — the deterministic black hole the
/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its
/// PRESENCE is what makes the dialer open the proof control stream and block.
fn proving() -> NetConfig {
    let mut cfg = hermetic();
    cfg.membership = Some(Arc::new(|| {
        vec![spt_daemon::seedproofx::SubnetCred {
            name: "blackhole".to_string(),
            seed: vec![7u8; 32],
            epoch: 1,
            prev: None,
        }]
    }));
    cfg
}

/// Build a [`PumpPaths`] rooted at `root` (every file a sibling under it).
fn pump_paths(root: &std::path::Path) -> PumpPaths {
    PumpPaths {
        owlery: root.join("owlery"),
        ctx_root: root.join("context"),
        notif_db: root.join("notifs.db"),
        scratch: root.join("scratch"),
        subnets: root.join("subnet.json"),
        vis: root.join("visibility.json"),
        ops: root.join("pump-ops.json"),
        releases: root.join("releases"),
        release_keys: root.join("release-keys.json"),
        pull_marker: root.join("sync-pull-now"),
        advertise_marker: root.join("advertise-now"),
        peer_addrs: root.join("peer-addrs.json"),
        heartbeat: root.join("pump-heartbeat.json"),
        rotations: root.join("rotation-pending.json"),
    }
}

/// Read the pump heartbeat file (`now_ms().to_string()`) as a u64, if present.
fn read_heartbeat(path: &std::path::Path) -> Option<u64> {
    std::fs::read_to_string(path).ok()?.trim().parse().ok()
}


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

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

/// A hermetic config that REQUIRES the mutual seed-proof (`membership: Some`): a
/// dial runs `prove_membership`, which a `membership: None` peer never answers
/// (the documented mixed-pair deadlock) — the deterministic black hole the
/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its
/// PRESENCE is what makes the dialer open the proof control stream and block.
fn proving() -> NetConfig {
    let mut cfg = hermetic();
    cfg.membership = Some(Arc::new(|| {
        vec![spt_daemon::seedproofx::SubnetCred {
            name: "blackhole".to_string(),
            seed: vec![7u8; 32],
            epoch: 1,
            prev: None,
        }]
    }));
    cfg
}

/// Build a [`PumpPaths`] rooted at `root` (every file a sibling under it).
fn pump_paths(root: &std::path::Path) -> PumpPaths {
    PumpPaths {
        owlery: root.join("owlery"),
        ctx_root: root.join("context"),
        notif_db: root.join("notifs.db"),
        scratch: root.join("scratch"),
        subnets: root.join("subnet.json"),
        vis: root.join("visibility.json"),
        ops: root.join("pump-ops.json"),
        releases: root.join("releases"),
        release_keys: root.join("release-keys.json"),
        pull_marker: root.join("sync-pull-now"),
        advertise_marker: root.join("advertise-now"),
        peer_addrs: root.join("peer-addrs.json"),
        heartbeat: root.join("pump-heartbeat.json"),
        rotations: root.join("rotation-pending.json"),
    }
}

/// Read the pump heartbeat file (`now_ms().to_string()`) as a u64, if present.
fn read_heartbeat(path: &std::path::Path) -> Option<u64> {
    std::fs::read_to_string(path).ok()?.trim().parse().ok()
}

+/// A hermetic config that REQUIRES the mutual seed-proof (`membership: Some`): a

+/// dial runs `prove_membership`, which a `membership: None` peer never answers

+/// (the documented mixed-pair deadlock) — the deterministic black hole the

+/// broker-side QUIC-op bound must cut. The credential content is irrelevant; its

+/// PRESENCE is what makes the dialer open the proof control stream and block.

+    cfg.membership = Some(Arc::new(|| {

+        vec![spt_daemon::seedproofx::SubnetCred {

+            name: "blackhole".to_string(),

+/// Build a [`PumpPaths`] rooted at `root` (every file a sibling under it).

+fn pump_paths(root: &std::path::Path) -> PumpPaths {

+        ctx_root: root.join("context"),

+        notif_db: root.join("notifs.db"),

+        subnets: root.join("subnet.json"),

+        vis: root.join("visibility.json"),

+        ops: root.join("pump-ops.json"),

+        releases: root.join("releases"),

+        release_keys: root.join("release-keys.json"),

+        pull_marker: root.join("sync-pull-now"),

+        advertise_marker: root.join("advertise-now"),

+        peer_addrs: root.join("peer-addrs.json"),

+        heartbeat: root.join("pump-heartbeat.json"),

+        rotations: root.join("rotation-pending.json"),

+/// Read the pump heartbeat file (`now_ms().to_string()`) as a u64, if present.

+fn read_heartbeat(path: &std::path::Path) -> Option<u64> {

+    std::fs::read_to_string(path).ok()?.trim().parse().ok()

fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {

let host = NetHost::start(hermetic()).expect("net host start");


    converge("A holds the consent notif", || {
        a_store
            .list(subnet)
            .map(|rows| rows.iter().any(|r| r.kind == "consent"))
            .unwrap_or(false)
    });

    stop.store(true, Ordering::Relaxed);
}

    converge("A holds the consent notif", || {
        a_store
            .list(subnet)
            .map(|rows| rows.iter().any(|r| r.kind == "consent"))
            .unwrap_or(false)
    });

    stop.store(true, Ordering::Relaxed);
}

// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the pump-IPC-deadline B-half, end to
// end: the production pump fanning out to a DEAD / black-holing roster peer (one
// that accepts the QUIC handshake but never answers the seed-proof — the
// 2.2h-hfenduleam-wedge shape, and the live recurrence) must NOT wedge. The
// broker bounds the dial, REPLIES an ordinary error, `ensure_conn` drops the
// peer for the round, and the loop continues — so the heartbeat keeps
// MONOTONICALLY ADVANCING and `run_peer_pump` never bubbles Err (no supervised
// restart). Pre-fix the dial awaited the dead peer unbounded and the
// single-threaded pump froze (the heartbeat stops). The ordinary-vs-TimedOut
// tier-split itself is unit-covered (`pump::peer_outcome` test); this proves the
// bound wires through to a live production pump end to end.
#[test]
fn pump_survives_a_black_holing_peer_heartbeat_advances_no_restart() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();
    let subnet = "bqd";
    let (a_name, b_name) = (unique_name(), unique_name());

    // A's broker: requires the seed-proof + a SHORT QUIC-op bound, so each
    // dead-peer dial fails fast (≈ the bound) instead of pinning the test ~10s.
    let a_host = NetHost::start(proving()).expect("a host");
    a_host.set_quic_op_timeout(Duration::from_millis(500));
    let a_broker = Broker::bind_in_with_net(&a_name, dir.path().join("a-eff.log"), Some(a_host))
        .expect("bind a");
    {
        let serve = Arc::clone(&a_broker);
        thread::spawn(move || {
            let _ = serve.serve();
        });
    }

    // B: the black hole — accepts the handshake (`membership: None`) but never
    // runs the proof responder, so A's dial hangs in `prove_membership` until the
    // broker-side bound cuts it.
    let _b_broker = net_broker(&b_name, dir.path());

    let mut a_probe = connect_retry(&a_name);
    let mut b_probe = connect_retry(&b_name);
    let a_hex = a_probe
        .net_status()
        .expect("a status")
        .node_id_hex
        .expect("net");
    let b_hex = b_probe
        .net_status()
        .expect("b status")
        .node_id_hex
        .expect("net");
    let b_addr = b_probe.net_status().expect("b status").addr;

    // A's stores: a subnet whose roster is the single black-holing peer B.
    let a_root = dir.path().join("a");
    std::fs::create_dir_all(&a_root).unwrap();
    let mut a_subnets = SubnetStore::default();
    a_subnets.create_subnet(subnet).expect("A subnet");
    a_subnets
        .save_to(&a_root.join("subnet.json"))
        .expect("save subnets");
    let mut a_roster = spt_store::roster::RosterStore::default();
    a_roster.merge_entry(spt_store::roster::RosterEntry {
        pubkey_hex: b_hex.clone(),
        subnet: subnet.to_string(),
        label: String::new(),
        machine_id: String::new(),
        address: None,
        last_seen: String::new(),
        lease_epoch: 1,
    });
    a_roster
        .save_to(&a_root.join("roster.json"))
        .expect("save roster");
    let mut a_vis = VisibilityStore::default();
    a_vis.set_sync_subnets("blackhole-ep", vec![subnet.to_string()]);
    a_vis
        .save_to(&a_root.join("visibility.json"))
        .expect("save vis");

    let a_registry = Arc::new(RegistryHost::new_at(
        &a_hex,
        EpochSource::load_from(&a_root.join("epoch.json")),
        a_root.join("registry"),
    ));

    let resolver: PeerResolver = {
        let b_hex = b_hex.clone();
        let b_addr = b_addr.clone();
        Box::new(move |hex| {
            if hex == b_hex {
                Some(b_addr.clone())
            } else {
                None
            }
        })
    };

    let stop = Arc::new(AtomicBool::new(false));
    let (tx, rx) = std::sync::mpsc::channel();
    {
        let name = a_name.clone();
        let reg = Arc::clone(&a_registry);
        let flag = Arc::clone(&stop);
        let paths = pump_paths(&a_root);
        thread::spawn(move || {
            let config = PumpConfig {
                cadence: PumpCadence {
                    registry: Duration::from_millis(100),
                    notif: Duration::from_millis(100),
                    sync: Duration::from_millis(100),
                    update: Duration::from_millis(100),
                    registry_evict_after: Duration::from_secs(300),
                },
                full_auto_update: false,
            };
            let r = run_peer_pump(
                &name,
                reg,
                &paths,
                resolver,
                config,
                PumpHooks::default(),
                &flag,
            );
            let _ = tx.send(r);
        });
    }

    // The heartbeat is written at the loop top (HEARTBEAT_PERIOD = 5s in prod). A
    // wedged pump (the pre-fix bug) freezes it on the dead-peer dial; a healthy
    // one keeps advancing across rounds. Read the first value, then require it to
    // ADVANCE while dead-peer rounds run.
    let hb = a_root.join("pump-heartbeat.json");
    let mut first = None;
    for _ in 0..100 {
        if let Some(v) = read_heartbeat(&hb) {
            first = Some(v);
            break;
        }
        thread::sleep(Duration::from_millis(100));
    }
    let first = first.expect("the pump wrote an initial heartbeat");

    let mut advanced = false;
    for _ in 0..200 {
        thread::sleep(Duration::from_millis(100));
        if read_heartbeat(&hb).is_some_and(|v| v > first) {
            advanced = true;
            break;
        }
    }
    assert!(
        advanced,
        "the heartbeat advanced across dead-peer rounds — the pump did not wedge on the bounded dial"
    );

    // No supervised restart: `run_peer_pump` exits CLEANLY on stop (Ok), never
    // bubbles Err — a TimedOut bubble is precisely what would trigger a supervised
    // restart, and the bound's ordinary error must never become one.
    stop.store(true, Ordering::Relaxed);
    let result = rx
        .recv_timeout(Duration::from_secs(10))
        .expect("the pump thread returns after stop");
    assert!(
        result.is_ok(),
        "run_peer_pump exits cleanly on stop, never bubbling Err (no supervised restart): {result:?}"
    );
}


    converge("A holds the consent notif", || {
        a_store
            .list(subnet)
            .map(|rows| rows.iter().any(|r| r.kind == "consent"))
            .unwrap_or(false)
    });

    stop.store(true, Ordering::Relaxed);
}

// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the pump-IPC-deadline B-half, end to
// end: the production pump fanning out to a DEAD / black-holing roster peer (one
// that accepts the QUIC handshake but never answers the seed-proof — the
// 2.2h-hfenduleam-wedge shape, and the live recurrence) must NOT wedge. The
// broker bounds the dial, REPLIES an ordinary error, `ensure_conn` drops the
// peer for the round, and the loop continues — so the heartbeat keeps
// MONOTONICALLY ADVANCING and `run_peer_pump` never bubbles Err (no supervised
// restart). Pre-fix the dial awaited the dead peer unbounded and the
// single-threaded pump froze (the heartbeat stops). The ordinary-vs-TimedOut
// tier-split itself is unit-covered (`pump::peer_outcome` test); this proves the
// bound wires through to a live production pump end to end.
#[test]
fn pump_survives_a_black_holing_peer_heartbeat_advances_no_restart() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();
    let subnet = "bqd";
    let (a_name, b_name) = (unique_name(), unique_name());

    // A's broker: requires the seed-proof + a SHORT QUIC-op bound, so each
    // dead-peer dial fails fast (≈ the bound) instead of pinning the test ~10s.
    let a_host = NetHost::start(proving()).expect("a host");
    a_host.set_quic_op_timeout(Duration::from_millis(500));
    let a_broker = Broker::bind_in_with_net(&a_name, dir.path().join("a-eff.log"), Some(a_host))
        .expect("bind a");
    {
        let serve = Arc::clone(&a_broker);
        thread::spawn(move || {
            let _ = serve.serve();
        });
    }

    // B: the black hole — accepts the handshake (`membership: None`) but never
    // runs the proof responder, so A's dial hangs in `prove_membership` until the
    // broker-side bound cuts it.
    let _b_broker = net_broker(&b_name, dir.path());

    let mut a_probe = connect_retry(&a_name);
    let mut b_probe = connect_retry(&b_name);
    let a_hex = a_probe
        .net_status()
        .expect("a status")
        .node_id_hex
        .expect("net");
    let b_hex = b_probe
        .net_status()
        .expect("b status")
        .node_id_hex
        .expect("net");
    let b_addr = b_probe.net_status().expect("b status").addr;

    // A's stores: a subnet whose roster is the single black-holing peer B.
    let a_root = dir.path().join("a");
    std::fs::create_dir_all(&a_root).unwrap();
    let mut a_subnets = SubnetStore::default();
    a_subnets.create_subnet(subnet).expect("A subnet");
    a_subnets
        .save_to(&a_root.join("subnet.json"))
        .expect("save subnets");
    let mut a_roster = spt_store::roster::RosterStore::default();
    a_roster.merge_entry(spt_store::roster::RosterEntry {
        pubkey_hex: b_hex.clone(),
        subnet: subnet.to_string(),
        label: String::new(),
        machine_id: String::new(),
        address: None,
        last_seen: String::new(),
        lease_epoch: 1,
    });
    a_roster
        .save_to(&a_root.join("roster.json"))
        .expect("save roster");
    let mut a_vis = VisibilityStore::default();
    a_vis.set_sync_subnets("blackhole-ep", vec![subnet.to_string()]);
    a_vis
        .save_to(&a_root.join("visibility.json"))
        .expect("save vis");

    let a_registry = Arc::new(RegistryHost::new_at(
        &a_hex,
        EpochSource::load_from(&a_root.join("epoch.json")),
        a_root.join("registry"),
    ));

    let resolver: PeerResolver = {
        let b_hex = b_hex.clone();
        let b_addr = b_addr.clone();
        Box::new(move |hex| {
            if hex == b_hex {
                Some(b_addr.clone())
            } else {
                None
            }
        })
    };

    let stop = Arc::new(AtomicBool::new(false));
    let (tx, rx) = std::sync::mpsc::channel();
    {
        let name = a_name.clone();
        let reg = Arc::clone(&a_registry);
        let flag = Arc::clone(&stop);
        let paths = pump_paths(&a_root);
        thread::spawn(move || {
            let config = PumpConfig {
                cadence: PumpCadence {
                    registry: Duration::from_millis(100),
                    notif: Duration::from_millis(100),
                    sync: Duration::from_millis(100),
                    update: Duration::from_millis(100),
                    registry_evict_after: Duration::from_secs(300),
                },
                full_auto_update: false,
            };
            let r = run_peer_pump(
                &name,
                reg,
                &paths,
                resolver,
                config,
                PumpHooks::default(),
                &flag,
            );
            let _ = tx.send(r);
        });
    }

    // The heartbeat is written at the loop top (HEARTBEAT_PERIOD = 5s in prod). A
    // wedged pump (the pre-fix bug) freezes it on the dead-peer dial; a healthy
    // one keeps advancing across rounds. Read the first value, then require it to
    // ADVANCE while dead-peer rounds run.
    let hb = a_root.join("pump-heartbeat.json");
    let mut first = None;
    for _ in 0..100 {
        if let Some(v) = read_heartbeat(&hb) {
            first = Some(v);
            break;
        }
        thread::sleep(Duration::from_millis(100));
    }
    let first = first.expect("the pump wrote an initial heartbeat");

    let mut advanced = false;
    for _ in 0..200 {
        thread::sleep(Duration::from_millis(100));
        if read_heartbeat(&hb).is_some_and(|v| v > first) {
            advanced = true;
            break;
        }
    }
    assert!(
        advanced,
        "the heartbeat advanced across dead-peer rounds — the pump did not wedge on the bounded dial"
    );

    // No supervised restart: `run_peer_pump` exits CLEANLY on stop (Ok), never
    // bubbles Err — a TimedOut bubble is precisely what would trigger a supervised
    // restart, and the bound's ordinary error must never become one.
    stop.store(true, Ordering::Relaxed);
    let result = rx
        .recv_timeout(Duration::from_secs(10))
        .expect("the pump thread returns after stop");
    assert!(
        result.is_ok(),
        "run_peer_pump exits cleanly on stop, never bubbling Err (no supervised restart): {result:?}"
    );
}

+// [int->REQ-HAZARD-BROKER-QUIC-DEADLINE] the pump-IPC-deadline B-half, end to

+// end: the production pump fanning out to a DEAD / black-holing roster peer (one

+// that accepts the QUIC handshake but never answers the seed-proof — the

+// 2.2h-hfenduleam-wedge shape, and the live recurrence) must NOT wedge. The

+// broker bounds the dial, REPLIES an ordinary error, `ensure_conn` drops the

+// peer for the round, and the loop continues — so the heartbeat keeps

+// MONOTONICALLY ADVANCING and `run_peer_pump` never bubbles Err (no supervised

+// restart). Pre-fix the dial awaited the dead peer unbounded and the

+// single-threaded pump froze (the heartbeat stops). The ordinary-vs-TimedOut

+// tier-split itself is unit-covered (`pump::peer_outcome` test); this proves the

+// bound wires through to a live production pump end to end.

+    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());

+    let (a_name, b_name) = (unique_name(), unique_name());

+    // A's broker: requires the seed-proof + a SHORT QUIC-op bound, so each

+    // dead-peer dial fails fast (≈ the bound) instead of pinning the test ~10s.

+    let a_host = NetHost::start(proving()).expect("a host");

+    a_host.set_quic_op_timeout(Duration::from_millis(500));

+    let a_broker = Broker::bind_in_with_net(&a_name, dir.path().join("a-eff.log"), Some(a_host))

+        let serve = Arc::clone(&a_broker);

+    // B: the black hole — accepts the handshake (`membership: None`) but never

+    // runs the proof responder, so A's dial hangs in `prove_membership` until the

+    let _b_broker = net_broker(&b_name, dir.path());

+    let mut a_probe = connect_retry(&a_name);

+    let mut b_probe = connect_retry(&b_name);

+    let b_addr = b_probe.net_status().expect("b status").addr;

+    // A's stores: a subnet whose roster is the single black-holing peer B.

+    std::fs::create_dir_all(&a_root).unwrap();

+    let mut a_subnets = SubnetStore::default();

+    a_subnets.create_subnet(subnet).expect("A subnet");

+        .save_to(&a_root.join("subnet.json"))

+    let mut a_roster = spt_store::roster::RosterStore::default();

+    a_roster.merge_entry(spt_store::roster::RosterEntry {

+        .save_to(&a_root.join("roster.json"))

+    let mut a_vis = VisibilityStore::default();

+    a_vis.set_sync_subnets("blackhole-ep", vec![subnet.to_string()]);

+        .save_to(&a_root.join("visibility.json"))

+    let a_registry = Arc::new(RegistryHost::new_at(

+        EpochSource::load_from(&a_root.join("epoch.json")),

+    let stop = Arc::new(AtomicBool::new(false));

+    let (tx, rx) = std::sync::mpsc::channel();

+        let reg = Arc::clone(&a_registry);

+        let paths = pump_paths(&a_root);

+                    registry: Duration::from_millis(100),

+                    notif: Duration::from_millis(100),

+                    sync: Duration::from_millis(100),

+                    update: Duration::from_millis(100),

+                    registry_evict_after: Duration::from_secs(300),

+                full_auto_update: false,

+    // The heartbeat is written at the loop top (HEARTBEAT_PERIOD = 5s in prod). A

+    // wedged pump (the pre-fix bug) freezes it on the dead-peer dial; a healthy

+    // one keeps advancing across rounds. Read the first value, then require it to

+    // ADVANCE while dead-peer rounds run.

+    let hb = a_root.join("pump-heartbeat.json");

+        if let Some(v) = read_heartbeat(&hb) {

+        thread::sleep(Duration::from_millis(100));

+    let first = first.expect("the pump wrote an initial heartbeat");

+        if read_heartbeat(&hb).is_some_and(|v| v > first) {

+        "the heartbeat advanced across dead-peer rounds — the pump did not wedge on the bounded dial"

+    // No supervised restart: `run_peer_pump` exits CLEANLY on stop (Ok), never

+    // bubbles Err — a TimedOut bubble is precisely what would trigger a supervised

+    // restart, and the bound's ordinary error must never become one.

+    stop.store(true, Ordering::Relaxed);

+        .recv_timeout(Duration::from_secs(10))

+        .expect("the pump thread returns after stop");

+        "run_peer_pump exits cleanly on stop, never bubbling Err (no supervised restart): {result:?}"


Bash

cargo test -p spt-daemon --test pump pump_survives_a_black_holing_peer 2>&1 | tail -25

Run pump black-hole regression test


   Compiling spt-daemon v0.8.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 3.30s
     Running tests\pump.rs (C:\Users\decid\Documents\projects\spt-core\target\debug\deps\pump-eea49d224d42cbab.exe)

running 1 test
test pump_survives_a_black_holing_peer_heartbeat_advances_no_restart ... ok

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


Pump regression passes (5.77s — heartbeat advanced, clean `Ok` stop). Genuine regression: pre-fix the dial freezes ≥30s, my 20s window catches it. All code evidence in place. Activate `[impl,unit,int]` and commit (docs+`doc` stage follow).


Edit

required_stages = []  # registry-first mint (rule 3); activated per wave in v0.8.3 (doc = KNOWN-HAZARDS 7.8 + 7.6 B-half-fixed reword + DEFERRED.md:42 resolved; impl = NetHost::bounded_block_on wrapping dial/open_stream/send_stream's QUIC await sites under a quic_op_timeout (default 10s, test-overridable via set_quic_op_timeout); unit = the bound wrapper returns a prompt non-TimedOut error on a never-completing op + a fast op is untouched [nethost.rs]; int = a black-holing peer (membership-mismatch hang) → the broker REPLIES an ordinary error within the bound (not a wedge) + exactly-once-on-timeout (journal not applied, conn table empty) [tests/netbroker.rs], and the pump round survives a dead peer — heartbeat monotonic-advances, no PEER_PUMP_RESTART [tests/pump.rs])

required_stages = ["impl", "unit", "int"]  # registry-first mint (rule 3); activated per wave in v0.8.3. impl = NetHost::bounded_block_on wrapping dial/open_stream/send_stream's QUIC await sites under a quic_op_timeout (default 10s = BROKER_QUIC_OP_TIMEOUT_MS, test-overridable via set_quic_op_timeout); unit = the bound wrapper returns a prompt non-TimedOut error on a never-completing op + a fast op is untouched [nethost.rs]; int = a black-holing peer (membership-mismatch hang) → the broker REPLIES an ordinary error within the bound (not a wedge) + exactly-once-on-timeout (journal not applied, conn table empty) [tests/netbroker.rs], and the pump round survives a dead peer — heartbeat monotonic-advances + run_peer_pump exits Ok (no PEER_PUMP_RESTART) [tests/pump.rs]. doc activates with the KNOWN-HAZARDS 7.8 + 7.6 B-half-fixed reword + DEFERRED.md:42-resolved commit


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

title = "The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16."

-required_stages = []  # registry-first mint (rule 3); activated per wave in v0.8.3 (doc = KNOWN-HAZARDS 7.8 + 7.6 B-half-fixed reword + DEFERRED.md:42 resolved; impl = NetHost::bounded_block_on wrapping dial/open_stream/send_stream's QUIC await sites under a quic_op_timeout (default 10s, test-overridable via set_quic_op_timeout); unit = the bound wrapper returns a prompt non-TimedOut error on a never-completing op + a fast op is untouched [nethost.rs]; int = a black-holing peer (membership-mismatch hang) → the broker REPLIES an ordinary error within the bound (not a wedge) + exactly-once-on-timeout (journal not applied, conn table empty) [tests/netbroker.rs], and the pump round survives a dead peer — heartbeat monotonic-advances, no PEER_PUMP_RESTART [tests/pump.rs])

+required_stages = ["impl", "unit", "int"]  # registry-first mint (rule 3); activated per wave in v0.8.3. impl = NetHost::bounded_block_on wrapping dial/open_stream/send_stream's QUIC await sites under a quic_op_timeout (default 10s = BROKER_QUIC_OP_TIMEOUT_MS, test-overridable via set_quic_op_timeout); unit = the bound wrapper returns a prompt non-TimedOut error on a never-completing op + a fast op is untouched [nethost.rs]; int = a black-holing peer (membership-mismatch hang) → the broker REPLIES an ordinary error within the bound (not a wedge) + exactly-once-on-timeout (journal not applied, conn table empty) [tests/netbroker.rs], and the pump round survives a dead peer — heartbeat monotonic-advances + run_peer_pump exits Ok (no PEER_PUMP_RESTART) [tests/pump.rs]. doc activates with the KNOWN-HAZARDS 7.8 + 7.6 B-half-fixed reword + DEFERRED.md:42-resolved commit

title = "Elevation guidance on Unix names the binary's ABSOLUTE path under sudo (a user-local install ~/.local/bin · ~/.cargo/bin is not on sudo's secure_path, so bare `sudo spt` dies 'command not found'); gated commands auto-elevate on an interactive TTY, else print the runnable hint (5.10)"


Run traceable-reqs check to confirm the impl/unit/int evidence is recognized.


Bash

traceable-reqs check 2>&1 | tail -25 || echo "--- trying cargo ---" && cargo run -q -p xtask -- traceable check 2>&1 | tail -25

Run traceable-reqs check


manifest_error: cannot read traceable-reqs.toml (The system cannot find the file specified. (os error 2))
usage: xtask <gen|check|site <outdir>|release-keygen <key-id>|release-sign …|release-publish …|debug-keygen <key-id>|debug-pin …|debug-rollout …|debug-converge --version <u64> …>


cd /c/Users/decid/Documents/projects/spt-core && pwd && ls traceable-reqs.toml && which traceable-reqs 2>/dev/null; echo "---"; traceable-reqs check 2>&1 | tail -20

Run traceable check from repo root


/c/Users/decid/Documents/projects/spt-core
traceable-reqs.toml
/c/Users/decid/AppData/Local/Programs/traceable-reqs/traceable-reqs
---
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).


traceable runs. Check my REQ's status + overall exit.


traceable-reqs check 2>&1 | grep -iE "BROKER-QUIC|PUMP-IPC-DEADLINE|EXIT|FAIL|MISSING|error|summary|passed|coverage" | head -30; echo "exit=${PIPESTATUS[0]}"

Check BROKER-QUIC REQ + overall exit


       Peer address seeding, both cold starts: durable peer-addrs.json (identity dir) maps peer pubkey → last-known dialable address; the pump's resolver consults it FIRST with id-only discovery fallback on miss or dial failure (a stale addr never strands a peer); written by the pairing ceremony (both sides, from the live connection) and by the pump on successful connect; post-join first sync and post-restart resync converge in seconds, not ~1 min (M8 decisions 14, 20)
       Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders.
       Cross-platform self-elevating re-launch for privilege-gated commands: a pure decision seam `decide_elevation_path(os, elevation, interactive_tty, has_display, has_pkexec, has_term_emulator) -> ElevatePath{AlreadyElevated, InlineSudo, UacWindow, Pkexec, TerminalEmulator, PrintHint}` selecting how to re-acquire privilege, and the per-OS impure launchers it dispatches — Windows UAC console (ShellExecuteW `runas` on the abs-exe + verbatim argv; the elevated child does the work, prints 'You can close this window', and pauses for a keypress; the original prints 'Elevated terminal launched…' and exits 0; NEVER pipes the child's stdout back across the privilege boundary), Linux desktop pkexec (preferred, native polkit GUI auth) else x-terminal-emulator -e sudo (fallback list x-terminal-emulator→gnome-terminal→konsole→xterm), the existing interactive-TTY inline sudo, and the headless/no-path floor that prints the absolute-path command. Reused by every gated command (not subnet-specific). Generalizes should_auto_elevate.
  [OK] REQ-HAZARD-BROKER-QUIC-DEADLINE  required: [impl, unit, int]  stages: -doc +impl +unit +int
       The broker bounds every brain-waiting QUIC op (dial / open_stream / send_stream) so a black-holed or dead peer fails PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded await. The bound (< the brain's 30s PUMP_PEER_IO_TIMEOUT so the BROKER fires first) surfaces to the pump as a normal broker error reply → peer_outcome's non-TimedOut arm → drop conn + redial next tick, the round CONTINUES and the heartbeat keeps advancing — it must NEVER manifest as the brain's own read-deadline (the A-half poison → supervised-restart path REQ-HAZARD-PUMP-IPC-DEADLINE guards). Exactly-once is preserved: a timed-out journaled op fails INSIDE its apply_once closure so no phantom conn_id/stream_id is recorded and a fresh tick re-dials cleanly. The happy path is unchanged (a live peer completes with zero added latency; the bound only bites a non-responsive peer). This is the ROOT-cause cure for the 2.2h hfenduleam pump wedge — a dead roster peer whose QUIC path the broker awaited unbounded — recurring on hfenduleam 2026-06-16.
       A surfaced concurrent context pair is durably preserved (both versions, tracked artifacts) until a strictly dominating write clears it; no reconcile failure path discards an unmerged version (6.6, ADR-0013)
       Ephemeral perch cleanup on every ring exit path (3.1)
       The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging.
       A daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure, surfaced like a spawn failure (closes the v0.8.1 residual masking): the REQ-HAZARD-LIVEHOST-BOOT-RACE signal stamps `psyche_host_error` only when `spawn_psyche` returns Err, NOT when the detached spawn() returns Ok but the child dies within moments (e.g. a bad-argv child exiting 2 — the F-009 case). That leaves the residual 'online + no Psyche + no cause' gap: the nested `{id}-psyche` info.json is written status=online with a real-but-DEAD pid and the PARENT perch carries NO psyche_host_error (perri's F-010: tasklist showed 0 host procs across the window while info.json read online). The host MUST confirm RESIDENCY — a hosted child not alive (or whose `{id}-psyche` perch never re-registers / has a dead pid) within N seconds of spawn is treated as a host failure: stamp the parent perch `psyche_host_error{reason:"host not resident within <n>s (psyche perch missing/dead pid)"}` (and do not leave a phantom online nested perch). Closes the last masking gap the v0.8.1 fix left open. perri's F-010 (v0.8.1 dogfood). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE.
  [OK] REQ-HAZARD-PUMP-IPC-DEADLINE  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       Command-template substitution fills argv ELEMENTS, not a re-tokenized string: spt-core currently `fill_template`s {key} values INTO the command STRING and THEN `tokenize`s the filled string (runtime.rs:94/122), so a multi-word {key} value whitespace-SPLITS into multiple argv tokens unless the adapter hand-quotes the placeholder, and a value containing a `"` (or `;`) injects/breaks tokenization (shell-injection-adjacent). A filled value MUST become exactly ONE argv element regardless of spaces/quotes in the value. Fix: tokenize the TEMPLATE into argv FIRST, then `fill_template` EACH token, so a `{key}` slot resolves to a single element and the value never participates in tokenization (no whitespace-split, no quote/semicolon injection); preserve the missing-key / empty-command errors and `{{`/`}}` non-interpretation. perri's F-009 (v0.8.1 dogfood, argv-capture-confirmed): a multi-word `{psyche_prompt}` = "PSYCHE REVIVAL time: epoch-ms:… incoming event: (none)" arrived as argv[6..12] (7 stray tokens), the harness runner strict-parsed `--prompt` against the 2nd word, exited 2 within ~1s → phantom hosted perch. Applies to EVERY [session.<role>] template (psyche_init, extractor, notif, …); digest survives today only because its fills ({session_id}/{source}) are single-token.
       Windows at-logon autostart runs the daemon in the background with no persistent window: the scheduled task launches `spt daemon start` (which spawn_detaches a console-less DETACHED_PROCESS daemon and exits) rather than the foreground `spt daemon run` — Task Scheduler's interactive ONLOGON launch of a long-lived console process otherwise leaves a visible console window for the daemon's whole lifetime (v0.7.4)
       File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
       spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes
       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
       Shell sleep/wake: link-break always closes the binary (pre-close instruction + termination timeout), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)
       Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
exit=0


`[OK] REQ-HAZARD-BROKER-QUIC-DEADLINE required: [impl, unit, int]` — all evidenced, exit=0. Commit 2 (impl+unit+int).


git add crates/spt-daemon/src/nethost.rs crates/spt-daemon/tests/netbroker.rs crates/spt-daemon/tests/pump.rs traceable-reqs.toml && git commit -q -m "v0.8.3 F-B-half: bound broker-side QUIC ops (impl+unit+int)

REQ-HAZARD-BROKER-QUIC-DEADLINE — the deferred B-half of
REQ-HAZARD-PUMP-IPC-DEADLINE. Bound every brain-waiting QUIC await
(dial / open_stream / send_stream) so a black-holed/dead peer fails
PROMPTLY with an ORDINARY error the broker REPLIES, never an unbounded
await that wedges the single-threaded pump (the 2.2h hfenduleam wedge,
recurring 2026-06-16).

- NetHost::bounded_block_on wraps the three QUIC await sites in
  tokio::time::timeout under quic_op_timeout (default 10s =
  BROKER_QUIC_OP_TIMEOUT_MS, < the brain's 30s PUMP_PEER_IO_TIMEOUT so
  the BROKER fires first). On elapse the future is dropped (cancelling
  the in-flight op — nothing half-registered) and a non-TimedOut
  io::Error is returned; at the broker boundary that becomes an ordinary
  error reply frame, so the brain reconstructs ErrorKind::Other and
  peer_outcome takes the per-peer arm (drop conn + redial) — never the
  brain's own read-deadline (the A-half poison/supervised-restart path).
- Exactly-once preserved: the timed-out journaled op fails INSIDE its
  apply_once closure (host.dial/open_stream/send_stream is called there),
  so no phantom conn_id/stream_id is recorded (effect()? before
  applied.insert) and a fresh tick re-dials cleanly.
- Happy path unchanged: a live op completes with zero added latency; the
  bound only bites a non-responsive peer.
- set_quic_op_timeout (mirrors set_roster_exchange, off NetConfig) lets
  the hermetic black-hole tests set a short bound.

Timeout value 10s: comfortably above any legit LAN/relay connect +
seed-proof round-trip (sub-second typical, a few seconds on a slow
relay), 20s below the brain's 30s deadline so the broker always REPLIES
first (margin covers the error frame's IPC transit + brain read
scheduling).

unit (nethost.rs): the wrapper returns a prompt non-TimedOut error on a
never-completing op; a ready op passes through untouched.
int (netbroker.rs): a membership-mismatch black-hole peer -> the broker
REPLIES an ordinary error within the bound (not a wedge) + exactly-once
-on-timeout (journal un-applied, conn table empty, clean redial).
int (pump.rs): the production pump against a black-holing roster peer —
heartbeat monotonic-advances + run_peer_pump exits Ok (no supervised
restart). Pre-fix the dial freezes >=30s; the 20s window catches it.

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

Commit impl+unit+int


cece4a6 v0.8.3 F-B-half: bound broker-side QUIC ops (impl+unit+int)


All evidence green (exit=0). Commit 2 done. Now commit 3 — docs. First update KNOWN-HAZARDS 7.6's two "deferred" mentions to "shipped".


Edit

bounded to ONE thread per actual wedge (the post-restart conn cache is fresh — the dead peer is re-dialed, not re-wedged), diagnosable by the reader's spawn/exit log lines, and fully cured only by the broker-side B-half (DEFERRED.md).

bounded to ONE thread per actual wedge (the post-restart conn cache is fresh — the dead peer is re-dialed, not re-wedged), diagnosable by the reader's spawn/exit log lines, and fully cured by the broker-side B-half (§7.8, `REQ-HAZARD-BROKER-QUIC-DEADLINE`), shipped in v0.8.3 — with the broker bounding its QUIC await, the brain's read returns promptly (an ordinary error) so the reader thread is never left parked.


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

### 7.6 Pump brain-IPC reads must be deadline-bounded (a blocked read wedges the whole pump)  `[REQ-HAZARD-PUMP-IPC-DEADLINE]`

- **Failure:** the peer pump is a SINGLE thread driving every leg (registry/notif/sync/update) against every peer over ONE brain-IPC client. Its reply reads (`net_open_stream`, `net_stream_send`, `net_dial`, and the sync/update pull `read_event` loops) were `loop { read_event() }` with no deadline. When a peer's QUIC path black-holes, the broker's stream-open/send awaits the dead peer and never sends the reply, so the brain's `read_frame` blocks FOREVER and the pump freezes mid-round. The heartbeat (loop-top) stops and the stall warning fires honestly, but `supervise_pump` cannot rescue it: the supervisor catches a panic / error / clean return, and a BLOCKED thread never returns. Observed twice — 2026-06-07, then a **2.2h wedge on hfenduleam (2026-06-11)** where the broker stayed responsive to `daemon status` while the pump thread sat dead.

-- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_timeouts()`) and its `set_nonblocking` uses deprecated `PIPE_NOWAIT`, which corrupts mid-stream (proven by the mesh E2E). Blocking reads work on both OSes; the channel supplies the deadline. A read that exceeds the deadline returns `io::ErrorKind::TimedOut`, treated as a **POISONED client** (a late reply could bind the wrong stream id in a later call — gotcha-#1-adjacent): it BUBBLES out of the round → `run_peer_pump` returns Err → `supervise_pump` restarts the pump with a fresh brain client + conn cache + reset WorkerLasts (the §V4 stagger re-primes every leg). NEVER a per-peer retry. An ordinary error (the broker REPLIED with one) stays a per-peer abort + conn drop + redial. **Leak watch (accepted, not fixed):** a restart abandons the old reader thread parked in `read_frame` (split halves share one OS handle → on a named pipe the broker may never signal the disconnect); bounded to ONE thread per actual wedge (the post-restart conn cache is fresh — the dead peer is re-dialed, not re-wedged), diagnosable by the reader's spawn/exit log lines, and fully cured only by the broker-side B-half (DEFERRED.md).

+- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_timeouts()`) and its `set_nonblocking` uses deprecated `PIPE_NOWAIT`, which corrupts mid-stream (proven by the mesh E2E). Blocking reads work on both OSes; the channel supplies the deadline. A read that exceeds the deadline returns `io::ErrorKind::TimedOut`, treated as a **POISONED client** (a late reply could bind the wrong stream id in a later call — gotcha-#1-adjacent): it BUBBLES out of the round → `run_peer_pump` returns Err → `supervise_pump` restarts the pump with a fresh brain client + conn cache + reset WorkerLasts (the §V4 stagger re-primes every leg). NEVER a per-peer retry. An ordinary error (the broker REPLIED with one) stays a per-peer abort + conn drop + redial. **Leak watch (accepted, not fixed):** a restart abandons the old reader thread parked in `read_frame` (split halves share one OS handle → on a named pipe the broker may never signal the disconnect); bounded to ONE thread per actual wedge (the post-restart conn cache is fresh — the dead peer is re-dialed, not re-wedged), diagnosable by the reader's spawn/exit log lines, and fully cured by the broker-side B-half (§7.8, `REQ-HAZARD-BROKER-QUIC-DEADLINE`), shipped in v0.8.3 — with the broker bounding its QUIC await, the brain's read returns promptly (an ordinary error) so the reader thread is never left parked.

- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound `net_open_stream`/`send`/`dial` handlers) — is deferred to the next broker-update batch (DEFERRED.md).

- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix.


The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound `net_open_stream`/`send`/`dial` handlers) — is deferred to the next broker-update batch (DEFERRED.md).
- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix.

The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).
- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.


The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound `net_open_stream`/`send`/`dial` handlers) — is deferred to the next broker-update batch (DEFERRED.md).
- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix.

The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).
- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.

- **Invariant:** in PUMP mode the brain carrier is **SPLIT at construction** — a dedicated `pump-ipc-reader` thread does blocking `read_frame` on the `RecvHalf` and forwards each framed result down a channel; the main thread writes on the `SendHalf` and reads with `Receiver::recv_timeout`. Every IPC reply read is bounded by a per-call **total-wait** deadline (`PUMP_PEER_IO_TIMEOUT` = 30s, > any legitimate round-trip, < the 60s QUIC idle; re-armed on stream progress for the streaming pull legs so a healthy long sync is never killed — only a ≥30s silence). **Mechanism note (Windows):** the carrier is a reader-thread + channel, NOT a non-blocking socket + poll — interprocess 2.4.2 on Windows named pipes has no portable read timeout (`set_recv_timeout` → `no_timeouts()`) and its `set_nonblocking` uses deprecated `PIPE_NOWAIT`, which corrupts mid-stream (proven by the mesh E2E). Blocking reads work on both OSes; the channel supplies the deadline. A read that exceeds the deadline returns `io::ErrorKind::TimedOut`, treated as a **POISONED client** (a late reply could bind the wrong stream id in a later call — gotcha-#1-adjacent): it BUBBLES out of the round → `run_peer_pump` returns Err → `supervise_pump` restarts the pump with a fresh brain client + conn cache + reset WorkerLasts (the §V4 stagger re-primes every leg). NEVER a per-peer retry. An ordinary error (the broker REPLIED with one) stays a per-peer abort + conn drop + redial. **Leak watch (accepted, not fixed):** a restart abandons the old reader thread parked in `read_frame` (split halves share one OS handle → on a named pipe the broker may never signal the disconnect); bounded to ONE thread per actual wedge (the post-restart conn cache is fresh — the dead peer is re-dialed, not re-wedged), diagnosable by the reader's spawn/exit log lines, and fully cured by the broker-side B-half (§7.8, `REQ-HAZARD-BROKER-QUIC-DEADLINE`), shipped in v0.8.3 — with the broker bounding its QUIC await, the brain's read returns promptly (an ordinary error) so the reader thread is never left parked.

-- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound `net_open_stream`/`send`/`dial` handlers) — is deferred to the next broker-update batch (DEFERRED.md).

-- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix.

+- **spt-core mapping:** `Brain::cold_start_pump` (splits the carrier + arms the deadline) / `BrainConn::Split` (the `SendHalf` + reader-thread channel) / `call_deadline` / `read_event_until` / `read_frame_until` (the `recv_timeout` dispatch), `pump::run_peer_pump` (connects in pump mode) + `pump::peer_outcome` (the tier-split), the `request_sync`/`request_update` pull loops (deadline re-armed on progress). The broker-side half — the broker must never make a brain wait unbounded on a QUIC op (bound the `net_dial`/`open_stream`/`send_stream` handlers) — shipped in v0.8.3 as §7.8 (`REQ-HAZARD-BROKER-QUIC-DEADLINE`).

+- **Source:** field diagnosis 2026-06-11 (the 2.2h hfenduleam wedge); doyle ruling A-now / B-deferred. The stall warning (M8 decision 23) was the band-aid; this is the fix. The B-half landed v0.8.3 (§7.8) after the 2026-06-16 recurrence.

### 7.7 A slow/dead/hostile remote VIEWER must never stall the controller, child, or drain  `[REQ-HAZARD-VIEWER-ISOLATION]`


Add the new §7.8 subsection (after 7.7, before the `---`):


- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).

---

## Conformance checklist (condensed)

- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).

### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`
<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->
- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).
- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-peer arm (drop conn + redial next tick), the round CONTINUES and the heartbeat keeps advancing. **Never the brain's read-deadline:** that `TimedOut` is the 7.6 poison → supervised-restart path this fix exists precisely to AVOID. **Exactly-once preserved:** a timed-out journaled op fails INSIDE its `apply_once` closure (the QUIC call is made there), so no phantom `conn_id`/`stream_id` is recorded (`effect()?` propagates before `applied.insert`) and a fresh tick re-dials cleanly — no dedupe into a dead conn. **Happy path unchanged:** a live peer completes with zero added latency; the bound only bites a non-responsive peer.
- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run_peer_pump` exits Ok, no supervised restart). The `peer_outcome` ordinary-vs-`TimedOut` tier-split itself is unit-covered at 7.6.
- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.

---

## Conformance checklist (condensed)


- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).

---

## Conformance checklist (condensed)

- **Source:** M12 W2.5 controller/viewer model (doyle ruling 2026-06-14, Q1).

### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`
<!-- [doc->REQ-HAZARD-BROKER-QUIC-DEADLINE] -->
- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).
- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-peer arm (drop conn + redial next tick), the round CONTINUES and the heartbeat keeps advancing. **Never the brain's read-deadline:** that `TimedOut` is the 7.6 poison → supervised-restart path this fix exists precisely to AVOID. **Exactly-once preserved:** a timed-out journaled op fails INSIDE its `apply_once` closure (the QUIC call is made there), so no phantom `conn_id`/`stream_id` is recorded (`effect()?` propagates before `applied.insert`) and a fresh tick re-dials cleanly — no dedupe into a dead conn. **Happy path unchanged:** a live peer completes with zero added latency; the bound only bites a non-responsive peer.
- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run_peer_pump` exits Ok, no supervised restart). The `peer_outcome` ordinary-vs-`TimedOut` tier-split itself is unit-covered at 7.6.
- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.

---

## Conformance checklist (condensed)

- **spt-core mapping:** `OutputLog::append` (controller blocking + `viewer_send_evicts` `try_send` fan-out), `OutputLog::add_viewer` (bounded `sync_channel(VIEWER_CHANNEL_DEPTH)` + `viewer_writer` thread), `MAX_VIEWERS` soft cap, `ViewerSink`. Unit: `viewer_overflow_or_disconnect_evicts_never_blocks`. Int: `wedged_viewer_does_not_stall_controller` (a non-reading viewer is evicted while the controller keeps receiving past a 200KB burst).

+### 7.8 The broker must never make a brain wait UNBOUNDED on a QUIC op (the pump-IPC-deadline B-half)  `[REQ-HAZARD-BROKER-QUIC-DEADLINE]`

+- **Failure:** the broker's brain-facing QUIC handlers (`dispatch_net_dial` / `dispatch_net_stream_open` / `dispatch_net_stream_send`) call into `NetHost::dial` / `open_stream` / `send_stream`, whose iroh awaits (`endpoint.connect` + `prove_membership`; `open_bi`; `write_all`/`finish`) had NO bound of their own. A dead/black-holed roster peer (its process gone, or a mixed-pair that accepts the conn but never answers the seed-proof) makes the broker await its QUIC path FOREVER, so the brain escapes only via its OWN 30s read-deadline (the 7.6 A-half) — a 30s stall + a full pump restart EACH round, and the supervised restart re-dials the SAME dead peer and re-wedges. Root cause of the 2.2h hfenduleam wedge (2026-06-11) and its recurrence (2026-06-16).

+- **Invariant:** every brain-waiting QUIC op is wrapped in a broker-side deadline (`NetHost::bounded_block_on` → `tokio::time::timeout`, `BROKER_QUIC_OP_TIMEOUT_MS` = 10s). On elapse the future is DROPPED (cancelling the in-flight connect/stream op, so nothing is half-registered) and a non-`TimedOut` `io::Error` is returned, which the broker REPLIES as an ordinary error frame. The bound (10s) sits comfortably above any legitimate LAN/relay round-trip and 20s below the brain's 30s `PUMP_PEER_IO_TIMEOUT`, so the BROKER fires FIRST — the brain reconstructs `ErrorKind::Other` (its `net_dial`/`net_open_stream`/`net_stream_send` map a broker error reply to `io::Error::other`), NOT its own read-deadline `TimedOut`, so `pump::peer_outcome` takes the ordinary per-peer arm (drop conn + redial next tick), the round CONTINUES and the heartbeat keeps advancing. **Never the brain's read-deadline:** that `TimedOut` is the 7.6 poison → supervised-restart path this fix exists precisely to AVOID. **Exactly-once preserved:** a timed-out journaled op fails INSIDE its `apply_once` closure (the QUIC call is made there), so no phantom `conn_id`/`stream_id` is recorded (`effect()?` propagates before `applied.insert`) and a fresh tick re-dials cleanly — no dedupe into a dead conn. **Happy path unchanged:** a live peer completes with zero added latency; the bound only bites a non-responsive peer.

+- **spt-core mapping:** `NetHost::bounded_block_on` (the timeout wrapper) wrapping `NetHost::dial` / `open_stream` (QUIC branch) / `send_stream`; `BROKER_QUIC_OP_TIMEOUT_MS` + `set_quic_op_timeout` (test override, off `NetConfig` — mirrors `set_roster_exchange`). Unit: `bounded_block_on_cuts_a_never_completing_op_with_an_ordinary_error` (a never-completing op → prompt non-`TimedOut` error; a ready op untouched). Int: `dial_to_a_black_holing_peer_fails_with_a_bounded_ordinary_error` (the broker REPLIES an ordinary error within the bound + exactly-once-on-timeout: journal un-applied, conn table empty, clean redial) and `pump_survives_a_black_holing_peer_heartbeat_advances_no_restart` (the production pump against a dead peer — heartbeat monotonic-advances, `run_peer_pump` exits Ok, no supervised restart). The `peer_outcome` ordinary-vs-`TimedOut` tier-split itself is unit-covered at 7.6.

+- **Source:** the 7.6 B-half — deferred 2026-06-11 (doyle ruling A-now / B-deferred, DEFERRED.md) and shipped in v0.8.3 after the 2026-06-16 hfenduleam recurrence.


Add the conformance-checklist row for 7.8 (next to the 7.6 row):


| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |

| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |
| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |


| 7.6 | Pump brain-IPC reads deadline-bounded (30s total-wait); TimedOut → supervised restart, never per-peer retry | `Brain::cold_start_pump` / `BrainConn::Split` (reader-thread + `recv_timeout`), `pump::peer_outcome` |
| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |

| 7.4 | Per-agent pulse/psyche/echo runs off the shared scheduler (no serial blocking across agents) | daemon multi-agent hosting (ADR-0004) |

| 7.5 | WAN-inbound origin = QUIC handshake identity from the broker's stream table, never payload bytes | wan receive funnel + every wire-inbound consumer (ADR-0009) |

+| 7.8 | Broker bounds every brain-waiting QUIC op (10s < the brain's 30s); a dead peer fails as an ORDINARY error the broker replies → per-peer redial, round continues, no pump restart (the 7.6 B-half) | `NetHost::bounded_block_on` wrapping `dial`/`open_stream`/`send_stream` |


Mark DEFERRED.md:42 RESOLVED:


| **Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)**: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes,

| **✅ RESOLVED — shipped v0.8.3 (REQ-HAZARD-BROKER-QUIC-DEADLINE, KNOWN-HAZARDS 7.8).** ~~Broker-side bound on brain-waiting QUIC ops (the pump-IPC-deadline B-half)~~: the brain-side deadline (REQ-HAZARD-PUMP-IPC-DEADLINE, KNOWN-HAZARDS 7.6) stops the single-threaded pump wedging when a peer black-holes,