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

//! Registry replication over the wire (M4-D4d) — transport → merge, nothing more.
//!
//! D3 built and hermetically proved the merge rule ([`SubnetRegistry::merge_instance`],
//! epoch lease, REQ-HAZARD-REGISTRY-EPOCH-LEASE). D4a/b gave the broker-owned QUIC
//! streams a brain can drive across restarts. This module is the thin seam between
//! them: a minimal registry-update record, a byte-stream framing that survives QUIC
//! read-chunk coalescing/splitting, and an apply function that feeds inbound updates
//! straight into the existing merge seam.
//!
//! ## Wire format: NDJSON
//!
//! One JSON object per `\n`-terminated line — the shared framing in
//! [`crate::net::ndjson`] (extracted there when D5a's `WanMessage` became the
//! second record type riding it). Unknown fields decode, undecodable lines are
//! skipped; the epoch lease makes replay/loss safe.
//!
//! ## Authoring and applying
//!
//! Outbound updates are authored from the node's own [`EpochSource`]-stamped
//! [`Instance`] — the same value the local advertise path inserted (the join/advertise
//! gates, D3d/D3e, run **locally before** an update is ever emitted; the wire carries
//! only already-admitted state). Inbound updates go straight through
//! [`SubnetRegistry::merge_instance`] — exactly the path [`SubnetRegistry::join_endpoint`]
//! documents for "replication of an existing instance via the D4 wire". An update for
//! a subnet this node is not a member of is **dropped** (fail-closed, mirroring the
//! REQ-INST-13 sync gate: unconfigured syncs nowhere).
// [impl->REQ-INST-7]

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

use crate::net::ndjson::{self, NdjsonDecoder};
use crate::net::registry::{Instance, MergeOutcome, SubnetRegistry};

/// One registry replication record: an endpoint's instance state in one subnet,
/// as authored by the node it runs on (the `instance.epoch` is that node's
/// monotonic [`spt_store::epoch::EpochSource`] stamp — the merge-precedence key).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RegistryUpdate {
    /// The subnet whose registry this update targets.
    pub subnet: String,
    /// The endpoint the instance belongs to.
    pub endpoint_id: String,
    /// The instance state (node, status, author epoch).
    pub instance: Instance,
}

impl RegistryUpdate {
    /// Encode as one NDJSON line (the JSON document + `\n`).
    pub fn encode_line(&self) -> Vec<u8> {
        ndjson::encode_line(self)
    }
}

/// The registry feed's NDJSON reassembler — the shared
/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to
/// [`RegistryUpdate`] records. One decoder per stream.
// [impl->REQ-INST-7]
pub type LineDecoder = NdjsonDecoder<RegistryUpdate>;

/// Apply one inbound update to this node's per-subnet registries: route by
/// `subnet` and feed [`SubnetRegistry::merge_instance`] (the epoch lease
/// governs — a stale update over the wire is dropped exactly like a stale
/// local one). `None` when this node holds no registry for the subnet — a
/// non-member **drops** the update rather than materializing a subnet it was
/// never admitted to (fail-closed, the REQ-INST-13 posture).
// [impl->REQ-INST-7]
pub fn apply_update(
    registries: &mut BTreeMap<String, SubnetRegistry>,
    update: RegistryUpdate,
) -> Option<MergeOutcome> {
    let reg = registries.get_mut(&update.subnet)?;
    Some(reg.merge_instance(&update.endpoint_id, update.instance))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::net::registry::Status;

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

    // [unit->REQ-INST-7] a record round-trips through the NDJSON line encoding,
    // and an unknown future field decodes (wire forward-compat).
    #[test]
    fn update_round_trips_and_tolerates_unknown_field() {
        let u = upd("home", "doyle", "cafe", Status::Active, 7);
        let line = u.encode_line();
        assert_eq!(line.last(), Some(&b'\n'));
        let back: RegistryUpdate = serde_json::from_slice(&line[..line.len() - 1]).unwrap();
        assert_eq!(back, u);

        let wire = br#"{"subnet":"home","endpoint_id":"doyle","instance":{"node":"cafe","status":"Active","epoch":7},"added_at_d6":true}"#;
        let future: RegistryUpdate = serde_json::from_slice(wire).unwrap();
        assert_eq!(future.instance.epoch, 7);
    }

    // [unit->REQ-INST-7] the decoder reassembles records across arbitrary chunk
    // boundaries (QUIC reads coalesce/split writes), keeps a partial tail
    // buffered, and skips an undecodable line without wedging the feed.
    #[test]
    fn decoder_survives_chunk_splits_and_corrupt_lines() {
        let a = upd("home", "doyle", "n1", Status::Active, 1);
        let b = upd("home", "echo", "n2", Status::Dormant, 4);

        let mut wire = a.encode_line();
        wire.extend_from_slice(b"this is not json\n");
        wire.extend_from_slice(&b.encode_line());

        // Split mid-record: every prefix length must yield the same final set.
        let mut dec = LineDecoder::new();
        let split = wire.len() / 2 + 3; // lands inside a record on purpose
        let mut got = dec.push(&wire[..split]);
        got.extend(dec.push(&wire[split..]));
        assert_eq!(got, vec![a.clone(), b.clone()], "reassembled in wire order, junk skipped");

        // A partial tail (no newline yet) emits nothing until completed.
        let mut dec = LineDecoder::new();
        let line = a.encode_line();
        assert!(dec.push(&line[..line.len() - 1]).is_empty(), "no newline, no record");
        assert_eq!(dec.push(&line[line.len() - 1..]), vec![a]);
    }

    // [unit->REQ-INST-7] apply routes by subnet into merge_instance — and an
    // update for a subnet this node is no member of is dropped fail-closed
    // (no registry materialized), mirroring the REQ-INST-13 sync posture.
    #[test]
    fn apply_routes_by_subnet_and_drops_non_member() {
        let mut regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
        regs.insert("home".into(), SubnetRegistry::new());

        let out = apply_update(&mut regs, upd("home", "doyle", "n1", Status::Active, 3));
        assert_eq!(out, Some(MergeOutcome::Inserted));
        assert_eq!(regs["home"].instances("doyle").len(), 1);

        let dropped = apply_update(&mut regs, upd("elsewhere", "doyle", "n1", Status::Active, 3));
        assert_eq!(dropped, None, "non-member subnet: dropped");
        assert!(!regs.contains_key("elsewhere"), "never materialized");
    }

    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] the lease governs the wire path:
    // a lagging Active replayed after a newer Offline merges Stale and the
    // stored row is untouched — same rule, now fed by decoded wire records.
    #[test]
    fn stale_wire_update_cannot_clobber_newer_state() {
        let mut regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
        regs.insert("home".into(), SubnetRegistry::new());

        assert_eq!(
            apply_update(&mut regs, upd("home", "doyle", "n1", Status::Offline, 9)),
            Some(MergeOutcome::Inserted)
        );
        assert_eq!(
            apply_update(&mut regs, upd("home", "doyle", "n1", Status::Active, 5)),
            Some(MergeOutcome::Stale),
            "lagging Active dropped"
        );
        let row = &regs["home"].instances("doyle")[0];
        assert_eq!((row.status, row.epoch), (Status::Offline, 9));
    }
}

-            instance: Instance { node: node.into(), status, epoch },

+            instance: Instance { node: node.into(), status, epoch, resources: None },


Edit

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

Instance { node: node.to_string(), status, epoch: 1, resources: None }


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

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


instance: Instance { node: b_hex.clone(), status: Status::Active, epoch: 1 },

instance: Instance { node: b_hex.clone(), status: Status::Active, epoch: 1, resources: None },


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

-                instance: Instance { node: b_hex.clone(), status: Status::Active, epoch: 1 },

+                instance: Instance { node: b_hex.clone(), status: Status::Active, epoch: 1, resources: None },


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

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


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

instance: Instance { node: node.to_string(), status: Status::Active, epoch, resources: None },


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

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

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


instance: Instance { node: node.to_string(), status, epoch },

instance: Instance { node: node.to_string(), status, epoch, resources: None },


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)

-            instance: Instance { node: node.to_string(), status: Status::Active, epoch },

+            instance: Instance { node: node.to_string(), status: Status::Active, epoch, resources: None },


-                instance: Instance { node: node.to_string(), status, epoch },

+                instance: Instance { node: node.to_string(), status, epoch, resources: None },


D9-3: dormancy budget measurement + DORMANCY-BUDGET.md + ADR-0003 #9 close

SPT_BUDGET=1 harness (done, uncommitted). HFENDULEAM runs done (shell 7.7MiB, claude 308MiB, zero idle CPU). Remaining: gravity-linux leg (workflow_dispatch budget job on gravity runner), docs/DORMANCY-BUDGET.md (methodology + numbers + policy lock, doc->REQ-INST-3), ADR-0003 #9 amendment, CONTEXT.md policy confirm. CI green.

D9-4: REQ-INST-14 resource advertisement (blurb on Instance + spt resources CLI + projection)

resources: Option<String> on Instance (serde-default, rides RegistryUpdate + lease); resource_projection over visible+routable rows; info.json resources field + set_resources; daemon.json resources_blurb node seed (drop Copy on DaemonConfig); advertise_local fills endpoint-else-node blurb; spt resources set/show/list CLI; CONTEXT doc tag; activate REQ-INST-14 [doc, impl, unit]. CI green.


95	mod tests {
96	    use super::*;
97	    use crate::net::registry::{Instance, Status};
98
99	    fn resolved(node: &str) -> Resolution {
100	        Resolution::Resolved {
101	            subnet: Some("home".into()),
102	            instance: Instance { node: node.into(), status: Status::Active, epoch: 1 },
103	        }
104	    }
105
106	    // [unit->REQ-NET-1] a record round-trips through the NDJSON line encoding,

mod tests {
    use super::*;
    use crate::net::registry::{Instance, Status};

    fn resolved(node: &str) -> Resolution {
        Resolution::Resolved {
            subnet: Some("home".into()),
            instance: Instance { node: node.into(), status: Status::Active, epoch: 1 },
        }
    }

    // [unit->REQ-NET-1] a record round-trips through the NDJSON line encoding,


348	    let wt_a = root_a.join("agents/doyle");
349	    let wt_b = root_b.join("agents/doyle");
350	    assert_eq!(a.list_conflicts(&wt_a, None).unwrap().len(), 1, "A holds B's version");
351	    assert_eq!(b.list_conflicts(&wt_b, None).unwrap().len(), 1, "B holds A's version");
352
353	    // Election: B's node holds the Active instance — A must stand down.
354	    let rows = vec![
355	        Instance { node: "node-b".into(), status: Status::Active, epoch: 2 },
356	        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1 },
357	    ];
358	    let runtime = merger_runtime();
359	    let wiring_a = ReconcileWiring {
360	        rows: &rows,
361	        self_node: "node-a",

    let wt_a = root_a.join("agents/doyle");
    let wt_b = root_b.join("agents/doyle");
    assert_eq!(a.list_conflicts(&wt_a, None).unwrap().len(), 1, "A holds B's version");
    assert_eq!(b.list_conflicts(&wt_b, None).unwrap().len(), 1, "B holds A's version");

    // Election: B's node holds the Active instance — A must stand down.
    let rows = vec![
        Instance { node: "node-b".into(), status: Status::Active, epoch: 2 },
        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1 },
    ];
    let runtime = merger_runtime();
    let wiring_a = ReconcileWiring {
        rows: &rows,
        self_node: "node-a",


104	    let mut a_regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
105	    a_regs.insert("home".into(), SubnetRegistry::new());
106
107	    // The advertise gate runs LOCALLY, before anything is emitted: the wire
108	    // only ever carries already-admitted state (D3d collision + D3e visibility).
109	    let e1 = epochs.next_epoch().expect("epoch");
110	    let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1 };
111	    let admitted = advertise_if_visible(
112	        a_regs.get_mut("home").expect("member"),
113	        &visibility,
114	        "home",
115	        false,
116	        "doyle",
117	        active.clone(),
118	    )
119	    .expect("no collision")
120	    .expect("visible — advertised");
121	    assert_eq!(admitted, MergeOutcome::Inserted);
122
123	    let update_active = RegistryUpdate {
124	        subnet: "home".into(),
125	        endpoint_id: "doyle".into(),
126	        instance: active,
127	    };
128	    let e2 = epochs.next_epoch().expect("epoch");
129	    assert!(e2 > e1, "EpochSource is strictly monotonic");
130	    let update_offline = RegistryUpdate {
131	        subnet: "home".into(),
132	        endpoint_id: "doyle".into(),
133	        instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2 },
134	    };
135
136	    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────
137	    let mut b_probe = connect_retry(&name_b);

    let mut a_regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
    a_regs.insert("home".into(), SubnetRegistry::new());

    // The advertise gate runs LOCALLY, before anything is emitted: the wire
    // only ever carries already-admitted state (D3d collision + D3e visibility).
    let e1 = epochs.next_epoch().expect("epoch");
    let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1 };
    let admitted = advertise_if_visible(
        a_regs.get_mut("home").expect("member"),
        &visibility,
        "home",
        false,
        "doyle",
        active.clone(),
    )
    .expect("no collision")
    .expect("visible — advertised");
    assert_eq!(admitted, MergeOutcome::Inserted);

    let update_active = RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: active,
    };
    let e2 = epochs.next_epoch().expect("epoch");
    assert!(e2 > e1, "EpochSource is strictly monotonic");
    let update_offline = RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2 },
    };

    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────
    let mut b_probe = connect_retry(&name_b);


44
45	#[cfg(test)]
46	mod tests {
47	    use super::*;
48
49	    fn inst(node: &str, status: Status) -> Instance {
50	        Instance { node: node.to_string(), status, epoch: 1 }
51	    }
52
53	    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.
54	    fn active_node_wins() {
55	        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Dormant)];

#[cfg(test)]
mod tests {
    use super::*;

    fn inst(node: &str, status: Status) -> Instance {
        Instance { node: node.to_string(), status, epoch: 1 }
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.
    fn active_node_wins() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Dormant)];


instance: Instance { node: node.into(), status: Status::Active, epoch: 1 },

instance: Instance { node: node.into(), status: Status::Active, epoch: 1, resources: None },


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

//! WAN message records over broker QUIC streams (M4-D5a) — the REQ-MSG path,
//! now off-node.
//!
//! D4 gave the broker-owned streams a brain can drive across restarts; D3 gave
//! addressing (per-subnet registry + the refuse-ambiguous resolution policy).
//! This module is the thin wire seam between them and the existing local
//! delivery path: a minimal message record, the shared NDJSON framing
//! ([`crate::net::ndjson`]), and a routing helper that decides local-vs-WAN
//! from a [`Resolution`]. The receive-side funnel (op-id dedup → access gate →
//! `spt_msg::deliver`) lives in the daemon, which owns the perch/owlery
//! machinery.
//!
//! ## Origin is transport truth, never payload (REQ-HAZARD-WAN-ORIGIN-AUTH)
//!
//! A [`WanMessage`] deliberately carries **no origin-node field**: the access
//! gate's subject (ADR-0009 origin-node whitelist) is the QUIC connection's
//! handshake-proven remote node id (iroh `EndpointId` == the Ed25519 node
//! pubkey, the REQ-NET-1 identity binding) — read from the broker's stream
//! table, never from record bytes. A forged `origin_node`/`node` field inside
//! the JSON decodes as an ignored unknown field and influences nothing. The
//! `from` field is reply-routing metadata (`__REPLY_TO__`), not an
//! authorization subject.

use serde::{Deserialize, Serialize};

use crate::net::ndjson::{self, NdjsonDecoder};
use crate::net::registry::{Ambiguity, Resolution};

/// One WAN message record: a message for `target` (an endpoint id at the
/// receiving node), as sent by `from` (reply-routing metadata). `op_id` is the
/// sender-minted durable idempotency key — the receiver dedups on it before
/// delivering, so a replayed feed (sender redrive, receiver brain restart
/// resubscribing from an earlier cursor) delivers exactly once
/// (REQ-HAZARD-RESTART-IDEMPOTENT, now wire-fed).
// [impl->REQ-NET-1]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WanMessage {
    /// The endpoint the message is for (resolved at the receiving node).
    pub target: String,
    /// The sender endpoint id — the `__REPLY_TO__` line at delivery (empty =
    /// anonymous). Metadata only; never an authorization subject.
    pub from: String,
    /// The message body.
    pub body: String,
    /// Sender-minted durable idempotency key, unique per logical send (e.g.
    /// `<sender-node-hex>:<epoch>` from the node's own `EpochSource`).
    pub op_id: String,
}

impl WanMessage {
    /// Encode as one NDJSON line (the JSON document + `\n`).
    pub fn encode_line(&self) -> Vec<u8> {
        ndjson::encode_line(self)
    }
}

/// The WAN message feed's NDJSON reassembler — the shared
/// [`NdjsonDecoder`](crate::net::ndjson::NdjsonDecoder) typed to
/// [`WanMessage`] records. One decoder per stream.
pub type WanDecoder = NdjsonDecoder<WanMessage>;

/// Where a send should go, decided from a registry [`Resolution`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WanRoute {
    /// A live instance on this node — the existing local path
    /// (`spt_msg::deliver`), no wire.
    Local,
    /// A live instance on another node — dial and send a [`WanMessage`].
    /// `subnet` is set when resolution crossed a per-subnet registry.
    Remote { subnet: Option<String>, node: String },
    /// Resolution refused — force qualification (D3c policy: never guess).
    Refused(Ambiguity),
    /// No live (routable) instance anywhere.
    NotFound,
}

