240	const MEET_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(7);
241
242	/// The production joiner path: derive the ±1-window rendezvous ids for
243	/// `(subnet_name, totp_step)` and probe ALL THREE **concurrently**, each
244	/// bounded by [`MEET_PROBE_TIMEOUT`] — first answering listener wins (the
245	/// losers are dropped; racing pre-trust meet ids is harmless, module docs).
246	/// Each derived id is dialed by **id alone** (relay + n0 DNS / mDNS resolve
247	/// it — no address ticket); the winner's reply is the responder's real
248	/// address, dialed over the ceremony ALPN. The returned connection is ready
249	/// for [`run_initiator`](super::wire::run_initiator) — the ceremony itself
250	/// is unchanged.
251	// [impl->REQ-PAIR-5]
252	// [impl->REQ-SUBNET-2]
253	pub async fn dial_via_rendezvous(
254	    dialer: &NetEndpoint,
255	    subnet_name: &str,
256	    totp_step: u64,
257	) -> Result<Connection, MeetError> {
258	    let mut probes = tokio::task::JoinSet::new();
259	    for token in rendezvous_window(subnet_name, totp_step) {
260	        let pk = rendezvous_identity(&token).public_key();
261	        let id = EndpointId::from_bytes(&pk.to_bytes())
262	            .map_err(|e| MeetError::Protocol(format!("derived id invalid: {e}")))?;
263	        let dialer = dialer.clone_handle();
264	        probes.spawn(async move {
265	            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
266	                .await
267	                .map_err(|_| MeetError::Protocol("meet probe timed out".into()))
268	                .and_then(|r| r)
269	        });
270	    }
271	    let mut last = MeetError::Protocol("rendezvous window empty".into());
272	    while let Some(joined) = probes.join_next().await {
273	        match joined {
274	            Ok(Ok(real_addr)) => {
275	                probes.abort_all();
276	                return dialer
277	                    .connect_with_alpn(real_addr, SPT_PAIR_ALPN)
278	                    .await
279	                    .map_err(MeetError::Net);
280	            }
281	            Ok(Err(e)) => last = e,
282	            Err(e) => last = MeetError::Protocol(format!("probe task: {e}")),
283	        }
284	    }
285	    Err(last)
286	}
287
288	#[cfg(test)]
289	mod tests {
290	    use super::*;
291	    use std::net::SocketAddr;
292
293	    // [unit->REQ-PAIR-5] both sides independently derive the SAME rendezvous
294	    // keypair from (name, step) — the joiner can compute the public id the
295	    // responder listens under, with nothing but the two user inputs.
296	    #[test]
297	    fn derived_identity_is_deterministic_and_input_bound() {
298	        let a = rendezvous_endpoint_id("home", 100);
299	        assert_eq!(a, rendezvous_endpoint_id("home", 100), "deterministic");
300	        assert_ne!(a, rendezvous_endpoint_id("work", 100), "name-bound");
301	        assert_ne!(a, rendezvous_endpoint_id("home", 101), "step-bound");
302	    }
303
304	    // [unit->REQ-PAIR-5] the key seed is domain-separated from the token: the
305	    // derived identity's seed never equals the (relay-visible) token bytes.
306	    #[test]
307	    fn key_seed_is_not_the_token() {
308	        let token = rendezvous_token("home", 100);
309	        assert_ne!(rendezvous_identity(&token).seed(), token);
310	    }
311
312	    /// Bind a hermetic (loopback, no relay/discovery) endpoint on `alpns`.
313	    async fn hermetic(id: &Identity, alpns: Vec<Vec<u8>>) -> NetEndpoint {
314	        NetEndpoint::bind_scoped(
315	            id,
316	            RelayPolicy::Disabled,
317	            LocalDiscovery::Off,
318	            alpns,
319	            BindScope::Loopback,
320	        )
321	        .await
322	        .expect("hermetic bind")
323	    }
324
325	    /// The meet listener's derived-id address with its loopback sockets
326	    /// attached — what relay/discovery resolution would produce, built
327	    /// explicitly because the hermetic test has neither. The id in the addr
328	    /// is RE-DERIVED from (name, step), not copied from the listener, so the
329	    /// dial proves the listener really holds the token-derived key (TLS

const MEET_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(7);

/// The production joiner path: derive the ±1-window rendezvous ids for
/// `(subnet_name, totp_step)` and probe ALL THREE **concurrently**, each
/// bounded by [`MEET_PROBE_TIMEOUT`] — first answering listener wins (the
/// losers are dropped; racing pre-trust meet ids is harmless, module docs).
/// Each derived id is dialed by **id alone** (relay + n0 DNS / mDNS resolve
/// it — no address ticket); the winner's reply is the responder's real
/// address, dialed over the ceremony ALPN. The returned connection is ready
/// for [`run_initiator`](super::wire::run_initiator) — the ceremony itself
/// is unchanged.
// [impl->REQ-PAIR-5]
// [impl->REQ-SUBNET-2]
pub async fn dial_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<Connection, MeetError> {
    let mut probes = tokio::task::JoinSet::new();
    for token in rendezvous_window(subnet_name, totp_step) {
        let pk = rendezvous_identity(&token).public_key();
        let id = EndpointId::from_bytes(&pk.to_bytes())
            .map_err(|e| MeetError::Protocol(format!("derived id invalid: {e}")))?;
        let dialer = dialer.clone_handle();
        probes.spawn(async move {
            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
                .await
                .map_err(|_| MeetError::Protocol("meet probe timed out".into()))
                .and_then(|r| r)
        });
    }
    let mut last = MeetError::Protocol("rendezvous window empty".into());
    while let Some(joined) = probes.join_next().await {
        match joined {
            Ok(Ok(real_addr)) => {
                probes.abort_all();
                return dialer
                    .connect_with_alpn(real_addr, SPT_PAIR_ALPN)
                    .await
                    .map_err(MeetError::Net);
            }
            Ok(Err(e)) => last = e,
            Err(e) => last = MeetError::Protocol(format!("probe task: {e}")),
        }
    }
    Err(last)
}

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

    // [unit->REQ-PAIR-5] both sides independently derive the SAME rendezvous
    // keypair from (name, step) — the joiner can compute the public id the
    // responder listens under, with nothing but the two user inputs.
    #[test]
    fn derived_identity_is_deterministic_and_input_bound() {
        let a = rendezvous_endpoint_id("home", 100);
        assert_eq!(a, rendezvous_endpoint_id("home", 100), "deterministic");
        assert_ne!(a, rendezvous_endpoint_id("work", 100), "name-bound");
        assert_ne!(a, rendezvous_endpoint_id("home", 101), "step-bound");
    }

    // [unit->REQ-PAIR-5] the key seed is domain-separated from the token: the
    // derived identity's seed never equals the (relay-visible) token bytes.
    #[test]
    fn key_seed_is_not_the_token() {
        let token = rendezvous_token("home", 100);
        assert_ne!(rendezvous_identity(&token).seed(), token);
    }

    /// Bind a hermetic (loopback, no relay/discovery) endpoint on `alpns`.
    async fn hermetic(id: &Identity, alpns: Vec<Vec<u8>>) -> NetEndpoint {
        NetEndpoint::bind_scoped(
            id,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            alpns,
            BindScope::Loopback,
        )
        .await
        .expect("hermetic bind")
    }

    /// The meet listener's derived-id address with its loopback sockets
    /// attached — what relay/discovery resolution would produce, built
    /// explicitly because the hermetic test has neither. The id in the addr
    /// is RE-DERIVED from (name, step), not copied from the listener, so the
    /// dial proves the listener really holds the token-derived key (TLS


Key finding: `dial_via_rendezvous` already does meet-then-redial internally — `meet_at` yields the responder's real addr, then it re-dials over `SPT_PAIR_ALPN`. So W2 = hold that `real_addr` between phases; phase 2 reuses the existing direct-addr ceremony path. Design locked. W2 is large (~6 files) — let me record the plan to memory before implementing, so the design survives a reset.


Write

---
name: v0170-w2-design
description: v0.17.0 W2 two-phase meet-before-code join — crisp impl plan (grounded), for build/resume
metadata:
  type: project
---

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]].


File created successfully at: C:\Users\decid\.ccs\instances\bigscreen\projects\C--Users-decid-Documents-projects-spt-core\memory\v0170-w2-design.md (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]].


Plan saved. Implementing W2 step 1 — extract `meet_via_rendezvous` (addr-only) in meet.rs; `dial_via_rendezvous` becomes that + ceremony dial.


Edit

// [impl->REQ-PAIR-5]
// [impl->REQ-SUBNET-2]
pub async fn dial_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<Connection, MeetError> {
    let mut probes = tokio::task::JoinSet::new();
    for token in rendezvous_window(subnet_name, totp_step) {
        let pk = rendezvous_identity(&token).public_key();
        let id = EndpointId::from_bytes(&pk.to_bytes())
            .map_err(|e| MeetError::Protocol(format!("derived id invalid: {e}")))?;
        let dialer = dialer.clone_handle();
        probes.spawn(async move {
            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
                .await
                .map_err(|_| MeetError::Protocol("meet probe timed out".into()))
                .and_then(|r| r)
        });
    }
    let mut last = MeetError::Protocol("rendezvous window empty".into());
    while let Some(joined) = probes.join_next().await {
        match joined {
            Ok(Ok(real_addr)) => {
                probes.abort_all();
                return dialer
                    .connect_with_alpn(real_addr, SPT_PAIR_ALPN)
                    .await
                    .map_err(MeetError::Net);
            }
            Ok(Err(e)) => last = e,
            Err(e) => last = MeetError::Protocol(format!("probe task: {e}")),
        }
    }
    Err(last)
}

