33://! | `kind=query` | update pull ([`crate::propagate::serve_update`]) |
37://! | untagged + `subnet`+`instance` | registry feed (D9-1-2) |
53://! (REQ-HAZARD-WAN-ORIGIN-AUTH); every family's own gate then runs unchanged.
73:use spt_store::trust::TrustStore;
78:use crate::propagate::serve_update;
79:use crate::registryhost::{RegistryGatePolicy, RegistryHost};
112:    /// A subnet-scoped liveness probe (`ServeProbeRecord::Request` — "serving
113:    /// subnet X?", the `--nodes` view's detach-aware refinement, REQ-SUBNET-5).
141:            } else if line.get("subnet").is_some() && line.get("instance").is_some() {
143:            } else if line.get("subnet").is_some()
149:                // first line is a NodeLabelUpdate. Same serve arm, same gate.
171:/// the access-gate stance, config edits take effect without a restart).
198:pub fn run_dispatch_loop(
326:            let trust = TrustStore::load();
327:            match serve_update(&mut brain, stream_id, origin, 0, &cache, &trust) {
336:            // The gate subject is the session's OWNING endpoint, resolved
362:            // The wire names whose whitelist gates (the rule set); the origin
387:            // The wire names whose whitelist gates (the xfer posture); the
389:            // through the LOCAL transition host — echo gate, wake effects,
399:            // The wire names the owner whose whitelist gates AND whose shell
410:            // No gate (serveprobe module docs): the answer is public
411:            // serve-state for a named subnet, read fresh from this node's
414:            match crate::serveprobe::serve_subnet_probe(&mut brain, stream_id) {
424:/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
425:/// origin trusted in that subnet), until the peer finishes.
431:/// dormant). The rest edge's transition echo is gate-armed
434:/// non-blocking posture: N flips in one feed are N gate files, not N LLM
443:    let policy = RegistryGatePolicy::load();
461:                // (REQ-SUBNET-3) — each goes through its own gated apply path.
505:/// fail-closed gate, until the peer finishes. The policy re-loads per feed
593:/// access-gate + op-id-dedup path, until the peer finishes.
680:        let reg = json!({"subnet": "home", "endpoint_id": "ling",
686:    // never routed to a gate-bearing serve loop on a guess.
695:            json!({"subnet": "home"}), // registry-ish but no instance


Design tension found: `run_dispatch_loop` loads its gate from canonical `SPT_HOME` (`dispatch.rs:443 RegistryGatePolicy::load()`) — can't give 3 in-process nodes distinct pairwise gates through it. The two buggy seams are precise though:
- **push target** — `peerloop.rs:356` `trust.peers_in(subnet)` (pairwise) → roster at D6
- **apply gate** — `registryhost.rs:228/291` `is_trusted` → membership at D5

Writing the JIT D0 plan, then building.


Write

# Mesh-D0 — staggered repro harness (red)

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 0. First artifact, **no production change**. Lands `#[ignore]`, asserting the *desired* mesh convergence — the acceptance that flips green at D6. Tags REQ-MESH-3 `int` evidence only when it passes (D6), so **no REQ activation here**.

## The bug, in two production seams

The grill repro — *A creates → B joins A → A offline → C joins B → B offline → A online → A and C invisible* — is two own-rows-only/pairwise facts:

1. **Push target is pairwise.** `peerloop.rs:356` — `for peer in trust.peers_in(&sub.name)`. A node advertises its own rows to *directly-paired* peers only. No relay, no roster. (Doc line 11: "no transitive gossip in v1: trust is pairwise per subnet.") → widens to **all roster members** at **D6**.
2. **Apply gate is pairwise.** `registryhost.rs:228,291` — `if !policy.trust.is_trusted(&u.subnet, origin_node) { reject }`. Even if a row from a non-paired member arrived, the receiver drops it. → swaps to the **seed-proof membership** flag at **D5**.

C joins B, so C's trust = {B}; A's trust = {B}. **A and C never pin each other.** Under (1) C's pump never targets A; under (2) A would reject C's row anyway. Invisible + unreachable. The mesh: C learns A from B's roster at the join ceremony (D3), so when A returns C dials A *directly* and A's seed-proof gate admits it (D5) — no B in the loop. D6 widens C's push target to the roster so the row actually flows.

## What the harness asserts (the desired end-state)

Two tests, both `#[ignore = "mesh lands at D6 — Mesh-D0 red repro"]`:

1. **`staggered_offliner_still_meshes`** (the acceptance). Topology + stagger exactly as the repro: B is **offline at the critical step**; at the end C and A are both online, B is not. Assert **A's registry holds C's perch row AND C's registry holds A's perch row** (visibility) and **each can resolve a dialable address for the other** (reach). Red today: C's pump targets only B; A's gate rejects C.
2. **`all_online_star_a_reaches_c_b_never_relays`**. A, B, C all online, pairwise A↔B and B↔C only. Assert A↔C converge **and** B never carried a third-party row (every row B forwarded was B-authored — i.e. A's registry never holds a C-row whose *delivering origin* was B). Locks KH 4.10/7.5: the fix is roster relay, **not** row relay.

## Fidelity decision (why not `run_dispatch_loop`)

`run_dispatch_loop` sources its gate from canonical `SPT_HOME` (`dispatch.rs:443 RegistryGatePolicy::load()`) — three in-process nodes can't each hold a distinct pairwise gate through it. So the harness:

- **Push side = the real loop.** Each node runs production `run_peer_pump` with explicit per-node `PumpPaths` (its own `trust`/`subnet`/`registry`). This captures the **D6** target-widening verbatim — when `peerloop.rs:356` reads the roster instead of `peers_in`, these pumps start targeting the full mesh with zero test change.
- **Receive side = a thin per-node arm.** The test reads each node's broker `NetStreamData` feed events and calls the **production** `node.registry.apply_feed(origin, updates, &per_node_policy)`. This captures the **D5** gate swap verbatim — the swap is *inside* `apply_feed`/`RegistryHost`, so the same call exercises whatever gate is current. (Same direct-`apply_feed` seam `peerloop.rs:265` already uses.)
- **Addresses = omniscient resolver.** All three loopback addresses are known to every node's resolver. Address *discovery* is the roster's job (D3/D4, separately tested); D0 isolates the **relay + gate** bug, which is the center of "the subnet was the pairing graph, not a mesh." A node still only *pushes* where its push-target says — so an omniscient resolver does not mask the bug (C still won't target A until the target widens at D6).

This is faithful to **both** flip seams (D5 gate, D6 target) while runnable in-process today. If a later phase needs the receive arm to ride the real dispatch loop, refine then — the `#[ignore]` lifts at D6 regardless.

## Build steps

1. New `crates/spt-daemon/tests/mesh.rs`. Lift the shared scaffold from `peerloop.rs` / `propagate.rs`: `unique_name`, `hermetic`, `net_broker`, `connect_retry`, `converge`. No `mod common` exists — copy-verbatim per the sibling tests' convention.
2. A `MeshNode` helper: `{ name, hex, addr, registry: Arc<RegistryHost>, paths: PumpPaths, root: PathBuf }`, plus `advertise(perch_label)` (write an `info.json` perch under its owlery so `advertise_local` has a row) and `policy(trusted: &[&str])` (a per-node `RegistryGatePolicy`).
3. A per-node **receive pump**: a thread that drains the node's broker feed streams and applies under the node's policy (the ~40-line dispatch-arm reimpl). Gate it on a stop flag.
4. A **push pump**: production `run_peer_pump` per online node, explicit `PumpPaths`, omniscient resolver, fast cadences (100ms), `registry_evict_after = 300s` (inert — same as peerloop).
5. Online/offline = start/stop a node's push+receive threads via its own stop flag. Stagger per the repro; `converge()` at each settle point that *current code can* reach (B-holds-A, B-holds-C), then the final mesh assertion that **fails** today.
6. Mark both tests `#[ignore = "..."]`. Reach assertion = each node resolves a non-empty addr for the other through the same resolver the pump uses.
7. `cargo test -p spt-daemon --test mesh -- --ignored` → both run **red** (compile-green, assert-red). `cargo test -p spt-daemon --test mesh` (no `--ignored`) → green-skip (CI stays green).

## Done when

- `mesh.rs` compiles; `--ignored` run shows both tests failing on the *mesh* assertion (visibility/reach), not on setup.
- Default `cargo test` skips them (CI green).
- `traceable-reqs check` clean (no REQ-MESH stage activated — D0 mints no evidence).
- Commit: `test(mesh): red staggered + star repro harness — Mesh-D0`.


File created successfully at: C:\Users\decid\Documents\projects\spt-core\MESH-D0-PLAN.md (file state is current in your context — no need to Read it back)

# Mesh-D0 — staggered repro harness (red)

> JIT plan for `SUBNET-MESH-PLAN.md` §Build phases · phase 0. First artifact, **no production change**. Lands `#[ignore]`, asserting the *desired* mesh convergence — the acceptance that flips green at D6. Tags REQ-MESH-3 `int` evidence only when it passes (D6), so **no REQ activation here**.

## The bug, in two production seams

The grill repro — *A creates → B joins A → A offline → C joins B → B offline → A online → A and C invisible* — is two own-rows-only/pairwise facts:

1. **Push target is pairwise.** `peerloop.rs:356` — `for peer in trust.peers_in(&sub.name)`. A node advertises its own rows to *directly-paired* peers only. No relay, no roster. (Doc line 11: "no transitive gossip in v1: trust is pairwise per subnet.") → widens to **all roster members** at **D6**.
2. **Apply gate is pairwise.** `registryhost.rs:228,291` — `if !policy.trust.is_trusted(&u.subnet, origin_node) { reject }`. Even if a row from a non-paired member arrived, the receiver drops it. → swaps to the **seed-proof membership** flag at **D5**.

C joins B, so C's trust = {B}; A's trust = {B}. **A and C never pin each other.** Under (1) C's pump never targets A; under (2) A would reject C's row anyway. Invisible + unreachable. The mesh: C learns A from B's roster at the join ceremony (D3), so when A returns C dials A *directly* and A's seed-proof gate admits it (D5) — no B in the loop. D6 widens C's push target to the roster so the row actually flows.

## What the harness asserts (the desired end-state)

Two tests, both `#[ignore = "mesh lands at D6 — Mesh-D0 red repro"]`:

1. **`staggered_offliner_still_meshes`** (the acceptance). Topology + stagger exactly as the repro: B is **offline at the critical step**; at the end C and A are both online, B is not. Assert **A's registry holds C's perch row AND C's registry holds A's perch row** (visibility) and **each can resolve a dialable address for the other** (reach). Red today: C's pump targets only B; A's gate rejects C.
2. **`all_online_star_a_reaches_c_b_never_relays`**. A, B, C all online, pairwise A↔B and B↔C only. Assert A↔C converge **and** B never carried a third-party row (every row B forwarded was B-authored — i.e. A's registry never holds a C-row whose *delivering origin* was B). Locks KH 4.10/7.5: the fix is roster relay, **not** row relay.

## Fidelity decision (why not `run_dispatch_loop`)

`run_dispatch_loop` sources its gate from canonical `SPT_HOME` (`dispatch.rs:443 RegistryGatePolicy::load()`) — three in-process nodes can't each hold a distinct pairwise gate through it. So the harness:

- **Push side = the real loop.** Each node runs production `run_peer_pump` with explicit per-node `PumpPaths` (its own `trust`/`subnet`/`registry`). This captures the **D6** target-widening verbatim — when `peerloop.rs:356` reads the roster instead of `peers_in`, these pumps start targeting the full mesh with zero test change.
- **Receive side = a thin per-node arm.** The test reads each node's broker `NetStreamData` feed events and calls the **production** `node.registry.apply_feed(origin, updates, &per_node_policy)`. This captures the **D5** gate swap verbatim — the swap is *inside* `apply_feed`/`RegistryHost`, so the same call exercises whatever gate is current. (Same direct-`apply_feed` seam `peerloop.rs:265` already uses.)
- **Addresses = omniscient resolver.** All three loopback addresses are known to every node's resolver. Address *discovery* is the roster's job (D3/D4, separately tested); D0 isolates the **relay + gate** bug, which is the center of "the subnet was the pairing graph, not a mesh." A node still only *pushes* where its push-target says — so an omniscient resolver does not mask the bug (C still won't target A until the target widens at D6).

This is faithful to **both** flip seams (D5 gate, D6 target) while runnable in-process today. If a later phase needs the receive arm to ride the real dispatch loop, refine then — the `#[ignore]` lifts at D6 regardless.

## Build steps

1. New `crates/spt-daemon/tests/mesh.rs`. Lift the shared scaffold from `peerloop.rs` / `propagate.rs`: `unique_name`, `hermetic`, `net_broker`, `connect_retry`, `converge`. No `mod common` exists — copy-verbatim per the sibling tests' convention.
2. A `MeshNode` helper: `{ name, hex, addr, registry: Arc<RegistryHost>, paths: PumpPaths, root: PathBuf }`, plus `advertise(perch_label)` (write an `info.json` perch under its owlery so `advertise_local` has a row) and `policy(trusted: &[&str])` (a per-node `RegistryGatePolicy`).
3. A per-node **receive pump**: a thread that drains the node's broker feed streams and applies under the node's policy (the ~40-line dispatch-arm reimpl). Gate it on a stop flag.
4. A **push pump**: production `run_peer_pump` per online node, explicit `PumpPaths`, omniscient resolver, fast cadences (100ms), `registry_evict_after = 300s` (inert — same as peerloop).
5. Online/offline = start/stop a node's push+receive threads via its own stop flag. Stagger per the repro; `converge()` at each settle point that *current code can* reach (B-holds-A, B-holds-C), then the final mesh assertion that **fails** today.
6. Mark both tests `#[ignore = "..."]`. Reach assertion = each node resolves a non-empty addr for the other through the same resolver the pump uses.
7. `cargo test -p spt-daemon --test mesh -- --ignored` → both run **red** (compile-green, assert-red). `cargo test -p spt-daemon --test mesh` (no `--ignored`) → green-skip (CI stays green).

## Done when

- `mesh.rs` compiles; `--ignored` run shows both tests failing on the *mesh* assertion (visibility/reach), not on setup.
- Default `cargo test` skips them (CI green).
- `traceable-reqs check` clean (no REQ-MESH stage activated — D0 mints no evidence).
- Commit: `test(mesh): red staggered + star repro harness — Mesh-D0`.


Plan set. Now the wire encode/decode + apply API for the receive arm. Reading registryhost `advertise_local` + `apply_feed` + the replicate codec.


200	    /// daemon advertises transitions epoch-bumped), which rests a local
201	    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes
202	    /// `ling@desktop` dormant).
203	    ///
204	    /// Flip = `Updated` merge where the stored row's status was NOT `Active`
205	    /// and the incoming one is. Deliberately **never `Inserted`**: this
206	    /// registry is in-memory, so a daemon restart re-inserts every peer row
207	    /// at its *current* status — counting inserts would knock local
208	    /// instances dormant on every restart; and never the periodic
209	    /// re-advertisement (`Updated` with prev already `Active` is not a
210	    /// flip). The lease has already dropped stale/equal epochs before any
211	    /// of this runs.
212	    // [impl->REQ-INST-3]
213	    pub fn apply_feed_flips(
214	        &self,
215	        origin_node: &str,
216	        updates: &[RegistryUpdate],
217	        policy: &RegistryGatePolicy,
218	    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {
219	        let mut regs = self.regs.lock().unwrap();
220	        let mut flips: Vec<String> = Vec::new();
221	        let mut admitted_any = false;
222	        let verdicts = updates
223	            .iter()
224	            .map(|u| {
225	                if !policy.subnets.iter().any(|s| s == &u.subnet) {
226	                    return RegistryApplyVerdict::NotMember;
227	                }
228	                if !policy.trust.is_trusted(&u.subnet, origin_node) {
229	                    return RegistryApplyVerdict::UntrustedOrigin;
230	                }
231	                admitted_any = true;
232	                let reg = regs.entry(u.subnet.clone()).or_default();
233	                let prev = reg
234	                    .instances(&u.endpoint_id)
235	                    .iter()
236	                    .find(|i| i.node == u.instance.node)
237	                    .map(|i| i.status);
238	                // The epoch lease (4.8) decides; an own-row echo from a peer
239	                // is dropped Stale by construction (our epoch is newer or
240	                // equal — never authored elsewhere).
241	                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());
242	                if outcome == MergeOutcome::Updated
243	                    && u.instance.status == Status::Active
244	                    && prev != Some(Status::Active)
245	                    && u.instance.node != self.node_hex
246	                    && !flips.contains(&u.endpoint_id)
247	                {
248	                    flips.push(u.endpoint_id.clone());
249	                }
250	                RegistryApplyVerdict::Merged(outcome)
251	            })
252	            .collect();
253	        drop(regs);
254	        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin
255	        // node is up right now — even a Stale merge verdict is a liveness
256	        // signal (the lease judged the row, not the sender). Refused feeds
257	        // stamp nothing (an untrusted sender earns no liveness credit).
258	        // [impl->REQ-SUBNET-1]
259	        if admitted_any {
260	            self.heard
261	                .lock()
262	                .unwrap()
263	                .insert(origin_node.to_string(), wall_ms());
264	        }
265	        self.write_snapshots();
266	        (verdicts, flips)
267	    }
268
269	    /// Apply inbound node-level label updates (REQ-SUBNET-3) under the SAME
270	    /// fail-closed gate as [`apply_feed_flips`]: a non-member subnet or an
271	    /// untrusted origin is refused; an admitted update merges via
272	    /// [`SubnetRegistry::merge_node_label`] (epoch lease) and stamps the
273	    /// origin's gossip-recency (an admitted label feed proves the peer is up,
274	    /// exactly like an instance feed). The endpoint-less carrier: this is how a
275	    /// peer with zero endpoints gets its hostname into our `--nodes` view.
276	    // [impl->REQ-SUBNET-3]
277	    pub fn apply_node_labels(
278	        &self,
279	        origin_node: &str,
280	        updates: &[NodeLabelUpdate],
281	        policy: &RegistryGatePolicy,
282	    ) -> Vec<RegistryApplyVerdict> {
283	        let mut regs = self.regs.lock().unwrap();
284	        let mut admitted_any = false;
285	        let verdicts = updates
286	            .iter()
287	            .map(|u| {
288	                if !policy.subnets.iter().any(|s| s == &u.subnet) {
289	                    return RegistryApplyVerdict::NotMember;
290	                }
291	                if !policy.trust.is_trusted(&u.subnet, origin_node) {
292	                    return RegistryApplyVerdict::UntrustedOrigin;
293	                }
294	                admitted_any = true;
295	                let reg = regs.entry(u.subnet.clone()).or_default();
296	                RegistryApplyVerdict::Merged(reg.merge_node_label(
297	                    &u.node,
298	                    u.label.clone(),
299	                    u.epoch,
300	                ))
301	            })
302	            .collect();
303	        drop(regs);
304	        if admitted_any {
305	            self.heard
306	                .lock()
307	                .unwrap()
308	                .insert(origin_node.to_string(), wall_ms());
309	        }
310	        self.write_snapshots();
311	        verdicts
312	    }
313
314	    /// Scan the owlery and (re-)advertise every self perch into every member
315	    /// subnet it is visible in, each row stamped with a fresh epoch. Returns
316	    /// the updates to push to peers (the node's own rows only — peers' rows
317	    /// reconverge from their own re-advertisement, no transitive gossip in
318	    /// v1: trust is pairwise per subnet, a relayed third-party row would
319	    /// carry an origin the receiver can't gate honestly).
320	    // [impl->REQ-INST-7]
321	    pub fn advertise_local(&self, owlery: &Path) -> Vec<RegistryFeedRecord> {
322	        let subnets = SubnetStore::load();
323	        let mut vis = VisibilityStore::load();
324	        // First-join home adoption (D9-5, REQ-INST-15): within one
325	        // advertisement cadence of the node's first join (a pair lands the
326	        // subnet record; no pairing-protocol threading needed), unhomed Self
327	        // perches adopt the sole subnet + seed their sync scope. Strictly
328	        // adopt-if-unset; a multi-subnet store adopts nothing.
329	        // [impl->REQ-INST-15]
330	        if !spt_store::home::adopt_for_unset(owlery, &subnets, &mut vis).is_empty() {
331	            let _ = vis.save();
332	        }
333	        let vis = vis;
334	        // The node-level resource-blurb seed (D9-4): an endpoint that has not
335	        // authored its own blurb advertises this default (config seeds, the
336	        // agent refines — CONTEXT §resource advertisement).
337	        let node_blurb = crate::config::DaemonConfig::load().resources_blurb;
338	        // The node's subnet-broadcast shell adapters (M5-D8c, CONTEXT
339	        // §Discovery scope): registered kind="shell" adapters that chose
340	        // broadcast=subnet ride every row this node authors — the other-node
341	        // discovery arm. Computed once per scan (the registration set is
342	        // node-level).
343	        // [impl->REQ-EP-5]
344	        let shell_adapters: Vec<String> = spt_runtime::registry::registered(&perch::adapters_dir())
345	            .iter()
346	            .filter(|(r, m)| {
347	                r.kind == spt_runtime::manifest::AdapterKind::Shell
348	                    && m.shell.as_ref().and_then(|s| s.broadcast)
349	                        == Some(spt_runtime::manifest::Broadcast::Subnet)
350	            })
351	            .map(|(r, _)| r.name.clone())
352	            .collect();
353	        let mut out = Vec::new();
354	        let mut regs = self.regs.lock().unwrap();
355	        let mut epoch = self.epoch.lock().unwrap();
356
357	        for id in perch::list_self_perch_ids(owlery) {
358	            let perch_path = perch::resolve_perch_path_in(owlery, &id, ParentHint::Infer);
359	            // Advertisement follows the resting-state machine (D9-2,
360	            // REQ-INST-3): a live perch with a resting record advertises its
361	            // machine state (active / dormant / suspended); a recordless
362	            // (pre-D9-2 / interim) or dead perch keeps the legacy
363	            // liveness-derived mapping. Every advertisement stamps a fresh
364	            // epoch below, so a transition reaches peers as an epoch-bumped
365	            // update — the lease orders it (KH 4.8).
366	            // [impl->REQ-INST-3]
367	            let status = if is_perch_alive(&perch_path) {
368	                match crate::resting::read_rest(&perch_path).map(|r| r.state) {
369	                    Some(crate::resting::RestState::Active) | None => Status::Active,
370	                    Some(crate::resting::RestState::Dormant) => Status::Dormant,
371	                    Some(crate::resting::RestState::Suspended) => Status::Suspended,
372	                }
373	            } else {
374	                Status::Dormant
375	            };
376	            // Endpoint-authored blurb wins; the node seed fills the gap
377	            // (both-authored, REQ-INST-14). Rides the same epoch lease as
378	            // every instance update — a blurb edit reaches peers as an
379	            // ordered, epoch-bumped row.
380	            // [impl->REQ-INST-14]
381	            let info = spt_store::info::read_info(&perch_path);
382	            let resources = info
383	                .as_ref()
384	                .and_then(|i| i.resources.clone())
385	                .or_else(|| node_blurb.clone());
386	            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat
387	            // stamp (`lifecycle::touch_active` — Active seats only; a resting
388	            // seat never stamps) rides the row like the blurb does — same
389	            // epoch lease, same visibility gate, no new channel. Subnet-wide
390	            // most-recently-active is a projection of these rows (D6b).
391	            // [impl->REQ-PRES-1]
392	            let last_active_ms = info.and_then(|i| i.last_active_ms);
393	            for sub in &subnets.subnets {
394	                let Ok(ep) = epoch.next_epoch() else { continue };
395	                let instance = Instance {
396	                    node: self.node_hex.clone(),
397	                    status,
398	                    epoch: ep,
399	                    resources: resources.clone(),

    /// daemon advertises transitions epoch-bumped), which rests a local
    /// instance of the same id warm (CONTEXT: driving `ling@laptop` makes
    /// `ling@desktop` dormant).
    ///
    /// Flip = `Updated` merge where the stored row's status was NOT `Active`
    /// and the incoming one is. Deliberately **never `Inserted`**: this
    /// registry is in-memory, so a daemon restart re-inserts every peer row
    /// at its *current* status — counting inserts would knock local
    /// instances dormant on every restart; and never the periodic
    /// re-advertisement (`Updated` with prev already `Active` is not a
    /// flip). The lease has already dropped stale/equal epochs before any
    /// of this runs.
    // [impl->REQ-INST-3]
    pub fn apply_feed_flips(
        &self,
        origin_node: &str,
        updates: &[RegistryUpdate],
        policy: &RegistryGatePolicy,
    ) -> (Vec<RegistryApplyVerdict>, Vec<String>) {
        let mut regs = self.regs.lock().unwrap();
        let mut flips: Vec<String> = Vec::new();
        let mut admitted_any = false;
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                admitted_any = true;
                let reg = regs.entry(u.subnet.clone()).or_default();
                let prev = reg
                    .instances(&u.endpoint_id)
                    .iter()
                    .find(|i| i.node == u.instance.node)
                    .map(|i| i.status);
                // The epoch lease (4.8) decides; an own-row echo from a peer
                // is dropped Stale by construction (our epoch is newer or
                // equal — never authored elsewhere).
                let outcome = reg.merge_instance(&u.endpoint_id, u.instance.clone());
                if outcome == MergeOutcome::Updated
                    && u.instance.status == Status::Active
                    && prev != Some(Status::Active)
                    && u.instance.node != self.node_hex
                    && !flips.contains(&u.endpoint_id)
                {
                    flips.push(u.endpoint_id.clone());
                }
                RegistryApplyVerdict::Merged(outcome)
            })
            .collect();
        drop(regs);
        // Gossip-recency stamp (M7 D2): an ADMITTED feed proves the origin
        // node is up right now — even a Stale merge verdict is a liveness
        // signal (the lease judged the row, not the sender). Refused feeds
        // stamp nothing (an untrusted sender earns no liveness credit).
        // [impl->REQ-SUBNET-1]
        if admitted_any {
            self.heard
                .lock()
                .unwrap()
                .insert(origin_node.to_string(), wall_ms());
        }
        self.write_snapshots();
        (verdicts, flips)
    }

    /// Apply inbound node-level label updates (REQ-SUBNET-3) under the SAME
    /// fail-closed gate as [`apply_feed_flips`]: a non-member subnet or an
    /// untrusted origin is refused; an admitted update merges via
    /// [`SubnetRegistry::merge_node_label`] (epoch lease) and stamps the
    /// origin's gossip-recency (an admitted label feed proves the peer is up,
    /// exactly like an instance feed). The endpoint-less carrier: this is how a
    /// peer with zero endpoints gets its hostname into our `--nodes` view.
    // [impl->REQ-SUBNET-3]
    pub fn apply_node_labels(
        &self,
        origin_node: &str,
        updates: &[NodeLabelUpdate],
        policy: &RegistryGatePolicy,
    ) -> Vec<RegistryApplyVerdict> {
        let mut regs = self.regs.lock().unwrap();
        let mut admitted_any = false;
        let verdicts = updates
            .iter()
            .map(|u| {
                if !policy.subnets.iter().any(|s| s == &u.subnet) {
                    return RegistryApplyVerdict::NotMember;
                }
                if !policy.trust.is_trusted(&u.subnet, origin_node) {
                    return RegistryApplyVerdict::UntrustedOrigin;
                }
                admitted_any = true;
                let reg = regs.entry(u.subnet.clone()).or_default();
                RegistryApplyVerdict::Merged(reg.merge_node_label(
                    &u.node,
                    u.label.clone(),
                    u.epoch,
                ))
            })
            .collect();
        drop(regs);
        if admitted_any {
            self.heard
                .lock()
                .unwrap()
                .insert(origin_node.to_string(), wall_ms());
        }
        self.write_snapshots();
        verdicts
    }

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

        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]
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),


Catch: `advertise_local` reads `SubnetStore::load()`/`VisibilityStore::load()` from canonical SPT_HOME (line 322-323) while the pump's target loop reads explicit `paths.subnets`. Same split peerloop handles — set both. Now the feed encode→wire→decode path.


340	            registry.consume_repair_evictions();
341	            // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-
342	            // HAZARDS 4.10): a dead node identity's rows decay instead of
343	            // poisoning bare-id resolution forever; a live peer re-stamps its
344	            // heard entry with every admitted feed and is never touched.
345	            // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]
346	            registry.evict_silent_peers(cadence.registry_evict_after);
347	            registry.advertise_local(&paths.owlery)
348	        } else {
349	            Vec::new()
350	        };
351
352	        for sub in &subnets.subnets {
353	            if attachment.is_detached(&sub.name) {
354	                continue; // detached: neither advertise into nor connect to (REQ-SUBNET-5)
355	            }
356	            for peer in trust.peers_in(&sub.name) {
357	                if peer.pubkey_hex == registry.node_hex() {
358	                    continue; // never pump at ourselves
359	                }
360	                let Some(conn_id) = ensure_conn(
361	                    &mut brain,
362	                    &mut conns,
363	                    &resolver,
364	                    &peer.pubkey_hex,
365	                    &paths.peer_addrs,
366	                ) else {
367	                    continue; // unresolvable/undialable now — next tick retries
368	                };
369
370	                let mut peer_io = || -> io::Result<()> {
371	                    if due_reg {
372	                        let lines: Vec<u8> = adverts
373	                            .iter()
374	                            .filter(|u| u.subnet() == sub.name)
375	                            .flat_map(|u| u.encode_line())
376	                            .collect();
377	                        if !lines.is_empty() {
378	                            push_feed(&mut brain, conn_id, &lines)?;
379	                        }
380	                    }
381	                    if due_notif {
382	                        let store = NotifStore::open_at(&paths.notif_db)
383	                            .map_err(|e| io::Error::other(e.to_string()))?;
384	                        let feed = emit_notif_feed(&store, &sub.name).map_err(io::Error::other)?;
385	                        if !feed.is_empty() {
386	                            push_feed(&mut brain, conn_id, &feed)?;
387	                        }
388	                    }
389	                    if due_sync {
390	                        let cs = ContextStore::open_or_init_in(&paths.ctx_root)?;
391	                        let mut want = select_refs(&policy, &cs, &peer.pubkey_hex)?;
392	                        // Registry-derived bootstrap refs: minds named in the
393	                        // shared subnet that this node never held locally.
394	                        if let Some(snap) = registry.snapshot(&sub.name) {
395	                            for id in snap.endpoint_ids() {
396	                                let r = format!("a-{id}");
397	                                if !want.contains(&r) && policy.allows(&r, &peer.pubkey_hex) {
398	                                    want.push(r);
399	                                }
400	                            }
401	                        }
402	                        if !want.is_empty() {
403	                            let open_op = ops.next_epoch()?;
404	                            let report = request_sync(
405	                                &mut brain,
406	                                conn_id,
407	                                &want,
408	                                open_op,
409	                                &cs,
410	                                &paths.scratch,
411	                            )?;
412	                            (hooks.on_pull)(&peer.pubkey_hex, &report);
413	                        }
414	                    }
415	                    if due_upd {
416	                        // The peer-propagation pull (REQ-UPD-1) under the
417	                        // PRODUCTION gate: keys + channel pin from
418	                        // release-keys.json (fail-closed when unprovisioned),
419	                        // version floor = the staged release (rollback gate),
420	                        // wall clock for expiry only.
421	                        let cache = ReleaseCache::open(&paths.releases);
422	                        let base_policy = VerifyPolicy::load_from(&paths.release_keys, 0, now_ms());
423	                        let current = if cache.staged_channel().as_deref()
424	                            == Some(base_policy.pinned_channel.as_str())
425	                        {
426	                            cache.staged_version().unwrap_or(0)
427	                        } else {
428	                            0
429	                        };

            registry.consume_repair_evictions();
            // Sweep silent-node ghost rows BEFORE re-advertising (KNOWN-
            // HAZARDS 4.10): a dead node identity's rows decay instead of
            // poisoning bare-id resolution forever; a live peer re-stamps its
            // heard entry with every admitted feed and is never touched.
            // [impl->REQ-HAZARD-REGISTRY-GHOST-ROWS]
            registry.evict_silent_peers(cadence.registry_evict_after);
            registry.advertise_local(&paths.owlery)
        } else {
            Vec::new()
        };

        for sub in &subnets.subnets {
            if attachment.is_detached(&sub.name) {
                continue; // detached: neither advertise into nor connect to (REQ-SUBNET-5)
            }
            for peer in trust.peers_in(&sub.name) {
                if peer.pubkey_hex == registry.node_hex() {
                    continue; // never pump at ourselves
                }
                let Some(conn_id) = ensure_conn(
                    &mut brain,
                    &mut conns,
                    &resolver,
                    &peer.pubkey_hex,
                    &paths.peer_addrs,
                ) else {
                    continue; // unresolvable/undialable now — next tick retries
                };

                let mut peer_io = || -> io::Result<()> {
                    if due_reg {
                        let lines: Vec<u8> = adverts
                            .iter()
                            .filter(|u| u.subnet() == sub.name)
                            .flat_map(|u| u.encode_line())
                            .collect();
                        if !lines.is_empty() {
                            push_feed(&mut brain, conn_id, &lines)?;
                        }
                    }
                    if due_notif {
                        let store = NotifStore::open_at(&paths.notif_db)
                            .map_err(|e| io::Error::other(e.to_string()))?;
                        let feed = emit_notif_feed(&store, &sub.name).map_err(io::Error::other)?;
                        if !feed.is_empty() {
                            push_feed(&mut brain, conn_id, &feed)?;
                        }
                    }
                    if due_sync {
                        let cs = ContextStore::open_or_init_in(&paths.ctx_root)?;
                        let mut want = select_refs(&policy, &cs, &peer.pubkey_hex)?;
                        // Registry-derived bootstrap refs: minds named in the
                        // shared subnet that this node never held locally.
                        if let Some(snap) = registry.snapshot(&sub.name) {
                            for id in snap.endpoint_ids() {
                                let r = format!("a-{id}");
                                if !want.contains(&r) && policy.allows(&r, &peer.pubkey_hex) {
                                    want.push(r);
                                }
                            }
                        }
                        if !want.is_empty() {
                            let open_op = ops.next_epoch()?;
                            let report = request_sync(
                                &mut brain,
                                conn_id,
                                &want,
                                open_op,
                                &cs,
                                &paths.scratch,
                            )?;
                            (hooks.on_pull)(&peer.pubkey_hex, &report);
                        }
                    }
                    if due_upd {
                        // The peer-propagation pull (REQ-UPD-1) under the
                        // PRODUCTION gate: keys + channel pin from
                        // release-keys.json (fail-closed when unprovisioned),
                        // version floor = the staged release (rollback gate),
                        // wall clock for expiry only.
                        let cache = ReleaseCache::open(&paths.releases);
                        let base_policy = VerifyPolicy::load_from(&paths.release_keys, 0, now_ms());
                        let current = if cache.staged_channel().as_deref()
                            == Some(base_policy.pinned_channel.as_str())
                        {
                            cache.staged_version().unwrap_or(0)
                        } else {
                            0
                        };


424	/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
425	/// origin trusted in that subnet), until the peer finishes.
426	///
427	/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer
428	/// instance of an id observed transitioning to `Active` — its node's daemon
429	/// just gave it a driver, so a *locally hosted* instance of the same id
430	/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`
431	/// dormant). The rest edge's transition echo is gate-armed
432	/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse
433	/// loop runs the bounded summarizer call, never this worker (the KH 7.4
434	/// non-blocking posture: N flips in one feed are N gate files, not N LLM
435	/// calls serialized here).
436	// [impl->REQ-INST-7]
437	fn serve_registry_feed(
438	    brain: &mut Brain,
439	    stream_id: u64,
440	    origin: &str,
441	    registry: &RegistryHost,
442	) -> DispatchOutcome {
443	    let policy = RegistryGatePolicy::load();
444	    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
445	        return DispatchOutcome::Failed(e.to_string());
446	    }
447	    let mut decoder = RegistryDecoder::new();
448	    let mut merged = 0usize;
449	    loop {
450	        match brain.read_event() {
451	            Ok(BrokerEvent::NetStreamData {
452	                stream_id: sid,
453	                bytes,
454	                ..
455	            }) if sid == stream_id => {
456	                let records = decoder.push(&bytes);
457	                if records.is_empty() {
458	                    continue;
459	                }
460	                // Split the feed into instance rows and node-level labels
461	                // (REQ-SUBNET-3) — each goes through its own gated apply path.
462	                let mut updates: Vec<RegistryUpdate> = Vec::new();
463	                let mut labels: Vec<NodeLabelUpdate> = Vec::new();
464	                for rec in records {
465	                    match rec {
466	                        RegistryFeedRecord::Instance(u) => updates.push(u),
467	                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),
468	                    }
469	                }
470	                if !labels.is_empty() {
471	                    merged += registry.apply_node_labels(origin, &labels, &policy).len();
472	                }
473	                if updates.is_empty() {
474	                    continue;
475	                }
476	                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);
477	                merged += verdicts.len();
478	                // Attention shift (D9-2): only ids this node actually hosts
479	                // (a perch with a record); everything else is a no-op here.
480	                // [impl->REQ-INST-3]
481	                for id in flips {
482	                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);
483	                    if local.join("info.json").exists() {
484	                        if let Err(e) = crate::resting::daemon_rest_event(
485	                            &id,
486	                            crate::resting::RestEvent::AttentionShift,
487	                            None,
488	                        ) {
489	                            eprintln!("REST_SHIFT_FAIL:{id}:{e}");
490	                        }
491	                    }
492	                }
493	            }
494	            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {
495	                return DispatchOutcome::Served(format!("registry feed: {merged} record(s)"));
496	            }
497	            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),
498	            Ok(_) => {}
499	            Err(e) => return DispatchOutcome::Failed(e.to_string()),
500	        }
501	    }
502	}
503
504	/// Notif feed arm: decode rows as they arrive, apply each batch under the
505	/// fail-closed gate, until the peer finishes. The policy re-loads per feed
506	/// (not per record) — the same freshness the other arms get per request.
507	///
508	/// A **freshly inserted** undismissed row additionally gets a first-fire here
509	/// (REQ-PRES-1, the convergent cross-node redirect's receiving half): the
510	/// producing node skipped its local surface because the MRA winner lives
511	/// here — this node's own presence resolution surfaces it. A row this node's
512	/// MRA also calls remote stays unmarked and rides on (someone else's
513	/// feed-apply surfaces it); replays merge `Unchanged` and never re-fire.
514	// [impl->REQ-NOTIF-1]
515	// [impl->REQ-PRES-1]
516	fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {
517	    let store = match NotifStore::open() {
518	        Ok(s) => s,
519	        Err(e) => return DispatchOutcome::Failed(e.to_string()),
520	    };
521	    let policy = NotifPolicy::load();
522	    // This serving node's identity for the presence resolution: the broker's
523	    // handshake identity (the same hex peers see in our registry rows).

/// the daemon's [`RegistryHost`] under the fail-closed gate (member subnet ∧
/// origin trusted in that subnet), until the peer finishes.
///
/// An admitted batch may carry **attention flips** (REQ-INST-3): a peer
/// instance of an id observed transitioning to `Active` — its node's daemon
/// just gave it a driver, so a *locally hosted* instance of the same id
/// rests warm (CONTEXT: driving `ling@laptop` makes `ling@desktop`
/// dormant). The rest edge's transition echo is gate-armed
/// ([`crate::resting::arm_transition_echo`]) — the endpoint's own pulse
/// loop runs the bounded summarizer call, never this worker (the KH 7.4
/// non-blocking posture: N flips in one feed are N gate files, not N LLM
/// calls serialized here).
// [impl->REQ-INST-7]
fn serve_registry_feed(
    brain: &mut Brain,
    stream_id: u64,
    origin: &str,
    registry: &RegistryHost,
) -> DispatchOutcome {
    let policy = RegistryGatePolicy::load();
    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
        return DispatchOutcome::Failed(e.to_string());
    }
    let mut decoder = RegistryDecoder::new();
    let mut merged = 0usize;
    loop {
        match brain.read_event() {
            Ok(BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            }) if sid == stream_id => {
                let records = decoder.push(&bytes);
                if records.is_empty() {
                    continue;
                }
                // Split the feed into instance rows and node-level labels
                // (REQ-SUBNET-3) — each goes through its own gated apply path.
                let mut updates: Vec<RegistryUpdate> = Vec::new();
                let mut labels: Vec<NodeLabelUpdate> = Vec::new();
                for rec in records {
                    match rec {
                        RegistryFeedRecord::Instance(u) => updates.push(u),
                        RegistryFeedRecord::NodeLabel(n) => labels.push(n),
                    }
                }
                if !labels.is_empty() {
                    merged += registry.apply_node_labels(origin, &labels, &policy).len();
                }
                if updates.is_empty() {
                    continue;
                }
                let (verdicts, flips) = registry.apply_feed_flips(origin, &updates, &policy);
                merged += verdicts.len();
                // Attention shift (D9-2): only ids this node actually hosts
                // (a perch with a record); everything else is a no-op here.
                // [impl->REQ-INST-3]
                for id in flips {
                    let local = perch::resolve_perch_path(&id, spt_store::perch::ParentHint::Infer);
                    if local.join("info.json").exists() {
                        if let Err(e) = crate::resting::daemon_rest_event(
                            &id,
                            crate::resting::RestEvent::AttentionShift,
                            None,
                        ) {
                            eprintln!("REST_SHIFT_FAIL:{id}:{e}");
                        }
                    }
                }
            }
            Ok(BrokerEvent::NetStreamEof { stream_id: sid, .. }) if sid == stream_id => {
                return DispatchOutcome::Served(format!("registry feed: {merged} record(s)"));
            }
            Ok(BrokerEvent::Error { message }) => return DispatchOutcome::Failed(message),
            Ok(_) => {}
            Err(e) => return DispatchOutcome::Failed(e.to_string()),
        }
    }
}

/// Notif feed arm: decode rows as they arrive, apply each batch under the
/// fail-closed gate, until the peer finishes. The policy re-loads per feed
/// (not per record) — the same freshness the other arms get per request.
///
/// A **freshly inserted** undismissed row additionally gets a first-fire here
/// (REQ-PRES-1, the convergent cross-node redirect's receiving half): the
/// producing node skipped its local surface because the MRA winner lives
/// here — this node's own presence resolution surfaces it. A row this node's
/// MRA also calls remote stays unmarked and rides on (someone else's
/// feed-apply surfaces it); replays merge `Unchanged` and never re-fire.
// [impl->REQ-NOTIF-1]
// [impl->REQ-PRES-1]
fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {
    let store = match NotifStore::open() {
        Ok(s) => s,
        Err(e) => return DispatchOutcome::Failed(e.to_string()),
    };
    let policy = NotifPolicy::load();
    // This serving node's identity for the presence resolution: the broker's
    // handshake identity (the same hex peers see in our registry rows).


Better fidelity path: split the two trust sources. Push target reads explicit `paths.trust` (set pairwise → reproduces the bug); apply gate reads canonical `SPT_HOME` (set permissive → lets me run the real `run_dispatch_loop` per node). This isolates D0's red to the push-target seam (the primary "pairing-graph-not-mesh" bug); the gate swap is D5's own unit. Let me confirm the two loop signatures.


175	    pub owlery: PathBuf,
176	    /// The staged-release cache dir ([`ReleaseCache::open`]).
177	    pub releases: PathBuf,
178	    /// Transient bundle/artifact scratch.
179	    pub scratch: PathBuf,
180	}
181
182	impl DispatchPaths {
183	    /// The production layout under [`perch::spt_home`].
184	    pub fn production() -> DispatchPaths {
185	        let home = perch::spt_home();
186	        DispatchPaths {
187	            owlery: perch::owlery_dir(),
188	            releases: home.join("releases"),
189	            scratch: home.join("scratch").join("net"),
190	        }
191	    }
192	}
193
194	/// Watch the broker's stream table and dispatch every **peer-initiated**
195	/// stream to a family worker, until `stop` is set. Blocks the calling thread
196	/// (the daemon spawns it on its own); workers detach (each ends with its
197	/// stream).
198	pub fn run_dispatch_loop(
199	    broker_name: &str,
200	    paths: &DispatchPaths,
201	    registry: Arc<RegistryHost>,
202	    stop: &AtomicBool,
203	    poll: Duration,
204	) -> io::Result<()> {
205	    let mut brain = connect(broker_name)?;
206	    let mut claimed: HashSet<u64> = HashSet::new();
207
208	    while !stop.load(Ordering::Relaxed) {
209	        let reply = brain.net_streams()?;
210	        for s in reply.streams {
211	            // Claim only peer-initiated rows with bytes (a QUIC stream is
212	            // invisible until its first write anyway — gotcha #6 — but the
213	            // next_seq guard also skips a row whose pump hasn't appended yet).
214	            if s.initiated_locally || s.next_seq == 0 || !claimed.insert(s.stream_id) {
215	                continue;
216	            }
217	            let name = broker_name.to_string();
218	            let origin = s.remote_id_hex.clone();
219	            let stream_id = s.stream_id;
220	            let p = paths.clone();
221	            let reg = Arc::clone(&registry);
222	            thread::spawn(move || {
223	                let outcome = worker(&name, stream_id, &origin, &p, &reg);
224	                match outcome {
225	                    DispatchOutcome::Served(_) => {}
226	                    other => eprintln!("DISPATCH:{stream_id}:{other:?}"),
227	                }
228	            });
229	        }
230	        thread::sleep(poll);
231	    }
232	    Ok(())
233	}
234
235	/// Connect a fresh brain IPC client (retrying through broker startup).
236	fn connect(broker_name: &str) -> io::Result<Brain> {
237	    let mut last = io::Error::other("broker never answered");
238	    for _ in 0..300 {
239	        match Brain::cold_start(broker_name, now_ms()) {
240	            Ok(b) => return Ok(b),
241	            Err(e) => last = e,
242	        }
243	        thread::sleep(Duration::from_millis(10));
244	    }
245	    Err(last)
246	}
247
248	fn now_ms() -> u64 {
249	    std::time::SystemTime::now()

    pub owlery: PathBuf,
    /// The staged-release cache dir ([`ReleaseCache::open`]).
    pub releases: PathBuf,
    /// Transient bundle/artifact scratch.
    pub scratch: PathBuf,
}

impl DispatchPaths {
    /// The production layout under [`perch::spt_home`].
    pub fn production() -> DispatchPaths {
        let home = perch::spt_home();
        DispatchPaths {
            owlery: perch::owlery_dir(),
            releases: home.join("releases"),
            scratch: home.join("scratch").join("net"),
        }
    }
}

/// Watch the broker's stream table and dispatch every **peer-initiated**
/// stream to a family worker, until `stop` is set. Blocks the calling thread
/// (the daemon spawns it on its own); workers detach (each ends with its
/// stream).
pub fn run_dispatch_loop(
    broker_name: &str,
    paths: &DispatchPaths,
    registry: Arc<RegistryHost>,
    stop: &AtomicBool,
    poll: Duration,
) -> io::Result<()> {
    let mut brain = connect(broker_name)?;
    let mut claimed: HashSet<u64> = HashSet::new();

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

/// Connect a fresh brain IPC client (retrying through broker startup).
fn connect(broker_name: &str) -> io::Result<Brain> {
    let mut last = io::Error::other("broker never answered");
    for _ in 0..300 {
        match Brain::cold_start(broker_name, now_ms()) {
            Ok(b) => return Ok(b),
            Err(e) => last = e,
        }
        thread::sleep(Duration::from_millis(10));
    }
    Err(last)
}

fn now_ms() -> u64 {
    std::time::SystemTime::now()


393	            for sub in &subnets.subnets {
394	                let Ok(ep) = epoch.next_epoch() else { continue };
395	                let instance = Instance {
396	                    node: self.node_hex.clone(),
397	                    status,
398	                    epoch: ep,
399	                    resources: resources.clone(),
400	                    last_active_ms,
401	                    shell_adapters: shell_adapters.clone(),
402	                    // The node label rides every row this node authors
403	                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
404	                    // startup, so a rename converges by re-advertisement.
405	                    // [impl->REQ-SUBNET-3]
406	                    node_label: self.node_label.clone(),
407	                    // The hashed machine id rides beside it (M8 decision
408	                    // 13/22) — the re-pair overwrite anchor.
409	                    // [impl->REQ-SUBNET-7]
410	                    machine_id: self.machine_id.clone(),
411	                };
412	                let reg = regs.entry(sub.name.clone()).or_default();
413	                match advertise_if_visible(
414	                    reg,
415	                    &vis,
416	                    &sub.name,
417	                    sub.hide_new_endpoints,
418	                    &id,
419	                    instance.clone(),
420	                ) {
421	                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {
422	                        subnet: sub.name.clone(),
423	                        endpoint_id: id.clone(),
424	                        instance,
425	                    })),
426	                    Ok(None) => {} // hidden in this subnet — not advertised
427	                    Err(collision) => {
428	                        // Bare-id collision (another node owns the name in
429	                        // this subnet): loud skip — resolution is the user's
430	                        // rename (D3f), never a silent overwrite.
431	                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);
432	                    }
433	                }
434	            }
435	        }
436	        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per
437	        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node
438	        // (zero Self perches, or all hidden) still names itself to peers rather
439	        // than rendering a bare key-prefix. Rides the same epoch lease; merged
440	        // into our own registry too so the snapshot carries it. Emitted every
441	        // round regardless of endpoint count — redundant-but-robust when rows
442	        // exist (the render prefers a trust/instance label anyway), the SOLE
443	        // carrier when they don't.
444	        // [impl->REQ-SUBNET-3]
445	        if let Some(label) = self.node_label.clone() {
446	            for sub in &subnets.subnets {
447	                let Ok(ep) = epoch.next_epoch() else { continue };
448	                let reg = regs.entry(sub.name.clone()).or_default();
449	                reg.merge_node_label(&self.node_hex, label.clone(), ep);
450	                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {
451	                    subnet: sub.name.clone(),
452	                    node: self.node_hex.clone(),
453	                    label: label.clone(),
454	                    epoch: ep,
455	                }));
456	            }
457	        }
458	        drop(epoch);
459	        drop(regs);
460	        self.write_snapshots();
461	        out
462	    }

            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let instance = Instance {
                    node: self.node_hex.clone(),
                    status,
                    epoch: ep,
                    resources: resources.clone(),
                    last_active_ms,
                    shell_adapters: shell_adapters.clone(),
                    // The node label rides every row this node authors
                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
                    // startup, so a rename converges by re-advertisement.
                    // [impl->REQ-SUBNET-3]
                    node_label: self.node_label.clone(),
                    // The hashed machine id rides beside it (M8 decision
                    // 13/22) — the re-pair overwrite anchor.
                    // [impl->REQ-SUBNET-7]
                    machine_id: self.machine_id.clone(),
                };
                let reg = regs.entry(sub.name.clone()).or_default();
                match advertise_if_visible(
                    reg,
                    &vis,
                    &sub.name,
                    sub.hide_new_endpoints,
                    &id,
                    instance.clone(),
                ) {
                    Ok(Some(_)) => out.push(RegistryFeedRecord::Instance(RegistryUpdate {
                        subnet: sub.name.clone(),
                        endpoint_id: id.clone(),
                        instance,
                    })),
                    Ok(None) => {} // hidden in this subnet — not advertised
                    Err(collision) => {
                        // Bare-id collision (another node owns the name in
                        // this subnet): loud skip — resolution is the user's
                        // rename (D3f), never a silent overwrite.
                        eprintln!("REGISTRY_COLLISION:{}:{}", sub.name, collision);
                    }
                }
            }
        }
        // Node-level label (REQ-SUBNET-3): advertise this node's hostname per
        // served subnet INDEPENDENT of endpoint rows, so an endpoint-less node
        // (zero Self perches, or all hidden) still names itself to peers rather
        // than rendering a bare key-prefix. Rides the same epoch lease; merged
        // into our own registry too so the snapshot carries it. Emitted every
        // round regardless of endpoint count — redundant-but-robust when rows
        // exist (the render prefers a trust/instance label anyway), the SOLE
        // carrier when they don't.
        // [impl->REQ-SUBNET-3]
        if let Some(label) = self.node_label.clone() {
            for sub in &subnets.subnets {
                let Ok(ep) = epoch.next_epoch() else { continue };
                let reg = regs.entry(sub.name.clone()).or_default();
                reg.merge_node_label(&self.node_hex, label.clone(), ep);
                out.push(RegistryFeedRecord::NodeLabel(NodeLabelUpdate {
                    subnet: sub.name.clone(),
                    node: self.node_hex.clone(),
                    label: label.clone(),
                    epoch: ep,
                }));
            }
        }
        drop(epoch);
        drop(regs);
        self.write_snapshots();
        out
    }


All seams confirmed. `advertise_if_visible` defaults visible for a `create_subnet` subnet; `RegistryHost::new_at` 3-arg; split-trust design holds. Writing the harness.


//! Mesh-D0 — the staggered + star subnet-mesh repro, RED.
//!
//! These two tests assert the *desired* full-mesh convergence the subnet does
//! NOT yet have (`SUBNET-MESH-PLAN.md`, ADR-0017). They are `#[ignore]`d: the
//! acceptance that flips green at **Mesh-D6** when the registry push target
//! widens from directly-paired peers to the full roster. No production change
//! lands with this file — it locks the bug as an executable repro. See
//! `MESH-D0-PLAN.md` for the fidelity decision.
//!
//! The bug, in one seam: `peerloop.rs` pushes a node's own rows to
//! `trust.peers_in(subnet)` — *directly-paired* peers only, no relay. So when
//! `C` joins `B` (C pins B, not A) and `A` joins `B`, A and C never pin each
//! other; C's pump never targets A and A's never targets C. They stay mutually
//! invisible even when both are online. The mesh restores reach by widening
//! that push target to every roster member (Mesh-D6).
//!
//! Fidelity: the **push side is the real loop** (`run_peer_pump` with explicit
//! per-node `PumpPaths` whose `trust` is the pairwise pin) — so the D6
//! target-widening flips these green untouched. The **gate side runs the real
//! dispatch loop** (`run_dispatch_loop`) under a permissive canonical
//! `SPT_HOME` trust, isolating the red strictly to the push-target seam (the
//! "subnet = pairing graph, not mesh" bug); the gate swap is Mesh-D5's own
//! unit. Addresses are omniscient (a node still only *pushes* where its
//! push-target says, so this does not mask the bug); address *discovery* is the
//! roster's job, tested at D3/D4.
//!
//! Refinement note: once the roster store exists (D3/D4) this harness seeds
//! rosters the way it seeds trust today; the `#[ignore]` lifts at D6 and the
//! file becomes the REQ-MESH-3 `int` evidence (tagged then, not now — D0
//! activates no REQ).

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;

use spt_daemon::brain::Brain;
use spt_daemon::broker::Broker;
use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::peerloop::{
    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,
};
use spt_daemon::registryhost::RegistryHost;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::epoch::EpochSource;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use tempfile::TempDir;

const SUBNET: &str = "mesh0";

// One canonical SPT_HOME for the binary; the lock serializes the two tests
// (SPT_HOME is process-global). Per-node registries/owleries live under
// fresh per-test tempdirs, so the only shared canonical state is the subnet
// record + the permissive gate trust — both monotonic supersets, safe to
// re-establish per test.
static HOME: OnceLock<TempDir> = OnceLock::new();
static LOCK: Mutex<()> = Mutex::new(());
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp home");
        std::env::set_var("SPT_HOME", dir.path());
        std::env::set_var("SPT_NTP_SERVER", "off");
        dir
    });
}

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

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

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

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

/// Poll `probe` until true; panic with `what` on timeout (15s — green paths
/// exit early, so the long bound only pays on the *red* mesh assertions).
fn converge(what: &str, mut probe: impl FnMut() -> bool) {
    for _ in 0..600 {
        if probe() {
            return;
        }
        thread::sleep(Duration::from_millis(25));
    }
    panic!("never converged: {what}");
}

/// The inert/registry-only cadence: only registry feeds flow (notif/sync/
/// update are pushed far enough out to never fire), so the shared canonical
/// notif/context stores stay untouched across nodes.
fn cadence() -> PumpCadence {
    PumpCadence {
        registry: Duration::from_millis(100),
        notif: Duration::from_secs(3_000),
        sync: Duration::from_secs(3_000),
        update: Duration::from_secs(3_000),
        registry_evict_after: Duration::from_secs(3_000),
    }
}

/// One mesh node: its broker + registry + on-disk pump roots. `perch_id` is
/// the node's sole Self perch (the row peers must come to see).
struct MeshNode {
    name: String,
    hex: String,
    addr: String,
    perch_id: String,
    registry: Arc<RegistryHost>,
    paths: PumpPaths,
    dispatch: DispatchPaths,
}

impl MeshNode {
    /// Stand up a node under `root`: broker, a single advertised perch, a
    /// registry, and pump paths whose `trust` pins exactly `paired` (the
    /// pairwise reality — NOT a full mesh).
    fn new(root: &std::path::Path, perch_id: &str, paired: &[&str], dir: &std::path::Path) -> Self {
        let name = unique_name();
        let _broker = net_broker(&name, &root.join("broker"));
        let mut probe = connect_retry(&name);
        let status = probe.net_status().expect("status");
        let hex = status.node_id_hex.expect("net id");
        let addr = status.addr;

        let owlery = root.join("owlery");
        let perch_path = perch::resolve_perch_path_in(&owlery, perch_id, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let rec = InfoJson::new(perch_id, "now", std::process::id(), "s", "ready_agent");
        info::write_info(&perch_path, &rec).unwrap();

        let paths = PumpPaths {
            owlery: owlery.clone(),
            ctx_root: root.join("context"),
            notif_db: root.join("notifs.db"),
            scratch: root.join("scratch"),
            subnets: root.join("subnet.json"),
            trust: root.join("trust.json"),
            vis: root.join("visibility.json"),
            ops: root.join("pump-ops.json"),
            releases: root.join("releases"),
            release_keys: root.join("release-keys.json"),
            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            peer_addrs: root.join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();
        let mut subnets = SubnetStore::default();
        subnets.create_subnet(SUBNET).expect("node subnet");
        subnets.save_to(&paths.subnets).expect("save node subnet");
        let mut trust = TrustStore::default();
        for p in paired {
            trust.record(SUBNET, p, None, None);
        }
        trust.save_to(&paths.trust).expect("save node trust");

        let registry = Arc::new(RegistryHost::new_at(
            &hex,
            EpochSource::load_from(&root.join("epoch.json")),
            root.join("registry"),
        ));
        let dispatch = DispatchPaths {
            owlery,
            releases: paths.releases.clone(),
            scratch: dir.join(format!("disp-{perch_id}")),
        };
        MeshNode {
            name,
            hex,
            addr,
            perch_id: perch_id.to_string(),
            registry,
            paths,
            dispatch,
        }
    }

    /// Spawn this node's production push + dispatch loops, gated on a fresh
    /// stop flag (the returned `Arc` — set it to take the node "offline").
    fn online(&self, resolver: PeerResolver) -> Arc<AtomicBool> {
        let stop = Arc::new(AtomicBool::new(false));
        {
            let (name, reg, paths, flag) = (
                self.name.clone(),
                Arc::clone(&self.registry),
                self.paths.clone(),
                Arc::clone(&stop),
            );
            thread::spawn(move || {
                let config = PumpConfig {
                    cadence: cadence(),
                    full_auto_update: false,
                };
                let _ = run_peer_pump(
                    &name,
                    reg,
                    &paths,
                    resolver,
                    config,
                    PumpHooks::default(),
                    &flag,
                );
            });
        }
        {
            let (name, disp, reg, flag) = (
                self.name.clone(),
                self.dispatch.clone(),
                Arc::clone(&self.registry),
                Arc::clone(&stop),
            );
            thread::spawn(move || {
                let _ = run_dispatch_loop(&name, &disp, reg, &flag, Duration::from_millis(25));
            });
        }
        stop
    }

    /// Does this node's registry hold a row for `endpoint_id` authored by
    /// node `node_hex`? (Visibility — and, with an address for `node_hex`,
    /// reach.)
    fn sees(&self, endpoint_id: &str, node_hex: &str) -> bool {
        self.registry
            .rows(SUBNET, endpoint_id)
            .iter()
            .any(|i| i.node == node_hex)
    }
}

/// Establish the permissive canonical gate trust: every member trusts every
/// other in `SUBNET`, so the dispatch-side `RegistryGatePolicy::load()` admits
/// any member feed. The pairwise pin that reproduces the bug lives in each
/// node's *explicit* `paths.trust` (the push target), not here.
fn canonical_full_trust(hexes: &[&str]) {
    let mut subnets = SubnetStore::load();
    let _ = subnets.create_subnet(SUBNET); // idempotent across tests
    subnets.save().expect("save canonical subnet");
    let mut trust = TrustStore::load();
    for h in hexes {
        trust.record(SUBNET, h, None, None);
    }
    trust.save().expect("save canonical trust");
}

/// An omniscient resolver over the whole fleet (hex → loopback addr).
fn resolver_over(addrs: &HashMap<String, String>) -> PeerResolver {
    let map = addrs.clone();
    Box::new(move |hex: &str| map.get(hex).cloned())
}

// ── The staggered repro (the acceptance) ────────────────────────────────────
// A joins B, A offline, C joins B, B offline, A online. At the end A and C are
// both online and B is not — the exact grill repro. The mesh must converge
// A↔C (see AND reach) with B absent. RED today: A pins only B, C pins only B,
// so neither ever pushes to the other. Flips green at Mesh-D6.
#[test]
#[ignore = "mesh lands at Mesh-D6 — this is the Mesh-D0 red repro"]
fn staggered_offliner_still_meshes() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();

    // Pairwise pins: A↔B, B↔C. A and C never pin each other.
    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
    // Re-pin now that hexes are known: A pins B; B pins A,C; C pins B.
    repin(&a, &[&b.hex]);
    repin(&b, &[&a.hex, &c.hex]);
    repin(&c, &[&b.hex]);

    canonical_full_trust(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, String> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    a_stop.store(true, Ordering::Relaxed); // A offline

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    b_stop.store(true, Ordering::Relaxed); // B offline

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone. This is the red assertion.
    let a_stop2 = a.online(resolver_over(&addrs));
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Reach: each end holds a dialable address for the other.
    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

// ── The all-online star (B never relays) ─────────────────────────────────────
// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —
// directly, with B as a non-relaying hub (B emits only its OWN rows, so an
// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// RED today: A pushes only to B, C only to B; B forwards neither.
#[test]
#[ignore = "mesh lands at Mesh-D6 — this is the Mesh-D0 red repro"]
fn all_online_star_a_reaches_c_b_never_relays() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();

    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
    repin(&a, &[&b.hex]);
    repin(&b, &[&a.hex, &c.hex]);
    repin(&c, &[&b.hex]);

    canonical_full_trust(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, String> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    let c_stop = c.online(resolver_over(&addrs));

    // The hub sees both spokes (the directly-paired baseline — green today).
    converge("B sees A", || b.sees("perchA", &a.hex));
    converge("B sees C", || b.sees("perchC", &c.hex));

    // The mesh: spokes see each other WITHOUT B relaying. Red today.
    converge("A sees C directly", || a.sees("perchC", &c.hex));
    converge("C sees A directly", || c.sees("perchA", &a.hex));

    a_stop.store(true, Ordering::Relaxed);
    b_stop.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

/// Overwrite a node's explicit push-target trust to pin exactly `paired`.
fn repin(node: &MeshNode, paired: &[&str]) {
    let mut trust = TrustStore::default();
    for p in paired {
        trust.record(SUBNET, p, None, None);
    }
    trust.save_to(&node.paths.trust).expect("re-pin trust");
}

// Silence unused-field warnings: these are part of the node model the harness
// documents even where a given test does not read them.
#[allow(dead_code)]
fn _touch(node: &MeshNode) -> (&str, &str) {
    (&node.perch_id, node.paths.owlery.to_str().unwrap_or(""))
}
#[allow(dead_code)]
type _Pb = PathBuf;


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

//! Mesh-D0 — the staggered + star subnet-mesh repro, RED.
//!
//! These two tests assert the *desired* full-mesh convergence the subnet does
//! NOT yet have (`SUBNET-MESH-PLAN.md`, ADR-0017). They are `#[ignore]`d: the
//! acceptance that flips green at **Mesh-D6** when the registry push target
//! widens from directly-paired peers to the full roster. No production change
//! lands with this file — it locks the bug as an executable repro. See
//! `MESH-D0-PLAN.md` for the fidelity decision.
//!
//! The bug, in one seam: `peerloop.rs` pushes a node's own rows to
//! `trust.peers_in(subnet)` — *directly-paired* peers only, no relay. So when
//! `C` joins `B` (C pins B, not A) and `A` joins `B`, A and C never pin each
//! other; C's pump never targets A and A's never targets C. They stay mutually
//! invisible even when both are online. The mesh restores reach by widening
//! that push target to every roster member (Mesh-D6).
//!
//! Fidelity: the **push side is the real loop** (`run_peer_pump` with explicit
//! per-node `PumpPaths` whose `trust` is the pairwise pin) — so the D6
//! target-widening flips these green untouched. The **gate side runs the real
//! dispatch loop** (`run_dispatch_loop`) under a permissive canonical
//! `SPT_HOME` trust, isolating the red strictly to the push-target seam (the
//! "subnet = pairing graph, not mesh" bug); the gate swap is Mesh-D5's own
//! unit. Addresses are omniscient (a node still only *pushes* where its
//! push-target says, so this does not mask the bug); address *discovery* is the
//! roster's job, tested at D3/D4.
//!
//! Refinement note: once the roster store exists (D3/D4) this harness seeds
//! rosters the way it seeds trust today; the `#[ignore]` lifts at D6 and the
//! file becomes the REQ-MESH-3 `int` evidence (tagged then, not now — D0
//! activates no REQ).

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread;
use std::time::Duration;

use spt_daemon::brain::Brain;
use spt_daemon::broker::Broker;
use spt_daemon::dispatch::{run_dispatch_loop, DispatchPaths};
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::peerloop::{
    run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths,
};
use spt_daemon::registryhost::RegistryHost;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;
use spt_store::epoch::EpochSource;
use spt_store::info::{self, InfoJson};
use spt_store::perch::{self, ParentHint};
use spt_store::subnet::SubnetStore;
use spt_store::trust::TrustStore;
use tempfile::TempDir;

const SUBNET: &str = "mesh0";

// One canonical SPT_HOME for the binary; the lock serializes the two tests
// (SPT_HOME is process-global). Per-node registries/owleries live under
// fresh per-test tempdirs, so the only shared canonical state is the subnet
// record + the permissive gate trust — both monotonic supersets, safe to
// re-establish per test.
static HOME: OnceLock<TempDir> = OnceLock::new();
static LOCK: Mutex<()> = Mutex::new(());
fn init_home() {
    HOME.get_or_init(|| {
        let dir = TempDir::new().expect("temp home");
        std::env::set_var("SPT_HOME", dir.path());
        std::env::set_var("SPT_NTP_SERVER", "off");
        dir
    });
}

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

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

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

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

/// Poll `probe` until true; panic with `what` on timeout (15s — green paths
/// exit early, so the long bound only pays on the *red* mesh assertions).
fn converge(what: &str, mut probe: impl FnMut() -> bool) {
    for _ in 0..600 {
        if probe() {
            return;
        }
        thread::sleep(Duration::from_millis(25));
    }
    panic!("never converged: {what}");
}

/// The inert/registry-only cadence: only registry feeds flow (notif/sync/
/// update are pushed far enough out to never fire), so the shared canonical
/// notif/context stores stay untouched across nodes.
fn cadence() -> PumpCadence {
    PumpCadence {
        registry: Duration::from_millis(100),
        notif: Duration::from_secs(3_000),
        sync: Duration::from_secs(3_000),
        update: Duration::from_secs(3_000),
        registry_evict_after: Duration::from_secs(3_000),
    }
}

/// One mesh node: its broker + registry + on-disk pump roots. `perch_id` is
/// the node's sole Self perch (the row peers must come to see).
struct MeshNode {
    name: String,
    hex: String,
    addr: String,
    perch_id: String,
    registry: Arc<RegistryHost>,
    paths: PumpPaths,
    dispatch: DispatchPaths,
}

impl MeshNode {
    /// Stand up a node under `root`: broker, a single advertised perch, a
    /// registry, and pump paths whose `trust` pins exactly `paired` (the
    /// pairwise reality — NOT a full mesh).
    fn new(root: &std::path::Path, perch_id: &str, paired: &[&str], dir: &std::path::Path) -> Self {
        let name = unique_name();
        let _broker = net_broker(&name, &root.join("broker"));
        let mut probe = connect_retry(&name);
        let status = probe.net_status().expect("status");
        let hex = status.node_id_hex.expect("net id");
        let addr = status.addr;

        let owlery = root.join("owlery");
        let perch_path = perch::resolve_perch_path_in(&owlery, perch_id, ParentHint::Infer);
        std::fs::create_dir_all(&perch_path).unwrap();
        let rec = InfoJson::new(perch_id, "now", std::process::id(), "s", "ready_agent");
        info::write_info(&perch_path, &rec).unwrap();

        let paths = PumpPaths {
            owlery: owlery.clone(),
            ctx_root: root.join("context"),
            notif_db: root.join("notifs.db"),
            scratch: root.join("scratch"),
            subnets: root.join("subnet.json"),
            trust: root.join("trust.json"),
            vis: root.join("visibility.json"),
            ops: root.join("pump-ops.json"),
            releases: root.join("releases"),
            release_keys: root.join("release-keys.json"),
            pull_marker: root.join("sync-pull-now"),
            advertise_marker: root.join("advertise-now"),
            peer_addrs: root.join("peer-addrs.json"),
            heartbeat: root.join("pump-heartbeat.json"),
        };
        std::fs::create_dir_all(root).unwrap();
        let mut subnets = SubnetStore::default();
        subnets.create_subnet(SUBNET).expect("node subnet");
        subnets.save_to(&paths.subnets).expect("save node subnet");
        let mut trust = TrustStore::default();
        for p in paired {
            trust.record(SUBNET, p, None, None);
        }
        trust.save_to(&paths.trust).expect("save node trust");

        let registry = Arc::new(RegistryHost::new_at(
            &hex,
            EpochSource::load_from(&root.join("epoch.json")),
            root.join("registry"),
        ));
        let dispatch = DispatchPaths {
            owlery,
            releases: paths.releases.clone(),
            scratch: dir.join(format!("disp-{perch_id}")),
        };
        MeshNode {
            name,
            hex,
            addr,
            perch_id: perch_id.to_string(),
            registry,
            paths,
            dispatch,
        }
    }

    /// Spawn this node's production push + dispatch loops, gated on a fresh
    /// stop flag (the returned `Arc` — set it to take the node "offline").
    fn online(&self, resolver: PeerResolver) -> Arc<AtomicBool> {
        let stop = Arc::new(AtomicBool::new(false));
        {
            let (name, reg, paths, flag) = (
                self.name.clone(),
                Arc::clone(&self.registry),
                self.paths.clone(),
                Arc::clone(&stop),
            );
            thread::spawn(move || {
                let config = PumpConfig {
                    cadence: cadence(),
                    full_auto_update: false,
                };
                let _ = run_peer_pump(
                    &name,
                    reg,
                    &paths,
                    resolver,
                    config,
                    PumpHooks::default(),
                    &flag,
                );
            });
        }
        {
            let (name, disp, reg, flag) = (
                self.name.clone(),
                self.dispatch.clone(),
                Arc::clone(&self.registry),
                Arc::clone(&stop),
            );
            thread::spawn(move || {
                let _ = run_dispatch_loop(&name, &disp, reg, &flag, Duration::from_millis(25));
            });
        }
        stop
    }

    /// Does this node's registry hold a row for `endpoint_id` authored by
    /// node `node_hex`? (Visibility — and, with an address for `node_hex`,
    /// reach.)
    fn sees(&self, endpoint_id: &str, node_hex: &str) -> bool {
        self.registry
            .rows(SUBNET, endpoint_id)
            .iter()
            .any(|i| i.node == node_hex)
    }
}

/// Establish the permissive canonical gate trust: every member trusts every
/// other in `SUBNET`, so the dispatch-side `RegistryGatePolicy::load()` admits
/// any member feed. The pairwise pin that reproduces the bug lives in each
/// node's *explicit* `paths.trust` (the push target), not here.
fn canonical_full_trust(hexes: &[&str]) {
    let mut subnets = SubnetStore::load();
    let _ = subnets.create_subnet(SUBNET); // idempotent across tests
    subnets.save().expect("save canonical subnet");
    let mut trust = TrustStore::load();
    for h in hexes {
        trust.record(SUBNET, h, None, None);
    }
    trust.save().expect("save canonical trust");
}

/// An omniscient resolver over the whole fleet (hex → loopback addr).
fn resolver_over(addrs: &HashMap<String, String>) -> PeerResolver {
    let map = addrs.clone();
    Box::new(move |hex: &str| map.get(hex).cloned())
}

// ── The staggered repro (the acceptance) ────────────────────────────────────
// A joins B, A offline, C joins B, B offline, A online. At the end A and C are
// both online and B is not — the exact grill repro. The mesh must converge
// A↔C (see AND reach) with B absent. RED today: A pins only B, C pins only B,
// so neither ever pushes to the other. Flips green at Mesh-D6.
#[test]
#[ignore = "mesh lands at Mesh-D6 — this is the Mesh-D0 red repro"]
fn staggered_offliner_still_meshes() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();

    // Pairwise pins: A↔B, B↔C. A and C never pin each other.
    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
    // Re-pin now that hexes are known: A pins B; B pins A,C; C pins B.
    repin(&a, &[&b.hex]);
    repin(&b, &[&a.hex, &c.hex]);
    repin(&c, &[&b.hex]);

    canonical_full_trust(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, String> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    // Phase 1 — A joins B (both online): B comes to see A, A to see B.
    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    converge("B sees A's perch", || b.sees("perchA", &a.hex));
    converge("A sees B's perch", || a.sees("perchB", &b.hex));
    a_stop.store(true, Ordering::Relaxed); // A offline

    // Phase 2 — C joins B (B still online): B comes to see C.
    let c_stop = c.online(resolver_over(&addrs));
    converge("B sees C's perch", || b.sees("perchC", &c.hex));
    b_stop.store(true, Ordering::Relaxed); // B offline

    // Phase 3 — A online again; C still online; B offline. The mesh must
    // converge A↔C with B gone. This is the red assertion.
    let a_stop2 = a.online(resolver_over(&addrs));
    converge("A meshes to C (sees perchC)", || a.sees("perchC", &c.hex));
    converge("C meshes to A (sees perchA)", || c.sees("perchA", &a.hex));

    // Reach: each end holds a dialable address for the other.
    assert!(addrs.contains_key(&c.hex) && addrs.contains_key(&a.hex));

    a_stop2.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

// ── The all-online star (B never relays) ─────────────────────────────────────
// A, B, C all online; pairwise A↔B, B↔C only. A must reach C and C reach A —
// directly, with B as a non-relaying hub (B emits only its OWN rows, so an
// A-row reaching C can only have come straight from A; KH 4.10/7.5 preserved).
// RED today: A pushes only to B, C only to B; B forwards neither.
#[test]
#[ignore = "mesh lands at Mesh-D6 — this is the Mesh-D0 red repro"]
fn all_online_star_a_reaches_c_b_never_relays() {
    init_home();
    let _g = LOCK.lock().unwrap_or_else(|p| p.into_inner());
    let dir = TempDir::new().unwrap();

    let a = MeshNode::new(&dir.path().join("a"), "perchA", &[], dir.path());
    let b = MeshNode::new(&dir.path().join("b"), "perchB", &[], dir.path());
    let c = MeshNode::new(&dir.path().join("c"), "perchC", &[], dir.path());
    repin(&a, &[&b.hex]);
    repin(&b, &[&a.hex, &c.hex]);
    repin(&c, &[&b.hex]);

    canonical_full_trust(&[&a.hex, &b.hex, &c.hex]);
    let addrs: HashMap<String, String> = [
        (a.hex.clone(), a.addr.clone()),
        (b.hex.clone(), b.addr.clone()),
        (c.hex.clone(), c.addr.clone()),
    ]
    .into_iter()
    .collect();

    let a_stop = a.online(resolver_over(&addrs));
    let b_stop = b.online(resolver_over(&addrs));
    let c_stop = c.online(resolver_over(&addrs));

    // The hub sees both spokes (the directly-paired baseline — green today).
    converge("B sees A", || b.sees("perchA", &a.hex));
    converge("B sees C", || b.sees("perchC", &c.hex));

    // The mesh: spokes see each other WITHOUT B relaying. Red today.
    converge("A sees C directly", || a.sees("perchC", &c.hex));
    converge("C sees A directly", || c.sees("perchA", &a.hex));

    a_stop.store(true, Ordering::Relaxed);
    b_stop.store(true, Ordering::Relaxed);
    c_stop.store(true, Ordering::Relaxed);
}

/// Overwrite a node's explicit push-target trust to pin exactly `paired`.
fn repin(node: &MeshNode, paired: &[&str]) {
    let mut trust = TrustStore::default();
    for p in paired {
        trust.record(SUBNET, p, None, None);
    }
    trust.save_to(&node.paths.trust).expect("re-pin trust");
}

// Silence unused-field warnings: these are part of the node model the harness
// documents even where a given test does not read them.
#[allow(dead_code)]
fn _touch(node: &MeshNode) -> (&str, &str) {
    (&node.perch_id, node.paths.owlery.to_str().unwrap_or(""))
}
#[allow(dead_code)]
type _Pb = PathBuf;


Compile-check the harness (catch API mismatches before running).


Bash

cd /c/Users/decid/Documents/projects/spt-core && cargo test -p spt-daemon --test mesh --no-run 2>&1 | tail -40

Compile mesh test