206	                                }
207	                                // Outcome not yet known — buffer; flushed on a
208	                                // controller confirmation, discarded on busy/viewer.
209	                                None => pending_input.push((op_id, bytes)),
210	                                // A viewer never reaches here (intent gate).
211	                                Some(ServeRole::Viewer) => {}
212	                            }
213	                        }
214	                        AttachRecord::Resize { rows, cols }
215	                            if attached && intent != AttachIntent::Viewer =>
216	                        {
217	                            // Controller → PTY resize (controller-EXCLUSIVE; the
218	                            // broker rejects it on a viewer's stream). A viewer's
219	                            // Resize is dropped here by the intent gate too.
220	                            let _ = brain.resize(rows, cols);
221	                        }
222	                        // Input/Resize before Request, a viewer's input/resize,
223	                        // or target-direction records echoed back: noise — ignore.
224	                        _ => {}
225	                    }
226	                }
227	            }
228
229	            // ── Broker → loop: the resolved controller/viewer role. ─────────
230	            BrokerEvent::Subscribed { outcome, .. } => {
231	                use crate::msg::SubscribeOutcome as O;
232	                match outcome {
233	                    O::Controller | O::TookControl => {
234	                        role = Some(ServeRole::Controller);
235	                        // A driver attached — the wake edge (controller-only;
236	                        // idempotent on an already-active seat).
237	                        feed_rest(endpoint, crate::resting::RestEvent::Wake);
238	                        // Flush any input that raced ahead of the reply.
239	                        for (op, b) in pending_input.drain(..) {
240	                            brain.send_effect(op, &b)?;
241	                        }
242	                    }
243	                    O::Viewer => {
244	                        role = Some(ServeRole::Viewer);
245	                        // Wake-neutral: a viewer never wakes the endpoint, never
246	                        // keeps it awake (Gap B). Discard any racing input.
247	                        pending_input.clear();
248	                    }
249	                    O::BusyControlled { .. } => {
250	                        // A different operator controls it — refuse (no PTY input
251	                        // ever reached it; the buffer is discarded). The rc
252	                        // renders the busy guidance client-side from driven_by.
253	                        wire.net_stream_send(stream_id, &[], None, true)?;
254	                        return Ok(AttachServeOutcome::Refused);
255	                    }
256	                }
257	            }
258	            // Viewer letterbox feed: the current PTY size (on attach + resize).
259	            BrokerEvent::Size { rows, cols, .. } if attached => {
260	                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });
261	                wire.net_stream_send(stream_id, &line, None, false)?;
262	            }
263	            // This controller was kicked by a different operator's `--take`.
264	            BrokerEvent::Displaced { by, .. } if attached => {
265	                let line = ndjson::encode_line(&AttachRecord::Displaced { by });
266	                wire.net_stream_send(stream_id, &line, None, true)?;
267	                // No dormant edge: the endpoint stays driven by the taker (whose
268	                // own serve loop already fired the wake).
269	                return Ok(AttachServeOutcome::Displaced);
270	            }
271
272	            // The operator closed its send side: the viewport is over.
273	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
274	                wire.net_stream_send(stream_id, &[], None, true)?;
275	                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes
276	                // info.json, carrying the current driven_by through), THEN release
277	                // the role over IPC so the broker's driven_by=None is the LAST
278	                // write — otherwise a concurrent resting RMW restores the stale
279	                // marker (the clear-race the broker-single-writer model averts).
280	                if role == Some(ServeRole::Controller) {
281	                    // The driver detached — the endpoint rests warm (controller-
282	                    // only; a viewer leaving is wake-neutral).
283	                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
284	                }
285	                if let Some(sid) = attached_session {
286	                    let _ = brain.detach_session(sid); // release the role (clears markers)
287	                }
288	                return Ok(AttachServeOutcome::Detached);
289	            }
290
291	            // ── Session → wire: output and exit. ────────────────────────────
292	            BrokerEvent::Output { seq, bytes, .. } if attached => {
293	                let line = ndjson::encode_line(&AttachRecord::Output {
294	                    seq,
295	                    data_b64: encode_bytes(&bytes),
296	                });
297	                // Unjournaled by design (see module docs): `seq` rides inside
298	                // the record, and the operator's render cursor is the dedup.
299	                wire.net_stream_send(stream_id, &line, None, false)?;
300	            }

                                }
                                // Outcome not yet known — buffer; flushed on a
                                // controller confirmation, discarded on busy/viewer.
                                None => pending_input.push((op_id, bytes)),
                                // A viewer never reaches here (intent gate).
                                Some(ServeRole::Viewer) => {}
                            }
                        }
                        AttachRecord::Resize { rows, cols }
                            if attached && intent != AttachIntent::Viewer =>
                        {
                            // Controller → PTY resize (controller-EXCLUSIVE; the
                            // broker rejects it on a viewer's stream). A viewer's
                            // Resize is dropped here by the intent gate too.
                            let _ = brain.resize(rows, cols);
                        }
                        // Input/Resize before Request, a viewer's input/resize,
                        // or target-direction records echoed back: noise — ignore.
                        _ => {}
                    }
                }
            }

            // ── Broker → loop: the resolved controller/viewer role. ─────────
            BrokerEvent::Subscribed { outcome, .. } => {
                use crate::msg::SubscribeOutcome as O;
                match outcome {
                    O::Controller | O::TookControl => {
                        role = Some(ServeRole::Controller);
                        // A driver attached — the wake edge (controller-only;
                        // idempotent on an already-active seat).
                        feed_rest(endpoint, crate::resting::RestEvent::Wake);
                        // Flush any input that raced ahead of the reply.
                        for (op, b) in pending_input.drain(..) {
                            brain.send_effect(op, &b)?;
                        }
                    }
                    O::Viewer => {
                        role = Some(ServeRole::Viewer);
                        // Wake-neutral: a viewer never wakes the endpoint, never
                        // keeps it awake (Gap B). Discard any racing input.
                        pending_input.clear();
                    }
                    O::BusyControlled { .. } => {
                        // A different operator controls it — refuse (no PTY input
                        // ever reached it; the buffer is discarded). The rc
                        // renders the busy guidance client-side from driven_by.
                        wire.net_stream_send(stream_id, &[], None, true)?;
                        return Ok(AttachServeOutcome::Refused);
                    }
                }
            }
            // Viewer letterbox feed: the current PTY size (on attach + resize).
            BrokerEvent::Size { rows, cols, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Size { rows, cols });
                wire.net_stream_send(stream_id, &line, None, false)?;
            }
            // This controller was kicked by a different operator's `--take`.
            BrokerEvent::Displaced { by, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Displaced { by });
                wire.net_stream_send(stream_id, &line, None, true)?;
                // No dormant edge: the endpoint stays driven by the taker (whose
                // own serve loop already fired the wake).
                return Ok(AttachServeOutcome::Displaced);
            }

            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {
                wire.net_stream_send(stream_id, &[], None, true)?;
                // ORDER MATTERS: feed the resting edge FIRST (it read-modify-writes
                // info.json, carrying the current driven_by through), THEN release
                // the role over IPC so the broker's driven_by=None is the LAST
                // write — otherwise a concurrent resting RMW restores the stale
                // marker (the clear-race the broker-single-writer model averts).
                if role == Some(ServeRole::Controller) {
                    // The driver detached — the endpoint rests warm (controller-
                    // only; a viewer leaving is wake-neutral).
                    feed_rest(endpoint, crate::resting::RestEvent::Detach);
                }
                if let Some(sid) = attached_session {
                    let _ = brain.detach_session(sid); // release the role (clears markers)
                }
                return Ok(AttachServeOutcome::Detached);
            }

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });
                // Unjournaled by design (see module docs): `seq` rides inside
                // the record, and the operator's render cursor is the dedup.
                wire.net_stream_send(stream_id, &line, None, false)?;
            }


check your ongoing Monitor tool call's output


render_until asserts strict contiguity on the operator's stream; serve_attach forwards exactly the seq life2's `read_event` delivers (line 292-299). So a skip in life2's snap-above → operator gap. My no-skip proof keeps holding on paper, yet it fails 33% — so my model of the topology or the epoch-gate engagement is wrong. Reading the exact stream wiring (stream_a vs stream_b, operator cursor init):


626	}
627
628	// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
629	// crash mid-attach: the successor re-serves the SAME broker-held stream from
630	// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
631	// dedup through the journals, input B sent INTO the dead window arrives, and
632	// B's viewport holds every marker exactly once.
633	// [unit->REQ-INST-8]
634	#[test]
635	fn attach_survives_target_brain_restart_exactly_once() {
636	    init_home();
637	    let dir = tempfile::tempdir().expect("tempdir");
638	    let (name_a, name_b) = (unique_name(), unique_name());
639	    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
640	    let _broker_b = net_broker(&name_b, &dir.path().join("b"));
641
642	    // Target A life 1: spawn the child; operator B dials + requests + types.
643	    let mut life1 = connect_retry(&name_a);
644	    let sid = life1
645	        .spawn_session(echo_spawn_req())
646	        .expect("spawn echo child");
647	    let a_addr = life1.net_status().expect("a status").addr;
648
649	    let mut operator = connect_retry(&name_b);
650	    let conn = operator.net_dial(a_addr, Some(1)).expect("dial");
651	    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, 2, AttachIntent::Control).expect("request");
652	    operator
653	        .net_stream_subscribe(stream_b, 0)
654	        .expect("operator subscribe");
655	    send_attach_input(&mut operator, stream_b, b"MARKER_ONE\r", 10).expect("input 1");
656
657	    // Life 1 serves manually (the serve loop's exact steps, stepped so the
658	    // crash point is deterministic): handle the Request + input, pump output
659	    // records until MARKER_ONE went onto the wire — then die.
660	    let (stream_a, origin) = wait_for_stream(&mut life1);
661	    life1
662	        .net_stream_subscribe(stream_a, 0)
663	        .expect("life1 subscribe");
664	    let mut decoder = AttachDecoder::new();
665	    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire

}

// [unit->REQ-HAZARD-RESTART-IDEMPOTENT] the viewport survives a target-brain
// crash mid-attach: the successor re-serves the SAME broker-held stream from
// seq 0 (worst-case stale cursor) — the replayed Request/input/output all
// dedup through the journals, input B sent INTO the dead window arrives, and
// B's viewport holds every marker exactly once.
// [unit->REQ-INST-8]
#[test]
fn attach_survives_target_brain_restart_exactly_once() {
    init_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let (name_a, name_b) = (unique_name(), unique_name());
    let _broker_a = net_broker(&name_a, &dir.path().join("a"));
    let _broker_b = net_broker(&name_b, &dir.path().join("b"));

    // Target A life 1: spawn the child; operator B dials + requests + types.
    let mut life1 = connect_retry(&name_a);
    let sid = life1
        .spawn_session(echo_spawn_req())
        .expect("spawn echo child");
    let a_addr = life1.net_status().expect("a status").addr;

    let mut operator = connect_retry(&name_b);
    let conn = operator.net_dial(a_addr, Some(1)).expect("dial");
    let stream_b = request_attach(&mut operator, conn.conn_id, sid, 0, 2, AttachIntent::Control).expect("request");
    operator
        .net_stream_subscribe(stream_b, 0)
        .expect("operator subscribe");
    send_attach_input(&mut operator, stream_b, b"MARKER_ONE\r", 10).expect("input 1");

    // Life 1 serves manually (the serve loop's exact steps, stepped so the
    // crash point is deterministic): handle the Request + input, pump output
    // records until MARKER_ONE went onto the wire — then die.
    let (stream_a, origin) = wait_for_stream(&mut life1);
    life1
        .net_stream_subscribe(stream_a, 0)
        .expect("life1 subscribe");
    let mut decoder = AttachDecoder::new();
    let mut sent: Vec<u8> = Vec::new(); // bytes life 1 pumped onto the wire


The bug is a real residual reorder at the operator stream, pre-existing but unmasked by P1c's brain-side fix. My no-skip proof holds on paper for shared-mutex ascending writers — so either the two writers don't actually share one mutex, or a non-ascending frame breaks it. doyle repros ~33% on kitsubito (Linux/forkpty); benign on Windows. Before committing to a kitsubito round-trip, let me see if it repros here at all — carrier ×40:


Bash

cd C:/Users/decid/Documents/projects/spt-core && pass=0; fail=0; for i in $(seq 1 40); do
  if timeout 60 cargo test -p spt-daemon --test attach attach_survives_target_brain_restart_exactly_once -- --exact >/dev/null 2>&1; then pass=$((pass+1)); else fail=$((fail+1)); echo "FAIL @ run $i"; fi
done; echo "RESULT: $pass pass / $fail fail of 40"

Run carrier 40x on Windows to check repro rate


RESULT: 40 pass / 0 fail of 40


40/40 green on Windows — confirms the carrier is benign here (kitsubito/forkpty is the RED-carrier). I can't iterate the fix locally. Before a kitsubito round-trip, one high-value code check: my no-skip proof assumes both writers share **one** mutex (`SharedSend`). If each subscribe gets a *fresh* `SharedSend` wrapping the same socket, the two writers don't serialize and the ascending-order-preserved-in-socket-order assumption breaks. Let me verify where `sub: SharedSend` comes from for the two subscribes:


65:    KIND_SPAWNED, KIND_SUBSCRIBE, KIND_UNSUBSCRIBE,
75:pub(crate) type SharedSend = Arc<Mutex<SendHalf>>;
115:    send: SharedSend,
179:    send: SharedSend,
339:    /// [`resolve_subscribe`] reads this.
358:    fn become_controller(&mut self, sub: SharedSend, by: Option<String>, from_seq: u64) {
414:    fn add_viewer(&mut self, sub: SharedSend, from_seq: u64) {
445:    fn resolve_subscribe(
447:        sub: SharedSend,
466:            self.become_controller(sub, by, from_seq);
474:            self.become_controller(sub, by, from_seq);
488:            self.become_controller(sub, by, from_seq);
503:                self.become_controller(sub, by, from_seq);
540:    fn detach_if(&mut self, sub: &SharedSend) {
557:    fn all_sinks(&self) -> Vec<SharedSend> {
558:        let mut v: Vec<SharedSend> = Vec::new();
595:    fn is_controller(&self, sub: &SharedSend) -> bool {
614:fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
718:    send: SharedSend,
1129:            sessions: Arc::new(Mutex::new(HashMap::new())),
1242:        let send: SharedSend = Arc::new(Mutex::new(send));
1262:                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
1401:    fn dispatch_spawn(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
1432:        let log = Arc::new(Mutex::new(OutputLog::new(
1440:            .become_controller(Arc::clone(send), None, 0);
1504:                    let floor = Arc::new(Mutex::new(InjectFloor::default()));
1562:    fn dispatch_subscribe(&self, env: Envelope, send: &SharedSend) -> Result<u64, String> {
1584:        let outcome = log.lock().unwrap().resolve_subscribe(
1600:    fn dispatch_unsubscribe(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1627:    fn dispatch_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1724:    fn dispatch_endpoint_input(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1787:    fn dispatch_resize(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1814:    fn dispatch_net_status(&self, send: &SharedSend) {
1844:    fn dispatch_brain_restart(&self, send: &SharedSend) {
1872:    fn dispatch_net_dial(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
1927:    fn dispatch_net_dial_loopback(&self, send: &SharedSend) -> Result<(), String> {
1949:    fn dispatch_pair_join(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
2003:    fn dispatch_net_stream_open(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
2051:    fn dispatch_net_stream_send(&self, env: Envelope, send: &SharedSend) -> Result<(), String> {
2085:    fn dispatch_net_streams(&self, send: &SharedSend) -> Result<(), String> {

[Showing results with pagination = limit: 40]


Verified shared mutex: broker.rs:1242 creates **one** `SharedSend` per connection; both of life2's subscribes (handoff + serve) use it. So my no-skip proof's premises hold — meaning the flaw is timing-specific to forkpty and I can't pin it by reasoning. This is the instrument-on-repro case doyle asked for.

Two leading hypotheses: **H1** the life1→stream / life2→stream *forwarding boundary* race (operator gets life2's seq K before life1's last frame K-1 lands on the stream); **H2** something breaks the ascending/shared-mutex premise on forkpty timing.

Per the confirm-first protocol, I'll build an env-gated RACEDIAG on a throwaway branch for doyle to run on kitsubito. Checking git state first:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== HEAD ===" && git log --oneline -3 && echo "=== STATUS ===" && git status --short && echo "=== BRANCH ===" && git branch --show-current

Check git state after doyle's commit


=== HEAD ===
9b0025b fix(broker): controller-writer reorder on brain-restart re-serve (REQ-HAZARD-CONTROLLER-WRITER-REORDER)
4a1c82f docs(harness-contract): clarify translation-binary {text} is applied verbatim — a trailing \r in text submits (perri F-contract-Q)
35cad57 docs(harness-contract): mark [session.resume] + [message-idle-translation-binary] "since v0.13.0"
=== STATUS ===
=== BRANCH ===
v0.13.0-p1b-ack-deadlock


Git clean at 9b0025b. Every loss/skip scenario I construct is saved by the ring-as-source-of-truth + writer-2's full replay — so this is genuinely timing-specific to forkpty and needs instrumentation, not a guessed fix (the confirm-first protocol). Building an env-gated RACEDIAG on a throwaway branch for doyle's kitsubito repro. Creating the branch:


cd C:/Users/decid/Documents/projects/spt-core && git checkout -b v0.13.0-p1c-reorder-diag 2>&1 | tail -2 && git branch --show-current

Create throwaway diag branch off 9b0025b


Switched to a new branch 'v0.13.0-p1c-reorder-diag'
v0.13.0-p1c-reorder-diag


Adding the RACEDIAG instrument. Capture point 1 — `controller_writer` (the two writers' socket commit order). Reading current state to edit precisely:


716	fn controller_writer(
717	    sid: u64,
718	    send: SharedSend,
719	    initial: Vec<(u64, Vec<u8>)>,
720	    rx: Receiver<CtrlMsg>,
721	    delivered_through: Arc<AtomicU64>,
722	    epoch: u64,
723	    controller_epoch: Arc<AtomicU64>,
724	) {
725	    for (seq, bytes) in &initial {
726	        let frame = output_envelope(sid, *seq, bytes);
727	        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared
728	        // epoch past ours) must write NOTHING further — otherwise its owned
729	        // initial batch races the new writer on the same socket and reorders the
730	        // seq stream. The gate is read UNDER `send.lock()`, atomically with the
731	        // write: both writers serialize on this one socket mutex, and
732	        // `become_controller` bumps the epoch BEFORE spawning the new writer, so
733	        // whichever superseded writer next holds the lock observes the bump and
734	        // bails before committing — no check-then-block-then-write window. (A
735	        // frame the stale writer committed BEFORE the bump is irreducible at the
736	        // broker; fix #1 removes the contradictory double-take that produced it.)
737	        let ok = match send.lock() {
738	            Ok(mut s) => {
739	                if controller_epoch.load(Ordering::Acquire) != epoch {
740	                    return;
741	                }
742	                write_frame(&mut *s, &frame).is_ok()
743	            }
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	        // NO epoch gate on the live path (P1c): new output only ever flows to the
757	        // CURRENT controller's channel (the drain clones `self.controller.tx`), so
758	        // a superseded writer's channel holds only its pre-supersede output
759	        // backlog (seq the consumer dedups via snap-above) plus its own TERMINAL
760	        // control frames — notably the `Displaced` kick the displaced controller
761	        // MUST still receive. The loop ends naturally when `tx` drops. Gating
762	        // output here would suppress that `Displaced` (the cv-matrix loud-take
763	        // hang). The reorder hazard is the INITIAL-BATCH replay, gated above.
764	        let ok = match send.lock() {
765	            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
766	            Err(_) => false,
767	        };
768	        if !ok {
769	            return;
770	        }
771	        if let Some(seq) = seq {
772	            advance_delivered(&delivered_through, seq);
773	        }
774	    }
775	}
776
777	/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present

fn controller_writer(
    sid: u64,
    send: SharedSend,
    initial: Vec<(u64, Vec<u8>)>,
    rx: Receiver<CtrlMsg>,
    delivered_through: Arc<AtomicU64>,
    epoch: u64,
    controller_epoch: Arc<AtomicU64>,
) {
    for (seq, bytes) in &initial {
        let frame = output_envelope(sid, *seq, bytes);
        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared
        // epoch past ours) must write NOTHING further — otherwise its owned
        // initial batch races the new writer on the same socket and reorders the
        // seq stream. The gate is read UNDER `send.lock()`, atomically with the
        // write: both writers serialize on this one socket mutex, and
        // `become_controller` bumps the epoch BEFORE spawning the new writer, so
        // whichever superseded writer next holds the lock observes the bump and
        // bails before committing — no check-then-block-then-write window. (A
        // frame the stale writer committed BEFORE the bump is irreducible at the
        // broker; fix #1 removes the contradictory double-take that produced it.)
        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;
        }
        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);
        }
    }
}

/// The per-session translation-binary attachment (W2 Layer C, ADR-0022). Present


Edit

    epoch: u64,
    controller_epoch: Arc<AtomicU64>,
) {
    for (seq, bytes) in &initial {
        let frame = output_envelope(sid, *seq, bytes);
        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared
        // epoch past ours) must write NOTHING further — otherwise its owned
        // initial batch races the new writer on the same socket and reorders the
        // seq stream. The gate is read UNDER `send.lock()`, atomically with the
        // write: both writers serialize on this one socket mutex, and
        // `become_controller` bumps the epoch BEFORE spawning the new writer, so
        // whichever superseded writer next holds the lock observes the bump and
        // bails before committing — no check-then-block-then-write window. (A
        // frame the stale writer committed BEFORE the bump is irreducible at the
        // broker; fix #1 removes the contradictory double-take that produced it.)
        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;
        }
        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);
        }
    }
}

    epoch: u64,
    controller_epoch: Arc<AtomicU64>,
) {
    // RACEDIAG (throwaway diag branch, env SPT_REORDER_DIAG): log each frame this
    // writer COMMITS to the socket, UNDER the lock, so the eprintln order is the
    // true on-wire commit order across the two racing writers (epoch tags which).
    let diag = std::env::var_os("SPT_REORDER_DIAG").is_some();
    for (seq, bytes) in &initial {
        let frame = output_envelope(sid, *seq, bytes);
        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared
        // epoch past ours) must write NOTHING further — otherwise its owned
        // initial batch races the new writer on the same socket and reorders the
        // seq stream. The gate is read UNDER `send.lock()`, atomically with the
        // write: both writers serialize on this one socket mutex, and
        // `become_controller` bumps the epoch BEFORE spawning the new writer, so
        // whichever superseded writer next holds the lock observes the bump and
        // bails before committing — no check-then-block-then-write window. (A
        // frame the stale writer committed BEFORE the bump is irreducible at the
        // broker; fix #1 removes the contradictory double-take that produced it.)
        let ok = match send.lock() {
            Ok(mut s) => {
                if controller_epoch.load(Ordering::Acquire) != epoch {
                    if diag {
                        eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} INITIAL-BAIL-superseded at seq={seq}");
                    }
                    return;
                }
                let w = write_frame(&mut *s, &frame).is_ok();
                if w && diag {
                    eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} INITIAL commit seq={seq}");
                }
                w
            }
            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),
        };
        // 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) => {
                let w = write_frame(&mut *s, frame).is_ok();
                if w && diag {
                    eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} LIVE commit seq={seq:?}");
                }
                w
            }
            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)

    epoch: u64,
    controller_epoch: Arc<AtomicU64>,
) {
    for (seq, bytes) in &initial {
        let frame = output_envelope(sid, *seq, bytes);
        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared
        // epoch past ours) must write NOTHING further — otherwise its owned
        // initial batch races the new writer on the same socket and reorders the
        // seq stream. The gate is read UNDER `send.lock()`, atomically with the
        // write: both writers serialize on this one socket mutex, and
        // `become_controller` bumps the epoch BEFORE spawning the new writer, so
        // whichever superseded writer next holds the lock observes the bump and
        // bails before committing — no check-then-block-then-write window. (A
        // frame the stale writer committed BEFORE the bump is irreducible at the
        // broker; fix #1 removes the contradictory double-take that produced it.)
        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;
        }
        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);
        }
    }
}

    epoch: u64,
    controller_epoch: Arc<AtomicU64>,
) {
    // RACEDIAG (throwaway diag branch, env SPT_REORDER_DIAG): log each frame this
    // writer COMMITS to the socket, UNDER the lock, so the eprintln order is the
    // true on-wire commit order across the two racing writers (epoch tags which).
    let diag = std::env::var_os("SPT_REORDER_DIAG").is_some();
    for (seq, bytes) in &initial {
        let frame = output_envelope(sid, *seq, bytes);
        // P1c: a SUPERSEDED writer (a newer `become_controller` bumped the shared
        // epoch past ours) must write NOTHING further — otherwise its owned
        // initial batch races the new writer on the same socket and reorders the
        // seq stream. The gate is read UNDER `send.lock()`, atomically with the
        // write: both writers serialize on this one socket mutex, and
        // `become_controller` bumps the epoch BEFORE spawning the new writer, so
        // whichever superseded writer next holds the lock observes the bump and
        // bails before committing — no check-then-block-then-write window. (A
        // frame the stale writer committed BEFORE the bump is irreducible at the
        // broker; fix #1 removes the contradictory double-take that produced it.)
        let ok = match send.lock() {
            Ok(mut s) => {
                if controller_epoch.load(Ordering::Acquire) != epoch {
                    if diag {
                        eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} INITIAL-BAIL-superseded at seq={seq}");
                    }
                    return;
                }
                let w = write_frame(&mut *s, &frame).is_ok();
                if w && diag {
                    eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} INITIAL commit seq={seq}");
                }
                w
            }
            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),
        };
        // 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) => {
                let w = write_frame(&mut *s, frame).is_ok();
                if w && diag {
                    eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} LIVE commit seq={seq:?}");
                }
                w
            }
            Err(_) => false,
        };
        if !ok {
            return;
        }
        if let Some(seq) = seq {
            advance_delivered(&delivered_through, seq);
        }
    }
}

+    // RACEDIAG (throwaway diag branch, env SPT_REORDER_DIAG): log each frame this

+    // writer COMMITS to the socket, UNDER the lock, so the eprintln order is the

+    // true on-wire commit order across the two racing writers (epoch tags which).

+    let diag = std::env::var_os("SPT_REORDER_DIAG").is_some();

+                        eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} INITIAL-BAIL-superseded at seq={seq}");

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

+                let w = write_frame(&mut *s, &frame).is_ok();

+                    eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} INITIAL commit seq={seq}");

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

+                let w = write_frame(&mut *s, frame).is_ok();

+                    eprintln!("[RACEDIAG] ctrlwriter epoch={epoch} LIVE commit seq={seq:?}");


Capture point 2 — serve_attach's Output forward (life2's post-snap-above delivery → operator):


            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                // RACEDIAG (env SPT_REORDER_DIAG): the seq this serve loop FORWARDS
                // to the operator stream = exactly what the brain's snap-above
                // read_event delivered. Compare against the ctrlwriter commit log
                // (socket order) to locate where a skip is introduced.
                if std::env::var_os("SPT_REORDER_DIAG").is_some() {
                    eprintln!("[RACEDIAG] serve forward Output seq={seq} -> operator");
                }
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });


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

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });

            // ── Session → wire: output and exit. ────────────────────────────
            BrokerEvent::Output { seq, bytes, .. } if attached => {
                // RACEDIAG (env SPT_REORDER_DIAG): the seq this serve loop FORWARDS
                // to the operator stream = exactly what the brain's snap-above
                // read_event delivered. Compare against the ctrlwriter commit log
                // (socket order) to locate where a skip is introduced.
                if std::env::var_os("SPT_REORDER_DIAG").is_some() {
                    eprintln!("[RACEDIAG] serve forward Output seq={seq} -> operator");
                }
                let line = ndjson::encode_line(&AttachRecord::Output {
                    seq,
                    data_b64: encode_bytes(&bytes),
                });

+                // RACEDIAG (env SPT_REORDER_DIAG): the seq this serve loop FORWARDS

+                // to the operator stream = exactly what the brain's snap-above

+                // read_event delivered. Compare against the ctrlwriter commit log

+                // (socket order) to locate where a skip is introduced.

+                if std::env::var_os("SPT_REORDER_DIAG").is_some() {

+                    eprintln!("[RACEDIAG] serve forward Output seq={seq} -> operator");


Capture point 3 — `render_until` (operator's received seq order + cursor):


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

                for rec in decoder.push(&bytes) {
                    if let AttachRecord::Output { seq, data_b64 } = rec {
                        if std::env::var_os("SPT_REORDER_DIAG").is_some() {
                            eprintln!(
                                "[RACEDIAG] operator recv Output seq={seq} cursor={} {}",
                                *next_seq,
                                if seq > *next_seq { "<<< FORWARD GAP" } else { "" }
                            );
                        }
                        if seq < *next_seq {
                            continue; // replay duplicate — dedup by cursor
                        }
                        assert_eq!(seq, *next_seq, "no forward gap in the viewport");


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

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

                for rec in decoder.push(&bytes) {
                    if let AttachRecord::Output { seq, data_b64 } = rec {
                        if std::env::var_os("SPT_REORDER_DIAG").is_some() {
                            eprintln!(
                                "[RACEDIAG] operator recv Output seq={seq} cursor={} {}",
                                *next_seq,
                                if seq > *next_seq { "<<< FORWARD GAP" } else { "" }
                            );
                        }
                        if seq < *next_seq {
                            continue; // replay duplicate — dedup by cursor
                        }
                        assert_eq!(seq, *next_seq, "no forward gap in the viewport");

+                        if std::env::var_os("SPT_REORDER_DIAG").is_some() {

+                                "[RACEDIAG] operator recv Output seq={seq} cursor={} {}",

+                                *next_seq,

+                                if seq > *next_seq { "<<< FORWARD GAP" } else { "" }


Capture point 3b — log `K = prior.next_seq` at re-serve in the carrier:


710	        }
711	    }
712	    let state = life1.snapshot();
713	    drop(life1); // the crash — broker A, session, stream, journal all survive
714
715	    // Into the DEAD window: B types the second marker (buffered by A's broker).
716	    send_attach_input(&mut operator, stream_b, b"MARKER_TWO\r", 11).expect("input 2");
717
718	    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the
719	    // worst-case replay. serve_attach re-handles the replayed Request (gate +
720	    // re-attach from 0, so every output record re-transmits — B's render
721	    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write

        }
    }
    let state = life1.snapshot();
    drop(life1); // the crash — broker A, session, stream, journal all survive

    // Into the DEAD window: B types the second marker (buffered by A's broker).
    send_attach_input(&mut operator, stream_b, b"MARKER_TWO\r", 11).expect("input 2");

    // Life 2: handoff, then re-serve the SAME stream from seq 0 — the
    // worst-case replay. serve_attach re-handles the replayed Request (gate +
    // re-attach from 0, so every output record re-transmits — B's render
    // cursor drops the duplicates), replays MARKER_ONE's input (PTY-write


    let state = life1.snapshot();
    drop(life1); // the crash — broker A, session, stream, journal all survive

    // Into the DEAD window: B types the second marker (buffered by A's broker).
    send_attach_input(&mut operator, stream_b, b"MARKER_TWO\r", 11).expect("input 2");

    let state = life1.snapshot();
    if std::env::var_os("SPT_REORDER_DIAG").is_some() {
        // K = life1's CONSUMPTION cursor = life2's resume floor. The operator
        // viewport should be gapless across the [0,K) (life1-forwarded) / [K,end]
        // (life2-forwarded) boundary; a gap AT K (or near it) points at the
        // life1→stream vs life2→stream forwarding boundary, a gap ABOVE K at the
        // two-writer socket reorder surviving snap-above.
        eprintln!("[RACEDIAG] re-serve boundary K=prior.next_seq={}", state.next_seq);
    }
    drop(life1); // the crash — broker A, session, stream, journal all survive

    // Into the DEAD window: B types the second marker (buffered by A's broker).
    send_attach_input(&mut operator, stream_b, b"MARKER_TWO\r", 11).expect("input 2");


    let state = life1.snapshot();
    drop(life1); // the crash — broker A, session, stream, journal all survive

    // Into the DEAD window: B types the second marker (buffered by A's broker).
    send_attach_input(&mut operator, stream_b, b"MARKER_TWO\r", 11).expect("input 2");

    let state = life1.snapshot();
    if std::env::var_os("SPT_REORDER_DIAG").is_some() {
        // K = life1's CONSUMPTION cursor = life2's resume floor. The operator
        // viewport should be gapless across the [0,K) (life1-forwarded) / [K,end]
        // (life2-forwarded) boundary; a gap AT K (or near it) points at the
        // life1→stream vs life2→stream forwarding boundary, a gap ABOVE K at the
        // two-writer socket reorder surviving snap-above.
        eprintln!("[RACEDIAG] re-serve boundary K=prior.next_seq={}", state.next_seq);
    }
    drop(life1); // the crash — broker A, session, stream, journal all survive

    // Into the DEAD window: B types the second marker (buffered by A's broker).
    send_attach_input(&mut operator, stream_b, b"MARKER_TWO\r", 11).expect("input 2");

+    if std::env::var_os("SPT_REORDER_DIAG").is_some() {

+        // K = life1's CONSUMPTION cursor = life2's resume floor. The operator

+        // viewport should be gapless across the [0,K) (life1-forwarded) / [K,end]

+        // (life2-forwarded) boundary; a gap AT K (or near it) points at the

+        // life1→stream vs life2→stream forwarding boundary, a gap ABOVE K at the

+        // two-writer socket reorder surviving snap-above.

+        eprintln!("[RACEDIAG] re-serve boundary K=prior.next_seq={}", state.next_seq);


Compile-check the instrument (verify `state.next_seq` is accessible + no errors):


Bash

cd C:/Users/decid/Documents/projects/spt-core && cargo build -p spt-daemon --tests 2>&1 | grep -E "error|warning: unused|Finished" | head -15; echo "BUILD_EXIT=${PIPESTATUS[0]}"

Compile-check the RACEDIAG instrument


    Finished `dev` profile [unoptimized + debuginfo] target(s) in 33.12s
BUILD_EXIT=0


Compiles clean. Let me confirm the instrument actually emits (run the carrier once with the env on Windows — it passes here, but the RACEDIAG lines should fire, proving the instrument works before doyle deploys):


cd C:/Users/decid/Documents/projects/spt-core && SPT_REORDER_DIAG=1 timeout 60 cargo test -p spt-daemon --test attach attach_survives_target_brain_restart_exactly_once -- --exact --nocapture 2>&1 | grep "RACEDIAG" | head -40; echo "---(count)---"; SPT_REORDER_DIAG=1 timeout 60 cargo test -p spt-daemon --test attach attach_survives_target_brain_restart_exactly_once -- --exact --nocapture 2>&1 | grep -c "RACEDIAG"

Verify RACEDIAG emits on a passing carrier run


[RACEDIAG] ctrlwriter epoch=1 LIVE commit seq=Some(0)
[RACEDIAG] ctrlwriter epoch=2 INITIAL commit seq=0
[RACEDIAG] ctrlwriter epoch=2 LIVE commit seq=Some(1)
[RACEDIAG] re-serve boundary K=prior.next_seq=2
[RACEDIAG] operator recv Output seq=0 cursor=0
[RACEDIAG] operator recv Output seq=1 cursor=1
[RACEDIAG] ctrlwriter epoch=4 INITIAL commit seq=0
[RACEDIAG] ctrlwriter epoch=4 INITIAL commit seq=1
[RACEDIAG] ctrlwriter epoch=4 LIVE commit seq=Some(2)
[RACEDIAG] serve forward Output seq=2 -> operator
[RACEDIAG] operator recv Output seq=2 cursor=2
[RACEDIAG] ctrlwriter epoch=5 INITIAL commit seq=0
[RACEDIAG] ctrlwriter epoch=5 INITIAL commit seq=1
[RACEDIAG] ctrlwriter epoch=5 INITIAL commit seq=2
---(count)---
14


Instrument works — and the passing-Windows trace is already telling: only `epoch=4` (life2's serve writer) commits the re-serve initial; the handoff writer (`epoch=3`) bails immediately via the gate before committing, so no reorder here. On kitsubito's 33%-fail timing, `epoch=3` will commit some frames interleaved with `epoch=4` → the skip. The diag will capture exactly that.

Committing + pushing the throwaway diag branch for doyle's kitsubito capture:


cd C:/Users/decid/Documents/projects/spt-core && git add crates/spt-daemon/src/broker.rs crates/spt-daemon/src/attach.rs crates/spt-daemon/tests/attach.rs && git commit -q -F - <<'EOF'
diag(broker,attach): env-gated RACEDIAG for the P1c operator-stream reorder (THROWAWAY)

Not a fix — confirm-first instrumentation to pin the residual operator-stream
reorder doyle's kitsubito gate caught (carrier ~33% fail isolated; Windows
benign 40/40). Env SPT_REORDER_DIAG captures three points to locate where the
skip enters the operator viewport:
  - controller_writer: each frame COMMITTED to the socket, under the lock, tagged
    by epoch (so the two racing writers' true on-wire commit order is visible).
  - serve_attach Output forward: the seq life2's snap-above read_event delivered.
  - carrier render_until + re-serve: operator recv seq vs cursor (gap flagged) +
    K=prior.next_seq.
Hypotheses to discriminate: gap AT K = life1→stream vs life2→stream forwarding
boundary; gap ABOVE K = two-writer socket reorder surviving snap-above.

Co-authored by: todlando
EOF
git log --oneline -2 && echo "=== push ===" && git push -u origin v0.13.0-p1c-reorder-diag 2>&1 | tail -4

Commit and push the diag branch


f5eef48 diag(broker,attach): env-gated RACEDIAG for the P1c operator-stream reorder (THROWAWAY)
9b0025b fix(broker): controller-writer reorder on brain-restart re-serve (REQ-HAZARD-CONTROLLER-WRITER-REORDER)
=== push ===
remote:
branch 'v0.13.0-p1c-reorder-diag' set up to track 'origin/v0.13.0-p1c-reorder-diag'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      v0.13.0-p1c-reorder-diag -> v0.13.0-p1c-reorder-diag


Diag branch pushed: `v0.13.0-p1c-reorder-diag` @f5eef48. Handing the capture protocol to doyle:


"$OWL" send doyle todlando <<'EOF'
GATE ACK — not-pass accepted, no guess-fix. Confirm-first instrument pushed for your kitsubito repro.

WINDOWS: carrier 40/40 green (benign, as you flagged — forkpty/kitsubito is the RED-carrier; I can't repro locally).

CODE CHECK before instrumenting: verified both of life2's subscribes (handoff subscribe@K + serve attach_as@0) come over life2's ONE connection → ONE SharedSend (broker.rs:1242, per-conn) → the two writers SHARE the mutex (frames serialize, no torn writes). With shared-mutex + each writer's initial ascending + the SURVIVING writer (serve, from 0) offering the COMPLETE [0,end] range, my no-skip proof holds ON PAPER — so the skip is forkpty-TIMING-specific and I need the real commit order, not more reasoning. Your hypotheses (boundary B_cursor<K vs >1 pre-bump frame) are exactly what the instrument discriminates.

DIAG BRANCH (THROWAWAY, off your 9b0025b): v0.13.0-p1c-reorder-diag @f5eef48, pushed to origin. Env-gated SPT_REORDER_DIAG, 3 capture points:
- [RACEDIAG] ctrlwriter epoch=N {INITIAL|LIVE} commit seq=S  — logged UNDER send.lock(), so eprintln order = TRUE on-wire commit order; epoch tags which of the two racing writers.
- [RACEDIAG] serve forward Output seq=S -> operator  — what life2's snap-above read_event delivered.
- [RACEDIAG] operator recv Output seq=S cursor=C <<< FORWARD GAP  — the operator viewport, gap flagged; plus "re-serve boundary K=prior.next_seq=N".

RUN ON KITSUBITO: build f5eef48, then loop the carrier single-threaded with the env until a FAIL, capture the RACEDIAG block of a FAILING run (and one passing for contrast):
  for i in $(seq 1 40); do SPT_REORDER_DIAG=1 timeout 60 cargo test -p spt-daemon --test attach attach_survives_target_brain_restart_exactly_once -- --exact --nocapture 2>&1 | tee run.$i.log | grep -q "FAILED" && echo "FAIL $i" ; done
(or your timeout-wrapped harness). Send me the failing run's RACEDIAG lines.

DISCRIMINATOR: a FORWARD GAP at the operator AT seq=K → life1→stream vs life2→stream forwarding-boundary race (life2's K landed before life1's K-1). Gap ABOVE K → the two-writer socket reorder survived snap-above (I'll see epoch=N-1 INITIAL commits interleaved above epoch=N). On the Windows passing run the handoff writer (epoch=3) BAILED before committing anything (gate) so only the serve writer committed — clean; on kitsubito it'll commit interleaved. The trace will pin it.

Two capture rounds if the first probe's insufficient (I can add a 4th point — the broker's net_stream_send order to the QUIC stream — if it's the boundary). Then I fix off the confirmed root. Reachable.
Co-authored by: todlando
EOF

Hand RACEDIAG capture protocol to doyle


➜ SENT:doyle


Handed off to doyle. Now waiting on his kitsubito RACEDIAG capture of a failing run.

## Status

**P1c gate: NOT PASS** (doyle, kitsubito) — P1c fixed the brain-side reorder (722 gone) but a **residual reorder surfaces at the operator stream** (~33% fail isolated; Windows 40/40 benign).

**My move (confirm-first, no guess-fix):**
- Verified the two writers share one mutex (broker.rs:1242) — so my no-skip proof holds *on paper*; the flaw is forkpty-timing-specific and needs the real commit order.
- Built env-gated **RACEDIAG** (3 capture points: writer socket-commit order by epoch / life2's forwarded seq / operator recv-vs-cursor + K), pushed throwaway branch `v0.13.0-p1c-reorder-diag` @f5eef48.
- Handed doyle the kitsubito capture protocol + the discriminator (gap **at K** = life1/life2 forwarding-boundary race; gap **above K** = two-writer socket reorder).

**Pre-staged both fix directions** so I can move fast once the trace lands:
- H1 (boundary, most likely — outside my proof's scope): operator-stream assembly across life1's `[0,K)` + life2's `[K,end]` isn't FIFO-ordered at the broker's per-stream forward.
- H2 (two-writer reorder surviving snap-above): would mean my ascending-merge premise is violated — the trace would show how.

Holding for doyle's failing-run RACEDIAG. Reachable.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/10" id="20d2a00c" type="msg" from="doyle">RACEDIAG CAPTURED on kitsubito (f5eef48, ~33% repro). VERDICT: it's the BOUNDARY race at seq=K, NOT the two-writer socket reorder. The epoch gate WORKS — epoch3 commits only its single seq 1, epoch4 cleanly commits 0,1,2,3, zero interleaving above K. The bug is CONSUMER-SIDE in life2's serve_attach.

FAILING run</EVENT-PART>
<EVENT-PART seq="2/10" id="20d2a00c">:
  ctrlwriter epoch=2 LIVE commit seq=0
  ctrlwriter epoch=2 LIVE commit seq=1            (life1)
  re-serve boundary K=prior.next_seq=1
  operator recv Output seq=0 cursor=0             (operator got seq0 from life1, cursor-&gt;1)
  ctrlwriter epoch=3 INITIAL commit seq=1         (handoff writer, from K=1)
  ctrlwriter epoc</EVENT-PART>
<EVENT-PART seq="3/10" id="20d2a00c">h=4 INITIAL commit seq=0
  ctrlwriter epoch=4 INITIAL commit seq=1         (serve writer, from 0)
  ctrlwriter epoch=4 LIVE commit seq=2
  ctrlwriter epoch=4 LIVE commit seq=3
  serve forward Output seq=2 -&gt; operator          &lt;-- life2 forwards 2,3; NEVER forwards seq 1
  serve forward Output seq=3 -&gt; operator
  oper</EVENT-PART>
<EVENT-PART seq="4/10" id="20d2a00c">ator recv Output seq=2 cursor=1 &lt;&lt;&lt; FORWARD GAP   (had seq0, needs seq1, gets seq2)

PASSING run (same prefix), the ONE difference:
  ctrlwriter epoch=3 INITIAL commit seq=1
  serve forward Output seq=1 -&gt; operator          &lt;-- life2 DOES forward seq1 here
  operator recv Output seq=1 cursor=1             (no gap)</EVENT-PART>
<EVENT-PART seq="5/10" id="20d2a00c">
  ...then epoch4 0,1,2,3, serve forwards 2,3 (0,1 deduped) -&gt; clean

ROOT (consumer-side, in life2): the missing seq is exactly K=1. In the FAIL timing, epoch4's become_controller (serve attach_as(0)) + its INITIAL 0,1 land BEFORE life2 forwards epoch3's seq 1. life2's cursor management across [handoff subscribe seeds session_curs</EVENT-PART>
<EVENT-PART seq="6/10" id="20d2a00c">ors=K=1] -&gt; [serve attach_as RESETS session_cursors=0] -&gt; snap-above dedup drops seq K=1 from the operator forward: life2 consumes seq 1 but its cursor is already past 1 (advanced to 2) when it would forward, so seq 1 (=K) is deduped, never forwarded. In the PASS timing life2 forwards epoch3's seq 1 before the epoch4 race, so cursor is 2</EVENT-PART>
<EVENT-PART seq="7/10" id="20d2a00c"> legitimately and 0,1 dedup correctly.

So fix #3 (attach_as cursor-reset to from_seq) is the culprit interacting with the handoff seed: the reset to 0 doesn't actually re-forward seq K because life2 already consumed epoch3's seq K at cursor=K and advanced to K+1, then attach_as resets to 0 but the epoch3 seq-K frame is already past — </EVENT-PART>
<EVENT-PART seq="8/10" id="20d2a00c">and epoch4's re-send of seq K arrives when cursor is K+1 (2) so it's deduped. Net: seq K is consumed-once by the handoff reader and the operator-forward boundary loses it.

This is the hand
...(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.


doyle pinned the root via RACEDIAG. Reading his full message (truncated) for his fix direction before I act:


cd C:/Users/decid/Documents/projects/spt-core && tail -c 1900 "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 -16

Read full tail of doyle's RACEDIAG verdict


serve attach_as(0)) + its INITIAL 0,1 land BEFORE life2 forwards epoch3's seq 1. life2's cursor management across [handoff subscribe seeds session_curs
ors=K=1] -> [serve attach_as RESETS session_cursors=0] -> snap-above dedup drops seq K=1 from the operator forward: life2 consumes seq 1 but its cursor is already past 1 (advanced to 2) when it would forward, so seq 1 (=K) is deduped, never forwarded. In the PASS timing life2 forwards epoch3's seq 1 before the epoch4 race, so cursor is 2
 legitimately and 0,1 dedup correctly.

So fix #3 (attach_as cursor-reset to from_seq) is the culprit interacting with the handoff seed: the reset to 0 doesn't actually re-forward seq K because life2 already consumed epoch3's seq K at cursor=K and advanced to K+1, then attach_as resets to 0 but the epoch3 seq-K frame is already past —
and epoch4's re-send of seq K arrives when cursor is K+1 (2) so it's deduped. Net: seq K is consumed-once by the handoff reader and the operator-forward boundary loses it.

This is the handoff-eager-subscribe (kept) reading seq K into life2 BEFORE the operator stream forwards it — the exact hazard dropping #1 would've removed, now resu
rfaced consumer-side. Your call on the fix: e.g. the operator-forward must not depend on life2's internal consume cursor at the K boundary (forward from the operator's OWN requested floor independent of the handoff seed), or attach_as must re-forward from from_seq even for already-consumed seqs. Diag branch + traces are yours; ping the fix and
 I re-run the 20x. NOT the socket reorder — the epoch gate is sound.

Co-authored by: doyle


doyle pinned the **root**: handoff's eager subscribe (epoch3) makes life2 receive seq K *before* serve_attach sets `attached=true` → the early Output is consumed (cursor advances) but dropped by the `if attached` guard → epoch4's re-send of seq K is then deduped → operator never gets K → gap. It's the exact hazard dropping #1 would've removed, resurfaced consumer-side.

doyle offered two consumer-side directions (keep handoff subscribe). The cleaner root is re-applying his original **fix #1** (handoff no eager subscribe) — but that needs the standalone callers to subscribe explicitly, and the choice hinges on whether serve_attach's brain in production is dedicated or a driving brain. Checking that before I decide:


cd C:/Users/decid/Documents/projects/spt-core && echo "=== serve_attach callers (non-test) ===" && rg -n "serve_attach\(" crates/ --type rust | grep -v "tests/\|fn serve_attach" && echo "=== production caller context ===" && rg -n "serve_attach" crates/spt-daemon/src/*.rs crates/spt/src/*.rs 2>/dev/null | grep -v "fn serve_attach\|//"

Find production serve_attach callers


=== serve_attach callers (non-test) ===
crates/spt-daemon\tests\attach.rs:162:            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0)
crates/spt-daemon\tests\attach.rs:347:            serve_attach(&mut target, &serve_broker, stream_a, "cc-ep", &origin, 0).expect("serve");
crates/spt-daemon\tests\attach.rs:553:        serve_attach(&mut target, &serve_broker, stream_a, "burst-ep", &origin, 0).expect("serve")
crates/spt-daemon\tests\attach.rs:736:        let outcome = serve_attach(&mut life2, &serve_broker, stream_a, "ling", &origin2, 0)
crates/spt-daemon\tests\attach.rs:840:            serve_attach(&mut target, &serve_broker, stream_a, &endpoint_serve, &origin, 0)
crates/spt-daemon\tests\input_ack_deadlock.rs:355:            let _ = serve_attach(&mut target, &serve_broker, stream_a, &flood_ep, &origin, 0);
crates/spt-daemon\tests\input_ack_deadlock.rs:458:            let _ = serve_attach(&mut target, &serve_broker, stream_a, &attach_ep, &origin, 0);
crates/spt-daemon\tests\inject_control_wedge.rs:576:            let _ = serve_attach(&mut target, &serve_broker, stream_a, "wedge-jrnl-ep", &origin, 0);
crates/spt-daemon\tests\inject_control_wedge.rs:1530:        let _ = serve_attach(&mut target, &serve_broker, stream_a, "w5-a2-ep", &origin, 0);
crates/spt-daemon\tests\inject_control_wedge.rs:1813:            let _ = serve_attach(&mut target, &serve_broker, stream_a, "p0-paste-wedge-ep", &origin, 0);
crates/spt-daemon\src\dispatch.rs:350:                Some(endpoint) => match serve_attach(&mut brain, broker_name, stream_id, &endpoint, origin, 0) {
=== production caller context ===
crates/spt-daemon/src/lib.rs:175:pub use attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
crates/spt-daemon/src/dispatch.rs:75:use crate::attach::serve_attach;
crates/spt-daemon/src/dispatch.rs:350:                Some(endpoint) => match serve_attach(&mut brain, broker_name, stream_id, &endpoint, origin, 0) {


Production caller is dispatch.rs:350. Let me see what `brain` it uses (fresh/dedicated vs driving):


325	            let cache = ReleaseCache::open(&paths.releases);
326	            let roster = RosterStore::load();
327	            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {
328	                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
329	                Err(e) => DispatchOutcome::Failed(e.to_string()),
330	            }
331	        }
332	        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
333	        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),
334	        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
335	        StreamFamily::Attach => {
336	            // The gate subject is the session's OWNING endpoint, resolved
337	            // from the broker's table (the spawn's label) — never from wire
338	            // bytes. An unlabeled or unknown session refuses, fail-closed.
339	            let session_id = line.get("session_id").and_then(|v| v.as_u64());
340	            let endpoint = session_id.and_then(|sid| {
341	                brain.sessions().ok().and_then(|reply| {
342	                    reply
343	                        .sessions
344	                        .iter()
345	                        .find(|s| s.session_id == sid && !s.endpoint.is_empty())
346	                        .map(|s| s.endpoint.clone())
347	                })
348	            });
349	            match endpoint {
350	                Some(endpoint) => match serve_attach(&mut brain, broker_name, stream_id, &endpoint, origin, 0) {
351	                    Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
352	                    Err(e) => DispatchOutcome::Failed(e.to_string()),
353	                },
354	                None => {
355	                    // Refuse politely: finish our send side, claim stays.
356	                    let _ = brain.net_stream_send(stream_id, &[], None, true);
357	                    DispatchOutcome::Served("attach refused: unlabeled/unknown session".into())
358	                }
359	            }

            let cache = ReleaseCache::open(&paths.releases);
            let roster = RosterStore::load();
            match serve_update(&mut brain, stream_id, origin, 0, &cache, &roster) {
                Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                Err(e) => DispatchOutcome::Failed(e.to_string()),
            }
        }
        StreamFamily::Notif => serve_notif_feed(&mut brain, stream_id, origin),
        StreamFamily::WanMsg => serve_wan_feed(&mut brain, stream_id, origin, paths, registry),
        StreamFamily::Registry => serve_registry_feed(&mut brain, stream_id, origin, registry),
        StreamFamily::Attach => {
            // The gate subject is the session's OWNING endpoint, resolved
            // from the broker's table (the spawn's label) — never from wire
            // bytes. An unlabeled or unknown session refuses, fail-closed.
            let session_id = line.get("session_id").and_then(|v| v.as_u64());
            let endpoint = session_id.and_then(|sid| {
                brain.sessions().ok().and_then(|reply| {
                    reply
                        .sessions
                        .iter()
                        .find(|s| s.session_id == sid && !s.endpoint.is_empty())
                        .map(|s| s.endpoint.clone())
                })
            });
            match endpoint {
                Some(endpoint) => match serve_attach(&mut brain, broker_name, stream_id, &endpoint, origin, 0) {
                    Ok(out) => DispatchOutcome::Served(format!("{out:?}")),
                    Err(e) => DispatchOutcome::Failed(e.to_string()),
                },
                None => {
                    // Refuse politely: finish our send side, claim stays.
                    let _ = brain.net_stream_send(stream_id, &[], None, true);
                    DispatchOutcome::Served("attach refused: unlabeled/unknown session".into())
                }
            }


cd C:/Users/decid/Documents/projects/spt-core && echo "=== dispatch fn signature + brain origin ===" && rg -n "fn dispatch.*brain|let mut brain|brain: .*Brain|cold_start" crates/spt-daemon/src/dispatch.rs | head -20

Trace how the dispatch brain is created


=== dispatch fn signature + brain origin ===
205:    let mut brain = connect(broker_name)?;
239:        match Brain::cold_start(broker_name, now_ms()) {
257:    let mut brain = connect(broker_name)?;
299:    let mut brain = match connect(broker_name) {
438:    brain: &mut Brain,
516:fn serve_notif_feed(brain: &mut Brain, stream_id: u64, origin: &str) -> DispatchOutcome {
596:    brain: &mut Brain,


Decisive: production `serve_attach` uses a fresh `connect()`/cold_start brain (dispatch.rs:299) — **session_cursors empty** — so doyle's **direction 2** (reset the dedup cursor on `attach_as`, gated to resume-mode) never fires in production. It's minimal, production-safe, keeps handoff's subscribe (no 4-caller churn), and is principled (an explicit re-subscribe honoring its `from_seq` floor matches `resume_sessions` semantics). I'll consolidate the cursor-reset into `subscribe_with` so both `attach` and `attach_as` get it.

Trace confirms it: serve's `attach_as(0)` resets life2's cursor to 0 → epoch4's full replay re-forwards seq K → operator dedups the overlap → gapless.

Switching to the real branch (off the diag branch) to apply the fix:


cd C:/Users/decid/Documents/projects/spt-core && git checkout v0.13.0-p1b-ack-deadlock 2>&1 | tail -2 && git log --oneline -1 && git status --short

Switch back to the real P1c branch


Your branch is up to date with 'origin/v0.13.0-p1b-ack-deadlock'.
Switched to branch 'v0.13.0-p1b-ack-deadlock'
9b0025b fix(broker): controller-writer reorder on brain-restart re-serve (REQ-HAZARD-CONTROLLER-WRITER-REORDER)


1	//! D5b regression — remote terminal attach (REQ-INST-8): drive a real PTY
2	//! session on daemon A from daemon B, byte-stream viewport over the
3	//! broker-owned wire; compute stays on A.
4	//!
5	//! Two in-process brokers (hermetic loopback endpoints, relays disabled) play
6	//! the two daemons; a real echo child runs under A's broker PTY. The
7	//! invariants:
8	//!
9	//! - **remote drive**: keystrokes sent from B arrive at A's PTY through the
10	//!   journaled input path, and the session's output streams back to B as
11	//!   sequenced records — B sees its own marker echoed by A's child;
12	//! - **origin is transport truth** (KNOWN-HAZARDS 7.5): the serve loop gates
13	//!   on the stream table's handshake-proven `remote_id_hex`;
14	//! - **restart survival** (REQ-HAZARD-RESTART-IDEMPOTENT): A's brain dies
15	//!   mid-attach; the broker keeps the QUIC stream, the session, the output
16	//!   log, and the effect journal. The successor brain re-serves the SAME
17	//!   stream from seq 0 — the worst-case stale cursor: the replayed input
18	//!   dedups at the PTY-write journal (typed exactly once), re-transmitted
19	//!   output dedups at B's render cursor (rendered exactly once) — including
20	//!   input B sent INTO the dead window.
21
22	use std::sync::atomic::{AtomicU32, Ordering};
23	use std::sync::{Arc, OnceLock};
24	use std::thread;
25	use std::time::Duration;
26
27	use spt_daemon::attach::{request_attach, send_attach_input, serve_attach, AttachServeOutcome};
28	use spt_daemon::brain::{Brain, BrainState, BrokerEvent};
29	use spt_daemon::msg::{decode_bytes, SpawnReq};
30	use spt_daemon::nethost::{NetConfig, NetHost};
31	use spt_daemon::Broker;
32	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
33	use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
34	use spt_net::net::ndjson;
35	use spt_proto::identity::Identity;
36
37	/// Hold the temp home for the whole test binary so it isn't deleted mid-run.
38	/// Every test inits it: serve_attach best-effort-stamps the endpoint perch's
39	/// `driven_by` marker, and that resolution must never touch a real home.
40	static HOME: OnceLock<tempfile::TempDir> = OnceLock::new();
41
42	fn init_home() {
43	    HOME.get_or_init(|| {
44	        let dir = tempfile::TempDir::new().expect("temp dir");
45	        std::env::set_var("SPT_HOME", dir.path());
46	        dir
47	    });
48	}
49
50	static SEQ: AtomicU32 = AtomicU32::new(0);
51	fn unique_name() -> String {
52	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
53	    format!("spt-daemon-d5b-{}-{}.sock", std::process::id(), n)
54	}
55
56	fn hermetic() -> NetConfig {
57	    NetConfig {
58	        identity: Identity::generate(),
59	        relay: RelayPolicy::Disabled,
60	        discovery: LocalDiscovery::Off,
61	        scope: BindScope::Loopback,
62	        membership: None,
63	    }
64	}
65
66	fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
67	    let host = NetHost::start(hermetic()).expect("net host start");
68	    let broker =
69	        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
70	    let serve = Arc::clone(&broker);
71	    thread::spawn(move || {
72	        let _ = serve.serve();
73	    });
74	    broker
75	}
76
77	fn connect_retry(name: &str) -> Brain {
78	    for _ in 0..300 {
79	        if let Ok(b) = Brain::cold_start(name, 1) {
80	            return b;
81	        }
82	        thread::sleep(Duration::from_millis(10));
83	    }
84	    panic!("brain could not connect");
85	}
86
87	fn echo_spawn_req() -> SpawnReq {
88	    #[cfg(unix)]
89	    let (program, args) = ("cat".to_string(), Vec::<String>::new());
90	    #[cfg(windows)]
91	    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
92	    SpawnReq {
93	        program,
94	        args,
95	        rows: 24,
96	        cols: 80,
97	        endpoint: String::new(),
98	        cwd: None,
99	        env: Default::default(),
100	        translation_binary: None,
101	    }
102	}
103
104	/// Poll the brain's `net-streams` until a PEER-initiated stream appears; return
105	/// `(stream_id, remote_id_hex)`. Filtering on `!initiated_locally` is what the
106	/// production dispatch loop does (dispatch.rs) — and it is REQUIRED for the
107	/// single-broker loopback case, where the table holds BOTH the operator's own
108	/// row and the cross-wired peer row: the serve target is always the peer row.
109	fn wait_for_stream(brain: &mut Brain) -> (u64, String) {
110	    for _ in 0..400 {
111	        let reply = brain.net_streams().expect("net-streams");
112	        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {
113	            return (s.stream_id, s.remote_id_hex.clone());
114	        }
115	        thread::sleep(Duration::from_millis(5));
116	    }
117	    panic!("attach stream never appeared at the target's broker");
118	}
119
120	/// The operator+serve drive shared by EVERY attach transport — the M12 W1.5
121	/// one-pump invariant made STRUCTURAL: cross-node (QUIC conn) and local
122	/// (broker loopback conn) call this identically; they diverge ONLY in how
123	/// `conn_id` was acquired (`net_dial` vs `net_dial_loopback`) and in
124	/// `expected_origin`, never in the `serve_attach`+render pump below. There is no
125	/// second code path that could drift.
126	///
127	/// Given an acquired `conn_id` on `operator` and the `target` broker connection
128	/// holding `session_id` for `endpoint`: open the attach stream, serve it on
129	/// `target` under `expected_origin` (asserted == the stream table's proven
130	/// origin), drive `marker` through the PTY and render it back over the wire,
131	/// then detach and confirm the serve loop ended `Detached` (session outlives).
132	/// Returns the rendered viewport bytes + the (still-live) target brain so the
133	/// caller can assert session survival and clean up.
134	#[allow(clippy::too_many_arguments)]
135	fn attach_drive_detach(
136	    operator: &mut Brain,
137	    target_broker: &str,
138	    mut target: Brain,
139	    conn_id: u64,
140	    session_id: u64,
141	    endpoint: &str,
142	    expected_origin: &str,
143	    marker: &[u8],
144	) -> (Vec<u8>, Brain) {
145	    let stream_b = request_attach(operator, conn_id, session_id, 0, 2, AttachIntent::Control).expect("request");
146	    operator
147	        .net_stream_subscribe(stream_b, 0)
148	        .expect("operator subscribe");
149
150	    // The target serves the inbound attach stream under the proven origin
151	    // (KH 7.5 — the stream table, never payload). Cross-node: the handshake id.
152	    // Loopback: this node's own id (broker-minted), which gates same-node.
153	    let (stream_a, origin) = wait_for_stream(&mut target);
154	    assert_eq!(
155	        origin, expected_origin,
156	        "serve gate subject IS the stream table's proven origin"
157	    );
158	    let endpoint_owned = endpoint.to_string();
159	    let broker_owned = target_broker.to_string();
160	    let server = thread::spawn(move || {
161	        let outcome =
162	            serve_attach(&mut target, &broker_owned, stream_a, &endpoint_owned, &origin, 0)
163	                .expect("serve");
164	        (outcome, target)
165	    });
166
167	    // Drive the marker through; the echo streams back over the SAME pump.
168	    let mut keys = marker.to_vec();
169	    keys.push(b'\r');
170	    send_attach_input(operator, stream_b, &keys, 1).expect("input");
171	    let mut decoder = AttachDecoder::new();
172	    let (mut cursor, mut viewport) = (0u64, Vec::new());
173	    render_until(
174	        operator,
175	        stream_b,
176	        &mut decoder,
177	        &mut cursor,
178	        &mut viewport,
179	        marker,
180	    );
181
182	    // Detach: the viewport ends with the session still running.
183	    operator
184	        .net_stream_send(stream_b, &[], None, true)
185	        .expect("detach");
186	    let (outcome, target) = server.join().expect("serve thread");
187	    assert_eq!(outcome, AttachServeOutcome::Detached);
188	    (viewport, target)
189	}
190
191	/// Operator-side viewport: consume attach-stream records, dedup `Output` by

... [1071 lines truncated] ...


1	//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
2	//! onto a local session for an operator on another node.
3	//!
4	//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
5	//! [`AttachRecord::Request`]; this module is the target-side pump between that
6	//! stream and the local broker-hosted session:
7	//!
8	//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
9	//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
10	//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
11	//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
12	//!   journal (a replayed input record cannot double-type);
13	//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
14	//!
15	//! ## Exactly-once lives at the EDGES, not the wire sends
16	//!
17	//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
18	//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
19	//! and *discards* everything interleaved — inside a full-duplex pump loop that
20	//! eats the operator's `Input` records (both sides starve; found the hard way
21	//! in `tests/attach.rs`). Idempotency holds without it because each direction
22	//! dedups at its **effect**: the PTY write is journaled by the operator-minted
23	//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
24	//! session-log `seq` (the render cursor). A post-restart re-serve may
25	//! re-transmit output records on the wire — duplicates are dropped at the
26	//! render cursor, the same discipline the local `OutputLog` replay uses
27	//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
28	//!
29	//! **Compute + files stay here**; the operator is a viewport (CONTEXT
30	//! §Remote-control vs local operation). The access gate runs before anything
31	//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
32	//! [`access_check`] sees the **handshake-proven** origin from the broker's
33	//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
34	//! your own running instance is ungated by default (ADR-0009 default-open);
35	//! the D5d whitelist drops in at this same call.
36	//!
37	//! Restart survival composes from D4/B5 with **no new machinery**: the broker
38	//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
39	//! A restarted target brain re-attaches the session from its output cursor,
40	//! resubscribes the stream from its net cursor, and re-drives journaled sends;
41	//! a restarted operator brain resubscribes and dedups rendered chunks by
42	//! `Output.seq`.
43
44	use std::io;
45
46	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
47	use spt_net::net::ndjson;
48
49	use crate::access::{access_check, AccessDecision, InboundClass};
50	use crate::brain::{now_ms, Brain, BrokerEvent};
51	use crate::msg::{decode_bytes, encode_bytes};
52
53	/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
54	/// served viewport is a **driver** — attaching wakes the endpoint, the
55	/// viewport ending detaches it (active → dormant; the transition echo is
56	/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
57	/// [`mark_driven_by`]: a session served without a perch has no resting
58	/// record to move.
59	// [impl->REQ-INST-3]
60	fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {
61	    let _ = crate::resting::daemon_rest_event(endpoint, event, None);
62	}
63
64	/// Why a serve loop returned.
65	#[derive(Debug, Clone, PartialEq, Eq)]
66	pub enum AttachServeOutcome {
67	    /// The access gate refused the requesting origin, OR a `Control` attach hit
68	    /// an endpoint a DIFFERENT operator already controls (busy-refuse,
69	    /// REQ-RCVIEW-1) — nothing was served. The operator's `rc` renders the
70	    /// busy guidance client-side from the endpoint's `driven_by`.
71	    Refused,
72	    /// The session's child exited; the `Exit` record was sent and the stream
73	    /// finished.
74	    Exited(Option<i32>),
75	    /// The operator closed its send side (detached); the viewport ended with
76	    /// the session still running.
77	    Detached,
78	    /// This controller was DISPLACED by a different operator's `--take`/`kick`
79	    /// (REQ-KICK-1): the loud `Displaced` notice was sent and the stream
80	    /// finished. The endpoint stays driven (by the taker) — no dormant edge.
81	    Displaced,
82	}
83
84	/// The role a serve loop resolved to, once the broker's [`SubscribeOutcome`]
85	/// arrives — gates wake (controller-only) and PTY input.
86	#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87	enum ServeRole {
88	    /// The interactive controller (input + resize; wakes the endpoint).
89	    Controller,
90	    /// A read-only viewer (output only; wake-neutral).
91	    Viewer,
92	}
93
94	/// Serve one attach stream against this brain's broker, blocking until the
95	/// session exits, the operator detaches, or the gate refuses.
96	///
97	/// `origin_node` MUST be the handshake-proven remote id from the broker's
98	/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
99	/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
100	/// is being attached, for the gate. `net_from_seq` is this brain's resume
101	/// cursor on the attach stream (0 cold — also the safe post-restart choice:
102	/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
103	/// journal, and re-pumped output dedups at the operator's render cursor).
104	// [impl->REQ-INST-8]
105	// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
106	// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
107	pub fn serve_attach(
108	    brain: &mut Brain,
109	    broker_name: &str,
110	    stream_id: u64,
111	    endpoint: &str,
112	    origin_node: &str,
113	    net_from_seq: u64,
114	) -> io::Result<AttachServeOutcome> {
115	    brain.net_stream_subscribe(stream_id, net_from_seq)?;
116	    // TWO-CONN SPLIT (REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT): the wire FORWARDS
117	    // (session Output / Size / Exit / Displaced / detach-finish onto the operator
118	    // stream) ride a SEPARATE broker connection from `brain`. `brain` RECEIVES
119	    // session output (broker→serve) — including the controller's synchronous,
120	    // inline, multi-frame ring replay (`become_controller`), which holds the
121	    // broker's single-threaded per-conn handler for the whole replay. If the
122	    // forwards rode `brain` too, that stuck handler could not read them →
123	    // serve→broker backs up while broker→serve replay backs up → mutual IPC
124	    // backpressure DEADLOCK, and the operator receives ZERO bytes (the bug: an
125	    // attach to an ALREADY-PRODUCING endpoint-run harness, non-empty ring). The
126	    // two directions on independent single-threaded handlers cannot block each
127	    // other. `send_stream` is stream_id-keyed (conn-agnostic), so forwarding from
128	    // a second conn is correct. Order is preserved: this one serve thread reads
129	    // every Output in seq order on `brain` and forwards it in that same order on
130	    // `wire`. (DEEPER ROOT, post-v0.12.1 follow-up: the CONTROLLER replay is a
131	    // SYNCHRONOUS inline write, while VIEWERS use the async dedicated-writer-
132	    // thread + bounded-channel pattern (`add_viewer`/`viewer_writer`); unifying
133	    // the controller onto that async path would remove the inline-replay block at
134	    // the source, but it touches the authoritative cursor-advancing path.)
135	    let mut wire = Brain::cold_start(broker_name, now_ms())?;
136	    let mut decoder = AttachDecoder::new();
137	    let mut attached = false;
138	    // The Request's intent — gates wire input (a viewer's keystrokes never reach
139	    // the PTY) independent of the broker's later role reply (no IPC-ordering
140	    // race on the input gate).
141	    let mut intent = AttachIntent::Control;
142	    // The broker-confirmed role (arrives as `Subscribed`). `None` until then:

... [241 lines truncated] ...


On v0.13.0-p1b-ack-deadlock @9b0025b (clean — branch switch reverted the diag instrument, which lives only on the diag branch). Now applying direction 2: move the cursor-reset from `attach()` into `subscribe_with` so `attach_as` also gets it. Reading the current state of those fns:


cd C:/Users/decid/Documents/projects/spt-core && rg -n "fn attach\b|fn attach_as|fn subscribe\b|fn subscribe_with|session_cursors.insert|REQ-HAZARD-CONTROLLER-WRITER-REORDER" crates/spt-daemon/src/brain.rs | head -20

Locate attach/subscribe_with and the existing reset


336:    /// ([`REQ-HAZARD-CONTROLLER-WRITER-REORDER`]).
342:    // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]
351:        session_cursors.insert(prior.session_id, prior.next_seq);
796:    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
809:        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]
811:            self.session_cursors.insert(session_id, from_seq);
825:    pub fn attach_as(
1156:    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
1176:    fn subscribe_with(


796	    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
797	        self.session_id = Some(session_id);
798	        self.next_seq = from_seq;
799	        // A resume-mode brain (non-empty map) routes this session's output through
800	        // the per-session map via `read_event`'s first-sight `or_insert` seed
801	        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above.
802	        //
803	        // P1c: `or_insert` only seeds a FIRST-sight session. An EXPLICIT re-attach
804	        // of an already-tracked session (e.g. a from-0 ground-truth re-read after
805	        // a resume, or any deliberate re-subscribe from a different floor) must
806	        // RESET this session's dedup cursor to `from_seq` — otherwise a prior
807	        // seed/advance dedup-drops the very replay the caller asked for. Only in
808	        // resume mode (the seeded map); a legacy brain keeps the `next_seq` path.
809	        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]
810	        if !self.session_cursors.is_empty() {
811	            self.session_cursors.insert(session_id, from_seq);
812	        }
813	        self.subscribe(session_id, from_seq)
814	    }
815
816	    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a
817	    /// role-aware subscribe with `intent` + the operator identity `by` (the
818	    /// handshake-proven origin for a remote attach). The broker resolves the role
819	    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]
820	    /// loop) reads to learn whether it became controller / viewer / was refused.
821	    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the
822	    /// generation is untouched).
823	    ///
824	    /// [`serve_attach`]: crate::attach::serve_attach
825	    pub fn attach_as(
826	        &mut self,
827	        session_id: u64,
828	        from_seq: u64,
829	        intent: AttachIntent,
830	        by: Option<&str>,
831	    ) -> io::Result<()> {
832	        self.session_id = Some(session_id);
833	        self.next_seq = from_seq;
834	        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))
835	    }
836
837	    /// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the
838	    /// broker for **every** hosted session and re-attach **each** in resume mode
839	    /// from the broker's per-session delivered cursor (`resume_seq`). This is the
840	    /// production replacement for the retired single-session `BrainState` handoff

    pub fn attach(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
        self.session_id = Some(session_id);
        self.next_seq = from_seq;
        // A resume-mode brain (non-empty map) routes this session's output through
        // the per-session map via `read_event`'s first-sight `or_insert` seed
        // (D4-2b); a legacy brain (empty map) uses the `next_seq` set above.
        //
        // P1c: `or_insert` only seeds a FIRST-sight session. An EXPLICIT re-attach
        // of an already-tracked session (e.g. a from-0 ground-truth re-read after
        // a resume, or any deliberate re-subscribe from a different floor) must
        // RESET this session's dedup cursor to `from_seq` — otherwise a prior
        // seed/advance dedup-drops the very replay the caller asked for. Only in
        // resume mode (the seeded map); a legacy brain keeps the `next_seq` path.
        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]
        if !self.session_cursors.is_empty() {
            self.session_cursors.insert(session_id, from_seq);
        }
        self.subscribe(session_id, from_seq)
    }

    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a
    /// role-aware subscribe with `intent` + the operator identity `by` (the
    /// handshake-proven origin for a remote attach). The broker resolves the role
    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]
    /// loop) reads to learn whether it became controller / viewer / was refused.
    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the
    /// generation is untouched).
    ///
    /// [`serve_attach`]: crate::attach::serve_attach
    pub fn attach_as(
        &mut self,
        session_id: u64,
        from_seq: u64,
        intent: AttachIntent,
        by: Option<&str>,
    ) -> io::Result<()> {
        self.session_id = Some(session_id);
        self.next_seq = from_seq;
        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))
    }

    /// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the
    /// broker for **every** hosted session and re-attach **each** in resume mode
    /// from the broker's per-session delivered cursor (`resume_seq`). This is the
    /// production replacement for the retired single-session `BrainState` handoff


1156	    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
1157	        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)
1158	    }
1159
1160	    /// Release this connection's controller/viewer role on `session_id`
1161	    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when
1162	    /// the viewport ends so the broker clears `driven_by` / `viewer_count`
1163	    /// promptly. Best-effort: a dropped connection clears the role anyway.
1164	    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {
1165	        self.send(
1166	            KIND_UNSUBSCRIBE,
1167	            serde_json::to_value(UnsubscribeReq { session_id })
1168	                .expect("UnsubscribeReq serializes"),
1169	        )
1170	    }
1171
1172	    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/
1173	    /// take, `by` carries the operator identity (`Some(node)` remote, `None`
1174	    /// local). The broker replies the resolved [`SubscribeOutcome`] as a
1175	    /// [`BrokerEvent::Subscribed`].
1176	    fn subscribe_with(
1177	        &mut self,
1178	        session_id: u64,
1179	        from_seq: u64,
1180	        intent: AttachIntent,
1181	        by: Option<String>,
1182	    ) -> io::Result<()> {
1183	        self.send(
1184	            KIND_SUBSCRIBE,
1185	            serde_json::to_value(SubscribeReq {
1186	                session_id,
1187	                from_seq,
1188	                intent,
1189	                by,
1190	            })
1191	            .expect("SubscribeReq serializes"),
1192	        )
1193	    }
1194
1195	    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {

    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)
    }

    /// Release this connection's controller/viewer role on `session_id`
    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when
    /// the viewport ends so the broker clears `driven_by` / `viewer_count`
    /// promptly. Best-effort: a dropped connection clears the role anyway.
    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {
        self.send(
            KIND_UNSUBSCRIBE,
            serde_json::to_value(UnsubscribeReq { session_id })
                .expect("UnsubscribeReq serializes"),
        )
    }

    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/
    /// take, `by` carries the operator identity (`Some(node)` remote, `None`
    /// local). The broker replies the resolved [`SubscribeOutcome`] as a
    /// [`BrokerEvent::Subscribed`].
    fn subscribe_with(
        &mut self,
        session_id: u64,
        from_seq: u64,
        intent: AttachIntent,
        by: Option<String>,
    ) -> io::Result<()> {
        self.send(
            KIND_SUBSCRIBE,
            serde_json::to_value(SubscribeReq {
                session_id,
                from_seq,
                intent,
                by,
            })
            .expect("SubscribeReq serializes"),
        )
    }

    fn send(&mut self, kind: &str, payload: serde_json::Value) -> io::Result<()> {


Now the fix — move the reset into `subscribe_with` (covers `attach` AND `attach_as`):