// [impl->REQ-PAIR-5]
// [impl->REQ-SUBNET-2]
pub async fn dial_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<Connection, MeetError> {
    let real_addr = meet_via_rendezvous(dialer, subnet_name, totp_step).await?;
    dialer
        .connect_with_alpn(real_addr, SPT_PAIR_ALPN)
        .await
        .map_err(MeetError::Net)
}

/// The rendezvous MEET only — resolve the seed-holder's **real, stable**
/// [`EndpointAddr`] by racing the ±1-window meet ids, WITHOUT the final ceremony
/// dial. This is the meet half of [`dial_via_rendezvous`] split out for the
/// two-phase join (REQ-JOIN-TWO-PHASE): the daemon resolves and HOLDS this
/// address, then collects the code and dials it over [`SPT_PAIR_ALPN`] for the
/// ceremony — so the code is fresh at the ceremony regardless of how long the
/// meet took. The returned address is the responder's stable endpoint (not the
/// rotating rendezvous id), so it does not age within the hold.
// [impl->REQ-PAIR-5] [impl->REQ-JOIN-TWO-PHASE]
pub async fn meet_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<EndpointAddr, MeetError> {
    let mut probes = tokio::task::JoinSet::new();
    for token in rendezvous_window(subnet_name, totp_step) {
        let pk = rendezvous_identity(&token).public_key();
        let id = EndpointId::from_bytes(&pk.to_bytes())
            .map_err(|e| MeetError::Protocol(format!("derived id invalid: {e}")))?;
        let dialer = dialer.clone_handle();
        probes.spawn(async move {
            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
                .await
                .map_err(|_| MeetError::Protocol("meet probe timed out".into()))
                .and_then(|r| r)
        });
    }
    let mut last = MeetError::Protocol("rendezvous window empty".into());
    while let Some(joined) = probes.join_next().await {
        match joined {
            Ok(Ok(real_addr)) => {
                probes.abort_all();
                return Ok(real_addr);
            }
            Ok(Err(e)) => last = e,
            Err(e) => last = MeetError::Protocol(format!("probe task: {e}")),
        }
    }
    Err(last)
}


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

// [impl->REQ-PAIR-5]
// [impl->REQ-SUBNET-2]
pub async fn dial_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<Connection, MeetError> {
    let mut probes = tokio::task::JoinSet::new();
    for token in rendezvous_window(subnet_name, totp_step) {
        let pk = rendezvous_identity(&token).public_key();
        let id = EndpointId::from_bytes(&pk.to_bytes())
            .map_err(|e| MeetError::Protocol(format!("derived id invalid: {e}")))?;
        let dialer = dialer.clone_handle();
        probes.spawn(async move {
            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
                .await
                .map_err(|_| MeetError::Protocol("meet probe timed out".into()))
                .and_then(|r| r)
        });
    }
    let mut last = MeetError::Protocol("rendezvous window empty".into());
    while let Some(joined) = probes.join_next().await {
        match joined {
            Ok(Ok(real_addr)) => {
                probes.abort_all();
                return dialer
                    .connect_with_alpn(real_addr, SPT_PAIR_ALPN)
                    .await
                    .map_err(MeetError::Net);
            }
            Ok(Err(e)) => last = e,
            Err(e) => last = MeetError::Protocol(format!("probe task: {e}")),
        }
    }
    Err(last)
}

// [impl->REQ-PAIR-5]
// [impl->REQ-SUBNET-2]
pub async fn dial_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<Connection, MeetError> {
    let real_addr = meet_via_rendezvous(dialer, subnet_name, totp_step).await?;
    dialer
        .connect_with_alpn(real_addr, SPT_PAIR_ALPN)
        .await
        .map_err(MeetError::Net)
}

/// The rendezvous MEET only — resolve the seed-holder's **real, stable**
/// [`EndpointAddr`] by racing the ±1-window meet ids, WITHOUT the final ceremony
/// dial. This is the meet half of [`dial_via_rendezvous`] split out for the
/// two-phase join (REQ-JOIN-TWO-PHASE): the daemon resolves and HOLDS this
/// address, then collects the code and dials it over [`SPT_PAIR_ALPN`] for the
/// ceremony — so the code is fresh at the ceremony regardless of how long the
/// meet took. The returned address is the responder's stable endpoint (not the
/// rotating rendezvous id), so it does not age within the hold.
// [impl->REQ-PAIR-5] [impl->REQ-JOIN-TWO-PHASE]
pub async fn meet_via_rendezvous(
    dialer: &NetEndpoint,
    subnet_name: &str,
    totp_step: u64,
) -> Result<EndpointAddr, MeetError> {
    let mut probes = tokio::task::JoinSet::new();
    for token in rendezvous_window(subnet_name, totp_step) {
        let pk = rendezvous_identity(&token).public_key();
        let id = EndpointId::from_bytes(&pk.to_bytes())
            .map_err(|e| MeetError::Protocol(format!("derived id invalid: {e}")))?;
        let dialer = dialer.clone_handle();
        probes.spawn(async move {
            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
                .await
                .map_err(|_| MeetError::Protocol("meet probe timed out".into()))
                .and_then(|r| r)
        });
    }
    let mut last = MeetError::Protocol("rendezvous window empty".into());
    while let Some(joined) = probes.join_next().await {
        match joined {
            Ok(Ok(real_addr)) => {
                probes.abort_all();
                return Ok(real_addr);
            }
            Ok(Err(e)) => last = e,
            Err(e) => last = MeetError::Protocol(format!("probe task: {e}")),
        }
    }
    Err(last)
}

+    let real_addr = meet_via_rendezvous(dialer, subnet_name, totp_step).await?;

+        .connect_with_alpn(real_addr, SPT_PAIR_ALPN)

+/// The rendezvous MEET only — resolve the seed-holder's **real, stable**

+/// [`EndpointAddr`] by racing the ±1-window meet ids, WITHOUT the final ceremony

+/// dial. This is the meet half of [`dial_via_rendezvous`] split out for the

+/// two-phase join (REQ-JOIN-TWO-PHASE): the daemon resolves and HOLDS this

+/// address, then collects the code and dials it over [`SPT_PAIR_ALPN`] for the

+/// ceremony — so the code is fresh at the ceremony regardless of how long the

+/// meet took. The returned address is the responder's stable endpoint (not the

+/// rotating rendezvous id), so it does not age within the hold.

-                    .connect_with_alpn(real_addr, SPT_PAIR_ALPN)

-                    .map_err(MeetError::Net);


Step 2 — `meet_seed_holder` in pairhost.rs. Checking the meet import first.


32:use spt_net::net::pairing::meet::{bind_rendezvous, dial_via_rendezvous};
33:use spt_net::net::pairing::ratelimit::{PairingRateLimiter, RateLimited, CEREMONY_TIMEOUT_SECS};
34:use spt_net::net::pairing::totp::{time_step, PERIOD_SECS};
35:use spt_net::net::pairing::wire::{run_initiator, run_responder, PairWireError};
429:            const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
432:                match dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
434:                    Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
592:        use spt_net::net::pairing::spake::PairError;


use spt_net::net::pairing::meet::{bind_rendezvous, dial_via_rendezvous, meet_via_rendezvous};


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

use spt_net::net::endpoint::{BindScope, Connection, LocalDiscovery, NetEndpoint, RelayPolicy};

+use spt_net::net::pairing::meet::{bind_rendezvous, dial_via_rendezvous, meet_via_rendezvous};

use spt_net::net::pairing::ratelimit::{PairingRateLimiter, RateLimited, CEREMONY_TIMEOUT_SECS};

use spt_net::net::pairing::wire::{run_initiator, run_responder, PairWireError};


                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
                }
            }
        }
    }
}

/// Map the wire error onto the CLI-facing failure classes.

                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
                }
            }
        }
    }
}

/// The MEET phase of the two-phase join (REQ-JOIN-TWO-PHASE): resolve a
/// seed-holder's real, stable pairing address via the rendezvous — NO ceremony,
/// no code — so the daemon can HOLD it while the operator enters the code. The
/// search-deadline sweep mirrors [`connect_seed_holder`]'s None branch exactly,
/// but returns the resolved address instead of an open ceremony connection (the
/// code phase re-dials this held address over `SPT_PAIR_ALPN`, no re-search).
// [impl->REQ-JOIN-TWO-PHASE]
pub async fn meet_seed_holder(
    endpoint: &NetEndpoint,
    subnet_name: &str,
) -> Result<spt_net::net::endpoint::EndpointAddr, JoinFail> {
    const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
    let start = std::time::Instant::now();
    loop {
        match meet_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
            Ok(addr) => return Ok(addr),
            Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
                return Err(JoinFail::NoSeedHolder(e.to_string()));
            }
            Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
        }
    }
}

/// Map the wire error onto the CLI-facing failure classes.


                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
                }
            }
        }
    }
}

/// Map the wire error onto the CLI-facing failure classes.

                    Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
                }
            }
        }
    }
}

/// The MEET phase of the two-phase join (REQ-JOIN-TWO-PHASE): resolve a
/// seed-holder's real, stable pairing address via the rendezvous — NO ceremony,
/// no code — so the daemon can HOLD it while the operator enters the code. The
/// search-deadline sweep mirrors [`connect_seed_holder`]'s None branch exactly,
/// but returns the resolved address instead of an open ceremony connection (the
/// code phase re-dials this held address over `SPT_PAIR_ALPN`, no re-search).
// [impl->REQ-JOIN-TWO-PHASE]
pub async fn meet_seed_holder(
    endpoint: &NetEndpoint,
    subnet_name: &str,
) -> Result<spt_net::net::endpoint::EndpointAddr, JoinFail> {
    const SEARCH_DEADLINE: Duration = Duration::from_secs(75);
    let start = std::time::Instant::now();
    loop {
        match meet_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {
            Ok(addr) => return Ok(addr),
            Err(e) if start.elapsed() >= SEARCH_DEADLINE => {
                return Err(JoinFail::NoSeedHolder(e.to_string()));
            }
            Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,
        }
    }
}

/// Map the wire error onto the CLI-facing failure classes.

+/// The MEET phase of the two-phase join (REQ-JOIN-TWO-PHASE): resolve a

+/// seed-holder's real, stable pairing address via the rendezvous — NO ceremony,

+/// no code — so the daemon can HOLD it while the operator enters the code. The

+/// search-deadline sweep mirrors [`connect_seed_holder`]'s None branch exactly,

+/// but returns the resolved address instead of an open ceremony connection (the

+/// code phase re-dials this held address over `SPT_PAIR_ALPN`, no re-search).

+) -> Result<spt_net::net::endpoint::EndpointAddr, JoinFail> {

+    const SEARCH_DEADLINE: Duration = Duration::from_secs(75);

+    let start = std::time::Instant::now();

+        match meet_via_rendezvous(endpoint, subnet_name, time_step(now_secs())).await {

+            Ok(addr) => return Ok(addr),

+            Err(e) if start.elapsed() >= SEARCH_DEADLINE => {

+                return Err(JoinFail::NoSeedHolder(e.to_string()));

+            Err(_) => tokio::time::sleep(Duration::from_millis(500)).await,

fn classify_wire_error(e: PairWireError) -> JoinFail {


Step 3 — nethost `pair_meet`. Reading the existing `pair_join` to mirror.


812	                Ok(inner) => inner,
813	                Err(_elapsed) => Err(io::Error::other(format!(
814	                    "broker QUIC op exceeded the {dur:?} bound (peer unresponsive) — \
815	                     failing as an ordinary per-peer error"
816	                ))),
817	            }
818	        })
819	    }
820
821	    /// Run the joiner leg of the pairing ceremony (M7 D3, REQ-SUBNET-2) on
822	    /// the host's runtime — the broker IPC `pair-join` arm. The ceremony
823	    /// must run HERE because it binds and pins this endpoint's real node
824	    /// pubkey. Returns the rendered trust outcome, or the classified failure.
825	    // [impl->REQ-SUBNET-2]
826	    pub fn pair_join(
827	        &self,
828	        subnet: &str,
829	        code: &str,
830	        responder_addr: Option<EndpointAddr>,
831	    ) -> Result<String, crate::pairhost::JoinFail> {
832	        let endpoint = Arc::clone(&self.endpoint);
833	        self.runtime.handle().block_on(async move {
834	            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
835	        })
836	    }
837
838	    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).
839	    pub fn node_id_hex(&self) -> String {
840	        self.endpoint.node_id_hex()
841	    }
842
843	    /// The endpoint's dialable address (id + currently-known direct/relay paths).
844	    pub fn addr(&self) -> EndpointAddr {
845	        self.endpoint.addr()
846	    }
847
848	    /// [`Self::addr`] as the opaque JSON the IPC status reply carries.
849	    pub fn addr_json(&self) -> serde_json::Value {
850	        serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null)
851	    }
852
853	    /// Live connections (accepted + dialed) currently held.
854	    pub fn conn_count(&self) -> u64 {
855	        self.shared.conns.lock().unwrap().len() as u64
856	    }
857
858	    /// Dial a peer over `SPT_NET_ALPN`, registering the connection in the table.
859	    /// Bridges sync→async via the host's own runtime (the broker's dispatch
860	    /// threads block here; the future runs on the net worker threads).
861	    // [impl->REQ-DAEMON-2]
862	    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
863	        let endpoint = Arc::clone(&self.endpoint);
864	        let shared = Arc::clone(&self.shared);
865	        let local_id = self.local_id;
866	        let membership = self.membership.clone();
867	        // Snapshot the (opt-in) roster exchange + our live address for this dial.
868	        let roster_rx = self.roster.lock().unwrap().clone();
869	        let self_addr = serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null);
870	        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): the `connect` + `prove_membership`
871	        // awaits below are the exact unbounded sites a dead peer black-holes — wrap
872	        // them so a non-responsive peer fails as an ordinary error within the bound.
873	        self.bounded_block_on(async move {
874	            let conn = endpoint
875	                .connect(addr)
876	                .await
877	                .map_err(|e| io::Error::other(e.to_string()))?;
878	            // Mesh-D2: prove membership in-band before the connection is usable.
879	            // A failed/empty proof drops the dial (the conn never registers).
880	            // Mesh-D4: on success, a roster leg rides the same control stream.
881	            let proven = match membership {

                Ok(inner) => inner,
                Err(_elapsed) => Err(io::Error::other(format!(
                    "broker QUIC op exceeded the {dur:?} bound (peer unresponsive) — \
                     failing as an ordinary per-peer error"
                ))),
            }
        })
    }

    /// Run the joiner leg of the pairing ceremony (M7 D3, REQ-SUBNET-2) on
    /// the host's runtime — the broker IPC `pair-join` arm. The ceremony
    /// must run HERE because it binds and pins this endpoint's real node
    /// pubkey. Returns the rendered trust outcome, or the classified failure.
    // [impl->REQ-SUBNET-2]
    pub fn pair_join(
        &self,
        subnet: &str,
        code: &str,
        responder_addr: Option<EndpointAddr>,
    ) -> Result<String, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).
    pub fn node_id_hex(&self) -> String {
        self.endpoint.node_id_hex()
    }

    /// The endpoint's dialable address (id + currently-known direct/relay paths).
    pub fn addr(&self) -> EndpointAddr {
        self.endpoint.addr()
    }

    /// [`Self::addr`] as the opaque JSON the IPC status reply carries.
    pub fn addr_json(&self) -> serde_json::Value {
        serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null)
    }

    /// Live connections (accepted + dialed) currently held.
    pub fn conn_count(&self) -> u64 {
        self.shared.conns.lock().unwrap().len() as u64
    }

    /// Dial a peer over `SPT_NET_ALPN`, registering the connection in the table.
    /// Bridges sync→async via the host's own runtime (the broker's dispatch
    /// threads block here; the future runs on the net worker threads).
    // [impl->REQ-DAEMON-2]
    pub fn dial(&self, addr: EndpointAddr) -> io::Result<(u64, String)> {
        let endpoint = Arc::clone(&self.endpoint);
        let shared = Arc::clone(&self.shared);
        let local_id = self.local_id;
        let membership = self.membership.clone();
        // Snapshot the (opt-in) roster exchange + our live address for this dial.
        let roster_rx = self.roster.lock().unwrap().clone();
        let self_addr = serde_json::to_value(self.endpoint.addr()).unwrap_or(serde_json::Value::Null);
        // Bounded (REQ-HAZARD-BROKER-QUIC-DEADLINE): the `connect` + `prove_membership`
        // awaits below are the exact unbounded sites a dead peer black-holes — wrap
        // them so a non-responsive peer fails as an ordinary error within the bound.
        self.bounded_block_on(async move {
            let conn = endpoint
                .connect(addr)
                .await
                .map_err(|e| io::Error::other(e.to_string()))?;
            // Mesh-D2: prove membership in-band before the connection is usable.
            // A failed/empty proof drops the dial (the conn never registers).
            // Mesh-D4: on success, a roster leg rides the same control stream.
            let proven = match membership {


Add `pair_meet` after `pair_join` in nethost.rs.


        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).

        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

    /// The MEET phase of the two-phase join (REQ-JOIN-TWO-PHASE): resolve a
    /// seed-holder's real, stable pairing address via the rendezvous — no code,
    /// no ceremony — so the broker can hold it while the operator enters the
    /// code. Same runtime/endpoint discipline as [`Self::pair_join`].
    // [impl->REQ-JOIN-TWO-PHASE]
    pub fn pair_meet(&self, subnet: &str) -> Result<EndpointAddr, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime
            .handle()
            .block_on(async move { crate::pairhost::meet_seed_holder(&endpoint, subnet).await })
    }

    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).


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

        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).

        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

    /// The MEET phase of the two-phase join (REQ-JOIN-TWO-PHASE): resolve a
    /// seed-holder's real, stable pairing address via the rendezvous — no code,
    /// no ceremony — so the broker can hold it while the operator enters the
    /// code. Same runtime/endpoint discipline as [`Self::pair_join`].
    // [impl->REQ-JOIN-TWO-PHASE]
    pub fn pair_meet(&self, subnet: &str) -> Result<EndpointAddr, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime
            .handle()
            .block_on(async move { crate::pairhost::meet_seed_holder(&endpoint, subnet).await })
    }

    /// The node's stable name (lowercase-hex pubkey == iroh EndpointId).

