340	    /// the updates to push to peers (the node's own rows only — peers' rows
341	    /// reconverge from their own re-advertisement, no transitive gossip in
342	    /// v1: trust is pairwise per subnet, a relayed third-party row would
343	    /// carry an origin the receiver can't gate honestly).
344	    // [impl->REQ-INST-7]
345	    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {
346	        let subnets = SubnetStore::load();
347	        let mut vis = VisibilityStore::load();
348	        // First-join home adoption (D9-5, REQ-INST-15): within one
349	        // advertisement cadence of the node's first join (a pair lands the
350	        // subnet record; no pairing-protocol threading needed), unhomed Self
351	        // perches adopt the sole subnet + seed their sync scope. Strictly
352	        // adopt-if-unset; a multi-subnet store adopts nothing.
353	        // [impl->REQ-INST-15]
354	        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
355	            let _ = vis.save();
356	        }
357	        let vis = vis;
358	        // The node-level resource-blurb seed (D9-4): an endpoint that has not
359	        // authored its own blurb advertises this default (config seeds, the
360	        // agent refines — CONTEXT §resource advertisement).
361	        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;
362	        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT
363	        // §Discovery scope): registered kind="shell" adapters that chose
364	        // broadcast=subnet ride every row this node authors — the other-node
365	        // discovery arm. Computed once per scan (the registration set is
366	        // node-level).
367	        // [impl->REQ-EP-5]
368	        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())
369	            .iter()
370	            .filter(|(r, m)| {
371	                r.kind == spt_runtime::manifest::AdapterKind::Shell
372	                    && m.shell.as_ref().and_then(|s| s.broadcast)
373	                        == Some(spt_runtime::manifest::Broadcast::Subnet)
374	            })
375	            .map(|(r, _)| r.name.clone())
376	            .collect();
377	        let mut out = Vec::new();
378	        let mut regs = self.regs.lock().unwrap();
379	        let mut epoch = self.epoch.lock().unwrap();
380
381	        // The set of self perches that still EXIST on disk this round — the
382	        // ghost-heal predicate below (a LOCAL registry row whose id is absent
383	        // here was erased, REQ-HAZARD-ROSTER-GHOST).
384	        let live_ids: std::collections::BTreeSet<String> =
385	            perch::list_self_perch_ids(owlery).into_iter().collect();
386
387	        for id in perch::list_self_perch_ids(owlery) {
388	            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
389	            // Advertisement follows the resting-state machine (D9-2,
390	            // REQ-INST-3): a live perch with a resting record advertises its
391	            // machine state (active / dormant / suspended); a recordless
392	            // (pre-D9-2 / interim) or dead perch keeps the legacy
393	            // liveness-derived mapping. Every advertisement stamps a fresh
394	            // epoch below, so a transition reaches peers as an epoch-bumped
395	            // update — the lease orders it (KH 4.8).
396	            // [impl->REQ-INST-3]
397	            let status = if is_perch_alive(&perch_path) {
398	                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
399	                    Some(crate::resting::RestState::Active) | None => Status::Active,
400	                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
401	                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
402	                }
403	            } else {
404	                Status::Dormant
405	            };
406	            // Endpoint-authored blurb wins; the node seed fills the gap
407	            // (both-authored, REQ-INST-14). Rides the same epoch lease as
408	            // every instance update — a blurb edit reaches peers as an
409	            // ordered, epoch-bumped row.
410	            // [impl->REQ-INST-14]
411	            let info = spt_store::info::read_info(&perch_path);
412	            let resources = info
413	                .as_ref()
414	                .and_then(|i| i.resources.clone())
415	                .or_else(|| node_blurb.clone());
416	            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat
417	            // stamp (`lifecycle::touch_active` — Active seats only; a resting
418	            // seat never stamps) rides the row like the blurb does — same
419	            // epoch lease, same visibility gate, no new channel. Subnet-wide
420	            // most-recently-active is a projection of these rows (D6b).
421	            // [impl->REQ-PRES-1]
422	            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the
423	            // info.json `state` IS the bound endpoint type
424	            // (`EndpointType::as_tag` — `"gateway"` for a Gateway surface).
425	            // A node advertises ONLY its own endpoints' types; the cross-node
426	            // user-msg gate reads this at the receive funnel (T3). A recordless
427	            // perch advertises no type → the funnel fail-closes to a re-stamp.
428	            // [impl->REQ-MSG-6]
429	            let endpoint_type = info.as_ref().map(|i| i.state.clone());

    /// the updates to push to peers (the node's own rows only — peers' rows
    /// reconverge from their own re-advertisement, no transitive gossip in
    /// v1: trust is pairwise per subnet, a relayed third-party row would
    /// carry an origin the receiver can't gate honestly).
    // [impl->REQ-INST-7]
    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {
        let subnets = SubnetStore::load();
        let mut vis = VisibilityStore::load();
        // First-join home adoption (D9-5, REQ-INST-15): within one
        // advertisement cadence of the node's first join (a pair lands the
        // subnet record; no pairing-protocol threading needed), unhomed Self
        // perches adopt the sole subnet + seed their sync scope. Strictly
        // adopt-if-unset; a multi-subnet store adopts nothing.
        // [impl->REQ-INST-15]
        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
            let _ = vis.save();
        }
        let vis = vis;
        // 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;
        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT
        // §Discovery scope): registered kind="shell" adapters that chose
        // broadcast=subnet ride every row this node authors — the other-node
        // discovery arm. Computed once per scan (the registration set is
        // node-level).
        // [impl->REQ-EP-5]
        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())
            .iter()
            .filter(|(r, m)| {
                r.kind == spt_runtime::manifest::AdapterKind::Shell
                    && m.shell.as_ref().and_then(|s| s.broadcast)
                        == Some(spt_runtime::manifest::Broadcast::Subnet)
            })
            .map(|(r, _)| r.name.clone())
            .collect();
        let mut out = Vec::new();
        let mut regs = self.regs.lock().unwrap();
        let mut epoch = self.epoch.lock().unwrap();

        // The set of self perches that still EXIST on disk this round — the
        // ghost-heal predicate below (a LOCAL registry row whose id is absent
        // here was erased, REQ-HAZARD-ROSTER-GHOST).
        let live_ids: std::collections::BTreeSet<String> =
            perch::list_self_perch_ids(owlery).into_iter().collect();

        for id in perch::list_self_perch_ids(owlery) {
            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
            // Advertisement follows the resting-state machine (D9-2,
            // REQ-INST-3): a live perch with a resting record advertises its
            // machine state (active / dormant / suspended); a recordless
            // (pre-D9-2 / interim) or dead perch keeps the legacy
            // liveness-derived mapping. Every advertisement stamps a fresh
            // epoch below, so a transition reaches peers as an epoch-bumped
            // update — the lease orders it (KH 4.8).
            // [impl->REQ-INST-3]
            let status = if is_perch_alive(&perch_path) {
                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
                    Some(crate::resting::RestState::Active) | None => Status::Active,
                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
                }
            } else {
                Status::Dormant
            };
            // 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 info = spt_store::info::read_info(&perch_path);
            let resources = info
                .as_ref()
                .and_then(|i| i.resources.clone())
                .or_else(|| node_blurb.clone());
            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat
            // stamp (`lifecycle::touch_active` — Active seats only; a resting
            // seat never stamps) rides the row like the blurb does — same
            // epoch lease, same visibility gate, no new channel. Subnet-wide
            // most-recently-active is a projection of these rows (D6b).
            // [impl->REQ-PRES-1]
            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the
            // info.json `state` IS the bound endpoint type
            // (`EndpointType::as_tag` — `"gateway"` for a Gateway surface).
            // A node advertises ONLY its own endpoints' types; the cross-node
            // user-msg gate reads this at the receive funnel (T3). A recordless
            // perch advertises no type → the funnel fail-closes to a re-stamp.
            // [impl->REQ-MSG-6]
            let endpoint_type = info.as_ref().map(|i| i.state.clone());


1456:id = "REQ-JOIN-TWO-PHASE"
1457:[Omitted long matching line]
1458-required_stages = ["doc"]  # doc = ADR-0030 + docs\design\robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.
1459-[[requirements]]
1460:id = "REQ-NET-FAMILY-GATE"
1461-[Omitted long context line]
1462-required_stages = ["doc"]  # doc = ADR-0030. impl+unit activate at the build milestone: per-family reachability probe at bind + bind-only-working-families + SPT_DISABLE_IPV6/IPV4 overrides; impl-verify the iroh single-family bind API + whether iroh hole-punch already fast-fails. unit = family-selection logic (both/v4-dead/v6-dead/forced) without live sockets.
1463-[[requirements]]
1464:id = "REQ-JOIN-DIAGNOSTICS"
1465-[Omitted long context line]
1466-required_stages = ["doc"]  # doc = ADR-0030. impl+unit activate at the build milestone: search-progress + detailed-failure-before-prompt + brain.rs:1024 terminal-event propagation + --verbose/SPT_LOG trace. unit = failure-message composition (what-was-tried) + terminal-event routing (no _=>continue swallow).
1467-
1468-# ── Endpoint-liveness gossip truth (diagnosed 2026-06-27, same session) — a remote node showed all of ──
1469-# another node's DEAD endpoints as ONLINE. No ADR (a correctness fix, unsurprising once seen). Seeded for
1470-# the next milestone JIT alongside the ADR-0030 join cluster.
1471-[[requirements]]
1472:id = "REQ-PRESENCE-LIVENESS-TRUTH"
1473:[Omitted long matching line]
1474:required_stages = []  # SEED (rule 5). next-milestone build: registryhost not-bound-alive → {Active/Dormant if unbound-live (warm), else Suspended (cold, node up)} — never the blanket Dormant, never self-gossiped Offline. unit = status-derivation over (alive / unbound-live-warm / cold-no-session). int = a dead perch reads NOT-online on a REMOTE node's picker (home-vs-remote agree). [display palette → REQ-SUBNET-DISPLAY-PARITY]
1475-[[requirements]]
1476:id = "REQ-SUBNET-DISPLAY-PARITY"
1477-[Omitted long context line]
1478-[Omitted long context line]


Now picker mapping + is_perch_alive/unbound semantics for W4.


