919	fn controller_viewer_matrix_and_loud_take() {
920	    let (_broker, name, _spawner, sid) = labeled_session("cv-matrix");
921
922	    // Operator A drives an (undriven) endpoint → controller.
923	    let mut a = connect_retry(&name);
924	    a.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("A control");
925	    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, "free → controller");
926
927	    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a
928	    // silent displace (gate #4). A is untouched.
929	    let mut b = connect_retry(&name);
930	    b.attach_as(sid, 0, AttachIntent::Control, Some("node-B")).expect("B control");
931	    assert_eq!(
932	        read_outcome(&mut b),
933	        SubscribeOutcome::BusyControlled { by: "node-A".to_string() },
934	        "controlled → busy-refuse with the controller's identity"
935	    );
936
937	    // A viewer coexists (read-only; never displaces).
938	    let mut v = connect_retry(&name);
939	    v.attach_as(sid, 0, AttachIntent::Viewer, Some("node-V")).expect("V view");
940	    assert_eq!(read_outcome(&mut v), SubscribeOutcome::Viewer, "viewer attaches");
941
942	    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes
943	    // controller. This is the ONLY displace path.
944	    b.attach_as(sid, 0, AttachIntent::Take, Some("node-B")).expect("B take");
945	    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, "take → took control");
946	    let displaced = (0..2000).find_map(|_| match a.read_event().expect("A event") {
947	        BrokerEvent::Displaced { by, .. } => Some(by),
948	        _ => None,
949	    });
950	    assert_eq!(displaced.as_deref(), Some("node-B"), "A receives the loud kick notice");
951	}
952
953	// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —
954	// a successor re-taking the controller slot after a brain restart — silently
955	// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a
956	// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert
957	// no Displaced reaches the first connection.
958	#[test]
959	fn same_origin_re_subscribe_does_not_displace() {
960	    let (_broker, name, _spawner, sid) = labeled_session("cv-gate7");
961
962	    // `first` rides the PUMP carrier so `read_event_until`'s deadline is honored
963	    // (a Whole conn blocks forever — the deadline is a pump-only feature). We need
964	    // a BOUNDED read to assert the absence of a Displaced frame.
965	    let mut first =
966	        Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect("first pump conn");
967	    first.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("first control");
968	    assert_eq!(read_outcome(&mut first), SubscribeOutcome::Controller);
969
970	    // The "successor" re-serves the SAME operator's stream (same by) on a new
971	    // connection — the brain-restart re-take.
972	    let mut successor = connect_retry(&name);
973	    successor.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("successor control");
974	    assert_eq!(
975	        read_outcome(&mut successor),
976	        SubscribeOutcome::Controller,
977	        "same-origin re-subscribe silently re-takes the controller"
978	    );
979
980	    // The first connection must NOT receive a Displaced — re-taking your own slot
981	    // is never a kick. Drive a marker through the successor and confirm `first`
982	    // saw no Displaced in the window.
983	    successor.send_effect(7, b"GATE7\r").expect("drive");
984	    let mut saw_displaced = false;
985	    for _ in 0..50 {
986	        // Non-fatal drain: the first conn may see nothing at all (it was silently
987	        // superseded); we only care that it is NEVER a Displaced.
988	        match first.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {
989	            Ok(BrokerEvent::Displaced { .. }) => {
990	                saw_displaced = true;
991	                break;
992	            }
993	            Ok(_) => continue,
994	            Err(_) => break, // timeout — nothing pending
995	        }
996	    }
997	    assert!(!saw_displaced, "a same-origin re-take must never kick the prior holder (gate #7)");
998	}
999
1000	// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a
1001	// viewer's connection is refused by the broker (gate #3); the controller's
1002	// resize is honored.
1003	#[test]
1004	fn resize_is_controller_exclusive() {
1005	    let (_broker, name, _spawner, sid) = labeled_session("cv-resize");
1006
1007	    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the
1008	    // controller's resize is honored ⇒ NO Error frame arrives), and the
1009	    // `read_event_until` deadline is only honored on the pump carrier — a Whole
1010	    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`
1011	    // emits nothing on resize, unlike Windows `findstr`).
1012	    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect("ctrl conn");
1013	    ctrl.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("control");
1014	    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);
1015
1016	    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect("view conn");
1017	    view.attach_as(sid, 0, AttachIntent::Viewer, Some("node-V")).expect("view");
1018	    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);
1019
1020	    // The viewer's resize is rejected (it is not the controller).
1021	    view.resize(50, 100).expect("send resize");
1022	    let refused = (0..200).find_map(|_| {
1023	        match view.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {
1024	            Ok(BrokerEvent::Error { message }) => Some(message),
1025	            _ => None,
1026	        }
1027	    });
1028	    assert!(
1029	        refused.as_deref().is_some_and(|m| m.contains("controller-exclusive")),
1030	        "a viewer's resize is refused as controller-exclusive, got {refused:?}"
1031	    );
1032
1033	    // The controller's resize is honored (no error).
1034	    ctrl.resize(50, 100).expect("controller resize");
1035	    let ctrl_err = ctrl.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(50)));
1036	    assert!(
1037	        !matches!(ctrl_err, Ok(BrokerEvent::Error { .. })),
1038	        "the controller's resize is honored"

fn controller_viewer_matrix_and_loud_take() {
    let (_broker, name, _spawner, sid) = labeled_session("cv-matrix");

    // Operator A drives an (undriven) endpoint → controller.
    let mut a = connect_retry(&name);
    a.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("A control");
    assert_eq!(read_outcome(&mut a), SubscribeOutcome::Controller, "free → controller");

    // Operator B's plain Control hits a CONTROLLED endpoint → busy-refuse, NOT a
    // silent displace (gate #4). A is untouched.
    let mut b = connect_retry(&name);
    b.attach_as(sid, 0, AttachIntent::Control, Some("node-B")).expect("B control");
    assert_eq!(
        read_outcome(&mut b),
        SubscribeOutcome::BusyControlled { by: "node-A".to_string() },
        "controlled → busy-refuse with the controller's identity"
    );

    // A viewer coexists (read-only; never displaces).
    let mut v = connect_retry(&name);
    v.attach_as(sid, 0, AttachIntent::Viewer, Some("node-V")).expect("V view");
    assert_eq!(read_outcome(&mut v), SubscribeOutcome::Viewer, "viewer attaches");

    // B's explicit Take displaces A: A gets a LOUD Displaced{by:node-B}, B becomes
    // controller. This is the ONLY displace path.
    b.attach_as(sid, 0, AttachIntent::Take, Some("node-B")).expect("B take");
    assert_eq!(read_outcome(&mut b), SubscribeOutcome::TookControl, "take → took control");
    let displaced = (0..2000).find_map(|_| match a.read_event().expect("A event") {
        BrokerEvent::Displaced { by, .. } => Some(by),
        _ => None,
    });
    assert_eq!(displaced.as_deref(), Some("node-B"), "A receives the loud kick notice");
}

// [int->REQ-RCVIEW-1] GATE #7 (the self-kick trap): a SAME-origin re-subscribe —
// a successor re-taking the controller slot after a brain restart — silently
// re-takes (Controller), and NEVER fires Displaced to the prior holder. Only a
// DIFFERENT origin's Take is loud. Here the same `by` re-subscribes and we assert
// no Displaced reaches the first connection.
#[test]
fn same_origin_re_subscribe_does_not_displace() {
    let (_broker, name, _spawner, sid) = labeled_session("cv-gate7");

    // `first` rides the PUMP carrier so `read_event_until`'s deadline is honored
    // (a Whole conn blocks forever — the deadline is a pump-only feature). We need
    // a BOUNDED read to assert the absence of a Displaced frame.
    let mut first =
        Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect("first pump conn");
    first.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("first control");
    assert_eq!(read_outcome(&mut first), SubscribeOutcome::Controller);

    // The "successor" re-serves the SAME operator's stream (same by) on a new
    // connection — the brain-restart re-take.
    let mut successor = connect_retry(&name);
    successor.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("successor control");
    assert_eq!(
        read_outcome(&mut successor),
        SubscribeOutcome::Controller,
        "same-origin re-subscribe silently re-takes the controller"
    );

    // The first connection must NOT receive a Displaced — re-taking your own slot
    // is never a kick. Drive a marker through the successor and confirm `first`
    // saw no Displaced in the window.
    successor.send_effect(7, b"GATE7\r").expect("drive");
    let mut saw_displaced = false;
    for _ in 0..50 {
        // Non-fatal drain: the first conn may see nothing at all (it was silently
        // superseded); we only care that it is NEVER a Displaced.
        match first.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {
            Ok(BrokerEvent::Displaced { .. }) => {
                saw_displaced = true;
                break;
            }
            Ok(_) => continue,
            Err(_) => break, // timeout — nothing pending
        }
    }
    assert!(!saw_displaced, "a same-origin re-take must never kick the prior holder (gate #7)");
}

// [int->REQ-RCVIEW-1] Resize is CONTROLLER-EXCLUSIVE: a resize arriving on a
// viewer's connection is refused by the broker (gate #3); the controller's
// resize is honored.
#[test]
fn resize_is_controller_exclusive() {
    let (_broker, name, _spawner, sid) = labeled_session("cv-resize");

    // PUMP conns: this test does bounded reads expecting a TIMEOUT (the
    // controller's resize is honored ⇒ NO Error frame arrives), and the
    // `read_event_until` deadline is only honored on the pump carrier — a Whole
    // conn blocks forever when nothing is incoming (hung the Linux runner: `cat`
    // emits nothing on resize, unlike Windows `findstr`).
    let mut ctrl = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect("ctrl conn");
    ctrl.attach_as(sid, 0, AttachIntent::Control, Some("node-A")).expect("control");
    assert_eq!(read_outcome(&mut ctrl), SubscribeOutcome::Controller);

    let mut view = Brain::cold_start_pump(&name, 1, Duration::from_secs(5)).expect("view conn");
    view.attach_as(sid, 0, AttachIntent::Viewer, Some("node-V")).expect("view");
    assert_eq!(read_outcome(&mut view), SubscribeOutcome::Viewer);

    // The viewer's resize is rejected (it is not the controller).
    view.resize(50, 100).expect("send resize");
    let refused = (0..200).find_map(|_| {
        match view.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20))) {
            Ok(BrokerEvent::Error { message }) => Some(message),
            _ => None,
        }
    });
    assert!(
        refused.as_deref().is_some_and(|m| m.contains("controller-exclusive")),
        "a viewer's resize is refused as controller-exclusive, got {refused:?}"
    );

    // The controller's resize is honored (no error).
    ctrl.resize(50, 100).expect("controller resize");
    let ctrl_err = ctrl.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(50)));
    assert!(
        !matches!(ctrl_err, Ok(BrokerEvent::Error { .. })),
        "the controller's resize is honored"


Root cause: my epoch gate on the **live loop** suppresses the `Displaced` control frame — the displaced controller (A) must still receive its kick notification, but the gate makes A's writer bail when B's `become_controller` bumps the epoch → A never gets `Displaced` → hang at line 946.

The fix: the epoch gate belongs on the **initial-batch loop only**. New output only ever flows to the *current* controller's channel, so a superseded writer's live channel holds just its pre-supersede backlog + its terminal `Displaced` — which it must deliver. The live loop already ends correctly on `tx`-drop (pre-P1c behavior). Restoring it:


Edit

    while let Ok(msg) = rx.recv() {
        let (frame, seq) = match &msg {
            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),
            CtrlMsg::Control(frame) => (frame, None),
        };
        // Same gate on the live path, read under the lock: a superseded writer
        // with queued frames stops here rather than interleaving with the new one.
        let ok = match send.lock() {
            Ok(mut s) => {
                if controller_epoch.load(Ordering::Acquire) != epoch {
                    return;
                }
                write_frame(&mut *s, frame).is_ok()
            }
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

    while let Ok(msg) = rx.recv() {
        let (frame, seq) = match &msg {
            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),
            CtrlMsg::Control(frame) => (frame, None),
        };
        // NO epoch gate on the live path (P1c): new output only ever flows to the
        // CURRENT controller's channel (the drain clones `self.controller.tx`), so
        // a superseded writer's channel holds only its pre-supersede output
        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL
        // control frames — notably the `Displaced` kick the displaced controller
        // MUST still receive. The loop ends naturally when `tx` drops. Gating
        // output here would suppress that `Displaced` (the cv-matrix loud-take
        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


check your ongoing Monitor tool call's output


744	            Err(_) => false,
745	        };
746	        if !ok {
747	            return;
748	        }
749	        advance_delivered(&delivered_through, *seq);
750	    }
751	    while let Ok(msg) = rx.recv() {
752	        let (frame, seq) = match &msg {
753	            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),
754	            CtrlMsg::Control(frame) => (frame, None),
755	        };
756	        // Same gate on the live path, read under the lock: a superseded writer
757	        // with queued frames stops here rather than interleaving with the new one.
758	        let ok = match send.lock() {
759	            Ok(mut s) => {
760	                if controller_epoch.load(Ordering::Acquire) != epoch {
761	                    return;
762	                }
763	                write_frame(&mut *s, frame).is_ok()
764	            }
765	            Err(_) => false,
766	        };
767	        if !ok {
768	            return;
769	        }
770	        if let Some(seq) = seq {
771	            advance_delivered(&delivered_through, seq);
772	        }
773	    }
774	}
775
776	/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present
777	/// ONLY when the spawn carried a `translation_binary` path; absent → the session
778	/// uses the degenerate raw-inject path (Layer E fallback) exactly as before.

            Err(_) => false,
        };
        if !ok {
            return;
        }
        advance_delivered(&delivered_through, *seq);
    }
    while let Ok(msg) = rx.recv() {
        let (frame, seq) = match &msg {
            CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),
            CtrlMsg::Control(frame) => (frame, None),
        };
        // Same gate on the live path, read under the lock: a superseded writer
        // with queued frames stops here rather than interleaving with the new one.
        let ok = match send.lock() {
            Ok(mut s) => {
                if controller_epoch.load(Ordering::Acquire) != epoch {
                    return;
                }
                write_frame(&mut *s, frame).is_ok()
            }
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present
/// ONLY when the spawn carried a `translation_binary` path; absent → the session
/// uses the degenerate raw-inject path (Layer E fallback) exactly as before.


        // Same gate on the live path, read under the lock: a superseded writer
        // with queued frames stops here rather than interleaving with the new one.
        let ok = match send.lock() {
            Ok(mut s) => {
                if controller_epoch.load(Ordering::Acquire) != epoch {
                    return;
                }
                write_frame(&mut *s, frame).is_ok()
            }
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

        // NO epoch gate on the live path (P1c): new output only ever flows to the
        // CURRENT controller's channel (the drain clones `self.controller.tx`), so
        // a superseded writer's channel holds only its pre-supersede output
        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL
        // control frames — notably the `Displaced` kick the displaced controller
        // MUST still receive. The loop ends naturally when `tx` drops. Gating
        // output here would suppress that `Displaced` (the cv-matrix loud-take
        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}


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)

        // Same gate on the live path, read under the lock: a superseded writer
        // with queued frames stops here rather than interleaving with the new one.
        let ok = match send.lock() {
            Ok(mut s) => {
                if controller_epoch.load(Ordering::Acquire) != epoch {
                    return;
                }
                write_frame(&mut *s, frame).is_ok()
            }
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

        // NO epoch gate on the live path (P1c): new output only ever flows to the
        // CURRENT controller's channel (the drain clones `self.controller.tx`), so
        // a superseded writer's channel holds only its pre-supersede output
        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL
        // control frames — notably the `Displaced` kick the displaced controller
        // MUST still receive. The loop ends naturally when `tx` drops. Gating
        // output here would suppress that `Displaced` (the cv-matrix loud-take
        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

CtrlMsg::Output(seq, frame) => (frame, Some(*seq)),

CtrlMsg::Control(frame) => (frame, None),

-        // Same gate on the live path, read under the lock: a superseded writer

-        // with queued frames stops here rather than interleaving with the new one.

+        // NO epoch gate on the live path (P1c): new output only ever flows to the

+        // CURRENT controller's channel (the drain clones `self.controller.tx`), so

+        // a superseded writer's channel holds only its pre-supersede output

+        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL

+        // control frames — notably the `Displaced` kick the displaced controller

+        // MUST still receive. The loop ends naturally when `tx` drops. Gating

+        // output here would suppress that `Displaced` (the cv-matrix loud-take

+        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.

-                if controller_epoch.load(Ordering::Acquire) != epoch {

-                write_frame(&mut *s, frame).is_ok()

+            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),


Live-loop gate removed (initial-batch gate stays). Rebuild + verify cv-matrix, carrier, and the broker keystone all green:


Bash

cd C:/Users/decid/Documents/projects/spt-core && echo "=== cv-matrix (was hanging) ===" && timeout 90 cargo test -p spt-daemon --test attach controller_viewer_matrix_and_loud_take -- --exact 2>&1 | grep -E "test result|FAILED|panicked|has been running"; echo "[EXIT=${PIPESTATUS[0]}]" && echo "=== carrier ===" && timeout 90 cargo test -p spt-daemon --test attach attach_survives_target_brain_restart_exactly_once -- --exact 2>&1 | grep -E "test result"; echo "[EXIT=${PIPESTATUS[0]}]"

Verify cv-matrix and carrier after live-loop gate removal


=== cv-matrix (was hanging) ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.06s
[EXIT=0]
=== carrier ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.29s
[EXIT=0]


Both green (cv-matrix 0.06s, carrier 0.29s). Now the broker keystone (lib white-box unit + int) + inject_control_wedge — confirm the live-loop removal didn't disturb them:


cd C:/Users/decid/Documents/projects/spt-core
echo "=== lib (white-box unit incl epoch-gate keystone) ===" && timeout 200 cargo test -p spt-daemon --lib 2>&1 | grep -E "test result|FAILED|panicked|reorder|controller_writer" | tail -8; echo "[lib EXIT=${PIPESTATUS[0]}]"
echo "=== broker (int keystone) ===" && timeout 200 cargo test -p spt-daemon --test broker -- --test-threads=1 2>&1 | grep -E "test result|FAILED"; echo "[broker EXIT=${PIPESTATUS[0]}]"
echo "=== inject_control_wedge ===" && timeout 200 cargo test -p spt-daemon --test inject_control_wedge -- --test-threads=1 2>&1 | grep -E "test result|FAILED"; echo "[inject EXIT=${PIPESTATUS[0]}]"

Run lib unit, broker int, inject_control_wedge


Command running in background with ID: b8l3z15w4. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b8l3z15w4.output. You will be notified when it completes. To check interim output, use Read on that file path.


Backgrounded (`b8l3z15w4`). While it runs, fixing the docs that still say "both loops" — the gate is now **initial-batch only** (the live loop must stay ungated to deliver `Displaced`):


Edit

(2) INVARIANT — `controller_writer` is EPOCH-GATED: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded, on BOTH the initial-batch and live loops — so a superseded writer can never flush past the bump and the live writer is unique (W1-safe: never blocks the drain under `Mutex<OutputLog>`);

(2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is EPOCH-GATED: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded, so a superseded writer can never flush its stale replay past the bump (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is deliberately NOT gated — new output only ever flows to the CURRENT controller's channel, so a superseded writer's channel holds only its pre-supersede backlog (deduped by snap-above) plus its TERMINAL `Displaced` kick, which the displaced controller MUST still receive; that loop ends naturally on `tx`-drop (gating it suppressed the loud-take `Displaced` — the cv-matrix hang);


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

### 7.21 Exactly ONE `controller_writer` per brain↔broker connection — a superseded writer must write nothing further  `[REQ-HAZARD-CONTROLLER-WRITER-REORDER]`

- **Failure (doyle instrumented RACEDIAG repro, kitsubito):** on a brain-restart re-serve the handoff brain registered as controller on the SAME session TWICE over the SAME socket — `Brain::handoff` eagerly `subscribe(prior.next_seq=1)` → `become_controller(from_seq=1)`, spawning writer-A (writes seq 1); then `serve_attach` re-handled the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, spawning writer-B (writes 0,1). `become_controller` dropped the prior `ControllerSink` (its `tx`) but did NOT stop the prior writer — writer-A kept flushing its OWNED `initial` batch, and both writers held clones of one `SharedSend` (`Arc<Mutex<socket>>`) with no inter-thread ordering. When writer-A's seq 1 beat writer-B's seq 0, the strict consumer saw `output gap: got seq 1 want 0` → `attach_survives_target_brain_restart_exactly_once` panicked at `.expect("re-serve")` OR HUNG in `render_until` (serve thread died on the gap → `MARKER_TWO` never reached the wire). `prior.next_seq` is life1's CONSUMPTION cursor (life1 forwards each frame to the operator immediately on consume, so at crash it has forwarded exactly `[0, K)`); life2 resumes from that same `K`, so the boundary aligns and `[0, K)` need never be re-sent. The crash was NOT byte loss — it was the consumer running the strict **reject-gap legacy path** (handoff left `session_cursors` empty) which treats any out-of-order seq as fatal. PRE-EXISTING, surfaced by the v0.13.0 green-both-runners gate; P1b is innocent. Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`.

-- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix #1 — "drop handoff's eager subscribe" — was REVERTED: that subscribe is the standalone-resume mechanism the brain-only update engine + `handoff`/`idempotent`/`daemon_e2e` replay through with no `serve_attach`): (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs dedup-below + snap-above (resume mode), never the reject-gap legacy trap; the ascending-merge property above makes this complete, not merely tolerant; (2) INVARIANT — `controller_writer` is EPOCH-GATED: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded, on BOTH the initial-batch and live loops — so a superseded writer can never flush past the bump and the live writer is unique (W1-safe: never blocks the drain under `Mutex<OutputLog>`); (3) EXPLICIT-RESUME — an explicit `Brain::attach(sid, from_seq)` in resume mode RESETS that session's dedup cursor to `from_seq` (a deliberate re-subscribe honors its floor; `read_event`'s `or_insert` only seeds a first-sight session, so without this a from-0 ground-truth re-read after a resume dedup-drops its own replay). Cold-start brains (empty map) keep the legacy `next_seq` path untouched.

+- **Invariant:** on a single brain↔broker connection exactly ONE `controller_writer` is ever the LIVE writer; a SUPERSEDED writer writes no further frames after the epoch bump it observes; and **every `controller_writer` emits a strictly ASCENDING seq stream** (sorted initial batch + ascending live frames). The CORRECTNESS guarantee that falls out: a snap-above consumer over any interleaving of ascending writers — where the surviving writer (`serve_attach`'s `attach_as(sid,0)`) offers the COMPLETE range `[0, end]` — delivers `[K, end]` with **no skip and no dup** (the first sighting of any seq `>M` is always preceded by a sighting of `M` on that same ascending writer, so `M` is delivered before the cursor can pass it). Enforced/relied-on three ways (NB: fix #1 — "drop handoff's eager subscribe" — was REVERTED: that subscribe is the standalone-resume mechanism the brain-only update engine + `handoff`/`idempotent`/`daemon_e2e` replay through with no `serve_attach`): (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs dedup-below + snap-above (resume mode), never the reject-gap legacy trap; the ascending-merge property above makes this complete, not merely tolerant; (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is EPOCH-GATED: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded, so a superseded writer can never flush its stale replay past the bump (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is deliberately NOT gated — new output only ever flows to the CURRENT controller's channel, so a superseded writer's channel holds only its pre-supersede backlog (deduped by snap-above) plus its TERMINAL `Displaced` kick, which the displaced controller MUST still receive; that loop ends naturally on `tx`-drop (gating it suppressed the loud-take `Displaced` — the cv-matrix hang); (3) EXPLICIT-RESUME — an explicit `Brain::attach(sid, from_seq)` in resume mode RESETS that session's dedup cursor to `from_seq` (a deliberate re-subscribe honors its floor; `read_event`'s `or_insert` only seeds a first-sight session, so without this a from-0 ground-truth re-read after a resume dedup-drops its own replay). Cold-start brains (empty map) keep the legacy `next_seq` path untouched.

- **spt-core mapping:** impl `brain.rs` `Brain::handoff` (KEEP the eager `subscribe`; seed `session_cursors`) + `Brain::attach` (resume-mode cursor reset to `from_seq`) + `broker.rs` `OutputLog.controller_epoch: Arc<AtomicU64>` / `become_controller` (atomic `fetch_add`, passes the new epoch + `Arc::clone` into the writer) / `controller_writer` (epoch gate read UNDER the lock on both loops) / `mark_controller_gone` + the `ControllerJob` epoch read; unit (white-box, `src/broker.rs`) the epoch-gated writer (a superseded writer flushes nothing — only the latest writer's monotonic stream reaches the wire) + `handoff` seeds `session_cursors`/resubscribes; int (keystone, `tests/broker.rs` + `tests/attach.rs`) deterministically force two `become_controller`-on-one-connection on a real broker+brain (no mocks) — PRE-FIX reorders/gaps, POST-FIX monotonic + byte-exact + session live, PLUS `attach_survives_target_brain_restart_exactly_once` green (doyle: 20× isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier).

- **Source:** v0.13.0 P1c (operator-ruled root-fix before ship; doyle root-cause via instrumented repro 2026-06-20; design corrected at the gate — fix #1 reverted, epoch-gate + snap-above + attach-reset carry it). The last v0.13.0 ship-blocker.


(2) INVARIANT — `controller_writer` is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) on BOTH the initial-batch and live loops and returns the instant it is superseded — single live writer per connection, no check-then-block-then-write window (W1-safe: never blocks the drain under `Mutex<OutputLog>`).

(2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop).


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

# ── P1c (operator-ruled root-fix before v0.13.0 ship; doyle root-cause via instrumented repro on kitsubito, 2026-06-20): controller-writer reorder on brain-restart re-serve ──

-title = "Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect(\"re-serve\")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer` is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) on BOTH the initial-batch and live loops and returns the instant it is superseded — single live writer per connection, no check-then-block-then-write window (W1-safe: never blocks the drain under `Mutex<OutputLog>`). (3) EXPLICIT-RESUME — an explicit `Brain::attach(sid,from_seq)` in resume mode resets that session's dedup cursor to `from_seq` (a deliberate re-subscribe honors its floor; `read_event`'s `or_insert` only seeds a first-sight session, so a from-0 ground-truth re-read after a resume would otherwise dedup-drop its own replay); cold-start brains (empty map) keep the legacy `next_seq` path. (v0.13.0)"

+title = "Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect(\"re-serve\")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop). (3) EXPLICIT-RESUME — an explicit `Brain::attach(sid,from_seq)` in resume mode resets that session's dedup cursor to `from_seq` (a deliberate re-subscribe honors its floor; `read_event`'s `or_insert` only seeds a first-sight session, so a from-0 ground-truth re-read after a resume would otherwise dedup-drop its own replay); cold-start brains (empty map) keep the legacy `next_seq` path. (v0.13.0)"

required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1c (todlando, the last v0.13.0 ship-blocker — pre-existing flaky resume race that gates green-both-runners; doyle root-caused + designed, doyle gates). doc = KNOWN-HAZARDS entry (the reorder + the single-live-writer-per-connection invariant + the two compounding defects) + CONTEXT note. impl = (1) Brain::handoff KEEPS its eager subscribe(prior.session_id, prior.next_seq) [fix #1 REVERTED — load-bearing resume] AND seeds session_cursors.insert(prior.session_id, prior.next_seq); (2) OutputLog.controller_epoch -> Arc<AtomicU64>, become_controller passes the new epoch + Arc::clone into controller_writer, which returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame, on both the initial-batch loop and the live recv loop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add; (3) Brain::attach resets session_cursors.insert(session_id, from_seq) when in resume mode (non-empty map) so an explicit re-attach honors its floor. unit = epoch-gated controller_writer: a superseded writer (epoch bumped) writes NOTHING from its initial batch (real broker socket, two become_controller on one connection -> only the latest writer's monotonic stream reaches the wire) + handoff seeds session_cursors (session_cursor(sid) == prior.next_seq) + resubscribes for resume. int = the keystone: deterministically force two become_controller-on-one-connection (handoff-then-reserve ordering) on a real broker+brain, NO mocks -> PRE-FIX the wire reorders (got seq 1 want 0 / gap or byte-drop) -> POST-FIX the wire stays monotonic + byte-exact + the session stays live; PLUS attach_survives_target_brain_restart_exactly_once green (doyle runs it 20x isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier). Full seam sweep (attach/broker/resume/brain_swap/inject_control_wedge — the shared W1 controller-writer model). clippy/traceable/docs-drift."

# ── P1c follow-up SEED (doyle proposed, todlando gated, doyle concurred 2026-06-20): the structural per-connection-floor guard — inactive, activate-don't-pre-fail ──


which returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame, on both the initial-batch loop and the live recv loop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add;

whose INITIAL-BATCH loop returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame) — the live recv loop is NOT gated (delivers a superseded controller's terminal Displaced, ends on tx-drop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add;


title = "Two `controller_writer` threads must never race ONE brain↔broker connection's socket. ROOT (doyle, instrumented RACEDIAG repro on kitsubito): on a brain-restart re-serve the handoff brain registers as controller on the SAME session TWICE over the SAME `Brain::conn` socket — (1) `Brain::handoff` eagerly `subscribe(prior.session_id, prior.next_seq=1)` → `become_controller(from_seq=1)`, initial=[1], spawns writer-A (writes seq 1); (2) `serve_attach` re-handles the replayed `Request{from_seq:0}` → `attach_as(sid,0)` → `become_controller(from_seq=0)`, initial=[0,1], spawns writer-B (writes 0 then 1). `become_controller` (broker.rs) drops the prior `ControllerSink` (its `tx`) but does NOT stop the prior writer thread — writer-A keeps flushing its owned `initial` batch, and both writers hold clones of the same `SharedSend` (`Arc<Mutex<socket>>`) with NO inter-thread ordering. When writer-A's seq 1 wins the socket before writer-B's seq 0, the strict legacy consumer (brain.rs read_event reject-gap path) sees `output gap: got seq 1 want 0` → the test `attach_survives_target_brain_restart_exactly_once` panics at `.expect(\"re-serve\")` OR HANGS in `render_until` (serve thread died on the gap → MARKER_TWO never reaches the wire). `prior.next_seq` is life1's CONSUMPTION cursor, NOT life2's connection state — life2's socket has been sent NOTHING, so a `from_seq=0` full replay on a connection that already streamed seq 1 is contradictory. Snap-above tolerance ALONE can't fix it (it would dedup-drop the late seq 0 → byte loss → the exactly-once byte-identity assert fails). PRE-EXISTING, surfaced by the v0.13.0 green-both-runners release gate; P1b is INNOCENT (its diff touches only input-ack machinery, proven mechanically + the test passes post-P1b in isolation). Sibling flaky cluster: `inject_control_wedge::g2`, `broker::spawn_env_reaches_child`. INVARIANT: on a single brain↔broker connection the controller output-frame stream is monotonic non-decreasing in seq (modulo dedup re-sends); exactly ONE `controller_writer` is ever live per connection; a SUPERSEDED writer writes NO further frames; a re-serve never replays a seq below what the connection already received. FIX (doyle design, corrected at the gate 2026-06-20): fix #1 as designed ('drop handoff's eager subscribe so serve_attach's attach_as is the sole registration') was REVERTED — handoff's `subscribe(prior.next_seq)` IS the standalone-resume mechanism (the brain-only update engine `apply_brain_only` + the `handoff`/`idempotent`/`daemon_e2e` int tests replay output through it with NO `serve_attach`; dropping it hung every resume-via-handoff test). The shipped fix is three parts: (1) CORRECTNESS — `Brain::handoff` seeds `session_cursors` at `prior.next_seq` so the consumer runs the production dedup-below+snap-above path, never the strict reject-gap legacy trap; this is COMPLETE (not merely tolerant) because every `controller_writer` emits an ASCENDING seq stream and the surviving writer (serve_attach's attach_as(sid,0)) offers the complete `[0,end]` range, so a snap-above merge of ascending writers delivers `[K,end]` with no skip/dup (first sighting of any seq>M is preceded by M on that writer). (2) INVARIANT — `controller_writer`'s INITIAL-BATCH replay is epoch-gated: `controller_epoch` is a shared `Arc<AtomicU64>`, the writer re-reads it UNDER `send.lock()` (atomically with `write_frame`) and returns the instant it is superseded — no check-then-block-then-write window, no superseded replay (W1-safe: never blocks the drain under `Mutex<OutputLog>`). The LIVE loop is NOT gated (new output only flows to the current controller; a superseded writer must still deliver its terminal `Displaced` kick — gating it suppressed the loud-take notice; it ends on `tx`-drop). (3) EXPLICIT-RESUME — an explicit `Brain::attach(sid,from_seq)` in resume mode resets that session's dedup cursor to `from_seq` (a deliberate re-subscribe honors its floor; `read_event`'s `or_insert` only seeds a first-sight session, so a from-0 ground-truth re-read after a resume would otherwise dedup-drop its own replay); cold-start brains (empty map) keep the legacy `next_seq` path. (v0.13.0)"

-required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1c (todlando, the last v0.13.0 ship-blocker — pre-existing flaky resume race that gates green-both-runners; doyle root-caused + designed, doyle gates). doc = KNOWN-HAZARDS entry (the reorder + the single-live-writer-per-connection invariant + the two compounding defects) + CONTEXT note. impl = (1) Brain::handoff KEEPS its eager subscribe(prior.session_id, prior.next_seq) [fix #1 REVERTED — load-bearing resume] AND seeds session_cursors.insert(prior.session_id, prior.next_seq); (2) OutputLog.controller_epoch -> Arc<AtomicU64>, become_controller passes the new epoch + Arc::clone into controller_writer, which returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame, on both the initial-batch loop and the live recv loop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add; (3) Brain::attach resets session_cursors.insert(session_id, from_seq) when in resume mode (non-empty map) so an explicit re-attach honors its floor. unit = epoch-gated controller_writer: a superseded writer (epoch bumped) writes NOTHING from its initial batch (real broker socket, two become_controller on one connection -> only the latest writer's monotonic stream reaches the wire) + handoff seeds session_cursors (session_cursor(sid) == prior.next_seq) + resubscribes for resume. int = the keystone: deterministically force two become_controller-on-one-connection (handoff-then-reserve ordering) on a real broker+brain, NO mocks -> PRE-FIX the wire reorders (got seq 1 want 0 / gap or byte-drop) -> POST-FIX the wire stays monotonic + byte-exact + the session stays live; PLUS attach_survives_target_brain_restart_exactly_once green (doyle runs it 20x isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier). Full seam sweep (attach/broker/resume/brain_swap/inject_control_wedge — the shared W1 controller-writer model). clippy/traceable/docs-drift."

+required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 P1c (todlando, the last v0.13.0 ship-blocker — pre-existing flaky resume race that gates green-both-runners; doyle root-caused + designed, doyle gates). doc = KNOWN-HAZARDS entry (the reorder + the single-live-writer-per-connection invariant + the two compounding defects) + CONTEXT note. impl = (1) Brain::handoff KEEPS its eager subscribe(prior.session_id, prior.next_seq) [fix #1 REVERTED — load-bearing resume] AND seeds session_cursors.insert(prior.session_id, prior.next_seq); (2) OutputLog.controller_epoch -> Arc<AtomicU64>, become_controller passes the new epoch + Arc::clone into controller_writer, whose INITIAL-BATCH loop returns when controller_epoch != its snapshot (read UNDER send.lock(), atomically with write_frame) — the live recv loop is NOT gated (delivers a superseded controller's terminal Displaced, ends on tx-drop), become_controller/mark_controller_gone/the ControllerJob epoch read switch to atomic load/fetch_add; (3) Brain::attach resets session_cursors.insert(session_id, from_seq) when in resume mode (non-empty map) so an explicit re-attach honors its floor. unit = epoch-gated controller_writer: a superseded writer (epoch bumped) writes NOTHING from its initial batch (real broker socket, two become_controller on one connection -> only the latest writer's monotonic stream reaches the wire) + handoff seeds session_cursors (session_cursor(sid) == prior.next_seq) + resubscribes for resume. int = the keystone: deterministically force two become_controller-on-one-connection (handoff-then-reserve ordering) on a real broker+brain, NO mocks -> PRE-FIX the wire reorders (got seq 1 want 0 / gap or byte-drop) -> POST-FIX the wire stays monotonic + byte-exact + the session stays live; PLUS attach_survives_target_brain_restart_exactly_once green (doyle runs it 20x isolated single-threaded timeout-wrapped on Linux/kitsubito — the deterministic RED-on-revert carrier). Full seam sweep (attach/broker/resume/brain_swap/inject_control_wedge — the shared W1 controller-writer model). clippy/traceable/docs-drift."


Docs aligned. Checking the lib/broker/inject result:


1	=== lib (white-box unit incl epoch-gate keystone) ===
2	test broker::tests::superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn ... ok
3	test result: ok. 349 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.35s
4	[lib EXIT=0]
5	=== broker (int keystone) ===
6	test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.47s
7	[broker EXIT=0]
8	=== inject_control_wedge ===
9

=== lib (white-box unit incl epoch-gate keystone) ===
test broker::tests::superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn ... ok
test result: ok. 349 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 8.35s
[lib EXIT=0]
=== broker (int keystone) ===
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.47s
[broker EXIT=0]
=== inject_control_wedge ===


lib **349/0** ✓ (incl. white-box keystone `superseded_controller_writer_is_epoch_gated_silent...` — confirms the initial-batch gate is what it exercises), broker **8/0** ✓. inject_control_wedge still finishing. The keystone passing with the live-loop gate removed confirms it tests the initial-batch reorder, exactly as intended.

Waiting on inject, then I run the **full attach suite + remaining daemon suites** as the comprehensive confirmation before surfacing. The piecemeal regressions (757 → brain_swap → cv-matrix) are why I'm running the whole set, not declaring early. Standing by.


<task-notification>
<task-id>b8l3z15w4</task-id>
<tool-use-id>toolu_01GXUBz2tiQKZwpgKFx5TBvc</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b8l3z15w4.output</output-file>
<status>completed</status>
<summary>Background command "Run lib unit, broker int, inject_control_wedge" completed (exit code 0)</summary>
</task-notification>


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


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


8	=== inject_control_wedge ===
9	[inject EXIT=124]
10

=== inject_control_wedge ===
[inject EXIT=124]


inject_control_wedge timed out — but it passed 10/10 at 30.99s in the earlier battery (*with* the live-loop gate). My live-loop edit made it **byte-identical to the original pre-P1c code** (just a comment), so behavior there is unchanged — this is likely the known-flaky `g2` or load slowness, not my change. But I must confirm. Running it alone with per-test visibility:


Bash

cd C:/Users/decid/Documents/projects/spt-core && timeout 300 cargo test -p spt-daemon --test inject_control_wedge -- --test-threads=1 2>&1 | grep -E "test .* \.\.\.|test result|has been running|FAILED|panicked" ; echo "[EXIT=${PIPESTATUS[0]} (124=timeout)]"

Run inject_control_wedge alone with per-test output


Command running in background with ID: btdv91hbh. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\btdv91hbh.output. You will be notified when it completes. To check interim output, use Read on that file path.


Backgrounded (`btdv91hbh`, 300s, isolated). The live loop is now byte-identical to pre-P1c, so if inject hangs deterministically it'd point at the *initial-batch* gate (become_controller re-takes); if it's `g2` flaking or load slowness, that matches doyle's known-flaky cluster. The isolated run will tell. Standing by.


<task-notification>
<task-id>btdv91hbh</task-id>
<tool-use-id>toolu_01V9s1Na2w2tWH2WiA15AZ4u</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\btdv91hbh.output</output-file>
<status>completed</status>
<summary>Background command "Run inject_control_wedge alone with per-test output" completed (exit code 0)</summary>
</task-notification>


1	test a_backed_up_controller_does_not_wedge_the_session ... ok
2	test a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach ... ok
3	test g1_choreography_happy_path_payload_reaches_pty_and_controller_keeps_control ... ok
4	test g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input ... ok
5	test g3_content_free_ping_never_duplicates_operator_keystrokes_into_binary ... ok
6	test g4_translation_child_is_reaped_no_zombie_after_session_down ... ok
7	test injecting_a_large_payload_into_a_stdin_ignoring_child ... ok
8	test p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker ... ok
9	test w5_a1_locally_driven_session_reports_controller_by_none ... ok
10	test w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some ... error: test failed, to rerun pass `-p spt-daemon --test inject_control_wedge`
11	[EXIT=124 (124=timeout)]
12

test a_backed_up_controller_does_not_wedge_the_session ... ok
test a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach ... ok
test g1_choreography_happy_path_payload_reaches_pty_and_controller_keeps_control ... ok
test g2_no_commit_deadline_faults_binary_and_does_not_wedge_controller_input ... ok
test g3_content_free_ping_never_duplicates_operator_keystrokes_into_binary ... ok
test g4_translation_child_is_reaped_no_zombie_after_session_down ... ok
test injecting_a_large_payload_into_a_stdin_ignoring_child ... ok
test p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker ... ok
test w5_a1_locally_driven_session_reports_controller_by_none ... ok
test w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some ... error: test failed, to rerun pass `-p spt-daemon --test inject_control_wedge`
[EXIT=124 (124=timeout)]


9 passed (**incl. g2** — doyle's known-flaky), then `w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some` failed/hung. That's the one doyle **explicitly owns** ("w5_a2_abandoned = doyle P3/CI triage, NOT mine ... don't let it block P1") — it's the flaky-neighbor in the driven_by area, not controller_writer reorder, and it passed in the earlier battery. Confirming it's flaky (not deterministically broken by my change) with repeat runs:


cd C:/Users/decid/Documents/projects/spt-core
for i in 1 2 3; do
  echo "=== w5_a2 run $i ==="
  timeout 90 cargo test -p spt-daemon --test inject_control_wedge w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some -- --exact 2>&1 | grep -E "test result|FAILED|has been running|panicked" | head -3
  echo "[run $i EXIT=${PIPESTATUS[0]} (124=hang)]"
done

Run w5_a2 three times to check flakiness


=== w5_a2 run 1 ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 4.27s
[run 1 EXIT=0 (124=hang)]
=== w5_a2 run 2 ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 4.27s
[run 2 EXIT=0 (124=hang)]
=== w5_a2 run 3 ===
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 9 filtered out; finished in 4.26s
[run 3 EXIT=0 (124=hang)]


w5_a2 passes **3/3 in isolation** (4.27s each) — it's the load-dependent flaky-neighbor doyle owns (P3/CI triage), not broken by P1c. All 10 inject tests pass individually (g2 ✓ too). The suite-level timeout was that flake under heavy load (this box has run many concurrent cargo rounds).

P1c correctness is confirmed across the board. Last comprehensive check — the **full attach suite completes** now that cv-matrix is fixed:


cd C:/Users/decid/Documents/projects/spt-core && timeout 560 cargo test -p spt-daemon --test attach -- --test-threads=1 2>&1 | grep -E "test .* \.\.\. (ok|FAILED)|test result|has been running|panicked" ; echo "[attach EXIT=${PIPESTATUS[0]} (124=timeout)]"

Run full attach suite to confirm completion


test attach_registers_remote_drive_detection ... ok
test attach_survives_target_brain_restart_exactly_once ... ok
test broker_spawns_the_pty_child_in_the_requested_cwd ... ok
test controller_restart_with_viewer_no_displace_and_viewer_survives ... ok
test controller_viewer_matrix_and_loud_take ... ok
test handoff_seeds_resume_cursor_and_resubscribes_for_resume ... ok
test local_attach_via_loopback_conn_rides_the_same_pump ... ok
test loopback_attach_to_a_prepopulated_ring_delivers_without_deadlock ... ok
test loopback_self_dial_is_refused_local_uses_fallback_transport ... ok
test remote_attach_drives_a_real_pty_cross_daemon ... ok
test resize_is_controller_exclusive ... ok
test same_origin_re_subscribe_does_not_displace ... ok
test spt_hosted_bringup_then_cross_node_attach_drives_the_pty ... ok
test wedged_viewer_does_not_stall_controller ... ok
test result: ok. 14 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.39s
[attach EXIT=0 (124=timeout)]


**Full attach suite: 14/14 in 3.39s** ✓ (carrier, cv-matrix, the new int test, all of it). The earlier timeouts were load-induced (concurrent cargo rounds), not code hangs.

P1c suites all green. Now the final gates — traceability coverage + clippy + docs-drift:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== traceable: the two P1c reqs ===" && traceable-reqs check 2>&1 | grep -E "CONTROLLER-WRITER-REORDER|CONTROLLER-RETAKE-FLOOR" | grep -E "\[OK\]|\[--\]|\[FAIL\]" && echo "=== traceable EXIT ===" && traceable-reqs check >/dev/null 2>&1; echo "traceable EXIT=$?"

Confirm traceability coverage for P1c reqs and overall exit


=== traceable: the two P1c reqs ===
  [OK] REQ-HAZARD-CONTROLLER-RETAKE-FLOOR  required: []  stages: -doc -impl -unit -int
  [OK] REQ-HAZARD-CONTROLLER-WRITER-REORDER  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
=== traceable EXIT ===
traceable EXIT=0


traceability green: `REORDER` = **+doc +impl +unit +int**, seed = `[]`, overall **EXIT=0**. Now clippy (CI form) + docs-drift:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== clippy CI form ===" && cargo clippy --workspace --all-targets -- -D warnings 2>&1 | tail -8; echo "[clippy EXIT=${PIPESTATUS[0]}]"

Run workspace clippy CI form


=== clippy CI form ===
    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Checking xtask v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 11.73s
[clippy EXIT=0]


clippy CI form: **EXIT=0, zero warnings**. Now docs-drift (`xtask gen` — confirm no generated-doc drift):


cd C:/Users/decid/Documents/projects/spt-core && cargo run -p xtask -- gen 2>&1 | tail -6; echo "[gen EXIT=${PIPESTATUS[0]}]"; echo "=== generated-doc drift (should be empty beyond my hand-edits) ===" && git status --short | grep -vE "brain.rs|broker.rs|attach.rs|tests/broker.rs|KNOWN-HAZARDS.md|DEFERRED.md|traceable-reqs.toml|P1C-CONTROLLER" || echo "(no unexpected generated drift)"

Run docs generation and check for unexpected drift


   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling xtask v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\xtask)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.84s
     Running `target\debug\xtask.exe gen`
xtask gen: wrote C:\Users\decid\Documents\projects\spt-core\docs-site/src/cli/reference.md
[gen EXIT=0]
=== generated-doc drift (should be empty beyond my hand-edits) ===
(no unexpected generated drift)


**All gates green:** clippy CI form EXIT 0, traceable +doc+impl+unit+int EXIT 0, docs-drift clean (no generated drift). Let me do a final change-set review before surfacing:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== change set ===" && git status --short && echo "=== src diff stat ===" && git diff --stat -- crates/ && echo "=== sanity: live loop ungated, init-batch gated ===" && rg -n "NO epoch gate on the live path|controller_epoch.load.*!= epoch" crates/spt-daemon/src/broker.rs

Review full change set and gate placement


=== change set ===
 M crates/spt-daemon/src/brain.rs
 M crates/spt-daemon/src/broker.rs
 M crates/spt-daemon/tests/attach.rs
 M crates/spt-daemon/tests/broker.rs
 M docs/DEFERRED.md
 M docs/KNOWN-HAZARDS.md
 M traceable-reqs.toml
=== src diff stat ===
warning: in the working copy of 'crates/spt-daemon/src/broker.rs', LF will be replaced by CRLF the next time Git touches it
 crates/spt-daemon/src/brain.rs    |  32 +++++-
 crates/spt-daemon/src/broker.rs   | 211 +++++++++++++++++++++++++++++++---
 crates/spt-daemon/tests/attach.rs | 103 ++++++++++++++++-
 crates/spt-daemon/tests/broker.rs | 230 ++++++++++++++++++++++++++++++++++++++
 4 files changed, 559 insertions(+), 17 deletions(-)
=== sanity: live loop ungated, init-batch gated ===
739:                if controller_epoch.load(Ordering::Acquire) != epoch {
756:        // NO epoch gate on the live path (P1c): new output only ever flows to the
2552:    //    `if controller_epoch.load(Acquire) != epoch { return; }` inside
2618:    /// `if controller_epoch.load(Acquire) != epoch { return; }` inside


Change set coherent (brain.rs +32, broker.rs +211 incl. white-box unit, attach.rs +103, tests/broker.rs +230 int keystone, + 3 doc files). Gate placement verified: line 739 initial-batch gate, line 756 live loop ungated.

Everything's green. Surfacing the full picture to doyle — his 4 demands + the gate-time design corrections + the proofs:


"$OWL" send doyle todlando <<'EOF'
SURFACE — P1c full-green. Carrier test name for your 20x: `attach_survives_target_brain_restart_exactly_once` (crates/spt-daemon/tests/attach.rs). Diff is yours to read; here's what changed at the gate + the proofs for #2/#3.

DESIGN CORRECTIONS MADE AT THE GATE (2, both caught by tests, neither shipped):
1. fix #1 REVERTED (handoff keeps subscribe) — as messaged; the standalone-resume callers needed it.
2. EPOCH GATE IS INITIAL-BATCH-ONLY, not both loops. The live-loop gate I'd added SUPPRESSED the `Displaced` kick — a displaced controller (cv-matrix loud-take) never got its Displaced frame → hang. ROOT: new output only ever flows to the CURRENT controller's channel (drain clones self.controller.tx), so a superseded writer's live channel holds only its pre-supersede backlog + its TERMINAL Displaced, which it MUST deliver. The live loop now = original pre-P1c (ends on tx-drop). The reorder hazard is the INITIAL-BATCH replay; that's the only loop gated. (A 3rd bug too: fix #3's seed broke the carrier's from-0 ground-truth re-read — fixed by the attach-from_seq cursor-reset.)

#2 REORDER-SAFETY (proof, structural not test-incidental): My change does NOT alter which frames life2 delivers ([K,end]) nor which life1 forwards ([0,K)). It only converts a reorder within [K,end] from reject-gap-CRASH to snap-above-tolerated. The completeness proof: every controller_writer emits a STRICTLY ASCENDING seq stream (sorted initial batch + ascending live), and the SURVIVING writer (serve_attach's attach_as(sid,0)) offers the COMPLETE [0,end] range. For any merge of ascending streams, the first sighting of any seq>M on the socket is necessarily preceded by a sighting of M on that SAME writer (it emits M before M+1) → so when cursor would pass M, M was already delivered → snap-above NEVER skips and dedups the rest. Hence [K,end] delivered complete+in-order regardless of interleave. Your [B_cursor,K) worry: life2 never sends <K (by design, both pre- and post-P1c) — those are life1's forward, UNCHANGED by P1c; B_cursor is a RENDER cursor, frames <K are already RECEIVED on the broker-owned QUIC stream (life1 forwarded each on consume, broke only after MARKER_ONE), lagging-render ≠ loss. So no NEW loss surface; the only behavior change is crash→tolerated.

#3 ATTACH CURSOR-RESET (gating proof): `Brain::attach(sid,from_seq)` inserts session_cursors[sid]=from_seq ONLY `if !session_cursors.is_empty()` (resume mode). Callers that hit it: only explicit Brain::attach on an already-resume-mode brain (the carrier's from-0 ground-truth re-read). Cold-start brains (empty map) → untouched, legacy next_seq path. read_event (per-frame) does NOT reset — only the explicit attach() call does, so nothing resets mid-stream; a live re-subscribe IS a deliberate attach(from_seq) and honoring its floor is correct. No first-attach regression (first-sight still or_inserts; this only fixes an explicit RE-attach of a tracked session).

#4 SWEEP (all green, single-threaded, this Windows box):
- attach 14/14 (carrier + cv-matrix loud-take + the new int handoff_seeds_resume_cursor_and_resubscribes_for_resume + same_origin_re_subscribe + resize + cross-daemon)
- broker 8/8 (int keystone controller_writer_reorder) ; lib 349/0 (white-box keystone superseded_controller_writer_is_epoch_gated_silent...)
- brain_swap 1/1 (THE canary — revert restored resume) ; handoff 1/1 ; idempotent 1/1
- inject_control_wedge: all 10 pass individually incl g2; w5_a2_abandoned passes 3/3 isolated but FLAKES under heavy concurrent suite load → that's YOUR P3/CI flaky-neighbor (driven_by area, not reorder), not P1c. Windows --test-threads=1 under load also just slow (the suites pass clean isolated).
GATES: clippy --workspace --all-targets -D warnings = 0 ; traceable +doc+impl+unit+int EXIT=0 (+ REQ-HAZARD-CONTROLLER-RETAKE-FLOOR seed []) ; docs-drift clean.
DOCS updated to shipped design: KH 7.21 (init-batch-gate + ascending-merge completeness + live-loop-ungated-for-Displaced + handoff-keeps-subscribe + attach-reset) + checklist row + REQ title/comment + DEFERRED row.

Yours: read the diff for #2/#3 + run the 20x kitsubito carrier. Nothing pushed. Reachable.
Co-authored by: todlando
EOF

Surface full P1c green + proofs to doyle


➜ SENT:doyle


Surfaced to doyle. P1c is complete on my side — full-green, gates passed, docs aligned, awaiting his diff-read + 20× kitsubito gate.

## Summary

**v0.13.0 P1c — controller-writer reorder root-fix: DONE, surfaced for gate.**

Shipped fix (3 mechanisms; **2 design corrections made at the gate**, neither shipped):
- **Correctness:** `handoff` seeds `session_cursors` → consumer uses dedup+snap-above. Complete (not just tolerant) because every writer emits *ascending* and the surviving writer offers the full `[0,end]` range → snap-above never skips.
- **Invariant:** `controller_writer` **initial-batch** loop epoch-gated under `send.lock()` (single live writer; no superseded replay). Live loop **ungated** — it must deliver a displaced controller's terminal `Displaced`.
- **Explicit-resume:** `attach(sid, from_seq)` resets the resume-mode dedup cursor to honor its floor.

Corrections caught by tests (not asserted-green):
1. **fix #1 reverted** — dropping handoff's eager subscribe broke the standalone-resume callers (brain_swap/update-engine, handoff, idempotent, daemon_e2e). doyle's anticipated "fix #2 becomes mandatory."
2. **live-loop gate removed** — it suppressed the loud-take `Displaced` (cv-matrix hang).
3. **carrier from-0 re-read** fixed via the attach cursor-reset.

Green:
- attach 14/14, broker 8/8 (int keystone), lib 349/0 (white-box keystone), brain_swap/handoff/idempotent ✓, inject 10/10 individually.
- clippy CI form = 0, traceable +doc+impl+unit+int EXIT 0 (+ deferred seed `[]`), docs-drift clean.
- `w5_a2_abandoned` flakes only under heavy load (doyle's P3/CI triage, not P1c).

Docs updated to the shipped design (KH 7.21 + checklist + REQ + DEFERRED). Nothing pushed. Reachable for doyle's gate.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="f6760724" type="msg" from="doyle">HEADS UP — shared-tree close call, resolved. Your P1c work was UNCOMMITTED in the shared working tree (C:/.../spt-core) and my gate-prep `git stash -u` swept it. Caught it before dropping (580-line diff across brain/broker/attach/broker-tests/DEFERRED/KH/traceable = your full P1c), git stash pop'd it back CLEAN, no co</EVENT-PART>
<EVENT-PART seq="2/3" id="f6760724">nflict, no loss. The SHARED-TREE NOTE biting exactly as flagged.

To de-risk (it almost vanished once) + because I need it on kitsubito to run the 20x carrier, I'm COMMITTING + PUSHING your P1c now (authored to you via co-author trailer). HOLD OFF editing the working tree until I finish the gate — otherwise we collide. If you have any </EVENT-PART>
<EVENT-PART seq="3/3" id="f6760724">more uncommitted P1c polish NOT in the tree right now, tell me before I push.

Then I gate off the pushed commit: 20x carrier on kitsubito + diff read (#2 epoch-gate-initial-batch-only, #3 attach cursor-reset, reorder-safety) + keystone non-vacuousness + full W1 seam sweep. Verdict to follow.

Co-authored by: doyle</EVENT-PART></event>
</task-notification>


doyle's `git stash -u` nearly swept my uncommitted P1c — caught it, stash-popped clean. He's committing+pushing it (co-authored to me) for the kitsubito 20× gate, and asks: any uncommitted polish NOT in the tree? + hold off editing.

Confirming completeness + ACK:


"$OWL" send --reply-to doyle todlando <<'EOF'
CONFIRMED COMPLETE — nothing uncommitted outside the tree. Every P1c edit went to disk via the editor; the 7-file diff you saw IS the whole thing, no hidden polish, no scratch buffers:
  brain.rs (handoff subscribe-kept + cursor-seed; attach from_seq cursor-reset)
  broker.rs (controller_epoch Arc<AtomicU64>; init-batch epoch-gate-under-lock; live loop UNGATED; white-box unit)
  tests/attach.rs (int handoff_seeds_resume_cursor_and_resubscribes_for_resume + carrier interaction)
  tests/broker.rs (int keystone)
  KNOWN-HAZARDS.md (7.21 + checklist) / DEFERRED.md (retake-floor row) / traceable-reqs.toml (REORDER updated + RETAKE-FLOOR seed)
The plan doc is already your @3749845. So the tree == complete P1c.

ACK hold-off — I will NOT touch the working tree until your gate verdict. Standing down, reachable.

Good catch on the stash sweep — that's the SHARED-TREE NOTE exactly. Push it; I'm clear.
Co-authored by: todlando
EOF

Confirm completeness + ack hold-off to doyle


➜ REPLIED:todlando


Replied — confirmed the tree is the complete P1c, ACK'd hold-off. doyle is committing+pushing + running the 20× kitsubito gate. I won't touch the working tree until his verdict.

While paused, saving a reusable process learning from the subagent-stall (cost ~50 min):


Write

---
name: test-subagent-stalls-on-hanging-test
description: A test subagent (spt-test-engineer) wrapping test runs in blocking until-loops stalls forever if a test it spawned HANGS; take over and bisect yourself.
metadata:
  type: feedback
---

When a delegated test subagent (e.g. [[delegate-tests-to-spt-test-engineer]]) runs suites via blocking `until <marker>; do sleep; done` loops, a HANGING test stalls it indefinitely — its loop never sees the completion marker, so it just keeps "coming to rest" with no result (saw ~50 min wasted on a P1c run). The subagent's owl perch keeps spawning (w185/w186/w187…) and its background cargo keeps running.

**Why:** spt-daemon e2e tests hang (not crash) on this bug class — `render_until`/`read_output_until` block forever when expected output never arrives. The subagent's until-loop can't distinguish "still running" from "hung."

**How to apply:** If a test subagent rests repeatedly with no final result, suspect a hung test. Don't wait. (1) `Get-CimInstance Win32_Process -Filter "Name='cargo.exe'"` + look for a long-lived `<suite>-<hash>.exe` test binary (minutes = hang); check `CommandLine` for which suite. (2) `TaskStop` the subagent so it stops respawning cargo. (3) Kill the orphan test/cargo procs by PID — SPARE the live daemon/psyche (filter by CreationDate; test procs are from the run window). (4) Bisect yourself: `--list` the suite, then run tests timeout-wrapped (`timeout 90 cargo test … <name> -- --exact --nocapture`) to pin the hanger. (5) On this Windows box, the heavy attach/inject e2e suites also just run SLOW under `--test-threads=1` + concurrent load — a suite that times out at 360s often passes clean isolated in seconds, so re-run isolated before concluding "hang." Always timeout-wrap test runs (`timeout … ; EXIT=$?`, 124=hang) so a hang surfaces as a clean signal, never an open-ended wait.


<owl_messages>
<owl_message from="doyle" priority="high">CONFIRMED COMPLETE — nothing uncommitted outside the tree. Every P1c edit went to disk via the editor; the 7-file diff you saw IS the whole thing, no hidden polish, no scratch buffers:
  brain.rs (handoff subscribe-kept + cursor-seed; attach from_seq cursor-reset)
  broker.rs (controller_epoch Arc<AtomicU64>; init-batch epoch-gate-under-lock; live loop UNGATED; white-box unit)
  tests/attach.rs (int handoff_seeds_resume_cursor_and_resubscribes_for_resume + carrier interaction)
  tests/broker.rs (int keystone)
  KNOWN-HAZARDS.md (7.21 + checklist) / DEFERRED.md (retake-floor row) / traceable-reqs.toml (REORDER updated + RETAKE-FLOOR seed)
The plan doc is already your @3749845. So the tree == complete P1c.

ACK hold-off — I will NOT touch the working tree until your gate verdict. Standing down, reachable.

Good catch on the stash sweep — that's the SHARED-TREE NOTE exactly. Push it; I'm clear.
Co-authored by: todlando</owl_message>
</owl_messages>

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


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

---
name: test-subagent-stalls-on-hanging-test
description: A test subagent (spt-test-engineer) wrapping test runs in blocking until-loops stalls forever if a test it spawned HANGS; take over and bisect yourself.
metadata:
  node_type: memory
  type: feedback
  originSessionId: ba9196fa-7544-41c6-854e-b36cebc3b771
---

When a delegated test subagent (e.g. [[delegate-tests-to-spt-test-engineer]]) runs suites via blocking `until <marker>; do sleep; done` loops, a HANGING test stalls it indefinitely — its loop never sees the completion marker, so it just keeps "coming to rest" with no result (saw ~50 min wasted on a P1c run). The subagent's owl perch keeps spawning (w185/w186/w187…) and its background cargo keeps running.

**Why:** spt-daemon e2e tests hang (not crash) on this bug class — `render_until`/`read_output_until` block forever when expected output never arrives. The subagent's until-loop can't distinguish "still running" from "hung."

**How to apply:** If a test subagent rests repeatedly with no final result, suspect a hung test. Don't wait. (1) `Get-CimInstance Win32_Process -Filter "Name='cargo.exe'"` + look for a long-lived `<suite>-<hash>.exe` test binary (minutes = hang); check `CommandLine` for which suite. (2) `TaskStop` the subagent so it stops respawning cargo. (3) Kill the orphan test/cargo procs by PID — SPARE the live daemon/psyche (filter by CreationDate; test procs are from the run window). (4) Bisect yourself: `--list` the suite, then run tests timeout-wrapped (`timeout 90 cargo test … <name> -- --exact --nocapture`) to pin the hanger. (5) On this Windows box, the heavy attach/inject e2e suites also just run SLOW under `--test-threads=1` + concurrent load — a suite that times out at 360s often passes clean isolated in seconds, so re-run isolated before concluding "hang." Always timeout-wrap test runs (`timeout … ; EXIT=$?`, 124=hang) so a hang surfaces as a clean signal, never an open-ended wait.


That owl message is my own reply echoed back in the thread — not a new ask, no action needed.

Memory saved. Adding the index pointer, then standing by for doyle's gate verdict:


9	- [Delegate tests to spt-test-engineer](delegate-tests-to-spt-test-engineer.md) — write spt-core tests via the `spt-test-engineer` subagent (Agent tool subagent_type), not inline.
10	- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.
11	- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).
12

- [Delegate tests to spt-test-engineer](delegate-tests-to-spt-test-engineer.md) — write spt-core tests via the `spt-test-engineer` subagent (Agent tool subagent_type), not inline.
- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.
- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).


Edit

- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).

- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).
- [Test subagent stalls on hanging test](test-subagent-stalls-on-hanging-test.md) — a test subagent's blocking until-loops stall forever on a hung test; take over (kill orphans, bisect timeout-wrapped) instead of waiting; Windows e2e suites also just run slow under load.


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

- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).
- [Test subagent stalls on hanging test](test-subagent-stalls-on-hanging-test.md) — a test subagent's blocking until-loops stall forever on a hung test; take over (kill orphans, bisect timeout-wrapped) instead of waiting; Windows e2e suites also just run slow under load.

# Memory index

- [Cross-machine test rig](cross-machine-test-rig.md) — enlyzeam (Windows, ssh key) for two-host network spikes + Tailscale-down gotcha; gravity-linux (Ubuntu, david/bigscreen pw-auth + askpass shim) as the Linux/forkpty build host.
- [CI traceability gate](ci-traceability-gate.md) — how CI installs traceable-reqs (TRACEABLE_REQS_TOKEN PAT + latest BigscreenVR release); the v0.1.0-stale-binary gotcha + the v0.1.1 fix; I have admin to cut releases.
- [Attractor build harness](attractor-build-harness.md) — attractor+pi/codex setup at ../attractor; codex model = openai-codex/gpt-5.5 (NOT the SPEC's gpt-5.3-codex); viable for bounded single-machine spt-core tasks (SPIKE-07b gix pilot).
- [Windows UAC binary-name 740](windows-uac-binary-name-740.md) — exe names containing update/setup/install/patch trigger UAC elevation (os error 740); name self-update artifacts + test bins to dodge it.
- [M4 resume state](m4-resume-state.md) — M4 (spt-net) in progress 2026-06-03; D0+D1 done (iroh =0.98.2 endpoint, identity-bound, relay policy, mDNS); spt-net = first async crate; next = D2 pairing.
- [Linux runner migration](linux-runner-migration.md) — replace gravity CI Linux runner with kitsubito (LAN Linux), not WSL (disk cost); deferred until after v0.1.1; retarget `gravity` host-label in ci/release/docs-publish workflows to a role label.
- [Delegate tests to spt-test-engineer](delegate-tests-to-spt-test-engineer.md) — write spt-core tests via the `spt-test-engineer` subagent (Agent tool subagent_type), not inline.
- [spt verification gates](spt-verification-gates.md) — before "done": workspace clippy (CI form), rebuild the real bin (not cargo test), verify terminal fixes live, keep the shared tree compilable.
- [Redirect folded REQ seeds](redirect-folded-req-seeds.md) — when folding a wave into existing reqs, retire/redirect the wave's minted seed in the SAME commit (doyle caught a dangling REQ-PICKER-UX-V013 @ W4).

+- [Test subagent stalls on hanging test](test-subagent-stalls-on-hanging-test.md) — a test subagent's blocking until-loops stall forever on a hung test; take over (kill orphans, bisect timeout-wrapped) instead of waiting; Windows e2e suites also just run slow under load.


Memory indexed. P1c work is complete and surfaced; doyle has the tree (committing + pushing for the kitsubito gate).

**Holding** — not touching the working tree until doyle's gate verdict (20× carrier + diff read of #2/#3 + keystone non-vacuousness + W1 seam sweep). Reachable.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/9" id="be4262a0" type="msg" from="doyle">GATE: NOT PASS — P1c does NOT make the carrier deterministically green. Don't fold to delivery-control as final yet.

DATA (carrier = attach_survives_target_brain_restart_exactly_once on kitsubito):
- 20x under concurrent CI load: 11 PASS / 9 FAIL / 0 HANG
- 12x CLEAN isolated (no CI contention): 8 PASS / </EVENT-PART>
<EVENT-PART seq="2/9" id="be4262a0">4 FAIL / 0 HANG (~33%)
- EVERY failure: panicked at attach.rs:215:25 — `assert_eq!(seq, *next_seq, &quot;no forward gap in the viewport&quot;)` in render_until (the OPERATOR viewport render loop).
- The ORIGINAL failure (attach.rs:722 &quot;output gap got seq 1 want 0&quot;, brain-side) is GONE. So P1c DID fix the brain-side reorder. B</EVENT-PART>
<EVENT-PART seq="3/9" id="be4262a0">ut a residual reorder now surfaces at the OPERATOR STREAM.

THIS IS A REAL BUG, NOT test-over-strictness — I checked the production consumer:
rc.rs:1016-1020 (the REAL operator, spt rc pump): `if seq &lt; cursor { continue } cursor = seq+1; render`. That's dedup-below + SNAP-ABOVE — rc does NOT crash on a forward gap, BUT a skippe</EVENT-PART>
<EVENT-PART seq="4/9" id="be4262a0">d seq's chunk is dedup-dropped if re-sent → SILENT operator content loss. render_until's strict assert correctly catches what rc loses silently. Relaxing render_until to snap-above would just move the failure to the byte-identity assert (758) — the content loss is real either way.

WHY THE KEYSTONES MISSED IT: the white-box unit (syn</EVENT-PART>
<EVENT-PART seq="5/9" id="be4262a0">thetic double become_controller) + the int keystone (consumer dedup view) both PASS — they don't reproduce the real brain-crash-handoff interleaving on stream_b that the carrier does. The carrier is the load-bearing oracle; the keystones are necessary-but-insufficient. (clippy/traceable/canaries brain_swap+handoff+idempotent all green — th</EVENT-PART>
<EVENT-PART seq="6/9" id="be4262a0">e revert restored standalone resume correctly; the gap is purely this carrier.)

HYPOTHESIS for the residual (instrument to confirm — RACEDIAG on stream_b's ACTUAL delivered seq order at the operator + the two writers' commit order + the operator render cursor vs K=prior.next_seq at the moment of re-serve):
- The reorder-safety proo</EVENT-PART>
<EVENT-PART seq="7/9" id="be4262a0">f assumed B_cursor == K (operator has [0,K) from life1). Suspect a window where the operator render cursor &lt; K when writer-A replays from K — e.g. life1 advanced next_seq to K (read the frame) but the forward to B's stream hadn't landed before the crash, OR writer-A commits &gt;1 pre-bump frame creating a hole, OR a Size/EOF frame interac</EVENT-PART>
<EVENT-PART seq="8/9" id="be4262a0">tion on stream_b. The epoch-gate closed the brain-side window but not this operator-stream one.

NEXT: pin the residual interleaving on kitsubito (the carrier repros there ~33% isolated), then close it. NOT passin
...(truncated)</event>
</task-notification>


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


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


Gate: **NOT PASS** — carrier ~33% fail *isolated* on kitsubito. P1c fixed the brain-side reorder (722 gap gone) but a residual reorder now surfaces at the **operator stream** (attach.rs:215, render_until "no forward gap"). My reorder-safety proof has a hole — doyle suspects B_cursor < K when writer-A replays from K. Reading the full message + the actual topology:


Bash

cd C:/Users/decid/Documents/projects/spt-core && tail -c 1400 "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/97787e5e-64b2-4afd-b3fe-becaa2ea8030/tasks/b4cwwiglf.output" | tr '\n' '\v' | sed 's/<EVENT-PART[^>]*>//g; s/<\/EVENT-PART>//g; s/<EVENT[^>]*>//g' | tr '\v' '\n' | tail -12

Read full tail of doyle's gate verdict


be4262a0">e revert restored standalone resume correctly; the gap is purely this carrier.)

HYPOTHESIS for the residual (instrument to confirm — RACEDIAG on stream_b's ACTUAL delivered seq order at the operator + the two writers' commit order + the operator render cursor vs K=prior.next_seq at the moment of re-serve):
- The reorder-safety proo
f assumed B_cursor == K (operator has [0,K) from life1). Suspect a window where the operator render cursor < K when writer-A replays from K — e.g. life1 advanced next_seq to K (read the frame) but the forward to B's stream hadn't landed before the crash, OR writer-A commits >1 pre-bump frame creating a hole, OR a Size/EOF frame interac
tion on stream_b. The epoch-gate closed the brain-side window but not this operator-stream one.

NEXT: pin the residual interleaving on kitsubito (the carrier repros there ~33% isolated), then close it. NOT passing until the carrier is 20x deterministically green AND there's no operator content-loss path. I'm holding the gate here. Ping
me with the instrumented root + the fix; I'll re-run the 20x. The fix de-risk commit @9b0025b stays on the branch (your work is safe), but it's NOT release-ready.

Co-authored by: doyle


doyle handed it back ("ping me with the instrumented root + the fix") — gate held, my work safe @9b0025b. The bug moved to the operator stream and my proof has a hole. Per the confirm-first discipline I won't guess-fix. Let me first read the actual topology to form a precise hypothesis — render_until, serve_attach's forward path, and the carrier's life1→operator flow:


195	    brain: &mut Brain,
196	    stream_id: u64,
197	    decoder: &mut AttachDecoder,
198	    next_seq: &mut u64,
199	    buf: &mut Vec<u8>,
200	    needle: &[u8],
201	) {
202	    let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle);
203	    while !contains(buf, needle) {
204	        match brain.read_event().expect("operator read event") {
205	            BrokerEvent::NetStreamData {
206	                stream_id: sid,
207	                bytes,
208	                ..
209	            } if sid == stream_id => {
210	                for rec in decoder.push(&bytes) {
211	                    if let AttachRecord::Output { seq, data_b64 } = rec {
212	                        if seq < *next_seq {
213	                            continue; // replay duplicate — dedup by cursor
214	                        }
215	                        assert_eq!(seq, *next_seq, "no forward gap in the viewport");
216	                        *next_seq = seq + 1;
217	                        buf.extend_from_slice(&decode_bytes(&data_b64).expect("chunk b64"));
218	                    }
219	                }
220	            }
221	            BrokerEvent::NetStreamEof { .. } => panic!("stream ended before the marker rendered"),
222	            BrokerEvent::Error { message } => panic!("operator broker error: {message}"),
223	            _ => {}
224	        }
225	    }
226	}
227
228	/// The prefix of `hay` through (and including) the first occurrence of
229	/// `needle` — the deterministic comparison window when two readers share a
230	/// "stop once the buffer contains the needle" rule but batch events
231	/// differently (each side's overshoot past the needle is chunking
232	/// nondeterminism, not a delivery difference).
233	fn prefix_through<'a>(hay: &'a [u8], needle: &[u8]) -> &'a [u8] {
234	    let pos = hay
235	        .windows(needle.len())
236	        .position(|w| w == needle)
237	        .expect("the stop rule guarantees the needle rendered");
238	    &hay[..pos + needle.len()]
239	}

    brain: &mut Brain,
    stream_id: u64,
    decoder: &mut AttachDecoder,
    next_seq: &mut u64,
    buf: &mut Vec<u8>,
    needle: &[u8],
) {
    let contains = |hay: &[u8], needle: &[u8]| hay.windows(needle.len()).any(|w| w == needle);
    while !contains(buf, needle) {
        match brain.read_event().expect("operator read event") {
            BrokerEvent::NetStreamData {
                stream_id: sid,
                bytes,
                ..
            } if sid == stream_id => {
                for rec in decoder.push(&bytes) {
                    if let AttachRecord::Output { seq, data_b64 } = rec {
                        if seq < *next_seq {
                            continue; // replay duplicate — dedup by cursor
                        }
                        assert_eq!(seq, *next_seq, "no forward gap in the viewport");
                        *next_seq = seq + 1;
                        buf.extend_from_slice(&decode_bytes(&data_b64).expect("chunk b64"));
                    }
                }
            }
            BrokerEvent::NetStreamEof { .. } => panic!("stream ended before the marker rendered"),
            BrokerEvent::Error { message } => panic!("operator broker error: {message}"),
            _ => {}
        }
    }
}

/// The prefix of `hay` through (and including) the first occurrence of
/// `needle` — the deterministic comparison window when two readers share a
/// "stop once the buffer contains the needle" rule but batch events
/// differently (each side's overshoot past the needle is chunking
/// nondeterminism, not a delivery difference).
fn prefix_through<'a>(hay: &'a [u8], needle: &[u8]) -> &'a [u8] {
    let pos = hay
        .windows(needle.len())
        .position(|w| w == needle)
        .expect("the stop rule guarantees the needle rendered");
    &hay[..pos + needle.len()]
}


render_until asserts the operator's stream_b is **gapless** (`seq == next_seq`, dedup-tolerant below). The operator gap comes from serve_attach forwarding life2's output. Reading serve_attach's output-forward path + the carrier's life1/life2/operator wiring: