Edit

    /// W1: the controller write is no longer inline here. The drain used to call
    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
    /// log lock blocked). Now this returns the job and releases the lock; the
    /// drain does a bounded `send_timeout` into the controller's writer-thread
    /// channel ([`OutputLog::deliver_to_controller`]) lock-free. Called only from
    /// the session's single drain thread.
    #[must_use]
    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {

    /// W1: the controller write is no longer inline here. The drain used to call
    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
    /// log lock blocked). Now this returns the job and releases the lock; the
    /// drain does a bounded, off-lock send into the controller's writer-thread
    /// channel ([`ControllerJob::deliver`]). Called only from the session's
    /// single drain thread.
    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[must_use]
    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {


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)

    /// W1: the controller write is no longer inline here. The drain used to call
    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
    /// log lock blocked). Now this returns the job and releases the lock; the
    /// drain does a bounded `send_timeout` into the controller's writer-thread
    /// channel ([`OutputLog::deliver_to_controller`]) lock-free. Called only from
    /// the session's single drain thread.
    #[must_use]
    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {

    /// W1: the controller write is no longer inline here. The drain used to call
    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
    /// log lock blocked). Now this returns the job and releases the lock; the
    /// drain does a bounded, off-lock send into the controller's writer-thread
    /// channel ([`ControllerJob::deliver`]). Called only from the session's
    /// single drain thread.
    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[must_use]
    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {

-    /// drain does a bounded `send_timeout` into the controller's writer-thread

-    /// channel ([`OutputLog::deliver_to_controller`]) lock-free. Called only from

-    /// the session's single drain thread.

+    /// drain does a bounded, off-lock send into the controller's writer-thread

+    /// channel ([`ControllerJob::deliver`]). Called only from the session's

+    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]


/// A failed write (dead operator) ends the thread; the drain's next bounded
/// `send_timeout` then sees `Disconnected` and detaches the row. `Output`
/// advances `delivered_through`; `Control` (Displaced/…) does not.
fn controller_writer(

/// A failed write (dead operator) ends the thread; the drain's next bounded
/// send then sees `Disconnected` and detaches the row. `Output` advances
/// `delivered_through`; `Control` (Displaced/…) does not.
// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
fn controller_writer(


/// A failed write (dead operator) ends the thread; the drain's next bounded
/// `send_timeout` then sees `Disconnected` and detaches the row. `Output`
/// advances `delivered_through`; `Control` (Displaced/…) does not.
fn controller_writer(

/// A failed write (dead operator) ends the thread; the drain's next bounded
/// send then sees `Disconnected` and detaches the row. `Output` advances
/// `delivered_through`; `Control` (Displaced/…) does not.
// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
fn controller_writer(

/// write each — so a backed-up controller blocks ONLY this thread, never the

/// session drain (which used to write the controller inline under the log lock).

-/// `send_timeout` then sees `Disconnected` and detaches the row. `Output`

-/// advances `delivered_through`; `Control` (Displaced/…) does not.

+/// send then sees `Disconnected` and detaches the row. `Output` advances

+/// `delivered_through`; `Control` (Displaced/…) does not.


Add the W1 unit test. Find the end of the tests module.


1775	            "a full viewer queue evicts (drain does not block)"
1776	        );
1777	        // Writer thread gone (its `rx` dropped) → `Disconnected` → evict.
1778	        drop(rx);
1779	        assert!(
1780	            viewer_send_evicts(&tx.try_send(3)),
1781	            "a disconnected viewer evicts"
1782	        );
1783	        // A viewer keeping up (room in the queue) is NOT evicted.
1784	        let (tx2, _rx2) = sync_channel::<u8>(1);
1785	        assert!(
1786	            !viewer_send_evicts(&tx2.try_send(9)),
1787	            "a viewer with room stays attached"
1788	        );
1789	    }
1790
1791	    /// The additive `resume_seq` field (D4-1, KH-2.3 N-1 window): an older
1792	    /// broker's `SessionsReply` JSON omits it → a newer brain deserializes it as
1793	    /// `0` (full-ring replay = safe, dup-only), never a parse reject; a present
1794	    /// value round-trips.
1795	    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
1796	    #[test]
1797	    fn sessions_reply_resume_seq_defaults_when_absent() {
1798	        // Old-broker shape: no `resume_seq` key on the session object.
1799	        let old: SessionsReply =
1800	            serde_json::from_str(r#"{"sessions":[{"session_id":7,"endpoint":"e"}]}"#)
1801	                .expect("old-broker reply deserializes");
1802	        assert_eq!(
1803	            old.sessions[0].resume_seq, 0,
1804	            "absent resume_seq -> default 0"
1805	        );
1806	        assert_eq!(old.sessions[0].session_id, 7);
1807	        // New-broker shape: the cursor round-trips.
1808	        let new: SessionsReply = serde_json::from_str(
1809	            r#"{"sessions":[{"session_id":7,"endpoint":"e","resume_seq":42}]}"#,
1810	        )
1811	        .expect("new-broker reply deserializes");
1812	        assert_eq!(
1813	            new.sessions[0].resume_seq, 42,
1814	            "present resume_seq round-trips"
1815	        );
1816	    }
1817	}
1818

            "a full viewer queue evicts (drain does not block)"
        );
        // Writer thread gone (its `rx` dropped) → `Disconnected` → evict.
        drop(rx);
        assert!(
            viewer_send_evicts(&tx.try_send(3)),
            "a disconnected viewer evicts"
        );
        // A viewer keeping up (room in the queue) is NOT evicted.
        let (tx2, _rx2) = sync_channel::<u8>(1);
        assert!(
            !viewer_send_evicts(&tx2.try_send(9)),
            "a viewer with room stays attached"
        );
    }

    /// The additive `resume_seq` field (D4-1, KH-2.3 N-1 window): an older
    /// broker's `SessionsReply` JSON omits it → a newer brain deserializes it as
    /// `0` (full-ring replay = safe, dup-only), never a parse reject; a present
    /// value round-trips.
    // [unit->REQ-HAZARD-HANDOFF-ARGV-COMPAT]
    #[test]
    fn sessions_reply_resume_seq_defaults_when_absent() {
        // Old-broker shape: no `resume_seq` key on the session object.
        let old: SessionsReply =
            serde_json::from_str(r#"{"sessions":[{"session_id":7,"endpoint":"e"}]}"#)
                .expect("old-broker reply deserializes");
        assert_eq!(
            old.sessions[0].resume_seq, 0,
            "absent resume_seq -> default 0"
        );
        assert_eq!(old.sessions[0].session_id, 7);
        // New-broker shape: the cursor round-trips.
        let new: SessionsReply = serde_json::from_str(
            r#"{"sessions":[{"session_id":7,"endpoint":"e","resume_seq":42}]}"#,
        )
        .expect("new-broker reply deserializes");
        assert_eq!(
            new.sessions[0].resume_seq, 42,
            "present resume_seq round-trips"
        );
    }
}


        assert_eq!(
            new.sessions[0].resume_seq, 42,
            "present resume_seq round-trips"
        );
    }
}

        assert_eq!(
            new.sessions[0].resume_seq, 42,
            "present resume_seq round-trips"
        );
    }

    /// W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the off-lock controller delivery
    /// kernel. `deliver` is BOUNDED, never a permanent park (the v0.12.x wedge):
    /// a channel with room queues instantly; a DISCONNECTED writer returns the
    /// epoch (detach); a FULL channel that never drains returns the epoch after
    /// the deadline (detach), instead of blocking the drain forever.
    // [unit->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[test]
    fn controller_deliver_is_bounded_never_a_permanent_park() {
        let frame = output_envelope(1, 0, b"x");
        // Room → queued instantly (the common path; no deadline wait).
        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);
        let job = ControllerJob {
            tx: tx.clone(),
            seq: 0,
            frame: frame.clone(),
            epoch: 7,
        };
        assert!(job.deliver().is_ok(), "a channel with room queues the chunk");

        // Disconnected writer (its rx dropped) → Err(epoch), no wait.
        let (tx2, rx2) = sync_channel::<CtrlMsg>(1);
        drop(rx2);
        let job = ControllerJob {
            tx: tx2,
            seq: 0,
            frame: frame.clone(),
            epoch: 7,
        };
        assert_eq!(
            job.deliver(),
            Err(7),
            "a disconnected controller writer detaches at its epoch"
        );

        // Full channel that never drains → Err(epoch) after the deadline (bounded),
        // NOT a permanent block. Use a tiny depth + a fill so the next send is Full.
        let (tx3, _rx3) = sync_channel::<CtrlMsg>(1);
        tx3.try_send(CtrlMsg::Output(0, frame.clone())).expect("fill the slot");
        let job = ControllerJob {
            tx: tx3,
            seq: 1,
            frame,
            epoch: 9,
        };
        let start = Instant::now();
        assert_eq!(
            job.deliver(),
            Err(9),
            "a fully-stalled controller detaches after the deadline"
        );
        assert!(
            start.elapsed() >= CONTROLLER_WRITE_DEADLINE,
            "deliver waits the full deadline before declaring a stalled controller gone"
        );
        assert!(
            start.elapsed() < CONTROLLER_WRITE_DEADLINE + Duration::from_secs(5),
            "deliver returns shortly after the deadline (bounded, never permanent)"
        );
    }

    /// W1 — `advance_delivered` moves the shared cursor monotonically (D4-1) via
    /// the same [`advanced_cursor`] kernel, now on the atomic the controller
    /// writer owns.
    // [unit->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[test]
    fn advance_delivered_is_monotonic() {
        let dt = AtomicU64::new(0);
        advance_delivered(&dt, 0);
        assert_eq!(dt.load(Ordering::Acquire), 1, "seq 0 -> cursor 1");
        advance_delivered(&dt, 5);
        assert_eq!(dt.load(Ordering::Acquire), 6, "seq 5 -> cursor 6");
        advance_delivered(&dt, 2); // a rewind re-send cannot lower the cursor
        assert_eq!(dt.load(Ordering::Acquire), 6, "rewind never decreases");
    }
}


        assert_eq!(
            new.sessions[0].resume_seq, 42,
            "present resume_seq round-trips"
        );
    }
}

        assert_eq!(
            new.sessions[0].resume_seq, 42,
            "present resume_seq round-trips"
        );
    }

    /// W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the off-lock controller delivery
    /// kernel. `deliver` is BOUNDED, never a permanent park (the v0.12.x wedge):
    /// a channel with room queues instantly; a DISCONNECTED writer returns the
    /// epoch (detach); a FULL channel that never drains returns the epoch after
    /// the deadline (detach), instead of blocking the drain forever.
    // [unit->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[test]
    fn controller_deliver_is_bounded_never_a_permanent_park() {
        let frame = output_envelope(1, 0, b"x");
        // Room → queued instantly (the common path; no deadline wait).
        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);
        let job = ControllerJob {
            tx: tx.clone(),
            seq: 0,
            frame: frame.clone(),
            epoch: 7,
        };
        assert!(job.deliver().is_ok(), "a channel with room queues the chunk");

        // Disconnected writer (its rx dropped) → Err(epoch), no wait.
        let (tx2, rx2) = sync_channel::<CtrlMsg>(1);
        drop(rx2);
        let job = ControllerJob {
            tx: tx2,
            seq: 0,
            frame: frame.clone(),
            epoch: 7,
        };
        assert_eq!(
            job.deliver(),
            Err(7),
            "a disconnected controller writer detaches at its epoch"
        );

        // Full channel that never drains → Err(epoch) after the deadline (bounded),
        // NOT a permanent block. Use a tiny depth + a fill so the next send is Full.
        let (tx3, _rx3) = sync_channel::<CtrlMsg>(1);
        tx3.try_send(CtrlMsg::Output(0, frame.clone())).expect("fill the slot");
        let job = ControllerJob {
            tx: tx3,
            seq: 1,
            frame,
            epoch: 9,
        };
        let start = Instant::now();
        assert_eq!(
            job.deliver(),
            Err(9),
            "a fully-stalled controller detaches after the deadline"
        );
        assert!(
            start.elapsed() >= CONTROLLER_WRITE_DEADLINE,
            "deliver waits the full deadline before declaring a stalled controller gone"
        );
        assert!(
            start.elapsed() < CONTROLLER_WRITE_DEADLINE + Duration::from_secs(5),
            "deliver returns shortly after the deadline (bounded, never permanent)"
        );
    }

    /// W1 — `advance_delivered` moves the shared cursor monotonically (D4-1) via
    /// the same [`advanced_cursor`] kernel, now on the atomic the controller
    /// writer owns.
    // [unit->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[test]
    fn advance_delivered_is_monotonic() {
        let dt = AtomicU64::new(0);
        advance_delivered(&dt, 0);
        assert_eq!(dt.load(Ordering::Acquire), 1, "seq 0 -> cursor 1");
        advance_delivered(&dt, 5);
        assert_eq!(dt.load(Ordering::Acquire), 6, "seq 5 -> cursor 6");
        advance_delivered(&dt, 2); // a rewind re-send cannot lower the cursor
        assert_eq!(dt.load(Ordering::Acquire), 6, "rewind never decreases");
    }
}

