    /// The broker net endpoint's relay stance (D4a, REQ-NET-2). Default
    /// [`RelayChoice::N0`] — the ADR-0002 zero-config path. An unrecognized
    /// string degrades to the default (forward-compat: a future value must not
    /// wedge an older daemon).
    pub relay: RelayChoice,
}

    /// The broker net endpoint's relay stance (D4a, REQ-NET-2). Default
    /// [`RelayChoice::N0`] — the ADR-0002 zero-config path. An unrecognized
    /// string degrades to the default (forward-compat: a future value must not
    /// wedge an older daemon).
    pub relay: RelayChoice,
    /// Whether the peer pump runs at all (D9-1). Default `true` — the subnet
    /// self-drives; `false` is the kill switch for debugging/hermetic setups.
    pub peer_pump: bool,
    /// Registry feed push cadence (D9-1).
    pub registry_pump_period: Duration,
    /// Notif feed push cadence (D9-1).
    pub notif_pump_period: Duration,
    /// Context sync pull cadence — the anti-entropy loop (D9-1).
    pub sync_pull_period: Duration,
    /// Update check cadence (D9-1; the pull rides the same trusted conns).
    pub update_check_period: Duration,
}

+    /// Whether the peer pump runs at all (D9-1). Default `true` — the subnet

+    /// self-drives; `false` is the kill switch for debugging/hermetic setups.

+    /// Registry feed push cadence (D9-1).

+    pub registry_pump_period: Duration,

+    /// Notif feed push cadence (D9-1).

+    /// Context sync pull cadence — the anti-entropy loop (D9-1).

+    /// Update check cadence (D9-1; the pull rides the same trusted conns).


113	    /// Context sync pull cadence — the anti-entropy loop (D9-1).
114	    pub sync_pull_period: Duration,
115	    /// Update check cadence (D9-1; the pull rides the same trusted conns).
116	    pub update_check_period: Duration,
117	}
118
119	impl Default for DaemonConfig {
120	    fn default() -> Self {
121	        DaemonConfig {
122	            pulse_period: DEFAULT_PULSE_PERIOD,
123	            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
124	            crash_grace: DEFAULT_CRASH_GRACE,
125	            full_auto_update: false, // gated by default (REQ-UPD-4)
126	            relay: RelayChoice::N0,  // zero-config default (ADR-0002)
127	        }
128	    }
129	}
130
131	impl DaemonConfig {
132	    /// The canonical config path: `<spt_home>/daemon.json`.

    /// Context sync pull cadence — the anti-entropy loop (D9-1).
    pub sync_pull_period: Duration,
    /// Update check cadence (D9-1; the pull rides the same trusted conns).
    pub update_check_period: Duration,
}

impl Default for DaemonConfig {
    fn default() -> Self {
        DaemonConfig {
            pulse_period: DEFAULT_PULSE_PERIOD,
            protection_window_ms: DEFAULT_PROTECTION_WINDOW_MS,
            crash_grace: DEFAULT_CRASH_GRACE,
            full_auto_update: false, // gated by default (REQ-UPD-4)
            relay: RelayChoice::N0,  // zero-config default (ADR-0002)
        }
    }
}

impl DaemonConfig {
    /// The canonical config path: `<spt_home>/daemon.json`.


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.


Edit

            full_auto_update: false, // gated by default (REQ-UPD-4)
            relay: RelayChoice::N0,  // zero-config default (ADR-0002)
        }
    }
}

            full_auto_update: false, // gated by default (REQ-UPD-4)
            relay: RelayChoice::N0,  // zero-config default (ADR-0002)
            peer_pump: true,         // the subnet self-drives (D9-1)
            registry_pump_period: DEFAULT_REGISTRY_PUMP_PERIOD,
            notif_pump_period: DEFAULT_NOTIF_PUMP_PERIOD,
            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,
            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,
        }
    }
}


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

            full_auto_update: false, // gated by default (REQ-UPD-4)
            relay: RelayChoice::N0,  // zero-config default (ADR-0002)
        }
    }
}

            full_auto_update: false, // gated by default (REQ-UPD-4)
            relay: RelayChoice::N0,  // zero-config default (ADR-0002)
            peer_pump: true,         // the subnet self-drives (D9-1)
            registry_pump_period: DEFAULT_REGISTRY_PUMP_PERIOD,
            notif_pump_period: DEFAULT_NOTIF_PUMP_PERIOD,
            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,
            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,
        }
    }
}

+            peer_pump: true,         // the subnet self-drives (D9-1)

+            registry_pump_period: DEFAULT_REGISTRY_PUMP_PERIOD,

+            notif_pump_period: DEFAULT_NOTIF_PUMP_PERIOD,

+            sync_pull_period: DEFAULT_SYNC_PULL_PERIOD,

+            update_check_period: DEFAULT_UPDATE_CHECK_PERIOD,


            relay: match file.relay.as_deref() {
                Some("disabled") => RelayChoice::Disabled,
                // "n0", absent, or an unrecognized future value → the default.
                _ => d.relay,
            },
        }
    }

            relay: match file.relay.as_deref() {
                Some("disabled") => RelayChoice::Disabled,
                // "n0", absent, or an unrecognized future value → the default.
                _ => d.relay,
            },
            peer_pump: file.peer_pump.unwrap_or(d.peer_pump),
            registry_pump_period: file
                .registry_pump_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.registry_pump_period),
            notif_pump_period: file
                .notif_pump_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.notif_pump_period),
            sync_pull_period: file
                .sync_pull_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.sync_pull_period),
            update_check_period: file
                .update_check_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.update_check_period),
        }
    }


            relay: match file.relay.as_deref() {
                Some("disabled") => RelayChoice::Disabled,
                // "n0", absent, or an unrecognized future value → the default.
                _ => d.relay,
            },
        }
    }

            relay: match file.relay.as_deref() {
                Some("disabled") => RelayChoice::Disabled,
                // "n0", absent, or an unrecognized future value → the default.
                _ => d.relay,
            },
            peer_pump: file.peer_pump.unwrap_or(d.peer_pump),
            registry_pump_period: file
                .registry_pump_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.registry_pump_period),
            notif_pump_period: file
                .notif_pump_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.notif_pump_period),
            sync_pull_period: file
                .sync_pull_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.sync_pull_period),
            update_check_period: file
                .update_check_period_ms
                .map(Duration::from_millis)
                .unwrap_or(d.update_check_period),
        }
    }

+            peer_pump: file.peer_pump.unwrap_or(d.peer_pump),

+                .registry_pump_period_ms

+                .map(Duration::from_millis)

+                .unwrap_or(d.registry_pump_period),

+                .unwrap_or(d.notif_pump_period),

+                .unwrap_or(d.sync_pull_period),

+                .update_check_period_ms

+                .unwrap_or(d.update_check_period),


            relay: Some(
                match self.relay {
                    RelayChoice::N0 => "n0",
                    RelayChoice::Disabled => "disabled",
                }
                .to_string(),
            ),
        };

            relay: Some(
                match self.relay {
                    RelayChoice::N0 => "n0",
                    RelayChoice::Disabled => "disabled",
                }
                .to_string(),
            ),
            peer_pump: Some(self.peer_pump),
            registry_pump_period_ms: Some(self.registry_pump_period.as_millis() as u64),
            notif_pump_period_ms: Some(self.notif_pump_period.as_millis() as u64),
            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),
            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),
        };


            relay: Some(
                match self.relay {
                    RelayChoice::N0 => "n0",
                    RelayChoice::Disabled => "disabled",
                }
                .to_string(),
            ),
        };

            relay: Some(
                match self.relay {
                    RelayChoice::N0 => "n0",
                    RelayChoice::Disabled => "disabled",
                }
                .to_string(),
            ),
            peer_pump: Some(self.peer_pump),
            registry_pump_period_ms: Some(self.registry_pump_period.as_millis() as u64),
            notif_pump_period_ms: Some(self.notif_pump_period.as_millis() as u64),
            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),
            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),
        };

+            peer_pump: Some(self.peer_pump),

+            registry_pump_period_ms: Some(self.registry_pump_period.as_millis() as u64),

+            notif_pump_period_ms: Some(self.notif_pump_period.as_millis() as u64),

+            sync_pull_period_ms: Some(self.sync_pull_period.as_millis() as u64),

+            update_check_period_ms: Some(self.update_check_period.as_millis() as u64),

let json = serde_json::to_string_pretty(&file)


