grep -n "pub fn parse\|pub struct Address" -A 10 crates/spt-proto/src/addr.rs | head -25; grep -n "spt-daemon\|spt-net" crates/spt/Cargo.toml

Address parser + spt deps


54:pub struct Address {
55-    /// The optional subnet qualifier (before `:`).
56-    pub subnet: Option<String>,
57-    /// The bare endpoint id.
58-    pub id: String,
59-    /// The optional node qualifier (after `@`).
60-    pub node: Option<String>,
61-}
62-
63-impl Address {
64-    /// Parse `[subnet:]id[@node]`. Every present component is validated against the id
--
68:    pub fn parse(s: &str) -> Result<Address, AddrError> {
69-        // Split the node qualifier off the end (`@` is after the id). `rsplit` so a
70-        // stray earlier `@` stays in `head` and is caught when `head`/id is validated.
71-        let (head, node) = match s.rfind('@') {
72-            Some(i) => (&s[..i], Some(&s[i + 1..])),
73-            None => (s, None),
74-        };
75-        // Split the subnet qualifier off the front (`:` is before the id). First `:` —
76-        // a second one lands in the id and is rejected by the charset validator.
77-        let (subnet, id) = match head.find(':') {
78-            Some(i) => (Some(&head[..i]), &head[i + 1..]),
18:spt-daemon = { path = "../spt-daemon" }
22:spt-net = { path = "../spt-net" }


Bash

grep -n "mod \|use " crates/spt/src/main.rs | head -20; grep -rn "fn read_stdin\|use spt_msg::deliver" crates/spt/src/cli.rs | head -5

spt crate module structure


10:mod api;
11:mod cli;
12:mod elevation;
13:mod roster;
16:mod testutil;
17:use spt_msg::deliver::{self, SendOutcome};
322:fn read_stdin() -> String {


Write

//! `spt send`'s WAN leg (D9-1-4) — the D5 deferral: "`spt send` grows the
//! off-node leg transparently."
//!
//! When local delivery finds no perch, the CLI consults the daemon's
//! registry **snapshots** (`identity/registry/<subnet>.json`, the
//! [`spt_daemon::registryhost::RegistryHost`] mirror — stale-tolerant by
//! contract, KNOWN-HAZARDS 4.3), resolves the target under the D3c policy
//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a
//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to
//! the resolved node. The receiving daemon's dispatcher funnels it through
//! the access gate + op-id dedup into the target's local delivery
//! (`spt-daemon::wan::receive_wan`).
//!
//! Deferred sends stay local-only in v1: the wire record carries no
//! deferred class, and inventing one is a protocol change — the seam is
//! documented, not faked.
// [impl->REQ-NET-1]
// [impl->REQ-INST-10]

use std::collections::BTreeMap;
use std::path::Path;

use spt_daemon::brain::Brain;
use spt_daemon::endpoint::broker_socket_name;
use spt_net::net::endpoint::addr_for_node_hex;
use spt_net::net::ndjson;
use spt_net::net::registry::{resolve_across_visible, Resolution, SubnetRegistry};
use spt_net::net::wanmsg::WanMessage;
use spt_proto::addr::Address;
use spt_store::epoch::EpochSource;
use spt_store::perch;
use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;

/// What the WAN leg did — the CLI's exit-path discriminant.
#[derive(Debug)]
pub enum WanSendOutcome {
    /// Shipped to the resolved node (hex). Delivery/spool happens there.
    Sent { node: String },
    /// Resolution refused — the message says which qualifier disambiguates.
    Ambiguous(String),
    /// No routable remote instance in any visible subnet.
    NotFound,
    /// Resolution succeeded but the wire leg failed (daemon down, dial
    /// failed, peer unreachable).
    Failed(String),
}

/// Production entry: resolve `target` against the canonical snapshots and
/// ship over the canonical broker. `target` accepts `[subnet:]id[@node]`.
pub fn wan_send(target: &str, from: &str, body: &str) -> WanSendOutcome {
    let own_hex = match spt_store::nodeid::load_or_create() {
        Ok(id) => id.public_key().to_hex(),
        Err(e) => return WanSendOutcome::Failed(format!("node identity: {e}")),
    };
    let regs = load_snapshots(&perch::identity_dir().join("registry"));
    if spt_daemon::daemon::ensure_running().is_err() {
        return WanSendOutcome::Failed("daemon did not come up".to_string());
    }
    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {
        Ok(b) => b,
        Err(e) => return WanSendOutcome::Failed(format!("broker connect: {e}")),
    };
    let mut ops = EpochSource::load_from(&perch::identity_dir().join("cli-ops.json"));
    let op_id = match ops.next_epoch() {
        Ok(e) => format!("{own_hex}:cli:{e}"),
        Err(e) => return WanSendOutcome::Failed(format!("op counter: {e}")),
    };
    wan_send_with(&mut brain, &regs, &own_hex, target, from, body, &op_id, |hex| {
        addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok())
    })
}

/// The testable core: resolve against `regs`, dial via `resolver`, write one
/// `WanMessage` line, finish the stream.
#[allow(clippy::too_many_arguments)]
pub fn wan_send_with(
    brain: &mut Brain,
    regs: &BTreeMap<String, SubnetRegistry>,
    own_hex: &str,
    target: &str,
    from: &str,
    body: &str,
    op_id: &str,
    resolver: impl Fn(&str) -> Option<serde_json::Value>,
) -> WanSendOutcome {
    let address = match Address::parse(target) {
        Ok(a) => a,
        Err(e) => return WanSendOutcome::Failed(format!("bad address: {e:?}")),
    };

    // The same exclusion rule the registry's advertisement side enforces
    // (ADR-0006 §6): hidden = not routable, even via an explicit pin.
    let subnets = SubnetStore::load();
    let vis = VisibilityStore::load();
    let hide_new: BTreeMap<String, bool> =
        subnets.subnets.iter().map(|s| (s.name.clone(), s.hide_new_endpoints)).collect();
    let excluded =
        |subnet: &str, id: &str| vis.hidden(id, subnet, *hide_new.get(subnet).unwrap_or(&false));

    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {
        Resolution::Resolved { instance, .. } => instance,
        Resolution::Ambiguous(a) => return WanSendOutcome::Ambiguous(format!("{a:?}")),
        Resolution::NotFound => return WanSendOutcome::NotFound,
    };
    if instance.node == own_hex {
        // A local instance resolves here only when the local perch scan
        // missed it (registry staleness) — WAN-dialing ourselves helps
        // nobody; let the caller report the local outcome.
        return WanSendOutcome::NotFound;
    }

    let Some(addr) = resolver(&instance.node) else {
        return WanSendOutcome::Failed(format!("no dialable address for {}", instance.node));
    };
    let msg = WanMessage {
        target: address.id.clone(),
        from: from.to_string(),
        body: body.to_string(),
        op_id: op_id.to_string(),
    };
    let send = || -> std::io::Result<()> {
        let conn = brain.net_dial(addr, None)?;
        let opened = brain.net_open_stream(conn.conn_id, None)?;
        brain.net_stream_send(opened.stream_id, &ndjson::encode_line(&msg), None, true)?;
        Ok(())
    };
    match send() {
        Ok(()) => WanSendOutcome::Sent { node: instance.node },
        Err(e) => WanSendOutcome::Failed(e.to_string()),
    }
}

/// Load every per-subnet registry snapshot under `dir`. Missing dir or an
/// unparseable snapshot degrade to absent (the reader's stale-tolerant
/// contract) — resolution then simply finds nothing there.
pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
    let mut out = BTreeMap::new();
    let Ok(entries) = std::fs::read_dir(dir) else {
        return out;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }
        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { continue };
        let Ok(raw) = std::fs::read_to_string(&path) else { continue };
        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else { continue };
        out.insert(stem.to_string(), reg);
    }
    out
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}


File created successfully at: C:\Users\decid\Documents\projects\spt-core\crates\spt\src\wansend.rs (file state is current in your context — no need to Read it back)

//! `spt send`'s WAN leg (D9-1-4) — the D5 deferral: "`spt send` grows the
//! off-node leg transparently."
//!
//! When local delivery finds no perch, the CLI consults the daemon's
//! registry **snapshots** (`identity/registry/<subnet>.json`, the
//! [`spt_daemon::registryhost::RegistryHost`] mirror — stale-tolerant by
//! contract, KNOWN-HAZARDS 4.3), resolves the target under the D3c policy
//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a
//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to
//! the resolved node. The receiving daemon's dispatcher funnels it through
//! the access gate + op-id dedup into the target's local delivery
//! (`spt-daemon::wan::receive_wan`).
//!
//! Deferred sends stay local-only in v1: the wire record carries no
//! deferred class, and inventing one is a protocol change — the seam is
//! documented, not faked.
// [impl->REQ-NET-1]
// [impl->REQ-INST-10]

use std::collections::BTreeMap;
use std::path::Path;

use spt_daemon::brain::Brain;
use spt_daemon::endpoint::broker_socket_name;
use spt_net::net::endpoint::addr_for_node_hex;
use spt_net::net::ndjson;
use spt_net::net::registry::{resolve_across_visible, Resolution, SubnetRegistry};
use spt_net::net::wanmsg::WanMessage;
use spt_proto::addr::Address;
use spt_store::epoch::EpochSource;
use spt_store::perch;
use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;

/// What the WAN leg did — the CLI's exit-path discriminant.
#[derive(Debug)]
pub enum WanSendOutcome {
    /// Shipped to the resolved node (hex). Delivery/spool happens there.
    Sent { node: String },
    /// Resolution refused — the message says which qualifier disambiguates.
    Ambiguous(String),
    /// No routable remote instance in any visible subnet.
    NotFound,
    /// Resolution succeeded but the wire leg failed (daemon down, dial
    /// failed, peer unreachable).
    Failed(String),
}

/// Production entry: resolve `target` against the canonical snapshots and
/// ship over the canonical broker. `target` accepts `[subnet:]id[@node]`.
pub fn wan_send(target: &str, from: &str, body: &str) -> WanSendOutcome {
    let own_hex = match spt_store::nodeid::load_or_create() {
        Ok(id) => id.public_key().to_hex(),
        Err(e) => return WanSendOutcome::Failed(format!("node identity: {e}")),
    };
    let regs = load_snapshots(&perch::identity_dir().join("registry"));
    if spt_daemon::daemon::ensure_running().is_err() {
        return WanSendOutcome::Failed("daemon did not come up".to_string());
    }
    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {
        Ok(b) => b,
        Err(e) => return WanSendOutcome::Failed(format!("broker connect: {e}")),
    };
    let mut ops = EpochSource::load_from(&perch::identity_dir().join("cli-ops.json"));
    let op_id = match ops.next_epoch() {
        Ok(e) => format!("{own_hex}:cli:{e}"),
        Err(e) => return WanSendOutcome::Failed(format!("op counter: {e}")),
    };
    wan_send_with(&mut brain, &regs, &own_hex, target, from, body, &op_id, |hex| {
        addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok())
    })
}

/// The testable core: resolve against `regs`, dial via `resolver`, write one
/// `WanMessage` line, finish the stream.
#[allow(clippy::too_many_arguments)]
pub fn wan_send_with(
    brain: &mut Brain,
    regs: &BTreeMap<String, SubnetRegistry>,
    own_hex: &str,
    target: &str,
    from: &str,
    body: &str,
    op_id: &str,
    resolver: impl Fn(&str) -> Option<serde_json::Value>,
) -> WanSendOutcome {
    let address = match Address::parse(target) {
        Ok(a) => a,
        Err(e) => return WanSendOutcome::Failed(format!("bad address: {e:?}")),
    };

    // The same exclusion rule the registry's advertisement side enforces
    // (ADR-0006 §6): hidden = not routable, even via an explicit pin.
    let subnets = SubnetStore::load();
    let vis = VisibilityStore::load();
    let hide_new: BTreeMap<String, bool> =
        subnets.subnets.iter().map(|s| (s.name.clone(), s.hide_new_endpoints)).collect();
    let excluded =
        |subnet: &str, id: &str| vis.hidden(id, subnet, *hide_new.get(subnet).unwrap_or(&false));

    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {
        Resolution::Resolved { instance, .. } => instance,
        Resolution::Ambiguous(a) => return WanSendOutcome::Ambiguous(format!("{a:?}")),
        Resolution::NotFound => return WanSendOutcome::NotFound,
    };
    if instance.node == own_hex {
        // A local instance resolves here only when the local perch scan
        // missed it (registry staleness) — WAN-dialing ourselves helps
        // nobody; let the caller report the local outcome.
        return WanSendOutcome::NotFound;
    }

    let Some(addr) = resolver(&instance.node) else {
        return WanSendOutcome::Failed(format!("no dialable address for {}", instance.node));
    };
    let msg = WanMessage {
        target: address.id.clone(),
        from: from.to_string(),
        body: body.to_string(),
        op_id: op_id.to_string(),
    };
    let send = || -> std::io::Result<()> {
        let conn = brain.net_dial(addr, None)?;
        let opened = brain.net_open_stream(conn.conn_id, None)?;
        brain.net_stream_send(opened.stream_id, &ndjson::encode_line(&msg), None, true)?;
        Ok(())
    };
    match send() {
        Ok(()) => WanSendOutcome::Sent { node: instance.node },
        Err(e) => WanSendOutcome::Failed(e.to_string()),
    }
}

/// Load every per-subnet registry snapshot under `dir`. Missing dir or an
/// unparseable snapshot degrade to absent (the reader's stale-tolerant
/// contract) — resolution then simply finds nothing there.
pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
    let mut out = BTreeMap::new();
    let Ok(entries) = std::fs::read_dir(dir) else {
        return out;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }
        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { continue };
        let Ok(raw) = std::fs::read_to_string(&path) else { continue };
        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else { continue };
        out.insert(stem.to_string(), reg);
    }
    out
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}


Edit

mod api;
mod cli;
mod elevation;
mod roster;

mod api;
mod cli;
mod elevation;
mod roster;
mod wansend;


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1	//! `spt` — the single spt-core binary. A **thin** command layer over `spt-msg`:
2	//! it owns I/O (stdin/stdout/stderr) and process exit codes; all messaging
3	//! logic lives in the crate (clean-room separation per the M1 plan).
4	//!
5	//! Subcommands (REQ-MSG-2 stable surface): `send` · `ring` · `ready` · `poll` ·
6	//! `list` · `stop` · `whoami`. The harness-contract inbound surface lives under
7	//! the `api` group (REQ-API-1), dispatched from [`api`].
8	// [impl->REQ-MSG-2]
9
10	mod api;
11	mod cli;
12	mod elevation;
13	mod roster;
14
15	#[cfg(test)]
16	mod testutil;
17
18	fn main() {
19	    let code = cli::run();
20	    std::process::exit(code);
21	}
22

//! `spt` — the single spt-core binary. A **thin** command layer over `spt-msg`:
//! it owns I/O (stdin/stdout/stderr) and process exit codes; all messaging
//! logic lives in the crate (clean-room separation per the M1 plan).
//!
//! Subcommands (REQ-MSG-2 stable surface): `send` · `ring` · `ready` · `poll` ·
//! `list` · `stop` · `whoami`. The harness-contract inbound surface lives under
//! the `api` group (REQ-API-1), dispatched from [`api`].
// [impl->REQ-MSG-2]

mod api;
mod cli;
mod elevation;
mod roster;

#[cfg(test)]
mod testutil;

fn main() {
    let code = cli::run();
    std::process::exit(code);
}


mod api;
mod cli;
mod elevation;
mod roster;

mod api;
mod cli;
mod elevation;
mod roster;
mod wansend;


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

mod api;
mod cli;
mod elevation;
mod roster;
mod wansend;

//! `spt` — the single spt-core binary. A **thin** command layer over `spt-msg`:
//! it owns I/O (stdin/stdout/stderr) and process exit codes; all messaging
//! logic lives in the crate (clean-room separation per the M1 plan).
//!
//! Subcommands (REQ-MSG-2 stable surface): `send` · `ring` · `ready` · `poll` ·
//! `list` · `stop` · `whoami`. The harness-contract inbound surface lives under
//! the `api` group (REQ-API-1), dispatched from [`api`].
// [impl->REQ-MSG-2]

mod api;
mod cli;
mod elevation;
mod roster;

#[cfg(test)]
mod testutil;

fn main() {
    let code = cli::run();
    std::process::exit(code);
}

mod cli;
mod elevation;
mod roster;
+mod wansend;
#[cfg(test)]
mod testutil;


Wiring the WAN fallback into `cmd_send`:


        SendOutcome::NoPerch => {
            eprintln!("NO_PERCH:{} is not listening", target);
            1
        }
        SendOutcome::Empty => {
            eprintln!("EMPTY_MSG: refusing to send empty message to {}", target);
            1
        }
    }
}

        SendOutcome::NoPerch => {
            if deferred {
                // The wire record carries no deferred class (v1) — a remote
                // deferred send is a documented seam, not a silent reclass.
                eprintln!("NO_PERCH:{} is not listening (deferred stays local-only)", target);
                return 1;
            }
            // The WAN leg (D9-1-4): resolve against the registry snapshots
            // and ship to the instance's node; its daemon delivers/spools.
            // [impl->REQ-NET-1]
            match crate::wansend::wan_send(&target, &from, &body) {
                crate::wansend::WanSendOutcome::Sent { node } => {
                    eprintln!(
                        "{}:{}@{}",
                        if is_reply { "REPLIED(WAN)" } else { "SENT(WAN)" },
                        target,
                        node
                    );
                    0
                }
                crate::wansend::WanSendOutcome::Ambiguous(why) => {
                    eprintln!("AMBIGUOUS:{target} — qualify with subnet: or @node ({why})");
                    1
                }
                crate::wansend::WanSendOutcome::NotFound => {
                    eprintln!("NO_PERCH:{} is not listening", target);
                    1
                }
                crate::wansend::WanSendOutcome::Failed(e) => {
                    eprintln!("WAN_FAIL:{target} — {e}");
                    1
                }
            }
        }
        SendOutcome::Empty => {
            eprintln!("EMPTY_MSG: refusing to send empty message to {}", target);
            1
        }
    }
}


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

        SendOutcome::NoPerch => {
            eprintln!("NO_PERCH:{} is not listening", target);
            1
        }
        SendOutcome::Empty => {
            eprintln!("EMPTY_MSG: refusing to send empty message to {}", target);
            1
        }
    }
}

        SendOutcome::NoPerch => {
            if deferred {
                // The wire record carries no deferred class (v1) — a remote
                // deferred send is a documented seam, not a silent reclass.
                eprintln!("NO_PERCH:{} is not listening (deferred stays local-only)", target);
                return 1;
            }
            // The WAN leg (D9-1-4): resolve against the registry snapshots
            // and ship to the instance's node; its daemon delivers/spools.
            // [impl->REQ-NET-1]
            match crate::wansend::wan_send(&target, &from, &body) {
                crate::wansend::WanSendOutcome::Sent { node } => {
                    eprintln!(
                        "{}:{}@{}",
                        if is_reply { "REPLIED(WAN)" } else { "SENT(WAN)" },
                        target,
                        node
                    );
                    0
                }
                crate::wansend::WanSendOutcome::Ambiguous(why) => {
                    eprintln!("AMBIGUOUS:{target} — qualify with subnet: or @node ({why})");
                    1
                }
                crate::wansend::WanSendOutcome::NotFound => {
                    eprintln!("NO_PERCH:{} is not listening", target);
                    1
                }
                crate::wansend::WanSendOutcome::Failed(e) => {
                    eprintln!("WAN_FAIL:{target} — {e}");
                    1
                }
            }
        }
        SendOutcome::Empty => {
            eprintln!("EMPTY_MSG: refusing to send empty message to {}", target);
            1
        }
    }
}

-            eprintln!("NO_PERCH:{} is not listening", target);

+                // The wire record carries no deferred class (v1) — a remote

+                // deferred send is a documented seam, not a silent reclass.

+                eprintln!("NO_PERCH:{} is not listening (deferred stays local-only)", target);

+            // The WAN leg (D9-1-4): resolve against the registry snapshots

+            // and ship to the instance's node; its daemon delivers/spools.

+            match crate::wansend::wan_send(&target, &from, &body) {

+                crate::wansend::WanSendOutcome::Sent { node } => {

+                        if is_reply { "REPLIED(WAN)" } else { "SENT(WAN)" },

+                crate::wansend::WanSendOutcome::Ambiguous(why) => {

+                    eprintln!("AMBIGUOUS:{target} — qualify with subnet: or @node ({why})");

+                crate::wansend::WanSendOutcome::NotFound => {

+                    eprintln!("NO_PERCH:{} is not listening", target);

+                crate::wansend::WanSendOutcome::Failed(e) => {

+                    eprintln!("WAN_FAIL:{target} — {e}");


D9-1: production trigger loops + VerifyPolicy + spt send WAN CLI

peerloop.rs pump host (sync/update/notif/reconcile thin calls over existing drivers), DaemonConfig knobs, production VerifyPolicy assembly, spt send WAN leg, loopback E2E proving loops fire undriven. Gotchas #1/#9.

D9-2: resting-state model + transition echo + wake-edge resurface

State machine (active→dormant→suspended, wake), Status::Suspended, transition echo commune (KH 3.3), resurface_at_boundary(Wake), freshness pull trigger. REQ-INST-3/4 → [impl,unit]; KH 7.4 if multi-agent echo scheduling.

D9-3: dormancy budget measurement + warm/cold policy lock

SPT_BUDGET=1 harness on real adapters (both rig hosts), docs/DORMANCY-BUDGET.md, policy lock, ADR-0003 #9 closed. doc→REQ-INST-3.

D9-4: REQ-INST-14 resource advertisement

resources field on Instance, spt resources set/show/list projection (visibility-gated). [doc,impl,unit].

D9-5: REQ-INST-15 immutable home subnet + spt fork

Home-at-creation (auto/ask/local-until-join, no setter), sync_subnets=[home] seed, adapter-at-creation rule, spt fork copy-then-diverge (--delete-source; same-node v1). [doc,impl,unit].

D9-6: two-host E2E + runbook + gh-repo interim retirement

SPT_TWO_HOST=1 int tests (full ladder), docs/TWO-HOST-RUNBOOK.md, run on rig, int stage activations, retire gh-repo-sync interim.

D9-7: closeout sweep

required_stages audit, traceable-reqs green, ROADMAP M4-delivered, CONTEXT amendments, plan-status COMPLETE, CI matrix FINAL green.


Bash

cargo check -p spt --all-targets 2>&1 | grep -E "^error|^warning" -A 6 | head -30; echo DONE

Check spt crate compiles


error[E0433]: failed to resolve: use of unresolved module or unlinked crate `serde_json`
  --> crates\spt\src\wansend.rs:70:45
   |
70 |         addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok())
   |                                             ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json`
   |
   = help: if you wanted to use a crate named `serde_json`, use `cargo add serde_json` to add it to your `Cargo.toml`
--
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `serde_json`
   --> crates\spt\src\wansend.rs:149:23
    |
149 |         let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else { continue };
    |                       ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json`
    |
    = help: if you wanted to use a crate named `serde_json`, use `cargo add serde_json` to add it to your `Cargo.toml`
--
error[E0433]: failed to resolve: use of unresolved module or unlinked crate `serde_json`
  --> crates\spt\src\wansend.rs:85:39
   |
85 |     resolver: impl Fn(&str) -> Option<serde_json::Value>,
   |                                       ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json`
   |
   = help: if you wanted to use a crate named `serde_json`, use `cargo add serde_json` to add it to your `Cargo.toml`
--
error: could not compile `spt` (bin "spt") due to 3 previous errors
warning: build failed, waiting for other jobs to finish...
error: could not compile `spt` (bin "spt" test) due to 3 previous errors
DONE


grep -n "dependencies" -A 15 crates/spt/Cargo.toml | head -22

spt Cargo.toml deps


12:[dependencies]
13-spt-proto = { path = "../spt-proto" }
14-spt-store = { path = "../spt-store" }
15-spt-msg = { path = "../spt-msg" }
16-spt-runtime = { path = "../spt-runtime" }
17-spt-live = { path = "../spt-live" }
18-spt-daemon = { path = "../spt-daemon" }
19-# The reference binary ships the WAN transport on by default (ADR-0002). D2g reuses its
20-# RFC-6238 TotpSeed (spt_net::net::pairing::totp) to render a subnet's local pairing
21-# code — reused, not reimplemented, so the CLI code matches the ceremony's exactly.
22-spt-net = { path = "../spt-net" }
23-clap = { version = "4", features = ["derive"] }
24-
25-# OS privilege-elevation probe for the node-bound code fetch (REQ-PAIR-6). Only the
26-# binary pulls the OS deps; the core stays dep-minimal. Matches the workspace's existing
27-# libc/windows-sys usage (spt-msg, spt-store) rather than the heavyweight `windows`.
28:[target.'cfg(unix)'.dependencies]
29-libc = "0.2"
30-
31:[target.'cfg(windows)'.dependencies]
32-windows-sys = { version = "0.59", features = [
33-    "Win32_Foundation",


grep -n "serde_json" Cargo.toml crates/spt-daemon/Cargo.toml | head -4

Workspace serde_json version convention


crates/spt-daemon/Cargo.toml:27:serde_json = { version = "1", features = ["preserve_order"] }


Edit

spt-net = { path = "../spt-net" }
clap = { version = "4", features = ["derive"] }

spt-net = { path = "../spt-net" }
clap = { version = "4", features = ["derive"] }
# The WAN send leg (D9-1-4) serializes the dial address + reads registry
# snapshots — same pinned shape as spt-daemon's.
serde_json = { version = "1", features = ["preserve_order"] }


Bash

python -c "
import re
p = 'crates/spt/Cargo.toml'
s = open(p, encoding='utf-8').read()
old = 'spt-net = { path = \"../spt-net\" }\nclap = { version = \"4\", features = [\"derive\"] }'
new = old + '\n# The WAN send leg (D9-1-4) serializes the dial address + reads registry\n# snapshots - same pinned shape as spt-daemon\'s.\nserde_json = { version = \"1\", features = [\"preserve_order\"] }'
assert old in s
open(p, 'w', encoding='utf-8', newline='').write(s.replace(old, new))
print('ok')"

Add serde_json dep to spt Cargo.toml


ok


cargo check -p spt --all-targets 2>&1 | grep -E "^error|^warning" -A 6 | head -20; echo DONE

Re-check spt compile


DONE


sed -n '1,40p' crates/spt/src/testutil.rs

spt testutil shape


//! Shared test-only home isolation for the `spt` binary's unit tests.
//!
//! Several test modules (`api::auth`, `api::startup`, …) mutate process-wide env
//! (`SPT_HOME` + the auth-proof vars) and touch perches under the owlery. A
//! per-module lock is not enough — tests in *different* modules run on different
//! threads of the same test process and would clobber each other's `SPT_HOME`.
//! [`isolated_home`] serializes them on one process-wide lock and hands each a
//! fresh throwaway home, restoring the previous value on drop.

use std::sync::{Mutex, MutexGuard};
use tempfile::TempDir;

static LOCK: Mutex<()> = Mutex::new(());

/// RAII guard: holds the global test lock and points `SPT_HOME` at a fresh temp
/// dir for the test's duration. Also clears the auth-proof env so a prior
/// test's `$OWL_SESSION_ID` / `$SPT_API_TOKEN` can't leak into a fallback.
pub struct HomeGuard {
    _lock: MutexGuard<'static, ()>,
    _tmp: TempDir,
    prev_home: Option<String>,
}

impl Drop for HomeGuard {
    fn drop(&mut self) {
        match &self.prev_home {
            Some(v) => std::env::set_var("SPT_HOME", v),
            None => std::env::remove_var("SPT_HOME"),
        }
    }
}

/// Acquire an isolated home for a test. Blocks until any other env-touching
/// test releases its guard.
pub fn isolated_home() -> HomeGuard {
    let lock = LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let tmp = TempDir::new().expect("temp SPT_HOME");
    let prev_home = std::env::var("SPT_HOME").ok();
    std::env::set_var("SPT_HOME", tmp.path());
    std::env::remove_var("SPT_API_TOKEN");


Adding the unit E2E inside wansend.rs:


Edit

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;
    use spt_daemon::broker::Broker;
    use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
    use spt_daemon::nethost::{NetConfig, NetHost};
    use spt_daemon::registryhost::RegistryHost;
    use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
    use spt_net::net::registry::{Instance, Status};
    use spt_proto::identity::Identity;
    use spt_store::info::{self, InfoJson};
    use spt_store::perch::ParentHint;
    use spt_store::spool;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use std::time::Duration;

    fn net_broker(name: &str, dir: &Path) -> Arc<Broker> {
        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
        })
        .expect("net host");
        let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
            .expect("bind broker");
        let serve = Arc::clone(&broker);
        std::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;
            }
            std::thread::sleep(Duration::from_millis(10));
        }
        panic!("brain could not connect");
    }

    fn reg_with(subnet: &str, id: &str, node: &str) -> BTreeMap<String, SubnetRegistry> {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance(
            id,
            Instance { node: node.to_string(), status: Status::Active, epoch: 1 },
        );
        let mut map = BTreeMap::new();
        map.insert(subnet.to_string(), reg);
        map
    }

    // [unit->REQ-NET-1] the WAN leg end to end: resolve from a registry map,
    // dial, ship one WanMessage; B's production dispatcher funnels it into
    // the target perch's spool. Ambiguity refuses (REQ-INST-10); an
    // own-node hit degrades to NotFound (the local path's report stands).
    #[test]
    fn wan_send_ships_to_the_resolved_node() {
        let _home = isolated_home();
        let dir = tempfile::tempdir().unwrap();
        let (a_name, b_name) = (
            format!("spt-wansend-a-{}.sock", std::process::id()),
            format!("spt-wansend-b-{}.sock", std::process::id()),
        );
        let _a = net_broker(&a_name, dir.path());
        let _b = net_broker(&b_name, dir.path());

        // B runs the production dispatcher against the canonical home.
        let stop = Arc::new(AtomicBool::new(false));
        {
            let name = b_name.clone();
            let flag = Arc::clone(&stop);
            let paths = DispatchPaths {
                owlery: perch::owlery_dir(),
                releases: perch::spt_home().join("releases"),
                scratch: dir.path().join("b-scratch"),
            };
            let registry = Arc::new(RegistryHost::new_at(
                "b-wansend",
                EpochSource::load_from(&dir.path().join("b-epoch.json")),
                dir.path().join("b-registry"),
            ));
            std::thread::spawn(move || {
                let _ = run_dispatch_loop(&name, &paths, registry, &flag, Duration::from_millis(25));
            });
        }
        // An offline perch at B so the funnel spools.
        let target = "ling-wan";
        let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let rec = InfoJson::new(target, "now", std::process::id(), "s", "ready_agent");
        info::write_info(&perch_path, &rec).unwrap();

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

        // Ambiguity refuses before any wire work (two subnets hold the id).
        let mut ambiguous = reg_with("s1", target, &b_hex);
        ambiguous.extend(reg_with("s2", target, &b_hex));
        let out = wan_send_with(
            &mut a, &ambiguous, &a_hex, target, "doyle", "x", "op-amb",
            |_| None,
        );
        assert!(matches!(out, WanSendOutcome::Ambiguous(_)), "got {out:?}");

        // An own-node hit is NotFound (the local report stands; no self-dial).
        let own = reg_with("s1", target, &a_hex);
        let out =
            wan_send_with(&mut a, &own, &a_hex, target, "doyle", "x", "op-own", |_| None);
        assert!(matches!(out, WanSendOutcome::NotFound), "got {out:?}");

        // The real ship: resolve → dial → one record → B spools it.
        let regs = reg_with("s1", target, &b_hex);
        let resolver = |hex: &str| {
            if hex == b_hex {
                Some(b_addr.clone())
            } else {
                None
            }
        };
        let out = wan_send_with(
            &mut a, &regs, &a_hex, target, "doyle", "over the wan", "op-ship", resolver,
        );
        assert!(matches!(out, WanSendOutcome::Sent { .. }), "got {out:?}");
        let mut spooled = false;
        for _ in 0..400 {
            let pending = spool::peek_all_at(&perch_path).expect("peek");
            if pending.iter().any(|(_, _, body, _)| body == "over the wan") {
                spooled = true;
                break;
            }
            std::thread::sleep(Duration::from_millis(25));
        }
        assert!(spooled, "B's dispatcher funneled the WAN message into the spool");
        stop.store(true, Ordering::Relaxed);
    }

    // [unit->REQ-NET-1] snapshot loading degrades: missing dir, junk files,
    // and non-json entries all yield absence, never an error.
    #[test]
    fn load_snapshots_degrades_on_junk() {
        let dir = tempfile::tempdir().unwrap();
        assert!(load_snapshots(&dir.path().join("nope")).is_empty());
        let d = dir.path().join("registry");
        std::fs::create_dir_all(&d).unwrap();
        std::fs::write(d.join("good.json"), serde_json::to_string(&SubnetRegistry::new()).unwrap())
            .unwrap();
        std::fs::write(d.join("bad.json"), "{{{").unwrap();
        std::fs::write(d.join("ignored.txt"), "x").unwrap();
        let map = load_snapshots(&d);
        assert_eq!(map.len(), 1);
        assert!(map.contains_key("good"));
    }
}


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

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::testutil::isolated_home;
    use spt_daemon::broker::Broker;
    use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
    use spt_daemon::nethost::{NetConfig, NetHost};
    use spt_daemon::registryhost::RegistryHost;
    use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
    use spt_net::net::registry::{Instance, Status};
    use spt_proto::identity::Identity;
    use spt_store::info::{self, InfoJson};
    use spt_store::perch::ParentHint;
    use spt_store::spool;
    use std::sync::atomic::{AtomicBool, Ordering};
    use std::sync::Arc;
    use std::time::Duration;

    fn net_broker(name: &str, dir: &Path) -> Arc<Broker> {
        let host = NetHost::start(NetConfig {
            identity: Identity::generate(),
            relay: RelayPolicy::Disabled,
            discovery: LocalDiscovery::Off,
            scope: BindScope::Loopback,
        })
        .expect("net host");
        let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))
            .expect("bind broker");
        let serve = Arc::clone(&broker);
        std::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;
            }
            std::thread::sleep(Duration::from_millis(10));
        }
        panic!("brain could not connect");
    }

    fn reg_with(subnet: &str, id: &str, node: &str) -> BTreeMap<String, SubnetRegistry> {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance(
            id,
            Instance { node: node.to_string(), status: Status::Active, epoch: 1 },
        );
        let mut map = BTreeMap::new();
        map.insert(subnet.to_string(), reg);
        map
    }

    // [unit->REQ-NET-1] the WAN leg end to end: resolve from a registry map,
    // dial, ship one WanMessage; B's production dispatcher funnels it into
    // the target perch's spool. Ambiguity refuses (REQ-INST-10); an
    // own-node hit degrades to NotFound (the local path's report stands).
    #[test]
    fn wan_send_ships_to_the_resolved_node() {
        let _home = isolated_home();
        let dir = tempfile::tempdir().unwrap();
        let (a_name, b_name) = (
            format!("spt-wansend-a-{}.sock", std::process::id()),
            format!("spt-wansend-b-{}.sock", std::process::id()),
        );
        let _a = net_broker(&a_name, dir.path());
        let _b = net_broker(&b_name, dir.path());

        // B runs the production dispatcher against the canonical home.
        let stop = Arc::new(AtomicBool::new(false));
        {
            let name = b_name.clone();
            let flag = Arc::clone(&stop);
            let paths = DispatchPaths {
                owlery: perch::owlery_dir(),
                releases: perch::spt_home().join("releases"),
                scratch: dir.path().join("b-scratch"),
            };
            let registry = Arc::new(RegistryHost::new_at(
                "b-wansend",
                EpochSource::load_from(&dir.path().join("b-epoch.json")),
                dir.path().join("b-registry"),
            ));
            std::thread::spawn(move || {
                let _ = run_dispatch_loop(&name, &paths, registry, &flag, Duration::from_millis(25));
            });
        }
        // An offline perch at B so the funnel spools.
        let target = "ling-wan";
        let perch_path = perch::resolve_perch_path(target, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let rec = InfoJson::new(target, "now", std::process::id(), "s", "ready_agent");
        info::write_info(&perch_path, &rec).unwrap();

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

        // Ambiguity refuses before any wire work (two subnets hold the id).
        let mut ambiguous = reg_with("s1", target, &b_hex);
        ambiguous.extend(reg_with("s2", target, &b_hex));
        let out = wan_send_with(
            &mut a, &ambiguous, &a_hex, target, "doyle", "x", "op-amb",
            |_| None,
        );
        assert!(matches!(out, WanSendOutcome::Ambiguous(_)), "got {out:?}");

        // An own-node hit is NotFound (the local report stands; no self-dial).
        let own = reg_with("s1", target, &a_hex);
        let out =
            wan_send_with(&mut a, &own, &a_hex, target, "doyle", "x", "op-own", |_| None);
        assert!(matches!(out, WanSendOutcome::NotFound), "got {out:?}");

        // The real ship: resolve → dial → one record → B spools it.
        let regs = reg_with("s1", target, &b_hex);
        let resolver = |hex: &str| {
            if hex == b_hex {
                Some(b_addr.clone())
            } else {
                None
            }
        };
        let out = wan_send_with(
            &mut a, &regs, &a_hex, target, "doyle", "over the wan", "op-ship", resolver,
        );
        assert!(matches!(out, WanSendOutcome::Sent { .. }), "got {out:?}");
        let mut spooled = false;
        for _ in 0..400 {
            let pending = spool::peek_all_at(&perch_path).expect("peek");
            if pending.iter().any(|(_, _, body, _)| body == "over the wan") {
                spooled = true;
                break;
            }
            std::thread::sleep(Duration::from_millis(25));
        }
        assert!(spooled, "B's dispatcher funneled the WAN message into the spool");
        stop.store(true, Ordering::Relaxed);
    }

    // [unit->REQ-NET-1] snapshot loading degrades: missing dir, junk files,
    // and non-json entries all yield absence, never an error.
    #[test]
    fn load_snapshots_degrades_on_junk() {
        let dir = tempfile::tempdir().unwrap();
        assert!(load_snapshots(&dir.path().join("nope")).is_empty());
        let d = dir.path().join("registry");
        std::fs::create_dir_all(&d).unwrap();
        std::fs::write(d.join("good.json"), serde_json::to_string(&SubnetRegistry::new()).unwrap())
            .unwrap();
        std::fs::write(d.join("bad.json"), "{{{").unwrap();
        std::fs::write(d.join("ignored.txt"), "x").unwrap();
        let map = load_snapshots(&d);
        assert_eq!(map.len(), 1);
        assert!(map.contains_key("good"));
    }
}

//! `spt send`'s WAN leg (D9-1-4) — the D5 deferral: "`spt send` grows the
//! off-node leg transparently."
//!
//! When local delivery finds no perch, the CLI consults the daemon's
//! registry **snapshots** (`identity/registry/<subnet>.json`, the
//! [`spt_daemon::registryhost::RegistryHost`] mirror — stale-tolerant by
//! contract, KNOWN-HAZARDS 4.3), resolves the target under the D3c policy
//! ([`resolve_across_visible`]: refuse-and-qualify on ambiguity, never a
//! guess), and ships one [`WanMessage`] over the broker's QUIC endpoint to
//! the resolved node. The receiving daemon's dispatcher funnels it through
//! the access gate + op-id dedup into the target's local delivery
//! (`spt-daemon::wan::receive_wan`).
//!
//! Deferred sends stay local-only in v1: the wire record carries no
//! deferred class, and inventing one is a protocol change — the seam is
//! documented, not faked.
// [impl->REQ-NET-1]
// [impl->REQ-INST-10]

use std::collections::BTreeMap;
use std::path::Path;

use spt_daemon::brain::Brain;
use spt_daemon::endpoint::broker_socket_name;
use spt_net::net::endpoint::addr_for_node_hex;
use spt_net::net::ndjson;
use spt_net::net::registry::{resolve_across_visible, Resolution, SubnetRegistry};
use spt_net::net::wanmsg::WanMessage;
use spt_proto::addr::Address;
use spt_store::epoch::EpochSource;
use spt_store::perch;
use spt_store::subnet::SubnetStore;
use spt_store::visibility::VisibilityStore;

/// What the WAN leg did — the CLI's exit-path discriminant.
#[derive(Debug)]
pub enum WanSendOutcome {
    /// Shipped to the resolved node (hex). Delivery/spool happens there.
    Sent { node: String },
    /// Resolution refused — the message says which qualifier disambiguates.
    Ambiguous(String),
    /// No routable remote instance in any visible subnet.
    NotFound,
    /// Resolution succeeded but the wire leg failed (daemon down, dial
    /// failed, peer unreachable).
    Failed(String),
}

/// Production entry: resolve `target` against the canonical snapshots and
/// ship over the canonical broker. `target` accepts `[subnet:]id[@node]`.
pub fn wan_send(target: &str, from: &str, body: &str) -> WanSendOutcome {
    let own_hex = match spt_store::nodeid::load_or_create() {
        Ok(id) => id.public_key().to_hex(),
        Err(e) => return WanSendOutcome::Failed(format!("node identity: {e}")),
    };
    let regs = load_snapshots(&perch::identity_dir().join("registry"));
    if spt_daemon::daemon::ensure_running().is_err() {
        return WanSendOutcome::Failed("daemon did not come up".to_string());
    }
    let mut brain = match Brain::cold_start(&broker_socket_name(), now_ms()) {
        Ok(b) => b,
        Err(e) => return WanSendOutcome::Failed(format!("broker connect: {e}")),
    };
    let mut ops = EpochSource::load_from(&perch::identity_dir().join("cli-ops.json"));
    let op_id = match ops.next_epoch() {
        Ok(e) => format!("{own_hex}:cli:{e}"),
        Err(e) => return WanSendOutcome::Failed(format!("op counter: {e}")),
    };
    wan_send_with(&mut brain, &regs, &own_hex, target, from, body, &op_id, |hex| {
        addr_for_node_hex(hex).and_then(|a| serde_json::to_value(a).ok())
    })
}

/// The testable core: resolve against `regs`, dial via `resolver`, write one
/// `WanMessage` line, finish the stream.
#[allow(clippy::too_many_arguments)]
pub fn wan_send_with(
    brain: &mut Brain,
    regs: &BTreeMap<String, SubnetRegistry>,
    own_hex: &str,
    target: &str,
    from: &str,
    body: &str,
    op_id: &str,
    resolver: impl Fn(&str) -> Option<serde_json::Value>,
) -> WanSendOutcome {
    let address = match Address::parse(target) {
        Ok(a) => a,
        Err(e) => return WanSendOutcome::Failed(format!("bad address: {e:?}")),
    };

    // The same exclusion rule the registry's advertisement side enforces
    // (ADR-0006 §6): hidden = not routable, even via an explicit pin.
    let subnets = SubnetStore::load();
    let vis = VisibilityStore::load();
    let hide_new: BTreeMap<String, bool> =
        subnets.subnets.iter().map(|s| (s.name.clone(), s.hide_new_endpoints)).collect();
    let excluded =
        |subnet: &str, id: &str| vis.hidden(id, subnet, *hide_new.get(subnet).unwrap_or(&false));

    let instance = match resolve_across_visible(regs, &address, own_hex, excluded) {
        Resolution::Resolved { instance, .. } => instance,
        Resolution::Ambiguous(a) => return WanSendOutcome::Ambiguous(format!("{a:?}")),
        Resolution::NotFound => return WanSendOutcome::NotFound,
    };
    if instance.node == own_hex {
        // A local instance resolves here only when the local perch scan
        // missed it (registry staleness) — WAN-dialing ourselves helps
        // nobody; let the caller report the local outcome.
        return WanSendOutcome::NotFound;
    }

    let Some(addr) = resolver(&instance.node) else {
        return WanSendOutcome::Failed(format!("no dialable address for {}", instance.node));
    };
    let msg = WanMessage {
        target: address.id.clone(),
        from: from.to_string(),
        body: body.to_string(),
        op_id: op_id.to_string(),
    };
    let send = || -> std::io::Result<()> {
        let conn = brain.net_dial(addr, None)?;
        let opened = brain.net_open_stream(conn.conn_id, None)?;
        brain.net_stream_send(opened.stream_id, &ndjson::encode_line(&msg), None, true)?;
        Ok(())
    };
    match send() {
        Ok(()) => WanSendOutcome::Sent { node: instance.node },
        Err(e) => WanSendOutcome::Failed(e.to_string()),
    }
}

/// Load every per-subnet registry snapshot under `dir`. Missing dir or an
/// unparseable snapshot degrade to absent (the reader's stale-tolerant
/// contract) — resolution then simply finds nothing there.
pub fn load_snapshots(dir: &Path) -> BTreeMap<String, SubnetRegistry> {
    let mut out = BTreeMap::new();
    let Ok(entries) = std::fs::read_dir(dir) else {
        return out;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("json") {
            continue;
        }
        let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { continue };
        let Ok(raw) = std::fs::read_to_string(&path) else { continue };
        let Ok(reg) = serde_json::from_str::<SubnetRegistry>(&raw) else { continue };
        out.insert(stem.to_string(), reg);
    }
    out
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_millis() as u64)
        .unwrap_or(0)
}

+    use crate::testutil::isolated_home;

+    use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};

+    use spt_daemon::nethost::{NetConfig, NetHost};

+    use spt_daemon::registryhost::RegistryHost;

+    use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};

+    use spt_net::net::registry::{Instance, Status};

+    use spt_store::info::{self, InfoJson};

+    use std::sync::atomic::{AtomicBool, Ordering};

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

+        let host = NetHost::start(NetConfig {

+            identity: Identity::generate(),

+            relay: RelayPolicy::Disabled,

+            discovery: LocalDiscovery::Off,

+            scope: BindScope::Loopback,

+        let broker = Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host))

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

+    fn connect_retry(name: &str) -> Brain {

+            if let Ok(b) = Brain::cold_start(name, 1) {

+            std::thread::sleep(Duration::from_millis(10));

+        panic!("brain could not connect");

+    fn reg_with(subnet: &str, id: &str, node: &str) -> BTreeMap<String, SubnetRegistry> {

+        let mut reg = SubnetRegistry::new();

+            Instance { node: node.to_string(), status: Status::Active, epoch: 1 },

+        map.insert(subnet.to_string(), reg);

+    // [unit->REQ-NET-1] the WAN leg end to end: resolve from a registry map,

+    // dial, ship one WanMessage; B's production dispatcher funnels it into

+    // the target perch's spool. Ambiguity refuses (REQ-INST-10); an

+    // own-node hit degrades to NotFound (the local path's report stands).

+    fn wan_send_ships_to_the_resolved_node() {

+        let dir = tempfile::tempdir().unwrap();

+            format!("spt-wansend-a-{}.sock", std::process::id()),

+            format!("spt-wansend-b-{}.sock", std::process::id()),

+        let _a = net_broker(&a_name, dir.path());

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

+        // B runs the production dispatcher against the canonical home.

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

+            let flag = Arc::clone(&stop);

+            let paths = DispatchPaths {

+                owlery: perch::owlery_dir(),

+                releases: perch::spt_home().join("releases"),

+                scratch: dir.path().join("b-scratch"),

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

+                EpochSource::load_from(&dir.path().join("b-epoch.json")),

+                dir.path().join("b-registry"),

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

+                let _ = run_dispatch_loop(&name, &paths, registry, &flag, Duration::from_millis(25));

+        // An offline perch at B so the funnel spools.

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

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

+        let rec = InfoJson::new(target, "now", std::process::id(), "s", "ready_agent");

+        info::write_info(&perch_path, &rec).unwrap();

+        let mut a = connect_retry(&a_name);

+        let mut b_probe = connect_retry(&b_name);

+        let a_hex = a.net_status().expect("status").node_id_hex.expect("net");

+        let b_hex = b_probe.net_status().expect("status").node_id_hex.expect("net");

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

+        // Ambiguity refuses before any wire work (two subnets hold the id).

+        let mut ambiguous = reg_with("s1", target, &b_hex);

+        ambiguous.extend(reg_with("s2", target, &b_hex));

+            &mut a, &ambiguous, &a_hex, target, "doyle", "x", "op-amb",

+        assert!(matches!(out, WanSendOutcome::Ambiguous(_)), "got {out:?}");

+        // An own-node hit is NotFound (the local report stands; no self-dial).

+        let own = reg_with("s1", target, &a_hex);

+            wan_send_with(&mut a, &own, &a_hex, target, "doyle", "x", "op-own", |_| None);

+        assert!(matches!(out, WanSendOutcome::NotFound), "got {out:?}");

+        // The real ship: resolve → dial → one record → B spools it.

+        let regs = reg_with("s1", target, &b_hex);

+            &mut a, &regs, &a_hex, target, "doyle", "over the wan", "op-ship", resolver,

+        assert!(matches!(out, WanSendOutcome::Sent { .. }), "got {out:?}");

+            let pending = spool::peek_all_at(&perch_path).expect("peek");

+            if pending.iter().any(|(_, _, body, _)| body == "over the wan") {

+            std::thread::sleep(Duration::from_millis(25));

+        assert!(spooled, "B's dispatcher funneled the WAN message into the spool");

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

+    // [unit->REQ-NET-1] snapshot loading degrades: missing dir, junk files,

+    // and non-json entries all yield absence, never an error.

+    fn load_snapshots_degrades_on_junk() {

+        let dir = tempfile::tempdir().unwrap();

+        assert!(load_snapshots(&dir.path().join("nope")).is_empty());

+        let d = dir.path().join("registry");

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

+        std::fs::write(d.join("good.json"), serde_json::to_string(&SubnetRegistry::new()).unwrap())

+        std::fs::write(d.join("bad.json"), "{{{").unwrap();

+        std::fs::write(d.join("ignored.txt"), "x").unwrap();

+        assert!(map.contains_key("good"));


Bash

cargo test -p spt wansend 2>&1 | grep -E "^error" -A 8 | head -30; cargo test -p spt wansend 2>&1 | grep "test result"

Run wansend tests


test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 59 filtered out; finished in 0.34s
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s


D9-7: closeout sweep


cargo test -p spt 2>&1 | grep -E "test result|FAILED" | head -8; cargo clippy -p spt --all-targets 2>&1 | grep -cE "^warning|^error"; traceable-reqs check > /dev/null 2>&1; echo TR:$?

Full spt tests + clippy + traceability


test result: ok. 61 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.59s
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.00s
0
TR:0


tail -3 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bhuz2mo4a.output" 2>/dev/null; echo ---

Check D9-1-3 CI status


All green locally. D9-1-3 CI still running — committing D9-1-4 locally, push gated on green:


git add crates/spt/src/wansend.rs crates/spt/src/main.rs crates/spt/src/cli.rs crates/spt/Cargo.toml Cargo.lock 2>/dev/null; git commit -m "feat(spt): M4-D9-1-4 spt send WAN leg (registry-resolved off-node delivery)

The D5 deferral: spt send grows the off-node leg transparently. On
NoPerch, the CLI resolves the target against the daemon's per-subnet
registry snapshots (stale-tolerant readers, KH 4.3) under the D3c policy -
resolve_across_visible refuses ambiguity with qualifier guidance
(REQ-INST-10), hidden endpoints are not routable (ADR-0006 §6), an
own-node hit degrades to the local report (no self-dial) - then dials the
instance's node by stable name and ships one WanMessage; the receiving
daemon's dispatcher funnels it through the access gate + op-id dedup.
CLI op ids mint from a durable identity/cli-ops.json EpochSource.

Deferred sends stay local-only: the wire carries no deferred class (v1) -
documented seam, not a silent reclass.

Unit E2E: resolve -> dial -> ship lands in the target perch's spool
through B's production dispatcher; ambiguity and own-node refuse before
any wire work; snapshot loading degrades on junk.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>" && git log --oneline -1

Commit D9-1-4 (push after CI green)


[dev-freeform af7f394] feat(spt): M4-D9-1-4 spt send WAN leg (registry-resolved off-node delivery)
 5 files changed, 359 insertions(+), 2 deletions(-)
 create mode 100644 crates/spt/src/wansend.rs
af7f394 feat(spt): M4-D9-1-4 spt send WAN leg (registry-resolved off-node delivery)


<task-notification>
<task-id>bhuz2mo4a</task-id>
<tool-use-id>toolu_01NGvBcF12ACYHmqMPaPQ36B</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bhuz2mo4a.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI until FINAL for b15cf2c" completed (exit code 0)</summary>
</task-notification>


While CI finishes — D9-1-5 (attach/xfer arms). Broker session table recon:


grep -n "SpawnReq\|sessions\|struct Session" crates/spt-daemon/src/broker.rs | head -20; echo ===; grep -n "pub fn transfers_dir\|pub fn xfer" crates/spt-store/src/xfer.rs | head -5; grep -rn "progress_dir" crates/spt-daemon/src/xfer.rs | head -4

Broker session table + xfer dirs


52:    ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,
154:/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state
160:    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
195:            sessions: Arc::new(Mutex::new(HashMap::new())),
235:    /// Number of currently hosted sessions (for tests / introspection).
237:        self.sessions.lock().unwrap().len()
243:        self.sessions.lock().unwrap().get(&id).and_then(|h| h.session.process_id())
248:    /// sessions it subscribed to — the sessions themselves persist (B2: a brain
329:        // Brain gone: detach as subscriber but leave the sessions running.
330:        let sessions = self.sessions.lock().unwrap();
332:            if let Some(h) = sessions.get(id) {
336:        drop(sessions);
353:        let req: SpawnReq = serde_json::from_value(env.payload)
381:        let sessions_exit = Arc::clone(&self.sessions);
399:            sessions_exit.lock().unwrap().remove(&id);
402:        self.sessions.lock().unwrap().insert(id, HostedSession { session, drain, log });
416:        // Clone the log Arc out from under the sessions lock, then replay under
419:            let sessions = self.sessions.lock().unwrap();
420:            let h = sessions
443:        // Clone the session Arc out from under the sessions lock so the PTY write
===
65:pub fn transfers_dir() -> PathBuf {
204:/// `progress_dir` is where this end's progress records land
218:    progress_dir: &Path,
259:                                progress_dir,
281:                                    progress_dir,


Prompt:
<task-notification>
<task-id>bhuz2mo4a</task-id>
<tool-use-id>toolu_01NGvBcF12ACYHmqMPaPQ36B</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bhuz2mo4a.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI until FINAL for b15cf2c" completed (exit code 0)</summary>
</task-notification>


tail -3 "C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\bhuz2mo4a.output"; git push && echo PUSHED_af7f394

Check b15cf2c result + push D9-1-4


FINAL:
ci success
To https://github.com/SaberMage/spt-core.git
   b15cf2c..af7f394  dev-freeform -> dev-freeform
PUSHED_af7f394


sleep 45; for i in $(seq 1 60); do out=$(gh run list --branch dev-freeform --limit 3 --json headSha,conclusion,status 2>/dev/null); echo "$out" | grep -q "af7f394" || { sleep 30; continue; }; pending=$(echo "$out" | python -c "import json,sys; rs=[r for r in json.load(sys.stdin) if r['headSha'].startswith('af7f394')]; print(sum(1 for r in rs if r['status']!='completed'))" 2>/dev/null); if [ "$pending" = "0" ]; then echo "FINAL:"; echo "$out" | python -c "import json,sys; rs=[r for r in json.load(sys.stdin) if r['headSha'].startswith('af7f394')]; [print(r['conclusion']) for r in rs]"; exit 0; fi; sleep 30; done; echo TIMEOUT

Poll CI until FINAL for af7f394


Command running in background with ID: b40vc2uij. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\648159b6-16b2-4568-b643-7b0c98c1871d\tasks\b40vc2uij.output. You will be notified when it completes. To check interim output, use Read on that file path.


D9-1-3 green, D9-1-4 pushed + polling. Building D9-1-5:


sed -n '340,410p' crates/spt-daemon/src/broker.rs; grep -n "KIND_NET_STREAMS =>" -B3 -A8 crates/spt-daemon/src/broker.rs | head -16

handle_spawn + IPC dispatch pattern


                host.detach_stream_sub(*id, &send);
            }
            // And presence: the liveness log + its ring persist (D4c).
            if my_presence_sub {
                host.detach_presence_sub(&send);
            }
        }
        Ok(())
    }

    /// Spawn a child under a fresh PTY, register it, auto-subscribe the spawning
    /// connection, start its output drain and exit waiter, and reply `spawned`.
    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
        let req: SpawnReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad spawn payload: {e}"))?;
        let session = PtySession::spawn_program(
            &req.program,
            &req.args,
            SurfaceSize::new(req.rows, req.cols),
        )
        .map_err(|e| format!("spawn failed: {e}"))?;
        let session = Arc::new(session);
        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
        let pid = session.process_id();

        // The output log, with the spawning connection pre-attached so no chunk
        // is missed before an explicit subscribe.
        let log = Arc::new(Mutex::new(OutputLog::new(id, self.log_cap_chunks)));
        log.lock().unwrap().attach(Arc::clone(send), 0);

        // Output drain → appends to the log (which live-sends to the subscriber).
        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                log_drain.lock().unwrap().append(chunk);
            })
            .map_err(|e| format!("drain failed: {e}"))?;

        // Exit waiter → one `exit` frame when the child is reaped, then the dead
        // session is removed from the table. Runs on its own thread; `wait`
        // holds the child lock but `kill` uses a split killer.
        let sessions_exit = Arc::clone(&self.sessions);
        let log_exit = Arc::clone(&log);
        let waiter = Arc::clone(&session);
        thread::spawn(move || {
            if let Ok(status) = waiter.wait() {
                let ev = ExitEvent { session_id: id, code: Some(status.exit_code() as i32) };
                let frame = Envelope::new(
                    KIND_EXIT,
                    serde_json::to_value(ev).expect("ExitEvent serializes"),
                );
                // Send to whoever is currently subscribed (under the log lock).
                if let Some(sub) = log_exit.lock().unwrap().subscriber.clone() {
                    if let Ok(mut s) = sub.lock() {
                        let _ = write_frame(&mut *s, &frame);
                    }
                }
            }
            // Reap the dead session from the table.
            sessions_exit.lock().unwrap().remove(&id);
        });

        self.sessions.lock().unwrap().insert(id, HostedSession { session, drain, log });

        let resp = Spawned { session_id: id, pid };
        let frame =
            Envelope::new(KIND_SPAWNED, serde_json::to_value(resp).expect("Spawned serializes"));
        send_frame(send, &frame);
        Ok(id)
    }

304-                        send_error(&send, &msg);
305-                    }
306-                }
307:                KIND_NET_STREAMS => {
308-                    if let Err(msg) = self.dispatch_net_streams(&send) {
309-                        send_error(&send, &msg);
310-                    }
311-                }
312-                KIND_NET_STREAM_SUBSCRIBE => {
313-                    match self.dispatch_net_stream_subscribe(env, &send) {
314-                        Ok(id) => my_stream_subs.push(id),
315-                        Err(msg) => send_error(&send, &msg),


Edit

pub struct SpawnReq {
    /// The program to exec (resolved on `PATH` by the OS spawn).
    pub program: String,
    /// Its arguments, verbatim.
    #[serde(default)]
    pub args: Vec<String>,
    /// Initial PTY rows.
    pub rows: u16,
    /// Initial PTY cols.
    pub cols: u16,
}

pub struct SpawnReq {
    /// The program to exec (resolved on `PATH` by the OS spawn).
    pub program: String,
    /// Its arguments, verbatim.
    #[serde(default)]
    pub args: Vec<String>,
    /// Initial PTY rows.
    pub rows: u16,
    /// Initial PTY cols.
    pub cols: u16,
    /// The endpoint this session belongs to (D9-1: the remote-drive
    /// dispatcher's gate subject — `serve_attach` gates on the *owning*
    /// endpoint's whitelist, resolved from the broker's table, never from
    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
}


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

pub struct SpawnReq {
    /// The program to exec (resolved on `PATH` by the OS spawn).
    pub program: String,
    /// Its arguments, verbatim.
    #[serde(default)]
    pub args: Vec<String>,
    /// Initial PTY rows.
    pub rows: u16,
    /// Initial PTY cols.
    pub cols: u16,
}

pub struct SpawnReq {
    /// The program to exec (resolved on `PATH` by the OS spawn).
    pub program: String,
    /// Its arguments, verbatim.
    #[serde(default)]
    pub args: Vec<String>,
    /// Initial PTY rows.
    pub rows: u16,
    /// Initial PTY cols.
    pub cols: u16,
    /// The endpoint this session belongs to (D9-1: the remote-drive
    /// dispatcher's gate subject — `serve_attach` gates on the *owning*
    /// endpoint's whitelist, resolved from the broker's table, never from
    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher
    /// refuses to serve an unlabeled session, fail-closed.
    #[serde(default)]
    pub endpoint: String,
}

+    /// The endpoint this session belongs to (D9-1: the remote-drive

+    /// dispatcher's gate subject — `serve_attach` gates on the *owning*

+    /// endpoint's whitelist, resolved from the broker's table, never from

+    /// wire bytes). Empty = unlabeled (a legacy/test spawn); the dispatcher

+    /// refuses to serve an unlabeled session, fail-closed.

/// `spawned` payload — the broker-minted session id + the child's OS pid.


Bash

grep -n "KIND_NET_PRESENCE_EVENT" crates/spt-daemon/src/msg.rs | head -2; sed -n '84,95p' crates/spt-daemon/src/msg.rs

Find spot for new kind consts


86:pub const KIND_NET_PRESENCE_EVENT: &str = "net-presence-event";
458:        KIND_NET_PRESENCE_EVENT,
pub const KIND_NET_PRESENCE_SUBSCRIBE: &str = "net-presence-subscribe";
/// Broker→brain: one sequenced conn-liveness event (live or replayed).
pub const KIND_NET_PRESENCE_EVENT: &str = "net-presence-event";

/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.
pub const PRESENCE_CONNECTED: &str = "connected";
/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.
pub const PRESENCE_DISCONNECTED: &str = "disconnected";

/// `spawn` payload — the program + args + initial geometry. The broker spawns
/// it under a PTY (no shell interpretation; argv is passed through).
#[derive(Debug, Clone, Serialize, Deserialize)]


Edit

/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.
pub const PRESENCE_CONNECTED: &str = "connected";
/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.
pub const PRESENCE_DISCONNECTED: &str = "disconnected";

/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.
pub const PRESENCE_CONNECTED: &str = "connected";
/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.
pub const PRESENCE_DISCONNECTED: &str = "disconnected";

/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound
/// dispatcher's session→endpoint resolution (D9-1).
pub const KIND_SESSIONS: &str = "sessions";
/// Broker→brain reply to [`KIND_SESSIONS`].
pub const KIND_SESSIONS_REPLY: &str = "sessions-reply";


/// [`NetPresenceEvent::kind`]: a connection joined the broker's conn table.
pub const PRESENCE_CONNECTED: &str = "connected";
/// [`NetPresenceEvent::kind`]: a connection ended and left the conn table.
pub const PRESENCE_DISCONNECTED: &str = "disconnected";

/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound
/// dispatcher's session→endpoint resolution (D9-1).
pub const KIND_SESSIONS: &str = "sessions";
/// Broker→brain reply to [`KIND_SESSIONS`].
pub const KIND_SESSIONS_REPLY: &str = "sessions-reply";

+/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound

+/// dispatcher's session→endpoint resolution (D9-1).

+pub const KIND_SESSIONS: &str = "sessions";

+/// Broker→brain reply to [`KIND_SESSIONS`].

+pub const KIND_SESSIONS_REPLY: &str = "sessions-reply";

/// `spawn` payload — the program + args + initial geometry. The broker spawns

/// it under a PTY (no shell interpretation; argv is passed through).

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


/// `net-streams-reply` payload — every stream the broker currently holds.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetStreamsReply {
    /// Held streams (opened + peer-initiated), unordered.
    #[serde(default)]
    pub streams: Vec<NetStreamInfo>,
}

/// `net-streams-reply` payload — every stream the broker currently holds.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetStreamsReply {
    /// Held streams (opened + peer-initiated), unordered.
    #[serde(default)]
    pub streams: Vec<NetStreamInfo>,
}

/// One hosted session in a `sessions-reply`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
    /// The broker-minted session id.
    pub session_id: u64,
    /// The owning endpoint (the spawn's label; empty = unlabeled).
    #[serde(default)]
    pub endpoint: String,
}

/// `sessions-reply` payload — every session the broker currently hosts.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionsReply {
    #[serde(default)]
    pub sessions: Vec<SessionInfo>,
}


/// `net-streams-reply` payload — every stream the broker currently holds.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetStreamsReply {
    /// Held streams (opened + peer-initiated), unordered.
    #[serde(default)]
    pub streams: Vec<NetStreamInfo>,
}

/// One hosted session in a `sessions-reply`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
    /// The broker-minted session id.
    pub session_id: u64,
    /// The owning endpoint (the spawn's label; empty = unlabeled).
    #[serde(default)]
    pub endpoint: String,
}

/// `sessions-reply` payload — every session the broker currently hosts.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionsReply {
    #[serde(default)]
    pub sessions: Vec<SessionInfo>,
}

+/// One hosted session in a `sessions-reply`.

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

+    /// The owning endpoint (the spawn's label; empty = unlabeled).

+/// `sessions-reply` payload — every session the broker currently hosts.

/// `net-stream-subscribe` payload — attach this connection as a stream's read

/// subscriber, replaying buffered chunks with `seq >= from_seq` first (D4b).


Now broker side — label storage + sessions verb:


Bash

grep -n "struct HostedSession" -A 8 crates/spt-daemon/src/broker.rs; grep -n "use crate::msg::" -A 8 crates/spt-daemon/src/broker.rs | head -12

HostedSession + broker imports


143:struct HostedSession {
144-    /// The PTY + child (PTY master/writer + spawned child process).
145-    session: Arc<PtySession>,
146-    /// The output drain pump (kept so teardown can stop it).
147-    #[allow(dead_code)] // held for ownership/teardown; not read directly
148-    drain: Drain,
149-    /// The sequenced, buffered output + current subscriber.
150-    log: Arc<Mutex<OutputLog>>,
151-}
48:use crate::msg::{
49-    applied_envelope, decode_bytes, net_dialed_envelope, output_envelope, ErrorEvent, ExitEvent,
50-    InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent, NetStatusReply,
51-    NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq, NetStreamsReply,
52-    ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,
53-    KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,
54-    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_OPEN,
55-    KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE,
56-    KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,


Edit

struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
}

struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
    /// The owning endpoint (the spawn's label; empty = unlabeled). The
    /// inbound dispatcher's gate subject for remote-drive (D9-1).
    endpoint: String,
}


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>


self.sessions.lock().unwrap().insert(id, HostedSession { session, drain, log });

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession { session, drain, log, endpoint: req.endpoint.clone() },
        );


140
141	/// A child the broker hosts under a PTY — the §B-owned resources for one
142	/// session, plus its gapless-resume output log.
143	struct HostedSession {
144	    /// The PTY + child (PTY master/writer + spawned child process).
145	    session: Arc<PtySession>,
146	    /// The output drain pump (kept so teardown can stop it).
147	    #[allow(dead_code)] // held for ownership/teardown; not read directly
148	    drain: Drain,
149	    /// The sequenced, buffered output + current subscriber.
150	    log: Arc<Mutex<OutputLog>>,
151	}
152
153	/// The broker process kernel. Owns the listening socket, the table of hosted
154	/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state
155	/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable

/// A child the broker hosts under a PTY — the §B-owned resources for one
/// session, plus its gapless-resume output log.
struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
}

/// The broker process kernel. Owns the listening socket, the table of hosted
/// sessions, and the broker-owned idempotency anchor (B5); holds no logic state
/// (ADR-0004 §B). The [`EffectJournal`] is a §B resource — the durable


struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
}

struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
    /// The owning endpoint (the spawn's label; empty = unlabeled). The
    /// inbound dispatcher's gate subject for remote-drive (D9-1).
    endpoint: String,
}


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

struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
}

struct HostedSession {
    /// The PTY + child (PTY master/writer + spawned child process).
    session: Arc<PtySession>,
    /// The output drain pump (kept so teardown can stop it).
    #[allow(dead_code)] // held for ownership/teardown; not read directly
    drain: Drain,
    /// The sequenced, buffered output + current subscriber.
    log: Arc<Mutex<OutputLog>>,
    /// The owning endpoint (the spawn's label; empty = unlabeled). The
    /// inbound dispatcher's gate subject for remote-drive (D9-1).
    endpoint: String,
}

+    /// The owning endpoint (the spawn's label; empty = unlabeled). The

+    /// inbound dispatcher's gate subject for remote-drive (D9-1).

/// The broker process kernel. Owns the listening socket, the table of hosted


self.sessions.lock().unwrap().insert(id, HostedSession { session, drain, log });

        self.sessions.lock().unwrap().insert(
            id,
            HostedSession { session, drain, log, endpoint: req.endpoint.clone() },
        );


        self.sessions.lock().unwrap().insert(
            id,
            HostedSession { session, drain, log, endpoint: req.endpoint.clone() },
        );

-        self.sessions.lock().unwrap().insert(id, HostedSession { session, drain, log });

+        self.sessions.lock().unwrap().insert(

+            HostedSession { session, drain, log, endpoint: req.endpoint.clone() },

let resp = Spawned { session_id: id, pid };


D9-7: closeout sweep


Now the sessions IPC verb in broker dispatch + Brain method:


                KIND_NET_STREAMS => {
                    if let Err(msg) = self.dispatch_net_streams(&send) {
                        send_error(&send, &msg);
                    }
                }

                KIND_NET_STREAMS => {
                    if let Err(msg) = self.dispatch_net_streams(&send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_SESSIONS => {
                    let sessions = self.sessions.lock().unwrap();
                    let reply = SessionsReply {
                        sessions: sessions
                            .iter()
                            .map(|(id, h)| SessionInfo {
                                session_id: *id,
                                endpoint: h.endpoint.clone(),
                            })
                            .collect(),
                    };
                    drop(sessions);
                    let frame = Envelope::new(
                        KIND_SESSIONS_REPLY,
                        serde_json::to_value(reply).expect("SessionsReply serializes"),
                    );
                    send_frame(&send, &frame);
                }


                KIND_NET_STREAMS => {
                    if let Err(msg) = self.dispatch_net_streams(&send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_SESSIONS => {
                    let sessions = self.sessions.lock().unwrap();
                    let reply = SessionsReply {
                        sessions: sessions
                            .iter()
                            .map(|(id, h)| SessionInfo {
                                session_id: *id,
                                endpoint: h.endpoint.clone(),
                            })
                            .collect(),
                    };
                    drop(sessions);
                    let frame = Envelope::new(
                        KIND_SESSIONS_REPLY,
                        serde_json::to_value(reply).expect("SessionsReply serializes"),
                    );
                    send_frame(&send, &frame);
                }

+                    let sessions = self.sessions.lock().unwrap();

+                    let reply = SessionsReply {

+                        sessions: sessions

+                            .map(|(id, h)| SessionInfo {

+                                session_id: *id,

+                                endpoint: h.endpoint.clone(),

+                            .collect(),

+                    let frame = Envelope::new(

+                        KIND_SESSIONS_REPLY,

+                        serde_json::to_value(reply).expect("SessionsReply serializes"),

+                    send_frame(&send, &frame);

match self.dispatch_net_stream_subscribe(env, &send) {


    NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq, NetStreamsReply,
    ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,
    KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,
    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_OPEN,
    KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE,
    KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,

    NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq, NetStreamsReply,
    ResizeReq, SessionInfo, SessionsReply, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT,
    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND,
    KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
    KIND_SPAWNED, KIND_SUBSCRIBE,


    NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq, NetStreamsReply,
    ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,
    KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,
    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_OPEN,
    KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE,
    KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,

    NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq, NetStreamsReply,
    ResizeReq, SessionInfo, SessionsReply, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT,
    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND,
    KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
    KIND_SPAWNED, KIND_SUBSCRIBE,

applied_envelope, decode_bytes, net_dialed_envelope, output_envelope, ErrorEvent, ExitEvent,

InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent, NetStatusReply,

-    ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL,

-    KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,

-    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_OPEN,

-    KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE,

-    KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,

+    ResizeReq, SessionInfo, SessionsReply, SpawnReq, Spawned, SubscribeReq, KIND_ERROR, KIND_EXIT,

+    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT,

+    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,

+    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND,

+    KIND_NET_STREAM_SUBSCRIBE, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,

use crate::transport::{recv_hello, DaemonTransport, LocalSocketTransport};


Bash

sed -n '570,600p' crates/spt-daemon/src/brain.rs; grep -n "NetStreams(" crates/spt-daemon/src/brain.rs | head -4

Brain net_streams + event variant pattern


    pub fn net_streams(&mut self) -> io::Result<NetStreamsReply> {
        self.send(KIND_NET_STREAMS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStreams(ev) => return Ok(ev),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),
                _ => continue,
            }
        }
    }

    /// (Re)subscribe to a stream's buffered reads from `from_seq`, setting the
    /// local cursor (gapless, exactly-once resume — the PTY `subscribe`
    /// discipline on a QUIC stream, D4b).
    // [impl->REQ-DAEMON-2]
    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {
        self.net_cursors.insert(stream_id, from_seq);
        self.send(
            KIND_NET_STREAM_SUBSCRIBE,
            serde_json::to_value(NetStreamSubscribeReq { stream_id, from_seq })
                .expect("NetStreamSubscribeReq serializes"),
        )
    }

    /// The next stream-read sequence still needed for `stream_id` (the durable
    /// state a handed-off brain carries to resubscribe gaplessly).
    pub fn net_cursor(&self, stream_id: u64) -> Option<u64> {
        self.net_cursors.get(&stream_id).copied()
    }

    /// (Re)subscribe to the broker's conn-liveness event log from `from_seq`,
95:    NetStreams(NetStreamsReply),
359:                    return Ok(BrokerEvent::NetStreams(ev));
574:                BrokerEvent::NetStreams(ev) => return Ok(ev),


350	                }
351	                KIND_NET_SENT => {
352	                    let ev: NetSent = serde_json::from_value(env.payload)
353	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
354	                    return Ok(BrokerEvent::NetSent(ev));
355	                }
356	                KIND_NET_STREAMS_REPLY => {
357	                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
358	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
359	                    return Ok(BrokerEvent::NetStreams(ev));
360	                }
361	                KIND_NET_STREAM_DATA => {
362	                    let ev: NetStreamData = serde_json::from_value(env.payload)
363	                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
364	                    // Same exactly-once discipline as PTY output, per stream:
365	                    // accept the contiguous next seq, drop a replay duplicate,
366	                    // reject a forward gap (a lost chunk must not pass silently).
367	                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);
368	                    if ev.seq < *cursor {
369	                        continue; // replay duplicate — dedup
370	                    }
371	                    if ev.seq > *cursor {
372	                        return Err(io::Error::new(
373	                            io::ErrorKind::InvalidData,
374	                            format!(

                }
                KIND_NET_SENT => {
                    let ev: NetSent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetSent(ev));
                }
                KIND_NET_STREAMS_REPLY => {
                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreams(ev));
                }
                KIND_NET_STREAM_DATA => {
                    let ev: NetStreamData = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    // Same exactly-once discipline as PTY output, per stream:
                    // accept the contiguous next seq, drop a replay duplicate,
                    // reject a forward gap (a lost chunk must not pass silently).
                    let cursor = self.net_cursors.entry(ev.stream_id).or_insert(ev.seq);
                    if ev.seq < *cursor {
                        continue; // replay duplicate — dedup
                    }
                    if ev.seq > *cursor {
                        return Err(io::Error::new(
                            io::ErrorKind::InvalidData,
                            format!(


Edit

                KIND_NET_STREAMS_REPLY => {
                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreams(ev));
                }

                KIND_NET_STREAMS_REPLY => {
                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreams(ev));
                }
                KIND_SESSIONS_REPLY => {
                    let ev: SessionsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Sessions(ev));
                }


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

                KIND_NET_STREAMS_REPLY => {
                    let ev: NetStreamsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::NetStreams(ev));
                }
                KIND_SESSIONS_REPLY => {
                    let ev: SessionsReply = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Sessions(ev));
                }

+                KIND_SESSIONS_REPLY => {

+                    let ev: SessionsReply = serde_json::from_value(env.payload)

+                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

+                    return Ok(BrokerEvent::Sessions(ev));

let ev: NetStreamData = serde_json::from_value(env.payload)


Bash

sed -n '76,110p' crates/spt-daemon/src/brain.rs; grep -n "use crate::msg::" -A 12 crates/spt-daemon/src/brain.rs | head -16

BrokerEvent enum + brain imports


pub enum BrokerEvent {
    /// A contiguous chunk of a session's output (the cursor has advanced past it).
    Output { session_id: u64, seq: u64, bytes: Vec<u8> },
    /// A spawn succeeded.
    Spawned { session_id: u64, pid: Option<u32> },
    /// A session's child exited.
    Exit { session_id: u64, code: Option<i32> },
    /// An idempotent effect is durably applied (or was deduped — `applied_now`
    /// distinguishes). The exactly-once ack for a [`Brain::send_effect`] (B5).
    Applied { session_id: u64, op_id: u64, applied_now: bool },
    /// The broker-owned net endpoint's status (a `net-status` reply, D4a).
    NetStatus(NetStatusReply),
    /// A net dial durably landed — fresh or deduped (D4a).
    NetDialed(NetDialed),
    /// A stream open durably landed — fresh or deduped (D4b).
    NetStreamOpened(NetStreamOpened),
    /// A journaled stream send durably landed — fresh or deduped (D4b).
    NetSent(NetSent),
    /// The broker's held-streams listing (D4b).
    NetStreams(NetStreamsReply),
    /// A contiguous chunk of a stream's reads (the cursor has advanced past it).
    NetStreamData { stream_id: u64, seq: u64, bytes: Vec<u8> },
    /// A stream's read side finished cleanly; `next_seq` is the completeness
    /// check (the subscriber's cursor must equal it to have missed nothing).
    NetStreamEof { stream_id: u64, next_seq: u64 },
    /// A contiguous conn-liveness event (the presence cursor has advanced past
    /// it) — connected/disconnected on the broker's conn table (D4c).
    NetPresence(NetPresenceEvent),
    /// The broker reported a command error.
    Error { message: String },
    /// A frame of an unrecognized kind (forward-compat: tolerated, surfaced).
    Other(Envelope),
}

/// A live brain attached to a broker.
35:use crate::msg::{
36-    decode_bytes, encode_bytes, AppliedEvent, ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq,
37-    NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent, NetStatusReply, NetStreamData,
38-    NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
39-    NetStreamsReply, OutputEvent, ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,
40-    KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIALED,
41-    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,
42-    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_DATA,
43-    KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND,
44-    KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_RESIZE, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,
45-};
46-use crate::transport::{send_hello, LocalSocketTransport};
47-


Edit

    NetStreamsReply, OutputEvent, ResizeReq, SpawnReq, Spawned, SubscribeReq, KIND_APPLIED,
    KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIALED,
    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,
    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_DATA,
    KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND,
    KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_RESIZE, KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,
};

    NetStreamsReply, OutputEvent, ResizeReq, SessionsReply, SpawnReq, Spawned, SubscribeReq,
    KIND_APPLIED, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIALED,
    KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE, KIND_NET_SENT, KIND_NET_STATUS,
    KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY, KIND_NET_STREAM_DATA,
    KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND,
    KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY,
    KIND_SPAWN, KIND_SPAWNED, KIND_SUBSCRIBE,
};