/// Map a [`Resolution`] to a [`WanRoute`] against this node's own id. The
/// resolution policy (local-first, refuse-ambiguous, `@node` pin, visibility)
/// already ran inside `resolve_across_visible`; this only splits its hit into
/// the local and wire legs.
// [impl->REQ-NET-1]
pub fn route_wan(resolution: Resolution, local_node: &str) -> WanRoute {
    match resolution {
        Resolution::Resolved { instance, .. } if instance.node == local_node => WanRoute::Local,
        Resolution::Resolved { subnet, instance } => {
            WanRoute::Remote { subnet, node: instance.node }
        }
        Resolution::Ambiguous(why) => WanRoute::Refused(why),
        Resolution::NotFound => WanRoute::NotFound,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::net::registry::{Instance, Status};

    fn resolved(node: &str) -> Resolution {
        Resolution::Resolved {
            subnet: Some("home".into()),
            instance: Instance { node: node.into(), status: Status::Active, epoch: 1 },
        }
    }

    // [unit->REQ-NET-1] a record round-trips through the NDJSON line encoding,
    // and an unknown future field decodes (wire forward-compat).
    #[test]
    fn message_round_trips_and_tolerates_unknown_field() {
        let m = WanMessage {
            target: "ling".into(),
            from: "doyle".into(),
            body: "hello across the wire".into(),
            op_id: "cafe:42".into(),
        };
        let line = m.encode_line();
        assert_eq!(line.last(), Some(&b'\n'));
        let back: WanMessage = serde_json::from_slice(&line[..line.len() - 1]).unwrap();
        assert_eq!(back, m);

        let wire = br#"{"target":"ling","from":"doyle","body":"x","op_id":"n:1","added_at_d6":true}"#;
        let future: WanMessage = serde_json::from_slice(wire).unwrap();
        assert_eq!(future.op_id, "n:1");
    }

    // [unit->REQ-HAZARD-WAN-ORIGIN-AUTH] the record has no origin-node field:
    // a forged `origin_node` (or `node`) inside the JSON decodes as an ignored
    // unknown field — nothing in the decoded record can impersonate a node.
    // The gate's subject comes from the QUIC handshake (the broker's stream
    // table), asserted end-to-end in the daemon's wanmsg int test.
    #[test]
    fn forged_origin_field_is_inert() {
        let wire = br#"{"target":"ling","from":"doyle","body":"x","op_id":"n:1","origin_node":"evil","node":"evil"}"#;
        let decoded: WanMessage = serde_json::from_slice(wire).unwrap();
        // Every field the record actually has is accounted for — there is no
        // place for the forged origin to land.
        assert_eq!(
            decoded,
            WanMessage {
                target: "ling".into(),
                from: "doyle".into(),
                body: "x".into(),
                op_id: "n:1".into()
            }
        );
    }

    // [unit->REQ-NET-1] route_wan splits a resolution into the local and wire
    // legs and passes refusals through (never guess past an ambiguity).
    #[test]
    fn route_splits_local_and_remote_and_passes_refusals() {
        assert_eq!(route_wan(resolved("here"), "here"), WanRoute::Local);
        assert_eq!(
            route_wan(resolved("there"), "here"),
            WanRoute::Remote { subnet: Some("home".into()), node: "there".into() }
        );
        let amb = Resolution::Ambiguous(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]));
        assert_eq!(
            route_wan(amb, "here"),
            WanRoute::Refused(Ambiguity::AcrossNodes(vec!["a".into(), "b".into()]))
        );
        assert_eq!(route_wan(Resolution::NotFound, "here"), WanRoute::NotFound);
    }
}

-            instance: Instance { node: node.into(), status: Status::Active, epoch: 1 },

+            instance: Instance { node: node.into(), status: Status::Active, epoch: 1, resources: None },


        Instance { node: "node-b".into(), status: Status::Active, epoch: 2 },
        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1 },

        Instance { node: "node-b".into(), status: Status::Active, epoch: 2, resources: None },
        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1, resources: None },


let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1 };

let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1, resources: None };


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

        Instance { node: "node-b".into(), status: Status::Active, epoch: 2 },
        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1 },

        Instance { node: "node-b".into(), status: Status::Active, epoch: 2, resources: None },
        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1, resources: None },

// Election: B's node holds the Active instance — A must stand down.

-        Instance { node: "node-b".into(), status: Status::Active, epoch: 2 },

-        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1 },

+        Instance { node: "node-b".into(), status: Status::Active, epoch: 2, resources: None },

+        Instance { node: "node-a".into(), status: Status::Dormant, epoch: 1, resources: None },


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

//! D4d regression — registry replication over the wire: transport → merge.
//!
//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play
//! two daemons. Node A advertises a local endpoint (through the D3d/D3e
//! advertise gate, authored with its own [`EpochSource`] stamp) and replicates
//! the admitted row to node B over a broker-owned QUIC stream (D4a/b). B
//! decodes the NDJSON feed and applies each record straight through the D3
//! `merge_instance` seam. The invariants:
//!
//! - the registries **converge**: B holds A's row (node, status, epoch) verbatim;
//! - the **epoch lease holds over the wire**: a lagging update replayed after a
//!   newer one merges `Stale` and the stored row is untouched
//!   (REQ-HAZARD-REGISTRY-EPOCH-LEASE, now wire-fed);
//! - the framing survives **real QUIC chunking**: a record split across two
//!   separate stream sends reassembles on B's decoder.

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

use spt_daemon::brain::{Brain, BrokerEvent};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_net::net::registry::{
    advertise_if_visible, Instance, MergeOutcome, Status, SubnetRegistry,
};
use spt_net::net::replicate::{apply_update, LineDecoder, RegistryUpdate};
use spt_proto::identity::Identity;
use spt_store::epoch::EpochSource;
use spt_store::visibility::VisibilityStore;

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

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

fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(hermetic()).expect("net host start");
    let broker =
        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}

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

/// Poll the receiving brain's `net-streams` until a stream appears.
fn wait_for_stream(brain: &mut Brain) -> u64 {
    for _ in 0..400 {
        let reply = brain.net_streams().expect("net-streams");
        if let Some(s) = reply.streams.first() {
            return s.stream_id;
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("stream never appeared at the receiver's broker");
}

// [unit->REQ-INST-7] two daemons converge a registry row over the broker-owned
// wire: advertise gate → EpochSource-authored update → NDJSON over a QUIC
// stream → merge_instance at the receiver. A record split across two separate
// sends reassembles (QUIC chunking is not record framing).
// [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] and the lease holds over the wire:
// a lagging Active replayed after a newer Offline merges Stale — the stored
// row keeps the newer state (a stale Active can't clobber a newer Offline,
// now fed by the real transport).
#[test]
fn registries_converge_over_the_wire_and_the_lease_holds() {
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));

    // ── Node A: admit the row locally, then author the wire updates. ────────
    let mut sender = connect_retry(&name_a);
    let a_node = sender.net_status().expect("a status").node_id_hex.expect("node id");
    let mut epochs = EpochSource::load_from(&dir.path().join("a-epoch"));
    let visibility = VisibilityStore::load_from(&dir.path().join("a-visibility.json"));

    let mut a_regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
    a_regs.insert("home".into(), SubnetRegistry::new());

    // The advertise gate runs LOCALLY, before anything is emitted: the wire
    // only ever carries already-admitted state (D3d collision + D3e visibility).
    let e1 = epochs.next_epoch().expect("epoch");
    let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1 };
    let admitted = advertise_if_visible(
        a_regs.get_mut("home").expect("member"),
        &visibility,
        "home",
        false,
        "doyle",
        active.clone(),
    )
    .expect("no collision")
    .expect("visible — advertised");
    assert_eq!(admitted, MergeOutcome::Inserted);

    let update_active = RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: active,
    };
    let e2 = epochs.next_epoch().expect("epoch");
    assert!(e2 > e1, "EpochSource is strictly monotonic");
    let update_offline = RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2 },
    };

    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────
    let mut b_probe = connect_retry(&name_b);
    let addr = b_probe.net_status().expect("b status").addr;
    let conn = sender.net_dial(addr, Some(1)).expect("dial");
    let opened = sender.net_open_stream(conn.conn_id, Some(2)).expect("open");

    // Send 1: the Active row, split MID-RECORD across two separate sends —
    // the receiver's decoder, not QUIC chunk shape, owns record framing.
    let line = update_active.encode_line();
    let split = line.len() / 2;
    sender
        .net_stream_send(opened.stream_id, &line[..split], Some(100), false)
        .expect("send first half");
    sender
        .net_stream_send(opened.stream_id, &line[split..], Some(101), false)
        .expect("send second half");
    // Send 2: the newer Offline. Send 3: the e1 Active REPLAYED (a lagging
    // duplicate arriving late) — the lease must drop it at B.
    sender
        .net_stream_send(opened.stream_id, &update_offline.encode_line(), Some(102), false)
        .expect("send offline");
    sender
        .net_stream_send(opened.stream_id, &update_active.encode_line(), Some(103), true)
        .expect("replay stale + finish");

    // ── Node B: decode the feed and apply through the merge seam. ───────────
    let mut b_regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
    b_regs.insert("home".into(), SubnetRegistry::new());

    let stream = wait_for_stream(&mut b_probe);
    b_probe.net_stream_subscribe(stream, 0).expect("subscribe");
    let mut decoder = LineDecoder::new();
    let mut outcomes: Vec<MergeOutcome> = Vec::new();
    loop {
        match b_probe.read_event().expect("read event") {
            BrokerEvent::NetStreamData { stream_id, bytes, .. } if stream_id == stream => {
                for upd in decoder.push(&bytes) {
                    outcomes.push(apply_update(&mut b_regs, upd).expect("member subnet"));
                }
            }
            BrokerEvent::NetStreamEof { stream_id, .. } if stream_id == stream => break,
            BrokerEvent::Error { message } => panic!("broker error: {message}"),
            _ => {}
        }
    }

    // Convergence + the lease, end to end: inserted, superseded, stale-dropped.
    assert_eq!(
        outcomes,
        vec![MergeOutcome::Inserted, MergeOutcome::Updated, MergeOutcome::Stale],
        "wire feed merged under the lease"
    );
    let row = &b_regs["home"].instances("doyle")[0];
    assert_eq!(row.node, a_node, "B holds A's row verbatim");
    assert_eq!(
        (row.status, row.epoch),
        (Status::Offline, e2),
        "the newer Offline survives the stale Active replay"
    );
    // And B's registry equals A's post-advertise state merged with the newer
    // update — the two nodes agree on the endpoint's instance.
    let a_row = &a_regs["home"].instances("doyle")[0];
    assert_eq!(a_row.node, row.node);
}

-    let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1 };

+    let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1, resources: None };


instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2 },

instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2, resources: None },