246
247	    /// Dial a peer by address over [`SPT_NET_ALPN`].
248	    // [impl->REQ-NET-1]
249	    pub async fn connect(&self, addr: EndpointAddr) -> Result<Connection, NetError> {
250	        self.connect_with_alpn(addr, SPT_NET_ALPN).await
251	    }
252
253	    /// Dial a peer over an explicit ALPN (the pairing driver dials `SPT_PAIR_ALPN`).
254	    // [impl->REQ-NET-1]
255	    pub async fn connect_with_alpn(
256	        &self,
257	        addr: EndpointAddr,
258	        alpn: &[u8],
259	    ) -> Result<Connection, NetError> {
260	        self.inner
261	            .connect(addr, alpn)
262	            .await
263	            .map_err(|e| NetError::Connect(e.to_string()))
264	    }
265
266	    /// Dial a peer by its node id (public key) alone, letting discovery (mDNS on the
267	    /// LAN, n0 DNS on the WAN) resolve the address. This is the "no ticket needed"
268	    /// path: with [`LocalDiscovery::Mdns`] enabled, two machines on one LAN connect
269	    /// knowing only each other's stable name.
270	    // [impl->REQ-NET-1]
271	    pub async fn connect_by_id(&self, peer: PublicKey) -> Result<Connection, NetError> {
272	        let id = EndpointId::from_bytes(&peer.to_bytes())
273	            .map_err(|e| NetError::Connect(format!("invalid peer id: {e}")))?;
274	        self.connect(EndpointAddr::new(id)).await
275	    }
276
277	    /// Borrow the underlying iroh endpoint — the seam the D4 broker uses to own the
278	    /// endpoint + its live QUIC streams across brain restarts (ADR-0004 §B).
279	    pub fn iroh(&self) -> &Endpoint {
280	        &self.inner
281	    }
282	}
283
284	#[cfg(test)]
285	mod tests {

    /// Dial a peer by address over [`SPT_NET_ALPN`].
    // [impl->REQ-NET-1]
    pub async fn connect(&self, addr: EndpointAddr) -> Result<Connection, NetError> {
        self.connect_with_alpn(addr, SPT_NET_ALPN).await
    }

    /// Dial a peer over an explicit ALPN (the pairing driver dials `SPT_PAIR_ALPN`).
    // [impl->REQ-NET-1]
    pub async fn connect_with_alpn(
        &self,
        addr: EndpointAddr,
        alpn: &[u8],
    ) -> Result<Connection, NetError> {
        self.inner
            .connect(addr, alpn)
            .await
            .map_err(|e| NetError::Connect(e.to_string()))
    }

    /// Dial a peer by its node id (public key) alone, letting discovery (mDNS on the
    /// LAN, n0 DNS on the WAN) resolve the address. This is the "no ticket needed"
    /// path: with [`LocalDiscovery::Mdns`] enabled, two machines on one LAN connect
    /// knowing only each other's stable name.
    // [impl->REQ-NET-1]
    pub async fn connect_by_id(&self, peer: PublicKey) -> Result<Connection, NetError> {
        let id = EndpointId::from_bytes(&peer.to_bytes())
            .map_err(|e| NetError::Connect(format!("invalid peer id: {e}")))?;
        self.connect(EndpointAddr::new(id)).await
    }

    /// Borrow the underlying iroh endpoint — the seam the D4 broker uses to own the
    /// endpoint + its live QUIC streams across brain restarts (ADR-0004 §B).
    pub fn iroh(&self) -> &Endpoint {
        &self.inner
    }
}

#[cfg(test)]
mod tests {


    /// Borrow the underlying iroh endpoint — the seam the D4 broker uses to own the
    /// endpoint + its live QUIC streams across brain restarts (ADR-0004 §B).
    pub fn iroh(&self) -> &Endpoint {
        &self.inner
    }
}

    /// Borrow the underlying iroh endpoint — the seam the D4 broker uses to own the
    /// endpoint + its live QUIC streams across brain restarts (ADR-0004 §B).
    pub fn iroh(&self) -> &Endpoint {
        &self.inner
    }
}

/// An id-only dialable [`EndpointAddr`] for a node named by its lowercase-hex
/// pubkey (the trust store's identity column), letting discovery (mDNS on the
/// LAN, n0 DNS on the WAN) resolve the transport address. The peer pump's
/// production resolver (D9-1): dial trusted peers knowing only their stable
/// name. `None` on a malformed hex — a corrupt trust row must not panic the
/// pump.
// [impl->REQ-NET-1]
pub fn addr_for_node_hex(hex: &str) -> Option<EndpointAddr> {
    let pk = spt_proto::identity::PublicKey::from_hex(hex).ok()?;
    let id = EndpointId::from_bytes(&pk.to_bytes()).ok()?;
    Some(EndpointAddr::new(id))
}


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

    /// Borrow the underlying iroh endpoint — the seam the D4 broker uses to own the
    /// endpoint + its live QUIC streams across brain restarts (ADR-0004 §B).
    pub fn iroh(&self) -> &Endpoint {
        &self.inner
    }
}

/// An id-only dialable [`EndpointAddr`] for a node named by its lowercase-hex
/// pubkey (the trust store's identity column), letting discovery (mDNS on the
/// LAN, n0 DNS on the WAN) resolve the transport address. The peer pump's
/// production resolver (D9-1): dial trusted peers knowing only their stable
/// name. `None` on a malformed hex — a corrupt trust row must not panic the
/// pump.
// [impl->REQ-NET-1]
pub fn addr_for_node_hex(hex: &str) -> Option<EndpointAddr> {
    let pk = spt_proto::identity::PublicKey::from_hex(hex).ok()?;
    let id = EndpointId::from_bytes(&pk.to_bytes()).ok()?;
    Some(EndpointAddr::new(id))
}

+/// An id-only dialable [`EndpointAddr`] for a node named by its lowercase-hex

+/// pubkey (the trust store's identity column), letting discovery (mDNS on the

+/// LAN, n0 DNS on the WAN) resolve the transport address. The peer pump's

+/// production resolver (D9-1): dial trusted peers knowing only their stable

+/// name. `None` on a malformed hex — a corrupt trust row must not panic the

+pub fn addr_for_node_hex(hex: &str) -> Option<EndpointAddr> {

+    let pk = spt_proto::identity::PublicKey::from_hex(hex).ok()?;

+    let id = EndpointId::from_bytes(&pk.to_bytes()).ok()?;


<task-notification>
<task-id>b5ue1kvut</task-id>
<tool-use-id>toolu_019CWPfpAeYzDGFRH2ueqRkV</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\b5ue1kvut.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI until FINAL conclusion for ea73c9a" completed (exit code 0)</summary>
</task-notification>


Now `registryhost.rs`:


Write

//! The daemon's live subnet-registry holder (D9-1-2) — REQ-INST-7's
//! production wiring.
//!
//! D3 built the registry **model** (`spt_net::net::registry`: the epoch
//! lease, the join-time collision check, visibility enforcement) and D4d the
//! wire record (`RegistryUpdate`), both consumed only by tests. This type is
//! the daemon-side instance: one in-memory `SubnetRegistry` per member
//! subnet, fed from two directions —
//!
//! - **inbound**: the dispatcher's registry arm applies peer feeds under the
//!   same fail-closed gate as every replicated store (member subnet ∧ origin
//!   trusted *in that subnet*, subject = handshake-proven stream origin —
//!   KNOWN-HAZARDS 7.5);
//! - **local**: [`advertise_local`](RegistryHost::advertise_local) scans the
//!   owlery and (re-)advertises every visible self perch with a fresh
//!   [`EpochSource`] epoch — the per-node monotonic lease key, never
//!   wall-clock (KNOWN-HAZARDS 4.8).
//!
//! The registry is **eventually consistent by re-advertisement**: state is
//! in-memory (a daemon restart forgets peers' rows), and each pump tick
//! re-pushes the node's own rows — peers reconverge within one cadence, the
//! same replay-is-recovery posture as the D8 notif feed. For out-of-process
//! consumers (`spt send`'s WAN resolution, D9-1-4) the host mirrors each
//! subnet's registry to an atomic JSON snapshot under
//! `identity/registry/` — readers tolerate staleness (the 4.3
//! stale-clean posture), never block on the daemon.
// [impl->REQ-INST-7]

use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

use spt_net::net::registry::{advertise_if_visible, Instance, MergeOutcome, Status, SubnetRegistry};
use spt_net::net::replicate::RegistryUpdate;
use spt_store::epoch::EpochSource;
use spt_store::liveness::is_perch_alive;
use spt_store::perch::{self, ParentHint};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

/// What the host did with one inbound registry record — loud + typed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryApplyVerdict {
    /// Admitted and merged with the stated outcome (the lease may still have
    /// dropped it as [`MergeOutcome::Stale`] — that IS a merge decision).
    Merged(MergeOutcome),
    /// Refused: this node is not a member of the record's subnet.
    NotMember,
    /// Refused: the origin is not trusted in the record's subnet.
    UntrustedOrigin,
}

/// The inbound gate's config — reloaded per feed (config edits take effect
/// without a restart; the access-gate stance shared with sync/notif).
pub struct RegistryGatePolicy {
    /// Names of every subnet this node is a member of.
    pub subnets: Vec<String>,
    pub trust: TrustStore,
}

impl RegistryGatePolicy {
    /// Load from the node's canonical stores.
    pub fn load() -> RegistryGatePolicy {
        let subnets =
            SubnetStore::load().subnets.iter().map(|s| s.name.clone()).collect();
        RegistryGatePolicy { subnets, trust: TrustStore::load() }
    }

    /// member subnet ∧ origin trusted in that subnet — fail-closed on both.
    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {
        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
    }
}

/// The daemon's per-subnet registry instances + this node's advertisement
/// epoch source. Shared (`Arc`) between the dispatcher's registry arm and
/// the peer pump.
pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,
    /// The node's one durable monotonic counter (the D3b lease source) — the
    /// epoch on every locally-authored instance row.
    epoch: Mutex<EpochSource>,
    /// Where snapshots land (`identity/registry/<subnet>.json`).
    snapshot_dir: PathBuf,
}

