818	                last_active_ms: None,
819	                shell_adapters: Vec::new(),
820	                node_label: None,
821	                machine_id: None,
822	            },
823	        }
824	    }
825
826	    /// A roster entry for `pk` in `subnet` — the Mesh-D5 gate seed
827	    /// (`is_member` keys on `(subnet, pubkey)`).
828	    fn member(subnet: &str, pk: &str) -> spt_store::roster::RosterEntry {
829	        spt_store::roster::RosterEntry {
830	            pubkey_hex: pk.to_string(),
831	            subnet: subnet.to_string(),
832	            label: String::new(),
833	            machine_id: String::new(),
834	            address: None,
835	            last_seen: String::new(),
836	            lease_epoch: 1,
837	        }
838	    }
839
840	    fn policy(subnet: &str, trusted: &str) -> RegistryGatePolicy {
841	        let mut roster = RosterStore::default();
842	        roster.merge_entry(member(subnet, trusted));
843	        RegistryGatePolicy {
844	            subnets: vec![subnet.to_string()],
845	            roster,
846	        }
847	    }
848
849	    fn host(dir: &Path) -> RegistryHost {
850	        RegistryHost::new_at(
851	            "aa11",
852	            EpochSource::load_from(&dir.join("epoch.json")),
853	            dir.join("registry"),
854	        )
855	    }
856
857	    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] the consent-notif producer
858	    // shares the node's ONE canonical epoch with the registry leg — the fix
859	    // for the rig presence-rung `UNIQUE constraint failed: notifs.notif_id`.
860	    // `with_epoch` is the canonical (identity/epoch.json) cursor: ids minted
861	    // through it continue the SAME sequence registry rows use and persist to
862	    // the SAME file — so a consent notif on a fresh node can never reissue a
863	    // low `node:epoch` that a notify/presence producer (which loads this same
864	    // file) already used.
865	    #[test]
866	    fn with_epoch_is_the_one_canonical_counter() {
867	        let dir = tempfile::tempdir().unwrap();
868	        let epoch_path = dir.path().join("epoch.json");
869	        let h = RegistryHost::new_at(
870	            "aa11",
871	            EpochSource::load_from(&epoch_path),
872	            dir.path().join("registry"),
873	        );
874	        // Registry leg advances the canonical counter (stand-in for
875	        // advertise_local stamping rows).
876	        let r1 = h.with_epoch(|e| e.next_epoch().unwrap());
877	        let r2 = h.with_epoch(|e| e.next_epoch().unwrap());
878	        assert_eq!((r1, r2), (1, 2));
879	        // A consent notif minting through the SAME accessor continues the
880	        // sequence — never restarts at 1 (the pump-ops.json bug).
881	        let consent = h.with_epoch(|e| e.next_epoch().unwrap());
882	        assert_eq!(consent, 3, "consent shares the registry counter");
883	        // And it is the canonical FILE: a fresh load (the CLI notify front
884	        // door's view) sees the advanced value and mints strictly above it —
885	        // no cross-producer id reuse.
886	        let mut front_door = EpochSource::load_from(&epoch_path);
887	        assert_eq!(front_door.current(), 3, "persisted to the canonical file");
888	        assert_eq!(front_door.next_epoch().unwrap(), 4);
889	    }
890
891	    /// An instance row stamped with a label + machine id (the M8 re-pair
892	    /// anchor pair).
893	    fn identified(node: &str, label: &str, mid: &str, epoch: u64) -> Instance {
894	        Instance {
895	            node: node.to_string(),
896	            status: Status::Active,
897	            epoch,
898	            resources: None,
899	            last_active_ms: None,
900	            shell_adapters: Vec::new(),
901	            node_label: Some(label.to_string()),
902	            machine_id: Some(mid.to_string()),
903	        }
904	    }
905
906	    // [unit->REQ-SUBNET-7] superseded-identity resolution: BOTH label and
907	    // machine id must match a DIFFERENT key — label alone (hostname

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        }
    }

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

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

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

    // [unit->REQ-HAZARD-REGISTRY-EPOCH-LEASE] the consent-notif producer
    // shares the node's ONE canonical epoch with the registry leg — the fix
    // for the rig presence-rung `UNIQUE constraint failed: notifs.notif_id`.
    // `with_epoch` is the canonical (identity/epoch.json) cursor: ids minted
    // through it continue the SAME sequence registry rows use and persist to
    // the SAME file — so a consent notif on a fresh node can never reissue a
    // low `node:epoch` that a notify/presence producer (which loads this same
    // file) already used.
    #[test]
    fn with_epoch_is_the_one_canonical_counter() {
        let dir = tempfile::tempdir().unwrap();
        let epoch_path = dir.path().join("epoch.json");
        let h = RegistryHost::new_at(
            "aa11",
            EpochSource::load_from(&epoch_path),
            dir.path().join("registry"),
        );
        // Registry leg advances the canonical counter (stand-in for
        // advertise_local stamping rows).
        let r1 = h.with_epoch(|e| e.next_epoch().unwrap());
        let r2 = h.with_epoch(|e| e.next_epoch().unwrap());
        assert_eq!((r1, r2), (1, 2));
        // A consent notif minting through the SAME accessor continues the
        // sequence — never restarts at 1 (the pump-ops.json bug).
        let consent = h.with_epoch(|e| e.next_epoch().unwrap());
        assert_eq!(consent, 3, "consent shares the registry counter");
        // And it is the canonical FILE: a fresh load (the CLI notify front
        // door's view) sees the advanced value and mints strictly above it —
        // no cross-producer id reuse.
        let mut front_door = EpochSource::load_from(&epoch_path);
        assert_eq!(front_door.current(), 3, "persisted to the canonical file");
        assert_eq!(front_door.next_epoch().unwrap(), 4);
    }

    /// An instance row stamped with a label + machine id (the M8 re-pair
    /// anchor pair).
    fn identified(node: &str, label: &str, mid: &str, epoch: u64) -> Instance {
        Instance {
            node: node.to_string(),
            status: Status::Active,
            epoch,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
        }
    }

    // [unit->REQ-SUBNET-7] superseded-identity resolution: BOTH label and
    // machine id must match a DIFFERENT key — label alone (hostname


Edit

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        }
    }

    /// A roster entry for `pk` in `subnet`

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        }
    }

    /// A roster entry for `pk` in `subnet`


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

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        }
    }

    /// A roster entry for `pk` in `subnet`

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        }
    }

    /// A roster entry for `pk` in `subnet`


            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
        }
    }

            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
            endpoint_type: None,
        }
    }


            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
        }
    }

            node_label: Some(label.to_string()),
            machine_id: Some(mid.to_string()),
            endpoint_type: None,
        }
    }


1198	            id: &str,
1199	            node: &str,
1200	            status: Status,
1201	            epoch: u64,
1202	        ) -> RegistryUpdate {
1203	            RegistryUpdate {
1204	                subnet: subnet.to_string(),
1205	                endpoint_id: id.to_string(),
1206	                instance: Instance {
1207	                    node: node.to_string(),
1208	                    status,
1209	                    epoch,
1210	                    resources: None,
1211	                    last_active_ms: None,
1212	                    shell_adapters: Vec::new(),
1213	                    node_label: None,
1214	                    machine_id: None,
1215	                },

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


REQ survey + mint REQ-MSG-6

Add REQ-MSG-6 to traceable-reqs.toml [doc,impl,unit]; update REQ-EP-6 title (drop deferred clause); note REQ-INST-7 model extension. Activate stages as satisfied.

T1 Instance.endpoint_type field

Add endpoint_type: Option<String> to Instance (additive serde-default, skip_serializing_if none). Update all ~24 construction sites with endpoint_type: None. impl->REQ-MSG-6.

T2 populate on advertise_local

In advertise_local, set endpoint_type from info.state (the endpoint's own record). Node advertises only its own endpoints' types.

T3 resolve at receive_wan funnel

Pure helper origin_user_backed(instances, origin_node); RegistryHost::instances_of(id); thread &RegistryHost into receive_wan + serve_wan_feed; flip hardcoded false. Update test callers.

T4 unit test matrix

5-row matrix on origin_user_backed + restamp. gateway/agent/none/wrong-node/plain-msg.

Sweep + traceable + push

Full workspace test, clippy -D, --no-default-features, xtask check, traceable EXIT=0, schema re-bless if needed. Push, CI both runners, ping doyle.


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },

+                    endpoint_type: None,


Now notif.rs, presence.rs, reconcile.rs, shelldisc.rs.


670	            let mut reg = spt_net::net::registry::SubnetRegistry::new();
671	            reg.merge_instance(
672	                "ling",
673	                spt_net::net::registry::Instance {
674	                    node: "remote-hex".into(),
675	                    status: spt_net::net::registry::Status::Active,
676	                    epoch: 1,
677	                    resources: None,
678	                    shell_adapters: Vec::new(),
679	                    last_active_ms: Some(9_000),
680	                    node_label: None,
681	                    machine_id: None,
682	                },
683	            );
684	            let mut regs = std::collections::BTreeMap::new();
685	            regs.insert("home".to_string(), reg);

            let mut reg = spt_net::net::registry::SubnetRegistry::new();
            reg.merge_instance(
                "ling",
                spt_net::net::registry::Instance {
                    node: "remote-hex".into(),
                    status: spt_net::net::registry::Status::Active,
                    epoch: 1,
                    resources: None,
                    shell_adapters: Vec::new(),
                    last_active_ms: Some(9_000),
                    node_label: None,
                    machine_id: None,
                },
            );
            let mut regs = std::collections::BTreeMap::new();
            regs.insert("home".to_string(), reg);


215	    }
216
217	    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {
218	        Instance {
219	            node: node.into(),
220	            status,
221	            epoch: 1,
222	            resources: None,
223	            last_active_ms: ts,
224	            shell_adapters: Vec::new(),
225	            node_label: None,
226	            machine_id: None,
227	        }
228	    }
229
230	    fn open_policy(subnets: &[&str]) -> NotifSurfacePolicy {

    }

    fn row(node: &str, status: Status, ts: Option<u64>) -> Instance {
        Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        }
    }

    fn open_policy(subnets: &[&str]) -> NotifSurfacePolicy {


                    last_active_ms: Some(9_000),
                    node_label: None,
                    machine_id: None,
                },

                    last_active_ms: Some(9_000),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },


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

                    last_active_ms: Some(9_000),
                    node_label: None,
                    machine_id: None,
                },

                    last_active_ms: Some(9_000),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },

let mut regs = std::collections::BTreeMap::new();


            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        }
    }

            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        }
    }


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

            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        }
    }

            last_active_ms: ts,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        }
    }


47	    use super::*;
48
49	    fn inst(node: &str, status: Status) -> Instance {
50	        Instance {
51	            node: node.to_string(),
52	            status,
53	            epoch: 1,
54	            resources: None,
55	            last_active_ms: None,
56	            shell_adapters: Vec::new(),
57	            node_label: None,
58	            machine_id: None,
59	        }
60	    }
61
62	    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.

    use super::*;

    fn inst(node: &str, status: Status) -> Instance {
        Instance {
            node: node.to_string(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        }
    }

    #[test] // [unit->REQ-HAZARD-CONFLICT-BOTH-PRESERVED] the Active instance's node reconciles; everyone else stands down.


320	        let registered = spt_runtime::registry::registered(&adapters);
321
322	        let inst = |node: &str, status: Status, shells: &[&str]| Instance {
323	            node: node.into(),
324	            status,
325	            epoch: 1,
326	            resources: None,
327	            last_active_ms: None,
328	            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
329	            node_label: None,
330	            machine_id: None,
331	        };
332	        let mut reg = SubnetRegistry::new();
333	        // Two endpoints on the same remote node carry the same node-level
334	        // datum: dedupes to one entry.
335	        reg.merge_instance(

        let registered = spt_runtime::registry::registered(&adapters);

        let inst = |node: &str, status: Status, shells: &[&str]| Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
            machine_id: None,
        };
        let mut reg = SubnetRegistry::new();
        // Two endpoints on the same remote node carry the same node-level
        // datum: dedupes to one entry.
        reg.merge_instance(


            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        }
    }

            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        }
    }


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

            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        }
    }

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

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

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

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

    fn inst(node: &str, status: Status) -> Instance {
        Instance {
            node: node.to_string(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        }
    }

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

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

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

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


            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
            machine_id: None,
        };

            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        };


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

            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
            machine_id: None,
        };

            shell_adapters: shells.iter().map(|s| s.to_string()).collect(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        };

// Two endpoints on the same remote node carry the same node-level


REQ survey + mint REQ-MSG-6

T1 Instance.endpoint_type field

T2 populate on advertise_local

T3 resolve at receive_wan funnel

T4 unit test matrix

Sweep + traceable + push


Now spt-daemon tests literals: dispatch.rs, pump.rs, replicate.rs, sync.rs.


594	    let mut reg = spt_net::net::registry::SubnetRegistry::new();
595	    reg.merge_instance(
596	        target,
597	        spt_net::net::registry::Instance {
598	            node: b_hex.clone(),
599	            status: spt_net::net::registry::Status::Active,
600	            epoch: 1,
601	            resources: None,
602	            last_active_ms: Some(9_000),
603	            shell_adapters: Vec::new(),
604	            node_label: None,
605	            machine_id: None,
606	        },
607	    );

    let mut reg = spt_net::net::registry::SubnetRegistry::new();
    reg.merge_instance(
        target,
        spt_net::net::registry::Instance {
            node: b_hex.clone(),
            status: spt_net::net::registry::Status::Active,
            epoch: 1,
            resources: None,
            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    );


908	    );
909
910	    let inst = RegistryFeedRecord::Instance(RegistryUpdate {
911	        subnet: "home".into(),
912	        endpoint_id: "doyle".into(),
913	        instance: Instance {
914	            node: "n1".into(),
915	            status: Status::Active,
916	            epoch: 1,
917	            resources: None,
918	            last_active_ms: None,
919	            shell_adapters: Vec::new(),
920	            node_label: None,
921	            machine_id: None,
922	        },
923	    });

    );

    let inst = RegistryFeedRecord::Instance(RegistryUpdate {
        subnet: "home".into(),
        endpoint_id: "doyle".into(),
        instance: Instance {
            node: "n1".into(),
            status: Status::Active,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    });


            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    );

            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    );


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

            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    );

            last_active_ms: Some(9_000),
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    );

let snap_dir = spt_daemon::presence::registry_snapshot_dir();


            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    });

            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    });


            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    });

            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    });


298	            &b_hex,
299	            &[RegistryUpdate {
300	                subnet: subnet.to_string(),
301	                endpoint_id: "ling-pumped".to_string(),
302	                instance: Instance {
303	                    node: b_hex.clone(),
304	                    status: Status::Active,
305	                    epoch: 1,
306	                    resources: None,
307	                    last_active_ms: None,
308	                    shell_adapters: Vec::new(),
309	                    node_label: None,
310	                    machine_id: None,
311	                },
312	            }],
313	            &pol,

            &b_hex,
            &[RegistryUpdate {
                subnet: subnet.to_string(),
                endpoint_id: "ling-pumped".to_string(),
                instance: Instance {
                    node: b_hex.clone(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            }],
            &pol,


112	    // The advertise gate runs LOCALLY, before anything is emitted: the wire
113	    // only ever carries already-admitted state (D3d collision + D3e visibility).
114	    let e1 = epochs.next_epoch().expect("epoch");
115	    let active = Instance {
116	        node: a_node.clone(),
117	        status: Status::Active,
118	        epoch: e1,
119	        resources: None,
120	        last_active_ms: None,
121	        shell_adapters: Vec::new(),
122	        node_label: None,
123	        machine_id: None,
124	    };
125	    let admitted = advertise_if_visible(
126	        a_regs.get_mut("home").expect("member"),
127	        &visibility,
128	        "home",
129	        false,
130	        "doyle",
131	        active.clone(),
132	    )
133	    .expect("no collision")
134	    .expect("visible — advertised");
135	    assert_eq!(admitted, MergeOutcome::Inserted);
136
137	    let update_active = RegistryUpdate {
138	        subnet: "home".into(),
139	        endpoint_id: "doyle".into(),
140	        instance: active,
141	    };
142	    let e2 = epochs.next_epoch().expect("epoch");
143	    assert!(e2 > e1, "EpochSource is strictly monotonic");
144	    let update_offline = RegistryUpdate {
145	        subnet: "home".into(),
146	        endpoint_id: "doyle".into(),
147	        instance: Instance {
148	            node: a_node.clone(),
149	            status: Status::Offline,
150	            epoch: e2,
151	            resources: None,
152	            last_active_ms: None,
153	            shell_adapters: Vec::new(),

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

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


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            }],

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },
            }],


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

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            }],

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },
            }],


153	            shell_adapters: Vec::new(),
154	            node_label: None,
155	            machine_id: None,
156	        },
157	    };
158
159	    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────
160	    let mut b_probe = connect_retry(&name_b);

            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    };

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


        node_label: None,
        machine_id: None,
    };
    let admitted = advertise_if_visible(

        node_label: None,
        machine_id: None,
        endpoint_type: None,
    };
    let admitted = advertise_if_visible(


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

        node_label: None,
        machine_id: None,
        endpoint_type: None,
    };
    let admitted = advertise_if_visible(

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    };

    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────

            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    };

    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────


            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    };

    // ── The wire: dial B, open a stream, send the feed (journaled ops). ─────

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


420	        "B holds A's version"
421	    );
422
423	    // Election: B's node holds the Active instance — A must stand down.
424	    let rows = vec![
425	        Instance {
426	            node: "node-b".into(),
427	            status: Status::Active,
428	            epoch: 2,
429	            resources: None,
430	            last_active_ms: None,
431	            shell_adapters: Vec::new(),
432	            node_label: None,
433	            machine_id: None,
434	        },
435	        Instance {
436	            node: "node-a".into(),
437	            status: Status::Dormant,
438	            epoch: 1,
439	            resources: None,
440	            last_active_ms: None,
441	            shell_adapters: Vec::new(),
442	            node_label: None,
443	            machine_id: None,

        "B holds A's version"
    );

    // Election: B's node holds the Active instance — A must stand down.
    let rows = vec![
        Instance {
            node: "node-b".into(),
            status: Status::Active,
            epoch: 2,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
        Instance {
            node: "node-a".into(),
            status: Status::Dormant,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,


            node: "node-b".into(),
            status: Status::Active,
            epoch: 2,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },

            node: "node-b".into(),
            status: Status::Active,
            epoch: 2,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },


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

            node: "node-b".into(),
            status: Status::Active,
            epoch: 2,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },

            node: "node-b".into(),
            status: Status::Active,
            epoch: 2,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },


436	        Instance {
437	            node: "node-a".into(),
438	            status: Status::Dormant,
439	            epoch: 1,
440	            resources: None,
441	            last_active_ms: None,
442	            shell_adapters: Vec::new(),
443	            node_label: None,
444	            machine_id: None,
445	        },
446	    ];
447	    let runtime = merger_runtime();

        Instance {
            node: "node-a".into(),
            status: Status::Dormant,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: None,
            machine_id: None,
        },
    ];
    let runtime = merger_runtime();


            node_label: None,
            machine_id: None,
        },
    ];
    let runtime = merger_runtime();

            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    ];
    let runtime = merger_runtime();


            node_label: None,
            machine_id: None,
        },
    ];
    let runtime = merger_runtime();

            node_label: None,
            machine_id: None,
            endpoint_type: None,
        },
    ];
    let runtime = merger_runtime();


All listed sites done. Verify no Instance literals remain elsewhere (other crates, test files) by compiling spt-net first, then a workspace check.


Bash

grep -rn "registry::Instance {\|^\s*Instance {\|: Instance {" --include=*.rs crates/ | grep -v "pub struct Instance\|crates/spt-net/src/net/registry.rs:72"

Find any remaining Instance literals workspace-wide


crates/spt/src/cli.rs:7492:            spt_net::net::registry::Instance {
crates/spt/src/cli.rs:7855:                Instance {
crates/spt/src/wansend.rs:599:            Instance {
crates/spt/src/wansend.rs:910:                Instance {
crates/spt-daemon/src/notif.rs:673:                spt_net::net::registry::Instance {
crates/spt-daemon/src/presence.rs:218:        Instance {
crates/spt-daemon/src/reconcile.rs:50:        Instance {
crates/spt-daemon/src/registryhost.rs:813:            instance: Instance {
crates/spt-daemon/src/registryhost.rs:895:        Instance {
crates/spt-daemon/src/registryhost.rs:1206:                instance: Instance {
crates/spt-daemon/tests/dispatch.rs:597:        spt_net::net::registry::Instance {
crates/spt-daemon/tests/dispatch.rs:914:        instance: Instance {
crates/spt-daemon/tests/pump.rs:302:                instance: Instance {
crates/spt-daemon/tests/replicate.rs:148:        instance: Instance {
crates/spt-daemon/tests/sync.rs:425:        Instance {
crates/spt-daemon/tests/sync.rs:436:        Instance {
crates/spt-net/src/net/registry.rs:763:        Instance {
crates/spt-net/src/net/registry.rs:1182:            Instance {
crates/spt-net/src/net/replicate.rs:152:            instance: Instance {
crates/spt-net/src/net/wanmsg.rs:106:            instance: Instance {


4 more sites in spt/src (cli.rs, wansend.rs) — missed earlier. Read them.


7488	        std::fs::create_dir_all(&snap_dir).unwrap();
7489	        let mut reg = spt_net::net::registry::SubnetRegistry::new();
7490	        reg.merge_instance(
7491	            "claimed",
7492	            spt_net::net::registry::Instance {
7493	                node: "bb22".into(),
7494	                status: spt_net::net::registry::Status::Dormant,
7495	                epoch: 1,
7496	                resources: None,
7497	                last_active_ms: None,
7498	                shell_adapters: Vec::new(),
7499	                node_label: None,
7500	                machine_id: None,
7501	            },
7502	        );
7503	        std::fs::write(

        std::fs::create_dir_all(&snap_dir).unwrap();
        let mut reg = spt_net::net::registry::SubnetRegistry::new();
        reg.merge_instance(
            "claimed",
            spt_net::net::registry::Instance {
                node: "bb22".into(),
                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        );
        std::fs::write(


7851	        let mut reg = SubnetRegistry::new();
7852	        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
7853	            reg.merge_instance(
7854	                id,
7855	                Instance {
7856	                    node: node.into(),
7857	                    status: Status::Active,
7858	                    epoch: 1,
7859	                    resources: None,
7860	                    last_active_ms: None,
7861	                    shell_adapters: Vec::new(),
7862	                    node_label: None,
7863	                    machine_id: None,
7864	                },
7865	            );
7866	        }

        let mut reg = SubnetRegistry::new();
        for (id, node) in [("lea", "aa"), ("sergey", "bb"), ("lea2", "aa")] {
            reg.merge_instance(
                id,
                Instance {
                    node: node.into(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            );
        }


REQ survey + mint REQ-MSG-6

T1 Instance.endpoint_type field

T2 populate on advertise_local

T3 resolve at receive_wan funnel

T4 unit test matrix

Sweep + traceable + push


Edit

                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        );
        std::fs::write(

                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        );
        std::fs::write(


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

                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },
        );
        std::fs::write(

                status: spt_net::net::registry::Status::Dormant,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },
        );
        std::fs::write(


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            );
        }

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },
            );
        }


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                },
            );
        }

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: None,
                    machine_id: None,
                    endpoint_type: None,
                },
            );
        }


595	    fn reg_with(subnet: &str, id: &str, node: &str) -> BTreeMap<String, SubnetRegistry> {
596	        let mut reg = SubnetRegistry::new();
597	        reg.merge_instance(
598	            id,
599	            Instance {
600	                node: node.to_string(),
601	                status: Status::Active,
602	                epoch: 1,
603	                resources: None,
604	                last_active_ms: None,
605	                shell_adapters: Vec::new(),
606	                node_label: None,
607	                machine_id: None,
608	            },

    fn reg_with(subnet: &str, id: &str, node: &str) -> BTreeMap<String, SubnetRegistry> {
        let mut reg = SubnetRegistry::new();
        reg.merge_instance(
            id,
            Instance {
                node: node.to_string(),
                status: Status::Active,
                epoch: 1,
                resources: None,
                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },


906	        let labeled = |sub: &str, id: &str, node: &str, label: Option<&str>| {
907	            let mut reg = SubnetRegistry::new();
908	            reg.merge_instance(
909	                id,
910	                Instance {
911	                    node: node.to_string(),
912	                    status: Status::Active,
913	                    epoch: 1,
914	                    resources: None,
915	                    last_active_ms: None,
916	                    shell_adapters: Vec::new(),
917	                    node_label: label.map(str::to_string),
918	                    machine_id: None,
919	                },

        let labeled = |sub: &str, id: &str, node: &str, label: Option<&str>| {
            let mut reg = SubnetRegistry::new();
            reg.merge_instance(
                id,
                Instance {
                    node: node.to_string(),
                    status: Status::Active,
                    epoch: 1,
                    resources: None,
                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: label.map(str::to_string),
                    machine_id: None,
                },


                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },


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

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
            },

                last_active_ms: None,
                shell_adapters: Vec::new(),
                node_label: None,
                machine_id: None,
                endpoint_type: None,
            },


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: label.map(str::to_string),
                    machine_id: None,
                },

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: label.map(str::to_string),
                    machine_id: None,
                    endpoint_type: None,
                },


                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: label.map(str::to_string),
                    machine_id: None,
                },

                    last_active_ms: None,
                    shell_adapters: Vec::new(),
                    node_label: label.map(str::to_string),
                    machine_id: None,
                    endpoint_type: None,
                },


All literal sites updated. Compile-check workspace (rebuild from clean) to catch any miss.


Bash

cargo check --workspace --all-targets 2>&1 | grep -E "^error|^warning: unused|endpoint_type|missing field|Compiling spt|Finished" | tail -40; echo "EXIT:${PIPESTATUS[0]}"

Workspace check after T1


error[E0063]: missing field `endpoint_type` in initializer of `spt_net::net::registry::Instance`
     |                                                                      ^^^^^^^^ missing `endpoint_type`
error: could not compile `spt` (bin "spt" test) due to 1 previous error
EXIT:101


One missed site. Get its location.


cargo check --workspace --all-targets 2>&1 | grep -B2 "missing field \`endpoint_type\`" | head; echo "==="; cargo check --workspace --all-targets 2>&1 | grep -E "cli.rs|wansend.rs|\.rs:[0-9]+:[0-9]+" | grep -i instance | head

Locate missing field site


    Checking spt v0.4.2 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
error[E0063]: missing field `endpoint_type` in initializer of `spt_net::net::registry::Instance`
===


cargo check -p spt --all-targets 2>&1 | grep -A3 "E0063" | head -20

Get file:line of missing field in spt crate


error[E0063]: missing field `endpoint_type` in initializer of `spt_net::net::registry::Instance`
    --> crates\spt\src\cli.rs:7930:70
     |
7930 |         let inst = |node: &str, status: Status, label: Option<&str>| Instance {
--
For more information about this error, try `rustc --explain E0063`.
error: could not compile `spt` (bin "spt" test) due to 1 previous error


7930	        let inst = |node: &str, status: Status, label: Option<&str>| Instance {
7931	            node: node.into(),
7932	            status,
7933	            epoch: 1,
7934	            resources: None,
7935	            last_active_ms: None,
7936	            shell_adapters: Vec::new(),
7937	            node_label: label.map(str::to_string),
7938	            machine_id: None,
7939	        };
7940	        let mut reg = SubnetRegistry::new();
7941	        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));

        let inst = |node: &str, status: Status, label: Option<&str>| Instance {
            node: node.into(),
            status,
            epoch: 1,
            resources: None,
            last_active_ms: None,
            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
            machine_id: None,
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));


Edit

            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
            machine_id: None,
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));

            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
            machine_id: None,
            endpoint_type: None,
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));


            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
            machine_id: None,
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));

            shell_adapters: Vec::new(),
            node_label: label.map(str::to_string),
            machine_id: None,
            endpoint_type: None,
        };
        let mut reg = SubnetRegistry::new();
        reg.merge_instance("a1", inst("self00aa", Status::Active, Some("ME")));


Bash

cargo check --workspace --all-targets 2>&1 | grep -E "^error|missing field|Finished" | tail -20; echo "EXIT:${PIPESTATUS[0]}"

Re-check workspace after final site


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.55s
EXIT:0


T1 compiles clean. Bump REQ-MSG-6 to `[doc,impl]` (field is impl evidence), traceable, commit T1.


Edit

required_stages = ["doc"]  # activated 2026-06-13: M9 cross-node Gateway WAN (CONTEXT.md §Gateway posture rewrite, doc commit 30fea7f). impl/unit activate as the branch lands the Instance.endpoint_type field + advertise populate + receive_wan resolve + the origin matrix.

required_stages = ["doc", "impl"]  # activated 2026-06-13: M9 cross-node Gateway WAN. doc = CONTEXT.md §Gateway posture rewrite (commit 30fea7f); impl = Instance.endpoint_type field (T1) + advertise populate (T2) + receive_wan resolve (T3). unit activates with the origin matrix (T4).


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

title = "cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)"

-required_stages = ["doc"]  # activated 2026-06-13: M9 cross-node Gateway WAN (CONTEXT.md §Gateway posture rewrite, doc commit 30fea7f). impl/unit activate as the branch lands the Instance.endpoint_type field + advertise populate + receive_wan resolve + the origin matrix.

+required_stages = ["doc", "impl"]  # activated 2026-06-13: M9 cross-node Gateway WAN. doc = CONTEXT.md §Gateway posture rewrite (commit 30fea7f); impl = Instance.endpoint_type field (T1) + advertise populate (T2) + receive_wan resolve (T3). unit activates with the origin matrix (T4).

# ───────────────────────────── Networking / pairing ─────────────────────────


Bash

traceable-reqs check 2>&1 | grep -E "REQ-MSG-6|FAIL|MISSING|error" | head; echo "EXIT:$?"

traceable check T1


       Gateway type acceptance: a Gateway-typed perch binds (api bind --type, open type system — un-hardcode the live_agent default), advertises/addressable like any endpoint, owns shells (owner validation not agent-family-gated), subscribes to digests, and is the user-msg identity gate's user-backed origin (REQ-MSG-5); in-tree mock-gateway fixture (R-DOCS-2 pattern, no downstream adapter code). Cross-node WAN Gateway-origin (registry endpoint_type trust) tracked by REQ-MSG-6
  [OK] REQ-MSG-6  required: [doc, impl]  stages: +doc +impl -unit -int
EXIT:0


git add -A && git commit -q -m @'
feat(net): Instance.endpoint_type seam for cross-node Gateway user-msg (T1)

Additive serde-default Option<String> on the registry Instance — the
advertised endpoint type tag the WAN user-msg identity gate (REQ-MSG-6)
reads to honor a Gateway-typed origin. Rides the existing epoch lease +
RegistryUpdate replication like resources/node_label; no new channel, no
new merge key. An N-1 row (no field) parses clean and resolves to "not
provably user-backed" (the fail-closed re-stamp; rollout grace). Extends
REQ-INST-7 data model. All construction sites default to None; T2
populates it on advertise from the endpoint's own info.json state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'@ && echo COMMITTED

Commit T1


Error: Exit code 2
error: pathspec 'own' did not match any file(s) known to git
error: pathspec 'info.json' did not match any file(s) known to git
error: pathspec 'state.' did not match any file(s) known to git
/usr/bin/bash: eval: line 29: syntax error near unexpected token `newline'
/usr/bin/bash: eval: line 29: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>'


Bash here-string mangled the apostrophe. Use a commit message file.


Write

feat(net): Instance.endpoint_type seam for cross-node Gateway user-msg (T1)

Additive serde-default Option<String> on the registry Instance — the
advertised endpoint type tag the WAN user-msg identity gate (REQ-MSG-6)
reads to honor a Gateway-typed origin. Rides the existing epoch lease +
RegistryUpdate replication like resources/node_label; no new channel, no
new merge key. An N-1 row (no field) parses clean and resolves to "not
provably user-backed" (the fail-closed re-stamp; rollout grace). Extends
REQ-INST-7 data model. All construction sites default to None; T2
populates it on advertise from the endpoint own info.json state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>


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

feat(net): Instance.endpoint_type seam for cross-node Gateway user-msg (T1)

Additive serde-default Option<String> on the registry Instance — the
advertised endpoint type tag the WAN user-msg identity gate (REQ-MSG-6)
reads to honor a Gateway-typed origin. Rides the existing epoch lease +
RegistryUpdate replication like resources/node_label; no new channel, no
new merge key. An N-1 row (no field) parses clean and resolves to "not
provably user-backed" (the fail-closed re-stamp; rollout grace). Extends
REQ-INST-7 data model. All construction sites default to None; T2
populates it on advertise from the endpoint own info.json state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>


REQ survey + mint REQ-MSG-6

T1 Instance.endpoint_type field

T2 populate on advertise_local

T3 resolve at receive_wan funnel

T4 unit test matrix

Sweep + traceable + push


Bash

git add -A && git commit -q -F .git/COMMIT_MSG.tmp && rm -f .git/COMMIT_MSG.tmp && echo COMMITTED && git log --oneline -1

Commit T1 via message file


COMMITTED
0b79951 feat(net): Instance.endpoint_type seam for cross-node Gateway user-msg (T1)


Updated task #2 status


Updated task #3 status


T2: populate endpoint_type from info.state. Read the capture region.


384	            // Endpoint-authored blurb wins; the node seed fills the gap
385	            // (both-authored, REQ-INST-14). Rides the same epoch lease as
386	            // every instance update — a blurb edit reaches peers as an
387	            // ordered, epoch-bumped row.
388	            // [impl->REQ-INST-14]
389	            let info = spt_store::info::read_info(&perch_path);
390	            let resources = info
391	                .as_ref()
392	                .and_then(|i| i.resources.clone())
393	                .or_else(|| node_blurb.clone());
394	            // The presence datum (M5-D6a, REQ-PRES-1): the perch's heartbeat
395	            // stamp (`lifecycle::touch_active` — Active seats only; a resting
396	            // seat never stamps) rides the row like the blurb does — same
397	            // epoch lease, same visibility gate, no new channel. Subnet-wide
398	            // most-recently-active is a projection of these rows (D6b).
399	            // [impl->REQ-PRES-1]
400	            let last_active_ms = info.and_then(|i| i.last_active_ms);
401	            for sub in &subnets.subnets {
402	                let Ok(ep) = epoch.next_epoch() else { continue };
403	                let instance = Instance {
404	                    node: self.node_hex.clone(),
405	                    status,
406	                    epoch: ep,
407	                    resources: resources.clone(),
408	                    last_active_ms,
409	                    shell_adapters: shell_adapters.clone(),
410	                    // The node label rides every row this node authors
411	                    // (REQ-SUBNET-3) — set from the OS hostname at daemon
412	                    // startup, so a rename converges by re-advertisement.
413	                    // [impl->REQ-SUBNET-3]
414	                    node_label: self.node_label.clone(),
415	                    // The hashed machine id rides beside it (M8 decision
416	                    // 13/22) — the re-pair overwrite anchor.
417	                    // [impl->REQ-SUBNET-7]
418	                    machine_id: self.machine_id.clone(),
419	                    endpoint_type: None,
420	                };

            // 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(),
                    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(),
                    endpoint_type: None,
                };


Edit

            // [impl->REQ-PRES-1]
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {

            // [impl->REQ-PRES-1]
            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the
            // info.json `state` IS the bound endpoint type
            // (`EndpointType::as_tag` — `"gateway"` for a Gateway surface).
            // A node advertises ONLY its own endpoints' types; the cross-node
            // user-msg gate reads this at the receive funnel (T3). A recordless
            // perch advertises no type → the funnel fail-closes to a re-stamp.
            // [impl->REQ-MSG-6]
            let endpoint_type = info.as_ref().map(|i| i.state.clone());
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {


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

            // [impl->REQ-PRES-1]
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {

            // [impl->REQ-PRES-1]
            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the
            // info.json `state` IS the bound endpoint type
            // (`EndpointType::as_tag` — `"gateway"` for a Gateway surface).
            // A node advertises ONLY its own endpoints' types; the cross-node
            // user-msg gate reads this at the receive funnel (T3). A recordless
            // perch advertises no type → the funnel fail-closes to a re-stamp.
            // [impl->REQ-MSG-6]
            let endpoint_type = info.as_ref().map(|i| i.state.clone());
            let last_active_ms = info.and_then(|i| i.last_active_ms);
            for sub in &subnets.subnets {

// epoch lease, same visibility gate, no new channel. Subnet-wide

// most-recently-active is a projection of these rows (D6b).

+            // The endpoint's own advertised type tag (M9, REQ-MSG-6): the

+            // info.json `state` IS the bound endpoint type

+            // (`EndpointType::as_tag` — `"gateway"` for a Gateway surface).

+            // A node advertises ONLY its own endpoints' types; the cross-node

+            // user-msg gate reads this at the receive funnel (T3). A recordless

+            // perch advertises no type → the funnel fail-closes to a re-stamp.

+            let endpoint_type = info.as_ref().map(|i| i.state.clone());

let Ok(ep) = epoch.next_epoch() else { continue };


                    // [impl->REQ-SUBNET-7]
                    machine_id: self.machine_id.clone(),
                    endpoint_type: None,
                };

                    // [impl->REQ-SUBNET-7]
                    machine_id: self.machine_id.clone(),
                    // The endpoint's own type tag (REQ-MSG-6) — read by the
                    // cross-node user-msg gate, keyed by the proven node.
                    // [impl->REQ-MSG-6]
                    endpoint_type: endpoint_type.clone(),
                };


                    // [impl->REQ-SUBNET-7]
                    machine_id: self.machine_id.clone(),
                    endpoint_type: None,
                };

                    // [impl->REQ-SUBNET-7]
                    machine_id: self.machine_id.clone(),
                    // The endpoint's own type tag (REQ-MSG-6) — read by the
                    // cross-node user-msg gate, keyed by the proven node.
                    // [impl->REQ-MSG-6]
                    endpoint_type: endpoint_type.clone(),
                };

// 13/22) — the re-pair overwrite anchor.

-                    endpoint_type: None,

+                    // The endpoint's own type tag (REQ-MSG-6) — read by the

+                    // cross-node user-msg gate, keyed by the proven node.

+                    // [impl->REQ-MSG-6]

+                    endpoint_type: endpoint_type.clone(),

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


Build spt-daemon to verify T2.


Bash

cargo check -p spt-daemon 2>&1 | grep -E "^error|^warning: unused|Finished" | tail; echo "EXIT:${PIPESTATUS[0]}"

Check spt-daemon after T2


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 20.70s
EXIT:0


T2 compiles. Commit (stages unchanged).


git add -A && git commit -q -m "feat(daemon): advertise endpoint_type from the perch info.json state (T2)

advertise_local stamps each self-perch row with its own bound endpoint
type tag (info.json state == EndpointType::as_tag). A node advertises
ONLY its own endpoints' types; a recordless perch advertises no type so
the cross-node user-msg gate (T3) fail-closes to a re-stamp. Rides the
existing epoch lease like resources/last_active_ms.

// [impl->REQ-MSG-6]

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" && echo OK && git log --oneline -1

Commit T2


OK
0f84e25 feat(daemon): advertise endpoint_type from the perch info.json state (T2)


Updated task #4 status


<task-notification>
<task-id>boi3q4jal</task-id>
<tool-use-id>toolu_01JpmUEfWsGkN5FTWHrgxsFp</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\boi3q4jal.output</output-file>
<status>completed</status>
<summary>Background command "Watch PR #8 CI rerun" completed (exit code 0)</summary>
</task-notification>


T3. Add `RegistryHost::instances_of` (gather instances for an id across subnets).


166	    /// This node's stable name.
167	    pub fn node_hex(&self) -> &str {
168	        &self.node_hex
169	    }
170
171	    /// Run `f` with this host's **canonical per-node epoch** — the ONE

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

    /// Run `f` with this host's **canonical per-node epoch** — the ONE


Prompt:
<task-notification>
<task-id>boi3q4jal</task-id>
<tool-use-id>toolu_01JpmUEfWsGkN5FTWHrgxsFp</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\boi3q4jal.output</output-file>
<status>completed</status>
<summary>Background command "Watch PR #8 CI rerun" completed (exit code 0)</summary>
</task-notification>


PR #8 CI rerun watch exited 0 — green. Verify then ping doyle for G2.


Confirm PR #8 all green


twohost-a	skipping	0	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184584718
twohost-b	skipping	0	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184584885
n1-gate (self-hosted, Linux, kitsubito)	pass	1m17s	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184142494
n1-gate (self-hosted, Windows, hfenduleam)	pass	8m44s	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184144943
test (self-hosted, Linux, kitsubito)	pass	6m15s	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184142915
test (self-hosted, Windows, hfenduleam)	pass	7m24s	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184133653
traceability	pass	12s	https://github.com/SaberMage/spt-core/actions/runs/27463977028/job/81184134019
EXIT:0


PR #8 GREEN both runners (twohost skip is expected). Ping doyle for G2 close. Check owl send mechanism.


echo "OWL=$OWL"; echo "LIVE=$LIVE"; ls "$env:LOCALAPPDATA/spt" 2>/dev/null; printenv | grep -i owl 2>/dev/null | head

Find owl send binary path


OWL=C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe
LIVE=C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe live
LIVE=C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe live
OWL_SESSION_ID=6b7355ca-c59b-4b57-a4e8-b248ce4673c6
OWL=C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe


& "C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe" send doyle "G2 UPDATE — PR #8 CI GREEN both runners. The earlier Windows RED (test job, 1m41s) was NOT a code regression: hfenduleam disk-full (LNK1318 PDB / os-error-112 / 'no space on device') — C: had 2GB free, dev target dir had bloated to 396GB. cargo clean'd (374GB free now), re-ran failed jobs → all pass (test+n1-gate both runners + traceability; twohost skip expected, no [twohost] tag). Reconcile fix 77e0fb1 unaffected. G2 CI bar met — ready for your G2 close stamp. Meanwhile I've STARTED m9-gateway-wan per your directive: T1 (Instance.endpoint_type, 0b79951) + T2 (advertise populate, 0f84e25) committed, T3 funnel resolve in progress." 2>&1 | tail -5; echo "rc=${PIPESTATUS[0]}"

Ping doyle: PR#8 green + gateway-wan started


Error: Exit code 2
/usr/bin/bash: eval: line 4: syntax error near unexpected token `&'
/usr/bin/bash: eval: line 4: `& "C:/Users/decid/.claude/plugins/cache/cplugs/spt/1.11.25/owl.exe" send doyle "G2 UPDATE — PR #8 CI GREEN both runners. The earlier Windows RED (test job, 1m41s) was NOT a code regression: hfenduleam disk-full (LNK1318 PDB / os-error-112 / 'no space on device') — C: had 2GB free, dev target dir had bloated to 396GB. cargo clean'd (374GB free now), re-ran failed jobs → all pass (test+n1-gate both runners + traceability; twohost skip expected, no [twohost] tag). Reconcile fix 77e0fb1 unaffected. G2 CI bar met — ready for your G2 close stamp. Meanwhile I've STARTED m9-gateway-wan per your directive: T1 (Instance.endpoint_type, 0b79951) + T2 (advertise populate, 0f84e25) committed, T3 funnel resolve in progress." 2>&1 | tail -5; echo "rc=${PIPESTATUS[0]}"'


Use PowerShell for the `&` call operator.


PowerShell

& $env:OWL send doyle "G2 UPDATE — PR #8 CI GREEN both runners. The earlier Windows RED (test job 1m41s) was NOT a code regression: hfenduleam disk-full (LNK1318 PDB / os-error-112 / no space on device) — C: had 2GB free, dev target dir had bloated to 396GB. cargo clean'd (374GB free now), re-ran failed jobs => all pass (test+n1-gate both runners + traceability; twohost skip expected, no [twohost] tag). Reconcile fix 77e0fb1 unaffected. G2 CI bar met — ready for your G2 close stamp. Meanwhile I STARTED m9-gateway-wan per your directive: T1 Instance.endpoint_type (0b79951) + T2 advertise populate (0f84e25) committed, T3 funnel resolve in progress."

Ping doyle: PR#8 green + gateway-wan started


Command running in background with ID: bl2u6brpe. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\c570fe63-c413-4a17-bb14-ee75e02a53bd\tasks\bl2u6brpe.output. You will be notified when it completes. To check interim output, use Read on that file path.


Doyle ping sent (background). Continue T3 — add `instances_of` to RegistryHost.


Edit

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

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

    /// Every advertised [`Instance`] this host knows for `endpoint_id`, across
    /// all of its per-subnet registries (the WAN user-msg origin resolver reads
    /// these and keys by the proven node — REQ-MSG-6). Cloned out so the lock
    /// is released before the caller inspects the rows. The same id can hold
    /// rows on several nodes / in several subnets; the resolver filters to the
    /// QUIC-proven origin node.
    // [impl->REQ-MSG-6]
    pub fn instances_of(&self, endpoint_id: &str) -> Vec<Instance> {
        self.regs
            .lock()
            .unwrap()
            .values()
            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())
            .collect()
    }


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

    /// Every advertised [`Instance`] this host knows for `endpoint_id`, across
    /// all of its per-subnet registries (the WAN user-msg origin resolver reads
    /// these and keys by the proven node — REQ-MSG-6). Cloned out so the lock
    /// is released before the caller inspects the rows. The same id can hold
    /// rows on several nodes / in several subnets; the resolver filters to the
    /// QUIC-proven origin node.
    // [impl->REQ-MSG-6]
    pub fn instances_of(&self, endpoint_id: &str) -> Vec<Instance> {
        self.regs
            .lock()
            .unwrap()
            .values()
            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())
            .collect()
    }

+    /// Every advertised [`Instance`] this host knows for `endpoint_id`, across

+    /// all of its per-subnet registries (the WAN user-msg origin resolver reads

+    /// these and keys by the proven node — REQ-MSG-6). Cloned out so the lock

+    /// is released before the caller inspects the rows. The same id can hold

+    /// rows on several nodes / in several subnets; the resolver filters to the

+    pub fn instances_of(&self, endpoint_id: &str) -> Vec<Instance> {

+            .flat_map(|reg| reg.instances(endpoint_id).iter().cloned())

/// Run `f` with this host's **canonical per-node epoch** — the ONE

/// monotonic counter (identity/epoch.json) behind every locally-authored

/// registry row. Every other per-node-id producer on this node MUST mint


REQ survey + mint REQ-MSG-6

T1 Instance.endpoint_type field

T2 populate on advertise_local

T3 resolve at receive_wan funnel

T4 unit test matrix

Sweep + traceable + push


Add the pure resolver helper + thread registry into receive_wan. First verify `EndpointType::from_tag` handling of "gateway".