+    /// The MEET phase of the two-phase join (REQ-JOIN-TWO-PHASE): resolve a

+    /// seed-holder's real, stable pairing address via the rendezvous — no code,

+    /// no ceremony — so the broker can hold it while the operator enters the

+    /// code. Same runtime/endpoint discipline as [`Self::pair_join`].

+    pub fn pair_meet(&self, subnet: &str) -> Result<EndpointAddr, crate::pairhost::JoinFail> {

+        let endpoint = Arc::clone(&self.endpoint);

+            .block_on(async move { crate::pairhost::meet_seed_holder(&endpoint, subnet).await })


Step 5 — msg.rs wire types + KIND constants. Locating the KIND definitions.


24:pub const KIND_SPAWN: &str = "spawn";
26:pub const KIND_SPAWNED: &str = "spawned";
28:pub const KIND_SUBSCRIBE: &str = "subscribe";
34:pub const KIND_UNSUBSCRIBE: &str = "unsubscribe";
36:pub const KIND_INPUT: &str = "input";
38:pub const KIND_RESIZE: &str = "resize";
40:pub const KIND_KILL: &str = "kill";
46:pub const KIND_ADAPTER_APPLY: &str = "adapter-apply";
48:pub const KIND_OUTPUT: &str = "output";
50:pub const KIND_EXIT: &str = "exit";
57:pub const KIND_SUBSCRIBED: &str = "subscribed";
63:pub const KIND_SIZE: &str = "size";
70:pub const KIND_DISPLACED: &str = "displaced";
77:pub const KIND_VIEWER_EVICTED: &str = "viewer_evicted";
82:pub const KIND_APPLIED: &str = "applied";
84:pub const KIND_ERROR: &str = "error";
88:pub const KIND_NET_STATUS: &str = "net-status";
90:pub const KIND_NET_STATUS_REPLY: &str = "net-status-reply";
93:pub const KIND_NET_DIAL: &str = "net-dial";
100:pub const KIND_NET_DIAL_LOOPBACK: &str = "net-dial-loopback";
103:pub const KIND_NET_DIALED: &str = "net-dialed";
106:pub const KIND_NET_STREAM_OPEN: &str = "net-stream-open";
108:pub const KIND_NET_STREAM_OPENED: &str = "net-stream-opened";
111:pub const KIND_NET_STREAM_SEND: &str = "net-stream-send";
113:pub const KIND_NET_SENT: &str = "net-sent";
117:pub const KIND_NET_STREAMS: &str = "net-streams";
119:pub const KIND_NET_STREAMS_REPLY: &str = "net-streams-reply";
122:pub const KIND_NET_STREAM_SUBSCRIBE: &str = "net-stream-subscribe";
124:pub const KIND_NET_STREAM_DATA: &str = "net-stream-data";
127:pub const KIND_NET_STREAM_EOF: &str = "net-stream-eof";
132:pub const KIND_NET_PRESENCE_SUBSCRIBE: &str = "net-presence-subscribe";
134:pub const KIND_NET_PRESENCE_EVENT: &str = "net-presence-event";
144:pub const KIND_PAIR_JOIN: &str = "pair-join";
146:pub const KIND_PAIR_JOINED: &str = "pair-joined";
150:pub const KIND_SESSIONS: &str = "sessions";
152:pub const KIND_SESSIONS_REPLY: &str = "sessions-reply";
163:pub const KIND_ENDPOINT_INPUT: &str = "endpoint-input";
167:pub const KIND_ENDPOINT_INJECTED: &str = "endpoint-injected";
176:pub const KIND_BRAIN_RESTART: &str = "brain-restart";
182:pub const KIND_BRAIN_RESTARTED: &str = "brain-restarted";


142	/// REQ-SUBNET-2). The ceremony must run broker-side: it dials via the
143	/// rendezvous and binds/pins the broker-owned endpoint's REAL node pubkey.
144	pub const KIND_PAIR_JOIN: &str = "pair-join";
145	/// Broker→brain: the join ceremony's outcome (success or classified failure).
146	pub const KIND_PAIR_JOINED: &str = "pair-joined";
147
148	/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound
149	/// dispatcher's session→endpoint resolution (D9-1).
150	pub const KIND_SESSIONS: &str = "sessions";
151	/// Broker→brain reply to [`KIND_SESSIONS`].

/// REQ-SUBNET-2). The ceremony must run broker-side: it dials via the
/// rendezvous and binds/pins the broker-owned endpoint's REAL node pubkey.
pub const KIND_PAIR_JOIN: &str = "pair-join";
/// Broker→brain: the join ceremony's outcome (success or classified failure).
pub const KIND_PAIR_JOINED: &str = "pair-joined";

/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound
/// dispatcher's session→endpoint resolution (D9-1).
pub const KIND_SESSIONS: &str = "sessions";
/// Broker→brain reply to [`KIND_SESSIONS`].


pub const KIND_PAIR_JOIN: &str = "pair-join";
/// Broker→brain: the join ceremony's outcome (success or classified failure).
pub const KIND_PAIR_JOINED: &str = "pair-joined";

pub const KIND_PAIR_JOIN: &str = "pair-join";
/// Broker→brain: the join ceremony's outcome (success or classified failure).
pub const KIND_PAIR_JOINED: &str = "pair-joined";

/// Brain→broker: phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE) — MEET a
/// seed-holder for the named subnet (no code). The daemon resolves and holds the
/// member's real pairing address, replying [`KIND_MET_MEMBER`].
pub const KIND_PAIR_MEET: &str = "pair-meet";
/// Broker→brain: a seed-holder was met — its real address is held under a
/// session id, awaiting the code ([`KIND_PAIR_CODE_SUBMIT`]).
pub const KIND_MET_MEMBER: &str = "met-member";
/// Brain→broker: phase 2 of the two-phase join — submit the code for a held
/// meet session. The daemon dials the HELD address (no re-search) and runs the
/// SPAKE2 ceremony, replying [`KIND_PAIR_JOINED`]. A wrong-code retry re-submits
/// against the same session id (ceremony only).
pub const KIND_PAIR_CODE_SUBMIT: &str = "pair-code-submit";


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)

pub const KIND_PAIR_JOIN: &str = "pair-join";
/// Broker→brain: the join ceremony's outcome (success or classified failure).
pub const KIND_PAIR_JOINED: &str = "pair-joined";

/// Brain→broker: phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE) — MEET a
/// seed-holder for the named subnet (no code). The daemon resolves and holds the
/// member's real pairing address, replying [`KIND_MET_MEMBER`].
pub const KIND_PAIR_MEET: &str = "pair-meet";
/// Broker→brain: a seed-holder was met — its real address is held under a
/// session id, awaiting the code ([`KIND_PAIR_CODE_SUBMIT`]).
pub const KIND_MET_MEMBER: &str = "met-member";
/// Brain→broker: phase 2 of the two-phase join — submit the code for a held
/// meet session. The daemon dials the HELD address (no re-search) and runs the
/// SPAKE2 ceremony, replying [`KIND_PAIR_JOINED`]. A wrong-code retry re-submits
/// against the same session id (ceremony only).
pub const KIND_PAIR_CODE_SUBMIT: &str = "pair-code-submit";

+/// Brain→broker: phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE) — MEET a

+/// seed-holder for the named subnet (no code). The daemon resolves and holds the

+/// member's real pairing address, replying [`KIND_MET_MEMBER`].

+pub const KIND_PAIR_MEET: &str = "pair-meet";

+/// Broker→brain: a seed-holder was met — its real address is held under a

+/// session id, awaiting the code ([`KIND_PAIR_CODE_SUBMIT`]).

+pub const KIND_MET_MEMBER: &str = "met-member";

+/// Brain→broker: phase 2 of the two-phase join — submit the code for a held

+/// meet session. The daemon dials the HELD address (no re-search) and runs the

+/// SPAKE2 ceremony, replying [`KIND_PAIR_JOINED`]. A wrong-code retry re-submits

+/// against the same session id (ceremony only).

+pub const KIND_PAIR_CODE_SUBMIT: &str = "pair-code-submit";

/// Brain→broker: list hosted sessions (id + owning endpoint). The inbound

/// dispatcher's session→endpoint resolution (D9-1).

pub const KIND_SESSIONS: &str = "sessions";


Now the payload structs in msg.rs (after `PairJoinReply`).


/// `net-dialed` event — a dial durably landed (fresh or deduped).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetDialed {

/// `pair-meet` payload — phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE).
/// Only the subnet name: the meet routes on the PUBLIC `(name, TOTP-epoch)`,
/// never the secret code (the code is collected after a member is found).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairMeetReq {
    /// The subnet to join (names the rendezvous).
    pub subnet: String,
}

/// `met-member` reply — a seed-holder was met and its real pairing address is
/// HELD daemon-side under `session_id` (bounded by a 5-minute wait-for-code
/// timeout). The CLI prompts for the code, then sends [`PairCodeSubmit`] with
/// this id.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetMember {
    /// The daemon-minted id keying the held address for the code phase.
    pub session_id: u64,
    /// The met member's stable name (lowercase-hex pubkey), proven by the meet
    /// exchange — surfaced so the operator sees WHO answered before entering the
    /// code. (A human label is unknown pre-trust; it arrives with the roster in
    /// the ceremony.)
    pub member_hex: String,
}

/// `pair-code-submit` payload — phase 2 of the two-phase join: the code for a
/// held meet session. A wrong-code retry re-sends against the SAME `session_id`
/// (the daemon re-dials the held address only — no re-search).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairCodeSubmit {
    /// The held meet session (from [`MetMember`]).
    pub session_id: u64,
    /// The current 6-digit TOTP code (the SPAKE2 password — never logged or
    /// journaled).
    pub code: String,
}

/// `net-dialed` event — a dial durably landed (fresh or deduped).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetDialed {


/// `pair-meet` payload — phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE).
/// Only the subnet name: the meet routes on the PUBLIC `(name, TOTP-epoch)`,
/// never the secret code (the code is collected after a member is found).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairMeetReq {
    /// The subnet to join (names the rendezvous).
    pub subnet: String,
}

/// `met-member` reply — a seed-holder was met and its real pairing address is
/// HELD daemon-side under `session_id` (bounded by a 5-minute wait-for-code
/// timeout). The CLI prompts for the code, then sends [`PairCodeSubmit`] with
/// this id.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetMember {
    /// The daemon-minted id keying the held address for the code phase.
    pub session_id: u64,
    /// The met member's stable name (lowercase-hex pubkey), proven by the meet
    /// exchange — surfaced so the operator sees WHO answered before entering the
    /// code. (A human label is unknown pre-trust; it arrives with the roster in
    /// the ceremony.)
    pub member_hex: String,
}

/// `pair-code-submit` payload — phase 2 of the two-phase join: the code for a
/// held meet session. A wrong-code retry re-sends against the SAME `session_id`
/// (the daemon re-dials the held address only — no re-search).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairCodeSubmit {
    /// The held meet session (from [`MetMember`]).
    pub session_id: u64,
    /// The current 6-digit TOTP code (the SPAKE2 password — never logged or
    /// journaled).
    pub code: String,
}

/// `net-dialed` event — a dial durably landed (fresh or deduped).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetDialed {

+/// `pair-meet` payload — phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE).

+/// Only the subnet name: the meet routes on the PUBLIC `(name, TOTP-epoch)`,

+/// never the secret code (the code is collected after a member is found).

+#[derive(Debug, Clone, Serialize, Deserialize)]

+    /// The subnet to join (names the rendezvous).

+/// `met-member` reply — a seed-holder was met and its real pairing address is

+/// HELD daemon-side under `session_id` (bounded by a 5-minute wait-for-code

+/// timeout). The CLI prompts for the code, then sends [`PairCodeSubmit`] with

+    /// The daemon-minted id keying the held address for the code phase.

+    /// The met member's stable name (lowercase-hex pubkey), proven by the meet

+    /// exchange — surfaced so the operator sees WHO answered before entering the

+    /// code. (A human label is unknown pre-trust; it arrives with the roster in

+/// `pair-code-submit` payload — phase 2 of the two-phase join: the code for a

+/// held meet session. A wrong-code retry re-sends against the SAME `session_id`

+/// (the daemon re-dials the held address only — no re-search).

+    /// The held meet session (from [`MetMember`]).

+    /// The current 6-digit TOTP code (the SPAKE2 password — never logged or


Step 4 — broker.rs. Examining the Broker struct (fields, id-minting, net access) to add `pair_holds`.


Grep

pub struct Broker|struct Broker|next_id|AtomicU64|fetch_add|conns:|streams:|sessions:|self\.net\b|Instant


36:use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
40:use std::time::{Duration, Instant};
197:    last_ok: Instant,
226:    /// thread (`Arc<AtomicU64>`). Bumped on every [`become_controller`]; the
233:    controller_epoch: Arc<AtomicU64>,
262:    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,
265:    delivered_through: Arc<AtomicU64>,
292:            controller_epoch: Arc::new(AtomicU64::new(0)),
297:            delivered_through: Arc::new(AtomicU64::new(0)),
359:        let now = Instant::now();
408:        let epoch = self.controller_epoch.fetch_add(1, Ordering::AcqRel) + 1;
428:            last_ok: Instant::now(),
722:fn advance_delivered(dt: &AtomicU64, seq: u64) {
746:fn contiguous_advance(dt: &AtomicU64, seq: u64) {
766:    delivered_through: Arc<AtomicU64>,
768:    controller_epoch: Arc<AtomicU64>,
1157:        let deadline = Instant::now() + inject_commit_deadline();
1160:            let now = Instant::now();
1178:                    let cap = deadline.saturating_duration_since(Instant::now());
1247:pub struct Broker {
1249:    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
1250:    next_id: AtomicU64,
1334:            sessions: Arc::new(Mutex::new(HashMap::new())),
1335:            next_id: AtomicU64::new(1),
1360:        self.net.set(host).is_ok()
1371:        self.net.get()
1452:        self.net.get().map(|h| h.conn_count() as usize).unwrap_or(0)
1570:                        sessions: rows
1622:        // Same for net streams: the streams + their rings persist (D4b).
1623:        if let Some(host) = self.net.get() {
1658:        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
2068:        let reply = match self.net.get() {
2259:        let host = self.net.get().ok_or("net disabled")?;
2309:        let host = self.net.get().ok_or("net disabled")?;
2339:        let host = self.net.get().ok_or("net disabled")?;
2343:                streams: host.stream_infos(),
2361:        let host = self.net.get().ok_or("net disabled")?;
2378:        let host = self.net.get().ok_or("net disabled")?;
2795:            last_ok: Instant::now(),
2835:            c.last_ok = Instant::now()

[Showing results with pagination = limit: 40]


1247	pub struct Broker {
1248	    listener: LocalSocketTransport,
1249	    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
1250	    next_id: AtomicU64,
1251	    log_cap_chunks: usize,
1252	    /// The exactly-once anchor for idempotent (`op_id`-carrying) effects.
1253	    journal: Arc<EffectJournal>,
1254	    /// The broker-owned net endpoint + live QUIC connections (D4a, ADR-0004 §B).
1255	    /// Empty when bound without a network host (net-disabled / most tests) OR
1256	    /// when net is still being brought up (REQ-DAEMON-9 boot-race self-heal):
1257	    /// a `OnceLock` so the background net-retry can ATTACH the host after bind
1258	    /// without a relock on the hot effect-dispatch read path (`.get()` is
1259	    /// lock-free). Settable exactly once — the first host wins.
1260	    net: std::sync::OnceLock<NetHost>,
1261	    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a
1262	    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the
1263	    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker
1264	    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
1265	    /// `Daemon::run` after it spawns the supervisor.
1266	    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
1267	}
1268
1269	/// The distinct `(endpoint, install_dir)` pairs whose session runs `adapter`,
1270	/// from per-session `(endpoint, adapter, install_dir)` rows. Sessions with no
1271	/// recorded install dir are dropped (a live update needs a target). Pure (no
1272	/// lock, no I/O) so the live-update endpoint-selection is unit-testable without
1273	/// constructing real `HostedSession`s. (W3a, ADR-0025)
1274	// [impl->REQ-ADAPTER-LIVE-UPDATE]
1275	fn select_endpoints_running_adapter(
1276	    rows: impl IntoIterator<Item = (String, String, Option<std::path::PathBuf>)>,
1277	    adapter: &str,
1278	) -> Vec<(String, std::path::PathBuf)> {
1279	    let mut out: Vec<(String, std::path::PathBuf)> = rows
1280	        .into_iter()
1281	        .filter_map(|(endpoint, adp, dir)| {
1282	            (adp == adapter).then_some(dir.map(|d| (endpoint, d)))?
1283	        })
1284	        .collect();
1285	    out.sort();
1286	    out.dedup();
1287	    out
1288	}
1289
1290	impl Broker {
1291	    /// Bind the broker's listening endpoint at the namespaced `name`, with the
1292	    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).
1293	    pub fn bind(name: &str) -> io::Result<Arc<Self>> {
1294	        Self::bind_in(name, default_journal_path())
1295	    }
1296
1297	    /// Bind with an explicit effect-journal path — the testable seam (tests point
1298	    /// the broker-owned anchor at a temp file rather than the shared home).
1299	    pub fn bind_in(name: &str, journal_path: std::path::PathBuf) -> io::Result<Arc<Self>> {
1300	        Self::bind_in_with_net(name, journal_path, None)
1301	    }
1302
1303	    /// Bind with an already-started broker-owned network host (D4a). Taking the
1304	    /// host (not its config) keeps a net bind failure separable from a listener
1305	    /// bind failure — the daemon degrades to a net-less broker rather than
1306	    /// losing PTY hosting (see `Daemon::run`).
1307	    // [impl->REQ-DAEMON-2]
1308	    pub fn bind_in_with_net(
1309	        name: &str,
1310	        journal_path: std::path::PathBuf,
1311	        net: Option<NetHost>,
1312	    ) -> io::Result<Arc<Self>> {
1313	        Self::bind_in_with_net_cap(name, journal_path, net, DEFAULT_LOG_CHUNKS)
1314	    }
1315
1316	    /// [`bind_in_with_net`](Self::bind_in_with_net) with an explicit output-log
1317	    /// ring cap (the per-session eviction bound). Production binds at
1318	    /// [`DEFAULT_LOG_CHUNKS`]; the restoration D4-2 resume harness binds a **tiny**
1319	    /// cap to force ring eviction and exercise the post-eviction floor clamp (a
1320	    /// `resume_seq` below the surviving ring floor must resume forward, losing only
1321	    /// the evicted tail — never gap-erroring the live stream).
1322	    pub fn bind_in_with_net_cap(
1323	        name: &str,
1324	        journal_path: std::path::PathBuf,
1325	        net: Option<NetHost>,
1326	        log_cap_chunks: usize,
1327	    ) -> io::Result<Arc<Self>> {
1328	        let net_cell = std::sync::OnceLock::new();
1329	        if let Some(host) = net {
1330	            let _ = net_cell.set(host);
1331	        }
1332	        Ok(Arc::new(Broker {
1333	            listener: LocalSocketTransport::bind(name)?,
1334	            sessions: Arc::new(Mutex::new(HashMap::new())),
1335	            next_id: AtomicU64::new(1),
1336	            log_cap_chunks,
1337	            journal: Arc::new(EffectJournal::open(journal_path)?),
1338	            net: net_cell,
1339	            brain_restart: std::sync::OnceLock::new(),
1340	        }))
1341	    }

pub struct Broker {
    listener: LocalSocketTransport,
    sessions: Arc<Mutex<HashMap<u64, HostedSession>>>,
    next_id: AtomicU64,
    log_cap_chunks: usize,
    /// The exactly-once anchor for idempotent (`op_id`-carrying) effects.
    journal: Arc<EffectJournal>,
    /// The broker-owned net endpoint + live QUIC connections (D4a, ADR-0004 §B).
    /// Empty when bound without a network host (net-disabled / most tests) OR
    /// when net is still being brought up (REQ-DAEMON-9 boot-race self-heal):
    /// a `OnceLock` so the background net-retry can ATTACH the host after bind
    /// without a relock on the hot effect-dispatch read path (`.get()` is
    /// lock-free). Settable exactly once — the first host wins.
    net: std::sync::OnceLock<NetHost>,
    /// The broker→supervisor planned-restart signal (ADR-0018 D3-3): a
    /// `KIND_BRAIN_RESTART` handler raises it so the brain supervisor cycles the
    /// child onto a freshly-swapped binary. Empty on a bare broker (a test broker
    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}

/// The distinct `(endpoint, install_dir)` pairs whose session runs `adapter`,
/// from per-session `(endpoint, adapter, install_dir)` rows. Sessions with no
/// recorded install dir are dropped (a live update needs a target). Pure (no
/// lock, no I/O) so the live-update endpoint-selection is unit-testable without
/// constructing real `HostedSession`s. (W3a, ADR-0025)
// [impl->REQ-ADAPTER-LIVE-UPDATE]
fn select_endpoints_running_adapter(
    rows: impl IntoIterator<Item = (String, String, Option<std::path::PathBuf>)>,
    adapter: &str,
) -> Vec<(String, std::path::PathBuf)> {
    let mut out: Vec<(String, std::path::PathBuf)> = rows
        .into_iter()
        .filter_map(|(endpoint, adp, dir)| {
            (adp == adapter).then_some(dir.map(|d| (endpoint, d)))?
        })
        .collect();
    out.sort();
    out.dedup();
    out
}

impl Broker {
    /// Bind the broker's listening endpoint at the namespaced `name`, with the
    /// effect journal at the default path (`<spt_home>/daemon-effects.log`).
    pub fn bind(name: &str) -> io::Result<Arc<Self>> {
        Self::bind_in(name, default_journal_path())
    }

    /// Bind with an explicit effect-journal path — the testable seam (tests point
    /// the broker-owned anchor at a temp file rather than the shared home).
    pub fn bind_in(name: &str, journal_path: std::path::PathBuf) -> io::Result<Arc<Self>> {
        Self::bind_in_with_net(name, journal_path, None)
    }

    /// Bind with an already-started broker-owned network host (D4a). Taking the
    /// host (not its config) keeps a net bind failure separable from a listener
    /// bind failure — the daemon degrades to a net-less broker rather than
    /// losing PTY hosting (see `Daemon::run`).
    // [impl->REQ-DAEMON-2]
    pub fn bind_in_with_net(
        name: &str,
        journal_path: std::path::PathBuf,
        net: Option<NetHost>,
    ) -> io::Result<Arc<Self>> {
        Self::bind_in_with_net_cap(name, journal_path, net, DEFAULT_LOG_CHUNKS)
    }

    /// [`bind_in_with_net`](Self::bind_in_with_net) with an explicit output-log
    /// ring cap (the per-session eviction bound). Production binds at
    /// [`DEFAULT_LOG_CHUNKS`]; the restoration D4-2 resume harness binds a **tiny**
    /// cap to force ring eviction and exercise the post-eviction floor clamp (a
    /// `resume_seq` below the surviving ring floor must resume forward, losing only
    /// the evicted tail — never gap-erroring the live stream).
    pub fn bind_in_with_net_cap(
        name: &str,
        journal_path: std::path::PathBuf,
        net: Option<NetHost>,
        log_cap_chunks: usize,
    ) -> io::Result<Arc<Self>> {
        let net_cell = std::sync::OnceLock::new();
        if let Some(host) = net {
            let _ = net_cell.set(host);
        }
        Ok(Arc::new(Broker {
            listener: LocalSocketTransport::bind(name)?,
            sessions: Arc::new(Mutex::new(HashMap::new())),
            next_id: AtomicU64::new(1),
            log_cap_chunks,
            journal: Arc::new(EffectJournal::open(journal_path)?),
            net: net_cell,
            brain_restart: std::sync::OnceLock::new(),
        }))
    }


Adding the `pair_holds` field + `PairHold` type to the Broker struct.


Edit

    /// Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}

    /// Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a
    /// `session_id → (subnet, real address, 5-min deadline)` between
    /// [`KIND_PAIR_MEET`] and [`KIND_PAIR_CODE_SUBMIT`]. The code phase dials the
    /// held address (no re-search); a wrong-code retry keeps the entry. Pairing
    /// is never journaled (the code must not persist) — this is in-memory only,
    /// and a daemon restart simply makes the operator re-run the meet.
    pair_holds: Mutex<HashMap<u64, PairHold>>,
}

/// One held meet result for the two-phase join (see [`Broker::pair_holds`]).
struct PairHold {
    subnet: String,
    addr: EndpointAddr,
    /// When the wait-for-code window expires (meet + 5 minutes).
    deadline: Instant,
}

/// The two-phase join's wait-for-code window: the daemon holds the met address
/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).
const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);


<tool_use_error>String to replace not found in file.
String:     /// Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}</tool_use_error>

Error: String to replace not found in file.
String:     /// Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}


    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}

    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a
    /// `session_id → (subnet, real address, 5-min deadline)` between
    /// [`KIND_PAIR_MEET`] and [`KIND_PAIR_CODE_SUBMIT`]. The code phase dials the
    /// held address (no re-search); a wrong-code retry keeps the entry. Pairing
    /// is never journaled (the code must not persist) — in-memory only, and a
    /// daemon restart simply makes the operator re-run the meet.
    pair_holds: Mutex<HashMap<u64, PairHold>>,
}

/// One held meet result for the two-phase join (see [`Broker::pair_holds`]).
struct PairHold {
    subnet: String,
    addr: EndpointAddr,
    /// When the wait-for-code window expires (meet + 5 minutes).
    deadline: Instant,
}

/// The two-phase join's wait-for-code window: the daemon holds the met address
/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).
const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);


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)

    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
}

    /// with no supervisor) — the verb is then accepted but unhonored. Set once by
    /// `Daemon::run` after it spawns the supervisor.
    brain_restart: std::sync::OnceLock<Arc<crate::brainproc::BrainRestart>>,
    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a
    /// `session_id → (subnet, real address, 5-min deadline)` between
    /// [`KIND_PAIR_MEET`] and [`KIND_PAIR_CODE_SUBMIT`]. The code phase dials the
    /// held address (no re-search); a wrong-code retry keeps the entry. Pairing
    /// is never journaled (the code must not persist) — in-memory only, and a
    /// daemon restart simply makes the operator re-run the meet.
    pair_holds: Mutex<HashMap<u64, PairHold>>,
}

/// One held meet result for the two-phase join (see [`Broker::pair_holds`]).
struct PairHold {
    subnet: String,
    addr: EndpointAddr,
    /// When the wait-for-code window expires (meet + 5 minutes).
    deadline: Instant,
}

/// The two-phase join's wait-for-code window: the daemon holds the met address
/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).
const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);

+    /// Held meet results for the two-phase join (REQ-JOIN-TWO-PHASE): a

+    /// `session_id → (subnet, real address, 5-min deadline)` between

+    /// [`KIND_PAIR_MEET`] and [`KIND_PAIR_CODE_SUBMIT`]. The code phase dials the

+    /// held address (no re-search); a wrong-code retry keeps the entry. Pairing

+    /// is never journaled (the code must not persist) — in-memory only, and a

+    /// daemon restart simply makes the operator re-run the meet.

+    pair_holds: Mutex<HashMap<u64, PairHold>>,

+/// One held meet result for the two-phase join (see [`Broker::pair_holds`]).

+    /// When the wait-for-code window expires (meet + 5 minutes).

+/// The two-phase join's wait-for-code window: the daemon holds the met address

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

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

/// The distinct `(endpoint, install_dir)` pairs whose session runs `adapter`,

/// from per-session `(endpoint, adapter, install_dir)` rows. Sessions with no

/// recorded install dir are dropped (a live update needs a target). Pure (no


            journal: Arc::new(EffectJournal::open(journal_path)?),
            net: net_cell,
            brain_restart: std::sync::OnceLock::new(),
        }))

            journal: Arc::new(EffectJournal::open(journal_path)?),
            net: net_cell,
            brain_restart: std::sync::OnceLock::new(),
            pair_holds: Mutex::new(HashMap::new()),
        }))


            journal: Arc::new(EffectJournal::open(journal_path)?),
            net: net_cell,
            brain_restart: std::sync::OnceLock::new(),
        }))

            journal: Arc::new(EffectJournal::open(journal_path)?),
            net: net_cell,
            brain_restart: std::sync::OnceLock::new(),
            pair_holds: Mutex::new(HashMap::new()),
        }))

+            pair_holds: Mutex::new(HashMap::new()),