impl RegistryHost {
    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home.
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// This node's stable name.
    pub fn node_hex(&self) -> &str {
        &self.node_hex
    }

    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        let mut regs = self.regs.lock().unwrap();
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                let reg = regs.entry(u.subnet.clone()).or_default();
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                RegistryApplyVerdict::Merged(
                    reg.merge_instance(&u.endpoint_id, u.instance.clone()),
                )
            })
            .collect();
        drop(regs);
        self.write_snapshots();
        verdicts
    }

    /// Scan the owlery and (re-)advertise every self perch into every member
    /// subnet it is visible in, each row stamped with a fresh epoch. Returns
    /// the updates to push to peers (the node's own rows only — peers' rows
    /// reconverge from their own re-advertisement, no transitive gossip in
    /// v1: trust is pairwise per subnet, a relayed third-party row would
    /// carry an origin the receiver can't gate honestly).
    // [impl->REQ-INST-7]
    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let vis = VisibilityStore::load();
        let mut out = Vec::new();
        let mut regs = self.regs.lock().unwrap();
        let mut epoch = self.epoch.lock().unwrap();

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            let status =
                if is_perch_alive(&perch_path) { Status::Active } else { Status::Dormant };
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance =
                    Instance { node: self.node_hex.clone(), status, epoch: ep };
                let reg = regs.entry(sub.name.clone()).or_default();
                match advertise_if_visible(
                    reg,
                    &vis,
                    &sub.name,
                    sub.hide_new_endpoints,
                    &id,
                    instance.clone(),
                ) {
                    Ok(Some(_)) => out.push(RegistryUpdate {
                        subnet: sub.name.clone(),
                        endpoint_id: id.clone(),
                        instance,
                    }),
                    Ok(None) => {} // hidden in this subnet — not advertised
                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision.holder.node);
                    }
                }
            }
        }
        drop(epoch);
        drop(regs);
        self.write_snapshots();
        out
    }

    /// Every instance row for `endpoint_id` in `subnet` (the reconcile
    /// election's input; empty when unknown).
    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
        self.regs
            .lock()
            .unwrap()
            .get(subnet)
            .map(|r| r.instances(endpoint_id).to_vec())
            .unwrap_or_default()
    }

    /// A point-in-time clone of one subnet's registry (resolution input).
    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {
        self.regs.lock().unwrap().get(subnet).cloned()
    }

    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`
    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process
    /// readers. Best-effort: a failed snapshot write must not fail the merge
    /// that triggered it (readers tolerate staleness by contract).
    fn write_snapshots(&self) {
        let regs = self.regs.lock().unwrap();
        if regs.is_empty() {
            return;
        }
        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {
            return;
        }
        for (subnet, reg) in regs.iter() {
            let Ok(json) = serde_json::to_vec_pretty(reg) else { continue };
            let _ = atomic_write(&self.snapshot_dir.join(format!("{subnet}.json")), &json);
        }
    }

    /// The snapshot path for one subnet (the CLI reader's contract).
    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {
        dir.join(format!("{subnet}.json"))
    }
}

/// tmp-write + atomic rename with the 5.2 Windows-EBUSY retry posture.
fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
    let tmp = path.with_extension("json.tmp");
    std::fs::write(&tmp, bytes)?;
    for attempt in 0..5 {
        match std::fs::rename(&tmp, path) {
            Ok(()) => return Ok(()),
            Err(_) if attempt < 4 => {
                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));
            }
            Err(e) => return Err(e),
        }
    }
    unreachable!("loop returns on every path")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::info::{self, InfoJson};

    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
        RegistryUpdate {
            subnet: subnet.to_string(),
            endpoint_id: id.to_string(),
            instance: Instance { node: node.to_string(), status: Status::Active, epoch },
        }
    }

    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
        let mut trust = TrustStore::default();
        trust.record(subnet, trusted, None, None);
        RegistryGatePolicy { subnets: vec![subnet.to_string()], trust }
    }

    fn host(dir: &Path) -> RegistryHost {
        RegistryHost::new_at(
            "aa11",
            EpochSource::load_from(&dir.join("epoch.json")),
            dir.join("registry"),
        )
    }

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {
        let dir = tempfile::tempdir().unwrap();
        let h = host(dir.path());
        let p = policy("home", "bb22");

        let v = h.apply_feed("bb22", &[upd("other", "ling", "bb22", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::NotMember]);
        let v = h.apply_feed("cc33", &[upd("home", "ling", "cc33", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::UntrustedOrigin]);
        assert!(h.rows("home", "ling").is_empty(), "refused feeds write nothing");

        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Inserted)]);
        // A lagging lower-epoch update is dropped by the lease (4.8).
        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 4)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);
        assert_eq!(h.rows("home", "ling")[0].epoch, 5);
    }

    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
    // epochs, respects visibility, and returns only this node's rows.
    #[test]
    fn advertise_local_scans_and_respects_visibility() {
        with_home(|home| {
            let owlery = home.join("owlery");
            for id in ["vis-ag", "hid-ag"] {
                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
                std::fs::create_dir_all(&p).unwrap();
                let rec = InfoJson::new(id, "now", std::process::id(), "s", "ready_agent");
                info::write_info(&p, &rec).unwrap();
            }
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("adv").unwrap();
            subnets.save().unwrap();
            let mut vis = VisibilityStore::load();
            vis.set_override("hid-ag", "adv", Some(true)); // hidden ⇒ excluded
            vis.save().unwrap();

            let h = host(home);
            let out = h.advertise_local(&owlery);
            assert!(
                out.iter().any(|u| u.endpoint_id == "vis-ag" && u.subnet == "adv"),
                "visible perch advertised: {out:?}"
            );
            assert!(
                !out.iter().any(|u| u.endpoint_id == "hid-ag"),
                "hidden perch is EXCLUDED (ADR-0006 §6), not merely unlisted"
            );
            assert!(!h.rows("adv", "vis-ag").is_empty(), "own registry holds the row");
            assert!(h.rows("adv", "hid-ag").is_empty());

            // Re-advertising bumps the epoch — the lease orders restarts.
            let e1 = h.rows("adv", "vis-ag")[0].epoch;
            h.advertise_local(&owlery);
            let e2 = h.rows("adv", "vis-ag")[0].epoch;
            assert!(e2 > e1, "fresh epoch per advertisement ({e1} -> {e2})");

            // The snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }
}


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

//! The daemon's live subnet-registry holder (D9-1-2) — REQ-INST-7's
//! production wiring.
//!
//! D3 built the registry **model** (`spt_net::net::registry`: the epoch
//! lease, the join-time collision check, visibility enforcement) and D4d the
//! wire record (`RegistryUpdate`), both consumed only by tests. This type is
//! the daemon-side instance: one in-memory `SubnetRegistry` per member
//! subnet, fed from two directions —
//!
//! - **inbound**: the dispatcher's registry arm applies peer feeds under the
//!   same fail-closed gate as every replicated store (member subnet ∧ origin
//!   trusted *in that subnet*, subject = handshake-proven stream origin —
//!   KNOWN-HAZARDS 7.5);
//! - **local**: [`advertise_local`](RegistryHost::advertise_local) scans the
//!   owlery and (re-)advertises every visible self perch with a fresh
//!   [`EpochSource`] epoch — the per-node monotonic lease key, never
//!   wall-clock (KNOWN-HAZARDS 4.8).
//!
//! The registry is **eventually consistent by re-advertisement**: state is
//! in-memory (a daemon restart forgets peers' rows), and each pump tick
//! re-pushes the node's own rows — peers reconverge within one cadence, the
//! same replay-is-recovery posture as the D8 notif feed. For out-of-process
//! consumers (`spt send`'s WAN resolution, D9-1-4) the host mirrors each
//! subnet's registry to an atomic JSON snapshot under
//! `identity/registry/` — readers tolerate staleness (the 4.3
//! stale-clean posture), never block on the daemon.
// [impl->REQ-INST-7]

use std::collections::BTreeMap;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

use spt_net::net::registry::{advertise_if_visible, Instance, MergeOutcome, Status, SubnetRegistry};
use spt_net::net::replicate::RegistryUpdate;
use spt_store::epoch::EpochSource;
use spt_store::liveness::is_perch_alive;
use spt_store::perch::{self, ParentHint};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use spt_store::visibility::VisibilityStore;

/// What the host did with one inbound registry record — loud + typed.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RegistryApplyVerdict {
    /// Admitted and merged with the stated outcome (the lease may still have
    /// dropped it as [`MergeOutcome::Stale`] — that IS a merge decision).
    Merged(MergeOutcome),
    /// Refused: this node is not a member of the record's subnet.
    NotMember,
    /// Refused: the origin is not trusted in the record's subnet.
    UntrustedOrigin,
}

/// The inbound gate's config — reloaded per feed (config edits take effect
/// without a restart; the access-gate stance shared with sync/notif).
pub struct RegistryGatePolicy {
    /// Names of every subnet this node is a member of.
    pub subnets: Vec<String>,
    pub trust: TrustStore,
}

impl RegistryGatePolicy {
    /// Load from the node's canonical stores.
    pub fn load() -> RegistryGatePolicy {
        let subnets =
            SubnetStore::load().subnets.iter().map(|s| s.name.clone()).collect();
        RegistryGatePolicy { subnets, trust: TrustStore::load() }
    }

    /// member subnet ∧ origin trusted in that subnet — fail-closed on both.
    pub fn admits(&self, subnet: &str, origin_node: &str) -> bool {
        self.subnets.iter().any(|s| s == subnet) && self.trust.is_trusted(subnet, origin_node)
    }
}

/// The daemon's per-subnet registry instances + this node's advertisement
/// epoch source. Shared (`Arc`) between the dispatcher's registry arm and
/// the peer pump.
pub struct RegistryHost {
    /// This node's stable name (lowercase-hex pubkey).
    node_hex: String,
    regs: Mutex<BTreeMap<String, SubnetRegistry>>,
    /// The node's one durable monotonic counter (the D3b lease source) — the
    /// epoch on every locally-authored instance row.
    epoch: Mutex<EpochSource>,
    /// Where snapshots land (`identity/registry/<subnet>.json`).
    snapshot_dir: PathBuf,
}

impl RegistryHost {
    /// Stand the host up for this node, with the canonical epoch counter and
    /// snapshot dir under the spt home.
    pub fn new(node_hex: &str) -> RegistryHost {
        RegistryHost::new_at(
            node_hex,
            EpochSource::load(),
            perch::identity_dir().join("registry"),
        )
    }

    /// Explicit-paths constructor (tests; the pump E2E gives each in-process
    /// "node" its own counter + snapshot dir).
    pub fn new_at(node_hex: &str, epoch: EpochSource, snapshot_dir: PathBuf) -> RegistryHost {
        RegistryHost {
            node_hex: node_hex.to_string(),
            regs: Mutex::new(BTreeMap::new()),
            epoch: Mutex::new(epoch),
            snapshot_dir,
        }
    }

    /// This node's stable name.
    pub fn node_hex(&self) -> &str {
        &self.node_hex
    }

    /// Apply one inbound feed under the gate. `origin_node` MUST be the
    /// handshake-proven stream-table origin, never payload bytes.
    // [impl->REQ-INST-7]
    // [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
    pub fn apply_feed(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        let mut regs = self.regs.lock().unwrap();
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                let reg = regs.entry(u.subnet.clone()).or_default();
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                RegistryApplyVerdict::Merged(
                    reg.merge_instance(&u.endpoint_id, u.instance.clone()),
                )
            })
            .collect();
        drop(regs);
        self.write_snapshots();
        verdicts
    }

    /// Scan the owlery and (re-)advertise every self perch into every member
    /// subnet it is visible in, each row stamped with a fresh epoch. Returns
    /// the updates to push to peers (the node's own rows only — peers' rows
    /// reconverge from their own re-advertisement, no transitive gossip in
    /// v1: trust is pairwise per subnet, a relayed third-party row would
    /// carry an origin the receiver can't gate honestly).
    // [impl->REQ-INST-7]
    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let vis = VisibilityStore::load();
        let mut out = Vec::new();
        let mut regs = self.regs.lock().unwrap();
        let mut epoch = self.epoch.lock().unwrap();

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            let status =
                if is_perch_alive(&perch_path) { Status::Active } else { Status::Dormant };
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance =
                    Instance { node: self.node_hex.clone(), status, epoch: ep };
                let reg = regs.entry(sub.name.clone()).or_default();
                match advertise_if_visible(
                    reg,
                    &vis,
                    &sub.name,
                    sub.hide_new_endpoints,
                    &id,
                    instance.clone(),
                ) {
                    Ok(Some(_)) => out.push(RegistryUpdate {
                        subnet: sub.name.clone(),
                        endpoint_id: id.clone(),
                        instance,
                    }),
                    Ok(None) => {} // hidden in this subnet — not advertised
                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision.holder.node);
                    }
                }
            }
        }
        drop(epoch);
        drop(regs);
        self.write_snapshots();
        out
    }

    /// Every instance row for `endpoint_id` in `subnet` (the reconcile
    /// election's input; empty when unknown).
    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
        self.regs
            .lock()
            .unwrap()
            .get(subnet)
            .map(|r| r.instances(endpoint_id).to_vec())
            .unwrap_or_default()
    }

    /// A point-in-time clone of one subnet's registry (resolution input).
    pub fn snapshot(&self, subnet: &str) -> Option<SubnetRegistry> {
        self.regs.lock().unwrap().get(subnet).cloned()
    }

    /// Mirror every subnet's registry to `identity/registry/<subnet>.json`
    /// (tmp-write + atomic rename — KNOWN-HAZARDS 5.2) for out-of-process
    /// readers. Best-effort: a failed snapshot write must not fail the merge
    /// that triggered it (readers tolerate staleness by contract).
    fn write_snapshots(&self) {
        let regs = self.regs.lock().unwrap();
        if regs.is_empty() {
            return;
        }
        if std::fs::create_dir_all(&self.snapshot_dir).is_err() {
            return;
        }
        for (subnet, reg) in regs.iter() {
            let Ok(json) = serde_json::to_vec_pretty(reg) else { continue };
            let _ = atomic_write(&self.snapshot_dir.join(format!("{subnet}.json")), &json);
        }
    }

    /// The snapshot path for one subnet (the CLI reader's contract).
    pub fn snapshot_path(dir: &Path, subnet: &str) -> PathBuf {
        dir.join(format!("{subnet}.json"))
    }
}

/// tmp-write + atomic rename with the 5.2 Windows-EBUSY retry posture.
fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {
    let tmp = path.with_extension("json.tmp");
    std::fs::write(&tmp, bytes)?;
    for attempt in 0..5 {
        match std::fs::rename(&tmp, path) {
            Ok(()) => return Ok(()),
            Err(_) if attempt < 4 => {
                std::thread::sleep(std::time::Duration::from_millis(20 << attempt));
            }
            Err(e) => return Err(e),
        }
    }
    unreachable!("loop returns on every path")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::info::{self, InfoJson};

    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
        RegistryUpdate {
            subnet: subnet.to_string(),
            endpoint_id: id.to_string(),
            instance: Instance { node: node.to_string(), status: Status::Active, epoch },
        }
    }

    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
        let mut trust = TrustStore::default();
        trust.record(subnet, trusted, None, None);
        RegistryGatePolicy { subnets: vec![subnet.to_string()], trust }
    }

    fn host(dir: &Path) -> RegistryHost {
        RegistryHost::new_at(
            "aa11",
            EpochSource::load_from(&dir.join("epoch.json")),
            dir.join("registry"),
        )
    }

    // [unit->REQ-INST-7] inbound feeds gate fail-closed: non-member subnet
    // and untrusted origin write nothing; an admitted row merges and the
    // epoch lease still drops a stale follow-up.
    #[test]
    fn apply_feed_gates_and_merges_under_the_lease() {
        let dir = tempfile::tempdir().unwrap();
        let h = host(dir.path());
        let p = policy("home", "bb22");

        let v = h.apply_feed("bb22", &[upd("other", "ling", "bb22", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::NotMember]);
        let v = h.apply_feed("cc33", &[upd("home", "ling", "cc33", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::UntrustedOrigin]);
        assert!(h.rows("home", "ling").is_empty(), "refused feeds write nothing");

        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 5)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Inserted)]);
        // A lagging lower-epoch update is dropped by the lease (4.8).
        let v = h.apply_feed("bb22", &[upd("home", "ling", "bb22", 4)], &p);
        assert_eq!(v, vec![RegistryApplyVerdict::Merged(MergeOutcome::Stale)]);
        assert_eq!(h.rows("home", "ling")[0].epoch, 5);
    }

    // [unit->REQ-INST-7] advertise_local scans the owlery, stamps fresh
    // epochs, respects visibility, and returns only this node's rows.
    #[test]
    fn advertise_local_scans_and_respects_visibility() {
        with_home(|home| {
            let owlery = home.join("owlery");
            for id in ["vis-ag", "hid-ag"] {
                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
                std::fs::create_dir_all(&p).unwrap();
                let rec = InfoJson::new(id, "now", std::process::id(), "s", "ready_agent");
                info::write_info(&p, &rec).unwrap();
            }
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("adv").unwrap();
            subnets.save().unwrap();
            let mut vis = VisibilityStore::load();
            vis.set_override("hid-ag", "adv", Some(true)); // hidden ⇒ excluded
            vis.save().unwrap();

            let h = host(home);
            let out = h.advertise_local(&owlery);
            assert!(
                out.iter().any(|u| u.endpoint_id == "vis-ag" && u.subnet == "adv"),
                "visible perch advertised: {out:?}"
            );
            assert!(
                !out.iter().any(|u| u.endpoint_id == "hid-ag"),
                "hidden perch is EXCLUDED (ADR-0006 §6), not merely unlisted"
            );
            assert!(!h.rows("adv", "vis-ag").is_empty(), "own registry holds the row");
            assert!(h.rows("adv", "hid-ag").is_empty());

            // Re-advertising bumps the epoch — the lease orders restarts.
            let e1 = h.rows("adv", "vis-ag")[0].epoch;
            h.advertise_local(&owlery);
            let e2 = h.rows("adv", "vis-ag")[0].epoch;
            assert!(e2 > e1, "fresh epoch per advertisement ({e1} -> {e2})");

            // The snapshot mirror exists and parses back.
            let snap = RegistryHost::snapshot_path(&home.join("registry"), "adv");
            let bytes = std::fs::read(&snap).expect("snapshot written");
            let parsed: SubnetRegistry = serde_json::from_slice(&bytes).expect("parses");
            assert!(!parsed.instances("vis-ag").is_empty());
        });
    }
}


Prompt:
<task-notification>
<task-id>b5ue1kvut</task-id>
<tool-use-id>toolu_019CWPfpAeYzDGFRH2ueqRkV</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\b5ue1kvut.output</output-file>
<status>completed</status>
<summary>Background command "Poll CI until FINAL conclusion for ea73c9a" completed (exit code 0)</summary>
</task-notification>


Bash

Read CI poll result for ea73c9a


FINAL:
ci success


**D9-1-1 CI-green FINAL** (`ea73c9a`). Continuing D9-1-2 — registering module + compile:


grep -n "pub struct JoinCollision" -A 8 crates/spt-net/src/net/registry.rs

JoinCollision fields


150:pub struct JoinCollision {
151-    /// The bare id that clashed.
152-    pub endpoint_id: String,
153-    /// The nodes already holding that id in the target subnet.
154-    pub nodes: Vec<String>,
155-}
156-
157-impl std::fmt::Display for JoinCollision {
158-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {


Edit

pub mod reconcile;
pub mod registryhost;
pub mod relay;


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

pub mod reconcile;
pub mod registryhost;
pub mod relay;

//! `spt-daemon` — the per-machine broker/brain supervisor (M3b).
//!
//! ADR-0004 splits the logical daemon into two implementation layers so a
//! routine self-update swaps the logic without disturbing live endpoints:
//!
//! - a **broker** (stable kernel) holds *only* the un-transferable resources a
//!   live consumer would lose continuity on if the logic restarted — PTY
//!   masters (from [`spt_term`]), spawned harness children, accepted/listening
//!   sockets — and runs no logic;
//! - a **brain** (restartable logic) holds routing, registry, manifest parse,
//!   and the re-hosted [`spt_live`] lifecycle, attaching to the broker over a
//!   **versioned local IPC** so a *newer brain talks to an older broker*.
//!
//! **B0 (this commit)** lands the crate's layer position plus that versioned
//! IPC seam — the load-bearing contract everything else rides:
//!
//! - [`frame`] — the forward-compatible [`Envelope`] schema + the version
//!   [`Hello`] handshake (KNOWN-HAZARDS 2.3, [`REQ-HAZARD-HANDOFF-ARGV-COMPAT`]);
//! - [`codec`] — length-prefixed JSON framing over any [`std::io::Read`]/`Write`;
//! - [`transport`] — the OS-split carrier (named pipe / UDS) behind one
//!   [`DaemonTransport`] trait, mirroring `spt-term`'s `SessionSurface`.
//!
//! **B1 (this commit)** adds the **broker kernel** ([`broker`]) — the stable
//! half that hosts a real `spt-term` PTY child and relays its output/input over
//! the IPC, holding only the ADR-0004 §B resources and no logic — plus the
//! broker↔brain message catalog ([`msg`]).
//!
//! **B3 (this commit)** consolidates the interim no-daemon model into the brain:
//! the `api listen` pulse / Psyche / drop-ingest loops become scheduled
//! in-process loops ([`lifecycle::BrainLifecycle`]) paced by a durable,
//! configurable period ([`config::DaemonConfig`]) instead of the interim 5s
//! constant; the only residue left in the harness-hosted session is the thin,
//! stateless [`relay::Relay`]. The `spt` binary's `LiveHost` becomes a shim over
//! `BrainLifecycle` (re-host, not rewrite) ([`REQ-DAEMON-1`]).
//!
//! **B5 (this commit)** makes the broker↔brain boundary **exactly-once across a
//! brain crash** ([`effect::EffectJournal`]): an idempotent `input` carries a
//! durable `op_id`, and the broker dedups at the effect keyed by that id over a
//! broker-owned recovery anchor (KNOWN-HAZARDS 7.2, [`REQ-HAZARD-RESTART-IDEMPOTENT`];
//! Spike #6's two constraints — broker-owned anchor, dedup-at-effect-by-id —
//! instantiated on the real PTY-write boundary).
//!
//! **C0 (this commit)** opens M3c — the self-update engine ([`update`]): the
//! ADR-0004 §A update-class taxonomy ([`UpdateClass`] + [`classify`]) and the
//! **brain-only zero-interruption swap** ([`apply_brain_only`], [`REQ-UPD-3`]).
//! The brain-only apply *triggers* a handoff over the B2 substrate — snapshot →
//! drop → re-attach — so the live PTY child + its output stream survive the logic
//! swap untouched; it never re-implements the handoff and never touches a broker
//! resource.
//!
//! **C1 (this commit)** adds the verify-before-handoff gate ([`release`]): a
//! release is signature-verified ([`REQ-UPD-2`]) and hardened against rollback
//! ([`REQ-HAZARD-UPDATE-ROLLBACK`]) — Ed25519 over signed metadata, key
//! trust/revocation, channel pinning, metadata expiry, monotonic version, and a
//! SHA-256 binding the signature to the actual binary bytes. [`plan_verified`] is
//! the front door: it produces an [`UpdatePlan`] *only* for a release that passes
//! every check, so an unverified binary can never reach [`apply_brain_only`].
//!
//! **C3 (this commit)** conducts the **adapter ripple** ([`adapter_update`]):
//! after self-update, spt-core updates each registered adapter via its manifest
//! `[update]` avenue ([`REQ-UPD-5`], [`REQ-SEAM-UPDATE`]). `file_pull` payloads
//! are verified against the **adapter's own** key (manifest-declared) + SHA-256
//! (adapter content signing, ADR-0004 §D); `delegated` updaters are opaque, so
//! spt-core delegates the trust only when the adapter attests `self_verifies`,
//! else skips them as unverifiable.
//!
//! **D4b (this commit)** makes broker-held QUIC streams survive brain restarts
//! **gaplessly and exactly-once** (Spike #3 invariants A–D as real tests): each
//! stream's reads are buffered in a sequenced ring (the B2 `OutputLog`
//! discipline) a brain subscribes to with a resume cursor; sends/opens carry
//! durable `op_id`s journaled at the broker, so a restarted brain's re-drive
//! dedups instead of double-writing the wire.
//!
//! **D4a (this commit)** moves the node's network presence into the broker
//! ([`nethost::NetHost`], ADR-0004 §B): a dedicated tokio runtime hosts the
//! iroh endpoint + every accepted/dialed QUIC connection, so a brain restart
//! never interrupts a cross-node connection. The brain drives net operations
//! (endpoint-status, dial) over new IPC frames in the same forward-compatible
//! catalog; an `op_id`-carrying dial is exactly-once through the B5
//! [`effect::EffectJournal`]. The endpoint binds as the durable node identity
//! (`spt-store`'s `node.key`), with the relay stance from `daemon.json`.
//!
//! **D5a (this commit)** puts WAN message delivery on the D4 substrate: a
//! [`WanMessage`](spt_net::net::wanmsg::WanMessage) NDJSON feed over a
//! broker-owned QUIC stream funnels through [`wan::receive_wan`] — the ADR-0009
//! access-gate seam ([`access`], handshake-proven origin only, KNOWN-HAZARDS
//! 7.5) plus durable op-id dedup (7.2, wire-fed) — into the existing local
//! REQ-MSG delivery path.
//!
//! **D5b (this commit)** adds remote terminal attach ([`attach`], REQ-INST-8):
//! a byte-stream viewport onto a local session for an operator daemon on
//! another node — session output pumped as sequenced
//! [`AttachRecord`](spt_net::net::attach::AttachRecord)s (deduped at the
//! operator's render cursor), operator input through the B5 exactly-once PTY
//! path, the whole loop gated and restart-survivable purely from the D4/B5
//! machinery.
//!
//! The brain + gapless handoff (B2) and the rest of the M3b task table build on
//! this seam. `spt-daemon` is internal supervisor machinery, **not** public SDK
//! (R-ARCH-2).
//!
//! [`Envelope`]: frame::Envelope
//! [`Hello`]: frame::Hello
//! [`DaemonTransport`]: transport::DaemonTransport

pub mod access;
pub mod adapter_update;
pub mod attach;
pub mod brain;
pub mod broker;
pub mod codec;
pub mod config;
pub mod consent;
pub mod daemon;
pub mod digest;
pub mod digesthub;
pub mod dispatch;
pub mod effect;
pub mod endpoint;
pub mod frame;
pub mod lifecycle;
pub mod msg;
pub mod nethost;
pub mod notif;
pub mod notifsync;
pub mod propagate;
pub mod psyrelay;
pub mod reconcile;
pub mod relay;
pub mod relcache;
pub mod release;
pub mod seedmap;
pub mod sync;
pub mod transport;
pub mod update;
pub mod wan;
pub mod xfer;

#[cfg(test)]
mod test_home;

pub use access::{access_check, classify_inbound, note_outbound, AccessDecision, InboundClass};
pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
pub use adapter_update::{
    conduct_ripple, plan_adapter_update, AdapterRipple, AdapterUpdateOutcome, FilePullPayload,
    SkipReason,
};
pub use brain::{Brain, BrainState, BrokerEvent};
pub use broker::Broker;
pub use config::{DaemonConfig, RelayChoice};
pub use consent::{decide as decide_consent, most_recently_active, ConsentDecision, ConsentTarget};
pub use daemon::{ensure_running, is_running, Daemon};
pub use digest::{ingest_chunk, DigestEngine, DigestUpdate};
pub use digesthub::{
    digest_to_json, follow, pull_snapshot, render_digest, render_update, serve_digest_control,
    update_to_json, DigestHub,
};
pub use effect::{EffectJournal, EffectKey, EffectKind, Outcome};
pub use endpoint::{broker_socket_name, daemon_pid_path, digest_socket_name, seed_socket_name};
pub use lifecycle::{BrainLifecycle, TickReport};
pub use nethost::{NetConfig, NetHost, NET_EFFECT_SESSION};
pub use notif::{
    first_fire, most_recently_active_visible, produce_and_first_fire, produce_consent_notif,
    resurface_at_boundary, FirstFireOutcome, NotifSurfacePolicy, ResurfaceOutcome,
    NOTIF_KIND_AGENT, NOTIF_KIND_CONSENT, NOTIF_KIND_PSYCHE, SUPPRESSION_WINDOW_MS,
};
pub use notifsync::{apply_notif_feed, emit_notif_feed, NotifApplyVerdict, NotifPolicy};
pub use propagate::{request_update, serve_update, UpdatePullOutcome, UpdateServeOutcome};
pub use psyrelay::{
    psyche_turn_and_relay, relay_psyche_outbound, PsycheNotifCtx, RelayOutcome, PSYCHE_BODY_CAP,
};
pub use relay::Relay;
pub use relcache::ReleaseCache;
pub use seedmap::{put_seed, take_seed, SeedRegistry};
pub use frame::{
    accept_hello, Envelope, HandshakeError, Hello, Role, IPC_PROTOCOL_VERSION,
    MIN_COMPATIBLE_VERSION,
};
pub use transport::{DaemonTransport, LocalSocketTransport};
pub use release::{
    parse_verifying_key, sha256_hex, verify_artifact, verify_metadata, verify_signature,
    RejectReason, ReleaseMetadata, SignedRelease, VerifyPolicy,
};
pub use update::{
    apply_brain_only, classify, plan_update, plan_verified, ApplyError, BrokerAbi, ReleaseSpec,
    UpdateClass, UpdatePlan, BROKER_RESOURCE_ABI,
};
pub use sync::{
    reconcile_after_sync, request_sync, select_refs, serve_sync, ReconcileWiring, SyncPolicy,
    SyncPullReport, SyncServeOutcome,
};
pub use xfer::{fetch_file, push_file, serve_xfer, XferServeOutcome};
pub use wan::{receive_wan, WanOutcome};

pub mod propagate;
pub mod psyrelay;
pub mod reconcile;
+pub mod registryhost;
pub mod relay;
pub mod relcache;
pub mod release;


D9-7: closeout sweep


                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision.holder.node);
                    }

                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);
                    }


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

                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision.holder.node);
                    }

                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);
                    }

-                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision.holder.node);

+                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);


Bash

cargo test -p spt-daemon --lib registryhost 2>&1 | grep -E "^error|test result|panicked|FAILED" -A3 | head -25

Compile + run registryhost unit tests


error[E0063]: missing fields `notif_pump_period`, `peer_pump`, `registry_pump_period` and 2 other fields in initializer of `config::DaemonConfig`
   --> crates\spt-daemon\src\config.rs:263:19
    |
263 |         let cfg = DaemonConfig {
--
error: could not compile `spt-daemon` (lib test) due to 1 previous error


250
251	    #[test] // [unit->REQ-DAEMON-1] a malformed config degrades to default, never wedges.
252	    fn malformed_file_degrades_to_default() {
253	        let dir = tempfile::tempdir().unwrap();
254	        let path = dir.path().join("daemon.json");
255	        std::fs::write(&path, "not json {{{").unwrap();
256	        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());
257	    }
258
259	    #[test] // [unit->REQ-DAEMON-1] save then load round-trips the configured period.
260	    fn save_load_roundtrip() {
261	        let dir = tempfile::tempdir().unwrap();
262	        let path = dir.path().join("daemon.json");
263	        let cfg = DaemonConfig {
264	            pulse_period: Duration::from_millis(750),
265	            protection_window_ms: 99,
266	            crash_grace: Duration::from_millis(1234),
267	            full_auto_update: true,
268	            relay: RelayChoice::Disabled,
269	        };
270	        cfg.save_to(&path).unwrap();
271	        assert_eq!(DaemonConfig::load_from(&path), cfg);
272	    }
273
274	    #[test] // [unit->REQ-UPD-4] full-auto update is OFF by default and config-driven.
275	    fn full_auto_update_is_gated_by_default() {
276	        assert!(!DaemonConfig::default().full_auto_update, "gated by default (consent required)");
277
278	        let dir = tempfile::tempdir().unwrap();
279	        let path = dir.path().join("daemon.json");
280	        std::fs::write(&path, r#"{"full_auto_update": true}"#).unwrap();
281	        assert!(DaemonConfig::load_from(&path).full_auto_update, "explicit opt-in honored");
282	        // An absent file keeps the gated default.
283	        assert!(!DaemonConfig::load_from(&dir.path().join("nope.json")).full_auto_update);
284	    }
285
286	    #[test] // [unit->REQ-NET-2] the relay stance is config-driven: "disabled" opts out,
287	            // absent/unknown values keep the zero-config n0 default, and it round-trips.
288	    fn relay_choice_is_config_driven() {
289	        assert_eq!(DaemonConfig::default().relay, RelayChoice::N0, "zero-config default");

    #[test] // [unit->REQ-DAEMON-1] a malformed config degrades to default, never wedges.
    fn malformed_file_degrades_to_default() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, "not json {{{").unwrap();
        assert_eq!(DaemonConfig::load_from(&path), DaemonConfig::default());
    }

    #[test] // [unit->REQ-DAEMON-1] save then load round-trips the configured period.
    fn save_load_roundtrip() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
            full_auto_update: true,
            relay: RelayChoice::Disabled,
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

    #[test] // [unit->REQ-UPD-4] full-auto update is OFF by default and config-driven.
    fn full_auto_update_is_gated_by_default() {
        assert!(!DaemonConfig::default().full_auto_update, "gated by default (consent required)");

        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("daemon.json");
        std::fs::write(&path, r#"{"full_auto_update": true}"#).unwrap();
        assert!(DaemonConfig::load_from(&path).full_auto_update, "explicit opt-in honored");
        // An absent file keeps the gated default.
        assert!(!DaemonConfig::load_from(&dir.path().join("nope.json")).full_auto_update);
    }

    #[test] // [unit->REQ-NET-2] the relay stance is config-driven: "disabled" opts out,
            // absent/unknown values keep the zero-config n0 default, and it round-trips.
    fn relay_choice_is_config_driven() {
        assert_eq!(DaemonConfig::default().relay, RelayChoice::N0, "zero-config default");


Edit

        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
            full_auto_update: true,
            relay: RelayChoice::Disabled,
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);

        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
            full_auto_update: true,
            relay: RelayChoice::Disabled,
            peer_pump: false,
            registry_pump_period: Duration::from_millis(11_000),
            notif_pump_period: Duration::from_millis(12_000),
            sync_pull_period: Duration::from_millis(13_000),
            update_check_period: Duration::from_millis(14_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);


        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
            full_auto_update: true,
            relay: RelayChoice::Disabled,
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);

        let cfg = DaemonConfig {
            pulse_period: Duration::from_millis(750),
            protection_window_ms: 99,
            crash_grace: Duration::from_millis(1234),
            full_auto_update: true,
            relay: RelayChoice::Disabled,
            peer_pump: false,
            registry_pump_period: Duration::from_millis(11_000),
            notif_pump_period: Duration::from_millis(12_000),
            sync_pull_period: Duration::from_millis(13_000),
            update_check_period: Duration::from_millis(14_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);

+            registry_pump_period: Duration::from_millis(11_000),

+            notif_pump_period: Duration::from_millis(12_000),

+            sync_pull_period: Duration::from_millis(13_000),

+            update_check_period: Duration::from_millis(14_000),


Bash

cargo test -p spt-daemon --lib 2>&1 | grep -E "^error|test result|panicked|FAILED" -A3 | head -25

Lib tests incl. registryhost + config


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


117 pass. Wiring registry arm into dispatcher:


Edit

use spt_net::net::notif::NotifDecoder;
use spt_net::net::wanmsg::WanDecoder;
use spt_store::contextstore::ContextStore;
use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::trust::TrustStore;

use crate::brain::{Brain, BrokerEvent};
use crate::notifsync::{apply_notif_feed, NotifPolicy};
use crate::propagate::serve_update;
use crate::relcache::ReleaseCache;
use crate::sync::{serve_sync, SyncPolicy};
use crate::wan::receive_wan;

use spt_net::net::notif::NotifDecoder;
use spt_net::net::replicate::LineDecoder as RegistryDecoder;
use spt_net::net::wanmsg::WanDecoder;
use spt_store::contextstore::ContextStore;
use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::trust::TrustStore;

use crate::brain::{Brain, BrokerEvent};
use crate::notifsync::{apply_notif_feed, NotifPolicy};
use crate::propagate::serve_update;
use crate::registryhost::{RegistryGatePolicy, RegistryHost};
use crate::relcache::ReleaseCache;
use crate::sync::{serve_sync, SyncPolicy};
use crate::wan::receive_wan;


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

use spt_net::net::notif::NotifDecoder;
use spt_net::net::wanmsg::WanDecoder;
use spt_store::contextstore::ContextStore;
use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::trust::TrustStore;

use crate::brain::{Brain, BrokerEvent};
use crate::notifsync::{apply_notif_feed, NotifPolicy};
use crate::propagate::serve_update;
use crate::relcache::ReleaseCache;
use crate::sync::{serve_sync, SyncPolicy};
use crate::wan::receive_wan;

use spt_net::net::notif::NotifDecoder;
use spt_net::net::replicate::LineDecoder as RegistryDecoder;
use spt_net::net::wanmsg::WanDecoder;
use spt_store::contextstore::ContextStore;
use spt_store::notif::NotifStore;
use spt_store::perch;
use spt_store::trust::TrustStore;

use crate::brain::{Brain, BrokerEvent};
use crate::notifsync::{apply_notif_feed, NotifPolicy};
use crate::propagate::serve_update;
use crate::registryhost::{RegistryGatePolicy, RegistryHost};
use crate::relcache::ReleaseCache;
use crate::sync::{serve_sync, SyncPolicy};
use crate::wan::receive_wan;

+use spt_net::net::replicate::LineDecoder as RegistryDecoder;


pub fn run_dispatch_loop(
    broker_name: &str,
    paths: &DispatchPaths,
    stop: &AtomicBool,
    poll: Duration,
) -> io::Result<()> {
    let mut brain = connect(broker_name)?;
    let mut claimed: HashSet<u64> = HashSet::new();

    while !stop.load(Ordering::Relaxed) {
        let reply = brain.net_streams()?;
        for s in reply.streams {
            // Claim only peer-initiated rows with bytes (a QUIC stream is
            // invisible until its first write anyway — gotcha #6 — but the
            // next_seq guard also skips a row whose pump hasn't appended yet).
            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {
                continue;
            }
            let name = broker_name.to_string();
            let origin = s.remote_id_hex.clone();
            let stream_id = s.stream_id;
            let p = paths.clone();
            thread::spawn(move || {
                let outcome = worker(&name, stream_id, &origin, &p);
                match outcome {
                    DispatchOutcome::Served(_) => {}
                    other => eprintln!("DISPATCH:{stream_id}:{other:?}"),
                }
            });
        }
        thread::sleep(poll);
    }
    Ok(())
}

pub fn run_dispatch_loop(
    broker_name: &str,
    paths: &DispatchPaths,
    registry: Arc<RegistryHost>,
    stop: &AtomicBool,
    poll: Duration,
) -> io::Result<()> {
    let mut brain = connect(broker_name)?;
    let mut claimed: HashSet<u64> = HashSet::new();

    while !stop.load(Ordering::Relaxed) {
        let reply = brain.net_streams()?;
        for s in reply.streams {
            // Claim only peer-initiated rows with bytes (a QUIC stream is
            // invisible until its first write anyway — gotcha #6 — but the
            // next_seq guard also skips a row whose pump hasn't appended yet).
            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {
                continue;
            }
            let name = broker_name.to_string();
            let origin = s.remote_id_hex.clone();
            let stream_id = s.stream_id;
            let p = paths.clone();
            let reg = Arc::clone(&registry);
            thread::spawn(move || {
                let outcome = worker(&name, stream_id, &origin, &p, &reg);
                match outcome {
                    DispatchOutcome::Served(_) => {}
                    other => eprintln!("DISPATCH:{stream_id}:{other:?}"),
                }
            });
        }
        thread::sleep(poll);
    }
    Ok(())
}


pub fn run_dispatch_loop(
    broker_name: &str,
    paths: &DispatchPaths,
    stop: &AtomicBool,
    poll: Duration,
) -> io::Result<()> {
    let mut brain = connect(broker_name)?;
    let mut claimed: HashSet<u64> = HashSet::new();

    while !stop.load(Ordering::Relaxed) {
        let reply = brain.net_streams()?;
        for s in reply.streams {
            // Claim only peer-initiated rows with bytes (a QUIC stream is
            // invisible until its first write anyway — gotcha #6 — but the
            // next_seq guard also skips a row whose pump hasn't appended yet).
            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {
                continue;
            }
            let name = broker_name.to_string();
            let origin = s.remote_id_hex.clone();
            let stream_id = s.stream_id;
            let p = paths.clone();
            thread::spawn(move || {
                let outcome = worker(&name, stream_id, &origin, &p);
                match outcome {
                    DispatchOutcome::Served(_) => {}
                    other => eprintln!("DISPATCH:{stream_id}:{other:?}"),
                }
            });
        }
        thread::sleep(poll);
    }
    Ok(())
}

pub fn run_dispatch_loop(
    broker_name: &str,
    paths: &DispatchPaths,
    registry: Arc<RegistryHost>,
    stop: &AtomicBool,
    poll: Duration,
) -> io::Result<()> {
    let mut brain = connect(broker_name)?;
    let mut claimed: HashSet<u64> = HashSet::new();

    while !stop.load(Ordering::Relaxed) {
        let reply = brain.net_streams()?;
        for s in reply.streams {
            // Claim only peer-initiated rows with bytes (a QUIC stream is
            // invisible until its first write anyway — gotcha #6 — but the
            // next_seq guard also skips a row whose pump hasn't appended yet).
            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {
                continue;
            }
            let name = broker_name.to_string();
            let origin = s.remote_id_hex.clone();
            let stream_id = s.stream_id;
            let p = paths.clone();
            let reg = Arc::clone(&registry);
            thread::spawn(move || {
                let outcome = worker(&name, stream_id, &origin, &p, &reg);
                match outcome {
                    DispatchOutcome::Served(_) => {}
                    other => eprintln!("DISPATCH:{stream_id}:{other:?}"),
                }
            });
        }
        thread::sleep(poll);
    }
    Ok(())
}

pub fn run_dispatch_loop(
broker_name: &str,
paths: &DispatchPaths,
+    registry: Arc<RegistryHost>,
stop: &AtomicBool,
poll: Duration,
) -> io::Result<()> {

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

-                let outcome = worker(&name, stream_id, &origin, &p);

+                let outcome = worker(&name, stream_id, &origin, &p, &reg);


/// One claimed stream's worker: peek → classify → run the family's serve
/// loop on a fresh brain connection.
fn worker(broker_name: &str, stream_id: u64, origin: &str, paths: &DispatchPaths) -> DispatchOutcome {

/// One claimed stream's worker: peek → classify → run the family's serve
/// loop on a fresh brain connection.
fn worker(
    broker_name: &str,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
    registry: &RegistryHost,
) -> DispatchOutcome {


/// One claimed stream's worker: peek → classify → run the family's serve
/// loop on a fresh brain connection.
fn worker(broker_name: &str, stream_id: u64, origin: &str, paths: &DispatchPaths) -> DispatchOutcome {

/// One claimed stream's worker: peek → classify → run the family's serve
/// loop on a fresh brain connection.
fn worker(
    broker_name: &str,
    stream_id: u64,
    origin: &str,
    paths: &DispatchPaths,
    registry: &RegistryHost,
) -> DispatchOutcome {

-fn worker(broker_name: &str, stream_id: u64, origin: &str, paths: &DispatchPaths) -> DispatchOutcome {

let line = match peek_first_line(broker_name, stream_id) {

Ok(None) => return DispatchOutcome::Unknown,


        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
        StreamFamily::Registry | StreamFamily::Attach | StreamFamily::Xfer => {
            DispatchOutcome::Unwired(family)
        }
        StreamFamily::Unknown => DispatchOutcome::Unknown,
    }
}

        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
        StreamFamily::Attach | StreamFamily::Xfer => DispatchOutcome::Unwired(family),
        StreamFamily::Unknown => DispatchOutcome::Unknown,
    }
}

/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into
/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {
                let updates = decoder.push(&bytes);
                if updates.is_empty() {
                    continue;
                }
                merged += registry.apply_feed(origin, &updates, &policy).len();
            }
            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {
                return DispatchOutcome::Served(format!("registry feed: {merged} record(s)"));
            }
            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),
            Ok(_) => {}
            Err(e) => return DispatchOutcome::Failed(e.to_string()),
        }
    }
}


        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
        StreamFamily::Registry | StreamFamily::Attach | StreamFamily::Xfer => {
            DispatchOutcome::Unwired(family)
        }
        StreamFamily::Unknown => DispatchOutcome::Unknown,
    }
}

        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths),
        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
        StreamFamily::Attach | StreamFamily::Xfer => DispatchOutcome::Unwired(family),
        StreamFamily::Unknown => DispatchOutcome::Unknown,
    }
}

/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into
/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {
                let updates = decoder.push(&bytes);
                if updates.is_empty() {
                    continue;
                }
                merged += registry.apply_feed(origin, &updates, &policy).len();
            }
            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {
                return DispatchOutcome::Served(format!("registry feed: {merged} record(s)"));
            }
            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),
            Ok(_) => {}
            Err(e) => return DispatchOutcome::Failed(e.to_string()),
        }
    }
}

-        StreamFamily::Registry | StreamFamily::Attach | StreamFamily::Xfer => {

-            DispatchOutcome::Unwired(family)

+        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),

+        StreamFamily::Attach | StreamFamily::Xfer => DispatchOutcome::Unwired(family),

+/// Registry feed arm: decode `RegistryUpdate` lines, merge each batch into

+/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧

+/// origin trusted in that subnet), until the peer finishes.

+    let policy = RegistryGatePolicy::load();

+    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {

+        return DispatchOutcome::Failed(e.to_string());

+    let mut decoder = RegistryDecoder::new();

+            Ok(BrokerEvent::NetStreamData { stream_id: sid, bytes, .. }) if sid == stream_id => {

+                let updates = decoder.push(&bytes);

+                if updates.is_empty() {

+                merged += registry.apply_feed(origin, &updates, &policy).len();

+            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {

+                return DispatchOutcome::Served(format!("registry feed: {merged} record(s)"));

+            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),

+            Err(e) => return DispatchOutcome::Failed(e.to_string()),

/// Notif feed arm: decode rows as they arrive, apply each batch under the

/// fail-closed gate, until the peer finishes. The policy re-loads per feed

/// (not per record) — the same freshness the other arms get per request.


/// Spawn the dispatcher on its own thread against the canonical broker
/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).
pub fn spawn_dispatcher(broker_name: String, stop: Arc<AtomicBool>) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        let paths = DispatchPaths::production();
        if let Err(e) = run_dispatch_loop(&broker_name, &paths, &stop, DEFAULT_DISPATCH_POLL) {
            eprintln!("DISPATCH_LOOP_FAIL: {e}");
        }
    })
}

/// Spawn the dispatcher on its own thread against the canonical broker
/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).
pub fn spawn_dispatcher(
    broker_name: String,
    registry: Arc<RegistryHost>,
    stop: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        let paths = DispatchPaths::production();
        if let Err(e) =
            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)
        {
            eprintln!("DISPATCH_LOOP_FAIL: {e}");
        }
    })
}


/// Spawn the dispatcher on its own thread against the canonical broker
/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).
pub fn spawn_dispatcher(broker_name: String, stop: Arc<AtomicBool>) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        let paths = DispatchPaths::production();
        if let Err(e) = run_dispatch_loop(&broker_name, &paths, &stop, DEFAULT_DISPATCH_POLL) {
            eprintln!("DISPATCH_LOOP_FAIL: {e}");
        }
    })
}

/// Spawn the dispatcher on its own thread against the canonical broker
/// socket — the daemon-boot entry ([`crate::daemon::Daemon::run`]).
pub fn spawn_dispatcher(
    broker_name: String,
    registry: Arc<RegistryHost>,
    stop: Arc<AtomicBool>,
) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        let paths = DispatchPaths::production();
        if let Err(e) =
            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)
        {
            eprintln!("DISPATCH_LOOP_FAIL: {e}");
        }
    })
}

-pub fn spawn_dispatcher(broker_name: String, stop: Arc<AtomicBool>) -> thread::JoinHandle<()> {

-        if let Err(e) = run_dispatch_loop(&broker_name, &paths, &stop, DEFAULT_DISPATCH_POLL) {

+            run_dispatch_loop(&broker_name, &paths, registry, &stop, DEFAULT_DISPATCH_POLL)


Bash

grep -n "pub fn public_key\|pub fn to_hex\|impl Identity" crates/spt-proto/src/identity.rs | head -6

Identity pubkey hex API


58:impl Identity {
81:    pub fn public_key(&self) -> PublicKey {
121:    pub fn to_hex(&self) -> String {


Edit

        let net = {
            let cfg = DaemonConfig::load();
            let relay = match cfg.relay {
                RelayChoice::N0 => RelayPolicy::N0Default,
                RelayChoice::Disabled => RelayPolicy::Disabled,
            };
            match spt_store::nodeid::load_or_create() {
                Ok(identity) => {
                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };
                    match NetHost::start(net_cfg) {
                        Ok(host) => Some(host),
                        Err(e) => {
                            eprintln!("NET_BIND_FAIL: {e} — broker runs net-less");
                            None
                        }
                    }
                }
                Err(e) => {
                    eprintln!("NODE_KEY_FAIL: {e} — broker runs net-less");
                    None
                }
            }
        };

        let mut node_hex: Option<String> = None;
        let net = {
            let cfg = DaemonConfig::load();
            let relay = match cfg.relay {
                RelayChoice::N0 => RelayPolicy::N0Default,
                RelayChoice::Disabled => RelayPolicy::Disabled,
            };
            match spt_store::nodeid::load_or_create() {
                Ok(identity) => {
                    node_hex = Some(identity.public_key().to_hex());
                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };
                    match NetHost::start(net_cfg) {
                        Ok(host) => Some(host),
                        Err(e) => {
                            eprintln!("NET_BIND_FAIL: {e} — broker runs net-less");
                            None
                        }
                    }
                }
                Err(e) => {
                    eprintln!("NODE_KEY_FAIL: {e} — broker runs net-less");
                    None
                }
            }
        };


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

        let net = {
            let cfg = DaemonConfig::load();
            let relay = match cfg.relay {
                RelayChoice::N0 => RelayPolicy::N0Default,
                RelayChoice::Disabled => RelayPolicy::Disabled,
            };
            match spt_store::nodeid::load_or_create() {
                Ok(identity) => {
                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };
                    match NetHost::start(net_cfg) {
                        Ok(host) => Some(host),
                        Err(e) => {
                            eprintln!("NET_BIND_FAIL: {e} — broker runs net-less");
                            None
                        }
                    }
                }
                Err(e) => {
                    eprintln!("NODE_KEY_FAIL: {e} — broker runs net-less");
                    None
                }
            }
        };

        let mut node_hex: Option<String> = None;
        let net = {
            let cfg = DaemonConfig::load();
            let relay = match cfg.relay {
                RelayChoice::N0 => RelayPolicy::N0Default,
                RelayChoice::Disabled => RelayPolicy::Disabled,
            };
            match spt_store::nodeid::load_or_create() {
                Ok(identity) => {
                    node_hex = Some(identity.public_key().to_hex());
                    let net_cfg = NetConfig {
                        identity,
                        relay,
                        discovery: LocalDiscovery::Mdns,
                        scope: BindScope::All, // production: real interfaces
                    };
                    match NetHost::start(net_cfg) {
                        Ok(host) => Some(host),
                        Err(e) => {
                            eprintln!("NET_BIND_FAIL: {e} — broker runs net-less");
                            None
                        }
                    }
                }
                Err(e) => {
                    eprintln!("NODE_KEY_FAIL: {e} — broker runs net-less");
                    None
                }
            }
        };

// net failure degrades to a net-less broker — it must not cost PTY

// hosting (mirrors the broker-bind soft-fail below).

+        let mut node_hex: Option<String> = None;

+                    node_hex = Some(identity.public_key().to_hex());


                // Inbound net dispatch (D9-1): the daemon-side consumer of
                // peer-initiated streams. Only meaningful with a net host —
                // a net-less broker accepts nothing to dispatch.
                if net_up {
                    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
                    let _ = crate::dispatch::spawn_dispatcher(broker_socket_name(), stop);
                }

                // Inbound net dispatch + outbound peer pump (D9-1): the
                // production trigger loops. Only meaningful with a net host —
                // a net-less broker accepts nothing and dials nobody.
                if net_up {
                    let hex = node_hex.clone().unwrap_or_default();
                    let registry = Arc::new(crate::registryhost::RegistryHost::new(&hex));
                    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
                    let _ = crate::dispatch::spawn_dispatcher(
                        broker_socket_name(),
                        Arc::clone(&registry),
                        Arc::clone(&stop),
                    );
                    let cfg = DaemonConfig::load();
                    if cfg.peer_pump {
                        let _ = crate::peerloop::spawn_peer_pump(
                            broker_socket_name(),
                            registry,
                            cfg,
                            stop,
                        );
                    }
                }


                // Inbound net dispatch (D9-1): the daemon-side consumer of
                // peer-initiated streams. Only meaningful with a net host —
                // a net-less broker accepts nothing to dispatch.
                if net_up {
                    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
                    let _ = crate::dispatch::spawn_dispatcher(broker_socket_name(), stop);
                }

                // Inbound net dispatch + outbound peer pump (D9-1): the
                // production trigger loops. Only meaningful with a net host —
                // a net-less broker accepts nothing and dials nobody.
                if net_up {
                    let hex = node_hex.clone().unwrap_or_default();
                    let registry = Arc::new(crate::registryhost::RegistryHost::new(&hex));
                    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
                    let _ = crate::dispatch::spawn_dispatcher(
                        broker_socket_name(),
                        Arc::clone(&registry),
                        Arc::clone(&stop),
                    );
                    let cfg = DaemonConfig::load();
                    if cfg.peer_pump {
                        let _ = crate::peerloop::spawn_peer_pump(
                            broker_socket_name(),
                            registry,
                            cfg,
                            stop,
                        );
                    }
                }

-                // Inbound net dispatch (D9-1): the daemon-side consumer of

-                // peer-initiated streams. Only meaningful with a net host —

-                // a net-less broker accepts nothing to dispatch.

+                // Inbound net dispatch + outbound peer pump (D9-1): the

+                // production trigger loops. Only meaningful with a net host —

+                // a net-less broker accepts nothing and dials nobody.

+                    let hex = node_hex.clone().unwrap_or_default();

+                    let registry = Arc::new(crate::registryhost::RegistryHost::new(&hex));

-                    let _ = crate::dispatch::spawn_dispatcher(broker_socket_name(), stop);

+                    let _ = crate::dispatch::spawn_dispatcher(

+                        broker_socket_name(),

+                        Arc::clone(&registry),

+                        Arc::clone(&stop),

+                    let cfg = DaemonConfig::load();

+                        let _ = crate::peerloop::spawn_peer_pump(

+                            broker_socket_name(),

Err(e) => eprintln!("BROKER_BIND_FAIL: {e}"),


Now `peerloop.rs` — the outbound pump host: