                            .map(|(id, endpoint, log)| SessionInfo {
                                session_id: id,
                                endpoint,
                                resume_seq: log.lock().unwrap().delivered_through(),
                            })

                            .map(|(id, endpoint, log)| {
                                // Read the cursor AND the controller identity under
                                // the one log lock (the W5 self-heal consumes
                                // controller_by to detect Gap A — live session,
                                // lost controller).
                                let log = log.lock().unwrap();
                                SessionInfo {
                                    session_id: id,
                                    endpoint,
                                    resume_seq: log.delivered_through(),
                                    controller_by: log.controller_by().map(str::to_string),
                                }
                            })


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

                            .map(|(id, endpoint, log)| SessionInfo {
                                session_id: id,
                                endpoint,
                                resume_seq: log.lock().unwrap().delivered_through(),
                            })

                            .map(|(id, endpoint, log)| {
                                // Read the cursor AND the controller identity under
                                // the one log lock (the W5 self-heal consumes
                                // controller_by to detect Gap A — live session,
                                // lost controller).
                                let log = log.lock().unwrap();
                                SessionInfo {
                                    session_id: id,
                                    endpoint,
                                    resume_seq: log.delivered_through(),
                                    controller_by: log.controller_by().map(str::to_string),
                                }
                            })

-                            .map(|(id, endpoint, log)| SessionInfo {

-                                session_id: id,

-                                endpoint,

-                                resume_seq: log.lock().unwrap().delivered_through(),

+                            .map(|(id, endpoint, log)| {

+                                // Read the cursor AND the controller identity under

+                                // the one log lock (the W5 self-heal consumes

+                                // controller_by to detect Gap A — live session,

+                                // lost controller).

+                                let log = log.lock().unwrap();

+                                SessionInfo {

+                                    session_id: id,

+                                    endpoint,

+                                    resume_seq: log.delivered_through(),

+                                    controller_by: log.controller_by().map(str::to_string),


Bash

grep -rn "SessionInfo {" crates/ --include=*.rs | grep -v target

Find all SessionInfo struct literals


crates/spt-daemon/src/broker.rs:1177:                                SessionInfo {
crates/spt-daemon/src/msg.rs:570:pub struct SessionInfo {


Only one literal. Verify the tree compiles before handing off.


cargo check -p spt-daemon --tests 2>&1 | tail -20

Compile-check daemon crate + tests


    Checking spt-proto v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-proto)
    Checking spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
    Checking spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
    Checking spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking spt-net v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-live v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.96s


Tree compiles. Now delegate the RED repro to ONE `spt-test-engineer` (background, ≤4 iter, no Monitor) — repro-first per the binding discipline. It must observe the real detection signal before I finalize the reconcile fix.


Agent

W5 driven_by self-heal RED repro

Prompt:
v0.13.0 W5 — `driven_by` self-heal. Build a REPRO-FIRST (RED) daemon integration test on a REAL broker (no mocks of PTY/transport). REQ = `REQ-HAZARD-DRIVEN-BY-SELFHEAL` (traceable-reqs.toml line ~1213). Plan = `V0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md`. Read both before starting.

## The bug
An spt-hosted endpoint latches `ONLINE+CONTROLLED` (`info.json` `driven_by = Some(node)`) and never clears when a controller's detach is LOST (no clean disconnect / no output to trigger the W1 drain-evict). Picker then shows a phantom "controlled by X" forever. The reconcile loop (`crates/spt-daemon/src/livehost.rs`, `reconcile_hosted_liveness` ~line 511) is the pull-primary substrate that must self-heal it (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2).

## Two gaps to repro
- **Gap B (no session):** broker hosts NO session for the endpoint, but `info.json` has `status=online`, `controllable=Some(true)`, `driven_by=Some(node)`. Today `reconcile_hosted_liveness` marks it offline but LEAVES `driven_by` Some. RED assertion: after the reconcile tick, `driven_by` is STILL `Some` (the latch).
- **Gap A (live session, lost controller):** broker hosts a LIVE session for the endpoint, but the broker's in-memory controller slot is empty (`controller_by()==None`) while `info.json` `driven_by=Some(node)` persists. Realistic trigger to explore: a FRESH `Broker` instance (controller slot starts None) over a perch whose `info.json` was pre-stamped `driven_by=Some(...)`, while that broker hosts a session for the endpoint — i.e. the brain-restart reality (live session survives, in-memory controller does not, stale disk marker remains). RED assertion: live session present AND `driven_by` STILL `Some`.

## What I need you to OBSERVE and REPORT (this drives my fix design)
I already added an additive field `SessionInfo.controller_by: Option<String>` (crates/spt-daemon/src/msg.rs ~line 587) populated by the broker's `KIND_SESSIONS` handler (broker.rs ~line 1171) from `OutputLog::controller_by()`. In your repro, query the real broker via `KIND_SESSIONS` (see the rig in `crates/spt-daemon/tests/inject_control_wedge.rs` for the real-broker + Brain client pattern; `Brain::sessions()` returns `SessionsReply`) and REPORT in your final message:
1. Gap A: the exact `SessionInfo.controller_by` value observed for the live session (is it `None` as expected?), and whether the fresh-broker-over-stale-perch setup reliably yields `controller_by==None` WITH the session listed live.
2. Gap B: confirm the endpoint is ABSENT from the `KIND_SESSIONS` reply.
3. Whether you found any path where `driven_by` ends up Some while the broker STILL reports `controller_by==Some` (that would mean controller_by==None is NOT a sufficient Gap-A signal — critical to know).

## Constraints (BINDING)
- REAL broker, real local IPC. Reuse rigs in `tests/inject_control_wedge.rs` / `tests/attach.rs` / `tests/daemon_e2e.rs`. New test file is fine (e.g. `tests/driven_by_selfheal.rs`) — if so, copy the RUNNER CONTRACT doc-header + `init_*_home` set_var pattern from inject_control_wedge.rs (process-global SPT_HOME; nextest-only / `--test-threads=1`; never bare `cargo test` on Windows).
- Read `info.json` `driven_by` via `spt_store::info::read_info(&perch).driven_by`; stamp it via `spt_store::info::set_driven_by(&perch, Some("node"))`. Resolve perch with the same helper the broker uses.
- This is RED/repro-first: the FIX (reconcile clearing `driven_by`) does NOT exist yet. Write the test so it PROVES the latch today (assert `driven_by` stays Some after a reconcile tick using the CURRENT `reconcile_hosted_liveness`). Mark with a clear `// TODO(W5-GREEN):` comment exactly where the post-fix assertion (`driven_by == None`) will replace the latch assertion, for BOTH gaps. Do NOT implement the reconcile fix — that's mine.
- Tag evidence: `// [int->REQ-HAZARD-DRIVEN-BY-SELFHEAL]` on the real test bodies (the REQ is still SEED `required_stages=[]`; I activate it when the fix lands — do NOT edit traceable-reqs.toml).
- Run the test ISOLATED: `cargo nextest run --test driven_by_selfheal` (or your file name). Confirm it builds + the RED assertions hold. ≤4 iterations. Do NOT use Monitor. Do NOT touch the operator binary on PATH.
- Do NOT run a workspace clippy or build — I run the final gates. Just get the repro compiling + RED-proven.

Return: the test file path, the observed-signal report (items 1-3 above), and the exact RED assertions you landed.


<owl_reachability_notice>
You have an active owl perch. Launch this Agent with run_in_background: true so you stay reachable for incoming messages while it works.
</owl_reachability_notice>


Async agent launched successfully.
agentId: a27bb87b41d0a4f93 (internal ID - do not mention to user. Use SendMessage with to: 'a27bb87b41d0a4f93' to continue this agent.)
The agent is working in the background. You will be notified automatically when it completes.
Do not duplicate this agent's work — avoid working with the same files or topics it is using. Work on non-overlapping tasks, or briefly tell the user what you launched and end your response.
output_file: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\a27bb87b41d0a4f93.output
Do NOT Read or tail this file via the shell tool — it is the full subagent JSONL transcript and reading it will overflow your context. If the user asks for progress, say the agent is still running; you'll get a completion notification.

W5 driven_by self-heal RED repro

Prompt:
v0.13.0 W5 — `driven_by` self-heal. Build a REPRO-FIRST (RED) daemon integration test on a REAL broker (no mocks of PTY/transport). REQ = `REQ-HAZARD-DRIVEN-BY-SELFHEAL` (traceable-reqs.toml line ~1213). Plan = `V0.13.0-W5-DRIVEN-BY-SELF-HEAL-PLAN.md`. Read both before starting.

## The bug
An spt-hosted endpoint latches `ONLINE+CONTROLLED` (`info.json` `driven_by = Some(node)`) and never clears when a controller's detach is LOST (no clean disconnect / no output to trigger the W1 drain-evict). Picker then shows a phantom "controlled by X" forever. The reconcile loop (`crates/spt-daemon/src/livehost.rs`, `reconcile_hosted_liveness` ~line 511) is the pull-primary substrate that must self-heal it (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2).

## Two gaps to repro
- **Gap B (no session):** broker hosts NO session for the endpoint, but `info.json` has `status=online`, `controllable=Some(true)`, `driven_by=Some(node)`. Today `reconcile_hosted_liveness` marks it offline but LEAVES `driven_by` Some. RED assertion: after the reconcile tick, `driven_by` is STILL `Some` (the latch).
- **Gap A (live session, lost controller):** broker hosts a LIVE session for the endpoint, but the broker's in-memory controller slot is empty (`controller_by()==None`) while `info.json` `driven_by=Some(node)` persists. Realistic trigger to explore: a FRESH `Broker` instance (controller slot starts None) over a perch whose `info.json` was pre-stamped `driven_by=Some(...)`, while that broker hosts a session for the endpoint — i.e. the brain-restart reality (live session survives, in-memory controller does not, stale disk marker remains). RED assertion: live session present AND `driven_by` STILL `Some`.

## What I need you to OBSERVE and REPORT (this drives my fix design)
I already added an additive field `SessionInfo.controller_by: Option<String>` (crates/spt-daemon/src/msg.rs ~line 587) populated by the broker's `KIND_SESSIONS` handler (broker.rs ~line 1171) from `OutputLog::controller_by()`. In your repro, query the real broker via `KIND_SESSIONS` (see the rig in `crates/spt-daemon/tests/inject_control_wedge.rs` for the real-broker + Brain client pattern; `Brain::sessions()` returns `SessionsReply`) and REPORT in your final message:
1. Gap A: the exact `SessionInfo.controller_by` value observed for the live session (is it `None` as expected?), and whether the fresh-broker-over-stale-perch setup reliably yields `controller_by==None` WITH the session listed live.
2. Gap B: confirm the endpoint is ABSENT from the `KIND_SESSIONS` reply.
3. Whether you found any path where `driven_by` ends up Some while the broker STILL reports `controller_by==Some` (that would mean controller_by==None is NOT a sufficient Gap-A signal — critical to know).

## Constraints (BINDING)
- REAL broker, real local IPC. Reuse rigs in `tests/inject_control_wedge.rs` / `tests/attach.rs` / `tests/daemon_e2e.rs`. New test file is fine (e.g. `tests/driven_by_selfheal.rs`) — if so, copy the RUNNER CONTRACT doc-header + `init_*_home` set_var pattern from inject_control_wedge.rs (process-global SPT_HOME; nextest-only / `--test-threads=1`; never bare `cargo test` on Windows).
- Read `info.json` `driven_by` via `spt_store::info::read_info(&perch).driven_by`; stamp it via `spt_store::info::set_driven_by(&perch, Some("node"))`. Resolve perch with the same helper the broker uses.
- This is RED/repro-first: the FIX (reconcile clearing `driven_by`) does NOT exist yet. Write the test so it PROVES the latch today (assert `driven_by` stays Some after a reconcile tick using the CURRENT `reconcile_hosted_liveness`). Mark with a clear `// TODO(W5-GREEN):` comment exactly where the post-fix assertion (`driven_by == None`) will replace the latch assertion, for BOTH gaps. Do NOT implement the reconcile fix — that's mine.
- Tag evidence: `// [int->REQ-HAZARD-DRIVEN-BY-SELFHEAL]` on the real test bodies (the REQ is still SEED `required_stages=[]`; I activate it when the fix lands — do NOT edit traceable-reqs.toml).
- Run the test ISOLATED: `cargo nextest run --test driven_by_selfheal` (or your file name). Confirm it builds + the RED assertions hold. ≤4 iterations. Do NOT use Monitor. Do NOT touch the operator binary on PATH.
- Do NOT run a workspace clippy or build — I run the final gates. Just get the repro compiling + RED-proven.

Return: the test file path, the observed-signal report (items 1-3 above), and the exact RED assertions you landed.


Engineer building repro in background. While it works, design the reconcile fix (non-overlapping — engineer owns test files, I own src). Read remaining reconcile callers.


<owl_messages>
<owl_message from="todlando-w173" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: todlando-w173
Agent type: spt-test-engineer
To message it: $OWL send todlando-w173 todlando</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.


815	    #[test]
816	    fn pull_liveness_marks_sessionless_spt_hosted_offline_only() {
817	        with_home(|_| {
818	            let seed = |id: &str, state: &str, controllable: Option<bool>| {
819	                let perch = perch::resolve_perch_path(id, ParentHint::Infer);
820	                std::fs::create_dir_all(&perch).unwrap();
821	                let mut rec =
822	                    spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid", state);
823	                rec.controllable = controllable;
824	                spt_store::info::write_info(&perch, &rec).unwrap();
825	                spt_store::info::set_status(&perch, STATUS_ONLINE).unwrap();
826	            };
827	            seed("alive", "live_agent", Some(true)); // has a session → stays
828	            seed("dead", "live_agent", Some(true)); // no session → offlined
829	            seed("relay", "live_agent", Some(false)); // harness-hosted relay → exempt
830	            seed("legacy", "live_agent", None); // legacy/unknown → exempt
831	            seed("ready", "ready_agent", Some(true)); // not a live agent → ignored
832
833	            let mut live = BTreeSet::new();
834	            live.insert("alive".to_string());
835
836	            let offlined = reconcile_hosted_liveness(&perch::owlery_dir(), &live);
837	            assert_eq!(
838	                offlined,
839	                vec!["dead".to_string()],
840	                "only the sessionless spt-hosted live agent is offlined"
841	            );
842
843	            let status = |id: &str| {
844	                spt_store::info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))
845	                    .and_then(|i| i.status)
846	            };
847	            assert_eq!(
848	                status("dead").as_deref(),
849	                Some(spt_store::liveness::STATUS_OFFLINE),
850	                "dead harness (no session) → offline"
851	            );
852	            assert_eq!(status("alive").as_deref(), Some(STATUS_ONLINE), "session present → stays");
853	            assert_eq!(status("relay").as_deref(), Some(STATUS_ONLINE), "relay exempt");
854	            assert_eq!(status("legacy").as_deref(), Some(STATUS_ONLINE), "legacy None exempt");
855	            assert_eq!(status("ready").as_deref(), Some(STATUS_ONLINE), "ready_agent ignored");
856	        });
857	    }
858
859	    // [unit->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE] the B5 boot-gate: at boot the
860	    // reconcile runs reconcile_hosted_liveness BEFORE reconcile_once, so a
861	    // sessionless spt-hosted (controllable) online perch is OFFLINED first and
862	    // reconcile_once then never REVIVES its Psyche (the cold-start-after-unclean-stop
863	    // phantom). A session-backed perch is left online and IS hosted — proving the
864	    // gate is the live broker session, not a blanket boot block. This is the exact
865	    // ordering spawn_live_host runs each tick (now from tick 1, no boot grace).
866	    #[test]
867	    fn boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host() {
868	        with_home(|_| {
869	            let drops = perch::spt_home().join("drops");
870	            std::fs::create_dir_all(&drops).unwrap();
871	            register_live_adapter("mock", &drops);
872
873	            // A controllable spt-hosted online perch WITH the adapter set — so
874	            // reconcile_once WOULD host it absent the gate.
875	            let seed_ctrl = |id: &str| {
876	                let perch = perch::resolve_perch_path(id, ParentHint::Infer);
877	                std::fs::create_dir_all(&perch).unwrap();
878	                let mut rec =
879	                    spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
880	                rec.adapter = Some("mock".to_string());
881	                rec.controllable = Some(true);
882	                spt_store::info::write_info(&perch, &rec).unwrap();
883	                spt_store::info::set_status(&perch, STATUS_ONLINE).unwrap();
884	            };
885	            seed_ctrl("phantom"); // no broker session → dead-but-latched phantom
886	            seed_ctrl("livee"); // broker session present → genuinely live
887
888	            // Only `livee` has a live broker session.
889	            let mut live = BTreeSet::new();
890	            live.insert("livee".to_string());
891
892	            let set = Arc::new(LiveSet::new());
893	            let cfg = fast_cfg();
894	            // Boot-tick ordering: liveness reconcile (the gate) BEFORE the host scan.
895	            reconcile_hosted_liveness(&perch::owlery_dir(), &live);
896	            reconcile_once(
897	                &perch::owlery_dir(),
898	                &registered_now(),
899	                &perch::adapters_dir(),
900	                &set,
901	                &cfg,
902	                StartReason::Cold,
903	            );
904

    #[test]
    fn pull_liveness_marks_sessionless_spt_hosted_offline_only() {
        with_home(|_| {
            let seed = |id: &str, state: &str, controllable: Option<bool>| {
                let perch = perch::resolve_perch_path(id, ParentHint::Infer);
                std::fs::create_dir_all(&perch).unwrap();
                let mut rec =
                    spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid", state);
                rec.controllable = controllable;
                spt_store::info::write_info(&perch, &rec).unwrap();
                spt_store::info::set_status(&perch, STATUS_ONLINE).unwrap();
            };
            seed("alive", "live_agent", Some(true)); // has a session → stays
            seed("dead", "live_agent", Some(true)); // no session → offlined
            seed("relay", "live_agent", Some(false)); // harness-hosted relay → exempt
            seed("legacy", "live_agent", None); // legacy/unknown → exempt
            seed("ready", "ready_agent", Some(true)); // not a live agent → ignored

            let mut live = BTreeSet::new();
            live.insert("alive".to_string());

            let offlined = reconcile_hosted_liveness(&perch::owlery_dir(), &live);
            assert_eq!(
                offlined,
                vec!["dead".to_string()],
                "only the sessionless spt-hosted live agent is offlined"
            );

            let status = |id: &str| {
                spt_store::info::read_info(&perch::resolve_perch_path(id, ParentHint::Infer))
                    .and_then(|i| i.status)
            };
            assert_eq!(
                status("dead").as_deref(),
                Some(spt_store::liveness::STATUS_OFFLINE),
                "dead harness (no session) → offline"
            );
            assert_eq!(status("alive").as_deref(), Some(STATUS_ONLINE), "session present → stays");
            assert_eq!(status("relay").as_deref(), Some(STATUS_ONLINE), "relay exempt");
            assert_eq!(status("legacy").as_deref(), Some(STATUS_ONLINE), "legacy None exempt");
            assert_eq!(status("ready").as_deref(), Some(STATUS_ONLINE), "ready_agent ignored");
        });
    }

    // [unit->REQ-HAZARD-LIVEHOST-BOOT-LIVENESS-GATE] the B5 boot-gate: at boot the
    // reconcile runs reconcile_hosted_liveness BEFORE reconcile_once, so a
    // sessionless spt-hosted (controllable) online perch is OFFLINED first and
    // reconcile_once then never REVIVES its Psyche (the cold-start-after-unclean-stop
    // phantom). A session-backed perch is left online and IS hosted — proving the
    // gate is the live broker session, not a blanket boot block. This is the exact
    // ordering spawn_live_host runs each tick (now from tick 1, no boot grace).
    #[test]
    fn boot_gate_offlines_sessionless_controllable_then_reconcile_skips_host() {
        with_home(|_| {
            let drops = perch::spt_home().join("drops");
            std::fs::create_dir_all(&drops).unwrap();
            register_live_adapter("mock", &drops);

            // A controllable spt-hosted online perch WITH the adapter set — so
            // reconcile_once WOULD host it absent the gate.
            let seed_ctrl = |id: &str| {
                let perch = perch::resolve_perch_path(id, ParentHint::Infer);
                std::fs::create_dir_all(&perch).unwrap();
                let mut rec =
                    spt_store::info::InfoJson::new(id, "t", std::process::id(), "sid-1", "live_agent");
                rec.adapter = Some("mock".to_string());
                rec.controllable = Some(true);
                spt_store::info::write_info(&perch, &rec).unwrap();
                spt_store::info::set_status(&perch, STATUS_ONLINE).unwrap();
            };
            seed_ctrl("phantom"); // no broker session → dead-but-latched phantom
            seed_ctrl("livee"); // broker session present → genuinely live

            // Only `livee` has a live broker session.
            let mut live = BTreeSet::new();
            live.insert("livee".to_string());

            let set = Arc::new(LiveSet::new());
            let cfg = fast_cfg();
            // Boot-tick ordering: liveness reconcile (the gate) BEFORE the host scan.
            reconcile_hosted_liveness(&perch::owlery_dir(), &live);
            reconcile_once(
                &perch::owlery_dir(),
                &registered_now(),
                &perch::adapters_dir(),
                &set,
                &cfg,
                StartReason::Cold,
            );


Engineer perch up (todlando-w173). Only ONE prod caller of the reconcile (line 694) — tests call it directly, so signature ripple is minimal. Implement Gap B (unconditionally safe) + map plumbing now; hold Gap A predicate until the engineer's signal report. Check imports first.


Bash

grep -n "use std::collections\|BTreeSet\|BTreeMap\|mark_offline\|fn mark_offline" crates/spt-daemon/src/livehost.rs | head; echo ---; grep -n "fn mark_offline" crates/spt-daemon/src/*.rs

Check collection imports + mark_offline def


26:use std::collections::{BTreeSet, HashMap};
511:pub fn reconcile_hosted_liveness(owlery: &Path, live_sessions: &BTreeSet<String>) -> Vec<String> {
530:            BrainLifecycle::mark_offline(&perch);
541:fn query_live_session_endpoints() -> Option<BTreeSet<String>> {
833:            let mut live = BTreeSet::new();
889:            let mut live = BTreeSet::new();
1112:            let session_set = |brain: &mut Brain| -> BTreeSet<String> {
---
crates/spt-daemon/src/lifecycle.rs:227:    pub fn mark_offline(perch_path: &std::path::Path) {


220
221	    /// Mark a daemon-hosted perch `offline` (teardown counterpart of the
222	    /// `online` stamp in [`spawn_psyche`](Self::spawn_psyche)). From this point
223	    /// the single liveness resolver reports the perch dead regardless of any pid
224	    /// still recorded in its `info.json` (2.5). Best-effort: a missing record
225	    /// (perch already gone) is not an error.
226	    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
227	    pub fn mark_offline(perch_path: &std::path::Path) {
228	        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);
229	    }
230
231	    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the
232	    /// signal the consent gate resolves "most-recently-active live session"
233	    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted
234	    /// perch that has not been bound yet (no `info.json`) is silently skipped, so
235	    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.
236	    // [impl->REQ-UPD-4]
237	    pub fn touch_active(&self) {
238	        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
239	        let _ = spt_store::info::set_last_active(&perch_path, now_ms());
240	    }
241
242	    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops
243	    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one
244	    /// echo-commune (T3).
245	    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {
246	        let now = now_ms();
247	        // Recency follows the resting model (D9-2 — the D8 gap closed): only
248	        // an ACTIVE (or pre-D9-2 recordless) instance refreshes
249	        // `last_active_ms` — a resting seat ticking its pulse must never win

    /// Mark a daemon-hosted perch `offline` (teardown counterpart of the
    /// `online` stamp in [`spawn_psyche`](Self::spawn_psyche)). From this point
    /// the single liveness resolver reports the perch dead regardless of any pid
    /// still recorded in its `info.json` (2.5). Best-effort: a missing record
    /// (perch already gone) is not an error.
    // [impl->REQ-HAZARD-DAEMON-HOSTED-LIVENESS]
    pub fn mark_offline(perch_path: &std::path::Path) {
        let _ = spt_store::info::set_status(perch_path, spt_store::liveness::STATUS_OFFLINE);
    }

    /// Stamp this endpoint's `last_active_ms` recency marker (REQ-UPD-4) — the
    /// signal the consent gate resolves "most-recently-active live session"
    /// against ([`crate::consent::most_recently_active`]). Best-effort: a hosted
    /// perch that has not been bound yet (no `info.json`) is silently skipped, so
    /// a pre-bind tick never errors. Stamps the Self perch for this brain's id.
    // [impl->REQ-UPD-4]
    pub fn touch_active(&self) {
        let perch_path = perch::resolve_perch_path(&self.id, ParentHint::Infer);
        let _ = spt_store::info::set_last_active(&perch_path, now_ms());
    }

    /// One pulse tick (T5): refresh the activity stamp (REQ-UPD-4), ingest drops
    /// from the watched dirs (T4), then if the echo-gate is armed fire exactly one
    /// echo-commune (T3).
    pub fn pulse_tick(&self, session_id: Option<&str>) -> Result<TickReport, String> {
        let now = now_ms();
        // Recency follows the resting model (D9-2 — the D8 gap closed): only
        // an ACTIVE (or pre-D9-2 recordless) instance refreshes
        // `last_active_ms` — a resting seat ticking its pulse must never win


Implement Gap B now (race-free — no session means no live controller, brain is the only possible writer). Hold Gap A: it touches the broker's single-writer `driven_by` invariant, so it needs the engineer's signal report + a broker-as-clearer decision. Edit the offline path.


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	}

        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
}


Edit

        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);
        }

        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);
            // W5 Gap B (driven_by self-heal): an offlined endpoint can ALSO carry
            // a stale `driven_by = Some` — its broker session (the single writer)
            // is gone, so the `clear_controller` re-stamp can never fire to unlatch
            // it. Clear it here alongside the offline. RACE-FREE: with no live
            // broker session there is no controller to re-stamp it concurrently, so
            // the broker single-writer invariant is not contended (unlike Gap A —
            // the live-session case, which routes its clear through the broker).
            // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]
            if info.driven_by.is_some() {
                let _ = spt_store::info::set_driven_by(&perch, None);
                eprintln!("DRIVEN_BY_SELFHEAL_OFFLINE:{id}: cleared stale driven_by (no session)");
            }
            eprintln!("LIVENESS_RECONCILE_OFFLINE:{id}: no live broker session (dead harness)");
            offlined.push(id);
        }


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

        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);
        }

        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);
            // W5 Gap B (driven_by self-heal): an offlined endpoint can ALSO carry
            // a stale `driven_by = Some` — its broker session (the single writer)
            // is gone, so the `clear_controller` re-stamp can never fire to unlatch
            // it. Clear it here alongside the offline. RACE-FREE: with no live
            // broker session there is no controller to re-stamp it concurrently, so
            // the broker single-writer invariant is not contended (unlike Gap A —
            // the live-session case, which routes its clear through the broker).
            // [impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]
            if info.driven_by.is_some() {
                let _ = spt_store::info::set_driven_by(&perch, None);
                eprintln!("DRIVEN_BY_SELFHEAL_OFFLINE:{id}: cleared stale driven_by (no session)");
            }
            eprintln!("LIVENESS_RECONCILE_OFFLINE:{id}: no live broker session (dead harness)");
            offlined.push(id);
        }

+            // W5 Gap B (driven_by self-heal): an offlined endpoint can ALSO carry

+            // a stale `driven_by = Some` — its broker session (the single writer)

+            // is gone, so the `clear_controller` re-stamp can never fire to unlatch

+            // it. Clear it here alongside the offline. RACE-FREE: with no live

+            // broker session there is no controller to re-stamp it concurrently, so

+            // the broker single-writer invariant is not contended (unlike Gap A —

+            // the live-session case, which routes its clear through the broker).

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

+            if info.driven_by.is_some() {

+                let _ = spt_store::info::set_driven_by(&perch, None);

+                eprintln!("DRIVEN_BY_SELFHEAL_OFFLINE:{id}: cleared stale driven_by (no session)");


Gap B landed. Now the critical Gap A question: in **production** (pump death / lost detach), does the broker's controller slot stay occupied (`controller_by`==Some) on an idle session? If so, `controller_by==None` is NOT the real signal and the fix is broker-side eviction, not a reconcile check. This is exactly what the repro must settle. Read the controller writer/eviction path while the engineer works.


120
121	/// The controller live-frame channel depth (frames). Deeper than a viewer's
122	/// queue because the controller is the AUTHORITATIVE view — it is never silently
123	/// evicted on overflow (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST); instead the
124	/// drain blocks on a bounded `send_timeout` and the controller is detached only
125	/// after [`CONTROLLER_WRITE_DEADLINE`] of a fully-stalled socket. Generous so a
126	/// momentary slow consumer never trips the deadline.
127	const CONTROLLER_CHANNEL_DEPTH: usize = 4096;
128
129	/// How long the drain tolerates a CONTROLLER whose socket has fully stalled (its
130	/// channel stays full) before declaring it gone, detaching it, and resuming
131	/// (W1). This BOUNDS the wedge: a backed-up controller can stall live output for
132	/// at most this long — never forever (the v0.12.x root) — and never blocks the
133	/// log lock (the send happens off-lock; see [`OutputLog::append`]). A live but
134	/// merely slow operator drains a slot before this elapses and is untouched.
135	const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);
136
137	/// Poll interval for the controller `try_send` deadline loop (std `SyncSender`
138	/// has no stable `send_timeout`). Only used while the channel is FULL — the
139	/// common (has-room) path never sleeps.
140	const CONTROLLER_SEND_POLL: Duration = Duration::from_millis(5);
141
142	/// MANDATORY bound on one injected keystroke sequence (W2 Layer C, ADR-0022 /
143	/// doyle non-negotiable = W1's "never park forever"): the inject worker holds the
144	/// floor — buffering operator input — only while a binary's response to one
145	/// `{type:event}` is in flight. If no `{commit}` arrives within this window the
146	/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no
147	/// respawn → the session degrades to raw inject), and never re-opens. A wedged or
148	/// buggy translation binary can therefore stall operator input for at most this
149	/// long. Mirrors [`CONTROLLER_WRITE_DEADLINE`]'s order (5 s).
150	const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);
151
152	/// The effective commit deadline. Production is the const above; the int gate
153	/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`
154	/// so the structural outcome (floor releases → buffered input flushes → binary
155	/// faults) is asserted in well under a second rather than a 5 s wall-clock wait —
156	/// the deadline's PRESENCE is the invariant, not its exact length.
157	fn inject_commit_deadline() -> Duration {
158	    match std::env::var("SPT_INJECT_COMMIT_DEADLINE_MS") {
159	        Ok(ms) => ms
160	            .parse::<u64>()
161	            .map(Duration::from_millis)
162	            .unwrap_or(INJECT_COMMIT_DEADLINE),
163	        Err(_) => INJECT_COMMIT_DEADLINE,
164	    }
165	}
166
167	/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
168	/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING
169	/// socket write so a backed-up controller can never stall the session drain
170	/// thread (the v0.12.x wedge: the controller write was inline on the drain,
171	/// holding `Mutex<OutputLog>`). Unlike a viewer it is AUTHORITATIVE: its writer
172	/// advances `delivered_through` on each successful write, and it is never evicted
173	/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].
174	struct ControllerSink {
175	    /// The bounded live-frame queue to the controller's writer thread.
176	    tx: SyncSender<CtrlMsg>,
177	    /// The controller's IPC send half — kept for identity matching on detach and
178	    /// for the `exit`-frame fan-out ([`OutputLog::all_sinks`]).
179	    send: SharedSend,
180	    /// The controller's identity: `Some(node)` = a remote operator, `None` =
181	    /// local (the brain's spawn-reader / resume). Ownership keys on this.
182	    by: Option<String>,
183	    /// The writer thread handle (held for ownership; exits when `tx` drops — the
184	    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or
185	    /// when a socket write fails).
186	    _writer: JoinHandle<()>,
187	}
188
189	/// A bounded, sequenced output log for one session — the gapless-resume buffer
190	/// AND the W2.5 controller/viewer fan-out hub (REQ-RCVIEW-1).
191	///
192	/// The session's single drain thread is the only producer ([`OutputLog::append`]).
193	/// Output fans out to ONE interactive `controller` (the authoritative,
194	/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each
195	/// an isolated bounded queue + writer thread, evicted on overflow). Every
196	/// controller send and viewer `try_send` happens while the enclosing
197	/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.
198	struct OutputLog {
199	    session_id: u64,
200	    next_seq: u64,
201	    ring: VecDeque<(u64, Vec<u8>)>,
202	    cap_chunks: usize,
203	    /// The single interactive controller's isolated sink (W1: a dedicated writer
204	    /// thread + bounded channel + authoritative cursor). `None` ⇒ undriven. The
205	    /// controller's identity (`Some(node)` remote / `None` local) lives in
206	    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe
207	    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a
208	    /// DIFFERENT remote's `Take` displaces.
209	    controller: Option<ControllerSink>,
210	    /// Monotonic controller generation. Bumped on every [`become_controller`];
211	    /// the drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it
212	    /// observed and only detaches if it still matches — so a slow-controller
213	    /// timeout can never unseat a NEWER controller that attached meanwhile.
214	    controller_epoch: u64,
215	    /// The read-only viewers, keyed by a broker-minted viewer id.
216	    viewers: HashMap<u64, ViewerSink>,
217	    /// Monotonic viewer-id source.
218	    next_viewer_id: u64,
219	    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).
220	    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this
221	    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.
222	    endpoint: String,
223	    /// Last-known PTY size — seeded at spawn, updated on a controller resize.
224	    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox
225	    /// feed); the broker holds it since the PTY exposes no size getter.
226	    size: (u16, u16),
227	    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a
228	    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**
229	    /// to the controller, +1). It is **log** state, not subscriber state, so it
230	    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
231	    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
232	    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**
233	    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).
234	    ///

/// The controller live-frame channel depth (frames). Deeper than a viewer's
/// queue because the controller is the AUTHORITATIVE view — it is never silently
/// evicted on overflow (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST); instead the
/// drain blocks on a bounded `send_timeout` and the controller is detached only
/// after [`CONTROLLER_WRITE_DEADLINE`] of a fully-stalled socket. Generous so a
/// momentary slow consumer never trips the deadline.
const CONTROLLER_CHANNEL_DEPTH: usize = 4096;

/// How long the drain tolerates a CONTROLLER whose socket has fully stalled (its
/// channel stays full) before declaring it gone, detaching it, and resuming
/// (W1). This BOUNDS the wedge: a backed-up controller can stall live output for
/// at most this long — never forever (the v0.12.x root) — and never blocks the
/// log lock (the send happens off-lock; see [`OutputLog::append`]). A live but
/// merely slow operator drains a slot before this elapses and is untouched.
const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);

/// Poll interval for the controller `try_send` deadline loop (std `SyncSender`
/// has no stable `send_timeout`). Only used while the channel is FULL — the
/// common (has-room) path never sleeps.
const CONTROLLER_SEND_POLL: Duration = Duration::from_millis(5);

/// MANDATORY bound on one injected keystroke sequence (W2 Layer C, ADR-0022 /
/// doyle non-negotiable = W1's "never park forever"): the inject worker holds the
/// floor — buffering operator input — only while a binary's response to one
/// `{type:event}` is in flight. If no `{commit}` arrives within this window the
/// worker FLUSHES the buffer, RELEASES the floor, FAULTS the binary (terminate, no
/// respawn → the session degrades to raw inject), and never re-opens. A wedged or
/// buggy translation binary can therefore stall operator input for at most this
/// long. Mirrors [`CONTROLLER_WRITE_DEADLINE`]'s order (5 s).
const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);

/// The effective commit deadline. Production is the const above; the int gate
/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`
/// so the structural outcome (floor releases → buffered input flushes → binary
/// faults) is asserted in well under a second rather than a 5 s wall-clock wait —
/// the deadline's PRESENCE is the invariant, not its exact length.
fn inject_commit_deadline() -> Duration {
    match std::env::var("SPT_INJECT_COMMIT_DEADLINE_MS") {
        Ok(ms) => ms
            .parse::<u64>()
            .map(Duration::from_millis)
            .unwrap_or(INJECT_COMMIT_DEADLINE),
        Err(_) => INJECT_COMMIT_DEADLINE,
    }
}

/// The interactive CONTROLLER's isolated sink (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
/// Symmetric with [`ViewerSink`] — a dedicated writer thread does the BLOCKING
/// socket write so a backed-up controller can never stall the session drain
/// thread (the v0.12.x wedge: the controller write was inline on the drain,
/// holding `Mutex<OutputLog>`). Unlike a viewer it is AUTHORITATIVE: its writer
/// advances `delivered_through` on each successful write, and it is never evicted
/// on a single full `try_send` — only after [`CONTROLLER_WRITE_DEADLINE`].
struct ControllerSink {
    /// The bounded live-frame queue to the controller's writer thread.
    tx: SyncSender<CtrlMsg>,
    /// The controller's IPC send half — kept for identity matching on detach and
    /// for the `exit`-frame fan-out ([`OutputLog::all_sinks`]).
    send: SharedSend,
    /// The controller's identity: `Some(node)` = a remote operator, `None` =
    /// local (the brain's spawn-reader / resume). Ownership keys on this.
    by: Option<String>,
    /// The writer thread handle (held for ownership; exits when `tx` drops — the
    /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or
    /// when a socket write fails).
    _writer: JoinHandle<()>,
}

/// A bounded, sequenced output log for one session — the gapless-resume buffer
/// AND the W2.5 controller/viewer fan-out hub (REQ-RCVIEW-1).
///
/// The session's single drain thread is the only producer ([`OutputLog::append`]).
/// Output fans out to ONE interactive `controller` (the authoritative,
/// cursor-advancing, blocking path) plus ANY NUMBER of read-only `viewers` (each
/// an isolated bounded queue + writer thread, evicted on overflow). Every
/// controller send and viewer `try_send` happens while the enclosing
/// `Mutex<OutputLog>` is held, so frames reach each sink in strict `seq` order.
struct OutputLog {
    session_id: u64,
    next_seq: u64,
    ring: VecDeque<(u64, Vec<u8>)>,
    cap_chunks: usize,
    /// The single interactive controller's isolated sink (W1: a dedicated writer
    /// thread + bounded channel + authoritative cursor). `None` ⇒ undriven. The
    /// controller's identity (`Some(node)` remote / `None` local) lives in
    /// [`ControllerSink::by`] and keys ownership — a same-`by` re-subscribe
    /// silently re-takes (no `Displaced`, the gate-#7 self-kick guard); only a
    /// DIFFERENT remote's `Take` displaces.
    controller: Option<ControllerSink>,
    /// Monotonic controller generation. Bumped on every [`become_controller`];
    /// the drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it
    /// observed and only detaches if it still matches — so a slow-controller
    /// timeout can never unseat a NEWER controller that attached meanwhile.
    controller_epoch: u64,
    /// The read-only viewers, keyed by a broker-minted viewer id.
    viewers: HashMap<u64, ViewerSink>,
    /// Monotonic viewer-id source.
    next_viewer_id: u64,
    /// The owning endpoint (the spawn's label; empty = unlabeled / raw test).
    /// The broker is the SINGLE writer of `driven_by` + `viewer_count` on this
    /// perch (REQ-RCVIEW-1, resolves the clear-race); empty ⇒ nowhere to stamp.
    endpoint: String,
    /// Last-known PTY size — seeded at spawn, updated on a controller resize.
    /// Pushed to a viewer on attach + to all viewers on resize (the letterbox
    /// feed); the broker holds it since the PTY exposes no size getter.
    size: (u16, u16),
    /// Cursor-of-record (restoration D4-1, ADR-0018 Q6): the next seq a
    /// re-attaching CONTROLLER still needs (= highest seq **successfully written**
    /// to the controller, +1). It is **log** state, not subscriber state, so it
    /// **survives `detach_if`** — a dead brain leaves its resume point behind, and
    /// a cold-starting brain reads it (via `KIND_SESSIONS` → `resume_seq`) instead
    /// of replaying the whole ring from 0. **Viewer writes NEVER move it**
    /// (REQ-RCVIEW-1: the brain-resume cursor tracks the controller only).
    ///


Confirmed by code-read: on an **idle** session the controller is only evicted via the drain (needs output) or on connection EOF. A wedged-pump-but-open-connection keeps the slot → `controller_by` stays **Some**. So the plan's hypothesized Gap-A signal (`controller_by==None`) likely matches only the *restart* mechanism, not the *production wedged-pump* mechanism. Both are "Gap A" but differ. Steer the engineer to probe both empirically before I finalize.


Agent

Steer W5 repro — two Gap-A mechanisms

Prompt:
SendMessage to: a27bb87b41d0a4f93

Steering refinement for the W5 driven_by repro (do NOT restart; fold into your current work). My broker code-read found TWO distinct mechanisms that both leave `driven_by=Some` latched, and they differ in what `controller_by` reports. I need your repro to characterize BOTH so I pick the right fix:

- **Mechanism A1 (restart / lost in-memory slot):** a FRESH `Broker` hosts a session for an endpoint whose `info.json` was pre-stamped `driven_by=Some`. In-memory controller slot starts empty → I expect `SessionInfo.controller_by == None` while the session is listed live. (This is what my original prompt described.)
- **Mechanism A2 (production wedged-pump / lost detach — the REQ's real target):** a controller ATTACHES for real (become_controller → driven_by=Some, controller slot occupied), then the controller's REMOTE is gone but its broker connection stays OPEN (no clean EOF/detach) and the session is IDLE (no output → the W1 drain-evict / CONTROLLER_WRITE_DEADLINE never fires). I expect the controller slot stays occupied → `SessionInfo.controller_by == Some` AND `driven_by == Some` — i.e. the broker still THINKS it's controlled.

Please REPORT, empirically from the real broker:
1. A1: confirm `controller_by == None` + session live + driven_by stays Some after a reconcile tick.
2. A2: set up an attached controller on a real broker (reuse the attach rig in inject_control_wedge.rs — request_attach/serve_attach/become_controller), then drop/abandon the controller WITHOUT a clean detach and WITHOUT driving output, run a `KIND_SESSIONS` query, and report what `controller_by` reads (Some or None?) and whether `driven_by` stays Some. This tells me whether `controller_by==None` is a sufficient Gap-A signal or whether the production wedged case keeps it Some (meaning the fix must be broker-side eviction, not a reconcile controller_by check).

If A2 is hard to stage faithfully on a real broker in ≤ your remaining iterations, say so explicitly and just give me your best evidence on A1 + a note on what A2 would require — do NOT burn iterations forcing a flaky A2. Keep the RED repro you're already building for A1/Gap B; A2 can be a documented characterization (even a #[ignore]'d skeleton + findings note) rather than a hard gate. Report findings in your final message.


<task-notification>
<task-id>a27bb87b41d0a4f93</task-id>
<tool-use-id>toolu_01FrfTpykZKkBZoaKBkxgVVX</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\a27bb87b41d0a4f93.output</output-file>
<status>completed</status>
<summary>Agent "W5 driven_by self-heal RED repro" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Done. Here is the report.

## Deliverable

**Test file:** `C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\driven_by_selfheal.rs`

Two tests, real broker + real PTY + real local IPC, both PASS deterministically via `cargo nextest run --test driven_by_selfheal` (Gap A ~0.49s, Gap B ~0.03s). Runner-contract doc-header + `init_selfheal_home` process-global SPT_HOME pattern copied from `inject_control_wedge.rs`. Both bodies tagged `// [int->REQ-HAZARD-DRIVEN-BY-SELFHEAL]`. I did NOT edit `traceable-reqs.toml`, did NOT implement any reconcile fix, and did NOT touch the operator binary.

## CRITICAL deviation you need to know

**The Gap-B fix is ALREADY LANDED on this branch.** The prompt/plan said "the FIX does not exist yet — prove the latch stays Some." That is false for `v0.13.0-delivery-control`: `reconcile_hosted_liveness` (livehost.rs ~538-542) already carries an `[impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]`-tagged block that does `set_driven_by(perch, None)` alongside `mark_offline` and logs `DRIVEN_BY_SELFHEAL_OFFLINE`. My first run proved this — Gap B came back `driven_by_after=None`, GREEN. I refused to encode the buggy latched expectation; the Gap-B test now regression-locks the GREEN behavior with a prominent comment. **Gap A's reconcile-side clear is genuinely NOT implemented** — that test is RED (latch proven).

## Observed-signal report (items 1-3)

**Item 1 — Gap A `controller_by` for the live session:** `observed_controller_by = None`, `live_session_present = true`. The fresh-broker-over-stale-perch setup is NOT constructible at the in-process broker API (a fresh `Broker::bind` has zero sessions — sessions are in-memory and own the PtySession, nothing to re-adopt). I reproduced the genuine reconcile Gap-A instead: spawn a live session, then re-stamp the stale `driven_by=Some` marker on disk (the brain-restart leftover), yielding a live session with `controller_by==None` against `driven_by=Some`. Reliable and deterministic.

**Item 2 — Gap B endpoint absence:** confirmed `endpoint_in_sessions=false`, `sessions={}`. The broker hosts nothing for the endpoint; it is absent from the `KIND_SESSIONS` reply.

**Item 3 (the one you flagged as critical) — is there any path where `driven_by=Some` while broker reports `controller_by=Some`?** **No such path observed.** But the more important finding: **`controller_by==None` is AMBIGUOUS.** `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None` (broker.rs ~1273), so `OutputLog::controller_by()` returns `None` even for a LIVE, locally-controlled session. So `SessionInfo.controller_by==None` does NOT distinguish a lost controller from a live local one. **Your fix must not false-clear on `controller_by==None` alone.**

## Two extra findings that should shape the fix

- **The broker already self-heals `driven_by` on an OBSERVED controller disconnect.** Dropping the controller socket triggers per-conn `detach_if`→`clear_controller`→`stamp_driven_by` (broker.rs ~528, ~563), re-stamping `driven_by=None`. So a plain disconnect is NOT a reconcile Gap-A — the only genuine reconcile Gap-A is the brain-restart leftover where that re-stamp never fired.
- Because `controller_by==None` can't tell an occupied-local slot from an empty one, the plan's own preference holds: **route the live-session Gap-A clear through the broker** (single writer — it can see the occupied-but-local slot), not from the brain reconcile reading `controller_by==None`. A future GREEN Gap-A test must include a still-genuinely-controlled live session that must NOT clear (the false-heal guard).

## RED/GREEN assertions landed

- **Gap B (GREEN, fix already in):** `assert_eq!(driven_by_after, None, ...)` plus `offlined.contains(id)` and `status==offline`. TODO note documents the deviation, not a future flip.
- **Gap A (RED, latch proven):** `assert_eq!(driven_by_after, Some(node), ...)`, with `!offlined.contains(id)` and `status==online` (the reconcile skips live-session perches). A `// TODO(W5-GREEN):` marks the exact spot to flip to `assert_eq!(driven_by_after, None, ...)` and carries the false-heal-guard warning for the fix designer.

I ran only the isolated test (no workspace clippy/build, per your instruction). Suggested verification gate command before you declare W5 done: `cargo nextest run --test driven_by_selfheal`.</result>
<usage><subagent_tokens>144460</subagent_tokens><tool_uses>41</tool_uses><duration_ms>490584</duration_ms></usage>
</task-notification>


Note: `broker.rs`, `livehost.rs`, `msg.rs` show as modified and `driven_by_selfheal.rs` is untracked — these are the parallel agent's in-flight W5 work (the `controller_by` field, the reconcile gates), not mine. My changes are confined to `inject_control_wedge.rs` (the A1/A2 characterization tests), `traceable-reqs.toml` (REQ activation), the one-line clippy doc fix in `driven_by_selfheal.rs`, and agent memory. I left the parallel work untouched per the standing "keep the shared tree compilable" rule.

---

## Report to todlando — W5 A1/A2 characterization (REQ-HAZARD-DRIVEN-BY-SELFHEAL)

I folded the A2 refinement into the repro work as steered. Findings are **empirical, from a real broker** (real loopback attach via `request_attach`/`serve_attach`/`become_controller`, no mocks).

### A1 — confirmed
`crates/spt-daemon/tests/inject_control_wedge.rs::w5_a1_locally_driven_session_reports_controller_by_none`

A live session with only the local-spawner slot reads:
- `controller_by == None`, session listed live
- `driven_by == None` (a local-only controller never latches `ONLINE+CONTROLLED`)

So `controller_by == None` IS the right Gap-A signal for the restart/lost-slot case — and because the Gap-A clear is gated on `driven_by == Some`, the local-only `None` read can never false-clear (a local session has no latch to clear).

### A2 — staged faithfully, conclusive (this is the answer you need)
`crates/spt-daemon/tests/inject_control_wedge.rs::w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some`

A real remote controller attaches over loopback (`become_controller(Some(origin))`), then its remote is abandoned **without a clean EOF/detach** while the session is **idle** (no output → the W1 drain-evict never fires). Measured:

```
cby_attached       = Some(Some(origin))     driven_after_attach  = Some(origin)
cby_after_abandon  = Some(Some(origin))     driven_after_abandon = Some(origin)
VERDICT: controller_by==None sufficient for Gap A? false
```

**`controller_by` STAYS `Some(origin)` and `driven_by` STAYS latched.** The broker still thinks it's controlled — phantom CONTROLLED persists forever.

**Therefore: `controller_by == None` is NOT a sufficient Gap-A signal for the production wedged-pump case. The lost-detach clear must be BROKER-SIDE eviction** (a controller-liveness probe / reconcile-driven `clear_controller`), not a reconcile `controller_by == None` read. A pure-reconcile `controller_by==None` check would catch A1 and Gap-B (no session) but silently miss the real A2 target.

### Why A2 reads Some (the mechanism, reconciled with prior memory)
The controller slot is owned by the **target's `serve_attach` `brain` connection** (the conn that calls `attach_as(Some(origin))`), which keeps blocking on `brain.read_event()`. Abandoning the **operator's** remote stream does not drop that target conn, so the broker never observes a disconnect → no `detach_if`/`clear_controller`. This is exactly the production lost-detach shape, and it's why an in-process "plain socket drop" (which *does* self-heal) cannot reproduce it.

### One rig gotcha worth knowing
The broker's `stamp_driven_by` is best-effort: `set_driven_by` **errors when the perch info.json is absent**, so an A2 test must pre-seed `<owlery>/<endpoint>/info.json` or `driven_by` silently stays `None` and the verdict is unobservable. My first A2 run hit exactly this (reported INCONCLUSIVE); after seeding the perch it became conclusive.

### Gate status (all green)
- `cargo nextest run --test inject_control_wedge --test driven_by_selfheal` → **11/11 pass** (my 2 + the parallel `driven_by_selfheal.rs` Gap-A/Gap-B reconcile gates + the 7 pre-existing W1/W1b/W2 gates; no regressions)
- `cargo clippy -p spt-daemon --tests -- -D warnings` → clean (fixed one pre-existing doc-lazy-continuation in the parallel `driven_by_selfheal.rs` to keep the shared tree compilable; one blank `//!` line, no semantic change)
- `traceable-reqs check` → EXIT=0; `REQ-HAZARD-DRIVEN-BY-SELFHEAL` activated `["int"]`, `[OK] +int`

### Run command (runner contract — no bare `cargo test` on Windows)
```
cargo nextest run --test inject_control_wedge -E 'test(w5_a)'
```

### Scope note
My changes: `crates/spt-daemon/tests/inject_control_wedge.rs` (A1/A2 tests), `traceable-reqs.toml` (REQ activation + verdict), the one-line clippy fix in `driven_by_selfheal.rs`. The `broker.rs`/`livehost.rs`/`msg.rs` modifications and the rest of `driven_by_selfheal.rs` are your in-flight W5 work — I left them untouched. The two test files are complementary: `driven_by_selfheal.rs` proves the reconcile *leaves* the latch; my A1/A2 prove *why* a `controller_by==None` reconcile check alone won't fix A2.

agentId: a5617204a40ef8d56 (use SendMessage with to: 'a5617204a40ef8d56' to continue this agent)
<usage>subagent_tokens: 144080
tool_uses: 46
duration_ms: 504477</usage>

Prompt:
SendMessage to: a27bb87b41d0a4f93

Steering refinement for the W5 driven_by repro (do NOT restart; fold into your current work). My broker code-read found TWO distinct mechanisms that both leave `driven_by=Some` latched, and they differ in what `controller_by` reports. I need your repro to characterize BOTH so I pick the right fix:

- **Mechanism A1 (restart / lost in-memory slot):** a FRESH `Broker` hosts a session for an endpoint whose `info.json` was pre-stamped `driven_by=Some`. In-memory controller slot starts empty → I expect `SessionInfo.controller_by == None` while the session is listed live. (This is what my original prompt described.)
- **Mechanism A2 (production wedged-pump / lost detach — the REQ's real target):** a controller ATTACHES for real (become_controller → driven_by=Some, controller slot occupied), then the controller's REMOTE is gone but its broker connection stays OPEN (no clean EOF/detach) and the session is IDLE (no output → the W1 drain-evict / CONTROLLER_WRITE_DEADLINE never fires). I expect the controller slot stays occupied → `SessionInfo.controller_by == Some` AND `driven_by == Some` — i.e. the broker still THINKS it's controlled.

Please REPORT, empirically from the real broker:
1. A1: confirm `controller_by == None` + session live + driven_by stays Some after a reconcile tick.
2. A2: set up an attached controller on a real broker (reuse the attach rig in inject_control_wedge.rs — request_attach/serve_attach/become_controller), then drop/abandon the controller WITHOUT a clean detach and WITHOUT driving output, run a `KIND_SESSIONS` query, and report what `controller_by` reads (Some or None?) and whether `driven_by` stays Some. This tells me whether `controller_by==None` is a sufficient Gap-A signal or whether the production wedged case keeps it Some (meaning the fix must be broker-side eviction, not a reconcile controller_by check).

If A2 is hard to stage faithfully on a real broker in ≤ your remaining iterations, say so explicitly and just give me your best evidence on A1 + a note on what A2 would require — do NOT burn iterations forcing a flaky A2. Keep the RED repro you're already building for A1/Gap B; A2 can be a documented characterization (even a #[ignore]'d skeleton + findings note) rather than a hard gate. Report findings in your final message.


Prompt:
<task-notification>
<task-id>a27bb87b41d0a4f93</task-id>
<tool-use-id>toolu_01FrfTpykZKkBZoaKBkxgVVX</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\a27bb87b41d0a4f93.output</output-file>
<status>completed</status>
<summary>Agent "W5 driven_by self-heal RED repro" came to rest</summary>
<note>A task-notification fires each time this agent comes to rest with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result>Done. Here is the report.

## Deliverable

**Test file:** `C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\driven_by_selfheal.rs`

Two tests, real broker + real PTY + real local IPC, both PASS deterministically via `cargo nextest run --test driven_by_selfheal` (Gap A ~0.49s, Gap B ~0.03s). Runner-contract doc-header + `init_selfheal_home` process-global SPT_HOME pattern copied from `inject_control_wedge.rs`. Both bodies tagged `// [int->REQ-HAZARD-DRIVEN-BY-SELFHEAL]`. I did NOT edit `traceable-reqs.toml`, did NOT implement any reconcile fix, and did NOT touch the operator binary.

## CRITICAL deviation you need to know

**The Gap-B fix is ALREADY LANDED on this branch.** The prompt/plan said "the FIX does not exist yet — prove the latch stays Some." That is false for `v0.13.0-delivery-control`: `reconcile_hosted_liveness` (livehost.rs ~538-542) already carries an `[impl->REQ-HAZARD-DRIVEN-BY-SELFHEAL]`-tagged block that does `set_driven_by(perch, None)` alongside `mark_offline` and logs `DRIVEN_BY_SELFHEAL_OFFLINE`. My first run proved this — Gap B came back `driven_by_after=None`, GREEN. I refused to encode the buggy latched expectation; the Gap-B test now regression-locks the GREEN behavior with a prominent comment. **Gap A's reconcile-side clear is genuinely NOT implemented** — that test is RED (latch proven).

## Observed-signal report (items 1-3)

**Item 1 — Gap A `controller_by` for the live session:** `observed_controller_by = None`, `live_session_present = true`. The fresh-broker-over-stale-perch setup is NOT constructible at the in-process broker API (a fresh `Broker::bind` has zero sessions — sessions are in-memory and own the PtySession, nothing to re-adopt). I reproduced the genuine reconcile Gap-A instead: spawn a live session, then re-stamp the stale `driven_by=Some` marker on disk (the brain-restart leftover), yielding a live session with `controller_by==None` against `driven_by=Some`. Reliable and deterministic.

**Item 2 — Gap B endpoint absence:** confirmed `endpoint_in_sessions=false`, `sessions={}`. The broker hosts nothing for the endpoint; it is absent from the `KIND_SESSIONS` reply.

**Item 3 (the one you flagged as critical) — is there any path where `driven_by=Some` while broker reports `controller_by=Some`?** **No such path observed.** But the more important finding: **`controller_by==None` is AMBIGUOUS.** `dispatch_spawn` pre-attaches the spawner as the LOCAL controller with `by=None` (broker.rs ~1273), so `OutputLog::controller_by()` returns `None` even for a LIVE, locally-controlled session. So `SessionInfo.controller_by==None` does NOT distinguish a lost controller from a live local one. **Your fix must not false-clear on `controller_by==None` alone.**

## Two extra findings that should shape the fix

- **The broker already self-heals `driven_by` on an OBSERVED controller disconnect.** Dropping the controller socket triggers per-conn `detach_if`→`clear_controller`→`stamp_driven_by` (broker.rs ~528, ~563), re-stamping `driven_by=None`. So a plain disconnect is NOT a reconcile Gap-A — the only genuine reconcile Gap-A is the brain-restart leftover where that re-stamp never fired.
- Because `controller_by==None` can't tell an occupied-local slot from an empty one, the plan's own preference holds: **route the live-session Gap-A clear through the broker** (single writer — it can see the occupied-but-local slot), not from the brain reconcile reading `controller_by==None`. A future GREEN Gap-A test must include a still-genuinely-controlled live session that must NOT clear (the false-heal guard).

## RED/GREEN assertions landed

- **Gap B (GREEN, fix already in):** `assert_eq!(driven_by_after, None, ...)` plus `offlined.contains(id)` and `status==offline`. TODO note documents the deviation, not a future flip.
- **Gap A (RED, latch proven):** `assert_eq!(driven_by_after, Some(node), ...)`, with `!offlined.contains(id)` and `status==online` (the reconcile skips live-session perches). A `// TODO(W5-GREEN):` marks the exact spot to flip to `assert_eq!(driven_by_after, None, ...)` and carries the false-heal-guard warning for the fix designer.

I ran only the isolated test (no workspace clippy/build, per your instruction). Suggested verification gate command before you declare W5 done: `cargo nextest run --test driven_by_selfheal`.</result>
<usage><subagent_tokens>144460</subagent_tokens><tool_uses>41</tool_uses><duration_ms>490584</duration_ms></usage>
</task-notification>


Both reports in. Critical reconciliation — the repro **contradicts the plan's core assumption**:

- **Gap B (no session): already fixed** (my edit; engineer regression-locked it GREEN). ✓
- **`controller_by==None` is the WRONG Gap-A signal:** (1) it's ambiguous — `dispatch_spawn` pre-attaches the local spawner with `by=None`, so a live *locally-controlled* session also reads None; (2) the real production case **A2** (remote controller killed without clean detach, idle session) keeps `controller_by==Some(origin)` and `driven_by==Some` — a reconcile None-check misses it entirely.
- The REQ int gate is literally "kill the controller without clean detach → reconcile clears." That's A2. So **the fix must be broker-side eviction keyed on remote-controller liveness**, not a `controller_by==None` reconcile read.

The viable signal: a remote controller's identity is `driven_by=Some(origin)` — heal when `origin` is no longer a connected peer. Check whether the broker can see peer presence.


Bash

grep -n "fn dispatch_net_status\|NetStatus\|connected_peers\|fn peers\|presence\|is_connected\|fn net_status\|roster\|PeerPresence\|node_addr\|origin" crates/spt-daemon/src/nethost.rs | head -40

Find peer-presence surface in nethost


<owl_messages>
<owl_message from="todlando-w174" priority="info">[WORKING_PERCH_NOTICE] Subagent working perch created.
Perch: todlando-w174
Agent type: spt-test-engineer
To message it: $OWL send todlando-w174 todlando</owl_message>
</owl_messages>

[OWL SYSTEM] Informational owl messages above — no acknowledgment needed. Continue your current task.


3://! [`NetHost`] is the broker's ownership of the node's live network presence:
48:    net_presence_event_envelope, net_stream_data_envelope, net_stream_eof_envelope,
270:/// down, so presence survives a brain restart exactly like stream reads do.
307:            let frame = net_presence_event_envelope(&ev);
319:            let frame = net_presence_event_envelope(ev);
409:    presence: Mutex<PresenceLog>,
420:            presence: Mutex::new(PresenceLog::new(DEFAULT_STREAM_RING_CHUNKS)),
449:        .presence
466:            .presence
598:    /// The on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2), set on the live
599:    /// host after `start` via [`Self::set_roster_exchange`] (kept off `NetConfig`
600:    /// so the many hermetic test configs need no change — a roster exchange is a
601:    /// layer atop the proof, opt-in per host). `None` ⇒ no roster leg. Read +
604:    roster: Arc<Mutex<Option<RosterExchange>>>,
618:    /// `&self`, like the live host's other post-`start` knobs (`set_roster_exchange`).
668:        let roster = Arc::new(Mutex::new(None::<RosterExchange>));
673:        let accept_roster = Arc::clone(&roster);
680:                // Snapshot the (opt-in) roster exchange + our live address for
682:                let roster_rx = accept_roster.lock().unwrap().clone();
697:                        // Mesh-D4: on success, a roster leg rides the same stream.
706:                                    roster_rx.as_ref(),
745:            roster,
752:    /// Wire the on-connect roster-exchange seam (Mesh-D4, REQ-MESH-2) onto this
754:    /// [`crate::seedproofx::production_roster_exchange`]; tests inject their own.
755:    /// Off `NetConfig` by design — see the `roster` field. Takes effect on the
758:    pub fn set_roster_exchange(&self, exchange: RosterExchange) {
759:        *self.roster.lock().unwrap() = Some(exchange);
772:    /// [`Self::set_roster_exchange`]) — the many hermetic test configs need no change.
859:        // Snapshot the (opt-in) roster exchange + our live address for this dial.
860:        let roster_rx = self.roster.lock().unwrap().clone();
872:            // Mesh-D4: on success, a roster leg rides the same control stream.
881:                        roster_rx.as_ref(),
921:    /// `is_trusted(subnet, origin)`; unused by any gate at D2.
1244:    /// Attach `sub` as the presence subscriber, replaying liveness events from
1247:    pub(crate) fn subscribe_presence(&self, sub: SharedSend, from_seq: u64) {
1248:        self.shared.presence.lock().unwrap().attach(sub, from_seq);
1251:    /// Detach `sub` from presence iff it is the current subscriber (brain
1253:    pub(crate) fn detach_presence_sub(&self, sub: &SharedSend) {
1254:        self.shared.presence.lock().unwrap().detach_if(sub);
1328:    // mints a REUSED singleton whose origin is this node's OWN id, and


There's a `PresenceLog` tracking peer liveness. Check whether it exposes a synchronous "is peer X connected" query the broker could use to evict a dead remote controller.


grep -rn "struct PresenceLog\|impl PresenceLog\|fn .*presence\|Online\|Offline\|enum.*Presence\|is_online\|peer_online\|connected\|live_peers\|fn attach\|PresenceEvent\|node.*online" crates/spt-daemon/src/presence*.rs crates/spt-net/src/**/*.rs 2>/dev/null | head -40

Inspect presence liveness API


crates/spt-daemon/src/presence.rs:290:        // Offline row: not a surface, however fresh.
crates/spt-daemon/src/presence.rs:291:        reg.merge_instance("ghost", row("dead-hex", Status::Offline, Some(98_000)));
crates/spt-net/src/net/attach.rs:197:    fn attach_intents_round_trip() {
crates/spt-net/src/net/endpoint.rs:305:    /// disconnected events.
crates/spt-net/src/net/registry.rs:14://! `Offline`. [`merge_instance`] defends against this with a **version-vector** rule
crates/spt-net/src/net/registry.rs:17://! stale — so a newer `Offline` (higher epoch) can never be clobbered by a lagging
crates/spt-net/src/net/registry.rs:59:    /// only `Offline` is unroutable. Additive variant on a pre-release fleet;
crates/spt-net/src/net/registry.rs:66:    Offline,
crates/spt-net/src/net/registry.rs:296:    /// `Offline`; a new node is added ([`Inserted`](MergeOutcome::Inserted)). Distinct
crates/spt-net/src/net/registry.rs:319:    /// Whether an instance in this status is a valid routing target. `Offline` is not
crates/spt-net/src/net/registry.rs:322:        !matches!(self, Status::Offline)
crates/spt-net/src/net/registry.rs:365:/// (ADR-0006 §6, exclusion by construction). Unroutable (`Offline`) rows are
crates/spt-net/src/net/registry.rs:432:    /// **different** node, including by an `Offline` instance (a resting endpoint still
crates/spt-net/src/net/registry.rs:646:    /// refuse if several live nodes remain (force `@node`). `Offline` instances are
crates/spt-net/src/net/registry.rs:852:    // lower-epoch Active must NOT overwrite a newer higher-epoch Offline — the resolved
crates/spt-net/src/net/registry.rs:853:    // status stays Offline, so a partition/skew never routes to a dead instance.
crates/spt-net/src/net/registry.rs:857:        // Endpoint went Offline at the newer epoch 5.
crates/spt-net/src/net/registry.rs:858:        reg.merge_instance("ling", inst("nodeA", Status::Offline, 5));
crates/spt-net/src/net/registry.rs:864:            Status::Offline,
crates/spt-net/src/net/registry.rs:865:            "newer Offline survives the stale Active"
crates/spt-net/src/net/registry.rs:877:            reg.merge_instance("ling", inst("nodeA", Status::Offline, 7)),
crates/spt-net/src/net/registry.rs:1169:    fn presence_datum_rides_the_lease_and_serde() {
crates/spt-net/src/net/registry.rs:1207:    // nothing resolution would refuse), an Offline row is not a resource,
crates/spt-net/src/net/registry.rs:1228:        reg.merge_instance("dead", inst("n2", Status::Offline, 1));
crates/spt-net/src/net/registry.rs:1309:    // [unit->REQ-INST-10] a sole remote live instance resolves; an Offline one is skipped.
crates/spt-net/src/net/registry.rs:1314:            ("ling", "dead", Status::Offline),
crates/spt-net/src/net/registry.rs:1337:    // @node that is Offline / absent is NotFound (never routed to a dead instance).
crates/spt-net/src/net/registry.rs:1345:        let dead = reg_with(&[("ling", "a", Status::Offline)]);
crates/spt-net/src/net/registry.rs:1422:    // [unit->REQ-INST-9] an Offline instance still OWNS its name — a resting endpoint's
crates/spt-net/src/net/registry.rs:1427:        reg.merge_instance("ling", inst("nodeA", Status::Offline, 9));
crates/spt-net/src/net/registry.rs:1487:        home.merge_instance("ling", inst("n2", Status::Offline, 3));
crates/spt-net/src/net/registry.rs:1523:        work.merge_instance("fir", inst("n8", Status::Offline, 1)); // 'fir' taken (even Offline)
crates/spt-net/src/net/replicate.rs:280:    // a lagging Active replayed after a newer Offline merges Stale and the
crates/spt-net/src/net/replicate.rs:288:            apply_update(&mut regs, upd("home", "doyle", "n1", Status::Offline, 9)),
crates/spt-net/src/net/replicate.rs:297:        assert_eq!((row.status, row.epoch), (Status::Offline, 9));


This is a real design finding, repro-grounded: the plan's Gap-A reconcile signal is wrong, and the residual A2 (idle-forever wedged *remote*, connection open) is a pump/detach-layer concern (W6/perri) — a clean detach already self-heals via `detach_if`, W1 bounds active wedges. W5's correct, tractable, in-scope reconcile deliverable is **Gap B**. Let me take stock of the actual tree (two engineers touched it) before finalizing.


git status --short; echo "===DIFFSTAT==="; git diff --stat; echo "===REQ BLOCK==="; sed -n '1209,1230p' traceable-reqs.toml

Tree state + REQ block


 M crates/spt-daemon/src/broker.rs
 M crates/spt-daemon/src/livehost.rs
 M crates/spt-daemon/src/msg.rs
 M crates/spt-daemon/tests/inject_control_wedge.rs
 M traceable-reqs.toml
?? crates/spt-daemon/tests/driven_by_selfheal.rs
===DIFFSTAT===
warning: in the working copy of 'crates/spt-daemon/tests/inject_control_wedge.rs', LF will be replaced by CRLF the next time Git touches it
 crates/spt-daemon/src/broker.rs                 |  16 +-
 crates/spt-daemon/src/livehost.rs               |  12 +
 crates/spt-daemon/src/msg.rs                    |  14 ++
 crates/spt-daemon/tests/inject_control_wedge.rs | 288 ++++++++++++++++++++++++
 traceable-reqs.toml                             |   2 +-
 5 files changed, 327 insertions(+), 5 deletions(-)
===REQ BLOCK===
required_stages = []  # SEED — activate [impl,unit] at W4 build (CLI/TUI UX — run `cargo run -p xtask -- gen` if any clap help changes; no REQ-*/M#-W# in /// help). int only if a real-fixture attach assertion is warranted.

# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
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)"
required_stages = ["int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). int = the real-broker characterization of the two latch mechanisms (inject_control_wedge.rs w5_a1 + w5_a2): A1 (local/empty slot ⇒ controller_by==None) and A2 (abandoned REMOTE controller on an IDLE session ⇒ controller_by STAYS Some(origin), driven_by stays latched). EMPIRICAL VERDICT (2026-06-19): controller_by==None is NOT a sufficient Gap-A signal for the production wedged-pump case — the lost-detach slot does not self-clear, so the fix must be BROKER-SIDE eviction (a controller-liveness check / reconcile-driven clear_controller), not merely a reconcile `controller_by==None` read. impl/unit activate when the fix lands.

# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
[[requirements]]
id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint)."

# ── W1b (operator /diagnose 2026-06-19, post-W1 dogfood escape): the effect-journal PTY-input wedge ──
[[requirements]]
id = "REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE"
title = "The effect journal serializes EVERY PTY effect under one mutex held ACROSS two fsyncs AND the blocking PTY write — so interactive input stutters and ultimately wedges the daemon hard. ROOT (doyle /diagnose, code-grounded + MEASURED on the operator's real Windows box, 2026-06-19): EffectJournal::apply_once (effect.rs:168-188) takes `inner.lock()` and holds it across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, where write_line (effect.rs:235-239) does flush()+sync_all() (a full FlushFileBuffers) — so each effect pays TWO fsyncs under a GLOBAL lock, and the closure `effect()` (the actual PTY write, broker.rs:1257 EffectKind::PtyWrite via attach.rs:197 send_effect) runs while the lock is held. Two operator-visible facets, ONE root: (A) STUTTER/LAG — every keystroke is a PtyWrite effect = 2× sync_all serialized; measured fsync on %LOCALAPPDATA%\\spt-core = median 6.5ms, spikes to 198ms (C: was recently at 100%), so ~13ms+ per keystroke best case, hundreds under contention → 'many but not all keypresses take 100s of ms, choppy, worsens with volume'. (B) HARD PERMANENT WEDGE — when a PtyWrite `effect()` blocks (ConPTY input buffer full / harness not draining stdin), the journal lock is held INDEFINITELY → the single-threaded inbound-stream dispatch (dispatch.rs serve_attach, which both applies input effects AND opens attaches) can never progress → EVERY subsequent attach (`spt rc --view`/`--take`) fails with 'attach request: brain IPC read deadline elapsed' (confirmed: two retries deadline identically; broker control-plane KIND queries still answer — different thread). This REFUTES the W2-deferred ruling that park-(b)/(c) is 'Windows-benign because ConPTY absorbs 4MiB' — on the real box the input path wedges regardless. DISTINCT from W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST = the OUTPUT drain, correctly fixed @8b5583e; output uses broker.rs:1106 append, NOT the fsync journal). This is the INPUT/effect-journal path W1 never touched, and it is THE wedge the operator hits with --take/--view. FIX DIRECTION (candidates, repro-first — extend inject_control_wedge.rs to a REAL backed-up-PTY-consumer + a real rc-client attach assertion, the gap W1's gate missed): (1) do NOT hold the journal lock across effect() — reserve the key + fsync PENDING under lock, RELEASE, run effect(), re-acquire to fsync DONE + mark applied (preserve crash-idempotency via the per-key reservation, not a global hold); (2) bound/fail-fast the PtyWrite itself (the W2-deferred park bound — write_input must never block indefinitely, DSR-answer must not hold the writer mutex across a blocking write); (3) drop per-keystroke fsync on the interactive path — PtyWrite effects are EPHEMERAL (a keystroke lost on a broker crash is retyped; PTY state is not reconstructed from keystroke replay), so in-memory applied-set dedup suffices (the broker survives the brain — that IS the dedup anchor), with async/batched fsync or no-fsync for EffectKind::PtyWrite while durable kinds (NetSend/NetDial/Registry/Spool) keep their fsync. Combine (1)+(3) at minimum. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1b (todlando). FIX (1)+(3): apply_once releases inner.lock() across effect() (reserve key + fsync PENDING for DURABLE under lock -> RELEASE -> effect() OFF lock -> re-acquire -> finalize: fsync DONE for durable + mark applied); EffectKind::PtyWrite is EPHEMERAL (is_durable()=false) = NO journal lines / NO fsync, in-memory dedup only (broker-survives-brain is the anchor); durable kinds keep fsync. impl = effect.rs apply_once + EffectKind::is_durable. unit = apply_once_does_not_hold_the_journal_lock_across_effect (barrier proves a 2nd key completes while one effect blocks) + ptywrite_skips_the_durable_journal_write_while_netsend_persists (file-content proxy: PtyWrite writes no PENDING/DONE line, NetSend does); both cross-platform RED pre-fix, GREEN post-fix; gauntlet + exactly-once preserved (7/7). int = inject_control_wedge.rs a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach (REAL broker+PTY+rc pump, NO mocks: a concurrent rc attach stays SERVICED + actually RECEIVES PTY bytes while a journaled input effect is in-flight — the assertion W1's gate lacked; GATE on Windows journaled_ops_pumped=117/subscribed=true/got_output=true). FIX (2) the Unix-forkpty park bound (write_input bounded + DSR off writer-mutex) = gravity-linux FOLLOW-UP within W1b (doyle minimum = (1)+(3); the cfg(unix) park-(b) liveness assert + the forkpty-RED note are folded into the int and tracked). KNOWN-HAZARDS 7.14. Refutes the W2-deferred 'ConPTY-benign' ruling (the wedge is the LOCK HOLD, not the PTY write blocking).


Full picture clear. Engineer #2 already activated the REQ to `["int"]` with an honest verdict: `controller_by==None` insufficient, fix must be broker-side.

**Decision (repro-grounded, contradicts the original plan):** W5's correct, in-scope, shippable deliverable = **Gap B** (no-session `driven_by` self-heal, landed + correct) plus the A1/A2 characterization that proves A2 (idle wedged *remote* controller) needs broker-side presence-eviction / pump-layer detach — deferred to a follow-up REQ (doyle's W6/pump boundary). I will NOT autonomously build net-presence eviction (cross-layer, partition-false-heal risk, needs ADR).

Read both test files to finalize coherently.