//! D4d regression — registry replication over the wire: transport → merge.
//!
//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play
//! two daemons. Node A advertises a local endpoint (through the D3d/D3e
//! advertise gate, authored with its own [`EpochSource`] stamp) and replicates
//! the admitted row to node B over a broker-owned QUIC stream (D4a/b). B
//! decodes the NDJSON feed and applies each record straight through the D3
//! `merge_instance` seam. The invariants:
//!
//! - the registries **converge**: B holds A's row (node, status, epoch) verbatim;
//! - the **epoch lease holds over the wire**: a lagging update replayed after a
//!   newer one merges `Stale` and the stored row is untouched
//!   (REQ-HAZARD-REGISTRY-EPOCH-LEASE, now wire-fed);
//! - the framing survives **real QUIC chunking**: a record split across two
//!   separate stream sends reassembles on B's decoder.

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

use spt_daemon::brain::{Brain, BrokerEvent};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_net::net::registry::{
    advertise_if_visible, Instance, MergeOutcome, Status, SubnetRegistry,
};
use spt_net::net::replicate::{apply_update, LineDecoder, RegistryUpdate};
use spt_proto::identity::Identity;
use spt_store::epoch::EpochSource;
use spt_store::visibility::VisibilityStore;

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

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

fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(hermetic()).expect("net host start");
    let broker =
        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}

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

/// Poll the receiving brain's `net-streams` until a stream appears.
fn wait_for_stream(brain: &mut Brain) -> u64 {
    for _ in 0..400 {
        let reply = brain.net_streams().expect("net-streams");
        if let Some(s) = reply.streams.first() {
            return s.stream_id;
        }
        thread::sleep(Duration::from_millis(5));
    }
    panic!("stream never appeared at the receiver's broker");
}

// [unit->REQ-INST-7] two daemons converge a registry row over the broker-owned
// wire: advertise gate → EpochSource-authored update → NDJSON over a QUIC
// stream → merge_instance at the receiver. A record split across two separate
// sends reassembles (QUIC chunking is not record framing).
// [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] and the lease holds over the wire:
// a lagging Active replayed after a newer Offline merges Stale — the stored
// row keeps the newer state (a stale Active can't clobber a newer Offline,
// now fed by the real transport).
#[test]
fn registries_converge_over_the_wire_and_the_lease_holds() {
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));

    // ── Node A: admit the row locally, then author the wire updates. ────────
    let mut sender = connect_retry(&name_a);
    let a_node = sender.net_status().expect("a status").node_id_hex.expect("node id");
    let mut epochs = EpochSource::load_from(&dir.path().join("a-epoch"));
    let visibility = VisibilityStore::load_from(&dir.path().join("a-visibility.json"));

    let mut a_regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
    a_regs.insert("home".into(), SubnetRegistry::new());

    // The advertise gate runs LOCALLY, before anything is emitted: the wire
    // only ever carries already-admitted state (D3d collision + D3e visibility).
    let e1 = epochs.next_epoch().expect("epoch");
    let active = Instance { node: a_node.clone(), status: Status::Active, epoch: e1, resources: None };
    let admitted = advertise_if_visible(
        a_regs.get_mut("home").expect("member"),
        &visibility,
        "home",
        false,
        "doyle",
        active.clone(),
    )
    .expect("no collision")
    .expect("visible — advertised");
    assert_eq!(admitted, MergeOutcome::Inserted);

    let update_active = RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: active,
    };
    let e2 = epochs.next_epoch().expect("epoch");
    assert!(e2 > e1, "EpochSource is strictly monotonic");
    let update_offline = RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2 },
    };

    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────
    let mut b_probe = connect_retry(&name_b);
    let addr = b_probe.net_status().expect("b status").addr;
    let conn = sender.net_dial(addr, Some(1)).expect("dial");
    let opened = sender.net_open_stream(conn.conn_id, Some(2)).expect("open");

    // Send 1: the Active row, split MID-RECORD across two separate sends —
    // the receiver's decoder, not QUIC chunk shape, owns record framing.
    let line = update_active.encode_line();
    let split = line.len() / 2;
    sender
        .net_stream_send(opened.stream_id, &line[..split], Some(100), false)
        .expect("send first half");
    sender
        .net_stream_send(opened.stream_id, &line[split..], Some(101), false)
        .expect("send second half");
    // Send 2: the newer Offline. Send 3: the e1 Active REPLAYED (a lagging
    // duplicate arriving late) — the lease must drop it at B.
    sender
        .net_stream_send(opened.stream_id, &update_offline.encode_line(), Some(102), false)
        .expect("send offline");
    sender
        .net_stream_send(opened.stream_id, &update_active.encode_line(), Some(103), true)
        .expect("replay stale + finish");

    // ── Node B: decode the feed and apply through the merge seam. ───────────
    let mut b_regs: BTreeMap<String, SubnetRegistry> = BTreeMap::new();
    b_regs.insert("home".into(), SubnetRegistry::new());

    let stream = wait_for_stream(&mut b_probe);
    b_probe.net_stream_subscribe(stream, 0).expect("subscribe");
    let mut decoder = LineDecoder::new();
    let mut outcomes: Vec<MergeOutcome> = Vec::new();
    loop {
        match b_probe.read_event().expect("read event") {
            BrokerEvent::NetStreamData { stream_id, bytes, .. } if stream_id == stream => {
                for upd in decoder.push(&bytes) {
                    outcomes.push(apply_update(&mut b_regs, upd).expect("member subnet"));
                }
            }
            BrokerEvent::NetStreamEof { stream_id, .. } if stream_id == stream => break,
            BrokerEvent::Error { message } => panic!("broker error: {message}"),
            _ => {}
        }
    }

    // Convergence + the lease, end to end: inserted, superseded, stale-dropped.
    assert_eq!(
        outcomes,
        vec![MergeOutcome::Inserted, MergeOutcome::Updated, MergeOutcome::Stale],
        "wire feed merged under the lease"
    );
    let row = &b_regs["home"].instances("doyle")[0];
    assert_eq!(row.node, a_node, "B holds A's row verbatim");
    assert_eq!(
        (row.status, row.epoch),
        (Status::Offline, e2),
        "the newer Offline survives the stale Active replay"
    );
    // And B's registry equals A's post-advertise state merged with the newer
    // update — the two nodes agree on the endpoint's instance.
    let a_row = &a_regs["home"].instances("doyle")[0];
    assert_eq!(a_row.node, row.node);
}

-        instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2 },

+        instance: Instance { node: a_node.clone(), status: Status::Offline, epoch: e2, resources: None },


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

Instance { node: node.to_string(), status, epoch: 1, resources: None }


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

//! Reconciler election (ADR-0013) — which node runs the Psyche reconcile turn
//! for a surfaced context conflict.
//!
//! Exactly **one** node may reconcile a given conflict, or two nodes mint
//! textually-different merges of the same pair and the pair re-conflicts (a
//! reconcile storm). The rule:
//!
//! 1. **The endpoint's Active instance's node** — the mind is being used
//!    there, the Psyche is warm there, and it matches the
//!    active-instance-authoritative single-writer rule. Multiple Active rows
//!    (a partition's transient) fall to the deterministic tiebreak below
//!    *among the Active rows*.
//! 2. **No Active instance anywhere** → the lexicographically lowest node id
//!    among non-Offline holders; no rows at all → self (a local-only endpoint
//!    reconciles locally).
//!
//! Worst case under partition both sides elect themselves, both merges
//! propagate, the merges conflict — **detected and re-reconciled, never
//! lost** (hazard 6.6); the election only bounds how often that happens.
//! The actual per-file merge mechanics live in [`spt_live::reconcile`].
//
// [impl->REQ-HAZARD-CONFLICT-BOTH-PRESERVED]

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

/// Decide whether `self_node` is the reconciler for an endpoint, given the
/// endpoint's registry rows (every instance, every node). Pure — the daemon
/// feeds it the live registry; tests feed it rows.
pub fn is_reconciler(rows: &[Instance], self_node: &str) -> bool {
    let active: Vec<&Instance> = rows.iter().filter(|i| i.status == Status::Active).collect();
    if !active.is_empty() {
        return active.iter().map(|i| i.node.as_str()).min() == Some(self_node);
    }
    let holders: Vec<&str> = rows
        .iter()
        .filter(|i| i.status != Status::Offline)
        .map(|i| i.node.as_str())
        .collect();
    match holders.iter().min() {
        Some(lowest) => *lowest == self_node,
        None => true, // no registry presence — local-only endpoint, reconcile here
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn inst(node: &str, status: Status) -> Instance {
        Instance { node: node.to_string(), status, epoch: 1 }
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.
    fn active_node_wins() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Dormant)];
        assert!(is_reconciler(&rows, "bbb"));
        assert!(!is_reconciler(&rows, "aaa"), "dormant lower id must stand down");
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] no Active → lowest non-Offline node id; Offline rows never elected.
    fn fallback_lowest_non_offline() {
        let rows = vec![
            inst("ccc", Status::Dormant),
            inst("aaa", Status::Offline),
            inst("bbb", Status::Dormant),
        ];
        assert!(is_reconciler(&rows, "bbb"), "lowest NON-OFFLINE wins");
        assert!(!is_reconciler(&rows, "aaa"), "offline never reconciles");
        assert!(!is_reconciler(&rows, "ccc"));
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] two Actives (partition transient): deterministic tiebreak among them.
    fn double_active_tiebreaks_deterministically() {
        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Active)];
        assert!(is_reconciler(&rows, "aaa"));
        assert!(!is_reconciler(&rows, "bbb"));
    }

    #[test] // no rows at all: a local-only endpoint reconciles locally.
    fn no_rows_reconciles_locally() {
        assert!(is_reconciler(&[], "me"));
    }
}

-        Instance { node: node.to_string(), status, epoch: 1 }

