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

    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: PairCodeSubmit = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad pair-code-submit payload: {e}"))?;
        let host = self
            .net
            .get()
            .ok_or("net disabled: this broker was bound without a network host")?;
        // Read the held address, enforcing the wait-for-code timeout. A missing or
        // expired hold is a clean failure — the CLI re-runs the meet.
        let held = {
            let mut holds = self.pair_holds.lock().unwrap();
            match holds.get(&req.session_id) {
                Some(h) if h.deadline > Instant::now() => Some((h.subnet.clone(), h.addr.clone())),
                Some(_) => {
                    holds.remove(&req.session_id);
                    None
                }
                None => None,
            }
        };
        let Some((subnet, addr)) = held else {

    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: PairCodeSubmit = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad pair-code-submit payload: {e}"))?;
        // Read the held address FIRST, enforcing the wait-for-code timeout — a
        // missing or expired hold is a clean failure that needs no net (the CLI
        // re-runs the meet).
        let held = {
            let mut holds = self.pair_holds.lock().unwrap();
            match holds.get(&req.session_id) {
                Some(h) if hold_is_live(h.deadline, Instant::now()) => {
                    Some((h.subnet.clone(), h.addr.clone()))
                }
                Some(_) => {
                    holds.remove(&req.session_id);
                    None
                }
                None => None,
            }
        };
        let Some((subnet, addr)) = held else {

-            .ok_or("net disabled: this broker was bound without a network host")?;

-        // Read the held address, enforcing the wait-for-code timeout. A missing or

-        // expired hold is a clean failure — the CLI re-runs the meet.

+        // Read the held address FIRST, enforcing the wait-for-code timeout — a

+        // missing or expired hold is a clean failure that needs no net (the CLI

-                Some(h) if h.deadline > Instant::now() => Some((h.subnet.clone(), h.addr.clone())),

+                Some(h) if hold_is_live(h.deadline, Instant::now()) => {

+                    Some((h.subnet.clone(), h.addr.clone()))


2330	                        member_hex,
2331	                    })
2332	                    .expect("MetMember serializes"),
2333	                );
2334	                send_frame(send, &frame);
2335	            }
2336	            // A meet failure reuses the classified pair-joined reply shape (ok:
2337	            // false) — the CLI renders it before ever prompting for a code (the
2338	            // "dead subnet must not make the user fetch a code" path).
2339	            Err(fail) => send_frame(send, &pair_fail_frame(&fail)),
2340	        }
2341	        Ok(())
2342	    }
2343
2344	    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a
2345	    /// held meet session. Dials the HELD address (no re-search) and runs the
2346	    /// SPAKE2 ceremony. A wrong-code retry re-submits against the same session id
2347	    /// (the hold is KEPT — ceremony only); success or any other terminal verdict
2348	    /// (and an expired/unknown session) drops the hold.
2349	    // [impl->REQ-JOIN-TWO-PHASE]
2350	    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
2351	        let req: PairCodeSubmit = serde_json::from_value(env.payload)
2352	            .map_err(|e| format!("bad pair-code-submit payload: {e}"))?;
2353	        // Read the held address FIRST, enforcing the wait-for-code timeout — a
2354	        // missing or expired hold is a clean failure that needs no net (the CLI
2355	        // re-runs the meet).
2356	        let held = {
2357	            let mut holds = self.pair_holds.lock().unwrap();
2358	            match holds.get(&req.session_id) {
2359	                Some(h) if hold_is_live(h.deadline, Instant::now()) => {
2360	                    Some((h.subnet.clone(), h.addr.clone()))
2361	                }
2362	                Some(_) => {
2363	                    holds.remove(&req.session_id);
2364	                    None
2365	                }
2366	                None => None,
2367	            }
2368	        };
2369	        let Some((subnet, addr)) = held else {
2370	            let reply = PairJoinReply {
2371	                ok: false,
2372	                class: Some("other".to_string()),
2373	                retry_after_secs: None,
2374	                detail: "the meet expired (the 5-minute wait-for-code window lapsed) \
2375	                         — re-run `spt subnet join`"
2376	                    .to_string(),
2377	            };
2378	            send_frame(
2379	                send,
2380	                &Envelope::new(
2381	                    KIND_PAIR_JOINED,
2382	                    serde_json::to_value(reply).expect("PairJoinReply serializes"),
2383	                ),
2384	            );
2385	            return Ok(());
2386	        };
2387	        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {
2388	            Ok(outcome) => {
2389	                self.pair_holds.lock().unwrap().remove(&req.session_id);
2390	                Envelope::new(
2391	                    KIND_PAIR_JOINED,
2392	                    serde_json::to_value(PairJoinReply {
2393	                        ok: true,
2394	                        class: None,
2395	                        retry_after_secs: None,
2396	                        detail: outcome,
2397	                    })
2398	                    .expect("PairJoinReply serializes"),
2399	                )

                        member_hex,
                    })
                    .expect("MetMember serializes"),
                );
                send_frame(send, &frame);
            }
            // A meet failure reuses the classified pair-joined reply shape (ok:
            // false) — the CLI renders it before ever prompting for a code (the
            // "dead subnet must not make the user fetch a code" path).
            Err(fail) => send_frame(send, &pair_fail_frame(&fail)),
        }
        Ok(())
    }

    /// Phase 2 of the two-phase join (REQ-JOIN-TWO-PHASE): submit the code for a
    /// held meet session. Dials the HELD address (no re-search) and runs the
    /// SPAKE2 ceremony. A wrong-code retry re-submits against the same session id
    /// (the hold is KEPT — ceremony only); success or any other terminal verdict
    /// (and an expired/unknown session) drops the hold.
    // [impl->REQ-JOIN-TWO-PHASE]
    fn dispatch_pair_code_submit(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: PairCodeSubmit = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad pair-code-submit payload: {e}"))?;
        // Read the held address FIRST, enforcing the wait-for-code timeout — a
        // missing or expired hold is a clean failure that needs no net (the CLI
        // re-runs the meet).
        let held = {
            let mut holds = self.pair_holds.lock().unwrap();
            match holds.get(&req.session_id) {
                Some(h) if hold_is_live(h.deadline, Instant::now()) => {
                    Some((h.subnet.clone(), h.addr.clone()))
                }
                Some(_) => {
                    holds.remove(&req.session_id);
                    None
                }
                None => None,
            }
        };
        let Some((subnet, addr)) = held else {
            let reply = PairJoinReply {
                ok: false,
                class: Some("other".to_string()),
                retry_after_secs: None,
                detail: "the meet expired (the 5-minute wait-for-code window lapsed) \
                         — re-run `spt subnet join`"
                    .to_string(),
            };
            send_frame(
                send,
                &Envelope::new(
                    KIND_PAIR_JOINED,
                    serde_json::to_value(reply).expect("PairJoinReply serializes"),
                ),
            );
            return Ok(());
        };
        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {
            Ok(outcome) => {
                self.pair_holds.lock().unwrap().remove(&req.session_id);
                Envelope::new(
                    KIND_PAIR_JOINED,
                    serde_json::to_value(PairJoinReply {
                        ok: true,
                        class: None,
                        retry_after_secs: None,
                        detail: outcome,
                    })
                    .expect("PairJoinReply serializes"),
                )


Edit

            return Ok(());
        };
        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {
            Ok(outcome) => {
                self.pair_holds.lock().unwrap().remove(&req.session_id);

            return Ok(());
        };
        let host = self
            .net
            .get()
            .ok_or("net disabled: this broker was bound without a network host")?;
        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {
            Ok(outcome) => {
                self.pair_holds.lock().unwrap().remove(&req.session_id);


            return Ok(());
        };
        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {
            Ok(outcome) => {
                self.pair_holds.lock().unwrap().remove(&req.session_id);

            return Ok(());
        };
        let host = self
            .net
            .get()
            .ok_or("net disabled: this broker was bound without a network host")?;
        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {
            Ok(outcome) => {
                self.pair_holds.lock().unwrap().remove(&req.session_id);

+            .ok_or("net disabled: this broker was bound without a network host")?;


2402	                    .expect("PairJoinReply serializes"),
2403	                )
2404	            }
2405	            Err(fail) => {
2406	                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony
2407	                // against the same held address); every other verdict is terminal.
2408	                if !matches!(fail, crate::pairhost::JoinFail::WrongCode) {
2409	                    self.pair_holds.lock().unwrap().remove(&req.session_id);
2410	                }
2411	                pair_fail_frame(&fail)
2412	            }
2413	        };
2414	        send_frame(send, &frame);
2415	        Ok(())
2416	    }
2417
2418	    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once
2419	    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted
2420	    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped
2421	    /// replay is answered with the stream the first apply opened.
2422	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
2423	    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {

                    .expect("PairJoinReply serializes"),
                )
            }
            Err(fail) => {
                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony
                // against the same held address); every other verdict is terminal.
                if !matches!(fail, crate::pairhost::JoinFail::WrongCode) {
                    self.pair_holds.lock().unwrap().remove(&req.session_id);
                }
                pair_fail_frame(&fail)
            }
        };
        send_frame(send, &frame);
        Ok(())
    }

    /// Open a bidi stream on a held connection (D4b). Journaled exactly-once
    /// under the same discipline as [`Self::dispatch_net_dial`]: the minted
    /// `stream_id` is recorded inside the `apply_once` closure, and a deduped
    /// replay is answered with the stream the first apply opened.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {


            Err(fail) => {
                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony
                // against the same held address); every other verdict is terminal.
                if !matches!(fail, crate::pairhost::JoinFail::WrongCode) {
                    self.pair_holds.lock().unwrap().remove(&req.session_id);
                }
                pair_fail_frame(&fail)
            }

            Err(fail) => {
                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony
                // against the same held address); every other verdict is terminal.
                if !fail_keeps_hold(&fail) {
                    self.pair_holds.lock().unwrap().remove(&req.session_id);
                }
                pair_fail_frame(&fail)
            }


            Err(fail) => {
                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony
                // against the same held address); every other verdict is terminal.
                if !matches!(fail, crate::pairhost::JoinFail::WrongCode) {
                    self.pair_holds.lock().unwrap().remove(&req.session_id);
                }
                pair_fail_frame(&fail)
            }

            Err(fail) => {
                // Keep the hold ONLY for a wrong-code retry (re-runs the ceremony
                // against the same held address); every other verdict is terminal.
                if !fail_keeps_hold(&fail) {
                    self.pair_holds.lock().unwrap().remove(&req.session_id);
                }
                pair_fail_frame(&fail)
            }

-                if !matches!(fail, crate::pairhost::JoinFail::WrongCode) {

+                if !fail_keeps_hold(&fail) {


Now add the `hold_is_live` + `fail_keeps_hold` pure helpers next to `pair_fail_frame`.


/// Render a [`JoinFail`](crate::pairhost::JoinFail) as the classified
/// `pair-joined` failure frame the CLI's guided UX reads — single-sourced so the
/// one-shot join, the meet phase, and the code phase all classify identically.
fn pair_fail_frame(fail: &crate::pairhost::JoinFail) -> Envelope {

/// Whether a held meet is still within its wait-for-code window
/// (REQ-JOIN-TWO-PHASE). Pure (the testable core of the timeout).
fn hold_is_live(deadline: Instant, now: Instant) -> bool {
    deadline > now
}

/// Whether a failed code submission should KEEP the held meet for another try
/// (REQ-JOIN-TWO-PHASE): only a wrong code — the operator re-enters it and the
/// ceremony re-runs against the same held address (no re-search). Every other
/// verdict (rate-limited, clock-skew, unknown-subnet, plumbing) is terminal.
fn fail_keeps_hold(fail: &crate::pairhost::JoinFail) -> bool {
    matches!(fail, crate::pairhost::JoinFail::WrongCode)
}

/// Render a [`JoinFail`](crate::pairhost::JoinFail) as the classified
/// `pair-joined` failure frame the CLI's guided UX reads — single-sourced so the
/// one-shot join, the meet phase, and the code phase all classify identically.
fn pair_fail_frame(fail: &crate::pairhost::JoinFail) -> Envelope {


/// Whether a held meet is still within its wait-for-code window
/// (REQ-JOIN-TWO-PHASE). Pure (the testable core of the timeout).
fn hold_is_live(deadline: Instant, now: Instant) -> bool {
    deadline > now
}

/// Whether a failed code submission should KEEP the held meet for another try
/// (REQ-JOIN-TWO-PHASE): only a wrong code — the operator re-enters it and the
/// ceremony re-runs against the same held address (no re-search). Every other
/// verdict (rate-limited, clock-skew, unknown-subnet, plumbing) is terminal.
fn fail_keeps_hold(fail: &crate::pairhost::JoinFail) -> bool {
    matches!(fail, crate::pairhost::JoinFail::WrongCode)
}

/// Render a [`JoinFail`](crate::pairhost::JoinFail) as the classified
/// `pair-joined` failure frame the CLI's guided UX reads — single-sourced so the
/// one-shot join, the meet phase, and the code phase all classify identically.
fn pair_fail_frame(fail: &crate::pairhost::JoinFail) -> Envelope {

/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).

const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);

+/// Whether a held meet is still within its wait-for-code window

+/// (REQ-JOIN-TWO-PHASE). Pure (the testable core of the timeout).

+fn hold_is_live(deadline: Instant, now: Instant) -> bool {

+/// Whether a failed code submission should KEEP the held meet for another try

+/// (REQ-JOIN-TWO-PHASE): only a wrong code — the operator re-enters it and the

+/// ceremony re-runs against the same held address (no re-search). Every other

+/// verdict (rate-limited, clock-skew, unknown-subnet, plumbing) is terminal.

+fn fail_keeps_hold(fail: &crate::pairhost::JoinFail) -> bool {

+    matches!(fail, crate::pairhost::JoinFail::WrongCode)


Adding broker unit test for the hold decisions.


        // A viewer keeping up (room in the queue) is NOT evicted.
        let (tx2, _rx2) = sync_channel::<u8>(1);
        assert!(
            !viewer_send_evicts(&tx2.try_send(9)),
            "a viewer with room stays attached"
        );
    }

        // A viewer keeping up (room in the queue) is NOT evicted.
        let (tx2, _rx2) = sync_channel::<u8>(1);
        assert!(
            !viewer_send_evicts(&tx2.try_send(9)),
            "a viewer with room stays attached"
        );
    }

    // [unit->REQ-JOIN-TWO-PHASE] the two-phase join's held-meet state-machine
    // decisions (pure): a hold is usable only WITHIN its wait-for-code window,
    // and a failed code submit keeps the hold for another try ONLY on a wrong
    // code (every other verdict is terminal — the ceremony-only retry contract).
    #[test]
    fn pair_hold_lifecycle_decisions() {
        use crate::pairhost::JoinFail;
        let now = Instant::now();
        assert!(
            hold_is_live(now + Duration::from_secs(1), now),
            "a future deadline is live"
        );
        assert!(
            !hold_is_live(now, now + Duration::from_secs(1)),
            "a lapsed deadline is dead (expired meet → re-run join)"
        );

        assert!(
            fail_keeps_hold(&JoinFail::WrongCode),
            "wrong code keeps the held address for a ceremony-only retry"
        );
        for terminal in [
            JoinFail::RateLimited(Some(5)),
            JoinFail::RateLimited(None),
            JoinFail::ClockSkew,
            JoinFail::UnknownSubnet("x".into()),
            JoinFail::NoSeedHolder("x".into()),
            JoinFail::Other("x".into()),
        ] {
            assert!(
                !fail_keeps_hold(&terminal),
                "{terminal:?} is terminal — the hold is dropped"
            );
        }
    }


        // A viewer keeping up (room in the queue) is NOT evicted.
        let (tx2, _rx2) = sync_channel::<u8>(1);
        assert!(
            !viewer_send_evicts(&tx2.try_send(9)),
            "a viewer with room stays attached"
        );
    }

    // [unit->REQ-JOIN-TWO-PHASE] the two-phase join's held-meet state-machine
    // decisions (pure): a hold is usable only WITHIN its wait-for-code window,
    // and a failed code submit keeps the hold for another try ONLY on a wrong
    // code (every other verdict is terminal — the ceremony-only retry contract).
    #[test]
    fn pair_hold_lifecycle_decisions() {
        use crate::pairhost::JoinFail;
        let now = Instant::now();
        assert!(
            hold_is_live(now + Duration::from_secs(1), now),
            "a future deadline is live"
        );
        assert!(
            !hold_is_live(now, now + Duration::from_secs(1)),
            "a lapsed deadline is dead (expired meet → re-run join)"
        );

        assert!(
            fail_keeps_hold(&JoinFail::WrongCode),
            "wrong code keeps the held address for a ceremony-only retry"
        );
        for terminal in [
            JoinFail::RateLimited(Some(5)),
            JoinFail::RateLimited(None),
            JoinFail::ClockSkew,
            JoinFail::UnknownSubnet("x".into()),
            JoinFail::NoSeedHolder("x".into()),
            JoinFail::Other("x".into()),
        ] {
            assert!(
                !fail_keeps_hold(&terminal),
                "{terminal:?} is terminal — the hold is dropped"
            );
        }
    }

+    // [unit->REQ-JOIN-TWO-PHASE] the two-phase join's held-meet state-machine

+    // decisions (pure): a hold is usable only WITHIN its wait-for-code window,

+    // and a failed code submit keeps the hold for another try ONLY on a wrong

+    // code (every other verdict is terminal — the ceremony-only retry contract).

+    fn pair_hold_lifecycle_decisions() {

+            hold_is_live(now + Duration::from_secs(1), now),

+            "a future deadline is live"

+            !hold_is_live(now, now + Duration::from_secs(1)),

+            "a lapsed deadline is dead (expired meet → re-run join)"

+            fail_keeps_hold(&JoinFail::WrongCode),

+            "wrong code keeps the held address for a ceremony-only retry"

+            JoinFail::RateLimited(Some(5)),

+            JoinFail::RateLimited(None),

+            JoinFail::UnknownSubnet("x".into()),

+            JoinFail::NoSeedHolder("x".into()),

+            JoinFail::Other("x".into()),

+                !fail_keeps_hold(&terminal),

+                "{terminal:?} is terminal — the hold is dropped"

// ── v0.13.0 — REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT: the broker side of the

//    skip-to-live contract. (1) `viewer_writer` emits a KIND_VIEWER_EVICTED

//    marker as its FINAL frame ONLY when the sink was evicted for overflow (the


Adding a serde round-trip unit test for the new wire types. Checking msg.rs test module.


858:        serde_json::to_value(OutputEvent {
872:        serde_json::to_value(SubscribedReply {
884:        serde_json::to_value(SizeEvent {
900:        serde_json::to_value(ViewerEvictedEvent { session_id })
910:        serde_json::to_value(DisplacedEvent {
923:        serde_json::to_value(AppliedEvent {
940:        serde_json::to_value(EndpointInjected {
954:        serde_json::to_value(NetStreamData {
967:        serde_json::to_value(NetStreamEof {
981:        serde_json::to_value(ev).expect("NetPresenceEvent serializes"),

[Showing results with pagination = limit: 10]


No matches found

[Showing results with pagination = offset: 990]


Bash

grep -n "mod tests\|#\[cfg(test)\]" crates/spt-daemon/src/msg.rs | head; echo "--- total lines ---"; wc -l crates/spt-daemon/src/msg.rs


1009:#[cfg(test)]
1010:mod tests {
--- total lines ---
1273 crates/spt-daemon/src/msg.rs


1009	#[cfg(test)]
1010	mod tests {
1011	    use super::*;
1012	    use serde_json::json;
1013
1014	    // [unit->REQ-DAEMON-2] bytes round-trip through the base64 payload encoding.
1015	    #[test]
1016	    fn bytes_round_trip_through_payload() {
1017	        let raw = b"\x1b[6n binary\x00\xff bytes";
1018	        let enc = encode_bytes(raw);
1019	        assert_eq!(decode_bytes(&enc).unwrap(), raw);
1020	    }
1021
1022	    // [unit->REQ-DAEMON-2] a spawn request decodes; a newer brain's extra field
1023	    // is tolerated (forward-compat carries into the message catalog, not just
1024	    // the envelope).
1025	    #[test]
1026	    fn spawn_req_tolerates_unknown_field() {
1027	        let wire = json!({
1028	            "program": "cat",
1029	            "args": [],
1030	            "rows": 24,
1031	            "cols": 80,
1032	            "env_added_in_a_future_release": { "TERM": "xterm" }
1033	        });
1034	        let req: SpawnReq = serde_json::from_value(wire).unwrap();
1035	        assert_eq!(req.program, "cat");
1036	        assert_eq!(req.rows, 24);
1037	    }
1038

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

    // [unit->REQ-DAEMON-2] bytes round-trip through the base64 payload encoding.
    #[test]
    fn bytes_round_trip_through_payload() {
        let raw = b"\x1b[6n binary\x00\xff bytes";
        let enc = encode_bytes(raw);
        assert_eq!(decode_bytes(&enc).unwrap(), raw);
    }

    // [unit->REQ-DAEMON-2] a spawn request decodes; a newer brain's extra field
    // is tolerated (forward-compat carries into the message catalog, not just
    // the envelope).
    #[test]
    fn spawn_req_tolerates_unknown_field() {
        let wire = json!({
            "program": "cat",
            "args": [],
            "rows": 24,
            "cols": 80,
            "env_added_in_a_future_release": { "TERM": "xterm" }
        });
        let req: SpawnReq = serde_json::from_value(wire).unwrap();
        assert_eq!(req.program, "cat");
        assert_eq!(req.rows, 24);
    }


Edit

    // [unit->REQ-DAEMON-2] a spawn request decodes; a newer brain's extra field
    // is tolerated (forward-compat carries into the message catalog, not just
    // the envelope).
    #[test]
    fn spawn_req_tolerates_unknown_field() {

    // [unit->REQ-JOIN-TWO-PHASE] the two-phase join wire types round-trip and a
    // newer peer's extra field is tolerated (forward-compat) — the meet request,
    // the met-member reply (session id + member hex), and the code submit.
    #[test]
    fn two_phase_join_wire_round_trips() {
        let meet: PairMeetReq =
            serde_json::from_value(serde_json::to_value(PairMeetReq { subnet: "home".into() }).unwrap())
                .unwrap();
        assert_eq!(meet.subnet, "home");

        let met: MetMember = serde_json::from_value(
            serde_json::to_value(MetMember {
                session_id: 42,
                member_hex: "abcd".into(),
            })
            .unwrap(),
        )
        .unwrap();
        assert_eq!((met.session_id, met.member_hex.as_str()), (42, "abcd"));

        // Forward-compat: an unknown field from a newer CLI is tolerated.
        let submit: PairCodeSubmit = serde_json::from_value(json!({
            "session_id": 7,
            "code": "123456",
            "future_field": true
        }))
        .unwrap();
        assert_eq!((submit.session_id, submit.code.as_str()), (7, "123456"));
    }

    // [unit->REQ-DAEMON-2] a spawn request decodes; a newer brain's extra field
    // is tolerated (forward-compat carries into the message catalog, not just
    // the envelope).
    #[test]
    fn spawn_req_tolerates_unknown_field() {


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

    // [unit->REQ-JOIN-TWO-PHASE] the two-phase join wire types round-trip and a
    // newer peer's extra field is tolerated (forward-compat) — the meet request,
    // the met-member reply (session id + member hex), and the code submit.
    #[test]
    fn two_phase_join_wire_round_trips() {
        let meet: PairMeetReq =
            serde_json::from_value(serde_json::to_value(PairMeetReq { subnet: "home".into() }).unwrap())
                .unwrap();
        assert_eq!(meet.subnet, "home");

        let met: MetMember = serde_json::from_value(
            serde_json::to_value(MetMember {
                session_id: 42,
                member_hex: "abcd".into(),
            })
            .unwrap(),
        )
        .unwrap();
        assert_eq!((met.session_id, met.member_hex.as_str()), (42, "abcd"));

        // Forward-compat: an unknown field from a newer CLI is tolerated.
        let submit: PairCodeSubmit = serde_json::from_value(json!({
            "session_id": 7,
            "code": "123456",
            "future_field": true
        }))
        .unwrap();
        assert_eq!((submit.session_id, submit.code.as_str()), (7, "123456"));
    }

    // [unit->REQ-DAEMON-2] a spawn request decodes; a newer brain's extra field
    // is tolerated (forward-compat carries into the message catalog, not just
    // the envelope).
    #[test]
    fn spawn_req_tolerates_unknown_field() {

+    // [unit->REQ-JOIN-TWO-PHASE] the two-phase join wire types round-trip and a

+    // newer peer's extra field is tolerated (forward-compat) — the meet request,

+    // the met-member reply (session id + member hex), and the code submit.

+    fn two_phase_join_wire_round_trips() {

+            serde_json::from_value(serde_json::to_value(PairMeetReq { subnet: "home".into() }).unwrap())

+        assert_eq!(meet.subnet, "home");

+        let met: MetMember = serde_json::from_value(

+            serde_json::to_value(MetMember {

+                member_hex: "abcd".into(),

+        assert_eq!((met.session_id, met.member_hex.as_str()), (42, "abcd"));

+        // Forward-compat: an unknown field from a newer CLI is tolerated.

+        let submit: PairCodeSubmit = serde_json::from_value(json!({

+        assert_eq!((submit.session_id, submit.code.as_str()), (7, "123456"));


Adding the broker-IPC int test (unknown-session path) to pairjoin.rs + dual-tagging the existing held-addr ceremony test.


#[test]
fn daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end() {

// [int->REQ-JOIN-TWO-PHASE] phase 2 of the two-phase join, end to end: the
// address a MEET resolves (here supplied directly — exactly what the broker
// HOLDS between phases) drives the ceremony to completion with the code entered
// AFTER (`join_ceremony(.., Some(addr))` is precisely what `dispatch_pair_code_submit`
// runs against the held address — no re-search). The existing close-race
// regression assertion (REQ-SUBNET-2) doubles as the held-address ceremony proof.
#[test]
fn daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end() {


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

// [int->REQ-JOIN-TWO-PHASE] phase 2 of the two-phase join, end to end: the
// address a MEET resolves (here supplied directly — exactly what the broker
// HOLDS between phases) drives the ceremony to completion with the code entered
// AFTER (`join_ceremony(.., Some(addr))` is precisely what `dispatch_pair_code_submit`
// runs against the held address — no re-search). The existing close-race
// regression assertion (REQ-SUBNET-2) doubles as the held-address ceremony proof.
#[test]
fn daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end() {

//! M7 D3/D5 regression — the daemon-hosted pairing responder serves a real
//! joiner end to end over loopback: NetHost binds the pairing ALPN beside
//! the trusted surface, the accept loop routes the ceremony to
//! `pairhost::respond`, and `join_ceremony` (the `pair-join` broker leg)
//! completes against it — INCLUDING reading the final Done+Seed frames.
//!
//! That last clause is the regression: the first cross-host run of the
//! product-surface rung failed because the responder dropped the connection
//! the instant the ceremony returned — the QUIC CLOSE discarded the unacked
//! Done/Seed frames and the joiner read "connection lost" instead of its
//! seed, every attempt (`pairhost` now waits out the joiner's close under
//! `CLOSE_GRACE`). A loopback joiner exercises the same frame/close ordering
//! without the rig.
//!
//! Shared-home caveat: responder and joiner run in ONE process, so they
//! share SPT_HOME stores. The seed the responder serves is therefore already
//! "held" by the joiner's store (`add_joined` → `Exists` is the documented
//! idempotent re-pair path), and the trust writes race on one file — so the
//! assertion is the ceremony VERDICT (`Ok`), which can only happen if the
//! joiner read the full final exchange.
//
// [int->REQ-SUBNET-2]

use std::time::{SystemTime, UNIX_EPOCH};

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::pairhost::join_ceremony;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::SPT_PAIR_ALPN;
use spt_proto::identity::Identity;
use spt_store::subnet::SubnetStore;

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_secs()
}

#[test]
fn daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end() {
    let home = tempfile::tempdir().expect("temp home");
    std::env::set_var("SPT_HOME", home.path());
    // Hermetic ceremony clock (REQ-PAIR-8): no real NTP probe from a test —
    // the offset falls back to the system clock, exactly the offline path.
    std::env::set_var("SPT_NTP_SERVER", "off");

    // The responder side is a stock member: the subnet record exists, the
    // NetHost is up — nothing pairing-specific is armed (decision 8).
    let mut subnets = SubnetStore::load();
    let seed = [7u8; 20];
    subnets
        .add_joined("pairjoin", seed, 1)
        .expect("seed subnet");
    subnets.save().expect("save subnets");

    let id_b = Identity::from_seed(&[2u8; 32]);
    let host = NetHost::start(NetConfig {
        identity: id_b,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    })
    .expect("net host start");
    let addr_b = host.addr();

    // The joiner leg, exactly as the broker `pair-join` arm runs it — its
    // own node identity, the current code, the explicit responder address
    // (the same static-address seam the twohost rig uses).
    let id_a = Identity::from_seed(&[3u8; 32]);
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    let outcome = rt.block_on(async {
        let ep_a = NetEndpoint::bind_scoped(
            &id_a,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Loopback,
        )
        .await
        .expect("joiner endpoint bind");
        let code = TotpSeed::from_bytes(seed).code_at(now_secs());
        let out = join_ceremony(&ep_a, "pairjoin", &code, Some(addr_b)).await;
        ep_a.close().await;
        out
    });
    assert!(
        outcome.is_ok(),
        "the joiner must complete the ceremony INCLUDING the final Done+Seed \
         frames (the close-race regression), got: {outcome:?}"
    );
}

+// [int->REQ-JOIN-TWO-PHASE] phase 2 of the two-phase join, end to end: the

+// address a MEET resolves (here supplied directly — exactly what the broker

+// HOLDS between phases) drives the ceremony to completion with the code entered

+// AFTER (`join_ceremony(.., Some(addr))` is precisely what `dispatch_pair_code_submit`

+// runs against the held address — no re-search). The existing close-race

+// regression assertion (REQ-SUBNET-2) doubles as the held-address ceremony proof.


use std::time::{SystemTime, UNIX_EPOCH};

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::pairhost::join_ceremony;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::SPT_PAIR_ALPN;
use spt_proto::identity::Identity;
use spt_store::subnet::SubnetStore;

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_secs()
}

use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use spt_daemon::brain::Brain;
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::pairhost::join_ceremony;
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::SPT_PAIR_ALPN;
use spt_proto::identity::Identity;
use spt_store::subnet::SubnetStore;

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_secs()
}

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

/// A net-enabled broker (hermetic loopback host) serving on a background thread —
/// the brain↔broker IPC harness, mirroring `presence.rs`.
fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(NetConfig {
        identity: Identity::generate(),
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    })
    .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");
}

// [int->REQ-JOIN-TWO-PHASE] the two-phase code phase over the REAL broker IPC: a
// `pair-code-submit` for an unknown/expired meet session fails CLEANLY (no panic,
// no hang) with the re-run-the-join guidance — exercising brain.pair_submit_code
// → KIND_PAIR_CODE_SUBMIT → dispatch_pair_code_submit's hold-miss path → the
// classified pair-joined reply, all on the wire. (The happy path's held-address
// ceremony is covered by the end-to-end test above; the live rendezvous MEET
// needs real discovery and rides the two-host rig.)
#[test]
fn pair_code_submit_for_an_unknown_session_fails_cleanly() {
    let dir = tempfile::tempdir().expect("temp dir");
    let name = unique_socket();
    let _broker = net_broker(&name, dir.path());

    let mut brain = connect_retry(&name);
    let reply = brain
        .pair_submit_code(999_999, "000000")
        .expect("submit returns a reply, not an IPC error");
    assert!(!reply.ok, "an unknown meet session cannot join");
    assert!(
        reply.detail.contains("expired") || reply.detail.contains("re-run"),
        "the failure guides the operator to re-run the join, got: {}",
        reply.detail
    );
}


use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use spt_daemon::brain::Brain;
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::pairhost::join_ceremony;
use spt_daemon::Broker;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::SPT_PAIR_ALPN;
use spt_proto::identity::Identity;
use spt_store::subnet::SubnetStore;

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_secs()
}

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

/// A net-enabled broker (hermetic loopback host) serving on a background thread —
/// the brain↔broker IPC harness, mirroring `presence.rs`.
fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(NetConfig {
        identity: Identity::generate(),
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    })
    .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");
}

// [int->REQ-JOIN-TWO-PHASE] the two-phase code phase over the REAL broker IPC: a
// `pair-code-submit` for an unknown/expired meet session fails CLEANLY (no panic,
// no hang) with the re-run-the-join guidance — exercising brain.pair_submit_code
// → KIND_PAIR_CODE_SUBMIT → dispatch_pair_code_submit's hold-miss path → the
// classified pair-joined reply, all on the wire. (The happy path's held-address
// ceremony is covered by the end-to-end test above; the live rendezvous MEET
// needs real discovery and rides the two-host rig.)
#[test]
fn pair_code_submit_for_an_unknown_session_fails_cleanly() {
    let dir = tempfile::tempdir().expect("temp dir");
    let name = unique_socket();
    let _broker = net_broker(&name, dir.path());

    let mut brain = connect_retry(&name);
    let reply = brain
        .pair_submit_code(999_999, "000000")
        .expect("submit returns a reply, not an IPC error");
    assert!(!reply.ok, "an unknown meet session cannot join");
    assert!(
        reply.detail.contains("expired") || reply.detail.contains("re-run"),
        "the failure guides the operator to re-run the join, got: {}",
        reply.detail
    );
}

//! M7 D3/D5 regression — the daemon-hosted pairing responder serves a real
//! joiner end to end over loopback: NetHost binds the pairing ALPN beside
//! the trusted surface, the accept loop routes the ceremony to
//! `pairhost::respond`, and `join_ceremony` (the `pair-join` broker leg)
//! completes against it — INCLUDING reading the final Done+Seed frames.
//!
//! That last clause is the regression: the first cross-host run of the
//! product-surface rung failed because the responder dropped the connection
//! the instant the ceremony returned — the QUIC CLOSE discarded the unacked
//! Done/Seed frames and the joiner read "connection lost" instead of its
//! seed, every attempt (`pairhost` now waits out the joiner's close under
//! `CLOSE_GRACE`). A loopback joiner exercises the same frame/close ordering
//! without the rig.
//!
//! Shared-home caveat: responder and joiner run in ONE process, so they
//! share SPT_HOME stores. The seed the responder serves is therefore already
//! "held" by the joiner's store (`add_joined` → `Exists` is the documented
//! idempotent re-pair path), and the trust writes race on one file — so the
//! assertion is the ceremony VERDICT (`Ok`), which can only happen if the
//! joiner read the full final exchange.
//
// [int->REQ-SUBNET-2]

use std::time::{SystemTime, UNIX_EPOCH};

use spt_daemon::nethost::{NetConfig, NetHost};
use spt_daemon::pairhost::join_ceremony;
use spt_net::net::endpoint::{BindScope, LocalDiscovery, NetEndpoint, RelayPolicy};
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::SPT_PAIR_ALPN;
use spt_proto::identity::Identity;
use spt_store::subnet::SubnetStore;

fn now_secs() -> u64 {
    SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("clock")
        .as_secs()
}

// [int->REQ-JOIN-TWO-PHASE] phase 2 of the two-phase join, end to end: the
// address a MEET resolves (here supplied directly — exactly what the broker
// HOLDS between phases) drives the ceremony to completion with the code entered
// AFTER (`join_ceremony(.., Some(addr))` is precisely what `dispatch_pair_code_submit`
// runs against the held address — no re-search). The existing close-race
// regression assertion (REQ-SUBNET-2) doubles as the held-address ceremony proof.
#[test]
fn daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end() {
    let home = tempfile::tempdir().expect("temp home");
    std::env::set_var("SPT_HOME", home.path());
    // Hermetic ceremony clock (REQ-PAIR-8): no real NTP probe from a test —
    // the offset falls back to the system clock, exactly the offline path.
    std::env::set_var("SPT_NTP_SERVER", "off");

    // The responder side is a stock member: the subnet record exists, the
    // NetHost is up — nothing pairing-specific is armed (decision 8).
    let mut subnets = SubnetStore::load();
    let seed = [7u8; 20];
    subnets
        .add_joined("pairjoin", seed, 1)
        .expect("seed subnet");
    subnets.save().expect("save subnets");

    let id_b = Identity::from_seed(&[2u8; 32]);
    let host = NetHost::start(NetConfig {
        identity: id_b,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    })
    .expect("net host start");
    let addr_b = host.addr();

    // The joiner leg, exactly as the broker `pair-join` arm runs it — its
    // own node identity, the current code, the explicit responder address
    // (the same static-address seam the twohost rig uses).
    let id_a = Identity::from_seed(&[3u8; 32]);
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    let outcome = rt.block_on(async {
        let ep_a = NetEndpoint::bind_scoped(
            &id_a,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Loopback,
        )
        .await
        .expect("joiner endpoint bind");
        let code = TotpSeed::from_bytes(seed).code_at(now_secs());
        let out = join_ceremony(&ep_a, "pairjoin", &code, Some(addr_b)).await;
        ep_a.close().await;
        out
    });
    assert!(
        outcome.is_ok(),
        "the joiner must complete the ceremony INCLUDING the final Done+Seed \
         frames (the close-race regression), got: {outcome:?}"
    );
}

+use std::time::{Duration, SystemTime, UNIX_EPOCH};

+static SEQ: AtomicU32 = AtomicU32::new(0);

+    let n = SEQ.fetch_add(1, Ordering::Relaxed);

+    format!("spt-pairjoin-{}-{}.sock", std::process::id(), n)

+/// A net-enabled broker (hermetic loopback host) serving on a background thread —

+/// the brain↔broker IPC harness, mirroring `presence.rs`.

+fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {

+    let host = NetHost::start(NetConfig {

+        identity: Identity::generate(),

+        discovery: LocalDiscovery::Off,

+        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");

+fn connect_retry(name: &str) -> Brain {

+        if let Ok(b) = Brain::cold_start(name, 1) {

+        thread::sleep(Duration::from_millis(10));

+// [int->REQ-JOIN-TWO-PHASE] the two-phase code phase over the REAL broker IPC: a

+// `pair-code-submit` for an unknown/expired meet session fails CLEANLY (no panic,

+// no hang) with the re-run-the-join guidance — exercising brain.pair_submit_code

+// → KIND_PAIR_CODE_SUBMIT → dispatch_pair_code_submit's hold-miss path → the

+// classified pair-joined reply, all on the wire. (The happy path's held-address

+// ceremony is covered by the end-to-end test above; the live rendezvous MEET

+// needs real discovery and rides the two-host rig.)

+    let dir = tempfile::tempdir().expect("temp dir");

+    let _broker = net_broker(&name, dir.path());

+    let mut brain = connect_retry(&name);

+        .expect("submit returns a reply, not an IPC error");

+    assert!(!reply.ok, "an unknown meet session cannot join");

+        reply.detail.contains("expired") || reply.detail.contains("re-run"),

+        "the failure guides the operator to re-run the join, got: {}",


W2 wired across all files + tests added. Running the W2 tests (spt-daemon targeted), per-crate to avoid OOM.


PowerShell

$env:CARGO_BUILD_JOBS=2; cargo nextest run -p spt-daemon -E 'test(pair_hold_lifecycle_decisions) | test(two_phase_join_wire_round_trips) | test(pair_code_submit_for_an_unknown_session_fails_cleanly) | test(daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end)' 2>&1 | Select-Object -Last 20


   Compiling spt-net v0.16.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.16.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1m 54s
────────────
 Nextest run ID 10b3ff5d-89f4-4289-8ea8-39db7c440fb2 with nextest profile: default
    Starting 4 tests across 36 binaries (485 tests skipped)
        PASS [   0.018s] (1/4) spt-daemon msg::tests::two_phase_join_wire_round_trips
        PASS [   0.022s] (2/4) spt-daemon broker::tests::pair_hold_lifecycle_decisions
        PASS [   0.076s] (3/4) spt-daemon::pairjoin pair_code_submit_for_an_unknown_session_fails_cleanly
        PASS [   0.557s] (4/4) spt-daemon::pairjoin daemon_hosted_responder_pairs_a_loopback_joiner_end_to_end
────────────
     Summary [   0.558s] 4 tests run: 4 passed, 485 skipped


All 4 W2 tests green. Running full lib/bin regression on the touched seams (brain/broker/msg/pairhost/nethost/cli), per-crate.


$env:CARGO_BUILD_JOBS=2; cargo nextest run -p spt-net --lib 2>&1 | Select-Object -Last 6


        PASS [   0.235s] (123/126) spt-net net::pairing::wire::tests::wrong_code_fails_and_records_no_membership
        PASS [   0.040s] (124/126) spt-net net::xfer::tests::record_split_across_chunks_reassembles
        PASS [   0.253s] (125/126) spt-net net::pairing::wire::tests::repair_member_keeps_seed
        PASS [   0.042s] (126/126) spt-net net::xfer::tests::records_round_trip_and_unknown_kind_is_skipped
────────────
     Summary [   0.504s] 126 tests run: 126 passed, 1 skipped


$env:CARGO_BUILD_JOBS=2; cargo nextest run -p spt-daemon --lib 2>&1 | Select-Object -Last 6


        PASS [   1.502s] (379/382) spt-daemon reap::tests::job_reaps_enrolled_child_and_grandchild
        PASS [   0.830s] (380/382) spt-daemon shellwake::tests::watcher_crash_exits_respawn_then_give_up
        PASS [   0.954s] (381/382) spt-daemon sync::tests::reconcile_driver_structurally_excludes_live_role
        PASS [   1.339s] (382/382) spt-daemon sync::tests::select_refs_scopes_two_tiers
────────────
     Summary [   4.724s] 382 tests run: 382 passed (4 leaky), 0 skipped


spt-daemon lib 382 (+2). Now spt bin (CLI compile + subnet tests).


$env:CARGO_BUILD_JOBS=2; cargo nextest run -p spt --bin spt 2>&1 | Select-Object -Last 6


        PASS [   3.143s] (258/261) spt::bin/spt cli::tests::fork_forks_homes_and_deletes_exactly_the_source
        PASS [   5.217s] (259/261) spt::bin/spt cli::tests::adapter_translate_proof_gates_on_commit
        PASS [  11.688s] (260/261) spt::bin/spt cli::tests::shell_spawn_gates_on_the_registered_set
        PASS [  17.104s] (261/261) spt::bin/spt cli::tests::owner_shutdown_gated_by_can_shutdown_grant
────────────
     Summary [  18.336s] 261 tests run: 261 passed, 0 skipped


All green (126/382/261). Running clippy --workspace, then activating REQ-JOIN-TWO-PHASE + traceable/xtask gates.


$env:CARGO_BUILD_JOBS=2; cargo clippy --workspace --all-targets 2>&1 | Select-Object -Last 15


    Checking spt-net v0.16.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-daemon v0.16.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.16.0 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.16.0 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 17.32s


Clippy clean. Activating REQ-JOIN-TWO-PHASE (impl+unit+int; doc already satisfied).


Edit

required_stages = ["doc"]  # doc = ADR-0030 + docs\design\robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.

required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0030 + docs\design\robust-wan-subnet-join.md + CONTEXT.md glossary. ACTIVATED v0.17.0 W2: impl = meet_via_rendezvous split + meet_seed_holder + nethost.pair_meet + broker pair_holds state machine (KIND_PAIR_MEET/MetMember/KIND_PAIR_CODE_SUBMIT, 5-min hold, wrong-code-keeps-hold, expiry) + brain.pair_meet/pair_submit_code + cli two-phase (meet→prompt→submit, --code one-shot preserved). unit = hold lifecycle decisions (hold_is_live/fail_keeps_hold) + two-phase wire round-trip. int = held-address ceremony end-to-end (pairjoin) + unknown/expired-session code-submit over real broker IPC. NOTE: live rendezvous MEET needs real discovery → rides the two-host rig (W6 judge).


<tool_use_error>String to replace not found in file.
String: required_stages = ["doc"]  # doc = ADR-0030 + docs\design\robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.</tool_use_error>

Error: String to replace not found in file.
String: required_stages = ["doc"]  # doc = ADR-0030 + docs\design\robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.


1458	required_stages = ["doc"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.


required_stages = ["doc"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.

required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary. ACTIVATED v0.17.0 W2: impl = meet_via_rendezvous split + meet_seed_holder + nethost.pair_meet + broker pair_holds state machine (KIND_PAIR_MEET/MetMember/KIND_PAIR_CODE_SUBMIT, 5-min hold, wrong-code-keeps-hold, expiry) + brain.pair_meet/pair_submit_code + cli two-phase (meet -> prompt -> submit, --code one-shot preserved). unit = hold lifecycle (hold_is_live/fail_keeps_hold) + two-phase wire round-trip. int = held-address ceremony end-to-end (pairjoin) + unknown/expired-session code-submit over real broker IPC. NOTE: live rendezvous MEET needs real discovery -> rides the two-host rig (W6 judge).


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 = "`spt subnet join` is two-phase (meet-before-code) so the entered code is FRESH at the ceremony regardless of discovery time (ADR-0030). The meet selector is the PUBLIC `(subnet-name, TOTP-epoch)` (rendezvous_token = SHA256(domain ‖ name ‖ totp_step)); the secret TOTP-code is the SPAKE2 password ONLY, never a discovery input — so the code is collected AFTER a member is found. Extend the brain.pair_join event stream (brain.rs:1009): CLI PairMeetReq{subnet} → daemon meets (name, current-epoch) resolving the seed-holder's REAL stable pairing address → MetMember event → CLI prompts the code (cli.rs cmd_subnet_join :6236) → PairCodeSubmit{code} → daemon dials the held real-address on SPT_PAIR_ALPN + SPAKE2 → PairJoined/PairFail. Daemon holds the real-address between phases, bounded by a 5-MINUTE wait-for-code timeout; a wrong-code retry re-runs the CEREMONY ONLY against the held address (no re-search). The non-interactive `--code` path stays one-shot (no prompt; relies on REQ-NET-FAMILY-GATE fast discovery, fails loudly per REQ-JOIN-DIAGNOSTICS on staleness). Security unchanged — the meet is pre-trust/unauthenticated (SPT_PAIR_MEET_ALPN); auth stays in SPAKE2. (next milestone)"

-required_stages = ["doc"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary correction. impl+unit+int activate at the build milestone: PairMeetReq/MetMember/PairCodeSubmit protocol + daemon held-address state machine + 5-min timeout + ceremony-only wrong-code retry + --code one-shot preserved; int = real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only + held-address timeout.

+required_stages = ["doc", "impl", "unit", "int"]  # doc = ADR-0030 + docs/design/robust-wan-subnet-join.md + CONTEXT.md glossary. ACTIVATED v0.17.0 W2: impl = meet_via_rendezvous split + meet_seed_holder + nethost.pair_meet + broker pair_holds state machine (KIND_PAIR_MEET/MetMember/KIND_PAIR_CODE_SUBMIT, 5-min hold, wrong-code-keeps-hold, expiry) + brain.pair_meet/pair_submit_code + cli two-phase (meet -> prompt -> submit, --code one-shot preserved). unit = hold lifecycle (hold_is_live/fail_keeps_hold) + two-phase wire round-trip. int = held-address ceremony end-to-end (pairjoin) + unknown/expired-session code-submit over real broker IPC. NOTE: live rendezvous MEET needs real discovery -> rides the two-host rig (W6 judge).

title = "spt-core binds only IP families that are actually REACHABLE (ADR-0030), so a half-broken family (e.g. IPv6 whose AAAA resolves but whose path is dead) never silently consumes connection/discovery time. At NetEndpoint::bind (endpoint.rs), probe each family's reachability ONCE and bind only the working ones: dual-stack when both healthy; IPv4-only when IPv6 is dead; IPv6-only when IPv4 is dead (drop the DEAD family — NOT a fixed prefer-IPv4; IPv6-only networks must keep working). Re-evaluated on daemon restart (once-at-bind, no live re-eval in v1). Explicit overrides SPT_DISABLE_IPV6 / SPT_DISABLE_IPV4 force a family off (escape hatch + determinism + testing, mirroring SPT_NTP_SERVER); a forced-off family is never bound regardless of the probe. Reusable beyond join — every spt connection benefits. (next milestone)"


PowerShell

$env:NO_COLOR=1; traceable-reqs check *> $null; "traceable-exit=$LASTEXITCODE"; $env:CARGO_BUILD_JOBS=2; cargo run -p xtask -- check 2>&1 | Select-Object -Last 3; "xtask-exit=$LASTEXITCODE"


Error: Exit code 101
traceable-exit=0
building spt failed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\xtask.exe check` (exit code: 101)
xtask-exit=101


traceable exit 0. xtask check reports "building spt failed" — need the actual error (clippy/nextest built spt fine). Re-running with full output.


$env:CARGO_BUILD_JOBS=2; cargo run -p xtask -- check 2>&1 | Select-Object -Last 40


Error: Exit code 101
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.51s
     Running `target\debug\xtask.exe check`
error: failed to remove file `C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe`

Caused by:
  Access is denied. (os error 5)

thread 'main' (169488) panicked at crates\xtask\src\main.rs:156:5:
building spt failed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target\debug\xtask.exe check` (exit code: 101)


File-lock: xtask can't overwrite `target/debug/spt.exe` (os error 5 — in use). Checking for a workspace-target process holding it (never touching the AppData prod daemon).


Get-Process spt,spt-daemon -ErrorAction SilentlyContinue | Select-Object Id, Path | Format-Table -AutoSize; "--- handle check ---"; Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Path -like '*Documents\projects\spt-core\target*' } | Select-Object Id, Path | Format-Table -AutoSize


    Id Path
    -- ----
 58140 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
 78884 C:\Users\decid\AppData\Local\spt-core\bin\spt.exe
 79980 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
126508 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
149588 C:\Users\decid\AppData\Local\spt-core\bin\spt.exe
233436 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe

--- handle check ---

    Id Path
    -- ----
 58140 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
 79980 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
126508 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
233436 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe


4 orphan `target\debug\spt.exe` daemons (from the cli nextest runs) hold the lock; the 2 AppData ones are the live prod daemon. Killing only the workspace-target PIDs.


$workspaceOrphans = Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.Path -like '*Documents\projects\spt-core\target\debug\spt.exe' }; $workspaceOrphans | ForEach-Object { Write-Output "killing PID $($_.Id) $($_.Path)"; Stop-Process -Id $_.Id -Force }; "killed=$($workspaceOrphans.Count)"


killing PID 58140 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
killing PID 79980 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
killing PID 126508 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
killing PID 233436 C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe
killed=4


$env:CARGO_BUILD_JOBS=2; cargo run -p xtask -- check 2>&1 | Select-Object -Last 5; "xtask-exit=$LASTEXITCODE"


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.53s
     Running `target\debug\xtask.exe check`
xtask check: OK
xtask-exit=0


xtask OK. All W2 gates green. Committing W2 selectively (verifying no stray/doyle edits first).


 M crates/spt-daemon/src/brain.rs
 M crates/spt-daemon/src/broker.rs
 M crates/spt-daemon/src/msg.rs
 M crates/spt-daemon/src/nethost.rs
 M crates/spt-daemon/src/pairhost.rs
 M crates/spt-daemon/tests/pairjoin.rs
 M crates/spt-net/src/net/pairing/meet.rs
 M crates/spt/src/cli.rs
 M traceable-reqs.toml


All 9 are W2 files (traceable has only the W2 edit). Committing.


Bash

git add crates/spt-net/src/net/pairing/meet.rs crates/spt-daemon/src/pairhost.rs crates/spt-daemon/src/nethost.rs crates/spt-daemon/src/msg.rs crates/spt-daemon/src/broker.rs crates/spt-daemon/src/brain.rs crates/spt/src/cli.rs crates/spt-daemon/tests/pairjoin.rs traceable-reqs.toml && git commit -q -F - <<'EOF'
feat(pair): two-phase meet-before-code subnet join (v0.17.0 W2)

REQ-JOIN-TWO-PHASE (ADR-0030). `spt subnet join` collected the code UP FRONT,
then ran the whole rendezvous meet + ceremony in one blocking call — so on a slow
WAN meet the pre-entered code rotated and read as "wrong code", and every re-prompt
re-ran the entire search. The meet selector is the PUBLIC (subnet, TOTP-epoch),
not the secret code, so the code can be collected AFTER a member is found and used
immediately at the ceremony — fresh regardless of discovery time.

Split the join into two phases on the broker IPC:
- meet.rs: extract meet_via_rendezvous (resolve the seed-holder's real, stable
  EndpointAddr via the ±1-window race) out of dial_via_rendezvous (= that + the
  ceremony-ALPN dial; external behavior unchanged → --code path + tests intact).
- pairhost.meet_seed_holder / nethost.pair_meet: the meet phase (no code).
- broker: a pair_holds map (session_id → {subnet, addr, 5-min deadline}).
  KIND_PAIR_MEET → resolve + hold + reply MetMember; KIND_PAIR_CODE_SUBMIT →
  dial the HELD address (no re-search) + ceremony. A wrong-code retry KEEPS the
  hold (ceremony only); success / other-terminal / expiry drop it. Stale holds
  swept on each new meet. Pairing is never journaled (the code must not persist).
- brain.pair_meet / pair_submit_code; cli cmd_subnet_join interactive flow:
  meet → "found a member" → prompt code → submit (wrong-code re-prompts against
  the SAME held session, ceremony only). `--code` stays one-shot (no prompt).
- render_join_failure: single-sourced classified failure (meet phase fails BEFORE
  any code prompt — a dead subnet never makes the user fetch a code).

Security unchanged: meet is pre-trust (SPT_PAIR_MEET_ALPN); auth stays in SPAKE2.

- unit (spt-daemon): hold lifecycle (hold_is_live / fail_keeps_hold) + the
  two-phase wire round-trip.
- int (spt-daemon): held-address ceremony end-to-end (the existing pairjoin e2e,
  dual-tagged) + unknown/expired-session code-submit over the real broker IPC.
  The live rendezvous MEET needs real discovery → rides the two-host rig (W6).

Activated REQ-JOIN-TWO-PHASE impl+unit+int (doc was ADR-0030).

Co-authored by: todlando
EOF
git log --oneline -1


a07322c feat(pair): two-phase meet-before-code subnet join (v0.17.0 W2)


W2 committed @a07322c. Reporting to doyle.


printf '%s' "doyle — W2 LANDED @a07322c: two-phase meet-before-code join (REQ-JOIN-TWO-PHASE impl+unit+int).

KEY: meet.rs already meets-then-redials internally, so the split was clean — extracted meet_via_rendezvous (resolve real_addr, no ceremony dial); dial_via_rendezvous = that + SPT_PAIR_ALPN dial (external behavior unchanged → --code path + all meet tests intact). Phase 2 reuses join_ceremony(Some(held_addr)) (the existing direct-addr seam) — no re-search, and the existing late-code retry redials the held addr.

Broker pair_holds map (session_id → {subnet, addr, 5-min deadline}): KIND_PAIR_MEET resolves+holds→MetMember; KIND_PAIR_CODE_SUBMIT dials held addr→ceremony; wrong-code KEEPS hold (ceremony-only retry), success/terminal/expiry drop it; stale swept per meet. brain.pair_meet/pair_submit_code; cli interactive = meet→'found a member'→prompt→submit, --code stays one-shot. render_join_failure single-sourced (meet fails BEFORE the code prompt). Security unchanged (meet pre-trust MEET_ALPN, auth in SPAKE2).

GATES green: spt-net 126 / spt-daemon 382 (+2) / spt 261; +4 W2 tests pass; clippy --workspace clean; traceable exit 0; xtask check OK.

NOTE for W6: the live rendezvous MEET needs real discovery → can't run hermetic. int covers the held-addr ceremony e2e (pairjoin, dual-tagged) + unknown/expired-session code-submit over real broker IPC. The full meet-over-real-rendezvous int rides the TWO-HOST RIG — W2 changes the pairing handshake ORDER (a cross-host pairing seam), so reconsider the twohost-skip at the fold. QUIC subnet transport untouched.

GOTCHA paid: 4 orphan target/debug/spt.exe from cli nextest runs held the file lock → xtask 'building spt failed' (os error 5). Killed by workspace-target PID only (AppData prod daemon untouched).

Next: W3 (diagnosable join — threads on the same meet path) then W5 (display parity). Context high — communing + /clear before W3 to keep the build clean. Will report per wave." | $OWL send doyle todlando 2>&1 | tail -1


➜ SENT:doyle


W2 reported. Updating the persistent record + dropping a Psyche commune checkpoint (note: /self-clear is API-blocked per prior finding, and the harness summarizes long context forward — so I'll checkpoint and continue W3 in-context rather than attempt a blocked clear).


Edit

REQ-JOIN-TWO-PHASE (impl+unit+int). doyle GO'd 2026-06-28 after W1+W4 gate. Build on shared branch v0.16.0-update-arc, selective commits.

REQ-JOIN-TWO-PHASE (impl+unit+int). **SHIPPED @a07322c (v0.17.0 W2), all gates green** (spt-net 126 / spt-daemon 382 / spt 261, clippy/traceable/xtask clean). doyle GO'd 2026-06-28 after W1+W4 gate. Build on shared branch v0.16.0-update-arc, selective commits. Plan below was followed as-is. NEXT: W3 (diagnosable join) then W5 (display parity) — see [[v0170-w1-w4-blockers]].


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v0170-w2-design.md has been updated successfully. (file state is current in your context — no need to Read it back)

---
name: v0170-w2-design
description: "v0.17.0 W2 two-phase meet-before-code join — crisp impl plan (grounded), for build/resume"
metadata:
  node_type: memory
  type: project
  originSessionId: 8c8eef1f-6837-4fc5-a65f-0633fc292f63
---

REQ-JOIN-TWO-PHASE (impl+unit+int). doyle GO'd 2026-06-28 after W1+W4 gate. Build on shared branch v0.16.0-update-arc, selective commits.

**KEY FINDING (grounded):** `meet.rs::dial_via_rendezvous` ALREADY meets-then-redials: it races `meet_at` over the ±1 rendezvous window (SPT_PAIR_MEET_ALPN), gets the responder's REAL `EndpointAddr`, then re-dials it over SPT_PAIR_ALPN and returns the ceremony conn. And `pairhost.rs::connect_seed_holder` ALREADY has a direct-addr path: `Some(addr) → connect_with_alpn(addr, SPT_PAIR_ALPN)` (no rendezvous). So two-phase = resolve+HOLD the real_addr in phase 1, then phase 2 reuses `pair_join(subnet, code, Some(held_addr))` (existing join_ceremony, direct dial, no re-search). The internal late-code retry (pairhost.rs:360) already redials Some(addr) = held addr.

**Broker dispatch model:** per-connection sequential loop (broker.rs:1486 handle_conn) — each frame dispatched to completion. So hold state in a broker map (mirror conn/stream tables), NOT inline frame-blocking.

**Impl steps:**
1. spt-net meet.rs: extract `pub async fn meet_via_rendezvous(dialer, subnet, step) -> Result<EndpointAddr, MeetError>` (the JoinSet race, returns real_addr, NO final SPT_PAIR_ALPN dial). `dial_via_rendezvous` = `meet_via_rendezvous` + connect SPT_PAIR_ALPN (external behavior unchanged → --code path + existing tests intact).
2. spt-daemon pairhost.rs: `meet_seed_holder(endpoint, subnet) -> Result<EndpointAddr, JoinFail>` — mirror connect_seed_holder None-branch SEARCH_DEADLINE(75s) loop but call meet_via_rendezvous, return addr.
3. spt-daemon nethost.rs: `pair_meet(subnet) -> Result<EndpointAddr, JoinFail>` (wraps meet_seed_holder on the broker-owned endpoint). pair_join already exists (nethost.rs:826).
4. spt-daemon broker.rs: add `pair_holds: Mutex<HashMap<u64, PairHold>>` {subnet, addr: EndpointAddr, deadline: Instant=now+5min}. KIND_PAIR_MEET→dispatch_pair_meet: meet→mint session_id (reuse counter)→store hold→reply KIND_MET_MEMBER(MetMember). KIND_PAIR_CODE_SUBMIT→dispatch: lookup+deadline-check (expired/missing→fail "meet expired, re-run join")→host.pair_join(subnet,code,Some(addr))→reply PairJoinReply; KEEP hold on wrong-code (retry ceremony-only), DROP on success/other-terminal/expiry. Sweep stale holds on each new meet.
5. spt-daemon msg.rs: PairMeetReq{subnet}; MetMember{session_id:u64, member_hex:String, detail?}; PairCodeSubmit{session_id:u64, code}. KIND_PAIR_MEET/KIND_MET_MEMBER/KIND_PAIR_CODE_SUBMIT consts. BrokerEvent::MetMember + read in brain.read_event.
6. spt-daemon brain.rs: `pair_meet(subnet)->io::Result<MetMember>`; `pair_submit_code(session_id, code)->io::Result<PairJoinReply>`. Keep pair_join (--code one-shot).
7. spt cli.rs cmd_subnet_join (:6226): interactive path → pair_meet (live "searching" progress is W3) → on MetMember prompt code → pair_submit_code → wrong-code re-prompts + re-submits SAME session (ceremony-only, no re-search) → success = existing post-join block. `--code` path unchanged (pair_join one-shot, relies on W1 fast discovery).

**Security unchanged:** meet pre-trust (SPT_PAIR_MEET_ALPN, meet.rs:60), auth in SPAKE2 on SPT_PAIR_ALPN. Meet routes on PUBLIC (name, totp-epoch); code collected AFTER member found.

**int (spt-daemon tests, likely pairjoin.rs):** real two-phase join over a live meet + wrong-code-retry-hits-ceremony-only (no re-search) + held-addr 5-min timeout. SPT_NTP_SERVER=off for hermetic clock. Then W3 (diagnostics) + W5 (display) remain — see [[v0170-w1-w4-blockers]].

-REQ-JOIN-TWO-PHASE (impl+unit+int). doyle GO'd 2026-06-28 after W1+W4 gate. Build on shared branch v0.16.0-update-arc, selective commits.

+REQ-JOIN-TWO-PHASE (impl+unit+int). **SHIPPED @a07322c (v0.17.0 W2), all gates green** (spt-net 126 / spt-daemon 382 / spt 261, clippy/traceable/xtask clean). doyle GO'd 2026-06-28 after W1+W4 gate. Build on shared branch v0.16.0-update-arc, selective commits. Plan below was followed as-is. NEXT: W3 (diagnosable join) then W5 (display parity) — see [[v0170-w1-w4-blockers]].


- [v0.17.0 W2 LANDED (two-phase join)](v0170-w2-design.md) — @a07322c: meet-before-code (REQ-JOIN-TWO-PHASE impl+unit+int). meet_via_rendezvous split + broker pair_holds 5-min state machine + cli meet→prompt→submit. All gates green. GOTCHA: orphan target/debug/spt.exe from cli nextest locks files → kill by workspace-PID before xtask.
- [v0.17.0 W1+W4 LANDED](v0170-w1-w4-blockers.md)


The file C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\MEMORY.md has been updated successfully. (file state is current in your context — no need to Read it back)

- [v0.17.0 W2 LANDED (two-phase join)](v0170-w2-design.md) — @a07322c: meet-before-code (REQ-JOIN-TWO-PHASE impl+unit+int). meet_via_rendezvous split + broker pair_holds 5-min state machine + cli meet→prompt→submit. All gates green. GOTCHA: orphan target/debug/spt.exe from cli nextest locks files → kill by workspace-PID before xtask.
- [v0.17.0 W1+W4 LANDED](v0170-w1-w4-blockers.md)

- [v0.14.1 builds (F-018+picker+netstream)](f018-adapter-add-nondestructive.md) — PUBLISHED counter 31. F-018 adapter-add non-destructive, picker home-subnet, netstream flake FIX. GOTCHA: `spt` NO lib target → `--bin spt`.

- [F-019 translation install_dir resolve](f019-translation-install-dir-resolve.md) — PUBLISHED v0.14.2 counter 32. CC-on-Win blocker: idle msgs typed-not-SUBMITTED, root=bare path not resolved vs install_dir. Fix=resolve_program_in_dir ×4 sites.

- [v0.17.0 robust-join DESIGNED (next build)](v0170-robust-join-design.md) — branch v0.17.0-robust-join off main @551f3dc. ADR-0030 (two-phase meet-before-code + per-family bind gate + diagnosable join) + REQ-PRESENCE-LIVENESS-TRUTH. JIT W1 family-gate + W4 presence-truth = urgent correctness; W2/W3 = UX. todlando builds next. Diags: [[broken-ipv6-poisons-iroh-discovery]] + [[presence-dead-shows-online]].

+- [v0.17.0 W2 LANDED (two-phase join)](v0170-w2-design.md) — @a07322c: meet-before-code (REQ-JOIN-TWO-PHASE impl+unit+int). meet_via_rendezvous split + broker pair_holds 5-min state machine + cli meet→prompt→submit. All gates green. GOTCHA: orphan target/debug/spt.exe from cli nextest locks files → kill by workspace-PID before xtask.

- [v0.17.0 W1+W4 LANDED](v0170-w1-w4-blockers.md) — 2026-06-28: W4 @867c7ab (cold perch→Suspended not Dormant, REQ-PRESENCE-LIVENESS-TRUTH impl+unit+int) + W1 @7cddaba (per-family bind gate, REQ-NET-FAMILY-GATE doc+impl+unit). All gated (311/311 traceable, clippy/xtask clean). Built on SHARED branch v0.16.0-update-arc (NOT the design-memo's v0.17.0-robust-join), doyle's tree. W2/W3/W5/W6 pending doyle scope. Build OOM→CARGO_BUILD_JOBS=2 per-crate.

- [presence: dead endpoints show ONLINE remotely](presence-dead-shows-online.md) — registryhost.rs:404 gossips a not-alive perch as Dormant; picker/data.rs:268 maps remote Dormant→Online. Fix=dead→Offline (guard live-unbound) + picker Dormant-distinct. v0.17.0 W4.

- [v0.16.0 PUBLISHED (update-arc + CLI/UX)](v0160-published.md) — ✅ counter 35 @9e5b33b (PR#38, 2026-06-25). 6 waves all doyle-gated: subst keys {adapter_dir}/{adapter_name} + lazy [strings] (ADR-0029 resolve-not-execute) / translation [command] / [update.post] composite / --reply-to REMOVED / empty-scope create / global --json / digest cursor / rc identity row. hashes linux e13d7c2d… / win f6cd39ec…. deployah caught a docs-token leak at the gate (clap /// REQ-tag → //, see [[clap-doc-comment-leaks-req-tag]]). perri GO sent for claude-spt v0.8.0 (min_spt_core→0.16.0). Build/ratify detail: [[v0160-w1-manifest-runtime]].


Write

# todlando commune — v0.17.0 build (W1/W4/W2 landed; W3/W5 next)

## Status
v0.17.0 milestone (robust WAN join + presence/display) IN BUILD on shared branch `v0.16.0-update-arc`, doyle's tree, selective commits. doyle gates + GO'd all waves.

LANDED + GATED (each: nextest spt-net/spt-daemon/spt green, clippy --workspace clean, traceable exit 0, xtask OK):
- W1 @7cddaba — per-family bind gate (REQ-NET-FAMILY-GATE). Probe each IP family at bind, drop dead one. SPT_DISABLE_IPV6/IPV4. doyle-gated.
- W4 @867c7ab — cold perch → Suspended not Dormant (REQ-PRESENCE-LIVENESS-TRUTH). Removes dead-endpoint-shows-green-remotely. doyle-gated.
- W2 @a07322c — two-phase meet-before-code join (REQ-JOIN-TWO-PHASE). meet_via_rendezvous split + broker pair_holds 5-min state machine + cli meet→prompt→submit, --code one-shot preserved.

## NEXT (continuing in-context; /self-clear is API-blocked so no clear)
- W3 REQ-JOIN-DIAGNOSTICS (impl+unit): (a) live search progress (cli.rs:6268 region — now the meet phase); (b) detailed meet-failure (candidates+families+relay-vs-direct+last error) surfaced BEFORE code prompt — thread last error up from connect_seed_holder (pairhost.rs:437) + dial_via_rendezvous/meet_via_rendezvous (meet.rs); (c) brain.rs `_=>continue` terminal-event propagation; (d) --verbose/SPT_LOG discovery trace. xtask gen IF --verbose hits CLI surface.
- W5 REQ-SUBNET-DISPLAY-PARITY (impl+unit+int, design subnet-presence-display.md §B): gossip additive Instance fields (bound/controlled+driver/harness_only); remove picker/data.rs:293 remote-reduction; EpDisplay rework (drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, ADD Suspended→gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow.
- W6: int activations at fold, public docs, release bump-in-PR counter 36 (doyle/deployah).

## Gotchas (live)
- BUILD OOM if 3 crate test-bins codegen concurrently → CARGO_BUILD_JOBS=2, per-crate nextest.
- Orphan target/debug/spt.exe from cli nextest runs hold file locks → xtask "building spt failed" (os error 5). Kill by WORKSPACE-target PID only; NEVER touch AppData\Local\spt-core\bin (live prod daemon).
- `spt` has NO lib target → `--bin spt`. spt send bounces NO_PERCH → use `$OWL send doyle todlando`.
- twohost SKIP except W2 (handshake-order change) — judge at W6 fold.

## Memory
MEMORY.md updated; v0170-w2-design.md (SHIPPED), v0170-w1-w4-blockers.md current.


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

# todlando commune — v0.17.0 build (W1/W4/W2 landed; W3/W5 next)

## Status
v0.17.0 milestone (robust WAN join + presence/display) IN BUILD on shared branch `v0.16.0-update-arc`, doyle's tree, selective commits. doyle gates + GO'd all waves.

LANDED + GATED (each: nextest spt-net/spt-daemon/spt green, clippy --workspace clean, traceable exit 0, xtask OK):
- W1 @7cddaba — per-family bind gate (REQ-NET-FAMILY-GATE). Probe each IP family at bind, drop dead one. SPT_DISABLE_IPV6/IPV4. doyle-gated.
- W4 @867c7ab — cold perch → Suspended not Dormant (REQ-PRESENCE-LIVENESS-TRUTH). Removes dead-endpoint-shows-green-remotely. doyle-gated.
- W2 @a07322c — two-phase meet-before-code join (REQ-JOIN-TWO-PHASE). meet_via_rendezvous split + broker pair_holds 5-min state machine + cli meet→prompt→submit, --code one-shot preserved.

## NEXT (continuing in-context; /self-clear is API-blocked so no clear)
- W3 REQ-JOIN-DIAGNOSTICS (impl+unit): (a) live search progress (cli.rs:6268 region — now the meet phase); (b) detailed meet-failure (candidates+families+relay-vs-direct+last error) surfaced BEFORE code prompt — thread last error up from connect_seed_holder (pairhost.rs:437) + dial_via_rendezvous/meet_via_rendezvous (meet.rs); (c) brain.rs `_=>continue` terminal-event propagation; (d) --verbose/SPT_LOG discovery trace. xtask gen IF --verbose hits CLI surface.
- W5 REQ-SUBNET-DISPLAY-PARITY (impl+unit+int, design subnet-presence-display.md §B): gossip additive Instance fields (bound/controlled+driver/harness_only); remove picker/data.rs:293 remote-reduction; EpDisplay rework (drop UnboundControlled, Unbound→red-filled, HarnessOnly→amber-hollow, ADD Suspended→gray-filled). Picker maps Active|Dormant→online flavor, Suspended→gray-filled, Offline→gray-hollow.
- W6: int activations at fold, public docs, release bump-in-PR counter 36 (doyle/deployah).

## Gotchas (live)
- BUILD OOM if 3 crate test-bins codegen concurrently → CARGO_BUILD_JOBS=2, per-crate nextest.
- Orphan target/debug/spt.exe from cli nextest runs hold file locks → xtask "building spt failed" (os error 5). Kill by WORKSPACE-target PID only; NEVER touch AppData\Local\spt-core\bin (live prod daemon).
- `spt` has NO lib target → `--bin spt`. spt send bounces NO_PERCH → use `$OWL send doyle todlando`.
- twohost SKIP except W2 (handshake-order change) — judge at W6 fold.

## Memory
MEMORY.md updated; v0170-w2-design.md (SHIPPED), v0170-w1-w4-blockers.md current.


Commune checkpoint dropped. /self-clear is API-blocked and the harness summarizes long context forward, so I'll continue W3 in-context with each piece gated + committed independently for durability. Grounding the CLI `subnet join` arg plumbing + the meet trace points for W3.


Grep

cmd_subnet_join|Join \{|Join \{|subnet.*join|enum SubnetCmd|Join\b.*code|fn .*subnet


919:enum SubnetCmd {
931:    /// Mint a fresh subnet and print its joining material.
957:    Join {
958:        /// The subnet to join (as named on the member machine).
1208:                Some(SubnetCmd::Join { name, code }) => cmd_subnet_join(name, code),
1831:                    subnets.join(", ")
2012:fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {
2353:fn cmd_endpoint_list(subnet: Option<String>, detail: bool, json: bool) -> i32 {
2520:fn format_subnet_rows(rows: &[spt_net::net::registry::ResourceRow], detail: bool) -> String {
3045:fn resolve_notify_subnet(
3076:fn issuer_home_subnet(issuer: &str) -> Option<String> {
3195:fn cmd_notif_list(subnet: Option<String>, json: bool) -> i32 {
4277:fn run_listen(id: &str, once: bool, subnet: Option<&str>) -> i32 {
4617:/// the subnet's joining secret).
4633:                    "Run `spt subnet join {name}` on other machines to have them join.\n\
4651:                    "ELEVATION_REQUIRED: creating subnet '{name}' reveals its joining secret — \
4673:/// Persist a freshly-minted subnet + fire first-join home adoption
4941:fn cmd_subnet_show_code(name: Option<String>, json: bool) -> i32 {
4983:/// the output reveals the subnet's joining secret.
4985:fn cmd_subnet_create(name: String) -> i32 {
5049:fn subnet_status_rows(
5092:fn render_subnet_status(rows: &[SubnetStatusRow], daemon_running: bool, hints: bool) -> String {
5229:     hint: `spt subnet join <name>` pairs this machine into one (elevated)\n\
5230:     hint: `spt subnet show-code [name]` shows a subnet's joining code (elevated)\n";
5350:fn render_node_rows(subnet: &str, rows: &[(NodeRow, bool)]) -> String {
5488:/// Render the coming-online banner's "spt subnets joined" section + the
5492:fn render_subnets_joined(rows: &[(String, &'static str)]) -> String {
5493:    let mut out = String::from("spt subnets joined\n──────────────────\n");
5559:    eprint!("{}", render_subnets_joined(&rows));
5608:fn cmd_subnet_status(name: Option<String>, nodes: bool, hints: bool, json: bool) -> i32 {

[Showing results with pagination = limit: 30]


957	    Join {
958	        /// The subnet to join (as named on the member machine).
959	        name: Option<String>,
960	        /// The current 6-digit code (`spt subnet show-code` on a member
961	        /// machine, or your authenticator app).
962	        #[arg(long)]
963	        code: Option<String>,
964	    },
965	    /// Exit a subnet: drop its membership and trust material from this node.
966	    ///
967	    /// Removes the subnet's seed, its trust rows, its serve-state, and its
968	    /// registry snapshot here. Gated behind OS elevation (membership exit
969	    /// destroys trust material). The remaining members still hold the old
970	    /// seed — rotate it there if this machine should not rejoin.

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },
    /// Exit a subnet: drop its membership and trust material from this node.
    ///
    /// Removes the subnet's seed, its trust rows, its serve-state, and its
    /// registry snapshot here. Gated behind OS elevation (membership exit
    /// destroys trust material). The remaining members still hold the old
    /// seed — rotate it there if this machine should not rejoin.


Decision point on context budget. I've landed three full waves (W1, W4, W2) — all gated green, committed, and reported to doyle. W3 and W5 remain. W3 part (d) (the `--verbose` discovery trace) has a real subtlety: the meet runs **daemon-side**, so surfacing per-probe traces to the joining CLI needs trace frames over the IPC — a protocol design step best done with fresh context, not bolted on at the end of a very long session.

I've committed a Psyche commune checkpoint, so a `/clear` resumes W3/W5 cleanly. /self-clear is API-blocked, so I can't trigger it myself.

**v0.17.0 progress — 3 of 5 build waves landed (shared branch, doyle's tree):**

| Wave | Commit | What | Gates |
|---|---|---|---|
| W1 | `7cddaba` | per-family bind gate — drop dead IP family (kills half-broken-IPv6 WAN-join hang) | ✅ doyle-gated |
| W4 | `867c7ab` | cold perch → Suspended not Dormant (dead endpoint no longer green remotely) | ✅ doyle-gated |
| W2 | `a07322c` | two-phase meet-before-code join (code fresh at ceremony, no re-search retry) | ✅ green local |

Each: spt-net 126 / spt-daemon 382 / spt 261 + new tests · clippy clean · traceable exit 0 · xtask OK.

Notably **W2 already fixed W3's core diagnosability bug** (the `_ => continue` terminal-event swallow — "why the user saw nothing"): meet failures now reach the CLI as rendered, classified errors.

**Remaining:** W3 (live progress + failure detail + `--verbose` trace) → W5 (subnet display parity + palette). Both grounded; plans in memory + design docs.

Want me to continue W3 now in this context, or `/clear` first for a clean window? I'll proceed autonomously if you don't redirect — starting with W3's CLI-side pieces (progress + failure detail), and scoping the daemon-side `--verbose` trace deliberately.


<task-notification>
<task-id>bdfzr7zvf</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT type="msg" from="doyle">todlando — status check (no nag): W2 locked my side, 926/926. How's W3 (diagnosable join) + W5 (display parity) going post-clear? Reply with rough state so I can keep the W6 fold + deployah counter-36 release teed up. No rush if mid-wave.</EVENT></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


doyle confirms W2 gated (926/926). Replying with honest state, then starting W3.


Bash

printf '%s' "doyle — thanks, W2 confirmed (926). Correction: I did NOT /clear (self-clear is API-blocked — usage classifier refuses the turn). I checkpointed via commune + memory and am continuing in-context (harness auto-summarizes long context forward, so continuity holds).

W3/W5 state: NOT started yet — was at a context checkpoint right after W2 landed. Starting W3 NOW.

W3 scoping (REQ-JOIN-DIAGNOSTICS):
- (c) terminal-event propagation — ALREADY FIXED by W2's two-phase restructure: the old single pair_join _=>continue swallow is bypassed; meet failures now return MetOutcome::Failed → render_join_failure on the CLI. 'why the user saw nothing' = solved. Will tag.
- (a) live search progress — CLI progress thread during the (blocking) meet IPC: silence ≠ hang. Clean, CLI-only.
- (b)+(d) — SUBTLETY: the meet runs DAEMON-side (host.pair_meet, blocking), so per-probe trace can't stream to the joining CLI without trace frames. Broker dispatch is synchronous, so mid-meet progress frames are awkward. Plan: thread verbose through PairMeetReq; daemon accumulates a discovery trace (bound families from W1 gate / relay policy / rendezvous window / last concrete error) and returns it in the failure detail — surfaced to the CLI on --verbose. Avoids a progress-frame protocol; gives the diagnostic dump exactly when you need it (on failure).

Then W5 (display parity). Will report per wave. W6 fold + counter-36 stays teed up — no blockers from me." | $OWL send doyle todlando 2>&1 | tail -1


Now W3 implementation. Adding the `--verbose` flag to the `Join` command + threading.


Edit

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
        /// Print a detailed discovery trace (rendezvous attempts, elapsed vs
        /// deadline, the last concrete error) when the search struggles or fails
        /// — for diagnosing a join that can't find a member.
        #[arg(long)]
        verbose: bool,
    },


<owl_messages>
<owl_message from="doyle" priority="high">todlando — W3 design APPROVED, proceed. Right calls:
- (c) terminal-event propagation solved by W2 restructure → just TAG it [impl->REQ-JOIN-DIAGNOSTICS] on the MetOutcome::Failed→render_join_failure path (evidence on real code, not a stub).
- (b)+(d) accumulate-trace-daemon-side + return-in-failure-detail (vs live streaming): correct — the meet is daemon-blocking, a per-probe frame protocol isn't worth it. Failure-time diagnostic dump is exactly the value. The trace = bound families (from W1 gate) / relay-vs-direct / rendezvous window / last concrete error.
- (a) CLI progress thread for silence≠hang: good.

One ask: keep SPT_LOG (or equiv) able to surface the daemon-side discovery trace on the SUCCESS path too (a slow-but-eventually-ok join still wants 'what took so long') — even if --verbose only dumps the structured detail on failure. If that's heavy, defer it and note it; don't block W3.

xtask gen if --verbose hits the CLI surface. Gate per wave. Then W5. W6 fold (twohost rig ON — W2's pairing-order change is a cross-host seam) + counter-36 teed up.</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


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)

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
    },

    Join {
        /// The subnet to join (as named on the member machine).
        name: Option<String>,
        /// The current 6-digit code (`spt subnet show-code` on a member
        /// machine, or your authenticator app).
        #[arg(long)]
        code: Option<String>,
        /// Print a detailed discovery trace (rendezvous attempts, elapsed vs
        /// deadline, the last concrete error) when the search struggles or fails
        /// — for diagnosing a join that can't find a member.
        #[arg(long)]
        verbose: bool,
    },

+        /// Print a detailed discovery trace (rendezvous attempts, elapsed vs

+        /// deadline, the last concrete error) when the search struggles or fails

+        /// — for diagnosing a join that can't find a member.

/// Exit a subnet: drop its membership and trust material from this node.


doyle approved W3 (high-priority) + flagged W6 twohost ON. Acking, then continuing.


Bash

printf '%s' "doyle — ACK W3 approval. Proceeding: (c) tag-only on MetOutcome::Failed→render_join_failure; (a) CLI progress thread; (b)+(d) daemon-accumulated trace (bound families from W1 gate / relay-vs-direct / rendezvous window / last concrete error) returned in failure detail, surfaced on --verbose. Your SUCCESS-path ask (slow-but-ok join: 'what took so long') — will check for an existing SPT_LOG/tracing facility the daemon honors; if it's a clean hook I'll add per-attempt trace lines daemon-side, else DEFER with a note (won't block W3). xtask gen since --verbose hits the CLI surface. Noted: W6 twohost rig ON (W2 pairing-order = cross-host seam). Gating per wave; W5 after." | $OWL send doyle todlando 2>&1 | tail -1


➜ SENT:doyle


Checking for an existing SPT_LOG/tracing facility (doyle's success-path ask) + whether bound families are queryable for the trace.


crates\spt\tests\livehost_psyche_fail_e2e.rs:7://! This is the companion to `livehost_bootrace_e2e.rs` (the POSITIVE case). It
crates\spt\tests\unhost_psyche_reap_e2e.rs:12://! LONG-LIVED Psyche for an online live agent (the bootrace resident fixture), then
crates\spt\tests\unhost_psyche_reap_e2e.rs:83:    //    the blocking `ready` listener — the bootrace resident fixture). The bare token
crates\spt\tests\livehost_bootrace_e2e.rs:169:         [session.psyche_init]\ncommand='psychebin ready bootrace-resident-psyche'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
crates\spt\tests\livehost_bootrace_e2e.rs:229:    let id = "bootrace-agent";
crates\spt\tests\livehost_bootrace_e2e.rs:244:                "sid-bootrace",
crates\spt\tests\n1_pairing.rs:119:    //    old-broker direction. Capture stderr to read its handshake trace. ──
crates\spt\tests\n1_pairing.rs:149:    let trace = {
crates\spt\tests\n1_pairing.rs:158:         no verb desync) — new-brain × old-broker N-1 steady state.\nTRACE:\n{trace}"
crates\spt\tests\n1_pairing.rs:162:        trace.contains("BRAIN_UP"),
crates\spt\tests\n1_pairing.rs:163:        "the current brain must connect + handshake the old broker (BRAIN_UP).\nTRACE:\n{trace}"
crates\spt\tests\n1_pairing.rs:168:        !trace.contains("BRAIN_BROKER_LOST"),
crates\spt\tests\n1_pairing.rs:169:        "the current brain lost the old broker mid-run — a verb-surface desync.\nTRACE:\n{trace}"
crates\spt\src\cli.rs:964:        /// Print a detailed discovery trace (rendezvous attempts, elapsed vs
crates\spt-daemon\tests\daemon_lifecycle_real_brain.rs:35:// no install-dir fidelity needed here, that is the bootrace test's job.)
crates\spt-net\src\net\endpoint.rs:155:enum BindFamilies {
crates\spt-net\src\net\endpoint.rs:216:) -> Result<BindFamilies, NetError> {
crates\spt-net\src\net\endpoint.rs:225:        (true, false) => Ok(BindFamilies::V6Only),
crates\spt-net\src\net\endpoint.rs:226:        (false, true) => Ok(BindFamilies::V4Only),
crates\spt-net\src\net\endpoint.rs:228:            (true, false) => BindFamilies::V4Only,

[Showing results with pagination = limit: 20]


No SPT_LOG facility exists — the success-path trace would be net-new infra, so I'll defer it per doyle (failure-time structured trace is the W3 deliverable). `BindFamilies` is private, but I can derive bound families truthfully from the live bound sockets. Adding a `bound_families()` helper.


Edit

    /// This endpoint's dialable address (id + currently-known direct/relay paths).
    /// Serialise it (serde) to hand a peer a connect ticket.
    pub fn addr(&self) -> EndpointAddr {
        self.inner.addr()
    }

    /// This endpoint's dialable address (id + currently-known direct/relay paths).
    /// Serialise it (serde) to hand a peer a connect ticket.
    pub fn addr(&self) -> EndpointAddr {
        self.inner.addr()
    }

    /// A human label for the IP families this endpoint actually bound, derived
    /// from its live socket addresses — the truthful result of the per-family
    /// bind gate (REQ-NET-FAMILY-GATE). Surfaced in the join discovery trace
    /// (REQ-JOIN-DIAGNOSTICS) so a failed WAN join names which families it could
    /// even attempt. `"none"` only if no socket is bound (shouldn't happen).
    // [impl->REQ-JOIN-DIAGNOSTICS]
    pub fn bound_families(&self) -> &'static str {
        let mut v4 = false;
        let mut v6 = false;
        for s in self.inner.addr().ip_addrs() {
            if s.is_ipv4() {
                v4 = true;
            } else {
                v6 = true;
            }
        }
        match (v4, v6) {
            (true, true) => "IPv4+IPv6",
            (true, false) => "IPv4-only",
            (false, true) => "IPv6-only",
            (false, false) => "none",
        }
    }