crates\spt-net\src\net\replicate.rs:187:        let b = upd("home", "echo", "n2", Status::Dormant, 4);
crates\spt-net\src\net\registry.rs:817:        reg.merge_instance("ling", inst("nodeB", Status::Dormant, 1));
crates\spt-net\src\net\registry.rs:844:            reg.merge_instance("ling", inst("nodeA", Status::Dormant, 2)),
crates\spt-net\src\net\registry.rs:848:        assert_eq!(reg.instances("ling")[0].status, Status::Dormant);
crates\spt-net\src\net\registry.rs:1226:        reg.merge_instance("oak", inst("n1", Status::Dormant, 1)); // no blurb
crates\spt-net\src\net\registry.rs:1241:            Status::Dormant,
crates\spt-net\src\net\registry.rs:1295:        let reg = reg_with(&[("ling", "desk", Status::Suspended)]);
crates\spt-net\src\net\registry.rs:1302:            Resolution::Resolved { instance, .. } if instance.status == Status::Suspended
crates\spt-net\src\net\registry.rs:1326:            ("ling", "b", Status::Dormant),
crates\spt-net\src\net\registry.rs:1442:            reg.join_endpoint("ling", inst("nodeA", Status::Dormant, 6)),
crates\spt-net\src\net\registry.rs:1451:        assert_eq!(reg.instances("ling")[0].status, Status::Dormant);
crates\spt-net\src\net\registry.rs:1491:            reg_with(&[("ling", "n1", Status::Dormant)]),
crates\spt\src\cli.rs:10094:                status: Status::Dormant,
crates\spt\src\cli.rs:12829:                status: spt_net::net::registry::Status::Dormant,
crates\spt\src\cli.rs:13295:        reg.merge_instance("a2", inst("self00aa", Status::Dormant, Some("ME")));
crates\spt-store\src\liveness.rs:81:pub fn is_perch_unbound(perch_path: &Path) -> bool {
crates\spt-store\src\liveness.rs:95:pub fn is_perch_alive(perch_path: &Path) -> bool {
crates\spt-store\src\liveness.rs:265:    fn is_perch_unbound_false_for_online_and_offline() {
crates\spt\src\picker\data.rs:268:                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
crates\spt-daemon\tests\twohost.rs:1134:            .any(|i| i.node == rig.b_hex() && i.status == spt_net::net::registry::Status::Suspended)
crates\spt-daemon\tests\sync.rs:438:            status: Status::Dormant,
crates\spt-daemon\src\registryhost.rs:400:                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
crates\spt-daemon\src\registryhost.rs:401:                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
crates\spt-daemon\src\registryhost.rs:404:                Status::Dormant
crates\spt-daemon\src\registryhost.rs:1324:            &[upd_with("home", "ling", "bb22", Status::Dormant, 3)],
crates\spt-daemon\src\registryhost.rs:1344:            &[upd_with("home", "ling", "bb22", Status::Dormant, 5)],
crates\spt-daemon\src\registryhost.rs:1440:                Status::Dormant,
crates\spt-daemon\src\registryhost.rs:1452:            assert_eq!(suspended.status, Status::Suspended);
crates\spt-daemon\src\reconcile.rs:65:        let rows = vec![inst("bbb", Status::Active), inst("aaa", Status::Dormant)];
crates\spt-daemon\src\reconcile.rs:76:            inst("ccc", Status::Dormant),
crates\spt-daemon\src\reconcile.rs:78:            inst("bbb", Status::Dormant),
crates\spt-daemon\src\resting.rs:72:            RestState::Dormant => "dormant",
crates\spt-daemon\src\resting.rs:73:            RestState::Suspended => "suspended",
crates\spt-daemon\src\shelldisc.rs:340:        reg.merge_instance("oak", inst("remote-1", Status::Dormant, &["GameRobot"]));


No files found


50	const READ_ATTEMPTS: u32 = 5;
51
52	/// One tolerant read of a perch's `info.json` as a raw value — schema-minimal
53	/// records (`{"pid": N}` from wrapper-owned listeners) parse where the full
54	/// [`crate::info::InfoJson`] shape would not. `None` on persistent absence/corruption.
55	fn read_raw(perch_path: &Path) -> Option<Value> {
56	    let path = perch_path.join("info.json");
57	    for _ in 0..READ_ATTEMPTS {
58	        if let Ok(content) = std::fs::read_to_string(&path) {
59	            if let Ok(val) = serde_json::from_str::<Value>(&content) {
60	                return Some(val);
61	            }
62	        }
63	    }
64	    None
65	}
66
67	/// The daemon-managed `status` of a perch, if it is daemon-hosted. `Some(_)` ⇒
68	/// daemon-hosted (liveness is authoritative via status); `None` ⇒ external /
69	/// interim (liveness is the per-pid probe).
70	fn read_status(perch_path: &Path) -> Option<String> {
71	    read_raw(perch_path)?
72	        .get("status")
73	        .and_then(|v| v.as_str())
74	        .map(str::to_string)
75	}
76
77	/// True when a perch is in the `unbound` state (skeleton created, harness not
78	/// yet called `spt api bind`). The endpoint is broker-attachable but NOT
79	/// message-addressable — `is_perch_alive` stays `false` for it.
80	// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]
81	pub fn is_perch_unbound(perch_path: &Path) -> bool {
82	    read_status(perch_path).as_deref() == Some(STATUS_UNBOUND)
83	}
84
85	/// Liveness for a perch by its **directory**. THE resolver for the path-aware
86	/// call sites (`is_online`, `from_has_live_perch`, roster).
87	///
88	/// - **Daemon-hosted** (`status` present): live iff `status == "online"`. The
89	///   `info.pid` (an ephemeral summarizer pid, or the shared daemon pid) is
90	///   **never** consulted — killing the summarizer does not flip the perch.
91	/// - **External/interim** (`status` absent): the per-pid probe, preserving the
92	///   exact interim semantics — a numeric pid is probed; a `"BUSY"` sentinel or
93	///   unparseable/absent pid reads alive (the listener is up between polls).
94	// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
95	pub fn is_perch_alive(perch_path: &Path) -> bool {
96	    match read_raw(perch_path) {
97	        Some(val) => {
98	            if let Some(status) = val.get("status").and_then(|v| v.as_str()) {
99	                return status == STATUS_ONLINE; // daemon-authoritative; never pid
100	            }
101	            match val.get("pid").and_then(|v| v.as_u64()) {
102	                Some(pid) => is_process_alive(pid as u32),
103	                None => true, // BUSY sentinel / unparseable pid — interim parity
104	            }
105	        }
106	        None => true, // unreadable info.json — interim parity (read_pid None ⇒ alive)
107	    }
108	}
109
110	/// Liveness for a **registry row**: its id plus the pid it was registered under,
111	/// resolved within `owlery` (the registry's own root). Daemon-hosted rows key on
112	/// the endpoint `status`; external rows pid-probe the **row's** pid exactly as
113	/// the interim `clean_stale_entries` did — so the daemon swap touches only the
114	/// hosted rows. An offline daemon-hosted endpoint is cleaned even though the
115	/// (shared) daemon pid is still alive; a still-online one survives a dead
116	/// ephemeral pid.
117	// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
118	// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]
119	pub fn is_registry_entry_alive(id: &str, registry_pid: u32, owlery: &Path) -> bool {
120	    let perch_path = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);
121	    match read_status(&perch_path) {
122	        // `unbound` is a live skeleton — don't clean it; `online` is live; anything
123	        // else (offline, unknown) is stale → clean.
124	        Some(status) => status == STATUS_ONLINE || status == STATUS_UNBOUND,
125	        None => is_process_alive(registry_pid), // external/interim: pid-probe the row
126	    }
127	}
128
129	#[cfg(test)]
130	mod tests {
131	    use super::*;
132	    use crate::info::{set_status, write_info, InfoJson};
133
134	    /// A pid that is never a real allocation (above Linux pid_max ~4.2M and
135	    /// Windows ranges; positive as i32). Deterministic — avoids pid-reuse flake.
136	    const DEAD_PID: u32 = 2_000_000_000;
137
138	    fn perch() -> tempfile::TempDir {
139	        tempfile::tempdir().unwrap()
140	    }
141
142	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked
143	    // `online` reads alive even though its recorded (summarizer) pid is DEAD —
144	    // killing the summarizer subprocess does not flip the perch offline.
145	    #[test]
146	    fn daemon_hosted_online_survives_dead_pid() {
147	        let d = perch();
148	        // Record the ephemeral summarizer pid (now dead) then mark daemon-online.
149	        write_info(
150	            d.path(),
151	            &InfoJson::new("doyle-psyche", "t", DEAD_PID, "sid", "psyche"),
152	        )
153	        .unwrap();
154	        set_status(d.path(), STATUS_ONLINE).unwrap();
155	        assert!(
156	            is_perch_alive(d.path()),
157	            "online status is authoritative; the dead pid is ignored"
158	        );
159	    }
160
161	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked
162	    // `offline` reads dead even though its recorded pid is ALIVE (this process).
163	    #[test]
164	    fn daemon_hosted_offline_overrides_live_pid() {
165	        let d = perch();
166	        write_info(
167	            d.path(),
168	            &InfoJson::new("doyle-psyche", "t", std::process::id(), "sid", "psyche"),
169	        )
170	        .unwrap();
171	        set_status(d.path(), STATUS_OFFLINE).unwrap();
172	        assert!(
173	            !is_perch_alive(d.path()),
174	            "offline status is authoritative; the live pid is ignored"
175	        );
176	    }
177
178	    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] an external/interim perch (no
179	    // status) still pid-probes: a dead pid reads offline, a live pid reads online.

const READ_ATTEMPTS: u32 = 5;

/// One tolerant read of a perch's `info.json` as a raw value — schema-minimal
/// records (`{"pid": N}` from wrapper-owned listeners) parse where the full
/// [`crate::info::InfoJson`] shape would not. `None` on persistent absence/corruption.
fn read_raw(perch_path: &Path) -> Option<Value> {
    let path = perch_path.join("info.json");
    for _ in 0..READ_ATTEMPTS {
        if let Ok(content) = std::fs::read_to_string(&path) {
            if let Ok(val) = serde_json::from_str::<Value>(&content) {
                return Some(val);
            }
        }
    }
    None
}

/// The daemon-managed `status` of a perch, if it is daemon-hosted. `Some(_)` ⇒
/// daemon-hosted (liveness is authoritative via status); `None` ⇒ external /
/// interim (liveness is the per-pid probe).
fn read_status(perch_path: &Path) -> Option<String> {
    read_raw(perch_path)?
        .get("status")
        .and_then(|v| v.as_str())
        .map(str::to_string)
}

/// True when a perch is in the `unbound` state (skeleton created, harness not
/// yet called `spt api bind`). The endpoint is broker-attachable but NOT
/// message-addressable — `is_perch_alive` stays `false` for it.
// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]
pub fn is_perch_unbound(perch_path: &Path) -> bool {
    read_status(perch_path).as_deref() == Some(STATUS_UNBOUND)
}

/// Liveness for a perch by its **directory**. THE resolver for the path-aware
/// call sites (`is_online`, `from_has_live_perch`, roster).
///
/// - **Daemon-hosted** (`status` present): live iff `status == "online"`. The
///   `info.pid` (an ephemeral summarizer pid, or the shared daemon pid) is
///   **never** consulted — killing the summarizer does not flip the perch.
/// - **External/interim** (`status` absent): the per-pid probe, preserving the
///   exact interim semantics — a numeric pid is probed; a `"BUSY"` sentinel or
///   unparseable/absent pid reads alive (the listener is up between polls).
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
pub fn is_perch_alive(perch_path: &Path) -> bool {
    match read_raw(perch_path) {
        Some(val) => {
            if let Some(status) = val.get("status").and_then(|v| v.as_str()) {
                return status == STATUS_ONLINE; // daemon-authoritative; never pid
            }
            match val.get("pid").and_then(|v| v.as_u64()) {
                Some(pid) => is_process_alive(pid as u32),
                None => true, // BUSY sentinel / unparseable pid — interim parity
            }
        }
        None => true, // unreadable info.json — interim parity (read_pid None ⇒ alive)
    }
}

/// Liveness for a **registry row**: its id plus the pid it was registered under,
/// resolved within `owlery` (the registry's own root). Daemon-hosted rows key on
/// the endpoint `status`; external rows pid-probe the **row's** pid exactly as
/// the interim `clean_stale_entries` did — so the daemon swap touches only the
/// hosted rows. An offline daemon-hosted endpoint is cleaned even though the
/// (shared) daemon pid is still alive; a still-online one survives a dead
/// ephemeral pid.
// [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
// [impl->REQ-ENDPOINT-UNBOUND-ATTACH]
pub fn is_registry_entry_alive(id: &str, registry_pid: u32, owlery: &Path) -> bool {
    let perch_path = perch::resolve_perch_path_in(owlery, id, ParentHint::Infer);
    match read_status(&perch_path) {
        // `unbound` is a live skeleton — don't clean it; `online` is live; anything
        // else (offline, unknown) is stale → clean.
        Some(status) => status == STATUS_ONLINE || status == STATUS_UNBOUND,
        None => is_process_alive(registry_pid), // external/interim: pid-probe the row
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::info::{set_status, write_info, InfoJson};

    /// A pid that is never a real allocation (above Linux pid_max ~4.2M and
    /// Windows ranges; positive as i32). Deterministic — avoids pid-reuse flake.
    const DEAD_PID: u32 = 2_000_000_000;

    fn perch() -> tempfile::TempDir {
        tempfile::tempdir().unwrap()
    }

    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked
    // `online` reads alive even though its recorded (summarizer) pid is DEAD —
    // killing the summarizer subprocess does not flip the perch offline.
    #[test]
    fn daemon_hosted_online_survives_dead_pid() {
        let d = perch();
        // Record the ephemeral summarizer pid (now dead) then mark daemon-online.
        write_info(
            d.path(),
            &InfoJson::new("doyle-psyche", "t", DEAD_PID, "sid", "psyche"),
        )
        .unwrap();
        set_status(d.path(), STATUS_ONLINE).unwrap();
        assert!(
            is_perch_alive(d.path()),
            "online status is authoritative; the dead pid is ignored"
        );
    }

    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] a daemon-hosted perch marked
    // `offline` reads dead even though its recorded pid is ALIVE (this process).
    #[test]
    fn daemon_hosted_offline_overrides_live_pid() {
        let d = perch();
        write_info(
            d.path(),
            &InfoJson::new("doyle-psyche", "t", std::process::id(), "sid", "psyche"),
        )
        .unwrap();
        set_status(d.path(), STATUS_OFFLINE).unwrap();
        assert!(
            !is_perch_alive(d.path()),
            "offline status is authoritative; the live pid is ignored"
        );
    }

    // [unit->REQ-HAZARD-DAEMON-HOSTED-LIVENESS] an external/interim perch (no
    // status) still pid-probes: a dead pid reads offline, a live pid reads online.


250	/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
251	/// like `spt endpoint list`.
252	fn subnet_rows() -> Vec<EndpointRow> {
253	    use spt_net::net::registry::resource_projection;
254	    use spt_store::visibility::VisibilityStore;
255
256	    let subnets = spt_store::subnet::SubnetStore::load();
257	    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
258	    let vis = VisibilityStore::load();
259	    let mut out = Vec::new();
260	    for s in &subnets.subnets {
261	        let Some(reg) = regs.get(&s.name) else {
262	            continue;
263	        };
264	        let hide_new = s.hide_new_endpoints;
265	        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
266	            let status = match row.status {
267	                spt_net::net::registry::Status::Active
268	                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
269	                // Suspended (cold) + Offline render offline in the W2 duality.
270	                _ => EpStatus::Offline,
271	            };
272	            // resource_projection threads the node label (REQ-PICKER-4) — render
273	            // the canonical "LABEL (keyprefix…)" via the one shared helper instead
274	            // of leaking bare key-hex into the picker.
275	            let node_display =
276	                spt_net::net::registry::node_label_display(&row.node, row.node_label.as_deref());
277	            out.push(EndpointRow {
278	                id: row.endpoint_id,
279	                group: format!("{}:{}", s.name, node_display),
280	                node: node_display,
281	                status,
282	                is_local: false,
283	                adapter_profile: row.resources.clone().unwrap_or_default(),
284	                project_history: Vec::new(),
285	                description: row.resources.unwrap_or_default(),
286	                resume_rows: Vec::new(),
287	                // Subnet rows: the registry projection doesn't carry the remote's
288	                // controller marker yet, so leave it unknown — a remote attach
289	                // still busy-refuses with client guidance (REQ-RCVIEW-1).
290	                driven_by: None,
291	                viewer_count: 0,
292	                // Controllability is a LOCAL fact (this node's hosting mode); a
293	                // remote row can't determine it, so it renders plain online/green —
294	                // amber "harness-only" is never inferred across the subnet
295	                // (REQ-PICKER-1). Self-owned rows get reconciled to the live roster
296	                // below (REQ-PICKER-3), which also carries the local controllable.
297	                controllable: None,
298	                endpoint_type: String::new(),
299	                // A remote node's pre-bind UNBOUND window isn't observable here —
300	                // subnet rows render plain (same limit as controllability above).
301	                is_unbound: false,
302	            });
303	        }
304	    }
305	    out
306	}
307
308	/// Union an endpoint's ORIGIN project (the cwd it was created in) into its
309	/// committed-context project membership (REQ-PICKER-HISTORY-FRESH). `origin` is

/// Subnet endpoints, from the WAN registry snapshots, visibility-gated exactly
/// like `spt endpoint list`.
fn subnet_rows() -> Vec<EndpointRow> {
    use spt_net::net::registry::resource_projection;
    use spt_store::visibility::VisibilityStore;

    let subnets = spt_store::subnet::SubnetStore::load();
    let regs = crate::wansend::load_snapshots(&perch::identity_dir().join("registry"));
    let vis = VisibilityStore::load();
    let mut out = Vec::new();
    for s in &subnets.subnets {
        let Some(reg) = regs.get(&s.name) else {
            continue;
        };
        let hide_new = s.hide_new_endpoints;
        for row in resource_projection(reg, |id| vis.hidden(id, &s.name, hide_new)) {
            let status = match row.status {
                spt_net::net::registry::Status::Active
                | spt_net::net::registry::Status::Dormant => EpStatus::Online,
                // Suspended (cold) + Offline render offline in the W2 duality.
                _ => EpStatus::Offline,
            };
            // resource_projection threads the node label (REQ-PICKER-4) — render
            // the canonical "LABEL (keyprefix…)" via the one shared helper instead
            // of leaking bare key-hex into the picker.
            let node_display =
                spt_net::net::registry::node_label_display(&row.node, row.node_label.as_deref());
            out.push(EndpointRow {
                id: row.endpoint_id,
                group: format!("{}:{}", s.name, node_display),
                node: node_display,
                status,
                is_local: false,
                adapter_profile: row.resources.clone().unwrap_or_default(),
                project_history: Vec::new(),
                description: row.resources.unwrap_or_default(),
                resume_rows: Vec::new(),
                // Subnet rows: the registry projection doesn't carry the remote's
                // controller marker yet, so leave it unknown — a remote attach
                // still busy-refuses with client guidance (REQ-RCVIEW-1).
                driven_by: None,
                viewer_count: 0,
                // Controllability is a LOCAL fact (this node's hosting mode); a
                // remote row can't determine it, so it renders plain online/green —
                // amber "harness-only" is never inferred across the subnet
                // (REQ-PICKER-1). Self-owned rows get reconciled to the live roster
                // below (REQ-PICKER-3), which also carries the local controllable.
                controllable: None,
                endpoint_type: String::new(),
                // A remote node's pre-bind UNBOUND window isn't observable here —
                // subnet rows render plain (same limit as controllability above).
                is_unbound: false,
            });
        }
    }
    out
}

/// Union an endpoint's ORIGIN project (the cwd it was created in) into its
/// committed-context project membership (REQ-PICKER-HISTORY-FRESH). `origin` is


Now checking env-knob convention (SPT_NTP_SERVER mirror) + Status enum + registryhost imports.


crates\spt-daemon\tests\mesh.rs-64-        let dir = TempDir::new().expect("temp home");
crates\spt-daemon\tests\mesh.rs-65-        std::env::set_var("SPT_HOME", dir.path());
crates\spt-daemon\tests\mesh.rs:66:        std::env::set_var("SPT_NTP_SERVER", "off");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs-67-        dir
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\mesh.rs-68-    });
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-27-
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-28-/// Default NTP servers, tried in order (first answer wins). Overridable via
crates\spt-net\src\net\pairing\ntp.rs:29:/// `SPT_NTP_SERVER` (tests point it at a loopback mock; `off` disables the
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-30-/// query entirely — pure system clock).
crates\spt-net\src\net\pairing\ntp.rs-31-const DEFAULT_SERVERS: &[&str] = &["time.google.com:123", "pool.ntp.org:123"];
--
crates\spt-net\src\net\pairing\ntp.rs-60-\ for loopback-scoped endpoints: a hermetic test must never probe a real
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-61-/// NTP server (the same "no network reached" promise as
crates\spt-net\src\net\pairing\ntp.rs:62:/// `BindScope::Loopback` itself). `SPT_NTP_SERVER=off` is the explicit env
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-63-/// override on top.
crates\spt-net\src\net\pairing\ntp.rs-64-static ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
--
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-104-        return None;
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-105-    }
crates\spt-net\src\net\pairing\ntp.rs:106:    let override_server = std::env::var("SPT_NTP_SERVER").ok();
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-107-    if override_server.as_deref() == Some("off") {
C:\Users\decid\Documents\projects\spt-core\crates\spt-net\src\net\pairing\ntp.rs-108-        return None;
--
crates\spt-daemon\tests\pairjoin.rs-45-    \ Hermetic ceremony clock (REQ-PAIR-8): no real NTP probe from a test —
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\pairjoin.rs-46-    // the offset falls back to the system clock, exactly the offline path.
crates\spt-daemon\tests\pairjoin.rs:47:    std::env::set_var("SPT_NTP_SERVER", "off");
C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\pairjoin.rs-48-
crates\spt-daemon\tests\pairjoin.rs-49-    \ The responder side is a stock member: the subnet record exists, the


39:use spt_store::liveness::is_perch_alive;
397:            let status = if is_perch_alive(&perch_path) {
1607:            "refused origin earns no liveness credit"


40	/// ceremony clock every window does not become a de-facto sync loop.
41	const OFFSET_TTL: Duration = Duration::from_secs(15 * 60);
42
43	/// Seconds between the NTP era origin (1900-01-01) and the unix epoch.
44	const NTP_UNIX_DELTA: u64 = 2_208_988_800;
45
46	/// The cached ceremony-clock offset: `when` stamps the last query ATTEMPT
47	/// (success or failure — a flaky network must not be re-probed per call),
48	/// `offset_secs` the last known correction (`0` = system clock).
49	struct CachedOffset {
50	    when: Option<Instant>,
51	    offset_secs: i64,
52	}
53
54	static OFFSET: Mutex<CachedOffset> = Mutex::new(CachedOffset {
55	    when: None,
56	    offset_secs: 0,
57	});
58
59	/// Process-wide NTP enable switch (default ON). The net host flips it OFF
60	/// for loopback-scoped endpoints: a hermetic test must never probe a real
61	/// NTP server (the same "no network reached" promise as
62	/// `BindScope::Loopback` itself). `SPT_NTP_SERVER=off` is the explicit env
63	/// override on top.
64	static ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
65
66	/// Enable/disable the ceremony-clock NTP query for this process (the
67	/// hermeticity switch — see [`ENABLED`]). Disabled ⇒ pure system clock.
68	pub fn set_enabled(enabled: bool) {
69	    ENABLED.store(enabled, std::sync::atomic::Ordering::Relaxed);
70	}
71
72	/// The ceremony clock (unix seconds): the system clock corrected by the
73	/// lazily-queried NTP offset. Every pairing-side time read (joiner ceremony,
74	/// responder window check, meet-rotation token derivation) goes through
75	/// here; nothing else does — the offset is TOTP-scoped by construction.
76	// [impl->REQ-PAIR-8]
77	pub fn ceremony_now_secs() -> u64 {
78	    let sys = system_now_secs();
79	    sys.saturating_add_signed(current_offset_secs())
80	}
81
82	/// The current in-process offset, refreshing the cache when stale. `0` when
83	/// NTP is disabled, unreachable, or simply agrees with the system clock.
84	fn current_offset_secs() -> i64 {
85	    let mut cache = OFFSET.lock().unwrap_or_else(|p| p.into_inner());
86	    let fresh = cache.when.is_some_and(|t| t.elapsed() < OFFSET_TTL);
87	    if !fresh {
88	        cache.when = Some(Instant::now());
89	        cache.offset_secs = query_offset_secs().unwrap_or(0);
90	        if cache.offset_secs != 0 {
91	            eprintln!(
92	                "NTP_TOTP_OFFSET: ceremony clock corrected by {}s (system clock skewed; OS clock untouched)",
93	                cache.offset_secs
94	            );
95	        }
96	    }
97	    cache.offset_secs
98	}
99
100	/// One offset query against the configured servers: NTP time minus system
101	/// time at receipt. `None` = no server answered (system-clock fallback).
102	fn query_offset_secs() -> Option<i64> {
103	    if !ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
104	        return None;
105	    }
106	    let override_server = std::env::var("SPT_NTP_SERVER").ok();
107	    if override_server.as_deref() == Some("off") {
108	        return None;
109	    }
110	    let servers: Vec<&str> = match &override_server {
111	        Some(s) => vec![s.as_str()],
112	        None => DEFAULT_SERVERS.to_vec(),
113	    };
114	    for server in servers {

/// ceremony clock every window does not become a de-facto sync loop.
const OFFSET_TTL: Duration = Duration::from_secs(15 * 60);

/// Seconds between the NTP era origin (1900-01-01) and the unix epoch.
const NTP_UNIX_DELTA: u64 = 2_208_988_800;

/// The cached ceremony-clock offset: `when` stamps the last query ATTEMPT
/// (success or failure — a flaky network must not be re-probed per call),
/// `offset_secs` the last known correction (`0` = system clock).
struct CachedOffset {
    when: Option<Instant>,
    offset_secs: i64,
}

static OFFSET: Mutex<CachedOffset> = Mutex::new(CachedOffset {
    when: None,
    offset_secs: 0,
});

/// Process-wide NTP enable switch (default ON). The net host flips it OFF
/// for loopback-scoped endpoints: a hermetic test must never probe a real
/// NTP server (the same "no network reached" promise as
/// `BindScope::Loopback` itself). `SPT_NTP_SERVER=off` is the explicit env
/// override on top.
static ENABLED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);

/// Enable/disable the ceremony-clock NTP query for this process (the
/// hermeticity switch — see [`ENABLED`]). Disabled ⇒ pure system clock.
pub fn set_enabled(enabled: bool) {
    ENABLED.store(enabled, std::sync::atomic::Ordering::Relaxed);
}

/// The ceremony clock (unix seconds): the system clock corrected by the
/// lazily-queried NTP offset. Every pairing-side time read (joiner ceremony,
/// responder window check, meet-rotation token derivation) goes through
/// here; nothing else does — the offset is TOTP-scoped by construction.
// [impl->REQ-PAIR-8]
pub fn ceremony_now_secs() -> u64 {
    let sys = system_now_secs();
    sys.saturating_add_signed(current_offset_secs())
}

/// The current in-process offset, refreshing the cache when stale. `0` when
/// NTP is disabled, unreachable, or simply agrees with the system clock.
fn current_offset_secs() -> i64 {
    let mut cache = OFFSET.lock().unwrap_or_else(|p| p.into_inner());
    let fresh = cache.when.is_some_and(|t| t.elapsed() < OFFSET_TTL);
    if !fresh {
        cache.when = Some(Instant::now());
        cache.offset_secs = query_offset_secs().unwrap_or(0);
        if cache.offset_secs != 0 {
            eprintln!(
                "NTP_TOTP_OFFSET: ceremony clock corrected by {}s (system clock skewed; OS clock untouched)",
                cache.offset_secs
            );
        }
    }
    cache.offset_secs
}

/// One offset query against the configured servers: NTP time minus system
/// time at receipt. `None` = no server answered (system-clock fallback).
fn query_offset_secs() -> Option<i64> {
    if !ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
        return None;
    }
    let override_server = std::env::var("SPT_NTP_SERVER").ok();
    if override_server.as_deref() == Some("off") {
        return None;
    }
    let servers: Vec<&str> = match &override_server {
        Some(s) => vec![s.as_str()],
        None => DEFAULT_SERVERS.to_vec(),
    };
    for server in servers {


13://! lagging node could re-announce a stale `Active` after the endpoint has actually gone
14://! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule
17://! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging
18://! `Active` (lower epoch). The epoch is the per-node monotonic counter
52:pub enum Status {
54:    Active,
56:    Dormant,
59:    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;
63:    Suspended,
64:    /// Believed down. Retained (not dropped) so a newer-epoch revival or a stale-Active
66:    Offline,
295:    /// ([`Stale`](MergeOutcome::Stale)) so a lagging `Active` cannot overwrite a newer
296:    /// `Offline`; a new node is added ([`Inserted`](MergeOutcome::Inserted)). Distinct
319:    /// Whether an instance in this status is a valid routing target. `Offline` is not
320:    /// (the resolver skips it); `Active`, `Dormant`, and `Suspended` are addressable.
322:        !matches!(self, Status::Offline)
365:/// (ADR-0006 §6, exclusion by construction). Unroutable (`Offline`) rows are
432:    /// **different** node, including by an `Offline` instance (a resting endpoint still
646:    /// refuse if several live nodes remain (force `@node`). `Offline` instances are
816:        reg.merge_instance("ling", inst("nodeA", Status::Active, 1));
817:        reg.merge_instance("ling", inst("nodeB", Status::Dormant, 1));
828:            reg.merge_instance("ling", inst("nodeA", Status::Active, 1)),
832:            reg.merge_instance("ling", inst("nodeB", Status::Active, 1)),
842:        reg.merge_instance("ling", inst("nodeA", Status::Active, 1));
844:            reg.merge_instance("ling", inst("nodeA", Status::Dormant, 2)),
848:        assert_eq!(reg.instances("ling")[0].status, Status::Dormant);
852:    // lower-epoch Active must NOT overwrite a newer higher-epoch Offline — the resolved
853:    // status stays Offline, so a partition/skew never routes to a dead instance.
857:        // Endpoint went Offline at the newer epoch 5.
858:        reg.merge_instance("ling", inst("nodeA", Status::Offline, 5));

[Showing results with pagination = limit: 30]


Read existing registryhost test region to mirror unit-test style.


1400	            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();
1401	        assert_eq!(reg.node_labels().collect::<Vec<_>>(), vec![("bb22", "RENAMED")]);
1402	        assert!(reg.endpoint_ids().next().is_none(), "no phantom endpoint row");
1403	    }
1404
1405	    // [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state
1406	    // machine: a live perch advertises its machine state (active → dormant →
1407	    // suspended), each transition reaching the registry as an epoch-bumped
1408	    // update (the lease orders them); a recordless perch keeps the legacy
1409	    // liveness-derived Active.
1410	    #[test]
1411	    fn advertisement_follows_resting_transitions_epoch_bumped() {
1412	        with_home(|home| {
1413	            let owlery = home.join("owlery");
1414	            let p = perch::resolve_perch_path_in(&owlery, "ling", ParentHint::Infer);
1415	            std::fs::create_dir_all(&p).unwrap();
1416	            let rec = InfoJson::new("ling", "now", std::process::id(), "s", "live_agent");
1417	            info::write_info(&p, &rec).unwrap();
1418	            let mut subnets = SubnetStore::load();
1419	            subnets.create_subnet("home").unwrap();
1420	            subnets.save().unwrap();
1421
1422	            let h = host(home);
1423
1424	            // No resting record yet (pre-D9-2 parity): live ⇒ Active.
1425	            h.advertise_local(&owlery);
1426	            let row = h.rows("home", "ling")[0].clone();
1427	            assert_eq!(
1428	                row.status,
1429	                Status::Active,
1430	                "recordless live perch reads Active"
1431	            );
1432
1433	            // active → dormant: the next advertisement carries Dormant at a
1434	            // strictly greater epoch.
1435	            crate::resting::write_rest(&p, crate::resting::RestState::Dormant, 1_000).unwrap();
1436	            h.advertise_local(&owlery);
1437	            let dormant = h.rows("home", "ling")[0].clone();
1438	            assert_eq!(
1439	                dormant.status,
1440	                Status::Dormant,
1441	                "advertisement follows the machine"
1442	            );
1443	            assert!(
1444	                dormant.epoch > row.epoch,
1445	                "transition is epoch-bumped (the lease orders it)"
1446	            );
1447
1448	            // dormant → suspended.
1449	            crate::resting::write_rest(&p, crate::resting::RestState::Suspended, 2_000).unwrap();
1450	            h.advertise_local(&owlery);
1451	            let suspended = h.rows("home", "ling")[0].clone();
1452	            assert_eq!(suspended.status, Status::Suspended);
1453	            assert!(suspended.epoch > dormant.epoch);
1454
1455	            // wake: suspended → active again.
1456	            crate::resting::write_rest(&p, crate::resting::RestState::Active, 3_000).unwrap();
1457	            h.advertise_local(&owlery);
1458	            assert_eq!(
1459	                h.rows("home", "ling")[0].status,
1460	                Status::Active,
1461	                "wake re-advertises Active"
1462	            );
1463	        });
1464	    }
1465
1466	    // [unit->REQ-HAZARD-ROSTER-GHOST] a LOCAL perch erased between rounds
1467	    // (`api session-end --erase` removes the owlery dir) stops being advertised
1468	    // ACTIVE: the next advertise_local sees it gone from disk and advertises it
1469	    // OFFLINE (fresh epoch), so resource_projection no longer lists it and peers

            serde_json::from_slice(&std::fs::read(&snap).unwrap()).unwrap();
        assert_eq!(reg.node_labels().collect::<Vec<_>>(), vec![("bb22", "RENAMED")]);
        assert!(reg.endpoint_ids().next().is_none(), "no phantom endpoint row");
    }

    // [unit->REQ-INST-3] registry advertisement FOLLOWS the resting-state
    // machine: a live perch advertises its machine state (active → dormant →
    // suspended), each transition reaching the registry as an epoch-bumped
    // update (the lease orders them); a recordless perch keeps the legacy
    // liveness-derived Active.
    #[test]
    fn advertisement_follows_resting_transitions_epoch_bumped() {
        with_home(|home| {
            let owlery = home.join("owlery");
            let p = perch::resolve_perch_path_in(&owlery, "ling", ParentHint::Infer);
            std::fs::create_dir_all(&p).unwrap();
            let rec = InfoJson::new("ling", "now", std::process::id(), "s", "live_agent");
            info::write_info(&p, &rec).unwrap();
            let mut subnets = SubnetStore::load();
            subnets.create_subnet("home").unwrap();
            subnets.save().unwrap();

            let h = host(home);

            // No resting record yet (pre-D9-2 parity): live ⇒ Active.
            h.advertise_local(&owlery);
            let row = h.rows("home", "ling")[0].clone();
            assert_eq!(
                row.status,
                Status::Active,
                "recordless live perch reads Active"
            );

            // active → dormant: the next advertisement carries Dormant at a
            // strictly greater epoch.
            crate::resting::write_rest(&p, crate::resting::RestState::Dormant, 1_000).unwrap();
            h.advertise_local(&owlery);
            let dormant = h.rows("home", "ling")[0].clone();
            assert_eq!(
                dormant.status,
                Status::Dormant,
                "advertisement follows the machine"
            );
            assert!(
                dormant.epoch > row.epoch,
                "transition is epoch-bumped (the lease orders it)"
            );

            // dormant → suspended.
            crate::resting::write_rest(&p, crate::resting::RestState::Suspended, 2_000).unwrap();
            h.advertise_local(&owlery);
            let suspended = h.rows("home", "ling")[0].clone();
            assert_eq!(suspended.status, Status::Suspended);
            assert!(suspended.epoch > dormant.epoch);

            // wake: suspended → active again.
            crate::resting::write_rest(&p, crate::resting::RestState::Active, 3_000).unwrap();
            h.advertise_local(&owlery);
            assert_eq!(
                h.rows("home", "ling")[0].status,
                Status::Active,
                "wake re-advertises Active"
            );
        });
    }

    // [unit->REQ-HAZARD-ROSTER-GHOST] a LOCAL perch erased between rounds
    // (`api session-end --erase` removes the owlery dir) stops being advertised
    // ACTIVE: the next advertise_local sees it gone from disk and advertises it
    // OFFLINE (fresh epoch), so resource_projection no longer lists it and peers


W4 first — surgical presence-liveness fix. Extracting status derivation into a testable helper.


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)

use spt_net::net::replicate::{NodeLabelUpdate, RegistryFeedRecord, RegistryUpdate};


Edit

            // [impl->REQ-INST-3]
            let status = if is_perch_alive(&perch_path) {
                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
                    Some(crate::resting::RestState::Active) | None => Status::Active,
                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
                }
            } else {
                Status::Dormant
            };

            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]
            let status = advertised_status(&perch_path);


            // [impl->REQ-INST-3]
            let status = if is_perch_alive(&perch_path) {
                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
                    Some(crate::resting::RestState::Active) | None => Status::Active,
                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
                }
            } else {
                Status::Dormant
            };

            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]
            let status = advertised_status(&perch_path);