+        Instance { node: node.to_string(), status, epoch: 1, resources: None }


**2. info.json `resources` field + setter:**


    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
}

    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
    /// The endpoint-authored resource-advertisement blurb (D9-4,
    /// REQ-INST-14): the agent's own "what I can serve" line, written via
    /// `spt resources set` ([`set_resources`]) and advertised into every
    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
    /// `daemon.json` seed (if any) advertises instead — config seeds the
    /// default, the agent refines its own at runtime (CONTEXT §resource
    /// advertisement, both-authored).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resources: Option<String>,
}


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

    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
}

    /// Per-endpoint `auto-suspend-after` override (ms) — the endpoint leg of
    /// the global → node → endpoint knob chain (REQ-INST-3). Absent ⇒ inherit
    /// the node knob (`daemon.json`); `0` ⇒ explicitly OFF for this endpoint
    /// even when the node enables it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub auto_suspend_after_ms: Option<u64>,
    /// The endpoint-authored resource-advertisement blurb (D9-4,
    /// REQ-INST-14): the agent's own "what I can serve" line, written via
    /// `spt resources set` ([`set_resources`]) and advertised into every
    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level
    /// `daemon.json` seed (if any) advertises instead — config seeds the
    /// default, the agent refines its own at runtime (CONTEXT §resource
    /// advertisement, both-authored).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub resources: Option<String>,
}

+    /// The endpoint-authored resource-advertisement blurb (D9-4,

+    /// REQ-INST-14): the agent's own "what I can serve" line, written via

+    /// `spt resources set` ([`set_resources`]) and advertised into every

+    /// visible subnet by the daemon's registry scan. Absent ⇒ the node-level

+    /// `daemon.json` seed (if any) advertises instead — config seeds the

+    /// default, the agent refines its own at runtime (CONTEXT §resource

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


            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        }
    }
}

            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
            resources: None,
        }
    }
}


            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        }
    }
}

            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
            resources: None,
        }
    }
}


            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        };
        write_info(d.path(), &info).unwrap();

            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
            resources: None,
        };
        write_info(d.path(), &info).unwrap();


            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
        };
        write_info(d.path(), &info).unwrap();

            rest_state: None,
            dormant_since_ms: None,
            auto_suspend_after_ms: None,
            resources: None,
        };
        write_info(d.path(), &info).unwrap();


/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an

/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through
/// the atomic path. Errors if the record is absent/unreadable — only an
/// existing endpoint advertises services.
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    rec.resources = blurb.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an


/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,
/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through
/// the atomic path. Errors if the record is absent/unreadable — only an
/// existing endpoint advertises services.
// [impl->REQ-INST-14]
pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {
    let mut rec = read_info(perch_path).ok_or_else(|| {
        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")
    })?;
    rec.resources = blurb.map(str::to_string);
    write_info(perch_path, &rec)
}

/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker on an

+/// Set (or clear, with `None`) the endpoint-authored resource blurb (D9-4,

+/// REQ-INST-14) — the `spt resources set` write. Read-modify-write through

+/// the atomic path. Errors if the record is absent/unreadable — only an

+/// existing endpoint advertises services.

+pub fn set_resources(perch_path: &Path, blurb: Option<&str>) -> std::io::Result<()> {

+    let mut rec = read_info(perch_path).ok_or_else(|| {

+        std::io::Error::new(std::io::ErrorKind::NotFound, "info.json absent or unreadable")

+    rec.resources = blurb.map(str::to_string);

/// existing perch's `info.json` (REQ-REACH-1): the daemon records which node's

/// operator is driving this endpoint's session while it serves a net-attached


Updated task #5 status


info.rs test + **3. config knob (drop Copy — String field):**


// [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an

    // [unit->REQ-INST-14] set_resources writes the endpoint-authored blurb
    // (absent by default — config's node seed advertises instead), clears it
    // with None, and errors on a missing record.
    #[test]
    fn set_resources_writes_and_clears_the_blurb() {
        let d = perch();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();
        assert_eq!(read_info(d.path()).unwrap().resources, None, "absent by default");

        set_resources(d.path(), Some("code review; rust builds")).unwrap();
        assert_eq!(
            read_info(d.path()).unwrap().resources.as_deref(),
            Some("code review; rust builds")
        );
        // The agent refines its own at runtime — a later set overwrites.
        set_resources(d.path(), Some("rust builds only")).unwrap();
        assert_eq!(read_info(d.path()).unwrap().resources.as_deref(), Some("rust builds only"));

        set_resources(d.path(), None).unwrap();
        assert_eq!(read_info(d.path()).unwrap().resources, None, "cleared");

        let empty = perch();
        assert!(set_resources(empty.path(), Some("x")).is_err(), "no record, no write");
    }

    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an


    // [unit->REQ-INST-14] set_resources writes the endpoint-authored blurb
    // (absent by default — config's node seed advertises instead), clears it
    // with None, and errors on a missing record.
    #[test]
    fn set_resources_writes_and_clears_the_blurb() {
        let d = perch();
        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();
        assert_eq!(read_info(d.path()).unwrap().resources, None, "absent by default");

        set_resources(d.path(), Some("code review; rust builds")).unwrap();
        assert_eq!(
            read_info(d.path()).unwrap().resources.as_deref(),
            Some("code review; rust builds")
        );
        // The agent refines its own at runtime — a later set overwrites.
        set_resources(d.path(), Some("rust builds only")).unwrap();
        assert_eq!(read_info(d.path()).unwrap().resources.as_deref(), Some("rust builds only"));

        set_resources(d.path(), None).unwrap();
        assert_eq!(read_info(d.path()).unwrap().resources, None, "cleared");

        let empty = perch();
        assert!(set_resources(empty.path(), Some("x")).is_err(), "no record, no write");
    }

    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an

+    // [unit->REQ-INST-14] set_resources writes the endpoint-authored blurb

+    // (absent by default — config's node seed advertises instead), clears it

+    // with None, and errors on a missing record.

+    fn set_resources_writes_and_clears_the_blurb() {

+        write_info(d.path(), &InfoJson::new("ling", "t", 4242, "sid", "live_agent")).unwrap();

+        assert_eq!(read_info(d.path()).unwrap().resources, None, "absent by default");

+        set_resources(d.path(), Some("code review; rust builds")).unwrap();

+            read_info(d.path()).unwrap().resources.as_deref(),

+            Some("code review; rust builds")

+        // The agent refines its own at runtime — a later set overwrites.

+        set_resources(d.path(), Some("rust builds only")).unwrap();

+        assert_eq!(read_info(d.path()).unwrap().resources.as_deref(), Some("rust builds only"));

+        set_resources(d.path(), None).unwrap();

+        assert_eq!(read_info(d.path()).unwrap().resources, None, "cleared");

+        assert!(set_resources(empty.path(), Some("x")).is_err(), "no record, no write");

// existing record (absent by default = locally driven), clears it with

// None, errors on a missing record — and a pre-D5c record (no field at


    #[serde(default, skip_serializing_if = "Option::is_none")]
    auto_suspend_after_ms: Option<u64>,
}

    #[serde(default, skip_serializing_if = "Option::is_none")]
    auto_suspend_after_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    resources_blurb: Option<String>,
}


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)

    #[serde(default, skip_serializing_if = "Option::is_none")]
    auto_suspend_after_ms: Option<u64>,
}

    #[serde(default, skip_serializing_if = "Option::is_none")]
    auto_suspend_after_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    resources_blurb: Option<String>,
}

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

/// Resolved daemon configuration. The brain's scheduled loops read their cadence


/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DaemonConfig {

/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
/// (No longer `Copy` — `resources_blurb` carries a `String`; callers clone.)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DaemonConfig {


/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DaemonConfig {

/// Resolved daemon configuration. The brain's scheduled loops read their cadence
/// from `pulse_period`; the drop-ingest guard reads `protection_window_ms`.
/// (No longer `Copy` — `resources_blurb` carries a `String`; callers clone.)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DaemonConfig {

-#[derive(Debug, Clone, Copy, PartialEq, Eq)]

+/// (No longer `Copy` — `resources_blurb` carries a `String`; callers clone.)

/// The pulse loop period (config-driven; defaults to [`DEFAULT_PULSE_PERIOD`]).


    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):
    /// dormant → suspended after this many ms counted from dormancy onset.
    /// **`None` = the global default, OFF** — auto-suspend is opt-in
    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a
    /// per-endpoint `info.json` override wins over this value
    /// ([`crate::resting::effective_auto_suspend`]).
    pub auto_suspend_after_ms: Option<u64>,
}

    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):
    /// dormant → suspended after this many ms counted from dormancy onset.
    /// **`None` = the global default, OFF** — auto-suspend is opt-in
    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a
    /// per-endpoint `info.json` override wins over this value
    /// ([`crate::resting::effective_auto_suspend`]).
    pub auto_suspend_after_ms: Option<u64>,
    /// The node-level resource-advertisement seed (D9-4, REQ-INST-14): the
    /// default blurb advertised for an endpoint that has not authored its
    /// own (`info.json` `resources` wins when present — config seeds, the
    /// agent refines; CONTEXT §resource advertisement). `None` = endpoints
    /// without their own blurb advertise none.
    pub resources_blurb: Option<String>,
}


    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):
    /// dormant → suspended after this many ms counted from dormancy onset.
    /// **`None` = the global default, OFF** — auto-suspend is opt-in
    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a
    /// per-endpoint `info.json` override wins over this value
    /// ([`crate::resting::effective_auto_suspend`]).
    pub auto_suspend_after_ms: Option<u64>,
}

    /// The NODE leg of the auto-suspend knob chain (D9-2, REQ-INST-3):
    /// dormant → suspended after this many ms counted from dormancy onset.
    /// **`None` = the global default, OFF** — auto-suspend is opt-in
    /// (CONTEXT §dormant/suspended); a constrained node sets it here, and a
    /// per-endpoint `info.json` override wins over this value
    /// ([`crate::resting::effective_auto_suspend`]).
    pub auto_suspend_after_ms: Option<u64>,
    /// The node-level resource-advertisement seed (D9-4, REQ-INST-14): the
    /// default blurb advertised for an endpoint that has not authored its
    /// own (`info.json` `resources` wins when present — config seeds, the
    /// agent refines; CONTEXT §resource advertisement). `None` = endpoints
    /// without their own blurb advertise none.
    pub resources_blurb: Option<String>,
}

