370	            send: sub,
371	            by,
372	            _writer: writer,
373	        });
374	        self.stamp_driven_by();
375	    }
376
377	    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared
378	    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per
379	    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its
380	    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an
381	    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).
382	    fn clear_controller(&mut self) {
383	        if self.controller.take().is_some() {
384	            self.stamp_driven_by();
385	        }
386	    }
387
388	    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and
389	    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same
390	    /// controller generation the drain saw (`epoch`), so a NEWER controller that
391	    /// attached during the stall is never unseated.
392	    fn mark_controller_gone(&mut self, epoch: u64) {
393	        if self.controller_epoch == epoch {
394	            self.clear_controller();
395	        }
396	    }
397
398	    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer
399	    /// thread (NOT through the bounded live queue — replay must not trip the
400	    /// overflow eviction), seed it the current size frame first, then run the
401	    /// live queue. The drain thread fans live frames in via `try_send`.
402	    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
403	        let vid = self.next_viewer_id;
404	        self.next_viewer_id += 1;
405	        // The size frame + the ring replay are the writer's initial batch (owned
406	        // by the thread, not the bounded queue).
407	        let mut initial: Vec<Envelope> =
408	            vec![size_envelope(self.session_id, self.size.0, self.size.1)];
409	        initial.extend(
410	            self.ring
411	                .iter()
412	                .filter(|(s, _)| *s >= from_seq)
413	                .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),
414	        );
415	        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
416	        let writer_send = Arc::clone(&sub);
417	        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
418	        self.viewers.insert(
419	            vid,
420	            ViewerSink {
421	                tx,
422	                send: sub,
423	                _writer: writer,
424	            },
425	        );
426	    }
427
428	    /// Resolve a controller/viewer subscribe under the W2.5 model
429	    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs
430	    /// the controller take / viewer add / loud displace as a side effect. See the
431	    /// identity matrix in the body — the `by`-keyed logic is what makes a
432	    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.
433	    fn resolve_subscribe(
434	        &mut self,
435	        sub: SharedSend,
436	        from_seq: u64,
437	        intent: AttachIntent,
438	        by: Option<String>,
439	    ) -> SubscribeOutcome {
440	        if intent == AttachIntent::Viewer {
441	            if self.viewers.len() >= MAX_VIEWERS {
442	                // Soft cap reached — refuse the viewer (bounds thread count).
443	                return SubscribeOutcome::BusyControlled {
444	                    by: "viewer cap reached".to_string(),
445	                };
446	            }
447	            self.add_viewer(sub, from_seq);
448	            self.stamp_viewer_count();
449	            return SubscribeOutcome::Viewer;
450	        }
451	        // Control or Take.
452	        if self.controller.is_none() {
453	            // Free endpoint → become controller (nothing to displace).
454	            self.become_controller(sub, by, from_seq);
455	            return SubscribeOutcome::Controller;
456	        }
457	        let same_identity = self.controller_by() == by.as_deref();
458	        if same_identity {
459	            // Same operator (or both local) re-serving — silent re-take. This is
460	            // the gate-#7 path: a successor re-taking the slot after a brain
461	            // restart NEVER fires Displaced.
462	            self.become_controller(sub, by, from_seq);
463	            return SubscribeOutcome::Controller;
464	        }
465	        if by.is_none() {
466	            // The LOCAL brain re-attaching while a REMOTE controls — never steal;
467	            // fall back to a silent viewer (gate #7 for the local-resume path).
468	            self.add_viewer(sub, from_seq);
469	            self.stamp_viewer_count();
470	            return SubscribeOutcome::Viewer;
471	        }
472	        if self.controller_by().is_none() {
473	            // A remote takes an UNDRIVEN session (only the local spawner held the
474	            // slot) — silent control, no loud displace (there is no remote driver
475	            // to notify).
476	            self.become_controller(sub, by, from_seq);
477	            return SubscribeOutcome::Controller;
478	        }
479	        // A DIFFERENT remote controls it.
480	        match intent {
481	            AttachIntent::Take => {
482	                // The ONLY loud-displace path: notify the incumbent, then take.
483	                // W1: route the Displaced notice through the OLD controller's
484	                // writer thread (best-effort, off the dispatch thread) — the
485	                // incumbent we're kicking may be exactly the wedged one, so an
486	                // inline write here would block the new attacher's dispatch.
487	                if let Some(old) = &self.controller {
488	                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(""));
489	                    let _ = old.tx.try_send(CtrlMsg::Control(frame));
490	                }
491	                self.become_controller(sub, by, from_seq);
492	                SubscribeOutcome::TookControl
493	            }
494	            _ => SubscribeOutcome::BusyControlled {
495	                by: self.controller_by().unwrap_or_default().to_string(),
496	            },
497	        }
498	    }
499
500	    /// Update the stored PTY size and push a `size` frame to every viewer (the
501	    /// letterbox feed). Called on a controller resize. Viewer pushes are
502	    /// non-blocking with eviction, like [`OutputLog::append`].
503	    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {
504	        self.size = (rows, cols);
505	        if self.viewers.is_empty() {
506	            return;
507	        }
508	        let frame = size_envelope(self.session_id, rows, cols);
509	        let mut evicted: Vec<u64> = Vec::new();
510	        for (vid, sink) in self.viewers.iter() {
511	            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
512	                evicted.push(*vid);
513	            }
514	        }
515	        if !evicted.is_empty() {
516	            for vid in evicted {
517	                self.viewers.remove(&vid);
518	            }
519	            self.stamp_viewer_count();
520	        }
521	    }
522
523	    /// Detach `sub` from whatever role it held: clear the controller slot iff it
524	    /// is the current controller (a no-op if a newer brain took over — a late
525	    /// disconnect can't unseat the live controller), and remove any viewer rows
526	    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps
527	    /// `driven_by` / `viewer_count` when a role actually changed.
528	    fn detach_if(&mut self, sub: &SharedSend) {
529	        let is_ctrl = self
530	            .controller
531	            .as_ref()
532	            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));
533	        if is_ctrl {
534	            self.clear_controller(); // drops the sink (ends its writer) + unlatches
535	        }
536	        let before = self.viewers.len();
537	        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
538	        if self.viewers.len() != before {
539	            self.stamp_viewer_count();
540	        }
541	    }
542
543	    /// Every sink currently attached (controller + viewers) — the recipients of
544	    /// an `exit` frame.
545	    fn all_sinks(&self) -> Vec<SharedSend> {
546	        let mut v: Vec<SharedSend> = Vec::new();
547	        if let Some(c) = &self.controller {
548	            v.push(Arc::clone(&c.send));
549	        }
550	        for sink in self.viewers.values() {
551	            v.push(Arc::clone(&sink.send));
552	        }
553	        v
554	    }
555
556	    /// Stamp the perch's `driven_by` to the current controller's identity (the
557	    /// broker is the single writer — resolves the clear-race). The remote-drive
558	    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced
559	    /// operator's teardown can never wipe the new controller's marker.
560	    /// Best-effort: a session served without a perch (raw broker tests) has
561	    /// nowhere to record.
562	    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]
563	    fn stamp_driven_by(&self) {
564	        if self.endpoint.is_empty() {
565	            return;
566	        }
567	        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
568	        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());
569	    }
570
571	    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the
572	    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].
573	    // [impl->REQ-RCVIEW-1]
574	    fn stamp_viewer_count(&self) {
575	        if self.endpoint.is_empty() {
576	            return;
577	        }
578	        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
579	        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);
580	    }
581
582	    /// Whether `sub` is the current controller (the resize-exclusivity check).
583	    fn is_controller(&self, sub: &SharedSend) -> bool {
584	        self.controller
585	            .as_ref()
586	            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))
587	    }
588
589	    /// The next seq a re-attaching controller still needs (the persisted resume

            send: sub,
            by,
            _writer: writer,
        });
        self.stamp_driven_by();
    }

    /// Detach the controller and UNLATCH the perch — the ONE unlatch path shared
    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per
    /// the W1↔W5 synergy) W5's reconcile self-heal. Dropping the sink ends its
    /// writer thread; clearing the slot re-stamps `driven_by` to `None` (so an
    /// `ONLINE+CONTROLLED` latch cannot outlive a gone controller).
    fn clear_controller(&mut self) {
        if self.controller.take().is_some() {
            self.stamp_driven_by();
        }
    }

    /// Deadline-evict path (W1): the drain observed a fully-stalled controller and
    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same
    /// controller generation the drain saw (`epoch`), so a NEWER controller that
    /// attached during the stall is never unseated.
    fn mark_controller_gone(&mut self, epoch: u64) {
        if self.controller_epoch == epoch {
            self.clear_controller();
        }
    }

    /// Add a read-only viewer: snapshot the ring from `from_seq` into the writer
    /// thread (NOT through the bounded live queue — replay must not trip the
    /// overflow eviction), seed it the current size frame first, then run the
    /// live queue. The drain thread fans live frames in via `try_send`.
    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
        let vid = self.next_viewer_id;
        self.next_viewer_id += 1;
        // The size frame + the ring replay are the writer's initial batch (owned
        // by the thread, not the bounded queue).
        let mut initial: Vec<Envelope> =
            vec![size_envelope(self.session_id, self.size.0, self.size.1)];
        initial.extend(
            self.ring
                .iter()
                .filter(|(s, _)| *s >= from_seq)
                .map(|(seq, bytes)| output_envelope(self.session_id, *seq, bytes)),
        );
        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
        self.viewers.insert(
            vid,
            ViewerSink {
                tx,
                send: sub,
                _writer: writer,
            },
        );
    }

    /// Resolve a controller/viewer subscribe under the W2.5 model
    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs
    /// the controller take / viewer add / loud displace as a side effect. See the
    /// identity matrix in the body — the `by`-keyed logic is what makes a
    /// brain-restart re-serve silent (gate #7) and a cross-operator `Take` loud.
    fn resolve_subscribe(
        &mut self,
        sub: SharedSend,
        from_seq: u64,
        intent: AttachIntent,
        by: Option<String>,
    ) -> SubscribeOutcome {
        if intent == AttachIntent::Viewer {
            if self.viewers.len() >= MAX_VIEWERS {
                // Soft cap reached — refuse the viewer (bounds thread count).
                return SubscribeOutcome::BusyControlled {
                    by: "viewer cap reached".to_string(),
                };
            }
            self.add_viewer(sub, from_seq);
            self.stamp_viewer_count();
            return SubscribeOutcome::Viewer;
        }
        // Control or Take.
        if self.controller.is_none() {
            // Free endpoint → become controller (nothing to displace).
            self.become_controller(sub, by, from_seq);
            return SubscribeOutcome::Controller;
        }
        let same_identity = self.controller_by() == by.as_deref();
        if same_identity {
            // Same operator (or both local) re-serving — silent re-take. This is
            // the gate-#7 path: a successor re-taking the slot after a brain
            // restart NEVER fires Displaced.
            self.become_controller(sub, by, from_seq);
            return SubscribeOutcome::Controller;
        }
        if by.is_none() {
            // The LOCAL brain re-attaching while a REMOTE controls — never steal;
            // fall back to a silent viewer (gate #7 for the local-resume path).
            self.add_viewer(sub, from_seq);
            self.stamp_viewer_count();
            return SubscribeOutcome::Viewer;
        }
        if self.controller_by().is_none() {
            // A remote takes an UNDRIVEN session (only the local spawner held the
            // slot) — silent control, no loud displace (there is no remote driver
            // to notify).
            self.become_controller(sub, by, from_seq);
            return SubscribeOutcome::Controller;
        }
        // A DIFFERENT remote controls it.
        match intent {
            AttachIntent::Take => {
                // The ONLY loud-displace path: notify the incumbent, then take.
                // W1: route the Displaced notice through the OLD controller's
                // writer thread (best-effort, off the dispatch thread) — the
                // incumbent we're kicking may be exactly the wedged one, so an
                // inline write here would block the new attacher's dispatch.
                if let Some(old) = &self.controller {
                    let frame = displaced_envelope(self.session_id, by.as_deref().unwrap_or(""));
                    let _ = old.tx.try_send(CtrlMsg::Control(frame));
                }
                self.become_controller(sub, by, from_seq);
                SubscribeOutcome::TookControl
            }
            _ => SubscribeOutcome::BusyControlled {
                by: self.controller_by().unwrap_or_default().to_string(),
            },
        }
    }

    /// Update the stored PTY size and push a `size` frame to every viewer (the
    /// letterbox feed). Called on a controller resize. Viewer pushes are
    /// non-blocking with eviction, like [`OutputLog::append`].
    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {
        self.size = (rows, cols);
        if self.viewers.is_empty() {
            return;
        }
        let frame = size_envelope(self.session_id, rows, cols);
        let mut evicted: Vec<u64> = Vec::new();
        for (vid, sink) in self.viewers.iter() {
            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
                evicted.push(*vid);
            }
        }
        if !evicted.is_empty() {
            for vid in evicted {
                self.viewers.remove(&vid);
            }
            self.stamp_viewer_count();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it
    /// is the current controller (a no-op if a newer brain took over — a late
    /// disconnect can't unseat the live controller), and remove any viewer rows
    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps
    /// `driven_by` / `viewer_count` when a role actually changed.
    fn detach_if(&mut self, sub: &SharedSend) {
        let is_ctrl = self
            .controller
            .as_ref()
            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));
        if is_ctrl {
            self.clear_controller(); // drops the sink (ends its writer) + unlatches
        }
        let before = self.viewers.len();
        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
        if self.viewers.len() != before {
            self.stamp_viewer_count();
        }
    }

    /// Every sink currently attached (controller + viewers) — the recipients of
    /// an `exit` frame.
    fn all_sinks(&self) -> Vec<SharedSend> {
        let mut v: Vec<SharedSend> = Vec::new();
        if let Some(c) = &self.controller {
            v.push(Arc::clone(&c.send));
        }
        for sink in self.viewers.values() {
            v.push(Arc::clone(&sink.send));
        }
        v
    }

    /// Stamp the perch's `driven_by` to the current controller's identity (the
    /// broker is the single writer — resolves the clear-race). The remote-drive
    /// detection fact (REQ-REACH-1) moved here from `serve_attach` so a displaced
    /// operator's teardown can never wipe the new controller's marker.
    /// Best-effort: a session served without a perch (raw broker tests) has
    /// nowhere to record.
    // [impl->REQ-RCVIEW-1] [impl->REQ-REACH-1]
    fn stamp_driven_by(&self) {
        if self.endpoint.is_empty() {
            return;
        }
        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());
    }

    /// Stamp the perch's `viewer_count` to the live viewer count (broker is the
    /// single writer). Best-effort, like [`OutputLog::stamp_driven_by`].
    // [impl->REQ-RCVIEW-1]
    fn stamp_viewer_count(&self) {
        if self.endpoint.is_empty() {
            return;
        }
        let perch = resolve_perch_path(&self.endpoint, ParentHint::Infer);
        let _ = spt_store::info::set_viewer_count(&perch, self.viewers.len() as u32);
    }

    /// Whether `sub` is the current controller (the resize-exclusivity check).
    fn is_controller(&self, sub: &SharedSend) -> bool {
        self.controller
            .as_ref()
            .is_some_and(|c| Arc::ptr_eq(&c.send, sub))
    }

    /// The next seq a re-attaching controller still needs (the persisted resume


Bash

grep -n "controller_by\|fn controller_epoch\|KIND_SESSIONS\|SessionInfo\|resume_seq" crates/spt-daemon/src/broker.rs | head -40

Find controller_by + sessions reply builder


58:    NetStreamsReply, PairJoinReply, PairJoinReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,
64:    KIND_PAIR_JOIN, KIND_PAIR_JOINED, KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
231:    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
291:    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
336:    fn controller_by(&self) -> Option<&str> {
457:        let same_identity = self.controller_by() == by.as_deref();
472:        if self.controller_by().is_none() {
495:                by: self.controller_by().unwrap_or_default().to_string(),
568:        let _ = spt_store::info::set_driven_by(&perch, self.controller_by());
590:    /// cursor surfaced in `KIND_SESSIONS` → `resume_seq`). W1: read from the
955:    /// `resume_seq` below the surviving ring floor must resume forward, losing only
1157:                KIND_SESSIONS => {
1171:                            .map(|(id, endpoint, log)| SessionInfo {
1174:                                resume_seq: log.lock().unwrap().delivered_through(),
1179:                        KIND_SESSIONS_REPLY,
2054:    /// The additive `resume_seq` field (D4-1, KH-2.3 N-1 window): an older
2060:    fn sessions_reply_resume_seq_defaults_when_absent() {
2061:        // Old-broker shape: no `resume_seq` key on the session object.
2066:            old.sessions[0].resume_seq, 0,
2067:            "absent resume_seq -> default 0"
2072:            r#"{"sessions":[{"session_id":7,"endpoint":"e","resume_seq":42}]}"#,
2076:            new.sessions[0].resume_seq, 42,
2077:            "present resume_seq round-trips"


<owl_messages>
<owl_message from="todlando-psyche" priority="normal"><EVENT type="echo_commune" from="todlando-psyche" timestamp="2026-06-19T08:00:49-07:00" machine="HFENDULEAM" project="spt-core" branch="v0.13.0-delivery-control" head_sha="5beb7683448fa464ad743074e9c8df842c684945" head_subject="docs(plan): W5 driven_by self-heal — grounded repro-first JIT plan" note="Echo commune brief — auto-fired">&lt;live-context&gt;
# Commune — v0.13.0 session delta (todlando), 2026-06-19

**Checkpointing at a clean boundary: W2 keystone DONE + doyle-gated. Self-clearing after this.**

## Landed this session (all doyle GATE PASS)
- **W7-swap @95f742f** — `rc.rs normalize_key_byte` one-way→SWAP (0x08↔0x7f). CC reads ^H=kill-word,
  DEL=char-delete → swapping input bytes gives Backspace=char-del + Ctrl+Backspace=word-del (native
  Win11). cfg(windows); 0x02 detach upstream; else passthrough. KH 7.13 + REQ note extended [impl,unit].
  doyle re-deployed the operator binary to PATH for the final HITL capture.
- **F1 @6034479** — runner-contract doc note on `inject_control_wedge.rs` (shared process-global
  SPT_HOME set_var race + contending brokers HANGS under bare `cargo test`; nextest/`--test-threads=1`
  only). Doc-note over serial_test (CI is nextest-safe). doyle PASS.
- **W2 Layer C+E @f9fe5bb** — the ADR-0022 keystone. Per translation-bearing session: `Translation`
  handle on HostedSession (floor + event_tx queue + shared Arc&lt;TranslationChild&gt; + faulted flag);
  `run_inject_worker` thread drains queued &lt;EVENT&gt;s one-at-a-time: open floor → feed binary
  {type:event} → drain key/text(write_input)/delay(clamped)/commit → flush buffered controller bytes
  OFF-lock AFTER the sequence → release. `inject_commit_deadline()` = 5s, env-override
  SPT_INJECT_COMMIT_DEADLINE_MS; FAULT on miss/death (`fault_translation` terminate, no respawn →
  raw-inject degrade). dispatch_input: content-free {type:input} ping + buffer-if-held (op_id buffers
  INSIDE the journal effect to keep ack+dedup). dispatch_endpoint_input routes whole &lt;EVENT&gt; to live
  worker; raw inject = degenerate fallback. TranslationChild.terminate() made &amp;self/Arc-callable.
- **W2 Layer G @5b51cb5** — real-fixture int gate. `src/bin/xlate_choreo_fixture.rs` (pure stdin→stdout
  filter, CC choreography ctrl+s→50ms→text→enter→50ms→ctrl+s→ctrl+q→commit; nocommit mode; stdin-log
  for privacy). g1-g4: happy-path (delivered+serviceable+fresh-attach hard both, byte-receipt hard
  cfg(unix)/captured cfg(windows) — ConPTY doesn't surface injected cooked echo) / no-commit FAULT
  (deadline=400ms → flush+raw-inject fallback, hard both) / content-free ping privacy (hard both) /
  no-zombie reap (hard both). 7/7 nextest, clippy clean, traceable EXIT=0.
  **REQ-MSG-IDLE-TRANSLATION-BINARY → [doc,impl,unit,int]. W2 CLOSED.**

## doyle gate verdicts (independently re-verified, not trust-the-green)
W2 GATE PASS @5b51cb5 — all 5 criteria met. ONE pending hard-proof (NOT a block): g1's byte-receipt is
cfg(unix)/gravity-linux only (local Windows can't fire it). Rides the gravity-linux CI leg at the
v0.13.0 PR — same discipline as M11/M12. SAME applies to Layer F (gravity follow-up) + W1b F3. **doyle
tracks confirming gravity-linux green at PR.**

## Process win
ONE spt-test-engineer for Layer G (perch todlando-w172), background, no Monitor, ≤4-iter constrained —
came to rest clean. NO two-subagent collision this time (the banked lesson held). Independently re-ran
all gates from a quiesced tree after it rested.

## RESUME POINT (next session): W5 repro-first
- doyle assigned **W5 (driven_by self-heal)**; doyle drives **W6/perri** (translation binary, unblocked)
  + tracks **Layer F gravity**. **W3 done. W4 (picker UX) still open** (alt to W5).
- W5 fully grounded → **`V0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md`** (committed @5beb768). Headline:
  `clear_controller` already self-heals a STALLED controller (re-stamps driven_by→None via the W1
  deadline-evict). W5 = the two NO-DRAIN gaps: (A) dead controller on an idle session, (B) offlined
  session keeps driven_by. Needs additive `SessionInfo.controller_by: Option&lt;String&gt;` (N-1 serde
  default, mirror resume_seq) so the reconcile tells &quot;live session, no controller&quot; apart. Extend
  `reconcile_hosted_liveness` (livehost.rs:511) to clear driven_by for both gaps. Watch: false-heal of a
  genuinely-controlled session + N-1 false-clear (plan flags both).
- **Discipline (unchanged):** repro-first RED on a REAL broker, never theory/mocks; tests via ONE
  spt-test-engineer (background, no Monitor, ≤4 iter); tag REQ evidence same commit; gates = clippy
  `--workspace --all-targets -D warnings` + `traceable-reqs check` EXIT=0 + real `cargo build -p spt<br>  --bin spt`; never clobber a running daemon's exe.

## Tree
CLEAN @5beb768 on v0.13.0-delivery-control. Worktrees: spt-core-opbuild @6034479 = doyle's operator
PATH-deploy build; spt-core-doyle @v0.12.1-help-md. No W5 work started elsewhere.
&lt;/live-context&gt;
&lt;project-context&gt;
W2 layer choreography landed: Layers A–B–D-plumbing–C–E on-disk, unified broker.rs + translation.rs + dispatch flow. Layer F shared (W1b F3, deferred). Layer G int staged/in-flight. W7-swap companion merged (floor key normalization). F1 hang-guard doc merged. doyle mid W7-swap PATH deploy; awaiting Layer G → W2 gate surface.
&lt;/project-context&gt;</EVENT></owl_message>
</owl_messages>


1155	                    }
1156	                }
1157	                KIND_SESSIONS => {
1158	                    // Clone the (id, endpoint, log) out from under the sessions
1159	                    // lock, then read each log's cursor under the log lock alone —
1160	                    // never both locks at once (the dispatch_subscribe discipline).
1161	                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {
1162	                        let sessions = self.sessions.lock().unwrap();
1163	                        sessions
1164	                            .iter()
1165	                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))
1166	                            .collect()
1167	                    };
1168	                    let reply = SessionsReply {
1169	                        sessions: rows
1170	                            .into_iter()
1171	                            .map(|(id, endpoint, log)| SessionInfo {
1172	                                session_id: id,
1173	                                endpoint,
1174	                                resume_seq: log.lock().unwrap().delivered_through(),
1175	                            })
1176	                            .collect(),
1177	                    };
1178	                    let frame = Envelope::new(
1179	                        KIND_SESSIONS_REPLY,
1180	                        serde_json::to_value(reply).expect("SessionsReply serializes"),
1181	                    );
1182	                    send_frame(&send, &frame);
1183	                }
1184	                KIND_ENDPOINT_INPUT => {
1185	                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {
1186	                        send_error(&send, &msg);
1187	                    }
1188	                }
1189	                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {

                    }
                }
                KIND_SESSIONS => {
                    // Clone the (id, endpoint, log) out from under the sessions
                    // lock, then read each log's cursor under the log lock alone —
                    // never both locks at once (the dispatch_subscribe discipline).
                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {
                        let sessions = self.sessions.lock().unwrap();
                        sessions
                            .iter()
                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))
                            .collect()
                    };
                    let reply = SessionsReply {
                        sessions: rows
                            .into_iter()
                            .map(|(id, endpoint, log)| SessionInfo {
                                session_id: id,
                                endpoint,
                                resume_seq: log.lock().unwrap().delivered_through(),
                            })
                            .collect(),
                    };
                    let frame = Envelope::new(
                        KIND_SESSIONS_REPLY,
                        serde_json::to_value(reply).expect("SessionsReply serializes"),
                    );
                    send_frame(&send, &frame);
                }
                KIND_ENDPOINT_INPUT => {
                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {


Echo commune = my own checkpoint, confirms W5 resume. Continue grounding.


330	        })
331	    }
332
333	    /// The current controller's identity (`Some(node)` remote / `None` local),
334	    /// or `None` when undriven. The `by`-keyed ownership logic in
335	    /// [`resolve_subscribe`] reads this.
336	    fn controller_by(&self) -> Option<&str> {
337	        self.controller.as_ref().and_then(|c| c.by.as_deref())
338	    }
339
340	    /// Become (or silently re-take) the interactive controller from `from_seq`.
341	    ///

        })
    }

    /// The current controller's identity (`Some(node)` remote / `None` local),
    /// or `None` when undriven. The `by`-keyed ownership logic in
    /// [`resolve_subscribe`] reads this.
    fn controller_by(&self) -> Option<&str> {
        self.controller.as_ref().and_then(|c| c.by.as_deref())
    }

    /// Become (or silently re-take) the interactive controller from `from_seq`.
    ///


555	    /// locally-opened outbound streams too.
556	    #[serde(default)]
557	    pub initiated_locally: bool,
558	}
559
560	/// `net-streams-reply` payload — every stream the broker currently holds.
561	#[derive(Debug, Clone, Serialize, Deserialize)]
562	pub struct NetStreamsReply {
563	    /// Held streams (opened + peer-initiated), unordered.
564	    #[serde(default)]
565	    pub streams: Vec<NetStreamInfo>,
566	}
567
568	/// One hosted session in a `sessions-reply`.
569	#[derive(Debug, Clone, Serialize, Deserialize)]
570	pub struct SessionInfo {
571	    /// The broker-minted session id.
572	    pub session_id: u64,
573	    /// The owning endpoint (the spawn's label; empty = unlabeled).
574	    #[serde(default)]
575	    pub endpoint: String,
576	    /// The broker's last-delivered output cursor for this session — the next seq
577	    /// a re-attaching brain still needs (restoration D4-1, cursor-of-record). A
578	    /// cold-starting brain re-`subscribe`s each session from here for a gapless,
579	    /// at-least-once resume instead of replaying the whole ring from 0.
580	    ///
581	    /// Additive + `#[serde(default)]` (KH-2.3 N-1 window): an **older broker**
582	    /// omits the field → a **newer brain** reads `0` and resumes with a full-ring
583	    /// replay (safe — at-least-once degrades only to dup-only), never a parse
584	    /// reject. An older brain ignores it.
585	    // [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
586	    #[serde(default)]
587	    pub resume_seq: u64,
588	}
589
590	/// `sessions-reply` payload — every session the broker currently hosts.
591	#[derive(Debug, Clone, Serialize, Deserialize)]
592	pub struct SessionsReply {
593	    #[serde(default)]
594	    pub sessions: Vec<SessionInfo>,

    /// locally-opened outbound streams too.
    #[serde(default)]
    pub initiated_locally: bool,
}

/// `net-streams-reply` payload — every stream the broker currently holds.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetStreamsReply {
    /// Held streams (opened + peer-initiated), unordered.
    #[serde(default)]
    pub streams: Vec<NetStreamInfo>,
}

/// One hosted session in a `sessions-reply`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
    /// The broker-minted session id.
    pub session_id: u64,
    /// The owning endpoint (the spawn's label; empty = unlabeled).
    #[serde(default)]
    pub endpoint: String,
    /// The broker's last-delivered output cursor for this session — the next seq
    /// a re-attaching brain still needs (restoration D4-1, cursor-of-record). A
    /// cold-starting brain re-`subscribe`s each session from here for a gapless,
    /// at-least-once resume instead of replaying the whole ring from 0.
    ///
    /// Additive + `#[serde(default)]` (KH-2.3 N-1 window): an **older broker**
    /// omits the field → a **newer brain** reads `0` and resumes with a full-ring
    /// replay (safe — at-least-once degrades only to dup-only), never a parse
    /// reject. An older brain ignores it.
    // [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
    #[serde(default)]
    pub resume_seq: u64,
}

/// `sessions-reply` payload — every session the broker currently hosts.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionsReply {
    #[serde(default)]
    pub sessions: Vec<SessionInfo>,