// liveness-derived mapping. Every advertisement stamps a fresh

// epoch below, so a transition reaches peers as an epoch-bumped

// update — the lease orders it (KH 4.8).

-            let status = if is_perch_alive(&perch_path) {

-                match crate::resting::read_rest(&perch_path).map(|r| r.state) {

-                    Some(crate::resting::RestState::Active) | None => Status::Active,

-                    Some(crate::resting::RestState::Dormant) => Status::Dormant,

-                    Some(crate::resting::RestState::Suspended) => Status::Suspended,

+            // [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]

+            let status = advertised_status(&perch_path);

// 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


Grep

^impl |^pub fn |^fn |^struct |^pub struct


59:pub struct RegistryGatePolicy {
69:impl RegistryGatePolicy {
94:pub struct RegistryHost {
122:impl RegistryHost {
703:pub fn superseded_identities(
730:pub struct RekeyNotice {
760:pub fn repair_evict_superseded(
834:pub fn request_advertise_now() {
845:pub fn take_advertise_now(marker_path: &Path) -> bool {
851:fn wall_ms() -> u64 {
859:fn atomic_write(path: &Path, bytes: &[u8]) -> io::Result<()> {


693	        evicted
694	    }
695	}
696
697	/// The superseded identities a completed ceremony's intro reveals (pure,
698	/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node
699	/// label AND machine id under a DIFFERENT node key than the re-paired
700	/// joiner. Both must match — a label alone re-pairs nothing (hostnames
701	/// collide), a machine id alone is never gossiped without its label.
702	// [impl->REQ-SUBNET-7]
703	pub fn superseded_identities(
704	    reg: &SubnetRegistry,
705	    intro: &spt_net::net::pairing::wire::NodeIntro,
706	    joiner_hex: &str,
707	) -> Vec<String> {

        evicted
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,
/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node
/// label AND machine id under a DIFFERENT node key than the re-paired
/// joiner. Both must match — a label alone re-pairs nothing (hostnames
/// collide), a machine id alone is never gossiped without its label.
// [impl->REQ-SUBNET-7]
pub fn superseded_identities(
    reg: &SubnetRegistry,
    intro: &spt_net::net::pairing::wire::NodeIntro,
    joiner_hex: &str,
) -> Vec<String> {


Insert the helper after the impl block (line 695).


Edit

        evicted
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,

        evicted
    }
}

/// The [`Status`] this node advertises for one of its OWN perches (the
/// self-gossip path of [`RegistryHost::advertise_local`]).
///
/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —
///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒
///   Active), the multi-instance routing differentiator (REQ-INST-3).
/// - **Live but UNBOUND** (`is_perch_unbound`): `is_perch_alive` is bound-gated
///   so it reads false, but a live broker session exists — the perch is WARM, a
///   valid routing fallback → `Dormant` (the picker maps Dormant→online; W5
///   refines the *display* of unbound).
/// - **Cold** (no live session, not unbound): this node is nonetheless UP — this
///   very daemon is gossiping the row — so `Suspended` (cold-but-node-up,
///   resumable-on-wake). NEVER `Dormant` (that requires warm and falsely reads
///   ONLINE on a remote picker — the bug this fixes) and NEVER `Offline` (a live
///   node never self-gossips Offline; node-down is only ever remote-inferred via
///   eviction, and `RestState` has no Offline).
// [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]
fn advertised_status(perch_path: &Path) -> Status {
    if is_perch_alive(perch_path) {
        match crate::resting::read_rest(perch_path).map(|r| r.state) {
            Some(crate::resting::RestState::Active) | None => Status::Active,
            Some(crate::resting::RestState::Dormant) => Status::Dormant,
            Some(crate::resting::RestState::Suspended) => Status::Suspended,
        }
    } else if is_perch_unbound(perch_path) {
        Status::Dormant
    } else {
        Status::Suspended
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,


        evicted
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,

        evicted
    }
}

/// The [`Status`] this node advertises for one of its OWN perches (the
/// self-gossip path of [`RegistryHost::advertise_local`]).
///
/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —
///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒
///   Active), the multi-instance routing differentiator (REQ-INST-3).
/// - **Live but UNBOUND** (`is_perch_unbound`): `is_perch_alive` is bound-gated
///   so it reads false, but a live broker session exists — the perch is WARM, a
///   valid routing fallback → `Dormant` (the picker maps Dormant→online; W5
///   refines the *display* of unbound).
/// - **Cold** (no live session, not unbound): this node is nonetheless UP — this
///   very daemon is gossiping the row — so `Suspended` (cold-but-node-up,
///   resumable-on-wake). NEVER `Dormant` (that requires warm and falsely reads
///   ONLINE on a remote picker — the bug this fixes) and NEVER `Offline` (a live
///   node never self-gossips Offline; node-down is only ever remote-inferred via
///   eviction, and `RestState` has no Offline).
// [impl->REQ-INST-3] [impl->REQ-PRESENCE-LIVENESS-TRUTH]
fn advertised_status(perch_path: &Path) -> Status {
    if is_perch_alive(perch_path) {
        match crate::resting::read_rest(perch_path).map(|r| r.state) {
            Some(crate::resting::RestState::Active) | None => Status::Active,
            Some(crate::resting::RestState::Dormant) => Status::Dormant,
            Some(crate::resting::RestState::Suspended) => Status::Suspended,
        }
    } else if is_perch_unbound(perch_path) {
        Status::Dormant
    } else {
        Status::Suspended
    }
}

/// The superseded identities a completed ceremony's intro reveals (pure,

+/// The [`Status`] this node advertises for one of its OWN perches (the

+/// self-gossip path of [`RegistryHost::advertise_local`]).

+/// - **Bound + alive** (`is_perch_alive`): follow the resting-state machine —

+///   Active / Dormant / Suspended per the perch's resting record (recordless ⇒

+///   Active), the multi-instance routing differentiator (REQ-INST-3).

+/// - **Live but UNBOUND** (`is_perch_unbound`): `is_perch_alive` is bound-gated

+///   so it reads false, but a live broker session exists — the perch is WARM, a

+///   valid routing fallback → `Dormant` (the picker maps Dormant→online; W5

+///   refines the *display* of unbound).

+/// - **Cold** (no live session, not unbound): this node is nonetheless UP — this

+///   very daemon is gossiping the row — so `Suspended` (cold-but-node-up,

+///   resumable-on-wake). NEVER `Dormant` (that requires warm and falsely reads

+///   ONLINE on a remote picker — the bug this fixes) and NEVER `Offline` (a live

+///   node never self-gossips Offline; node-down is only ever remote-inferred via

+///   eviction, and `RestState` has no Offline).

+fn advertised_status(perch_path: &Path) -> Status {

+        match crate::resting::read_rest(perch_path).map(|r| r.state) {

+            Some(crate::resting::RestState::Active) | None => Status::Active,

+            Some(crate::resting::RestState::Dormant) => Status::Dormant,

+            Some(crate::resting::RestState::Suspended) => Status::Suspended,

+    } else if is_perch_unbound(perch_path) {

/// M8 decision 13 / REQ-SUBNET-7): rows in `reg` advertising the SAME node

/// label AND machine id under a DIFFERENT node key than the re-paired


W4 unit test. Checking status-constant + set_status availability.


crates\spt-store\src\info.rs:264:pub fn set_status(perch_path: &Path, status: &str) -> std::io::Result<()> {
crates\spt-store\src\registry.rs:236:        use crate::liveness::STATUS_OFFLINE;
crates\spt-store\src\registry.rs:251:        set_status(&hosted, STATUS_OFFLINE).unwrap();
crates\spt-store\src\shellinfo.rs:31:pub const SHELL_STATUS_ONLINE: &str = "online";
crates\spt-store\src\shellinfo.rs:34:pub const SHELL_STATUS_OFFLINE: &str = "offline";
crates\spt-store\src\shellinfo.rs:51:    /// [`SHELL_STATUS_ONLINE`] | [`SHELL_STATUS_OFFLINE`].
crates\spt-store\src\shellinfo.rs:66:            status: SHELL_STATUS_OFFLINE.to_string(),
crates\spt-store\src\shellinfo.rs:413:        assert_eq!(info.status, SHELL_STATUS_OFFLINE);
crates\spt-store\src\liveness.rs:37:pub const STATUS_ONLINE: &str = "online";
crates\spt-store\src\liveness.rs:39:pub const STATUS_OFFLINE: &str = "offline";
crates\spt-store\src\liveness.rs:46:pub const STATUS_UNBOUND: &str = "unbound";
crates\spt-store\src\liveness.rs:82:    read_status(perch_path).as_deref() == Some(STATUS_UNBOUND)
crates\spt-store\src\liveness.rs:99:                return status == STATUS_ONLINE; // daemon-authoritative; never pid
crates\spt-store\src\liveness.rs:124:        Some(status) => status == STATUS_ONLINE || status == STATUS_UNBOUND,
crates\spt-store\src\liveness.rs:154:        set_status(d.path(), STATUS_ONLINE).unwrap();
crates\spt-store\src\liveness.rs:171:        set_status(d.path(), STATUS_OFFLINE).unwrap();
crates\spt-store\src\liveness.rs:235:        set_status(d.path(), STATUS_UNBOUND).unwrap();
crates\spt-store\src\liveness.rs:255:        set_status(&ep_perch, STATUS_UNBOUND).unwrap();
crates\spt-store\src\liveness.rs:272:        set_status(on.path(), STATUS_ONLINE).unwrap();
crates\spt-store\src\liveness.rs:281:        set_status(off.path(), STATUS_OFFLINE).unwrap();
crates\spt-store\src\liveness.rs:302:        set_status(&off, STATUS_OFFLINE).unwrap();
crates\spt-store\src\liveness.rs:312:        set_status(&on, STATUS_ONLINE).unwrap();


Grep

mod tests|use super|use spt_store|use crate::|fn with_home|fn host\b


No matches found

[Showing results with pagination = offset: 900]


mod tests|use super::|use spt_store|fn with_home|fn host\(|fn rows\(


38:use spt_store::epoch::EpochSource;
39:use spt_store::liveness::{is_perch_alive, is_perch_unbound};
40:use spt_store::perch::{self, ParentHint};
41:use spt_store::roster::RosterStore;
42:use spt_store::subnet::SubnetStore;
43:use spt_store::visibility::VisibilityStore;
592:    pub fn rows(&self, subnet: &str, endpoint_id: &str) -> Vec<Instance> {
906:mod tests {
907:    use super::*;
909:    use spt_store::info::{self, InfoJson};
952:    fn host(dir: &Path) -> RegistryHost {


906	mod tests {
907	    use super::*;
908	    use crate::test_home::with_home;
909	    use spt_store::info::{self, InfoJson};
910
911	    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
912	        RegistryUpdate {
913	            subnet: subnet.to_string(),
914	            endpoint_id: id.to_string(),
915	            instance: Instance {
916	                node: node.to_string(),
917	                status: Status::Active,
918	                epoch,
919	                resources: None,
920	                last_active_ms: None,
921	                shell_adapters: Vec::new(),
922	                node_label: None,
923	                machine_id: None,
924	                endpoint_type: None,
925	            },
926	        }
927	    }
928
929	    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed
930	    /// (`is_member` keys on `(subnet, pubkey)`).
931	    fn member(subnet: &str, pk: &str) -> spt_store::roster::RosterEntry {
932	        spt_store::roster::RosterEntry {
933	            pubkey_hex: pk.to_string(),
934	            subnet: subnet.to_string(),
935	            label: String::new(),
936	            machine_id: String::new(),
937	            address: None,
938	            last_seen: String::new(),
939	            lease_epoch: 1,
940	        }
941	    }
942
943	    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
944	        let mut roster = RosterStore::default();
945	        roster.merge_entry(member(subnet, trusted));
946	        RegistryGatePolicy {
947	            subnets: vec![subnet.to_string()],
948	            roster,
949	        }
950	    }
951
952	    fn host(dir: &Path) -> RegistryHost {
953	        RegistryHost::new_at(
954	            "aa11",
955	            EpochSource::load_from(&dir.join("epoch.json")),
956	            dir.join("registry"),
957	        )
958	    }
959
960	    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] the consent-notif producer

mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::info::{self, InfoJson};

    fn upd(subnet: &str, id: &str, node: &str, epoch: u64) -> RegistryUpdate {
        RegistryUpdate {
            subnet: subnet.to_string(),
            endpoint_id: id.to_string(),
            instance: Instance {
                node: node.to_string(),
                status: Status::Active,
                epoch,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        }
    }

    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed
    /// (`is_member` keys on `(subnet, pubkey)`).
    fn member(subnet: &str, pk: &str) -> spt_store::roster::RosterEntry {
        spt_store::roster::RosterEntry {
            pubkey_hex: pk.to_string(),
            subnet: subnet.to_string(),
            label: String::new(),
            machine_id: String::new(),
            address: None,
            last_seen: String::new(),
            lease_epoch: 1,
        }
    }

    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
        let mut roster = RosterStore::default();
        roster.merge_entry(member(subnet, trusted));
        RegistryGatePolicy {
            subnets: vec![subnet.to_string()],
            roster,
        }
    }

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

    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] the consent-notif producer


Edit

// [unit->REQ-HAZARD-ROSTER-GHOST] a LOCAL perch erased between rounds

    // [unit->REQ-PRESENCE-LIVENESS-TRUTH] the self-gossip status split that
    // removes the false-ONLINE: a live-but-UNBOUND perch is WARM → Dormant
    // (a routing fallback), but a COLD perch (no live session) reads
    // Suspended — NOT the old blanket Dormant (which a remote picker mapped to
    // green ONLINE for a dead endpoint). Offline is never self-gossiped here.
    #[test]
    fn advertised_status_splits_unbound_warm_from_cold_suspended() {
        use spt_store::info::set_status;
        use spt_store::liveness::{STATUS_OFFLINE, STATUS_UNBOUND};

        // A pid that is never a real allocation (mirrors the liveness suite) —
        // deterministic, avoids pid-reuse flake.
        const DEAD_PID: u32 = 2_000_000_000;

        with_home(|home| {
            let owlery = home.join("owlery");
            let mk = |id: &str| {
                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
                std::fs::create_dir_all(&p).unwrap();
                p
            };

            // Bound + alive (live pid, no resting record) ⇒ Active.
            let alive = mk("alive");
            info::write_info(
                &alive,
                &InfoJson::new("alive", "now", std::process::id(), "s", "live_agent"),
            )
            .unwrap();
            assert_eq!(advertised_status(&alive), Status::Active);

            // Live but UNBOUND (skeleton, harness has not bound) ⇒ warm Dormant.
            let unbound = mk("unbound");
            info::write_info(
                &unbound,
                &InfoJson::new("unbound", "now", std::process::id(), "s", "live_agent"),
            )
            .unwrap();
            set_status(&unbound, STATUS_UNBOUND).unwrap();
            assert_eq!(
                advertised_status(&unbound),
                Status::Dormant,
                "unbound-live is warm — a routing fallback, not cold"
            );

            // Cold via explicit offline status (no live session) ⇒ Suspended,
            // never the old blanket Dormant (the false-ONLINE source).
            let cold = mk("cold");
            info::write_info(
                &cold,
                &InfoJson::new("cold", "now", std::process::id(), "s", "live_agent"),
            )
            .unwrap();
            set_status(&cold, STATUS_OFFLINE).unwrap();
            assert_eq!(
                advertised_status(&cold),
                Status::Suspended,
                "cold-but-node-up advertises Suspended (picker → gray), not Dormant"
            );

            // Cold via dead pid (interim/external perch, no status) ⇒ Suspended.
            let dead = mk("dead");
            info::write_info(
                &dead,
                &InfoJson::new("dead", "now", DEAD_PID, "s", "live_agent"),
            )
            .unwrap();
            assert_eq!(
                advertised_status(&dead),
                Status::Suspended,
                "a dead external perch is cold, node still up ⇒ Suspended"
            );
        });
    }

    // [unit->REQ-HAZARD-ROSTER-GHOST] a LOCAL perch erased between rounds


    // [unit->REQ-PRESENCE-LIVENESS-TRUTH] the self-gossip status split that
    // removes the false-ONLINE: a live-but-UNBOUND perch is WARM → Dormant
    // (a routing fallback), but a COLD perch (no live session) reads
    // Suspended — NOT the old blanket Dormant (which a remote picker mapped to
    // green ONLINE for a dead endpoint). Offline is never self-gossiped here.
    #[test]
    fn advertised_status_splits_unbound_warm_from_cold_suspended() {
        use spt_store::info::set_status;
        use spt_store::liveness::{STATUS_OFFLINE, STATUS_UNBOUND};

        // A pid that is never a real allocation (mirrors the liveness suite) —
        // deterministic, avoids pid-reuse flake.
        const DEAD_PID: u32 = 2_000_000_000;

        with_home(|home| {
            let owlery = home.join("owlery");
            let mk = |id: &str| {
                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);
                std::fs::create_dir_all(&p).unwrap();
                p
            };

            // Bound + alive (live pid, no resting record) ⇒ Active.
            let alive = mk("alive");
            info::write_info(
                &alive,
                &InfoJson::new("alive", "now", std::process::id(), "s", "live_agent"),
            )
            .unwrap();
            assert_eq!(advertised_status(&alive), Status::Active);

            // Live but UNBOUND (skeleton, harness has not bound) ⇒ warm Dormant.
            let unbound = mk("unbound");
            info::write_info(
                &unbound,
                &InfoJson::new("unbound", "now", std::process::id(), "s", "live_agent"),
            )
            .unwrap();
            set_status(&unbound, STATUS_UNBOUND).unwrap();
            assert_eq!(
                advertised_status(&unbound),
                Status::Dormant,
                "unbound-live is warm — a routing fallback, not cold"
            );

            // Cold via explicit offline status (no live session) ⇒ Suspended,
            // never the old blanket Dormant (the false-ONLINE source).
            let cold = mk("cold");
            info::write_info(
                &cold,
                &InfoJson::new("cold", "now", std::process::id(), "s", "live_agent"),
            )
            .unwrap();
            set_status(&cold, STATUS_OFFLINE).unwrap();
            assert_eq!(
                advertised_status(&cold),
                Status::Suspended,
                "cold-but-node-up advertises Suspended (picker → gray), not Dormant"
            );

            // Cold via dead pid (interim/external perch, no status) ⇒ Suspended.
            let dead = mk("dead");
            info::write_info(
                &dead,
                &InfoJson::new("dead", "now", DEAD_PID, "s", "live_agent"),
            )
            .unwrap();
            assert_eq!(
                advertised_status(&dead),
                Status::Suspended,
                "a dead external perch is cold, node still up ⇒ Suspended"
            );
        });
    }

    // [unit->REQ-HAZARD-ROSTER-GHOST] a LOCAL perch erased between rounds

+    // [unit->REQ-PRESENCE-LIVENESS-TRUTH] the self-gossip status split that

+    // removes the false-ONLINE: a live-but-UNBOUND perch is WARM → Dormant

+    // (a routing fallback), but a COLD perch (no live session) reads

+    // Suspended — NOT the old blanket Dormant (which a remote picker mapped to

+    // green ONLINE for a dead endpoint). Offline is never self-gossiped here.

+    fn advertised_status_splits_unbound_warm_from_cold_suspended() {

+        use spt_store::info::set_status;

+        use spt_store::liveness::{STATUS_OFFLINE, STATUS_UNBOUND};

+        // A pid that is never a real allocation (mirrors the liveness suite) —

+        // deterministic, avoids pid-reuse flake.

+            let owlery = home.join("owlery");

+                let p = perch::resolve_perch_path_in(&owlery, id, ParentHint::Infer);

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

+            // Bound + alive (live pid, no resting record) ⇒ Active.

+                &InfoJson::new("alive", "now", std::process::id(), "s", "live_agent"),

+            assert_eq!(advertised_status(&alive), Status::Active);

+            // Live but UNBOUND (skeleton, harness has not bound) ⇒ warm Dormant.

+            let unbound = mk("unbound");

+                &InfoJson::new("unbound", "now", std::process::id(), "s", "live_agent"),

+            set_status(&unbound, STATUS_UNBOUND).unwrap();

+                advertised_status(&unbound),

+                "unbound-live is warm — a routing fallback, not cold"

+            // Cold via explicit offline status (no live session) ⇒ Suspended,

+            // never the old blanket Dormant (the false-ONLINE source).

+                &InfoJson::new("cold", "now", std::process::id(), "s", "live_agent"),

+            set_status(&cold, STATUS_OFFLINE).unwrap();

+                advertised_status(&cold),

+                "cold-but-node-up advertises Suspended (picker → gray), not Dormant"

+            // Cold via dead pid (interim/external perch, no status) ⇒ Suspended.

+                &InfoJson::new("dead", "now", DEAD_PID, "s", "live_agent"),

+                advertised_status(&dead),

+                "a dead external perch is cold, node still up ⇒ Suspended"

// (`api session-end --erase` removes the owlery dir) stops being advertised

// ACTIVE: the next advertise_local sees it gone from disk and advertises it


Now W4 int stage. Checking the existing twohost Suspended assertion + picker test patterns.


1090	            cache_a
1091	                .load_artifact()
1092	                .map(|b| b == RELEASE_BYTES)
1093	                .unwrap_or(false)
1094	        },
1095	    );
1096	    rig_wait("update: consent notif surfaced at A", rig.wait, || {
1097	        store_a
1098	            .list(&rig.subnet)
1099	            .map(|rows| rows.iter().any(|r| r.kind == "consent"))
1100	            .unwrap_or(false)
1101	    });
1102
1103	    // ── Rung 7b — notif acceptance leg: B's dismissal replicated back to A.
1104	    // [int->REQ-NOTIF-1]
1105	    rig_wait("notif: B's dismissal replicated to A", rig.wait, || {
1106	        store_a
1107	            .list(&rig.subnet)
1108	            .map(|rows| rows.iter().any(|r| r.body == NOTIF_BODY && r.dismissed))
1109	            .unwrap_or(false)
1110	    });
1111
1112	    // ── M5 rung 8 — remote suspend/wake (drive side): `spt suspend id@node`'s
1113	    // machinery — one wire rest op each way, the TARGET's transition host
1114	    // runs the edge, and the state flip comes back through the ordinary
1115	    // registry advertisement (epoch-leased, never wall-clock).
1116	    // [int->REQ-INST-6]
1117	    let out = spt_daemon::resthost::request_rest(
1118	        &mut a,
1119	        conn.conn_id,
1120	        ID_B,
1121	        spt_net::net::rest::REST_EVENT_SUSPEND,
1122	        "twohost-rest-suspend",
1123	        op(),
1124	    )
1125	    .expect("suspend round trip");
1126	    assert!(
1127	        matches!(out, spt_daemon::resthost::RestRequestOutcome::Edge(_)),
1128	        "remote suspend applied an edge at B: {out:?}"
1129	    );
1130	    rig_wait("rest: B's row advertises Suspended at A", rig.wait, || {
1131	        registry
1132	            .rows(&rig.subnet, ID_B)
1133	            .iter()
1134	            .any(|i| i.node == rig.b_hex() && i.status == spt_net::net::registry::Status::Suspended)
1135	    });
1136	    let out = spt_daemon::resthost::request_rest(
1137	        &mut a,
1138	        conn.conn_id,
1139	        ID_B,
1140	        spt_net::net::rest::REST_EVENT_WAKE,
1141	        "twohost-rest-wake",
1142	        op(),
1143	    )
1144	    .expect("wake round trip");
1145	    assert!(
1146	        matches!(out, spt_daemon::resthost::RestRequestOutcome::Edge(_)),
1147	        "remote wake applied an edge at B: {out:?}"
1148	    );
1149	    rig_wait(
1150	        "rest: B's row advertises Active again at A (with a presence stamp)",
1151	        rig.wait,
1152	        || {
1153	            registry.rows(&rig.subnet, ID_B).iter().any(|i| {
1154	                i.node == rig.b_hex()
1155	                    && i.status == spt_net::net::registry::Status::Active
1156	                    && i.last_active_ms.is_some()
1157	            })
1158	        },
1159	    );

            cache_a
                .load_artifact()
                .map(|b| b == RELEASE_BYTES)
                .unwrap_or(false)
        },
    );
    rig_wait("update: consent notif surfaced at A", rig.wait, || {
        store_a
            .list(&rig.subnet)
            .map(|rows| rows.iter().any(|r| r.kind == "consent"))
            .unwrap_or(false)
    });

    // ── Rung 7b — notif acceptance leg: B's dismissal replicated back to A.
    // [int->REQ-NOTIF-1]
    rig_wait("notif: B's dismissal replicated to A", rig.wait, || {
        store_a
            .list(&rig.subnet)
            .map(|rows| rows.iter().any(|r| r.body == NOTIF_BODY && r.dismissed))
            .unwrap_or(false)
    });

    // ── M5 rung 8 — remote suspend/wake (drive side): `spt suspend id@node`'s
    // machinery — one wire rest op each way, the TARGET's transition host
    // runs the edge, and the state flip comes back through the ordinary
    // registry advertisement (epoch-leased, never wall-clock).
    // [int->REQ-INST-6]
    let out = spt_daemon::resthost::request_rest(
        &mut a,
        conn.conn_id,
        ID_B,
        spt_net::net::rest::REST_EVENT_SUSPEND,
        "twohost-rest-suspend",
        op(),
    )
    .expect("suspend round trip");
    assert!(
        matches!(out, spt_daemon::resthost::RestRequestOutcome::Edge(_)),
        "remote suspend applied an edge at B: {out:?}"
    );
    rig_wait("rest: B's row advertises Suspended at A", rig.wait, || {
        registry
            .rows(&rig.subnet, ID_B)
            .iter()
            .any(|i| i.node == rig.b_hex() && i.status == spt_net::net::registry::Status::Suspended)
    });
    let out = spt_daemon::resthost::request_rest(
        &mut a,
        conn.conn_id,
        ID_B,
        spt_net::net::rest::REST_EVENT_WAKE,
        "twohost-rest-wake",
        op(),
    )
    .expect("wake round trip");
    assert!(
        matches!(out, spt_daemon::resthost::RestRequestOutcome::Edge(_)),
        "remote wake applied an edge at B: {out:?}"
    );
    rig_wait(
        "rest: B's row advertises Active again at A (with a presence stamp)",
        rig.wait,
        || {
            registry.rows(&rig.subnet, ID_B).iter().any(|i| {
                i.node == rig.b_hex()
                    && i.status == spt_net::net::registry::Status::Active
                    && i.last_active_ms.is_some()
            })
        },
    );


Checking picker test surface + EpStatus to place the W4 int faithfully.


Bash

ls crates/spt-daemon/tests/ && echo "---model EpStatus---" && grep -rn "enum EpStatus\|pub enum EpStatus" crates/spt/src/picker/ && echo "---picker data tests---" && grep -n "mod tests\|fn .*test\|#\[test\]" crates/spt/src/picker/data.rs | head -30


access.rs
attach.rs
brain_swap.rs
broker.rs
budget.rs
daemon_e2e.rs
daemon_lifecycle_real_brain.rs
digest.rs
dispatch.rs
driven_by_selfheal.rs
handoff.rs
idempotent.rs
inject_control_wedge.rs
input_ack_deadlock.rs
mesh.rs
netbroker.rs
netstream.rs
notifsync.rs
pairjoin.rs
presence.rs
propagate.rs
pump.rs
pumpdeadline.rs
replicate.rs
reseed.rs
resume.rs
rosterprop.rs
seedproofx.rs
shellchan.rs
sync.rs
two_origin_spanning.rs
twohost.rs
wanmsg.rs
xfer.rs
---model EpStatus---
crates/spt/src/picker/model.rs:34:pub enum EpStatus {
---picker data tests---
390:mod tests {
419:    #[test]
450:    #[test]
500:    #[test]
531:    #[test]
558:    #[test]
585:    #[test]


`presence.rs` integration test exists — natural home for W4 int. Reading it.


1	//! D4c regression — the PresenceChannel broker seam (REQ-EP-4): connection
2	//! liveness as a sequenced, buffered, cursor-resumable event log.
3	//!
4	//! One in-process broker (hermetic loopback endpoint, relays disabled) plays
5	//! the daemon; a standalone droppable [`NetHost`] plays the remote peer. The
6	//! invariants:
7	//!
8	//! - a peer's dial surfaces as a `connected` presence event carrying the
9	//!   handshake-proven remote identity;
10	//! - a peer's close removes the conn row (conns LEAVE the table — the D4c
11	//!   fix) and surfaces as a `disconnected` event;
12	//! - presence events produced while no brain is attached sit in the ring and
13	//!   replay from the resume cursor — a brain restart loses no liveness
14	//!   transition (the StreamLog discipline applied to the conn table).
15
16	use std::sync::atomic::{AtomicU32, Ordering};
17	use std::sync::Arc;
18	use std::thread;
19	use std::time::Duration;
20
21	use spt_daemon::brain::{Brain, BrokerEvent};
22	use spt_daemon::msg::{NetPresenceEvent, PRESENCE_CONNECTED, PRESENCE_DISCONNECTED};
23	use spt_daemon::nethost::{NetConfig, NetHost};
24	use spt_daemon::Broker;
25	use spt_net::net::endpoint::{BindScope, EndpointAddr, LocalDiscovery, RelayPolicy};
26	use spt_proto::identity::Identity;
27
28	static SEQ: AtomicU32 = AtomicU32::new(0);
29	fn unique_name() -> String {
30	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
31	    format!("spt-daemon-d4c-{}-{}.sock", std::process::id(), n)
32	}
33
34	fn hermetic() -> NetConfig {
35	    NetConfig {
36	        identity: Identity::generate(),
37	        relay: RelayPolicy::Disabled,
38	        discovery: LocalDiscovery::Off,
39	        scope: BindScope::Loopback,
40	        membership: None,
41	    }
42	}
43
44	fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
45	    let host = NetHost::start(hermetic()).expect("net host start");
46	    let broker =
47	        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
48	    let serve = Arc::clone(&broker);
49	    thread::spawn(move || {
50	        let _ = serve.serve();
51	    });
52	    broker
53	}
54
55	fn connect_retry(name: &str) -> Brain {
56	    for _ in 0..300 {
57	        if let Ok(b) = Brain::cold_start(name, 1) {
58	            return b;
59	        }
60	        thread::sleep(Duration::from_millis(10));
61	    }
62	    panic!("brain could not connect");
63	}
64
65	/// Read events until the next presence event arrives.
66	fn read_presence(brain: &mut Brain) -> NetPresenceEvent {
67	    loop {
68	        match brain.read_event().expect("read event") {
69	            BrokerEvent::NetPresence(ev) => return ev,
70	            BrokerEvent::Error { message } => panic!("broker error: {message}"),
71	            _ => {}
72	        }
73	    }
74	}
75
76	// [unit->REQ-EP-4] the full presence lifecycle across a brain restart: a
77	// standalone peer host dials the broker (→ live `connected` event with the
78	// handshake-proven identity), the brain crashes, the peer closes while no
79	// brain is attached (→ conn row removed + `disconnected` buffered in the
80	// ring), and a fresh brain resumes from its durable cursor — replaying the
81	// missed transition exactly once, no gap, no duplicate.
82	#[test]
83	fn presence_survives_brain_restart_with_cursor_resume() {
84	    let dir = tempfile::tempdir().expect("tempdir");
85	    let name_b = unique_name();
86	    let _broker_b = net_broker(&name_b, dir.path());
87
88	    // Brain life 1: subscribe to presence from the start of history.
89	    let mut life1 = connect_retry(&name_b);
90	    life1.net_presence_subscribe(0).expect("subscribe");
91
92	    // The remote peer: a standalone broker-shaped net host that dials in.
93	    let peer = NetHost::start(hermetic()).expect("peer host");
94	    let peer_hex = peer.node_id_hex();
95	    let addr: EndpointAddr =
96	        serde_json::from_value(life1.net_status().expect("status").addr).expect("addr decodes");
97	    peer.dial(addr).expect("peer dials the broker");
98
99	    // Live `connected`, carrying the handshake-proven peer identity.
100	    let ev = read_presence(&mut life1);
101	    assert_eq!(ev.seq, 0, "first presence event");
102	    assert_eq!(ev.kind, PRESENCE_CONNECTED);
103	    assert_eq!(
104	        ev.remote_id_hex, peer_hex,
105	        "identity proven by the handshake"
106	    );
107	    let conn_id = ev.conn_id;
108	    assert_eq!(
109	        life1.net_status().expect("status").conn_count,
110	        1,
111	        "row in the table"
112	    );
113
114	    // Carry the durable cursor forward, then crash the brain.
115	    let cursor = life1.presence_cursor().expect("cursor set");
116	    assert_eq!(cursor, 1);
117	    drop(life1); // the crash — broker, conn, and presence log survive
118
119	    // The peer closes INTO the dead window: the closed-watcher removes the
120	    // conn row and buffers `disconnected` with no subscriber attached.
121	    peer.close();
122	    drop(peer);
123
124	    // Brain life 2: resume from the durable cursor — the ring replays the
125	    // missed `disconnected` (and nothing already seen).
126	    let mut life2 = connect_retry(&name_b);
127	    life2.net_presence_subscribe(cursor).expect("resubscribe");
128	    let ev = read_presence(&mut life2);
129	    assert_eq!(ev.seq, 1, "contiguous resume — no gap, no duplicate");
130	    assert_eq!(ev.kind, PRESENCE_DISCONNECTED);
131	    assert_eq!(ev.conn_id, conn_id, "the same connection's lifecycle");
132	    assert_eq!(ev.remote_id_hex, peer_hex);
133
134	    // The D4c fix proper: the closed conn LEFT the table.
135	    assert_eq!(
136	        life2.net_status().expect("status").conn_count,
137	        0,
138	        "row removed on close"
139	    );
140	    assert_eq!(
141	        life2.presence_cursor(),
142	        Some(2),
143	        "cursor advanced past the replay"
144	    );
145	}
146
147	// [unit->REQ-EP-4] a late subscriber replays the WHOLE retained history from
148	// seq 0 (connected + disconnected, in order) — presence is a log, not a
149	// snapshot, so a brain that was never attached still reconstructs liveness.
150	#[test]
151	fn presence_history_replays_from_zero_for_a_late_subscriber() {
152	    let dir = tempfile::tempdir().expect("tempdir");
153	    let name_b = unique_name();
154	    let _broker_b = net_broker(&name_b, dir.path());
155
156	    // Resolve the broker's address, but do NOT subscribe yet.
157	    let mut probe = connect_retry(&name_b);
158	    let addr: EndpointAddr =
159	        serde_json::from_value(probe.net_status().expect("status").addr).expect("addr decodes");
160
161	    // The peer's whole lifecycle happens with no presence subscriber attached.
162	    let peer = NetHost::start(hermetic()).expect("peer host");
163	    let peer_hex = peer.node_id_hex();
164	    peer.dial(addr).expect("peer dials");
165	    // Wait until the broker registered the row before closing it.
166	    for _ in 0..400 {
167	        if probe.net_status().expect("status").conn_count == 1 {
168	            break;
169	        }
170	        thread::sleep(Duration::from_millis(5));
171	    }
172	    peer.close();
173	    drop(peer);
174
175	    // Late subscribe from 0: both buffered transitions replay, in seq order.
176	    probe.net_presence_subscribe(0).expect("subscribe");
177	    let first = read_presence(&mut probe);
178	    let second = read_presence(&mut probe);
179	    assert_eq!((first.seq, first.kind.as_str()), (0, PRESENCE_CONNECTED));
180	    assert_eq!(
181	        (second.seq, second.kind.as_str()),
182	        (1, PRESENCE_DISCONNECTED)
183	    );
184	    assert_eq!(first.conn_id, second.conn_id);
185	    assert_eq!(first.remote_id_hex, peer_hex);
186	}
187

//! D4c regression — the PresenceChannel broker seam (REQ-EP-4): connection
//! liveness as a sequenced, buffered, cursor-resumable event log.
//!
//! One in-process broker (hermetic loopback endpoint, relays disabled) plays
//! the daemon; a standalone droppable [`NetHost`] plays the remote peer. The
//! invariants:
//!
//! - a peer's dial surfaces as a `connected` presence event carrying the
//!   handshake-proven remote identity;
//! - a peer's close removes the conn row (conns LEAVE the table — the D4c
//!   fix) and surfaces as a `disconnected` event;
//! - presence events produced while no brain is attached sit in the ring and
//!   replay from the resume cursor — a brain restart loses no liveness
//!   transition (the StreamLog discipline applied to the conn table).

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::msg::{NetPresenceEvent, PRESENCE_CONNECTED, PRESENCE_DISCONNECTED};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, EndpointAddr, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

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

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

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");
}

/// Read events until the next presence event arrives.
fn read_presence(brain: &mut Brain) -> NetPresenceEvent {
    loop {
        match brain.read_event().expect("read event") {
            BrokerEvent::NetPresence(ev) => return ev,
            BrokerEvent::Error { message } => panic!("broker error: {message}"),
            _ => {}
        }
    }
}

// [unit->REQ-EP-4] the full presence lifecycle across a brain restart: a
// standalone peer host dials the broker (→ live `connected` event with the
// handshake-proven identity), the brain crashes, the peer closes while no
// brain is attached (→ conn row removed + `disconnected` buffered in the
// ring), and a fresh brain resumes from its durable cursor — replaying the
// missed transition exactly once, no gap, no duplicate.
#[test]
fn presence_survives_brain_restart_with_cursor_resume() {
    let dir = tempfile::tempdir().expect("tempdir");
    let name_b = unique_name();
    let _broker_b = net_broker(&name_b, dir.path());

    // Brain life 1: subscribe to presence from the start of history.
    let mut life1 = connect_retry(&name_b);
    life1.net_presence_subscribe(0).expect("subscribe");

    // The remote peer: a standalone broker-shaped net host that dials in.
    let peer = NetHost::start(hermetic()).expect("peer host");
    let peer_hex = peer.node_id_hex();
    let addr: EndpointAddr =
        serde_json::from_value(life1.net_status().expect("status").addr).expect("addr decodes");
    peer.dial(addr).expect("peer dials the broker");

    // Live `connected`, carrying the handshake-proven peer identity.
    let ev = read_presence(&mut life1);
    assert_eq!(ev.seq, 0, "first presence event");
    assert_eq!(ev.kind, PRESENCE_CONNECTED);
    assert_eq!(
        ev.remote_id_hex, peer_hex,
        "identity proven by the handshake"
    );
    let conn_id = ev.conn_id;
    assert_eq!(
        life1.net_status().expect("status").conn_count,
        1,
        "row in the table"
    );

    // Carry the durable cursor forward, then crash the brain.
    let cursor = life1.presence_cursor().expect("cursor set");
    assert_eq!(cursor, 1);
    drop(life1); // the crash — broker, conn, and presence log survive

    // The peer closes INTO the dead window: the closed-watcher removes the
    // conn row and buffers `disconnected` with no subscriber attached.
    peer.close();
    drop(peer);

    // Brain life 2: resume from the durable cursor — the ring replays the
    // missed `disconnected` (and nothing already seen).
    let mut life2 = connect_retry(&name_b);
    life2.net_presence_subscribe(cursor).expect("resubscribe");
    let ev = read_presence(&mut life2);
    assert_eq!(ev.seq, 1, "contiguous resume — no gap, no duplicate");
    assert_eq!(ev.kind, PRESENCE_DISCONNECTED);
    assert_eq!(ev.conn_id, conn_id, "the same connection's lifecycle");
    assert_eq!(ev.remote_id_hex, peer_hex);

    // The D4c fix proper: the closed conn LEFT the table.
    assert_eq!(
        life2.net_status().expect("status").conn_count,
        0,
        "row removed on close"
    );
    assert_eq!(
        life2.presence_cursor(),
        Some(2),
        "cursor advanced past the replay"
    );
}

// [unit->REQ-EP-4] a late subscriber replays the WHOLE retained history from
// seq 0 (connected + disconnected, in order) — presence is a log, not a
// snapshot, so a brain that was never attached still reconstructs liveness.
#[test]
fn presence_history_replays_from_zero_for_a_late_subscriber() {
    let dir = tempfile::tempdir().expect("tempdir");
    let name_b = unique_name();
    let _broker_b = net_broker(&name_b, dir.path());

    // Resolve the broker's address, but do NOT subscribe yet.
    let mut probe = connect_retry(&name_b);
    let addr: EndpointAddr =
        serde_json::from_value(probe.net_status().expect("status").addr).expect("addr decodes");

    // The peer's whole lifecycle happens with no presence subscriber attached.
    let peer = NetHost::start(hermetic()).expect("peer host");
    let peer_hex = peer.node_id_hex();
    peer.dial(addr).expect("peer dials");
    // Wait until the broker registered the row before closing it.
    for _ in 0..400 {
        if probe.net_status().expect("status").conn_count == 1 {
            break;
        }
        thread::sleep(Duration::from_millis(5));
    }
    peer.close();
    drop(peer);

    // Late subscribe from 0: both buffered transitions replay, in seq order.
    probe.net_presence_subscribe(0).expect("subscribe");
    let first = read_presence(&mut probe);
    let second = read_presence(&mut probe);
    assert_eq!((first.seq, first.kind.as_str()), (0, PRESENCE_CONNECTED));
    assert_eq!(
        (second.seq, second.kind.as_str()),
        (1, PRESENCE_DISCONNECTED)
    );
    assert_eq!(first.conn_id, second.conn_id);
    assert_eq!(first.remote_id_hex, peer_hex);
}


Reading picker data API + EpStatus + existing tests to place the remote-picker int faithfully.