+    /// The node-level resource-advertisement seed (D9-4, REQ-INST-14): the

+    /// default blurb advertised for an endpoint that has not authored its

+    /// own (`info.json` `resources` wins when present — config seeds, the

+    /// agent refines; CONTEXT §resource advertisement). `None` = endpoints

+    /// without their own blurb advertise none.

+    pub resources_blurb: Option<String>,


            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)
        }
    }
}

            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)
            resources_blurb: None,       // endpoints author their own (REQ-INST-14)
        }
    }
}


            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)
        }
    }
}

            auto_suspend_after_ms: None, // opt-in, default OFF (REQ-INST-3)
            resources_blurb: None,       // endpoints author their own (REQ-INST-14)
        }
    }
}

+            resources_blurb: None,       // endpoints author their own (REQ-INST-14)


            auto_suspend_after_ms: file.auto_suspend_after_ms,
        }
    }

            auto_suspend_after_ms: file.auto_suspend_after_ms,
            resources_blurb: file.resources_blurb,
        }
    }


            auto_suspend_after_ms: file.auto_suspend_after_ms,
        }
    }

            auto_suspend_after_ms: file.auto_suspend_after_ms,
            resources_blurb: file.resources_blurb,
        }
    }

+            resources_blurb: file.resources_blurb,


            auto_suspend_after_ms: self.auto_suspend_after_ms,
        };

            auto_suspend_after_ms: self.auto_suspend_after_ms,
            resources_blurb: self.resources_blurb.clone(),
        };


            auto_suspend_after_ms: self.auto_suspend_after_ms,
        };

            auto_suspend_after_ms: self.auto_suspend_after_ms,
            resources_blurb: self.resources_blurb.clone(),
        };

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),

+            resources_blurb: self.resources_blurb.clone(),

let json = serde_json::to_string_pretty(&file)


            auto_suspend_after_ms: Some(15_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

            auto_suspend_after_ms: Some(15_000),
            resources_blurb: Some("node default services".to_string()),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }


            auto_suspend_after_ms: Some(15_000),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

            auto_suspend_after_ms: Some(15_000),
            resources_blurb: Some("node default services".to_string()),
        };
        cfg.save_to(&path).unwrap();
        assert_eq!(DaemonConfig::load_from(&path), cfg);
    }

+            resources_blurb: Some("node default services".to_string()),


**4. advertise_local fills the blurb:**


    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();

    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let vis = VisibilityStore::load();
        // The node-level resource-blurb seed (D9-4): an endpoint that has not
        // authored its own blurb advertises this default (config seeds, the
        // agent refines — CONTEXT §resource advertisement).
        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;
        let mut out = Vec::new();
        let mut regs = self.regs.lock().unwrap();
        let mut epoch = self.epoch.lock().unwrap();


    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();

    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryUpdate> {
        let subnets = SubnetStore::load();
        let vis = VisibilityStore::load();
        // The node-level resource-blurb seed (D9-4): an endpoint that has not
        // authored its own blurb advertises this default (config seeds, the
        // agent refines — CONTEXT §resource advertisement).
        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;
        let mut out = Vec::new();
        let mut regs = self.regs.lock().unwrap();
        let mut epoch = self.epoch.lock().unwrap();

+        // The node-level resource-blurb seed (D9-4): an endpoint that has not

+        // authored its own blurb advertises this default (config seeds, the

+        // agent refines — CONTEXT §resource advertisement).

+        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;


            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance =
                    Instance { node: self.node_hex.clone(), status, epoch: ep };

            // Endpoint-authored blurb wins; the node seed fills the gap
            // (both-authored, REQ-INST-14). Rides the same epoch lease as
            // every instance update — a blurb edit reaches peers as an
            // ordered, epoch-bumped row.
            // [impl->REQ-INST-14]
            let resources = spt_store::info::read_info(&perch_path)
                .and_then(|i| i.resources)
                .or_else(|| node_blurb.clone());
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                };


            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance =
                    Instance { node: self.node_hex.clone(), status, epoch: ep };

            // Endpoint-authored blurb wins; the node seed fills the gap
            // (both-authored, REQ-INST-14). Rides the same epoch lease as
            // every instance update — a blurb edit reaches peers as an
            // ordered, epoch-bumped row.
            // [impl->REQ-INST-14]
            let resources = spt_store::info::read_info(&perch_path)
                .and_then(|i| i.resources)
                .or_else(|| node_blurb.clone());
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                };

+            // Endpoint-authored blurb wins; the node seed fills the gap

+            // (both-authored, REQ-INST-14). Rides the same epoch lease as

+            // every instance update — a blurb edit reaches peers as an

+            // ordered, epoch-bumped row.

+            let resources = spt_store::info::read_info(&perch_path)

+                .and_then(|i| i.resources)

+                .or_else(|| node_blurb.clone());

-                    Instance { node: self.node_hex.clone(), status, epoch: ep };

+                let instance = Instance {

+                    node: self.node_hex.clone(),

+                    resources: resources.clone(),

let reg = regs.entry(sub.name.clone()).or_default();


**5. `spt resources` CLI:**


/// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's

    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text
    /// service blurbs — an agent yellow-pages. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {
        #[command(subcommand)]
        action: ResourcesCmd,
    },
    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's


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

    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text
    /// service blurbs — an agent yellow-pages. `set` authors your own,
    /// `show` reads a local endpoint's, `list` projects the registry view
    /// over visible rows.
    Resources {
        #[command(subcommand)]
        action: ResourcesCmd,
    },
    /// Rest a local endpoint cold (REQ-INST-3): the resting-state machine's

+    /// The subnet resource registry (REQ-INST-14): per-endpoint free-text

+    /// service blurbs — an agent yellow-pages. `set` authors your own,

+    /// `show` reads a local endpoint's, `list` projects the registry view

/// suspend edge. From dormant — or straight from active, in which case

/// the transition echo commune still fires (gate-armed for the


/// `spt notif <action>` subcommands (ADR-0007).

/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
#[derive(Subcommand)]
enum ResourcesCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb.
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
    /// The subnet resource-registry projection: `(id, node, status, blurb)`
    /// over VISIBLE rows only (hidden endpoints never appear — discovery
    /// leaks nothing a viewer couldn't reach).
    List {
        /// Limit to one subnet (all member subnets otherwise).
        #[arg(long)]
        subnet: Option<String>,
    },
}

/// `spt notif <action>` subcommands (ADR-0007).


/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).
#[derive(Subcommand)]
enum ResourcesCmd {
    /// Author this endpoint's blurb (the agent refines its own at runtime;
    /// an empty string clears it back to the node-config seed).
    Set {
        /// The blurb text ("" clears).
        text: String,
        /// Which local endpoint (auto-detected from the session if omitted).
        #[arg(long)]
        id: Option<String>,
    },
    /// Show a local endpoint's authored blurb.
    Show {
        /// The local endpoint id (auto-detected if omitted).
        id: Option<String>,
    },
    /// The subnet resource-registry projection: `(id, node, status, blurb)`
    /// over VISIBLE rows only (hidden endpoints never appear — discovery
    /// leaks nothing a viewer couldn't reach).
    List {
        /// Limit to one subnet (all member subnets otherwise).
        #[arg(long)]
        subnet: Option<String>,
    },
}

/// `spt notif <action>` subcommands (ADR-0007).

+/// `spt resources <action>` subcommands (D9-4, REQ-INST-14).

+    /// Author this endpoint's blurb (the agent refines its own at runtime;

+    /// an empty string clears it back to the node-config seed).

+        /// The blurb text ("" clears).

+        /// Which local endpoint (auto-detected from the session if omitted).

+    /// Show a local endpoint's authored blurb.

+        /// The local endpoint id (auto-detected if omitted).

+    /// The subnet resource-registry projection: `(id, node, status, blurb)`

+    /// over VISIBLE rows only (hidden endpoints never appear — discovery

+    /// leaks nothing a viewer couldn't reach).

+        /// Limit to one subnet (all member subnets otherwise).


        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),

        Cmd::Resources { action } => cmd_resources(action),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),


        Cmd::Resources { action } => cmd_resources(action),
        Cmd::Suspend { id } => cmd_rest(&id, spt_daemon::RestEvent::Suspend, "SUSPENDED"),
        Cmd::Wake { id } => cmd_rest(&id, spt_daemon::RestEvent::Wake, "WOKE"),

NotifCmd::Dismiss { notif_id } => cmd_notif_dismiss(&notif_id),

Cmd::Digest { id, follow, json } => cmd_digest(&id, follow, json),

+        Cmd::Resources { action } => cmd_resources(action),

Cmd::Rename { old_id, new_id } => cmd_rename(&old_id, &new_id),


Now the handler:


/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges

/// `spt resources set|show|list` — the subnet resource registry surface
/// (D9-4, REQ-INST-14). `set`/`show` operate the LOCAL endpoint's authored
/// blurb on its perch record; `list` is the registry **projection** read off
/// the daemon's stale-tolerant snapshots (`identity/registry/<subnet>.json`),
/// visibility-filtered exactly like resolution — a hidden endpoint never
/// appears (ADR-0006 §6, exclusion by construction).
// [impl->REQ-INST-14]
fn cmd_resources(action: ResourcesCmd) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        ResourcesCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("RESOURCES_SET:{id}: {b}"),
                        None => eprintln!("RESOURCES_CLEARED:{id} (node seed advertises, if any)"),
                    }
                    0
                }
                Err(e) => {
                    eprintln!("RESOURCES_FAIL:{id}: {e}");
                    1
                }
            }
        }
        ResourcesCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
        ResourcesCmd::List { subnet } => {
            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
            let subnets = SubnetStore::load();
            let vis = VisibilityStore::load();
            let names: Vec<String> = match subnet {
                Some(one) => vec![one],
                None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
            };
            let mut shown = 0usize;
            for name in names {
                let Some(reg) = regs.get(&name) else { continue };
                let hide_new = subnets.find(&name).map(|s| s.hide_new_endpoints).unwrap_or(false);
                // The same exclusion the resolver enforces — the projection
                // can never list what resolution would refuse to route to.
                let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
                for row in rows {
                    println!(
                        "{}\t{}\t{}\t{:?}\t{}",
                        name,
                        row.endpoint_id,
                        row.node,
                        row.status,
                        row.resources.as_deref().unwrap_or("-")
                    );
                    shown += 1;
                }
            }
            eprintln!("RESOURCES:{shown}");
            0
        }
    }
}

/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges


/// `spt resources set|show|list` — the subnet resource registry surface
/// (D9-4, REQ-INST-14). `set`/`show` operate the LOCAL endpoint's authored
/// blurb on its perch record; `list` is the registry **projection** read off
/// the daemon's stale-tolerant snapshots (`identity/registry/<subnet>.json`),
/// visibility-filtered exactly like resolution — a hidden endpoint never
/// appears (ADR-0006 §6, exclusion by construction).
// [impl->REQ-INST-14]
fn cmd_resources(action: ResourcesCmd) -> i32 {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    /// Resolve the operated endpoint: explicit `--id`, else session detect.
    fn target(id: Option<String>) -> Result<String, i32> {
        match id.or_else(roster::detect_self_id) {
            Some(id) => Ok(id),
            None => {
                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");
                Err(1)
            }
        }
    }

    match action {
        ResourcesCmd::Set { text, id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            let blurb = (!text.trim().is_empty()).then_some(text.trim());
            match spt_store::info::set_resources(&perch_path, blurb) {
                Ok(()) => {
                    match blurb {
                        Some(b) => eprintln!("RESOURCES_SET:{id}: {b}"),
                        None => eprintln!("RESOURCES_CLEARED:{id} (node seed advertises, if any)"),
                    }
                    0
                }
                Err(e) => {
                    eprintln!("RESOURCES_FAIL:{id}: {e}");
                    1
                }
            }
        }
        ResourcesCmd::Show { id } => {
            let id = match target(id) {
                Ok(id) => id,
                Err(code) => return code,
            };
            let perch_path = perch::resolve_perch_path(&id, ParentHint::Infer);
            match spt_store::info::read_info(&perch_path) {
                Some(rec) => {
                    match rec.resources {
                        Some(b) => println!("{b}"),
                        None => println!("(no authored blurb — the node seed advertises, if any)"),
                    }
                    0
                }
                None => {
                    eprintln!("NO_ENDPOINT:{id}");
                    1
                }
            }
        }
        ResourcesCmd::List { subnet } => {
            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
            let subnets = SubnetStore::load();
            let vis = VisibilityStore::load();
            let names: Vec<String> = match subnet {
                Some(one) => vec![one],
                None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),
            };
            let mut shown = 0usize;
            for name in names {
                let Some(reg) = regs.get(&name) else { continue };
                let hide_new = subnets.find(&name).map(|s| s.hide_new_endpoints).unwrap_or(false);
                // The same exclusion the resolver enforces — the projection
                // can never list what resolution would refuse to route to.
                let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));
                for row in rows {
                    println!(
                        "{}\t{}\t{}\t{:?}\t{}",
                        name,
                        row.endpoint_id,
                        row.node,
                        row.status,
                        row.resources.as_deref().unwrap_or("-")
                    );
                    shown += 1;
                }
            }
            eprintln!("RESOURCES:{shown}");
            0
        }
    }
}

/// `spt suspend <id>` / `spt wake <id>` — the manual resting-state edges

+/// `spt resources set|show|list` — the subnet resource registry surface

+/// (D9-4, REQ-INST-14). `set`/`show` operate the LOCAL endpoint's authored

+/// blurb on its perch record; `list` is the registry **projection** read off

+/// the daemon's stale-tolerant snapshots (`identity/registry/<subnet>.json`),

+/// visibility-filtered exactly like resolution — a hidden endpoint never

+/// appears (ADR-0006 §6, exclusion by construction).