440	        return false; // not tabled (or just un-hosted) — nothing to confirm
441	    };
442	    let psyche_perch = perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
443	    // Handle-correct liveness: the brain OWNS the psyche child, so probe its exit via
444	    // try_wait (a held handle makes an exited process read ALIVE through OpenProcess —
445	    // gotcha #1 — which would mask the F-010 fast-exit and skip the stamp).
446	    let psyche_alive = set.psyche_running(id);
447	    let parent_perch = perch::resolve_perch_path(id, ParentHint::Infer);
448	    match classify_residency(elapsed, PSYCHE_RESIDENCY_GRACE, psyche_alive) {
449	        Residency::Resident => {
450	            // RESIDENT past observation: the host truly succeeded. CLEAR any prior
451	            // psyche-host-failure stamp so a now-confirmed live agent shows clean.
452	            // (The clear was deliberately MOVED here from host_one's spawn-Ok leg:
453	            // post-F-010 a successful spawn() is NOT a successful host — only a
454	            // live, resident psyche is. Best-effort; status untouched.)
455	            // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
456	            let _ = spt_store::info::set_psyche_host_error(&parent_perch, None);
457	            false
458	        }
459	        Residency::Pending => false, // inside grace — judge next tick
460	        Residency::Failed => {
461	            let reason = format!(
462	                "host not resident within {}s (psyche perch missing/dead pid)",
463	                PSYCHE_RESIDENCY_GRACE.as_secs()
464	            );
465	            // Stamp the PARENT (harness-reachable signal) — status is NOT touched
466	            // (liveness stays daemon-authoritative; the SIGNAL is psyche_host_error,
467	            // sibling of the v0.8.1 spawn-Err stamp).
468	            let _ = spt_store::info::set_psyche_host_error(&parent_perch, Some(&reason));
469	            // Clear the phantom online nested perch so it is not falsely reported
470	            // live: mark it offline (best-effort; the perch dir may already be gone).
471	            let _ = spt_store::info::set_status(&psyche_perch, spt_store::liveness::STATUS_OFFLINE);
472	            // Un-host the driver + de-table, then ARM a cooldown so the start-side
473	            // does NOT re-spawn the doomed psyche this same sweep (which would clear
474	            // the phantom then re-create it + churn). The system goes quiescent in
475	            // the failed state until the cooldown expires (then one retry).
476	            eprintln!("LIVEHOST_PSYCHE_NONRESIDENT:{id}: {reason}");
477	            set.stop_host(id);
478	            set.arm_nonresident_cooldown(id);
479	            true
480	        }
481	    }
482	}
483
484	/// B2 KEYSTONE — PULL liveness reconcile (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE):
485	/// mark OFFLINE any spt-hosted (controllable) `status=online` `live_agent` perch
486	/// whose broker session is GONE — the correction for the one-way `status=online`
487	/// latch (the broker exit-waiter reaps its session but never clears `info.json`).
488	/// This is the LIVE-STATUS analog of the v0.11.0 roster ghost-heal
489	/// (`advertise_local` → Offline): same crash-robust, self-healing-next-tick shape.
490	///
491	/// `live_sessions` is the set of endpoint ids the broker currently hosts a PTY
492	/// session for ([`crate::msg::SessionsReply`] via `KIND_SESSIONS`). A perch that
493	/// is online + controllable but ABSENT from it has a dead/exited harness → mark it
494	/// offline; the existing [`reconcile_once`] stop-side then un-hosts its driver and
495	/// the start-side stops re-hosting it.
496	///
497	/// **Controllable gate (load-bearing):** only `controllable == Some(true)`
498	/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay
499	/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online
500	/// with NO broker session (its liveness is the relay + ready marker), so it is
501	/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /
502	/// dead-psyche like wall-a) is also exempt here — its phantom class is the
503	/// residency-confirm arm's job ([`confirm_residency_or_unhost`]), not the
504	/// broker-session arm.
505	///
506	/// The caller passes `live_sessions` ONLY when the broker is reachable (it runs
507	/// this from boot tick 1 — the B5 boot-gate — so a phantom is never revived, and
508	/// SKIPS the pass entirely when the broker is unreachable so a transient hiccup
509	/// never mass-offlines). Returns the ids it marked offline.
510	// [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]
511	pub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {
512	    let mut offlined = Vec::new();
513	    for id in perch::list_self_perch_ids(owlery) {
514	        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
515	        let Some(info) = spt_store::info::read_info(&perch) else {
516	            continue;
517	        };
518	        if info.state != LIVE_AGENT_STATE {
519	            continue; // not a live agent
520	        }
521	        if info.status.as_deref() != Some(STATUS_ONLINE) {
522	            continue; // already offline / interim (status-absent)
523	        }
524	        if info.controllable != Some(true) {
525	            continue; // harness-hosted relay (Some(false)) / legacy (None) — exempt
526	        }
527	        if !live_sessions.contains(&id) {
528	            // spt-hosted, online-latched, but the broker hosts no session → the
529	            // harness child is gone. Clear the latch (the keystone).
530	            BrainLifecycle::mark_offline(&perch);
531	            eprintln!("LIVENESS_RECONCILE_OFFLINE:{id}: no live broker session (dead harness)");
532	            offlined.push(id);
533	        }
534	    }
535	    offlined
536	}
537
538	/// Query the broker for the set of endpoint ids it currently hosts a session for
539	/// (`KIND_SESSIONS`) — the B2 pull signal. `None` when the broker is unreachable
540	/// (the caller then SKIPS the offline pass this tick rather than mass-offlining).
541	fn query_live_session_endpoints() -> Option<BTreeSet<String>> {
542	    let mut brain = Brain::cold_start(&crate::endpoint::broker_socket_name(), now_ms()).ok()?;
543	    let reply = brain.sessions().ok()?;
544	    Some(
545	        reply
546	            .sessions
547	            .into_iter()
548	            .map(|s| s.endpoint)
549	            .filter(|e| !e.is_empty())
550	            .collect(),
551	    )
552	}
553
554	/// The normalized program basename a perch's adapter would spawn its Psyche as —
555	/// the first whitespace token of `psyche_init.command`, file-name only, run through
556	/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /
557	/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for
558	/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
559	fn psyche_program_norm(command: &str) -> Option<String> {
560	    let token = command.split_whitespace().next()?;
561	    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();
562	    Some(spt_runtime::resolve::normalize_basename(&file))
563	}
564
565	/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC
566	/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a
567	/// shared box sibling agents share the `claude` basename, so a recycled pid landing
568	/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.
569	/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the
570	/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv
571	/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS
572	/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.
573	/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a
574	/// wrong-kill is catastrophic.
575	///
576	/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's
577	/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
578	/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap
579	/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a
580	/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time
581	/// discriminator (process younger than the perch spawn stamp ⇒ spare).
582	// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
583	fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
584	    if !spt_store::proc::is_process_alive(pid) {
585	        return false;
586	    }
587	    let basename_ok = spt_store::proc::exe_basename(pid)
588	        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)
589	        .unwrap_or(false);
590	    if !basename_ok {
591	        return false;
592	    }
593	    spt_store::proc::process_cmdline(pid)
594	        .map(|cl| cl.contains(psyche_id))
595	        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)
596	}
597
598	/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare
599	/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`

        return false; // not tabled (or just un-hosted) — nothing to confirm
    };
    let psyche_perch = perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    // Handle-correct liveness: the brain OWNS the psyche child, so probe its exit via
    // try_wait (a held handle makes an exited process read ALIVE through OpenProcess —
    // gotcha #1 — which would mask the F-010 fast-exit and skip the stamp).
    let psyche_alive = set.psyche_running(id);
    let parent_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    match classify_residency(elapsed, PSYCHE_RESIDENCY_GRACE, psyche_alive) {
        Residency::Resident => {
            // RESIDENT past observation: the host truly succeeded. CLEAR any prior
            // psyche-host-failure stamp so a now-confirmed live agent shows clean.
            // (The clear was deliberately MOVED here from host_one's spawn-Ok leg:
            // post-F-010 a successful spawn() is NOT a successful host — only a
            // live, resident psyche is. Best-effort; status untouched.)
            // [impl->REQ-HAZARD-LIVEHOST-NONRESIDENT]
            let _ = spt_store::info::set_psyche_host_error(&parent_perch, None);
            false
        }
        Residency::Pending => false, // inside grace — judge next tick
        Residency::Failed => {
            let reason = format!(
                "host not resident within {}s (psyche perch missing/dead pid)",
                PSYCHE_RESIDENCY_GRACE.as_secs()
            );
            // Stamp the PARENT (harness-reachable signal) — status is NOT touched
            // (liveness stays daemon-authoritative; the SIGNAL is psyche_host_error,
            // sibling of the v0.8.1 spawn-Err stamp).
            let _ = spt_store::info::set_psyche_host_error(&parent_perch, Some(&reason));
            // Clear the phantom online nested perch so it is not falsely reported
            // live: mark it offline (best-effort; the perch dir may already be gone).
            let _ = spt_store::info::set_status(&psyche_perch, spt_store::liveness::STATUS_OFFLINE);
            // Un-host the driver + de-table, then ARM a cooldown so the start-side
            // does NOT re-spawn the doomed psyche this same sweep (which would clear
            // the phantom then re-create it + churn). The system goes quiescent in
            // the failed state until the cooldown expires (then one retry).
            eprintln!("LIVEHOST_PSYCHE_NONRESIDENT:{id}: {reason}");
            set.stop_host(id);
            set.arm_nonresident_cooldown(id);
            true
        }
    }
}

/// B2 KEYSTONE — PULL liveness reconcile (REQ-HAZARD-HOSTED-LIVENESS-RECONCILE):
/// mark OFFLINE any spt-hosted (controllable) `status=online` `live_agent` perch
/// whose broker session is GONE — the correction for the one-way `status=online`
/// latch (the broker exit-waiter reaps its session but never clears `info.json`).
/// This is the LIVE-STATUS analog of the v0.11.0 roster ghost-heal
/// (`advertise_local` → Offline): same crash-robust, self-healing-next-tick shape.
///
/// `live_sessions` is the set of endpoint ids the broker currently hosts a PTY
/// session for ([`crate::msg::SessionsReply`] via `KIND_SESSIONS`). A perch that
/// is online + controllable but ABSENT from it has a dead/exited harness → mark it
/// offline; the existing [`reconcile_once`] stop-side then un-hosts its driver and
/// the start-side stops re-hosting it.
///
/// **Controllable gate (load-bearing):** only `controllable == Some(true)`
/// (spt-hosted, broker-PTY) perches are reconciled here. A HARNESS-HOSTED relay
/// live agent (`api listen`, `controllable == Some(false)`) is legitimately online
/// with NO broker session (its liveness is the relay + ready marker), so it is
/// NEVER marked offline by this pass. A `controllable == None` perch (legacy /
/// dead-psyche like wall-a) is also exempt here — its phantom class is the
/// residency-confirm arm's job ([`confirm_residency_or_unhost`]), not the
/// broker-session arm.
///
/// The caller passes `live_sessions` ONLY when the broker is reachable (it runs
/// this from boot tick 1 — the B5 boot-gate — so a phantom is never revived, and
/// SKIPS the pass entirely when the broker is unreachable so a transient hiccup
/// never mass-offlines). Returns the ids it marked offline.
// [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]
pub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {
    let mut offlined = Vec::new();
    for id in perch::list_self_perch_ids(owlery) {
        let perch = perch::resolve_perch_path(&id, ParentHint::Infer);
        let Some(info) = spt_store::info::read_info(&perch) else {
            continue;
        };
        if info.state != LIVE_AGENT_STATE {
            continue; // not a live agent
        }
        if info.status.as_deref() != Some(STATUS_ONLINE) {
            continue; // already offline / interim (status-absent)
        }
        if info.controllable != Some(true) {
            continue; // harness-hosted relay (Some(false)) / legacy (None) — exempt
        }
        if !live_sessions.contains(&id) {
            // spt-hosted, online-latched, but the broker hosts no session → the
            // harness child is gone. Clear the latch (the keystone).
            BrainLifecycle::mark_offline(&perch);
            eprintln!("LIVENESS_RECONCILE_OFFLINE:{id}: no live broker session (dead harness)");
            offlined.push(id);
        }
    }
    offlined
}

/// Query the broker for the set of endpoint ids it currently hosts a session for
/// (`KIND_SESSIONS`) — the B2 pull signal. `None` when the broker is unreachable
/// (the caller then SKIPS the offline pass this tick rather than mass-offlining).
fn query_live_session_endpoints() -> Option<BTreeSet<String>> {
    let mut brain = Brain::cold_start(&crate::endpoint::broker_socket_name(), now_ms()).ok()?;
    let reply = brain.sessions().ok()?;
    Some(
        reply
            .sessions
            .into_iter()
            .map(|s| s.endpoint)
            .filter(|e| !e.is_empty())
            .collect(),
    )
}

/// The normalized program basename a perch's adapter would spawn its Psyche as —
/// the first whitespace token of `psyche_init.command`, file-name only, run through
/// [`normalize_basename`](spt_runtime::resolve::normalize_basename) (lowercase /
/// `.exe`-strip / dot-stem) so it keys the same as a live exe basename. `None` for
/// an empty command. // [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_program_norm(command: &str) -> Option<String> {
    let token = command.split_whitespace().next()?;
    let file = Path::new(token).file_name()?.to_string_lossy().into_owned();
    Some(spt_runtime::resolve::normalize_basename(&file))
}

/// Whether `pid` is the live `{id}-psyche` orphan we may reap — the ID-SPECIFIC
/// recycle guard (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP). NOT basename-only: on a
/// shared box sibling agents share the `claude` basename, so a recycled pid landing
/// on a SIBLING'S `claude` would match basename alone → a wrong-kill of a live agent.
/// Requires ALL THREE — alive, exe basename == the adapter's psyche program, AND the
/// command line contains the FULL psyche id `<id>-psyche` (baked into the psyche argv
/// via `{id}`; a sibling `agent-other-psyche` never contains `agent-psyche`). FAILS
/// SAFE: any None (dead / unreadable basename / unreadable cmdline) ⇒ `false`, i.e.
/// decline to reap — a missed dup is cheap (Breap bounds it at daemon-stop), a
/// wrong-kill is catastrophic.
///
/// CAVEAT: the cmdline carries `<id>-psyche` only when the adapter's
/// `psyche_init.command` uses `{id}` (the norm — a Psyche needs its identity;
/// spt-claude-code does). An adapter that omits `{id}` SAFELY MISSES this reap
/// (the dup lingers until daemon-stop/Breap, exactly today's behavior) — never a
/// wrong-kill. If a real non-`{id}` psyche adapter ever ships, add a start-time
/// discriminator (process younger than the perch spawn stamp ⇒ spare).
// [impl->REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP]
fn psyche_orphan_should_reap(psyche_id: &str, expected_prog_norm: &str, pid: u32) -> bool {
    if !spt_store::proc::is_process_alive(pid) {
        return false;
    }
    let basename_ok = spt_store::proc::exe_basename(pid)
        .map(|b| spt_runtime::resolve::normalize_basename(&b) == expected_prog_norm)
        .unwrap_or(false);
    if !basename_ok {
        return false;
    }
    spt_store::proc::process_cmdline(pid)
        .map(|cl| cl.contains(psyche_id))
        .unwrap_or(false) // cmdline unreadable ⇒ fail safe (decline to reap)
}

/// Brain-start orphan-psyche reap (REQ-HAZARD-BRAIN-RESTART-PSYCHE-DUP): a bare
/// brain restart leaves the PRIOR brain's `{id}-psyche` processes ALIVE — `stop_host`


Reconcile queries LOCAL broker only — same binary version. N-1 skew for Gap A not reachable here (resume_seq's N-1 was cross-machine net). Now nail the gap mechanics — when does `controller_by` actually go None?


grep -n "detach_if\|mark_controller_gone\|send_timeout\|fn drain\|exit_wait\|EOF\|read_frame\|handle_conn\|fn dispatch\b\|reap\|fn serve\|controller_epoch" crates/spt-daemon/src/broker.rs | head -50

Find connection lifecycle + detach callers


31://! (reaped by the exit waiter) or is explicitly killed.
49:use crate::codec::{read_frame, write_frame};
124:/// drain blocks on a bounded `send_timeout` and the controller is detached only
138:/// has no stable `send_timeout`). Only used while the channel is FULL — the
184:    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or
211:    /// the drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it
214:    controller_epoch: u64,
230:    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
273:            controller_epoch: 0,
329:            epoch: self.controller_epoch,
347:    /// ends its writer; bumping `controller_epoch` makes a stale deadline-evict a
354:        self.controller_epoch += 1;
378:    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per
389:    /// timed out its `send_timeout`. Detach it — but ONLY if it is still the same
392:    fn mark_controller_gone(&mut self, epoch: u64) {
393:        if self.controller_epoch == epoch {
528:    fn detach_if(&mut self, sub: &SharedSend) {
652:    /// A `try_send` poll loop (std `SyncSender` has no stable `send_timeout`): the
811:/// (session teardown), reaping the child.
890:/// reverts to raw inject and `dispatch_input` stops pinging), log it, and reap the
1015:    pub fn serve(self: &Arc<Self>) -> io::Result<()> {
1020:                let _ = me.handle_conn(conn);
1027:    pub fn serve_once(self: &Arc<Self>) -> io::Result<JoinHandle<()>> {
1031:            let _ = me.handle_conn(conn);
1080:    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
1092:            let env = match read_frame(&mut recv) {
1094:                // Any read failure (EOF included) ends the connection.
1209:                h.log.lock().unwrap().detach_if(&send);
1279:                        log_drain.lock().unwrap().mark_controller_gone(epoch);
1285:        // Exit waiter → one `exit` frame when the child is reaped, then the dead
1396:        // attach. The dead-not-reaped case needs no fast-detect: the exit-waiter
1397:        // already broadcasts an `Exit` frame to all attached sinks on reap, and a
1437:        log.lock().unwrap().detach_if(send);
1943:    /// reaps the session once the child is dead.


1080	    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
1081	        let (mut recv, send) = conn.split();
1082	        let send: SharedSend = Arc::new(Mutex::new(send));
1083
1084	        // Version handshake: this connection must be a brain (2.3).
1085	        recv_hello(&mut recv, Role::Brain)?;
1086
1087	        let mut my_subs: Vec<u64> = Vec::new();
1088	        let mut my_stream_subs: Vec<u64> = Vec::new();
1089	        let mut my_presence_sub = false;
1090
1091	        loop {
1092	            let env = match read_frame(&mut recv) {
1093	                Ok(e) => e,
1094	                // Any read failure (EOF included) ends the connection.
1095	                Err(_) => break,
1096	            };
1097	            match env.kind.as_str() {
1098	                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
1099	                    Ok(id) => my_subs.push(id),
1100	                    Err(msg) => send_error(&send, &msg),
1101	                },
1102	                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
1103	                    Ok(id) => my_subs.push(id),
1104	                    Err(msg) => send_error(&send, &msg),
1105	                },
1106	                KIND_UNSUBSCRIBE => {
1107	                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {
1108	                        send_error(&send, &msg);
1109	                    }
1110	                }
1111	                KIND_INPUT => {
1112	                    if let Err(msg) = self.dispatch_input(env, &send) {
1113	                        send_error(&send, &msg);
1114	                    }
1115	                }
1116	                KIND_RESIZE => {
1117	                    if let Err(msg) = self.dispatch_resize(env, &send) {
1118	                        send_error(&send, &msg);
1119	                    }
1120	                }
1121	                KIND_KILL => {
1122	                    if let Err(msg) = self.dispatch_kill(env) {
1123	                        send_error(&send, &msg);
1124	                    }
1125	                }
1126	                KIND_NET_STATUS => self.dispatch_net_status(&send),
1127	                KIND_NET_DIAL => {
1128	                    if let Err(msg) = self.dispatch_net_dial(env, &send) {
1129	                        send_error(&send, &msg);
1130	                    }
1131	                }
1132	                KIND_NET_DIAL_LOOPBACK => {
1133	                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {
1134	                        send_error(&send, &msg);
1135	                    }
1136	                }
1137	                KIND_PAIR_JOIN => {
1138	                    if let Err(msg) = self.dispatch_pair_join(env, &send) {
1139	                        send_error(&send, &msg);
1140	                    }
1141	                }
1142	                KIND_NET_STREAM_OPEN => {
1143	                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {
1144	                        send_error(&send, &msg);
1145	                    }
1146	                }
1147	                KIND_NET_STREAM_SEND => {
1148	                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {
1149	                        send_error(&send, &msg);
1150	                    }
1151	                }
1152	                KIND_NET_STREAMS => {
1153	                    if let Err(msg) = self.dispatch_net_streams(&send) {
1154	                        send_error(&send, &msg);
1155	                    }
1156	                }
1157	                KIND_SESSIONS => {
1158	                    // Clone the (id, endpoint, log) out from under the sessions
1159	                    // lock, then read each log's cursor under the log lock alone —
1160	                    // never both locks at once (the dispatch_subscribe discipline).
1161	                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {
1162	                        let sessions = self.sessions.lock().unwrap();
1163	                        sessions
1164	                            .iter()
1165	                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))
1166	                            .collect()
1167	                    };
1168	                    let reply = SessionsReply {
1169	                        sessions: rows
1170	                            .into_iter()
1171	                            .map(|(id, endpoint, log)| SessionInfo {
1172	                                session_id: id,
1173	                                endpoint,
1174	                                resume_seq: log.lock().unwrap().delivered_through(),
1175	                            })
1176	                            .collect(),
1177	                    };
1178	                    let frame = Envelope::new(
1179	                        KIND_SESSIONS_REPLY,
1180	                        serde_json::to_value(reply).expect("SessionsReply serializes"),
1181	                    );
1182	                    send_frame(&send, &frame);
1183	                }
1184	                KIND_ENDPOINT_INPUT => {
1185	                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {
1186	                        send_error(&send, &msg);
1187	                    }
1188	                }
1189	                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {
1190	                    Ok(id) => my_stream_subs.push(id),
1191	                    Err(msg) => send_error(&send, &msg),
1192	                },
1193	                KIND_NET_PRESENCE_SUBSCRIBE => {
1194	                    match self.dispatch_net_presence_subscribe(env, &send) {
1195	                        Ok(()) => my_presence_sub = true,
1196	                        Err(msg) => send_error(&send, &msg),
1197	                    }
1198	                }
1199	                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),
1200	                // An unknown kind is tolerated (forward-compat): report, don't die.
1201	                other => send_error(&send, &format!("unknown command kind {other:?}")),
1202	            }
1203	        }
1204
1205	        // Brain gone: detach as subscriber but leave the sessions running.
1206	        let sessions = self.sessions.lock().unwrap();
1207	        for id in &my_subs {
1208	            if let Some(h) = sessions.get(id) {
1209	                h.log.lock().unwrap().detach_if(&send);

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            match env.kind.as_str() {
                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
                    Ok(id) => my_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
                    Ok(id) => my_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_UNSUBSCRIBE => {
                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_INPUT => {
                    if let Err(msg) = self.dispatch_input(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_RESIZE => {
                    if let Err(msg) = self.dispatch_resize(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_KILL => {
                    if let Err(msg) = self.dispatch_kill(env) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STATUS => self.dispatch_net_status(&send),
                KIND_NET_DIAL => {
                    if let Err(msg) = self.dispatch_net_dial(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_DIAL_LOOPBACK => {
                    if let Err(msg) = self.dispatch_net_dial_loopback(&send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_PAIR_JOIN => {
                    if let Err(msg) = self.dispatch_pair_join(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_OPEN => {
                    if let Err(msg) = self.dispatch_net_stream_open(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_SEND => {
                    if let Err(msg) = self.dispatch_net_stream_send(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAMS => {
                    if let Err(msg) = self.dispatch_net_streams(&send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_SESSIONS => {
                    // Clone the (id, endpoint, log) out from under the sessions
                    // lock, then read each log's cursor under the log lock alone —
                    // never both locks at once (the dispatch_subscribe discipline).
                    let rows: Vec<(u64, String, Arc<Mutex<OutputLog>>)> = {
                        let sessions = self.sessions.lock().unwrap();
                        sessions
                            .iter()
                            .map(|(id, h)| (*id, h.endpoint.clone(), Arc::clone(&h.log)))
                            .collect()
                    };
                    let reply = SessionsReply {
                        sessions: rows
                            .into_iter()
                            .map(|(id, endpoint, log)| SessionInfo {
                                session_id: id,
                                endpoint,
                                resume_seq: log.lock().unwrap().delivered_through(),
                            })
                            .collect(),
                    };
                    let frame = Envelope::new(
                        KIND_SESSIONS_REPLY,
                        serde_json::to_value(reply).expect("SessionsReply serializes"),
                    );
                    send_frame(&send, &frame);
                }
                KIND_ENDPOINT_INPUT => {
                    if let Err(msg) = self.dispatch_endpoint_input(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {
                    Ok(id) => my_stream_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_NET_PRESENCE_SUBSCRIBE => {
                    match self.dispatch_net_presence_subscribe(env, &send) {
                        Ok(()) => my_presence_sub = true,
                        Err(msg) => send_error(&send, &msg),
                    }
                }
                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),
                // An unknown kind is tolerated (forward-compat): report, don't die.
                other => send_error(&send, &format!("unknown command kind {other:?}")),
            }
        }

        // Brain gone: detach as subscriber but leave the sessions running.
        let sessions = self.sessions.lock().unwrap();
        for id in &my_subs {
            if let Some(h) = sessions.get(id) {
                h.log.lock().unwrap().detach_if(&send);


Connection model clear: controller detaches on read EOF (line 1092-1095 → 1205-1209 `detach_if` → `clear_controller` → unlatch). Clean disconnect self-heals. The gaps are lost-FIN / broker-restart where the slot empties without re-stamp, or persists stale. Plan says decide in repro. Check test rigs + REQ registry.


ls crates/spt-daemon/tests/; echo ---REQ---; grep -n -i "driven.by\|self.heal\|REACH-1\|RCVIEW" traceable-reqs.toml | head -20

List int tests + grep reqs


access.rs
attach.rs
brain_swap.rs
broker.rs
budget.rs
daemon_e2e.rs
daemon_lifecycle_real_brain.rs
digest.rs
dispatch.rs
handoff.rs
idempotent.rs
inject_control_wedge.rs
mesh.rs
netbroker.rs
netstream.rs
notifsync.rs
pairjoin.rs
presence.rs
propagate.rs
pump.rs
pumpdeadline.rs
replicate.rs
reseed.rs
resume.rs
rosterprop.rs
seedproofx.rs
shellchan.rs
sync.rs
two_origin_spanning.rs
twohost.rs
wanmsg.rs
xfer.rs
---REQ---
269:id = "REQ-REACH-1"
397:required_stages = ["impl", "unit", "int"]  # activated M4-D7-1: net/update.rs UpdRecord offer-then-fetch wire protocol + relcache.rs staged-release cache (verified-pull ⇒ staged ⇒ servable — the self-heal hop); D7-2 adds the serve/request drivers + loopback propagation E2E. int activated M4-D9-6: two-host rig E2E green (tests/twohost.rs — B's staged v6 verified + staged at A under the trusted key, consent-notified)
424:title = "Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent."
522:id = "REQ-RCVIEW-1"
532:title = "The picker renders a FOUR-state endpoint status (extending the W2 online/offline duality): the list-item square AND a color-coded STATUS line at the top of the pick-existing right-side details both show — gray OFFLINE; green ONLINE (online + PTY-controllable spt-hosted, not controlled); amber 'ONLINE - HARNESS ONLY' (online but NOT broker-PTY-controllable = harness-hosted, no broker PTY seat — today mis-shows green); blue 'ONLINE + CONTROLLED' (online + driven_by.is_some()). Derived on EndpointRow from {offline | controllable | driven_by} with precedence offline→gray, else driven_by→blue, else !controllable→amber, else green (driven_by outranks harness-only; mutually exclusive in practice — a harness-only endpoint has no broker PTY to control). The controllable discriminator is a NEW InfoJson.controllable: Option<bool> (serde-default, N-1-safe), stamped at the establish seam — cmd_listen (harness-hosted relay, no broker PTY) → Some(false); cmd_bind live_agent (spt-hosted broker PTY) → Some(true); absent → not-controllable (amber) default (harness-hosted is the common mis-reported case; one bind self-corrects). Store-projection-only (no live daemon query — doyle ruling). (v0.10.0)"
569:title = "A LOCAL subnet roster entry whose backing perch is erased does NOT keep advertising Active (no phantom perch-less endpoint). `api session-end <id> --erase` removes the perch (owlery dir gone) but the subnet roster (identity/registry/<subnet>.json) keeps the endpoint's instance row ACTIVE with no backing perch; `endpoint stop` says 'address unregistered' yet the line persists; no CLI verb forgets a roster entry, and a hand-edit is re-added by the single-writer daemon advertiser. FIX: daemon-side self-heal — the advertiser DROPS/forgets a LOCAL roster entry whose backing perch no longer exists (stops advertising it Active), and/or a `forget`/evict verb; verify whether the epoch lease eventually evicts it (slow-self-heal) vs a real leak and scope accordingly. doyle secondary finding (perri). (post-v0.10.0)"
574:title = "B2 KEYSTONE: a daemon-hosted (spt-hosted) endpoint's info.json status is RECONCILED to real liveness, not left latched online. The broker exit-waiter (broker.rs:889-910) reaps its in-mem session table + emits ExitEvent but NEVER touches info.json; lifecycle::mark_offline only fires on Psyche teardown — so a dead/exited harness (operator closed the tab) stays status=online forever (is_perch_alive returns ONLINE for daemon-hosted, liveness.rs:80-93). FIX (doyle ruled PULL-PRIMARY — the live-status analog of REQ-HAZARD-ROSTER-GHOST): the livehost reconcile loop (reconcile_once livehost.rs:226-313) queries the broker's live session set (KIND_SESSIONS) each tick and, for any status=online live_agent perch PAST the boot grace whose endpoint has NO live broker session, marks it offline (lifecycle::mark_offline → status=offline → is_perch_alive=false). GATED on spt-hosted (controllable==Some(true)) so a HARNESS-HOSTED relay live agent (api listen, legitimately online with no broker session) is NEVER mis-marked. Crash-robust + self-healing on the next tick (clear-on-event is not crash-robust alone). PUSH (brain ExitEvent→mark_offline) is an OPTIONAL fast-path only if the daemon brain is reliably subscribed to all hosted sessions; correctness rides the pull. Broker stays stateless (ADR-0004 §B — brain owns the info.json write). (v0.12.0)"
672:title = "Durable active-profile pointer for bind-time profile selection (ADR-0021): adapters/active-profiles.toml at the registry ROOT (sibling to the per-adapter <name>/ dirs, so adapter add/update/remove — which only rewrite a <name>/ subdir — can never clobber it), a flat host_binary → \"adapter[:profile]\" map. Read at bind as the PRIMARY profile selector; unset → the registered_at_ms fallback (REQ-START-5). Written ONLY by `spt adapter use <adapter>[:profile]` (resolves the adapter's host_binaries → sets each binary→adapter[:profile]); `spt adapter use --clear <adapter|binary>` drops. NEVER auto-written by install/update/adapter add (that is precisely what would let an update silently flip the active profile). A stale pointer (uninstalled adapter / deleted profile) self-heals: ignored, fall back, warn once. Pruned on adapter remove. Atomic write (spt_store atomic). (v0.9.0)"
673:required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W3: doc = CONTEXT.md §180 active-profile pointer rule + docs/MANIFEST.md adapter use guidance. impl = the active-profiles.toml pointer store (spt-runtime resolve: load/save at the adapters root, set_active/clear_active/prune_adapter, stale self-heal in resolve_from_basename) + the `spt adapter use <adapter>[:profile]` / `--clear` CLI verb (cli.rs AdapterCmd::Use) + prune-on-remove (AdapterCmd::Remove). unit = set_clear_prune_rules/stale_pointer_self_heals/pointer_overrides_fallback/pointer_is_sibling (resolve) + adapter_use_sets_and_clears_pointer (cli). int (pointer→fallback resolution) folds into REQ-START-5's final-wave bringup E2E
1048:title = "Pump liveness: the peer pump writes a last-tick heartbeat consumed by daemon status / subnet status (decision 23 render legs in REQ-CLI-2/REQ-SUBNET-8); the daemon supervises the pump task — a panic is caught, logged loudly, and the pump restarts with capped backoff (≤5 min), so a 5.9-class death self-heals visibly instead of silently halving the daemon (M8 decision 23; field motivation: hfenduleam 2026-06-07 half-death)"
1067:title = "Net-bind boot-race resilience: a daemon that comes up net-less (NetHost::start failed — e.g. the systemd unit autostarted before the network/DNS stack was ready, `Failed to create an address lookup service`) must SELF-HEAL — retry the net bring-up in the background with capped backoff and, on success, attach net to the broker + spawn the dispatcher/peer-pump (which today are gated on `net_up` at boot and so never start, leaving the node silently unreachable until a manual restart — kitsubito 2026-06-08). Status surfaces the net-less state honestly (a net-less broker renders as 'no connection', not only a pump-STALLED line with a bogus pre-boot heartbeat age). The installer's autostart unit waits for the network (`Wants=/After=network-online.target`) as belt-and-suspenders."
1191:required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.
1211:# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
1213:id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
1214:title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
1215:required_stages = []  # SEED — activate [impl,unit,int] at W5 build. int gate (real fixture): kill the controller without a clean detach → next reconcile tick clears driven_by (ONLINE+CONTROLLED → just ONLINE/idle).


REQ already seeded: `REQ-HAZARD-DRIVEN-BY-SELFHEAL` (line 1213, stages=[]). Activate at build. Now read the reconcile caller + info accessors.


grep -n "reconcile_hosted_liveness\|query_live_session_endpoints\|fn reconcile_once\|driven_by\|set_driven_by\|fn read_info" crates/spt-daemon/src/livehost.rs crates/spt-store/src/info.rs | head -40

Find reconcile caller + info driven_by


crates/spt-daemon/src/livehost.rs:263:pub fn reconcile_once(
crates/spt-daemon/src/livehost.rs:511:pub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {
crates/spt-daemon/src/livehost.rs:541:fn query_live_session_endpoints() -> Option<BTreeSet<String>> {
crates/spt-daemon/src/livehost.rs:694:            if let Some(live) = query_live_session_endpoints() {
crates/spt-daemon/src/livehost.rs:695:                reconcile_hosted_liveness(&owlery, &live);
crates/spt-daemon/src/livehost.rs:836:            let offlined = reconcile_hosted_liveness(&perch::owlery_dir(), &live);
crates/spt-daemon/src/livehost.rs:860:    // reconcile runs reconcile_hosted_liveness BEFORE reconcile_once, so a
crates/spt-daemon/src/livehost.rs:895:            reconcile_hosted_liveness(&perch::owlery_dir(), &live);
crates/spt-daemon/src/livehost.rs:1130:            assert!(reconcile_hosted_liveness(&perch::owlery_dir(), &live).is_empty());
crates/spt-daemon/src/livehost.rs:1142:            let offlined = reconcile_hosted_liveness(&perch::owlery_dir(), &live2);
crates/spt-store/src/info.rs:100:    pub driven_by: Option<String>,
crates/spt-store/src/info.rs:104:    /// add/remove/evict; the picker renders `controlled by <driven_by> (+N
crates/spt-store/src/info.rs:106:    /// best-effort/self-heal caveat as `driven_by`. `default` so older records
crates/spt-store/src/info.rs:192:            driven_by: None,
crates/spt-store/src/info.rs:286:/// Stamp (or clear, with `None`) the `driven_by` remote-drive marker (REQ-REACH-1):
crates/spt-store/src/info.rs:290:pub fn set_driven_by(perch_path: &Path, node: Option<&str>) -> std::io::Result<()> {
crates/spt-store/src/info.rs:291:    mutate_info(perch_path, |rec| rec.driven_by = node.map(str::to_string))
crates/spt-store/src/info.rs:354:pub fn read_info(perch_path: &Path) -> Option<InfoJson> {
crates/spt-store/src/info.rs:431:            driven_by: None,
crates/spt-store/src/info.rs:505:    // the transient setters (last_active/rest/driven_by/viewer_count — each a
crates/spt-store/src/info.rs:532:                    let _ = set_driven_by(&path, Some("nodehex"));
crates/spt-store/src/info.rs:672:    // [unit->REQ-REACH-1] set_driven_by stamps the remote-drive marker onto an
crates/spt-store/src/info.rs:675:    // all) still parses with driven_by == None.
crates/spt-store/src/info.rs:677:    fn set_driven_by_stamps_and_clears() {
crates/spt-store/src/info.rs:686:            !raw.contains("driven_by"),
crates/spt-store/src/info.rs:690:        set_driven_by(d.path(), Some("cafe1234")).unwrap();
crates/spt-store/src/info.rs:692:            read_info(d.path()).unwrap().driven_by.as_deref(),
crates/spt-store/src/info.rs:696:        set_driven_by(d.path(), None).unwrap();
crates/spt-store/src/info.rs:698:            read_info(d.path()).unwrap().driven_by,
crates/spt-store/src/info.rs:705:        assert!(set_driven_by(empty.path(), Some("cafe1234")).is_err());
crates/spt-store/src/info.rs:714:        assert_eq!(read_info(old.path()).unwrap().driven_by, None);


680	            // latch for any spt-hosted endpoint whose broker session is gone — and
681	            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once
682	            // so the same-tick stop-side un-hosts the now-offline perch and the
683	            // start-side never REVIVES its Psyche. The boot-gate is what stops a
684	            // cold start after an unclean stop from spawning N phantom Psyches for N
685	            // dead-but-latched perches. Skipped only when the broker is unreachable
686	            // (None) — never mass-offline on a hiccup; safe at tick 1 because
687	            // run_brain connects to the broker BEFORE this host spawns, so the query
688	            // returns the REAL session set: empty at a cold start (every online
689	            // controllable perch is a phantom → offlined), the surviving sessions at
690	            // a brain restart (live agents stay, only the dead are offlined). The
691	            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.
692	            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]
693	            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]
694	            if let Some(live) = query_live_session_endpoints() {
695	                reconcile_hosted_liveness(&owlery, &live);
696	            }
697	            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);
698	            // Sleep in slices so a stop lands promptly.
699	            let mut left = LIVE_RECONCILE_INTERVAL_MS;
700	            while left > 0 && !stop.load(Ordering::SeqCst) {
701	                let step = left.min(100);
702	                std::thread::sleep(Duration::from_millis(step));
703	                left -= step;
704	            }
705	        }
706	    })
707	}
708
709	#[cfg(test)]
710	mod tests {
711	    use super::*;
712	    use crate::test_home::with_home;
713	    use std::time::{Duration, Instant};
714
715	    // [unit->REQ-HAZARD-LIVEHOST-NONRESIDENT] the pure residency decision: a live
716	    // psyche pid is RESIDENT regardless of elapsed; a dead/missing pid is PENDING
717	    // inside the grace (too early to judge — a slow-but-healthy psyche may still be
718	    // coming up) and FAILED only once past the grace (the F-009 fast-exit case).
719	    #[test]

            // latch for any spt-hosted endpoint whose broker session is gone — and
            // do it from BOOT tick 1, NOT after a grace. Runs BEFORE reconcile_once
            // so the same-tick stop-side un-hosts the now-offline perch and the
            // start-side never REVIVES its Psyche. The boot-gate is what stops a
            // cold start after an unclean stop from spawning N phantom Psyches for N
            // dead-but-latched perches. Skipped only when the broker is unreachable
            // (None) — never mass-offline on a hiccup; safe at tick 1 because
            // run_brain connects to the broker BEFORE this host spawns, so the query
            // returns the REAL session set: empty at a cold start (every online
            // controllable perch is a phantom → offlined), the surviving sessions at
            // a brain restart (live agents stay, only the dead are offlined). The
            // controllable==Some(true) gate inside keeps relay/legacy perches exempt.
            // [impl->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]
            // [impl->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE]
            if let Some(live) = query_live_session_endpoints() {
                reconcile_hosted_liveness(&owlery, &live);
            }
            reconcile_once(&owlery, &registered, &adapters_dir, &set, &cfg, reason);
            // Sleep in slices so a stop lands promptly.
            let mut left = LIVE_RECONCILE_INTERVAL_MS;
            while left > 0 && !stop.load(Ordering::SeqCst) {
                let step = left.min(100);
                std::thread::sleep(Duration::from_millis(step));
                left -= step;
            }
        }
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use std::time::{Duration, Instant};

    // [unit->REQ-HAZARD-LIVEHOST-NONRESIDENT] the pure residency decision: a live
    // psyche pid is RESIDENT regardless of elapsed; a dead/missing pid is PENDING
    // inside the grace (too early to judge — a slow-but-healthy psyche may still be
    // coming up) and FAILED only once past the grace (the F-009 fast-exit case).
    #[test]


Enough grounding. Architecture map:
- **Gap B** (no session): reconcile already `mark_offline`s — just also clear `driven_by`. No new field, N-1 safe.
- **Gap A** (live session, lost controller): need controller-presence → `SessionInfo.controller_by`. Real trigger = broker controller slot empties without re-stamp (e.g. fresh broker memory + stale disk `driven_by`). Exact trigger + observed state = repro's job.
- REQ `REQ-HAZARD-DRIVEN-BY-SELFHEAL` seeded; activate `[impl,unit,int]` at landing (same commit as evidence — not now, avoids pre-fail).

Peek rig + attach mechanism before delegating repro.


1	//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY
2	//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local
3	//! IPC (no mocks of the PTY or the transport).
4	//!
5	//! ## The bug this gate locks down (park (a), the output-stall / wedge root)
6	//!
7	//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the
8	//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the
9	//! session's single drain thread, while the `Mutex<OutputLog>` was locked.
10	//! VIEWERS instead get a dedicated writer thread + a bounded, evicting
11	//! `sync_channel` and can never stall the drain. So a slow / non-draining
12	//! controller (here a controller connection that stops reading while the child
13	//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output
14	//! stalled and any path needing that log lock (a fresh attach, or the global
15	//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)
16	//! blocked FOREVER, while the broker process stayed alive. The repro that proved
17	//! this (captured 2026-06-19): `APPEND_CTRL_ENTER sid=1 seq=2` with no matching
18	//! `APPEND_CTRL_EXIT`, `broker_alive=true` but `sessions_answered=false`.
19	//!
20	//! POST-FIX (W1): controller delivery moved off the drain thread onto a dedicated
21	//! writer thread + bounded channel (the viewer_writer pattern, but authoritative:
22	//! the writer advances `delivered_through`). The drain hands each chunk off with a
23	//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached
24	//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed
25	//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test
26	//! asserts that fixed behavior.
27	//!
28	//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded (W1b F1)
29	//!
30	//! These tests share ONE process-global `SPT_HOME` (set once via
31	//! [`std::env::set_var`] in [`init_wedge_home`]) and each spins a REAL broker +
32	//! PTY child. Under the default `cargo test` harness — N tests as parallel
33	//! THREADS in ONE process — the `set_var` mutation races and the concurrent
34	//! brokers contend, which HANGS on Windows (cost a 30-min wedge during W1b).
35	//!
36	//! Run them isolated:
37	//!   - `cargo nextest run --test inject_control_wedge`  (process-per-test — CI's path, always safe), or
38	//!   - `cargo test --test inject_control_wedge -- --test-threads=1`  (single-threaded fallback).
39	//!
40	//! Do NOT run a bare `cargo test --test inject_control_wedge` on Windows.
41
42	use std::sync::Arc;
43	use std::thread;
44	use std::time::{Duration, Instant};
45
46	use interprocess::local_socket::Stream;
47	use spt_daemon::codec::{read_frame, write_frame};
48	use spt_daemon::frame::{Envelope, Role};
49	use spt_daemon::msg::{
50	    encode_bytes, EndpointInputReq, SpawnReq, Spawned, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT,
51	    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
52	    KIND_SPAWNED,
53	};
54	use spt_daemon::transport::{send_hello, LocalSocketTransport};
55	use spt_daemon::Broker;
56
57	// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach
58	//    path, the Brain client API, and a net-backed broker for loopback attach. ──
59	use std::sync::atomic::{AtomicBool, AtomicU64};
60	use std::sync::OnceLock;
61	use spt_daemon::attach::{request_attach, serve_attach};
62	use spt_daemon::brain::{Brain, BrokerEvent};
63	use spt_daemon::msg::decode_bytes;
64	use spt_daemon::nethost::{NetConfig, NetHost};
65	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
66	use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
67	use spt_proto::identity::Identity;
68
69	use std::sync::atomic::{AtomicU32, Ordering};
70	static SEQ: AtomicU32 = AtomicU32::new(0);

//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY
//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local
//! IPC (no mocks of the PTY or the transport).
//!
//! ## The bug this gate locks down (park (a), the output-stall / wedge root)
//!
//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the
//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the
//! session's single drain thread, while the `Mutex<OutputLog>` was locked.
//! VIEWERS instead get a dedicated writer thread + a bounded, evicting
//! `sync_channel` and can never stall the drain. So a slow / non-draining
//! controller (here a controller connection that stops reading while the child
//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output
//! stalled and any path needing that log lock (a fresh attach, or the global
//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)
//! blocked FOREVER, while the broker process stayed alive. The repro that proved
//! this (captured 2026-06-19): `APPEND_CTRL_ENTER sid=1 seq=2` with no matching
//! `APPEND_CTRL_EXIT`, `broker_alive=true` but `sessions_answered=false`.
//!
//! POST-FIX (W1): controller delivery moved off the drain thread onto a dedicated
//! writer thread + bounded channel (the viewer_writer pattern, but authoritative:
//! the writer advances `delivered_through`). The drain hands each chunk off with a
//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached
//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed
//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test
//! asserts that fixed behavior.
//!
//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded (W1b F1)
//!
//! These tests share ONE process-global `SPT_HOME` (set once via
//! [`std::env::set_var`] in [`init_wedge_home`]) and each spins a REAL broker +
//! PTY child. Under the default `cargo test` harness — N tests as parallel
//! THREADS in ONE process — the `set_var` mutation races and the concurrent
//! brokers contend, which HANGS on Windows (cost a 30-min wedge during W1b).
//!
//! Run them isolated:
//!   - `cargo nextest run --test inject_control_wedge`  (process-per-test — CI's path, always safe), or
//!   - `cargo test --test inject_control_wedge -- --test-threads=1`  (single-threaded fallback).
//!
//! Do NOT run a bare `cargo test --test inject_control_wedge` on Windows.

use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use interprocess::local_socket::Stream;
use spt_daemon::codec::{read_frame, write_frame};
use spt_daemon::frame::{Envelope, Role};
use spt_daemon::msg::{
    encode_bytes, EndpointInputReq, SpawnReq, Spawned, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
    KIND_SPAWNED,
};
use spt_daemon::transport::{send_hello, LocalSocketTransport};
use spt_daemon::Broker;

// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach
//    path, the Brain client API, and a net-backed broker for loopback attach. ──
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::OnceLock;
use spt_daemon::attach::{request_attach, serve_attach};
use spt_daemon::brain::{Brain, BrokerEvent};
use spt_daemon::msg::decode_bytes;
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

use std::sync::atomic::{AtomicU32, Ordering};
static SEQ: AtomicU32 = AtomicU32::new(0);


Rig confirmed. Repro needs `SessionInfo.controller_by` to even observe Gap A — add the additive plumbing first (observability, not the fix; the reconcile clear stays repro-gated), then delegate RED repro.


Edit

    // [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
    #[serde(default)]
    pub resume_seq: u64,
}

    // [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
    #[serde(default)]
    pub resume_seq: u64,
    /// The live controller's remote identity (`Some(node)`) or `None` when the
    /// session has NO controller (free / local-only). Lets the W5 `driven_by`
    /// self-heal tell a genuinely-controlled live session apart from one whose
    /// controller is gone but whose `info.json` still latches `ONLINE+CONTROLLED`
    /// (Gap A — live session, lost controller).
    ///
    /// Additive + `#[serde(default)]` (KH-2.3 N-1 window, mirrors `resume_seq`):
    /// an **older broker** omits the field → `None`. The self-heal only consumes
    /// it on the LOCAL broker query (same binary version — no skew), and the
    /// Gap-A clear is gated on `driven_by` already being `Some`, so an N-1 read
    /// can never invent a controller, only fail to report one.
    // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]
    #[serde(default)]
    pub controller_by: Option<String>,
}


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)

    // [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
    #[serde(default)]
    pub resume_seq: u64,
}

    // [impl->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
    #[serde(default)]
    pub resume_seq: u64,
    /// The live controller's remote identity (`Some(node)`) or `None` when the
    /// session has NO controller (free / local-only). Lets the W5 `driven_by`
    /// self-heal tell a genuinely-controlled live session apart from one whose
    /// controller is gone but whose `info.json` still latches `ONLINE+CONTROLLED`
    /// (Gap A — live session, lost controller).
    ///
    /// Additive + `#[serde(default)]` (KH-2.3 N-1 window, mirrors `resume_seq`):
    /// an **older broker** omits the field → `None`. The self-heal only consumes
    /// it on the LOCAL broker query (same binary version — no skew), and the
    /// Gap-A clear is gated on `driven_by` already being `Some`, so an N-1 read
    /// can never invent a controller, only fail to report one.
    // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]
    #[serde(default)]
    pub controller_by: Option<String>,
}

+    /// The live controller's remote identity (`Some(node)`) or `None` when the

+    /// session has NO controller (free / local-only). Lets the W5 `driven_by`

+    /// self-heal tell a genuinely-controlled live session apart from one whose

+    /// controller is gone but whose `info.json` still latches `ONLINE+CONTROLLED`

+    /// (Gap A — live session, lost controller).

+    /// Additive + `#[serde(default)]` (KH-2.3 N-1 window, mirrors `resume_seq`):

+    /// an **older broker** omits the field → `None`. The self-heal only consumes

+    /// it on the LOCAL broker query (same binary version — no skew), and the

+    /// Gap-A clear is gated on `driven_by` already being `Some`, so an N-1 read

+    /// can never invent a controller, only fail to report one.

+    // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]

/// `sessions-reply` payload — every session the broker currently hosts.


Now populate it in the broker's `KIND_SESSIONS` builder — read `controller_by()` under the same log lock as `delivered_through`.