Adding the dispatch handlers after `dispatch_pair_join`.


        let frame = Envelope::new(
            KIND_PAIR_JOINED,
            serde_json::to_value(reply).expect("PairJoinReply serializes"),
        );
        send_frame(send, &frame);
        Ok(())
    }

        let frame = Envelope::new(
            KIND_PAIR_JOINED,
            serde_json::to_value(reply).expect("PairJoinReply serializes"),
        );
        send_frame(send, &frame);
        Ok(())
    }

    /// Phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE): MEET a seed-holder for
    /// the named subnet — resolve its real, stable pairing address via the
    /// rendezvous (PUBLIC `(name, epoch)`, no code) — and HOLD it under a fresh
    /// session id, bounded by [`PAIR_CODE_WAIT`]. Replies [`KIND_MET_MEMBER`] so
    /// the CLI can collect the code, fresh, against a found member. Expired holds
    /// from abandoned earlier attempts are swept here (no separate reaper — pairing
    /// is rare and the map is tiny).
    // [impl->REQ-JOIN-TWO-PHASE]
    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: PairMeetReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad pair-meet payload: {e}"))?;
        let host = self
            .net
            .get()
            .ok_or("net disabled: this broker was bound without a network host")?;
        match host.pair_meet(&req.subnet) {
            Ok(addr) => {
                let member_hex = addr.id().to_string();
                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);
                {
                    let mut holds = self.pair_holds.lock().unwrap();
                    let now = Instant::now();
                    holds.retain(|_, h| h.deadline > now); // sweep abandoned holds
                    holds.insert(
                        session_id,
                        PairHold {
                            subnet: req.subnet.clone(),
                            addr,
                            deadline: now + PAIR_CODE_WAIT,
                        },
                    );
                }
                let frame = Envelope::new(
                    KIND_MET_MEMBER,
                    serde_json::to_value(MetMember {
                        session_id,
                        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}"))?;
        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 {
            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"),
                )
            }
            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(())
    }


        let frame = Envelope::new(
            KIND_PAIR_JOINED,
            serde_json::to_value(reply).expect("PairJoinReply serializes"),
        );
        send_frame(send, &frame);
        Ok(())
    }

    /// Phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE): MEET a seed-holder for
    /// the named subnet — resolve its real, stable pairing address via the
    /// rendezvous (PUBLIC `(name, epoch)`, no code) — and HOLD it under a fresh
    /// session id, bounded by [`PAIR_CODE_WAIT`]. Replies [`KIND_MET_MEMBER`] so
    /// the CLI can collect the code, fresh, against a found member. Expired holds
    /// from abandoned earlier attempts are swept here (no separate reaper — pairing
    /// is rare and the map is tiny).
    // [impl->REQ-JOIN-TWO-PHASE]
    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
        let req: PairMeetReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad pair-meet payload: {e}"))?;
        let host = self
            .net
            .get()
            .ok_or("net disabled: this broker was bound without a network host")?;
        match host.pair_meet(&req.subnet) {
            Ok(addr) => {
                let member_hex = addr.id().to_string();
                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);
                {
                    let mut holds = self.pair_holds.lock().unwrap();
                    let now = Instant::now();
                    holds.retain(|_, h| h.deadline > now); // sweep abandoned holds
                    holds.insert(
                        session_id,
                        PairHold {
                            subnet: req.subnet.clone(),
                            addr,
                            deadline: now + PAIR_CODE_WAIT,
                        },
                    );
                }
                let frame = Envelope::new(
                    KIND_MET_MEMBER,
                    serde_json::to_value(MetMember {
                        session_id,
                        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}"))?;
        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 {
            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"),
                )
            }
            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(())
    }

+    /// Phase 1 of the two-phase join (REQ-JOIN-TWO-PHASE): MEET a seed-holder for

+    /// the named subnet — resolve its real, stable pairing address via the

+    /// rendezvous (PUBLIC `(name, epoch)`, no code) — and HOLD it under a fresh

+    /// session id, bounded by [`PAIR_CODE_WAIT`]. Replies [`KIND_MET_MEMBER`] so

+    /// the CLI can collect the code, fresh, against a found member. Expired holds

+    /// from abandoned earlier attempts are swept here (no separate reaper — pairing

+    fn dispatch_pair_meet(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {

+        let req: PairMeetReq = serde_json::from_value(env.payload)

+            .map_err(|e| format!("bad pair-meet payload: {e}"))?;

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

+        match host.pair_meet(&req.subnet) {

+                let member_hex = addr.id().to_string();

+                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);

+                    let mut holds = self.pair_holds.lock().unwrap();

+                    let now = Instant::now();

+                    holds.retain(|_, h| h.deadline > now); // sweep abandoned holds

+                            subnet: req.subnet.clone(),

+                            deadline: now + PAIR_CODE_WAIT,

+                let frame = Envelope::new(

+                    serde_json::to_value(MetMember {

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

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

+    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, enforcing the wait-for-code timeout. A missing or

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

+            match holds.get(&req.session_id) {

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

+                    holds.remove(&req.session_id);

+        let Some((subnet, addr)) = held else {

+            let reply = PairJoinReply {

+                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`"

+                    serde_json::to_value(reply).expect("PairJoinReply serializes"),

+        let frame = match host.pair_join(&subnet, &req.code, Some(addr)) {

+                self.pair_holds.lock().unwrap().remove(&req.session_id);

+                    serde_json::to_value(PairJoinReply {

+                        retry_after_secs: None,

+                        detail: outcome,

+                    .expect("PairJoinReply serializes"),

+                // 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) {

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


Adding the `pair_fail_frame` helper + verifying how to read the hex id from an `EndpointAddr`.


31:pub use iroh::EndpointAddr;
432:        PublicKey::from_bytes(self.inner.id().as_bytes())
438:        self.node_id().to_hex()
443:    pub fn addr(&self) -> EndpointAddr {
472:    pub async fn connect(&self, addr: EndpointAddr) -> Result<Connection, NetError> {
480:        addr: EndpointAddr,
497:        self.connect(EndpointAddr::new(id)).await
507:/// An id-only dialable [`EndpointAddr`] for a node named by its lowercase-hex
514:pub fn addr_for_node_hex(hex: &str) -> Option<EndpointAddr> {
517:    Some(EndpointAddr::new(id))
529:pub fn conn_remote_addr(conn: &Connection) -> EndpointAddr {
536:    EndpointAddr::from_parts(conn.remote_id(), addrs)
678:        assert_eq!(ep.node_id_hex(), id.public_key().to_hex());


27://! answers with its real [`EndpointAddr`] (one tiny length-prefixed frame on a
46:use iroh::{EndpointAddr, EndpointId};
67:/// Hard cap on the meet reply frame — a serialized [`EndpointAddr`] is a few
156:    pub async fn serve_once(&self, real_addr: &EndpointAddr) -> Result<(), MeetError> {
198:    meet_addr: EndpointAddr,
199:) -> Result<EndpointAddr, MeetError> {
226:    let addr: EndpointAddr = serde_json::from_slice(&json)
266:/// [`EndpointAddr`] by racing the ±1-window meet ids, WITHOUT the final ceremony
278:) -> Result<EndpointAddr, MeetError> {
286:            tokio::time::timeout(MEET_PROBE_TIMEOUT, meet_at(&dialer, EndpointAddr::new(id)))
349:    fn derived_addr(listener: &RendezvousListener, subnet: &str, step: u64) -> EndpointAddr {
353:        let mut addr = EndpointAddr::new(id);
408:            fetched.id.as_bytes(),


`EndpointAddr` has a public `.id` field (EndpointId with `.as_bytes()`). Fixing member_hex to the canonical hex, and adding the `pair_fail_frame` helper.


            Ok(addr) => {
                let member_hex = addr.id().to_string();
                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);

            Ok(addr) => {
                // The canonical lowercase-hex name (== node_id_hex everywhere),
                // derived from the met address's proven id.
                let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())
                    .map(|p| p.to_hex())
                    .unwrap_or_default();
                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);


            Ok(addr) => {
                let member_hex = addr.id().to_string();
                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);

            Ok(addr) => {
                // The canonical lowercase-hex name (== node_id_hex everywhere),
                // derived from the met address's proven id.
                let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())
                    .map(|p| p.to_hex())
                    .unwrap_or_default();
                let session_id = self.next_id.fetch_add(1, Ordering::Relaxed);

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

-                let member_hex = addr.id().to_string();

+                // The canonical lowercase-hex name (== node_id_hex everywhere),

+                // derived from the met address's proven id.

+                let member_hex = spt_proto::identity::PublicKey::from_bytes(addr.id.as_bytes())

+                    .map(|p| p.to_hex())

+                    .unwrap_or_default();

let mut holds = self.pair_holds.lock().unwrap();


Now refactor `dispatch_pair_join`'s failure branch into a shared `pair_fail_frame` helper.


        let reply = match host.pair_join(&req.subnet, &req.code, responder_addr) {
            Ok(outcome) => PairJoinReply {
                ok: true,
                class: None,
                retry_after_secs: None,
                detail: outcome,
            },
            Err(fail) => {
                use crate::pairhost::JoinFail;
                let (class, retry) = match &fail {
                    JoinFail::NoSeedHolder(_) => ("no-seed-holder", None),
                    JoinFail::WrongCode => ("wrong-code", None),
                    JoinFail::RateLimited(retry) => ("rate-limited", *retry),
                    JoinFail::UnknownSubnet(_) => ("unknown-subnet", None),
                    JoinFail::ClockSkew => ("clock-skew", None),
                    JoinFail::Other(_) => ("other", None),
                };
                PairJoinReply {
                    ok: false,
                    class: Some(class.to_string()),
                    retry_after_secs: retry,
                    detail: fail.to_string(),
                }
            }
        };
        let frame = Envelope::new(
            KIND_PAIR_JOINED,
            serde_json::to_value(reply).expect("PairJoinReply serializes"),
        );
        send_frame(send, &frame);
        Ok(())
    }

        let frame = match host.pair_join(&req.subnet, &req.code, responder_addr) {
            Ok(outcome) => Envelope::new(
                KIND_PAIR_JOINED,
                serde_json::to_value(PairJoinReply {
                    ok: true,
                    class: None,
                    retry_after_secs: None,
                    detail: outcome,
                })
                .expect("PairJoinReply serializes"),
            ),
            Err(fail) => pair_fail_frame(&fail),
        };
        send_frame(send, &frame);
        Ok(())
    }


        let reply = match host.pair_join(&req.subnet, &req.code, responder_addr) {
            Ok(outcome) => PairJoinReply {
                ok: true,
                class: None,
                retry_after_secs: None,
                detail: outcome,
            },
            Err(fail) => {
                use crate::pairhost::JoinFail;
                let (class, retry) = match &fail {
                    JoinFail::NoSeedHolder(_) => ("no-seed-holder", None),
                    JoinFail::WrongCode => ("wrong-code", None),
                    JoinFail::RateLimited(retry) => ("rate-limited", *retry),
                    JoinFail::UnknownSubnet(_) => ("unknown-subnet", None),
                    JoinFail::ClockSkew => ("clock-skew", None),
                    JoinFail::Other(_) => ("other", None),
                };
                PairJoinReply {
                    ok: false,
                    class: Some(class.to_string()),
                    retry_after_secs: retry,
                    detail: fail.to_string(),
                }
            }
        };
        let frame = Envelope::new(
            KIND_PAIR_JOINED,
            serde_json::to_value(reply).expect("PairJoinReply serializes"),
        );
        send_frame(send, &frame);
        Ok(())
    }

        let frame = match host.pair_join(&req.subnet, &req.code, responder_addr) {
            Ok(outcome) => Envelope::new(
                KIND_PAIR_JOINED,
                serde_json::to_value(PairJoinReply {
                    ok: true,
                    class: None,
                    retry_after_secs: None,
                    detail: outcome,
                })
                .expect("PairJoinReply serializes"),
            ),
            Err(fail) => pair_fail_frame(&fail),
        };
        send_frame(send, &frame);
        Ok(())
    }

-        let reply = match host.pair_join(&req.subnet, &req.code, responder_addr) {

-            Ok(outcome) => PairJoinReply {

-                retry_after_secs: None,

-                use crate::pairhost::JoinFail;

-                let (class, retry) = match &fail {

-                    JoinFail::NoSeedHolder(_) => ("no-seed-holder", None),

-                    JoinFail::WrongCode => ("wrong-code", None),

-                    JoinFail::RateLimited(retry) => ("rate-limited", *retry),

-                    JoinFail::UnknownSubnet(_) => ("unknown-subnet", None),

-                    JoinFail::ClockSkew => ("clock-skew", None),

-                    JoinFail::Other(_) => ("other", None),

-                    class: Some(class.to_string()),

-                    retry_after_secs: retry,

-                    detail: fail.to_string(),

+        let frame = match host.pair_join(&req.subnet, &req.code, responder_addr) {

+            Ok(outcome) => Envelope::new(

+                serde_json::to_value(PairJoinReply {

+                    retry_after_secs: None,

+                .expect("PairJoinReply serializes"),

+            Err(fail) => pair_fail_frame(&fail),

-            serde_json::to_value(reply).expect("PairJoinReply serializes"),


/// The two-phase join's wait-for-code window: the daemon holds the met address
/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).
const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);

/// The two-phase join's wait-for-code window: the daemon holds the met address
/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).
const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);

/// 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 {
    use crate::pairhost::JoinFail;
    let (class, retry) = match fail {
        JoinFail::NoSeedHolder(_) => ("no-seed-holder", None),
        JoinFail::WrongCode => ("wrong-code", None),
        JoinFail::RateLimited(retry) => ("rate-limited", *retry),
        JoinFail::UnknownSubnet(_) => ("unknown-subnet", None),
        JoinFail::ClockSkew => ("clock-skew", None),
        JoinFail::Other(_) => ("other", None),
    };
    Envelope::new(
        KIND_PAIR_JOINED,
        serde_json::to_value(PairJoinReply {
            ok: false,
            class: Some(class.to_string()),
            retry_after_secs: retry,
            detail: fail.to_string(),
        })
        .expect("PairJoinReply serializes"),
    )
}


/// The two-phase join's wait-for-code window: the daemon holds the met address
/// this long before a code must arrive (ADR-0030 / REQ-JOIN-TWO-PHASE).
const PAIR_CODE_WAIT: Duration = Duration::from_secs(5 * 60);

/// 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 {
    use crate::pairhost::JoinFail;
    let (class, retry) = match fail {
        JoinFail::NoSeedHolder(_) => ("no-seed-holder", None),
        JoinFail::WrongCode => ("wrong-code", None),
        JoinFail::RateLimited(retry) => ("rate-limited", *retry),
        JoinFail::UnknownSubnet(_) => ("unknown-subnet", None),
        JoinFail::ClockSkew => ("clock-skew", None),
        JoinFail::Other(_) => ("other", None),
    };
    Envelope::new(
        KIND_PAIR_JOINED,
        serde_json::to_value(PairJoinReply {
            ok: false,
            class: Some(class.to_string()),
            retry_after_secs: retry,
            detail: fail.to_string(),
        })
        .expect("PairJoinReply serializes"),
    )
}

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

+        JoinFail::NoSeedHolder(_) => ("no-seed-holder", None),

+        JoinFail::WrongCode => ("wrong-code", None),

+        JoinFail::RateLimited(retry) => ("rate-limited", *retry),

+        JoinFail::UnknownSubnet(_) => ("unknown-subnet", None),

+        JoinFail::ClockSkew => ("clock-skew", None),

+        JoinFail::Other(_) => ("other", None),

+        serde_json::to_value(PairJoinReply {

+            class: Some(class.to_string()),

+        .expect("PairJoinReply serializes"),

/// The distinct `(endpoint, install_dir)` pairs whose session runs `adapter`,

/// from per-session `(endpoint, adapter, install_dir)` rows. Sessions with no

/// recorded install dir are dropped (a live update needs a target). Pure (no


Now routing in `handle_conn` + imports. Reading the import block.