+    /// W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the off-lock controller delivery

+    /// kernel. `deliver` is BOUNDED, never a permanent park (the v0.12.x wedge):

+    /// a channel with room queues instantly; a DISCONNECTED writer returns the

+    /// epoch (detach); a FULL channel that never drains returns the epoch after

+    /// the deadline (detach), instead of blocking the drain forever.

+    // [unit->REQ-HAZARD-INJECT-CONTROL-COEXIST]

+    fn controller_deliver_is_bounded_never_a_permanent_park() {

+        let frame = output_envelope(1, 0, b"x");

+        // Room → queued instantly (the common path; no deadline wait).

+        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);

+        assert!(job.deliver().is_ok(), "a channel with room queues the chunk");

+        // Disconnected writer (its rx dropped) → Err(epoch), no wait.

+        let (tx2, rx2) = sync_channel::<CtrlMsg>(1);

+            "a disconnected controller writer detaches at its epoch"

+        // Full channel that never drains → Err(epoch) after the deadline (bounded),

+        // NOT a permanent block. Use a tiny depth + a fill so the next send is Full.

+        let (tx3, _rx3) = sync_channel::<CtrlMsg>(1);

+        tx3.try_send(CtrlMsg::Output(0, frame.clone())).expect("fill the slot");

+            "a fully-stalled controller detaches after the deadline"

+            start.elapsed() >= CONTROLLER_WRITE_DEADLINE,

+            "deliver waits the full deadline before declaring a stalled controller gone"

+            start.elapsed() < CONTROLLER_WRITE_DEADLINE + Duration::from_secs(5),

+            "deliver returns shortly after the deadline (bounded, never permanent)"

+    /// W1 — `advance_delivered` moves the shared cursor monotonically (D4-1) via

+    /// the same [`advanced_cursor`] kernel, now on the atomic the controller

+    fn advance_delivered_is_monotonic() {

+        assert_eq!(dt.load(Ordering::Acquire), 1, "seq 0 -> cursor 1");

+        assert_eq!(dt.load(Ordering::Acquire), 6, "seq 5 -> cursor 6");

+        advance_delivered(&dt, 2); // a rewind re-send cannot lower the cursor

+        assert_eq!(dt.load(Ordering::Acquire), 6, "rewind never decreases");


Activate the REQ in traceable-reqs.toml.


required_stages = []  # SEED — activate [impl,unit,int] at W1 build. int gate (real dummy fixture + real daemon): after an injected keystroke sequence into an rc-attached spt-hosted session, the controller keeps control, no ONLINE+CONTROLLED latch, no wedge, a NEW attach still serves, and `daemon stop` is bounded.

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1↔W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.


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


1189	id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"
1190	title = "SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)"
1191	required_stages = []  # SEED — activate [impl,unit,int] at W1 build. int gate (real dummy fixture + real daemon): after an injected keystroke sequence into an rc-attached spt-hosted session, the controller keeps control, no ONLINE+CONTROLLED latch, no wedge, a NEW attach still serves, and `daemon stop` is bounded.
1192

id = "REQ-HAZARD-INJECT-CONTROL-COEXIST"
title = "SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)"
required_stages = []  # SEED — activate [impl,unit,int] at W1 build. int gate (real dummy fixture + real daemon): after an injected keystroke sequence into an rc-attached spt-hosted session, the controller keeps control, no ONLINE+CONTROLLED latch, no wedge, a NEW attach still serves, and `daemon stop` is bounded.


required_stages = []  # SEED — activate [impl,unit,int] at W1 build. int gate (real dummy fixture + real daemon): after an injected keystroke sequence into an rc-attached spt-hosted session, the controller keeps control, no ONLINE+CONTROLLED latch, no wedge, a NEW attach still serves, and `daemon stop` is bounded.

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.


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

title = "SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)"

-required_stages = []  # SEED — activate [impl,unit,int] at W1 build. int gate (real dummy fixture + real daemon): after an injected keystroke sequence into an rc-attached spt-hosted session, the controller keeps control, no ONLINE+CONTROLLED latch, no wedge, a NEW attach still serves, and `daemon stop` is bounded.

+required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W1 (todlando). ROOT confirmed repro-first on a REAL broker + REAL PTY (inject_control_wedge.rs): park (a) = OutputLog::append fanned each chunk to the controller via a SYNCHRONOUS inline write_frame on the session drain thread while holding Mutex<OutputLog> → a backed-up controller socket parked the drain WITH THE LOCK HELD → permanent per-session wedge (KIND_SESSIONS hung; net_status answered = process alive). Parks (b)/(c) (write_input write_all on a full input buffer + DSR-answer writer-mutex contention) confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix forkpty. FIX: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (controller_writer, the viewer_writer pattern) but AUTHORITATIVE — the writer advances delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic); the drain hands each chunk off via a bounded off-lock send (ControllerJob::deliver, CONTROLLER_WRITE_DEADLINE=5s → detach/clear-driven-by/unlatch via the shared clear_controller, never park forever); displaced/exit notices ride the writer channel (CtrlMsg::Control). impl = broker.rs append/controller_writer/become_controller/clear_controller. unit = controller_deliver_is_bounded_never_a_permanent_park + advance_delivered_is_monotonic. int = inject_control_wedge.rs (a_backed_up_controller_does_not_wedge_the_session: a fully-backed-up controller keeps KIND_SESSIONS answering = no wedge). W1<->W5 SYNERGY: clear_controller IS the lost-controller unlatch W5's reconcile reuses.

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──


Run the new unit tests + clippy (`-D warnings`) for the touched crates.


Bash

cargo test -p spt-daemon --lib 2>&1 | tail -20 && echo "===CLIPPY===" && cargo clippy -p spt-daemon -p spt-term --all-targets -- -D warnings 2>&1 | tail -25

Run broker lib unit tests + clippy


Command running in background with ID: buyjyrmub. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\buyjyrmub.output. You will be notified when it completes. To check interim output, use Read on that file path.


Check traceable-reqs availability + run the gate (it scans source, no cargo lock contention).


(command -v traceable-reqs >/dev/null 2>&1 && traceable-reqs check 2>&1 | tail -30 || echo "traceable-reqs NOT on PATH")

Run traceable-reqs check


  [OK] REQ-TERM-3  required: [impl, unit]  stages: -doc +impl +unit -int
       Byte-stream remote terminal streaming for v1
  [OK] REQ-TERM-4  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Live activity buffer (session digest): projection of normalized session logs, snapshot-pull (spt endpoint digest) + structured-delta-stream contract + api digest-entry push
  [OK] REQ-TERM-5  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Adapter-declared digest extractor seam: a `[digest]` manifest section declaring an imperative extractor (native harness log -> the {role,text,tool,ts} contract; defaults to the [history] source files with an own-source escape hatch), `api digest-entry` push fallback, register-time validation of the section, adapter-declared presentation defaults (window depth, arg-truncation, sprint-collapse) that any consumer may override, and a `spt adapter digest-proof` author tool plus runtime skip-diagnostics (no silent drop). Reverses M9's no-manifest-seam stance; no declarative DSL.
  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).


traceable-reqs ran. Need the verdict + my REQ's status + exit code.


traceable-reqs check 2>&1 | grep -iE "FAIL|MISSING|ERROR|INJECT-CONTROL|RC-INPUT-KEY|EXIT|summary|[0-9]+ ok|covered" | tail -30; echo "EXIT=${PIPESTATUS[0]}"

traceable-reqs verdict + INJECT-CONTROL status


       `spt endpoint run` HONORS manifest [env.<VAR>] direction=inject values (with {key} substitution) on the spt-hosted spawn. Today only the [session.self] command ARGV is {id}-substituted; the [env] inject value is NEITHER substituted NOR applied — manifest.schema.json promises EnvVar.value = 'Value to inject (with substitution)' but prepare_harness_spawn fills only argv and SpawnReq carries no env, so a [env.SPT_ENDPOINT_ID].value='{id}' arrives EMPTY. A FLAGLESS harness (bare `claude`, no argv slot for {id}) then routes the id via [env] → empty → SessionStart sees empty $SPT_ENDPOINT_ID → seeds-by-PPID instead of binding → ZERO perch → NO_PERCH (the actual wall-b bind blocker; perri hard-repro'd). SILENT failure (empty inject, no error). FIX (doyle ruled a): fill every [env] inject value from the SAME {key} catalog as argv/role (mirror F-009 TEMPLATE fill, whole-string fill_template for an env value), thread it through SpawnReq.env → the broker sets it on the spawned PTY child. Correctness fix — schema already promises it, NO manifest change, NO new binary. PAIRS with REQ-SEND-SPT-HOSTED to make endpoint run fully work. doyle F-013. (post-v0.10.0)
       Ephemeral perch cleanup on every ring exit path (3.1)
       B2 KEYSTONE: a daemon-hosted (spt-hosted) endpoint's info.json status is RECONCILED to real liveness, not left latched online. The broker exit-waiter (broker.rs:889-910) reaps its in-mem session table + emits ExitEvent but NEVER touches info.json; lifecycle::mark_offline only fires on Psyche teardown — so a dead/exited harness (operator closed the tab) stays status=online forever (is_perch_alive returns ONLINE for daemon-hosted, liveness.rs:80-93). FIX (doyle ruled PULL-PRIMARY — the live-status analog of REQ-HAZARD-ROSTER-GHOST): the livehost reconcile loop (reconcile_once livehost.rs:226-313) queries the broker's live session set (KIND_SESSIONS) each tick and, for any status=online live_agent perch PAST the boot grace whose endpoint has NO live broker session, marks it offline (lifecycle::mark_offline → status=offline → is_perch_alive=false). GATED on spt-hosted (controllable==Some(true)) so a HARNESS-HOSTED relay live agent (api listen, legitimately online with no broker session) is NEVER mis-marked. Crash-robust + self-healing on the next tick (clear-on-event is not crash-robust alone). PUSH (brain ExitEvent→mark_offline) is an OPTIONAL fast-path only if the daemon brain is reliably subscribed to all hosted sessions; correctness rides the pull. Broker stays stateless (ADR-0004 §B — brain owns the info.json write). (v0.12.0)
  [OK] REQ-HAZARD-INJECT-CONTROL-COEXIST  required: [impl, unit, int]  stages: -doc +impl +unit +int
       SPINE INVARIANT (v0.13.0 keystone): the broker must accept INJECTED keystrokes into an spt-hosted PTY (the v0.11.0 raw direct-inject today; the ADR-0022 translation-binary choreography tomorrow) WHILE a live `spt rc` controller is attached to the SAME PTY, without (a) the operator losing control, (b) the endpoint latching ONLINE+CONTROLLED, or (c) the broker wedging. The injection inlet is PERMANENT — spt-claude-code requires keystroke injection — so this is root-caused + fixed at the PTY-injection layer, IN STEP with the ADR-0022 delivery redesign that formalizes the inlet. REOPENS the wedge facet of REQ-HAZARD-ATTACH-WEDGE: the v0.12.1 prove-don't-change covered only DEAD-CHILD backpressure, NOT the injection trigger (operator's signal — one injected keystroke succeeds, the next wedges → the single-threaded broker parks on a blocking PTY/loopback write after injection-induced harness output). REPRO-FIRST on the real dummy-harness fixture (NO theory): instrument to nail the exact blocking call before any fix. Fix candidates: non-blocking/fail-fast PTY write, split input/output, bounded-evicting. Mechanism shared with W2 — spt-core owns EVERY PTY write and applies an injected sequence ATOMICALLY (controller input buffered during the sequence, flushed after) so a stash/restore can't be clobbered. CONFIRMED ROOT (doyle /diagnose 2026-06-19, code-grounded): Broker::append (broker.rs:205-227) fans each live output chunk to the CONTROLLER on a SYNCHRONOUS BLOCKING write_frame held inline in the session's drain thread (the 'authoritative, advances delivered_through' path, D4-1), while VIEWERS use a dedicated writer thread + bounded evicting sync_channel (add_viewer:273 / viewer_writer) that can never stall the drain. So a slow/backed-up controller socket — or the full 64KB loopback duplex (the ATTACH-WEDGE buffer) — BLOCKS the drain thread → output stalls → keystroke echoes stall (PERCEIVED input lag) → unrecoverable wedge when the consumer never drains. TRIGGERS ON NORMAL INTERACTIVE rc USE under heavy harness output (TUI redraw), NOT only message injection — same root, wider repro. FIX DIRECTION: move controller delivery off the drain thread onto a dedicated writer (the viewer_writer pattern) BUT preserve the authoritative cursor — block the WRITER thread (not the drain), bound the wedge (deadline → detach/mark-gone, never park forever), never silently evict the operator's authoritative view. (v0.13.0)
       B5: `spt daemon start` does NOT revive phantom Psyches for dead-but-online-latched perches. Today reconcile_once (livehost.rs:285) spawns a Psyche per status=online live_agent perch at boot WITHOUT verifying the harness child / {id}-psyche is actually alive — so a Cold start after an unclean stop revives N psyches for N dead-but-latched perches (3 psyches for 3 dead perches). FIX: gate the boot psyche-spawn on real child-liveness — a perch with NO live broker session (the B2 reconcile signal) is marked OFFLINE at boot instead of hosted, so a dead-harness perch is never revived. Shares the B2 reconcile loop (this is its boot-gate arm); composes with B2's honest latch. Also closes wall-a's psyche_host_error gap (residency-confirm does not run at boot tick-1, livehost.rs:395-441 / 257-263). (v0.12.0)
       The brain's daemon-hosted Psyche lifecycle surfaces a host-FAILURE on the live perch (harness-diagnosable) and runs net-INDEPENDENTLY. When reconcile_once→host_one→spawn_psyche fails for a state=live_agent+status=online endpoint (e.g. the adapter's psyche binary absent from its install dir, REQ-INSTALL-11), the failure MUST be written to the perch info.json as a CURRENT-STATE field (reason + ts + attempt count; overwritten each 5s retry, CLEARED on successful host) and surfaced by `spt endpoint list`/status — never left as an eprintln on the brain's invisible stderr where a harness reading only perch state is blind. status=online stays authoritative (agent reachable; only the Psyche is missing — brain-restart rehydrate legitimately has online-without-Psyche windows), so this is a SEPARATE psyche-host-health field, never a status de-stamp. Net-independence is a locked-in invariant: spawn_live_host (brainproc.rs:230) reaches the reconcile and hosts the Psyche on a net-less/unpaired/peer-pump-STALLED node, proven by a REAL detached-daemon E2E (real broker→brain-child, real api seed+listen, real install-dir psyche binary). spt-core SURFACES the failure; the adapter owns fixing its packaging.
       A daemon-hosted Psyche that spawns then EXITS IMMEDIATELY is a host failure, surfaced like a spawn failure (closes the v0.8.1 residual masking): the REQ-HAZARD-LIVEHOST-BOOT-RACE signal stamps `psyche_host_error` only when `spawn_psyche` returns Err, NOT when the detached spawn() returns Ok but the child dies within moments (e.g. a bad-argv child exiting 2 — the F-009 case). That leaves the residual 'online + no Psyche + no cause' gap: the nested `{id}-psyche` info.json is written status=online with a real-but-DEAD pid and the PARENT perch carries NO psyche_host_error (perri's F-010: tasklist showed 0 host procs across the window while info.json read online). The host MUST confirm RESIDENCY — a hosted child not alive (or whose `{id}-psyche` perch never re-registers / has a dead pid) within N seconds of spawn is treated as a host failure: stamp the parent perch `psyche_host_error{reason:"host not resident within <n>s (psyche perch missing/dead pid)"}` (and do not leave a phantom online nested perch). Closes the last masking gap the v0.8.1 fix left open. perri's F-010 (v0.8.1 dogfood). Sibling of REQ-HAZARD-LIVEHOST-BOOT-RACE.
  [OK] REQ-HAZARD-RC-ATTACH-FAILFAST  required: [impl, unit, int]  stages: -doc +impl +unit +int
       B1: `spt rc <id>` to a DEAD or non-streaming session fails fast with a clear message, never an INFINITE blank screen. Today rc.rs run_attach (209-231) + pump spawns PUMP_IPC_READER and blocks: the poll times out each slice but the stream never produces output, so the operator sees a permanent blank (operator: fresh wall-f attached, closed tab, then `spt rc wall-f` HUNG — the broker still resolved a session for it). FIX: (a) once B2 lands, gate attach on is_online/status — an offline endpoint yields a clean 'endpoint offline, start it' not an attach; (b) fail-fast — if the attach-open ack / first output does not arrive within a bound, surface a clear message, never an infinite blank; (c) the broker EOFs the attach stream when the session's child is dead, so rc's existing PumpEnd::BrokerGone graceful path (REQ-HAZARD-RC-EOF) catches it. PIN the exact sub-mechanism with a repro test FIRST (dead-session-lingers-in-broker vs reaped-but-rc-waits vs alive-resting-no-wake — the wall-f Windows tab-close: child alive-silent vs dead-not-reaped). (v0.12.0)
       A severed broker stream during a live rc session surfaces GRACEFULLY, never as a raw io error that crashes the PTY. The rc read-loop (rc.rs:352-362) continues only on WouldBlock/TimedOut; ANY other read_event_until error — including UnexpectedEof 'failed to fill whole buffer' — returns Err → RC_FAIL → the PTY 'crashes' from the user's view. Confirmed trigger: a deliberate `spt daemon stop` (broker bounce) severs an active rc (perri stopped the daemon to release owlery watch handles). Same severed-broker-stream EOF class as the v0.9.1 seed fix (seed_fail_message) and the listener-death case — spt-core must classify a broker-gone EOF and (a) surface a CLEAR actionable message ('daemon stopped/restarted — re-run / reconnect'), never the raw buffer error, and ideally (b) AUTO-REATTACH to the same session on the fresh broker (the broker is the daemon-lifetime anchor; it returns on the next `spt api` call). FOLD two side-observations: (1) `spt daemon stop` SILENTLY drops active rc/live sessions — warn ('N active session(s) will drop') or graceful-detach on stop; (2) the daemon holds owlery WATCH HANDLES on perch dirs so a torn-down perch dir stays 'Device busy' until a full daemon stop releases them (perri's rt-* cleanup) — a torn-down perch's handle should release without a daemon stop. doyle Finding C, root-caused. (post-v0.10.0)
  [OK] REQ-HAZARD-RC-INPUT-KEY-ENCODING  required: []  stages: -doc -impl -unit -int
       Concurrent SQLite openers (registry/spool) must not fail with 'database is locked' (4.7)
       Stale registry entries degrade to fallback, never hard-fail (4.3)
       Command-template substitution fills argv ELEMENTS, not a re-tokenized string: spt-core currently `fill_template`s {key} values INTO the command STRING and THEN `tokenize`s the filled string (runtime.rs:94/122), so a multi-word {key} value whitespace-SPLITS into multiple argv tokens unless the adapter hand-quotes the placeholder, and a value containing a `"` (or `;`) injects/breaks tokenization (shell-injection-adjacent). A filled value MUST become exactly ONE argv element regardless of spaces/quotes in the value. Fix: tokenize the TEMPLATE into argv FIRST, then `fill_template` EACH token, so a `{key}` slot resolves to a single element and the value never participates in tokenization (no whitespace-split, no quote/semicolon injection); preserve the missing-key / empty-command errors and `{{`/`}}` non-interpretation. perri's F-009 (v0.8.1 dogfood, argv-capture-confirmed): a multi-word `{psyche_prompt}` = "PSYCHE REVIVAL time: epoch-ms:… incoming event: (none)" arrived as argv[6..12] (7 stray tokens), the harness runner strict-parsed `--prompt` against the 2nd word, exited 2 within ~1s → phantom hosted perch. Applies to EVERY [session.<role>] template (psyche_init, extractor, notif, …); digest survives today only because its fills ({session_id}/{source}) are single-token.
       Native-PTY spawn must resolve a bare program name with PATHEXT precedence and run a non-PE target through its interpreter: portable-pty's own `which` takes the FIRST PATH match — an extensionless shebang shim (e.g. a node CLI `ccs` shipped beside `ccs.cmd`) — and CreateProcessW then rejects the non-PE file with os error 193 ('not a valid Win32 application'); spt-term resolves the program itself (PATHEXT order prefers .EXE over .CMD; .cmd/.bat → cmd.exe /d /c, .ps1 → powershell -NoProfile -File) so a bare harness/shell [session.self] command actually launches on Windows. Unix is a passthrough (execve honours the shebang).
       `spt daemon start` launches the daemon with NO visible console window. REGRESSION (v0.12.1 L1.5): the WMI job-neutral launch (spawn_daemon_via_wmi) set CREATE_NO_WINDOW on the launching powershell but NOT on the Win32_Process.Create call — Win32_Process.Create does not inherit it, so the spawned cmd.exe env-forwarding wrapper popped a console window on every cold-start (violating REQ-INSTALL-10's v0.7.4 no-persistent-window invariant; the old detached_no_inherit path set DETACHED_PROCESS|CREATE_NO_WINDOW). FIX: pass a Win32_ProcessStartup with CreateFlags=DETACHED_PROCESS (0x8 — no console so no window; CREATE_NO_WINDOW 0x08000000 is NOT a valid Win32_ProcessStartup flag → ReturnValue 21 invalid-param, which is why the naive port fails) + ShowWindow=SW_HIDE(0) belt, via the ProcessStartupInformation argument. (v0.12.2)
       Windows at-logon autostart runs the daemon in the background with no persistent window: the scheduled task launches `spt daemon start` (which spawn_detaches a console-less DETACHED_PROCESS daemon and exits) rather than the foreground `spt daemon run` — Task Scheduler's interactive ONLOGON launch of a long-lived console process otherwise leaves a visible console window for the daemon's whole lifetime (v0.7.4)
       File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = "rel/path" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings].
       [adapter] host_binaries declares the harness executable basenames a kind="harness" adapter hosts agents inside (e.g. host_binaries = ["claude"]); bind-time pid→exe-basename match (case-insensitive, .exe-stripped) over the seed's parent_pid selects the candidate adapter set; zero matches → a friendly error naming the binary + the --adapter escape hatch. Additive + N-1-safe: optional Vec<String>, #[serde(default, skip_serializing_if = "Vec::is_empty")] (omitted-serialized like shortcut_basename, old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). The match-key for ADR-0021 adapter-agnostic bind-time resolution. (v0.9.0)
       spt binary CLI surface: send/ring/ready(+--once)/list/stop/whoami, stable arg shapes + exit codes
       cross-node Gateway user-msg honored via advertised endpoint_type: a user-msg from a Gateway-typed origin survives the receive_wan funnel as user-msg (vs the fail-closed re-stamp), keyed on the QUIC-handshake-proven origin node (never wire `from`). Trust boundary = subnet membership (operator-ratified 2026-06-13); no defense against an in-subnet member forging the type. Instance.endpoint_type is an additive serde-default field extending REQ-INST-7's data model. Absent/unknown type → re-stamp (N-1 rollout grace)
       spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest key `[message-idle-translation-binary]` = a scalar binary PATH; spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:"init",endpoint_id,node}` first · `{type:"event",envelope:"<EVENT…>"}` per inbound message (ADR-0020 envelope) · `{type:"input"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. The v0.11.0 raw inject is the degenerate `{text:payload}{key:enter}` case. (v0.13.0)
       NTP TOTP offset: the pairing ceremony queries NTP at ceremony time (both sides) and applies the derived offset to the TOTP calculation in-process only; system-clock fallback when NTP is unreachable (offline LAN pairing unaffected — NTP failure never blocks a pairing that succeeds today); never sets the OS clock; no background sync loop (M8 decision 18; field trigger: enlyzeam clock >1 min off exceeds the ±1 window)
       The picker's project-history loader reads the git-backed context store, not the bare working tree: data.rs project_history_for enumerates an endpoint's projects via the BranchStore branch set (the context store keeps per-project context in git branches — contextstore::project_branch(project_id), checked out to projects/<project>/<id>/ only on-demand) instead of raw std::fs::read_dir over the empty working tree (which returned empty for ALL rows incl wall-a — the operator bug). Ordered newest→oldest by branch commit recency; degrades to empty (informational pane), never fails. (v0.10.0)
       An inbound `spt send` is DELIVERED to an spt-hosted endpoint (brought up via `spt endpoint run` → `api bind`, broker holds its PTY, NO `api listen` relay). Today cmd_bind→establish_perch (api/startup.rs ~441) writes info.json + ready marker + controllable=Some(true) but registers NO message-listener / NO address, so deliver.rs resolve_address→None→spool (deliver.rs:132-140) and the message NEVER reaches the live PTY — the endpoint reads 'online' (ready marker) yet `spt send` silently SPOOLS ('online but not deliverable' lie). Per CONTEXT:187-188 the daemon owns the PTY and delivers, manifest-configurable per activity-state (direct PTY injection / relay / HTTP). FIX: route an inbound send for an spt-hosted target through the daemon → broker InputReq → session.write_input PTY-inject (broker.rs dispatch_input/write_input ~988-1022), the same path the brain uses; the live-delivery handshake must report Sent (not Queued) and stop the spool-only fallback for a broker-hosted, PTY-resident endpoint. Detection is local: controllable==Some(true) + spt-hosted state + resolve_address==None. = the spt-core HALF of the wall-b finding (perri owns the adapter half: bind-hook fired-zero-perch + the missing endpoint-run int test). (post-v0.10.0)
       Shell sleep/wake: link-break always closes the binary (pre-close instruction + termination timeout), ephemeral teardown vs persistent offline/relink, wake_command wake-watcher (offline-only, exit-opcode supervision, exponential backoff + give-up), state-keyed wake resolution (dormant/suspended/active-elsewhere; no-reachable refuses — spawn-anywhere branch deferred), spt shutdown owner cascade + api owner-shutdown gated by can_shutdown (CONTEXT Shell sleep/wake)
       Adapter-agnostic harness-hosted seed + bind-time adapter/profile resolution (ADR-0021): `api seed` carries only parent_pid + session_id (+ optional cwd), no --adapter — a pure "a harness session exists at this pid" record; --adapter becomes an OPTIONAL override across the whole api group (an explicit name[:profile] for adapter dev, never required). Omitted, listen/poll resolve the owning adapter/profile AT BIND as a pure read against the live registry — never a seed-time snapshot that can drift: seed parent_pid → exe basename → host_binaries candidate set (REQ-MANIFEST-8) → active-profile pointer (REQ-INSTALL-12) primary, else greatest-registered_at_ms candidate base profile (name-asc tie) → friendly zero-match error. Covers BOTH LiveAgent (listen) and ReadyAgent (poll) bringup. Restores legacy parity: `$LIVE start <id>` → `$SPT listen <id>` with no mandatory --adapter, one generic SessionStart hook per harness binary. (v0.9.0)
       Trust lifecycle verbs, elevation-gated: spt subnet leave <NAME> (membership exit) and spt subnet prune <node> (removes a dead identity's trust + registry rows, killing its dead dials; trust mutation = security surface, REQ-PAIR-6 gate machinery) (M8 decisions 6-7)
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
EXIT=0


traceable-reqs **EXIT=0**, REQ-HAZARD-INJECT-CONTROL-COEXIST = `[OK] +impl +unit +int`. Check the lib-test + clippy result.


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


Peek lib-test/clippy interim output


(Bash completed with no output)


Lib-test + clippy still building. Waiting for completion notification.


<task-notification>
<task-id>buyjyrmub</task-id>
<tool-use-id>toolu_01RRFUCzcrXsWoSzXpyaBfSz</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\buyjyrmub.output</output-file>
<status>completed</status>
<summary>Background command "Run broker lib unit tests + clippy" 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.


1	test update::tests::plan_verified_update_set_uses_selected_platform_spec ... ok
2	test xfer::tests::resolve_under_confines_to_root ... ok
3	test xfer::tests::recv_state_chunks_idempotent_and_commit_replays_safely ... ok
4	test shellhost::tests::close_shell_delivers_pre_close_then_kills_and_flips_offline ... ok
5	test relay::tests::run_honors_stop ... ok
6	test resting::tests::daemon_rest_event_arms_gate_at_rest_edge_only ... ok
7	test shellwake::tests::watcher_crash_exits_respawn_then_give_up ... ok
8	test resting::tests::rest_edges_cascade_shells_with_divergence ... ok
9	test seedmap::tests::put_refreshes_existing ... ok
10	test seedmap::tests::put_then_take_consumes_once ... ok
11	test seedmap::tests::request_stop_barrier_holds_until_no_listener ... ok
12	test seedmap::tests::stop_op_acks_then_serve_returns ... ok
13	test seedmap::tests::take_absent_is_none ... ok
14	test serveprobe::tests::is_serving_subnet_honors_membership_and_detach ... ok
15	test shellwake::tests::resolve_wake_revives_a_suspended_owner_without_double_launch ... ok
16	test sync::tests::reconcile_driver_structurally_excludes_live_role ... ok
17	test sync::tests::select_refs_scopes_two_tiers ... ok
18
19	test result: ok. 334 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.64s
20
21	===CLIPPY===
22	    Checking spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
23	    Checking n0-future v0.3.2
24	    Checking tempfile v3.27.0
25	    Checking backon v1.6.0
26	    Checking n0-watcher v0.6.1
27	    Checking iroh-dns v0.98.0
28	    Checking netwatch v0.16.0
29	    Checking iroh-relay v0.98.0
30	    Checking portmapper v0.16.0
31	    Checking spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
32	    Checking spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
33	    Checking spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
34	    Checking iroh v0.98.2
35	    Checking spt-live v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
36	    Checking spt-net v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
37	    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
38	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.47s
39

test update::tests::plan_verified_update_set_uses_selected_platform_spec ... ok
test xfer::tests::resolve_under_confines_to_root ... ok
test xfer::tests::recv_state_chunks_idempotent_and_commit_replays_safely ... ok
test shellhost::tests::close_shell_delivers_pre_close_then_kills_and_flips_offline ... ok
test relay::tests::run_honors_stop ... ok
test resting::tests::daemon_rest_event_arms_gate_at_rest_edge_only ... ok
test shellwake::tests::watcher_crash_exits_respawn_then_give_up ... ok
test resting::tests::rest_edges_cascade_shells_with_divergence ... ok
test seedmap::tests::put_refreshes_existing ... ok
test seedmap::tests::put_then_take_consumes_once ... ok
test seedmap::tests::request_stop_barrier_holds_until_no_listener ... ok
test seedmap::tests::stop_op_acks_then_serve_returns ... ok
test seedmap::tests::take_absent_is_none ... ok
test serveprobe::tests::is_serving_subnet_honors_membership_and_detach ... ok
test shellwake::tests::resolve_wake_revives_a_suspended_owner_without_double_launch ... ok
test sync::tests::reconcile_driver_structurally_excludes_live_role ... ok
test sync::tests::select_refs_scopes_two_tiers ... ok

test result: ok. 334 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 15.64s

===CLIPPY===
    Checking spt-store v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-store)
    Checking n0-future v0.3.2
    Checking tempfile v3.27.0
    Checking backon v1.6.0
    Checking n0-watcher v0.6.1
    Checking iroh-dns v0.98.0
    Checking netwatch v0.16.0
    Checking iroh-relay v0.98.0
    Checking portmapper v0.16.0
    Checking spt-msg v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-msg)
    Checking spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
    Checking spt-runtime v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-runtime)
    Checking iroh v0.98.2
    Checking spt-live v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-live)
    Checking spt-net v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.47s


All green: **334 lib tests pass** (incl. 2 new W1 units), **clippy `-D warnings` clean**, traceable EXIT=0. Now run the real-daemon e2e (dummy_harness + attach_wedge) — they exercise become_controller/append through a real detached daemon + rc, the closest to doyle's "real fixture" gate.


cargo test -p spt --test dummy_harness_e2e --test attach_wedge_e2e -- --test-threads=1 2>&1 | tail -35

Run real-daemon dummy-harness + attach-wedge e2e


Command running in background with ID: bz8ukgxqo. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\bz8ukgxqo.output. You will be notified when it completes. To check interim output, use Read on that file path.


E2e running. While waiting — let me reconsider (b)/(c): they're Unix-only and unconfirmed on Linux locally; the proper bound is W2's unified PTY-writer substrate. Repro-first says don't add an unverified PTY-write change. I'll propose deferring (b)/(c) to W2 in the gate ping and let doyle rule. Check e2e result.


until grep -qE "test result:|error\[|error:|panicked|PRECONDITION" "C:/Users/decid/AppData/Local/Temp/claude/C--Users-decid-Documents-projects-spt-core/73618172-412e-4954-b8aa-e2c8955dfd49/tasks/bz8ukgxqo.output" 2>/dev/null; do sleep 5; done; echo DONE

Wait for e2e completion


Command running in background with ID: byedvaay4. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\byedvaay4.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>bz8ukgxqo</task-id>
<tool-use-id>toolu_011Do4bxmyd5jb4yfruDknVm</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\bz8ukgxqo.output</output-file>
<status>completed</status>
<summary>Background command "Run real-daemon dummy-harness + attach-wedge e2e" completed (exit code 0)</summary>
</task-notification>


<task-notification>
<task-id>byedvaay4</task-id>
<tool-use-id>toolu_01E4eSoE5toSZ8d8v3vLGRmK</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\73618172-412e-4954-b8aa-e2c8955dfd49\tasks\byedvaay4.output</output-file>
<status>completed</status>
<summary>Background command "Wait for e2e completion" completed (exit code 0)</summary>
</task-notification>


Waiting on the e2e run; I'll be notified.


1	   Compiling spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
2	   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
3	   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
4	    Finished `test` profile [unoptimized + debuginfo] target(s) in 12.54s
5	     Running tests\attach_wedge_e2e.rs (target\debug\deps\attach_wedge_e2e-893bfd290daafaaa.exe)
6
7	running 1 test
8	test attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker ... ok
9
10	test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.51s
11
12	     Running tests\dummy_harness_e2e.rs (target\debug\deps\dummy_harness_e2e-5e4f1725b5b9612c.exe)
13
14	running 1 test
15	test endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches ... ok
16
17	test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.95s
18
19

   Compiling spt-term v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-term)
   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 12.54s
     Running tests\attach_wedge_e2e.rs (target\debug\deps\attach_wedge_e2e-893bfd290daafaaa.exe)

running 1 test
test attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 7.51s

     Running tests\dummy_harness_e2e.rs (target\debug\deps\dummy_harness_e2e-5e4f1725b5b9612c.exe)

running 1 test
test endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 2.95s


Both real-daemon e2e green (attach_wedge + dummy_harness rc-attach). Full W1 verification complete. Check exact working-tree changes before committing.


cd C:/Users/decid/Documents/projects/spt-core && git status --short && echo "===DIFFSTAT===" && git diff --stat

Git status + diffstat


 M crates/spt-daemon/src/broker.rs
 M crates/spt/src/rc.rs
 M traceable-reqs.toml
?? crates/spt-daemon/tests/inject_control_wedge.rs
===DIFFSTAT===
 crates/spt-daemon/src/broker.rs | 435 ++++++++++++++++++++++++++++++++++------
 crates/spt/src/rc.rs            |  10 +
 traceable-reqs.toml             |   2 +-
 3 files changed, 382 insertions(+), 65 deletions(-)


Clean: pty.rs/reader.rs fully reverted (not in diff). Changes are broker.rs (W1 fix), inject_control_wedge.rs (int gate), traceable-reqs.toml (activation), rc.rs (W7 confirm-hexdump). Commit W1 and the W7 diagnostic separately.


cd C:/Users/decid/Documents/projects/spt-core && git rev-parse --abbrev-ref HEAD && git add crates/spt-daemon/src/broker.rs crates/spt-daemon/tests/inject_control_wedge.rs traceable-reqs.toml && git commit -q -m @'
fix(broker): W1 — controller delivery off the drain thread (no more wedge)

REQ-HAZARD-INJECT-CONTROL-COEXIST root, confirmed repro-first on a REAL
broker + REAL PTY (no mocks): OutputLog::append fanned each output chunk to
the controller via a SYNCHRONOUS inline write_frame ON the session drain
thread while holding Mutex<OutputLog>. A backed-up controller socket (slow
operator, or the full 64KB rc loopback duplex under heavy TUI redraw) parked
the drain WITH THE LOG LOCK HELD -> permanent per-session wedge: output and
echo stalled and every attach / KIND_SESSIONS that needs the log lock blocked
forever, while the broker process stayed alive. Captured: APPEND_CTRL_ENTER
seq=2 with no EXIT, net_status answered but KIND_SESSIONS hung.

Fix (the viewer_writer pattern, but authoritative): controller delivery moves
onto a dedicated writer thread + bounded channel. The drain hands each chunk
off via a bounded, OFF-LOCK send (ControllerJob::deliver, 5s deadline -> detach
+ clear_controller, never park forever, never silent-evict a live operator).
The writer thread does the blocking socket write and advances the authoritative
delivered_through cursor (now Arc<AtomicU64>, advance_delivered/advanced_cursor
monotonic). Ring replay becomes the writer's initial batch (subsumes the
inline-replay block, REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT Option-b). Displaced
notices ride the writer channel (CtrlMsg::Control). clear_controller is the one
unlatch path W5's reconcile self-heal will reuse.

Parks (b) write_input write_all and (c) DSR-answer writer-mutex contention
confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix
forkpty -> the defensive PTY-write bound folds into W2's atomic-write substrate.

int gate (inject_control_wedge.rs): a fully-backed-up controller keeps
KIND_SESSIONS answering = no wedge (pre-fix: hung). unit: deliver is bounded
(room/disconnected/deadline) + advance_delivered monotonic. 334 broker lib
tests, dummy_harness_e2e + attach_wedge_e2e, spt-term suite, clippy -D warnings,
traceable EXIT=0 all green.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP
'@ && echo "===W1 COMMITTED===" && git log --oneline -1

Commit W1 fix


Error: Exit code 2
/usr/bin/bash: eval: line 62: syntax error near unexpected token `('
/usr/bin/bash: eval: line 62: `monotonic). Ring replay becomes the writer's initial batch (subsumes the'


Heredoc `@'...'@` is PowerShell, not bash. Write the message to a file and use `git commit -F`.


Write

fix(broker): W1 — controller delivery off the drain thread (no more wedge)

REQ-HAZARD-INJECT-CONTROL-COEXIST root, confirmed repro-first on a REAL
broker + REAL PTY (no mocks): OutputLog::append fanned each output chunk to
the controller via a SYNCHRONOUS inline write_frame ON the session drain
thread while holding Mutex<OutputLog>. A backed-up controller socket (slow
operator, or the full 64KB rc loopback duplex under heavy TUI redraw) parked
the drain WITH THE LOG LOCK HELD -> permanent per-session wedge: output and
echo stalled and every attach / KIND_SESSIONS that needs the log lock blocked
forever, while the broker process stayed alive. Captured: APPEND_CTRL_ENTER
seq=2 with no EXIT, net_status answered but KIND_SESSIONS hung.

Fix (the viewer_writer pattern, but authoritative): controller delivery moves
onto a dedicated writer thread + bounded channel. The drain hands each chunk
off via a bounded, OFF-LOCK send (ControllerJob::deliver, 5s deadline -> detach
+ clear_controller, never park forever, never silent-evict a live operator).
The writer thread does the blocking socket write and advances the authoritative
delivered_through cursor (now Arc<AtomicU64>, advance_delivered/advanced_cursor
monotonic). Ring replay becomes the writer's initial batch (subsumes the
inline-replay block, REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT Option-b). Displaced
notices ride the writer channel (CtrlMsg::Control). clear_controller is the one
unlatch path W5's reconcile self-heal will reuse.

Parks (b) write_input write_all and (c) DSR-answer writer-mutex contention
confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix
forkpty -> the defensive PTY-write bound folds into W2's atomic-write substrate.

int gate (inject_control_wedge.rs): a fully-backed-up controller keeps
KIND_SESSIONS answering = no wedge (pre-fix: hung). unit: deliver is bounded
(room/disconnected/deadline) + advance_delivered monotonic. 334 broker lib
tests, dummy_harness_e2e + attach_wedge_e2e, spt-term suite, clippy -D warnings,
traceable EXIT=0 all green.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP


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

fix(broker): W1 — controller delivery off the drain thread (no more wedge)

REQ-HAZARD-INJECT-CONTROL-COEXIST root, confirmed repro-first on a REAL
broker + REAL PTY (no mocks): OutputLog::append fanned each output chunk to
the controller via a SYNCHRONOUS inline write_frame ON the session drain
thread while holding Mutex<OutputLog>. A backed-up controller socket (slow
operator, or the full 64KB rc loopback duplex under heavy TUI redraw) parked
the drain WITH THE LOG LOCK HELD -> permanent per-session wedge: output and
echo stalled and every attach / KIND_SESSIONS that needs the log lock blocked
forever, while the broker process stayed alive. Captured: APPEND_CTRL_ENTER
seq=2 with no EXIT, net_status answered but KIND_SESSIONS hung.

Fix (the viewer_writer pattern, but authoritative): controller delivery moves
onto a dedicated writer thread + bounded channel. The drain hands each chunk
off via a bounded, OFF-LOCK send (ControllerJob::deliver, 5s deadline -> detach
+ clear_controller, never park forever, never silent-evict a live operator).
The writer thread does the blocking socket write and advances the authoritative
delivered_through cursor (now Arc<AtomicU64>, advance_delivered/advanced_cursor
monotonic). Ring replay becomes the writer's initial batch (subsumes the
inline-replay block, REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT Option-b). Displaced
notices ride the writer channel (CtrlMsg::Control). clear_controller is the one
unlatch path W5's reconcile self-heal will reuse.

Parks (b) write_input write_all and (c) DSR-answer writer-mutex contention
confirmed BENIGN on Windows ConPTY (absorbs a 4MiB inject), real only on Unix
forkpty -> the defensive PTY-write bound folds into W2's atomic-write substrate.

int gate (inject_control_wedge.rs): a fully-backed-up controller keeps
KIND_SESSIONS answering = no wedge (pre-fix: hung). unit: deliver is bounded
(room/disconnected/deadline) + advance_delivered monotonic. 334 broker lib
tests, dummy_harness_e2e + attach_wedge_e2e, spt-term suite, clippy -D warnings,
traceable EXIT=0 all green.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP


Bash

cd C:/Users/decid/Documents/projects/spt-core && git add crates/spt-daemon/src/broker.rs crates/spt-daemon/tests/inject_control_wedge.rs traceable-reqs.toml && git commit -q -F .git/W1_MSG.txt && rm -f .git/W1_MSG.txt && echo "===W1 COMMITTED===" && git log --oneline -1

Commit W1 fix via message file


warning: in the working copy of 'crates/spt-daemon/tests/inject_control_wedge.rs', LF will be replaced by CRLF the next time Git touches it
===W1 COMMITTED===
8b5583e fix(broker): W1 — controller delivery off the drain thread (no more wedge)


W1 committed (8b5583e). Now the W7 confirm-instrument (rc.rs hexdump) as a separate commit.


Write

chore(rc): W7 confirm-first — env-gated stdin keystroke hexdump

REQ-HAZARD-RC-INPUT-KEY-ENCODING gate-the-root diagnostic (not the fix): with
SPT_RC_DEBUG_KEYS set, spawn_stdin_reader prints the RAW bytes the console
delivered for each keystroke (RC_KEYS n=.. bytes=[..]). The operator presses
Backspace then ctrl+Backspace in a real rc session; a Backspace arriving as
0x08 (^H) instead of 0x7f (VT DEL) confirms the legacy-console root
(ENABLE_VIRTUAL_TERMINAL_INPUT off on Windows crossterm raw mode). The fix
(enable VT input) lands only after the byte is captured.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP


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

chore(rc): W7 confirm-first — env-gated stdin keystroke hexdump

REQ-HAZARD-RC-INPUT-KEY-ENCODING gate-the-root diagnostic (not the fix): with
SPT_RC_DEBUG_KEYS set, spawn_stdin_reader prints the RAW bytes the console
delivered for each keystroke (RC_KEYS n=.. bytes=[..]). The operator presses
Backspace then ctrl+Backspace in a real rc session; a Backspace arriving as
0x08 (^H) instead of 0x7f (VT DEL) confirms the legacy-console root
(ENABLE_VIRTUAL_TERMINAL_INPUT off on Windows crossterm raw mode). The fix
(enable VT input) lands only after the byte is captured.

Co-authored by: todlando
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVVByRoZnz85acTkVJ1uAP


Bash

cd C:/Users/decid/Documents/projects/spt-core && git add crates/spt/src/rc.rs && git commit -q -F .git/W7_MSG.txt && rm -f .git/W7_MSG.txt && echo "===W7 DIAG COMMITTED===" && git log --oneline -3

Commit W7 confirm-hexdump


===W7 DIAG COMMITTED===
f05566b chore(rc): W7 confirm-first — env-gated stdin keystroke hexdump
8b5583e fix(broker): W1 — controller delivery off the drain thread (no more wedge)
0fcde06 v0.13.0 /diagnose: confirm W1 controller-blocking root + mint rc-backspace REQ


W1 (8b5583e) + W7 diagnostic (f05566b) committed. Ping doyle for the gate.


$OWL send doyle <<'EOF'
W1 FIX UP + GREEN — committed 8b5583e on v0.13.0-delivery-control. Gate it.

WHAT: controller delivery moved OFF the drain thread onto a dedicated writer thread + bounded channel (the viewer_writer pattern, authoritative):
- append() no longer writes the controller inline; it returns a ControllerJob and releases the log lock. The drain delivers via ControllerJob::deliver — a bounded OFF-LOCK send (5s CONTROLLER_WRITE_DEADLINE → detach via the shared clear_controller; never park forever; never silent-evict a live operator).
- controller_writer thread does the blocking socket write + advances the authoritative delivered_through (now Arc<AtomicU64>, advance_delivered/advanced_cursor monotonic, CAS-safe through a displace overlap).
- Ring replay is now the writer's initial batch → SUBSUMES the inline-replay block (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT Option-b deeper root). Displaced/exit ride the channel (CtrlMsg::Control).
- clear_controller = the ONE unlatch path (clears driven_by, drops the sink) — W5's reconcile self-heal reuses it (our synergy).

VERIFY (all green, Windows):
- int gate inject_control_wedge.rs flipped to assert FIXED: a fully-backed-up controller keeps KIND_SESSIONS answering = NO wedge (pre-fix hung). [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
- unit: controller_deliver_is_bounded_never_a_permanent_park (room/disconnected/deadline) + advance_delivered_is_monotonic. [unit->...]
- impl tags on append/controller_writer. REQ ACTIVATED [impl,unit,int].
- 334 broker lib tests; broker/attach/dispatch/resume/brain_swap/idempotent; dummy_harness_e2e + attach_wedge_e2e (real detached daemon + rc); spt-term suite; clippy -D warnings clean; traceable-reqs EXIT=0.

(b)/(c) PROPOSAL — your ruling: they're BENIGN on Windows (operator platform; ConPTY absorbs a 4MiB inject) and unconfirmed-reproduced on Linux locally. Per repro-first I did NOT add an unverified PTY-write change. The proper bound IS W2's unified "spt-core owns every PTY write, applied atomically" substrate (one writer queue → no caller blocks, no shared-mutex contention) — design-once-there. Proposal: land W1 (a) now; confirm (b)/(c) on gravity-linux + bound them as part of W2. OK, or do you want the Unix-only PTY-write bound inside W1?

W7: confirm-hexdump committed f05566b (chore, no fix, env-gated). Operator can capture now (you teed them up) — relay the rckeys.log hex and I'll land the VT-input fix.
EOF

Ping doyle: W1 fix committed + green, gate request


➜ SENT:doyle


Pinged doyle for the gate.

## W1 done — keystone fixed, repro-first, all green

**Root (confirmed on real broker + real PTY, no mocks):** `OutputLog::append` wrote the controller inline on the drain thread holding `Mutex<OutputLog>`. Backed-up controller socket (slow operator / full 64KB rc duplex under TUI redraw) → drain parked **with the log lock held** → permanent per-session wedge.

**Fix:** controller delivery moved off the drain thread → dedicated writer thread + bounded channel (viewer_writer pattern, but authoritative — preserves `delivered_through`). Drain hands off via a bounded **off-lock** send (5s deadline → detach, never park forever, never silent-evict a live operator). Ring replay rides the writer thread too (kills the inline-replay block as a bonus).

**Verified (Windows):** int gate flipped to no-wedge ✓ · 334 broker lib tests ✓ · broker/attach/dispatch/resume/brain_swap/idempotent ✓ · real-daemon dummy_harness + attach_wedge e2e ✓ · spt-term ✓ · clippy `-D warnings` ✓ · traceable EXIT=0, REQ `[impl,unit,int]` ✓

**Committed:** `8b5583e` (W1) + `f05566b` (W7 confirm-hexdump). Pinged doyle to gate.

**(b)/(c)** (input-side parks) — benign on Windows (ConPTY absorbs), Unix-forkpty-only; proposed folding the defensive bound into W2's unified PTY-writer. doyle to rule.

**W7 still needs your hands** — the Backspace byte capture (doyle's teeing it up in his session). When you run it:
```
$env:SPT_RC_DEBUG_KEYS=1
C:\Users\decid\Documents\projects\spt-core\target\debug\spt.exe rc <id> 2> rckeys.log
```
press **Backspace**, then **Ctrl+Backspace**, detach, send me the `RC_KEYS` lines. Then I land the VT-input fix.


<command-name>/agents</command-name>
            <command-message>agents</command-message>
            <command-args></command-args>


<local-command-stdout>Agents dialog dismissed</local-command-stdout>


<local-command-caveat>Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.</local-command-caveat>


<command-name>/reload-skills</command-name>
            <command-message>reload-skills</command-message>
            <command-args></command-args>


<local-command-stdout>Reloaded skills: 167 skills available (no changes)</local-command-stdout>


<command-name>/reload-plugins</command-name>
            <command-message>reload-plugins</command-message>
            <command-args></command-args>


<local-command-stdout>Reloaded: 7 plugins · 0 skills · 39 agents · 15 hooks · 1 plugin MCP server · 1 plugin LSP server</local-command-stdout>


<task-notification>
<task-id>bv8r78fk1</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="14c8d474" type="msg" from="doyle">W1 (a) GATE-PASS. Verified on-machine, not on the relay:
- Read the 8b5583e diff: append→Option&lt;ControllerJob&gt; releases the log lock; controller_writer does the blocking write off-lock; ControllerJob::deliver bounded (5s deadline→clear_controller, never park, never silent-evict); delivered_through now Arc&l</EVENT-PART>
<EVENT-PART seq="2/7" id="14c8d474">t;AtomicU64&gt; advanced only-on-success + monotonic; controller_epoch guards a stale deadline-evict through a displace overlap; clear_controller = the one unlatch (W5 reuses). Correct + matches every gate condition.
- Int test is NON-VACUOUS: renamed a_backed_up_controller_does_not_wedge_the_session, flipped to assert sessions_answered=tru</EVENT-PART>
<EVENT-PART seq="3/7" id="14c8d474">e (no wedge) — fails if the bug returns.
- Ran it HERE (Windows): 2 passed/0 failed (park-b inject_acked=true = benign as designed). cargo clippy --workspace --all-targets -D warnings (exact CI form) = clean. REQ activated [impl,unit,int], traceable EXIT=0.

(b)/(c) RULING: APPROVED — land W1(a) now, defer the Unix park bound to W</EVENT-PART>
<EVENT-PART seq="4/7" id="14c8d474">2. Your repro-first call is right (don't add an unverified PTY-write change for a Windows-benign, Linux-unconfirmed park). CONDITIONS: (1) keep the cfg(unix) park-(b) assertion GREEN on gravity-linux CI — it proves the park is real + keeps it tracked; (2) W2 explicitly OWNS the bound — I added that to REQ-MSG-IDLE-TRANSLATION-BINARY @66dad</EVENT-PART>
<EVENT-PART seq="5/7" id="14c8d474">ef so it's not a silent gap. The unified atomic-write substrate bounds it there.

INSTALL GAP (heads-up, not a gate fail): the W7 hexdump (f05566b) AND the W1 fix are NOT in the operator's PATH binary — C:\...\spt-core\bin\spt.exe is stale (06-18 23:25, pre-commit). The operator's empty rckeys.log was that stale binary. I pointed them </EVENT-PART>
<EVENT-PART seq="6/7" id="14c8d474">at target\debug\spt.exe for the W7 capture. For the eventual ship: the release/install step must rebuild+install — but DON'T install over a running daemon's spt.exe (Windows lock); coordinate via daemon stop or the release path.

W1 banked. Next: the operator's W7 byte (incoming), then W3/W4/W5 dispatch — W5 is the thin clear_control</EVENT-PART>
<EVENT-PART seq="7/7" id="14c8d474">ler-reuse confirm. Ping me on W7 once you have the byte.</EVENT-PART></event>
</task-notification>