+fn cmd_resources(action: ResourcesCmd) -> i32 {

+    use spt_net::net::registry::resource_projection;

+    use spt_store::visibility::VisibilityStore;

+    /// Resolve the operated endpoint: explicit `--id`, else session detect.

+    fn target(id: Option<String>) -> Result<String, i32> {

+        match id.or_else(roster::detect_self_id) {

+                eprintln!("NO_SELF: name an endpoint (--id <id> or $OWL_SESSION_ID)");

+        ResourcesCmd::Set { text, id } => {

+            let id = match target(id) {

+                Err(code) => return code,

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

+            let blurb = (!text.trim().is_empty()).then_some(text.trim());

+            match spt_store::info::set_resources(&perch_path, blurb) {

+                        Some(b) => eprintln!("RESOURCES_SET:{id}: {b}"),

+                        None => eprintln!("RESOURCES_CLEARED:{id} (node seed advertises, if any)"),

+                    eprintln!("RESOURCES_FAIL:{id}: {e}");

+            match spt_store::info::read_info(&perch_path) {

+                    match rec.resources {

+                        Some(b) => println!("{b}"),

+                        None => println!("(no authored blurb — the node seed advertises, if any)"),

+                    eprintln!("NO_ENDPOINT:{id}");

+        ResourcesCmd::List { subnet } => {

+            let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));

+            let subnets = SubnetStore::load();

+            let vis = VisibilityStore::load();

+            let names: Vec<String> = match subnet {

+                Some(one) => vec![one],

+                None => subnets.subnets.iter().map(|s| s.name.clone()).collect(),

+                let Some(reg) = regs.get(&name) else { continue };

+                let hide_new = subnets.find(&name).map(|s| s.hide_new_endpoints).unwrap_or(false);

+                // The same exclusion the resolver enforces — the projection

+                // can never list what resolution would refuse to route to.

+                let rows = resource_projection(reg, |id| vis.hidden(id, &name, hide_new));

+                        row.endpoint_id,

+                        row.resources.as_deref().unwrap_or("-")

+            eprintln!("RESOURCES:{shown}");

/// (REQ-INST-3): apply the event against the local endpoint's durable record

/// through the daemon-side transition host (echo gate-armed for the


CLI parse test + projection unit tests:


// [unit->REQ-INST-3] the resting-command surfaces parse (one local id).

        // [unit->REQ-INST-14] the resources surfaces parse.
        assert!(matches!(
            parse(&["spt", "resources", "set", "rust builds", "--id", "ling"]).unwrap().cmd,
            Cmd::Resources { action: ResourcesCmd::Set { .. } }
        ));
        assert!(matches!(
            parse(&["spt", "resources", "show"]).unwrap().cmd,
            Cmd::Resources { action: ResourcesCmd::Show { id: None } }
        ));
        assert!(matches!(
            parse(&["spt", "resources", "list", "--subnet", "home"]).unwrap().cmd,
            Cmd::Resources { action: ResourcesCmd::List { subnet: Some(_) } }
        ));
        assert!(parse(&["spt", "resources", "set"]).is_err(), "blurb text required");
        // [unit->REQ-INST-3] the resting-command surfaces parse (one local id).


        // [unit->REQ-INST-14] the resources surfaces parse.
        assert!(matches!(
            parse(&["spt", "resources", "set", "rust builds", "--id", "ling"]).unwrap().cmd,
            Cmd::Resources { action: ResourcesCmd::Set { .. } }
        ));
        assert!(matches!(
            parse(&["spt", "resources", "show"]).unwrap().cmd,
            Cmd::Resources { action: ResourcesCmd::Show { id: None } }
        ));
        assert!(matches!(
            parse(&["spt", "resources", "list", "--subnet", "home"]).unwrap().cmd,
            Cmd::Resources { action: ResourcesCmd::List { subnet: Some(_) } }
        ));
        assert!(parse(&["spt", "resources", "set"]).is_err(), "blurb text required");
        // [unit->REQ-INST-3] the resting-command surfaces parse (one local id).

// [unit->REQ-INST-11] the rename surface parses (two positional ids).

assert!(matches!(parse(&["spt", "rename", "ling", "oak"]).unwrap().cmd, Cmd::Rename { .. }));

assert!(parse(&["spt", "rename", "ling"]).is_err(), "new id required");

+        // [unit->REQ-INST-14] the resources surfaces parse.

+            parse(&["spt", "resources", "set", "rust builds", "--id", "ling"]).unwrap().cmd,

+            Cmd::Resources { action: ResourcesCmd::Set { .. } }

+            parse(&["spt", "resources", "show"]).unwrap().cmd,

+            Cmd::Resources { action: ResourcesCmd::Show { id: None } }

+            parse(&["spt", "resources", "list", "--subnet", "home"]).unwrap().cmd,

+            Cmd::Resources { action: ResourcesCmd::List { subnet: Some(_) } }

+        assert!(parse(&["spt", "resources", "set"]).is_err(), "blurb text required");

assert!(matches!(parse(&["spt", "suspend", "ling"]).unwrap().cmd, Cmd::Suspend { .. }));

assert!(matches!(parse(&["spt", "wake", "ling"]).unwrap().cmd, Cmd::Wake { .. }));


// [unit->REQ-INST-3] a Suspended instance is resting, not gone: it stays

    // [unit->REQ-INST-14] the blurb is a FIELD under the lease, not a store:
    // a newer-epoch row carries the updated blurb; a lagging blurb edit is
    // dropped stale; and the wire shape roundtrips with the blurb present
    // and parses a pre-D9-4 row (no field) clean.
    #[test]
    fn resource_blurb_rides_the_lease_and_serde() {
        let mut reg = SubnetRegistry::new();
        let with = |blurb: Option<&str>, epoch: u64| Instance {
            node: "n1".to_string(),
            status: Status::Active,
            epoch,
            resources: blurb.map(str::to_string),
        };
        reg.merge_instance("ling", with(Some("rust builds"), 1));
        assert_eq!(
            reg.merge_instance("ling", with(Some("rust builds; code review"), 2)),
            MergeOutcome::Updated,
            "a blurb edit is an ordinary epoch-bumped update"
        );
        assert_eq!(
            reg.instances("ling")[0].resources.as_deref(),
            Some("rust builds; code review")
        );
        // A lagging edit drops under the lease — the newer blurb survives.
        assert_eq!(reg.merge_instance("ling", with(Some("stale"), 1)), MergeOutcome::Stale);
        assert_eq!(
            reg.instances("ling")[0].resources.as_deref(),
            Some("rust builds; code review")
        );

        // Serde: roundtrips with the blurb; a pre-D9-4 row parses clean.
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back);
        let old: Instance = serde_json::from_str(
            r#"{"node":"n1","status":"Active","epoch":1}"#,
        )
        .unwrap();
        assert_eq!(old.resources, None, "absent field parses as no blurb");
    }

    // [unit->REQ-INST-14] the projection is the yellow-pages view over
    // VISIBLE rows only: a hidden endpoint never appears (discovery leaks
    // nothing resolution would refuse), an Offline row is not a resource,
    // and an absent blurb renders clean (None, not an error).
    #[test]
    fn resource_projection_filters_hidden_and_offline() {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance(
            "ling",
            Instance {
                node: "n1".to_string(),
                status: Status::Active,
                epoch: 1,
                resources: Some("rust builds".to_string()),
            },
        );
        reg.merge_instance("oak", inst("n1", Status::Dormant, 1)); // no blurb
        reg.merge_instance("hidden-ag", inst("n1", Status::Active, 1));
        reg.merge_instance("dead", inst("n2", Status::Offline, 1));

        let rows = resource_projection(&reg, |id| id == "hidden-ag");
        let ids: Vec<&str> = rows.iter().map(|r| r.endpoint_id.as_str()).collect();
        assert_eq!(ids, vec!["ling", "oak"], "hidden excluded, offline skipped: {rows:?}");
        assert_eq!(rows[0].resources.as_deref(), Some("rust builds"));
        assert_eq!(rows[1].resources, None, "absent blurb renders clean");
        assert_eq!(rows[1].status, Status::Dormant, "resting rows still list (addressable)");
    }

    // [unit->REQ-INST-3] a Suspended instance is resting, not gone: it stays


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

    // [unit->REQ-INST-14] the blurb is a FIELD under the lease, not a store:
    // a newer-epoch row carries the updated blurb; a lagging blurb edit is
    // dropped stale; and the wire shape roundtrips with the blurb present
    // and parses a pre-D9-4 row (no field) clean.
    #[test]
    fn resource_blurb_rides_the_lease_and_serde() {
        let mut reg = SubnetRegistry::new();
        let with = |blurb: Option<&str>, epoch: u64| Instance {
            node: "n1".to_string(),
            status: Status::Active,
            epoch,
            resources: blurb.map(str::to_string),
        };
        reg.merge_instance("ling", with(Some("rust builds"), 1));
        assert_eq!(
            reg.merge_instance("ling", with(Some("rust builds; code review"), 2)),
            MergeOutcome::Updated,
            "a blurb edit is an ordinary epoch-bumped update"
        );
        assert_eq!(
            reg.instances("ling")[0].resources.as_deref(),
            Some("rust builds; code review")
        );
        // A lagging edit drops under the lease — the newer blurb survives.
        assert_eq!(reg.merge_instance("ling", with(Some("stale"), 1)), MergeOutcome::Stale);
        assert_eq!(
            reg.instances("ling")[0].resources.as_deref(),
            Some("rust builds; code review")
        );

        // Serde: roundtrips with the blurb; a pre-D9-4 row parses clean.
        let json = serde_json::to_string(&reg).unwrap();
        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();
        assert_eq!(reg, back);
        let old: Instance = serde_json::from_str(
            r#"{"node":"n1","status":"Active","epoch":1}"#,
        )
        .unwrap();
        assert_eq!(old.resources, None, "absent field parses as no blurb");
    }

    // [unit->REQ-INST-14] the projection is the yellow-pages view over
    // VISIBLE rows only: a hidden endpoint never appears (discovery leaks
    // nothing resolution would refuse), an Offline row is not a resource,
    // and an absent blurb renders clean (None, not an error).
    #[test]
    fn resource_projection_filters_hidden_and_offline() {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance(
            "ling",
            Instance {
                node: "n1".to_string(),
                status: Status::Active,
                epoch: 1,
                resources: Some("rust builds".to_string()),
            },
        );
        reg.merge_instance("oak", inst("n1", Status::Dormant, 1)); // no blurb
        reg.merge_instance("hidden-ag", inst("n1", Status::Active, 1));
        reg.merge_instance("dead", inst("n2", Status::Offline, 1));

        let rows = resource_projection(&reg, |id| id == "hidden-ag");
        let ids: Vec<&str> = rows.iter().map(|r| r.endpoint_id.as_str()).collect();
        assert_eq!(ids, vec!["ling", "oak"], "hidden excluded, offline skipped: {rows:?}");
        assert_eq!(rows[0].resources.as_deref(), Some("rust builds"));
        assert_eq!(rows[1].resources, None, "absent blurb renders clean");
        assert_eq!(rows[1].status, Status::Dormant, "resting rows still list (addressable)");
    }

    // [unit->REQ-INST-3] a Suspended instance is resting, not gone: it stays

assert!(matches!(r, Resolution::Resolved { instance, .. } if instance.node == "here"));

+    // [unit->REQ-INST-14] the blurb is a FIELD under the lease, not a store:

+    // a newer-epoch row carries the updated blurb; a lagging blurb edit is

+    // dropped stale; and the wire shape roundtrips with the blurb present

+    // and parses a pre-D9-4 row (no field) clean.

+    fn resource_blurb_rides_the_lease_and_serde() {

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

+        let with = |blurb: Option<&str>, epoch: u64| Instance {

+            resources: blurb.map(str::to_string),

+        reg.merge_instance("ling", with(Some("rust builds"), 1));

+            reg.merge_instance("ling", with(Some("rust builds; code review"), 2)),

+            "a blurb edit is an ordinary epoch-bumped update"

+            reg.instances("ling")[0].resources.as_deref(),

+            Some("rust builds; code review")

+        // A lagging edit drops under the lease — the newer blurb survives.

+        assert_eq!(reg.merge_instance("ling", with(Some("stale"), 1)), MergeOutcome::Stale);

+        // Serde: roundtrips with the blurb; a pre-D9-4 row parses clean.

+        let json = serde_json::to_string(&reg).unwrap();

+        let back: SubnetRegistry = serde_json::from_str(&json).unwrap();

+        let old: Instance = serde_json::from_str(

+        assert_eq!(old.resources, None, "absent field parses as no blurb");

+    // [unit->REQ-INST-14] the projection is the yellow-pages view over

+    // VISIBLE rows only: a hidden endpoint never appears (discovery leaks

+    // nothing resolution would refuse), an Offline row is not a resource,

+    // and an absent blurb renders clean (None, not an error).

+    fn resource_projection_filters_hidden_and_offline() {

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

+                node: "n1".to_string(),

+                status: Status::Active,

+                resources: Some("rust builds".to_string()),

+        reg.merge_instance("oak", inst("n1", Status::Dormant, 1)); // no blurb

+        reg.merge_instance("hidden-ag", inst("n1", Status::Active, 1));

+        reg.merge_instance("dead", inst("n2", Status::Offline, 1));

+        let rows = resource_projection(&reg, |id| id == "hidden-ag");

+        let ids: Vec<&str> = rows.iter().map(|r| r.endpoint_id.as_str()).collect();

+        assert_eq!(ids, vec!["ling", "oak"], "hidden excluded, offline skipped: {rows:?}");

+        assert_eq!(rows[0].resources.as_deref(), Some("rust builds"));

+        assert_eq!(rows[1].resources, None, "absent blurb renders clean");

+        assert_eq!(rows[1].status, Status::Dormant, "resting rows still list (addressable)");

// addressable (resolves bare and via @node pin — a wake must route to it)

// and the variant serde-roundtrips (the on-wire/persisted form).


Registryhost both-authored test + CONTEXT tag + toml activation: