commit 677c3e7cd620c3a532ffc884ba20f70c82dc4786
Author: Reavo End <decidel@gmail.com>
Date:   Sun Jun 21 02:19:38 2026 -0700

    fix(broker): decouple controller backpressure from the drain so a slow controller can't starve a concurrent viewer (b4)

    REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE. Root 'b4' (doyle's
    code-read + verification, warm-forkpty 3-gate counters): W1 moved the controller
    SOCKET WRITE off the drain (controller_writer thread) but left the HANDOFF as an
    INLINE try_send sleep-poll (ControllerJob::deliver, up to CONTROLLER_WRITE_DEADLINE)
    ON the drain closure. So a controller draining slower than the PTY floods filled
    its 4096-deep channel, the inline poll throttled the drain to the controller's read
    rate, and OutputLog::append's viewer fan-out stopped running -> a concurrent
    `rc --view` viewer received only the initial replay then nothing (a_journaled warm
    forkpty: c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery for
    the whole run; forkpty-only because forkpty floods harder than Windows ConPTY).

    FIX: the controller handoff becomes a SINGLE non-blocking try_send, exactly like a
    viewer, done in append() under the log lock; the drain NEVER sleeps. append() now
    returns Option<evict-epoch>; the drain off-lock turns Some(epoch) into
    mark_controller_gone. A slow-but-alive controller DROPS the chunk + falls behind the
    ring (resume-from-floor, the existing reconnect case); a controller whose channel
    stays Full for CONTROLLER_WRITE_DEADLINE since its last successful handoff
    (ControllerSink::last_ok, stateful across chunks) is EVICTED (bounded-wedge property
    preserved). ControllerJob + its sleep-poll deliver() and CONTROLLER_SEND_POLL are
    deleted.

    B2 GAPLESS-HANDOFF preserved via a CONTIGUOUS cursor (doyle's gate): the drop-on-full
    can SKIP a controller seq, and a high-watermark advance PAST the gap would make a
    cold brain's resume_seq skip the dropped chunk = a not-exactly-once resume. So
    controller_writer's live path advances delivered_through via the new
    `contiguous_advance` kernel — a single compare_exchange(seq -> seq+1) that advances
    ONLY when the cursor is exactly `seq`; a gap FREEZES it at last-contiguous (a
    re-attaching brain replays from there / D4 floor-clamps). The initial-batch replay
    keeps the high-watermark advance (contiguous by construction + handles D4 forward-
    resume). In the no-drop path this is byte-identical to before. Epoch gate /
    one-live-writer / monotonic on-wire seq unchanged.

    Tests (in-process, no PTY): a_full_controller_does_not_starve_a_viewer_or_block_the_drain
    (a never-drained controller drops + never blocks + never evicts within the deadline
    while the viewer gets every frame; evicts at its epoch past the deadline) and
    contiguous_advance_freezes_on_a_gap (advance on next-expected, freeze on gap, no-op
    on rewind). INT (a_journaled warm forkpty) activates when b4 folds to delivery-
    control alongside pump-carrier-fix (rule 5).

    Closes a_journaled only; p0_paste (viewer skip-to-live) + g2 (re-measure post-b4)
    are tracked separately in V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md.

    Co-authored by: todlando

 V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md | 197 +++++++++++++++++++++
 crates/spt-daemon/src/broker.rs      | 322 ++++++++++++++++++++---------------
 traceable-reqs.toml                  |   6 +
 3 files changed, 385 insertions(+), 140 deletions(-)
=====DIFF=====
commit 677c3e7cd620c3a532ffc884ba20f70c82dc4786
Author: Reavo End <decidel@gmail.com>
Date:   Sun Jun 21 02:19:38 2026 -0700

    fix(broker): decouple controller backpressure from the drain so a slow controller can't starve a concurrent viewer (b4)

    REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE. Root 'b4' (doyle's
    code-read + verification, warm-forkpty 3-gate counters): W1 moved the controller
    SOCKET WRITE off the drain (controller_writer thread) but left the HANDOFF as an
    INLINE try_send sleep-poll (ControllerJob::deliver, up to CONTROLLER_WRITE_DEADLINE)
    ON the drain closure. So a controller draining slower than the PTY floods filled
    its 4096-deep channel, the inline poll throttled the drain to the controller's read
    rate, and OutputLog::append's viewer fan-out stopped running -> a concurrent
    `rc --view` viewer received only the initial replay then nothing (a_journaled warm
    forkpty: c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery for
    the whole run; forkpty-only because forkpty floods harder than Windows ConPTY).

    FIX: the controller handoff becomes a SINGLE non-blocking try_send, exactly like a
    viewer, done in append() under the log lock; the drain NEVER sleeps. append() now
    returns Option<evict-epoch>; the drain off-lock turns Some(epoch) into
    mark_controller_gone. A slow-but-alive controller DROPS the chunk + falls behind the
    ring (resume-from-floor, the existing reconnect case); a controller whose channel
    stays Full for CONTROLLER_WRITE_DEADLINE since its last successful handoff
    (ControllerSink::last_ok, stateful across chunks) is EVICTED (bounded-wedge property
    preserved). ControllerJob + its sleep-poll deliver() and CONTROLLER_SEND_POLL are
    deleted.

    B2 GAPLESS-HANDOFF preserved via a CONTIGUOUS cursor (doyle's gate): the drop-on-full
    can SKIP a controller seq, and a high-watermark advance PAST the gap would make a
    cold brain's resume_seq skip the dropped chunk = a not-exactly-once resume. So
    controller_writer's live path advances delivered_through via the new
    `contiguous_advance` kernel — a single compare_exchange(seq -> seq+1) that advances
    ONLY when the cursor is exactly `seq`; a gap FREEZES it at last-contiguous (a
    re-attaching brain replays from there / D4 floor-clamps). The initial-batch replay
    keeps the high-watermark advance (contiguous by construction + handles D4 forward-
    resume). In the no-drop path this is byte-identical to before. Epoch gate /
    one-live-writer / monotonic on-wire seq unchanged.

    Tests (in-process, no PTY): a_full_controller_does_not_starve_a_viewer_or_block_the_drain
    (a never-drained controller drops + never blocks + never evicts within the deadline
    while the viewer gets every frame; evicts at its epoch past the deadline) and
    contiguous_advance_freezes_on_a_gap (advance on next-expected, freeze on gap, no-op
    on rewind). INT (a_journaled warm forkpty) activates when b4 folds to delivery-
    control alongside pump-carrier-fix (rule 5).

    Closes a_journaled only; p0_paste (viewer skip-to-live) + g2 (re-measure post-b4)
    are tracked separately in V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md.

    Co-authored by: todlando

diff --git a/crates/spt-daemon/src/broker.rs b/crates/spt-daemon/src/broker.rs
index 87cf45f..587f3b2 100644
--- a/crates/spt-daemon/src/broker.rs
+++ b/crates/spt-daemon/src/broker.rs
@@ -119,26 +119,25 @@ struct ViewerSink {
 }

 /// The controller live-frame channel depth (frames). Deeper than a viewer's
-/// queue because the controller is the AUTHORITATIVE view — it is never silently
-/// evicted on overflow (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST); instead the
-/// drain blocks on a bounded `send_timeout` and the controller is detached only
-/// after [`CONTROLLER_WRITE_DEADLINE`] of a fully-stalled socket. Generous so a
-/// momentary slow consumer never trips the deadline.
+/// queue because the controller is the AUTHORITATIVE view — unlike a viewer it is
+/// NOT evicted on a SINGLE full `try_send` (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
+/// b4: the drain does ONE non-blocking `try_send` per chunk and NEVER blocks; a
+/// merely-slow controller DROPS chunks + falls behind the ring, and only a channel
+/// that stays full for [`CONTROLLER_WRITE_DEADLINE`] (tracked via
+/// [`ControllerSink::last_ok`]) is detached. Generous so a momentary slow consumer
+/// never trips the deadline.
 const CONTROLLER_CHANNEL_DEPTH: usize = 4096;

-/// How long the drain tolerates a CONTROLLER whose socket has fully stalled (its
-/// channel stays full) before declaring it gone, detaching it, and resuming
-/// (W1). This BOUNDS the wedge: a backed-up controller can stall live output for
-/// at most this long — never forever (the v0.12.x root) — and never blocks the
-/// log lock (the send happens off-lock; see [`OutputLog::append`]). A live but
-/// merely slow operator drains a slot before this elapses and is untouched.
+/// How long a CONTROLLER's channel may stay FULL (no successful handoff since
+/// [`ControllerSink::last_ok`]) before the drain declares it stalled and detaches it
+/// (W1, refined by b4 / REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE). This
+/// BOUNDS the wedge: a backed-up controller falls behind for at most this long before
+/// eviction — never forever (the v0.12.x root) — and the drain NEVER blocks or sleeps
+/// (a single off-lock `try_send` per chunk; see [`OutputLog::append`]). A live but
+/// merely slow operator lands a frame before this elapses (resetting `last_ok`) and is
+/// untouched.
 const CONTROLLER_WRITE_DEADLINE: Duration = Duration::from_secs(5);

-/// Poll interval for the controller `try_send` deadline loop (std `SyncSender`
-/// has no stable `send_timeout`). Only used while the channel is FULL — the
-/// common (has-room) path never sleeps.
-const CONTROLLER_SEND_POLL: Duration = Duration::from_millis(5);
-
 /// MANDATORY bound on one injected keystroke sequence (W2 Layer C, ADR-0022 /
 /// doyle non-negotiable = W1's "never park forever"): the inject worker holds the
 /// floor — buffering operator input — only while a binary's response to one
@@ -180,6 +179,14 @@ struct ControllerSink {
     /// The controller's identity: `Some(node)` = a remote operator, `None` =
     /// local (the brain's spawn-reader / resume). Ownership keys on this.
     by: Option<String>,
+    /// Time of the last SUCCESSFUL handoff (`try_send` Ok) to this controller's
+    /// writer channel (b4, REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE).
+    /// The drain does a SINGLE non-blocking `try_send` per chunk and NEVER sleeps;
+    /// a channel that stays Full for [`CONTROLLER_WRITE_DEADLINE`] PAST this stamp
+    /// evicts the stalled controller (bounded wedge preserved), while a slow-but-
+    /// alive controller just drops the chunk + falls behind the ring (resume-from-
+    /// floor). Seeded `now` at [`OutputLog::become_controller`].
+    last_ok: Instant,
     /// The writer thread handle (held for ownership; exits when `tx` drops — the
     /// drain's bounded `send_timeout` then sees `Disconnected` and detaches — or
     /// when a socket write fails).
@@ -283,23 +290,31 @@ impl OutputLog {
         }
     }

-    /// Append a freshly-produced output chunk: assign the next `seq`, retain it
-    /// in the bounded ring, FAN OUT to viewers (non-blocking `try_send`, evicting
-    /// any overflowed/gone queue — REQ-HAZARD-VIEWER-ISOLATION), and HAND the
-    /// controller frame back to the caller as a [`ControllerJob`] to deliver
-    /// **off the log lock** (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST).
+    /// Append a freshly-produced output chunk: assign the next `seq`, retain it in
+    /// the bounded ring, then FAN OUT to viewers AND the controller — both a SINGLE
+    /// non-blocking `try_send`, so the drain NEVER blocks/sleeps here. Returns
+    /// `Some(epoch)` when the controller must be EVICTED (stalled past the deadline,
+    /// or its writer is gone) so the drain can `mark_controller_gone(epoch)` OFF the
+    /// log lock; `None` otherwise.
     ///
-    /// W1: the controller write is no longer inline here. The drain used to call
-    /// `write_frame(controller)` synchronously while holding `Mutex<OutputLog>`,
-    /// so a backed-up controller socket parked the drain thread WITH THE LOCK HELD
-    /// — wedging the whole session (every attach / `KIND_SESSIONS` that needs the
-    /// log lock blocked). Now this returns the job and releases the lock; the
-    /// drain does a bounded, off-lock send into the controller's writer-thread
-    /// channel ([`ControllerJob::deliver`]). Called only from the session's
+    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the
+    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but
+    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to
+    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower
+    /// than the PTY floods filled its channel, the inline poll throttled the drain,
+    /// and the viewer fan-out below stopped running → a concurrent `rc --view`
+    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`
+    /// exactly like a viewer: the drain runs at full PTY speed regardless of
+    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind
+    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`
+    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted
+    /// (bounded wedge preserved). The writer still does the blocking socket write +
+    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's
     /// single drain thread.
     // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
+    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
     #[must_use]
-    fn append(&mut self, bytes: &[u8]) -> Option<ControllerJob> {
+    fn append(&mut self, bytes: &[u8]) -> Option<u64> {
         let seq = self.next_seq;
         self.next_seq += 1;
         self.ring.push_back((seq, bytes.to_vec()));
@@ -322,16 +337,31 @@ impl OutputLog {
                 self.stamp_viewer_count();
             }
         }
-        // Controller: hand the frame to the caller for an off-lock, bounded send
-        // into the controller writer thread's channel (which does the blocking
-        // socket write + advances the authoritative cursor). The drain never
-        // blocks under the log lock again.
-        self.controller.as_ref().map(|c| ControllerJob {
-            tx: c.tx.clone(),
-            seq,
-            frame,
-            epoch: self.controller_epoch.load(Ordering::Acquire),
-        })
+        // Controller: the SAME non-blocking discipline as a viewer (b4). One
+        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a
+        // slow controller falls behind the ring) unless it has been Full for the
+        // whole deadline since its last success → EVICT. Disconnected (writer gone)
+        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.
+        let now = Instant::now();
+        let epoch = self.controller_epoch.load(Ordering::Acquire);
+        if let Some(c) = self.controller.as_mut() {
+            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {
+                Ok(()) => {
+                    c.last_ok = now;
+                    None
+                }
+                Err(TrySendError::Full(_)) => {
+                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {
+                        Some(epoch) // stalled past the deadline → bounded detach
+                    } else {
+                        None // drop this chunk; controller falls behind the ring
+                    }
+                }
+                Err(TrySendError::Disconnected(_)) => Some(epoch),
+            }
+        } else {
+            None
+        }
     }

     /// The current controller's identity (`Some(node)` remote / `None` local),
@@ -381,6 +411,7 @@ impl OutputLog {
             tx,
             send: sub,
             by,
+            last_ok: Instant::now(),
             _writer: writer,
         });
         self.stamp_driven_by();
@@ -641,50 +672,6 @@ enum CtrlMsg {
     Control(Envelope),
 }

-/// One live-output delivery to the controller, handed back by
-/// [`OutputLog::append`] so the drain can do the bounded send OFF the log lock
-/// (W1 / REQ-HAZARD-INJECT-CONTROL-COEXIST). [`ControllerJob::deliver`] blocks at
-/// most [`CONTROLLER_WRITE_DEADLINE`]; on a fully-stalled controller it returns
-/// the `epoch` so the drain can detach exactly that generation.
-struct ControllerJob {
-    tx: SyncSender<CtrlMsg>,
-    seq: u64,
-    frame: Envelope,
-    epoch: u64,
-}
-
-impl ControllerJob {
-    /// Deliver into the controller's writer-thread channel with a bounded
-    /// deadline. `Ok(())` = queued (the writer does the blocking socket write);
-    /// `Err(epoch)` = the controller is fully stalled or gone and must be
-    /// detached (the wedge is thereby BOUNDED, never permanent — the v0.12.x
-    /// root). A live-but-slow controller drains a slot before the deadline and is
-    /// untouched.
-    ///
-    /// A `try_send` poll loop (std `SyncSender` has no stable `send_timeout`): the
-    /// common case (channel has room) succeeds on the first `try_send` with zero
-    /// added latency; only a FULL channel polls every [`CONTROLLER_SEND_POLL`] up
-    /// to [`CONTROLLER_WRITE_DEADLINE`]. This runs OFF the log lock (in the drain
-    /// closure), so a stalled controller never holds the lock.
-    fn deliver(self) -> Result<(), u64> {
-        let mut msg = CtrlMsg::Output(self.seq, self.frame);
-        let deadline = Instant::now() + CONTROLLER_WRITE_DEADLINE;
-        loop {
-            match self.tx.try_send(msg) {
-                Ok(()) => return Ok(()),
-                Err(TrySendError::Disconnected(_)) => return Err(self.epoch),
-                Err(TrySendError::Full(returned)) => {
-                    if Instant::now() >= deadline {
-                        return Err(self.epoch);
-                    }
-                    msg = returned;
-                    thread::sleep(CONTROLLER_SEND_POLL);
-                }
-            }
-        }
-    }
-}
-
 /// Advance the shared `delivered_through` cursor to `seq + 1`, monotonically
 /// (D4-1, via [`advanced_cursor`]) and CAS-safe against a brief two-writer
 /// overlap during a controller displace.
@@ -702,6 +689,20 @@ fn advance_delivered(dt: &AtomicU64, seq: u64) {
     }
 }

+/// Advance the controller resume cursor CONTIGUOUSLY (b4 / the (a) B2-gate): move to
+/// `seq + 1` ONLY when the cursor is EXACTLY `seq` (the next expected seq). A gap
+/// (`seq > cursor` — a frame the drain dropped while the controller channel was Full,
+/// the b4 backpressure) FREEZES the cursor at last-contiguous, so a re-attaching
+/// brain's `resume_seq` never skips a dropped chunk (a high-watermark jump PAST the
+/// gap would be a not-exactly-once / non-gapless resume = a B2 violation). An
+/// already-delivered `seq` (`seq < cursor`, a rewind re-send) is a no-op. A single CAS,
+/// no loop: it advances by exactly one, or not at all. In the NO-DROP path (every
+/// live frame is contiguous) this is identical to [`advance_delivered`].
+// [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
+fn contiguous_advance(dt: &AtomicU64, seq: u64) {
+    let _ = dt.compare_exchange(seq, seq + 1, Ordering::AcqRel, Ordering::Acquire);
+}
+
 /// The interactive CONTROLLER's dedicated writer thread (W1 /
 /// REQ-HAZARD-INJECT-CONTROL-COEXIST) — the authoritative analog of
 /// [`viewer_writer`]. Write the `initial` ring-replay batch (advancing the resume
@@ -769,7 +770,12 @@ fn controller_writer(
             return;
         }
         if let Some(seq) = seq {
-            advance_delivered(&delivered_through, seq);
+            // (a) CONTIGUOUS cursor (b4 / B2 gapless-resume — doyle's gate): with the
+            // drop-on-full handoff in `append`, the controller's live channel can SKIP
+            // a seq (dropped while Full). The frame still went to the socket (the live
+            // operator sees it), but the durable resume cursor must NOT jump past the
+            // gap — see [`contiguous_advance`].
+            contiguous_advance(&delivered_through, seq);
         }
     }
 }
@@ -1448,11 +1454,14 @@ impl Broker {
         let log_drain = Arc::clone(&log);
         let drain = session
             .drain(move |chunk| {
-                let job = log_drain.lock().unwrap().append(chunk);
-                if let Some(job) = job {
-                    if let Err(epoch) = job.deliver() {
-                        log_drain.lock().unwrap().mark_controller_gone(epoch);
-                    }
+                // append() does the controller handoff inline now (one non-blocking
+                // try_send — b4) and returns Some(epoch) ONLY when the controller is
+                // stalled-past-deadline / gone and must be evicted. mark it gone OFF
+                // the first lock (a fresh lock, no self-deadlock). The drain never
+                // sleeps here, so a slow controller can't throttle the viewer fan-out.
+                let evict = log_drain.lock().unwrap().append(chunk);
+                if let Some(epoch) = evict {
+                    log_drain.lock().unwrap().mark_controller_gone(epoch);
                 }
             })
             .map_err(|e| format!("drain failed: {e}"))?;
@@ -2266,64 +2275,97 @@ mod tests {
         );
     }

-    /// W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the off-lock controller delivery
-    /// kernel. `deliver` is BOUNDED, never a permanent park (the v0.12.x wedge):
-    /// a channel with room queues instantly; a DISCONNECTED writer returns the
-    /// epoch (detach); a FULL channel that never drains returns the epoch after
-    /// the deadline (detach), instead of blocking the drain forever.
-    // [unit->REQ-HAZARD-INJECT-CONTROL-COEXIST]
+    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): a CONTROLLER whose
+    /// channel is FULL (it reads slower than the PTY floods) must NOT starve a
+    /// concurrent viewer and must NOT block the drain. `append` does ONE non-blocking
+    /// `try_send` to the controller: a full channel within the deadline DROPS (returns
+    /// `None`, never blocks), the viewer fan-out still runs, and only a controller Full
+    /// for `CONTROLLER_WRITE_DEADLINE` since its last success is evicted. (Pre-b4 the
+    /// drain inline-sleep-polled up to 5s on a full controller, starving the viewer —
+    /// the warm-forkpty a_journaled wedge.)
+    // [unit->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
     #[test]
-    fn controller_deliver_is_bounded_never_a_permanent_park() {
-        let frame = output_envelope(1, 0, b"x");
-        // Room → queued instantly (the common path; no deadline wait).
-        let (tx, _rx) = sync_channel::<CtrlMsg>(CONTROLLER_CHANNEL_DEPTH);
-        let job = ControllerJob {
-            tx: tx.clone(),
-            seq: 0,
-            frame: frame.clone(),
-            epoch: 7,
-        };
-        assert!(job.deliver().is_ok(), "a channel with room queues the chunk");
-
-        // Disconnected writer (its rx dropped) → Err(epoch), no wait.
-        let (tx2, rx2) = sync_channel::<CtrlMsg>(1);
-        drop(rx2);
-        let job = ControllerJob {
-            tx: tx2,
-            seq: 0,
-            frame: frame.clone(),
-            epoch: 7,
-        };
+    fn a_full_controller_does_not_starve_a_viewer_or_block_the_drain() {
+        let mut log = OutputLog::new(1, 1024, String::new(), (24, 80));
+        log.controller_epoch.store(9, Ordering::Release);
+        // A CONTROLLER whose 2-slot channel we NEVER drain (rx held, unread) — a
+        // controller reading slower than the flood. No real writer thread is needed:
+        // the b4 decision lives in append's `try_send`, and `send` is never written here.
+        let (csend, _cclient, _crecv) = controller_socket_pair();
+        let (ctx, _crx) = sync_channel::<CtrlMsg>(2);
+        log.controller = Some(ControllerSink {
+            tx: ctx,
+            send: csend,
+            by: None,
+            last_ok: Instant::now(),
+            _writer: thread::spawn(|| {}),
+        });
+        // A VIEWER with a generous channel we CAN drain — to prove it stays fed.
+        let (vsend, _vclient, _vrecv) = controller_socket_pair();
+        let (vtx, vrx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
+        log.viewers
+            .insert(0, ViewerSink { tx: vtx, send: vsend, _writer: thread::spawn(|| {}) });
+
+        // Append 50 chunks. The controller channel (depth 2) fills after 2; every
+        // further append DROPS (returns None — within the deadline, never evict) and
+        // NEVER blocks. The viewer receives ALL 50 — the b4 fix: a full/slow
+        // controller no longer throttles the drain or the viewer fan-out.
+        for i in 0..50u32 {
+            let evict = log.append(format!("chunk-{i}").as_bytes());
+            assert_eq!(
+                evict, None,
+                "a full controller within the deadline drops + never blocks + never evicts"
+            );
+        }
+        let mut got = 0;
+        while vrx.try_recv().is_ok() {
+            got += 1;
+        }
         assert_eq!(
-            job.deliver(),
-            Err(7),
-            "a disconnected controller writer detaches at its epoch"
+            got, 50,
+            "the viewer is NOT starved by a full/slow controller (b4: the drain runs free)"
         );

-        // Full channel that never drains → Err(epoch) after the deadline (bounded),
-        // NOT a permanent block. Use a tiny depth + a fill so the next send is Full.
-        let (tx3, _rx3) = sync_channel::<CtrlMsg>(1);
-        tx3.try_send(CtrlMsg::Output(0, frame.clone())).expect("fill the slot");
-        let job = ControllerJob {
-            tx: tx3,
-            seq: 1,
-            frame,
-            epoch: 9,
-        };
-        let start = Instant::now();
+        // Age the controller PAST the deadline since its last success → the next Full
+        // append evicts it at its epoch (bounded wedge), still without blocking.
+        if let Some(c) = log.controller.as_mut() {
+            c.last_ok = Instant::now()
+                .checked_sub(CONTROLLER_WRITE_DEADLINE + Duration::from_secs(1))
+                .expect("CI uptime exceeds the deadline");
+        }
         assert_eq!(
-            job.deliver(),
-            Err(9),
-            "a fully-stalled controller detaches after the deadline"
-        );
-        assert!(
-            start.elapsed() >= CONTROLLER_WRITE_DEADLINE,
-            "deliver waits the full deadline before declaring a stalled controller gone"
+            log.append(b"past-deadline"),
+            Some(9),
+            "a controller Full past CONTROLLER_WRITE_DEADLINE is evicted at its epoch"
         );
-        assert!(
-            start.elapsed() < CONTROLLER_WRITE_DEADLINE + Duration::from_secs(5),
-            "deliver returns shortly after the deadline (bounded, never permanent)"
+    }
+
+    /// (a) contiguous cursor kernel (b4 / B2 gapless-resume — doyle's gate): the
+    /// controller resume cursor advances ONLY on the next-expected seq; a gap (a
+    /// frame the drain dropped while the controller was Full) FREEZES it, and a
+    /// rewind re-send cannot lower it.
+    // [unit->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
+    #[test]
+    fn contiguous_advance_freezes_on_a_gap() {
+        let dt = AtomicU64::new(0);
+        contiguous_advance(&dt, 0);
+        assert_eq!(dt.load(Ordering::Acquire), 1, "seq 0 (== cursor) advances to 1");
+        contiguous_advance(&dt, 1);
+        assert_eq!(dt.load(Ordering::Acquire), 2, "contiguous seq 1 advances to 2");
+        // GAP: cursor is 2 but seq 5 arrives (3,4 dropped while Full). FREEZE at 2 —
+        // a high-watermark jump to 6 would skip 3,4 on resume = a B2 violation.
+        contiguous_advance(&dt, 5);
+        assert_eq!(
+            dt.load(Ordering::Acquire),
+            2,
+            "a gap (seq > cursor) freezes the cursor at last-contiguous (B2 gapless)"
         );
+        // Re-delivering the frozen seq (2, via ring replay) resumes contiguous advance.
+        contiguous_advance(&dt, 2);
+        assert_eq!(dt.load(Ordering::Acquire), 3, "re-delivering the frozen seq resumes");
+        // A rewind re-send (seq < cursor) is a no-op.
+        contiguous_advance(&dt, 0);
+        assert_eq!(dt.load(Ordering::Acquire), 3, "a rewind re-send cannot lower the cursor");
     }

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


Read b4. Pure controller-handoff/drain change — nothing in attach/subscribe path. So Windows `subscribed=false` regression is indirect. Let me check the JIT doc for skip-to-live + p0_paste scope before replying.


1	# V0.13.0 — viewer-drain decouple (b4 root) — JIT plan
2
3	**Status:** b4 fix IMPLEMENTED + verified + PUSHED → `origin/viewer-drain-decouple-b4` @ **677c3e7** (off delivery-control@18416fd). Local gates ALL GREEN: broker unit tests 10/10 (2 new b4 tests + regressions), workspace clippy -D warnings clean, traceable-reqs exit 0. AWAITING doyle's post-b4 forkpty re-measure (cherry-pick 677c3e7 onto PR #27 + v5 tally). Expect a_journaled GREEN, p0_paste still RED (next), g2 TBD (may close indirectly). NEXT items: p0_paste skip-to-live (gated, below) + g2 (re-measure post-b4).
4
5	## ⚠ v0.13.0 IS THREE ITEMS, NOT ONE (doyle's 3-gate counters, run 27898894089)
6
7	The 3 RED gates have THREE DISTINCT roots. b4 closes ONLY a_journaled. Do NOT ship
8	b4 and expect a clean board.
9
10	1. **a_journaled** — c1=0, EVICT=0, journaled_ops=13139, got_output=FALSE = **b4
11	   drain-throttle** (controller deliver blocks the drain inline; viewer never fanned).
12	   ✅ **THE b4 FIX (this plan) CLOSES THIS.**
13	2. **p0_paste** — EVICT FIRED ("dropped 1 viewer at seq 35584, drain_appends=35585"),
14	   pumped=35658, attach_received_output=FALSE, backpressured=true, BOTH platforms.
15	   = the drain ran FREE; the VIEWER (serve_attach's broker subscription) overflowed
16	   its 256 channel + was EVICTED because serve_attach forwards
17	   (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the
18	   drain fans out. **SEPARATE root. b4 does NOT touch it.** Triage lean (gate pending
19	   doyle): eviction-of-a-hopelessly-behind-viewer is correct session-protection; the
20	   bug is it's SILENT + PERMANENT. RIGHT product = SKIP-TO-LIVE — serve_attach
21	   re-subscribes from the current ring floor on eviction (like tail -f: see live, not
22	   die replaying an uncatchable backlog).
23	   **GATED (doyle): skip-to-live is REAL, not relax.** Eviction itself = correct
24	   (keep it); SILENT+PERMANENT eviction = the bug. VIEWER-only → B2-SAFE (viewers do
25	   NOT advance delivered_through / not authoritative; no cursor exposure). Design:
26	   (1) explicit broker→viewer EVICTION SIGNAL (unambiguous marker, DISTINCT from
27	   session-exit EOF — serve must not tear down a live attach on it) sent to the
28	   evicted viewer's conn; (2) serve_attach re-subscribes from the CURRENT ring floor
29	   (skip-to-live) on that signal; (3) HARD constraint: NO evict→resubscribe busy-loop
30	   — rate-limit / forward-progress so under max-flood the viewer sees intermittent
31	   LIVE bursts, never a CPU spin. Do NOT lead with serve stall/gap-detection (fragile
32	   fallback only). SEPARATE fix, AFTER b4.
33	3. **g2** — assert :1196 `assert!(delivered)` = the broker did NOT ACCEPT the injected
34	   endpoint event within 8s (NOT a got_output failure). forkpty-only. = inject-
35	   acceptance (dispatch_endpoint_input / inject path) starved by the output-flood
36	   CPU storm. **Distinct.** b4 frees drain-thread CPU (deletes the 5s inline poll), so
37	   b4 MAY close g2 indirectly — **RE-MEASURE g2 AFTER b4 lands before designing a
38	   separate fix.** If still red → real inject-acceptance-must-not-starve fix.
39
40	**v0.13.0 close = b4 (this plan) + p0_paste skip-to-live (doyle-gate pending) + g2
41	(re-measure post-b4, then decide).** This JIT plan covers item 1 (b4); items 2 & 3
42	are captured here but not yet designed/gated.
43
44	## The bug (b4 — confirmed by doyle's code read + todlando's verification)
45
46	The 3 inject_control_wedge gates fail WARM on forkpty (a_journaled/g2 Linux-only;
47	p0_paste both platforms) with subscribed=TRUE, got_output=FALSE. Localized via the
48	v4 counters: **c1=0, c2=0, c3=1, EVICT=0**.
49
50	Root: the session drain closure (`broker.rs:1450-1457`) runs `job.deliver()`
51	**inline**. `ControllerJob::deliver` (`broker.rs:669-685`) is a `try_send`
52	sleep-poll loop: on a FULL controller channel it sleeps `CONTROLLER_SEND_POLL`
53	and re-polls up to `CONTROLLER_WRITE_DEADLINE` (5s), **on the drain thread**. So a
54	controller that drains slower than the PTY floods → its `CONTROLLER_CHANNEL_DEPTH`
55	(4096) channel fills → `deliver()` throttles the drain → `append()` (and its viewer
56	`try_send` fan-out) doesn't run → **the viewer starves**. EVICT=0 because the
57	throttled drain is too slow to even fill the viewer's 256 channel.
58
59	No-recovery at 18.97s = steady-state-near-full: the controller socket drains JUST
60	enough that each `deliver()` gets a slot within 5s → returns Ok → controller is
61	NEVER deadline-evicted → drain throttled the WHOLE run (not 5s episodes).
62
63	Controller identity in a_journaled: after the driver's `w.attach(sid,0)` (Control,
64	by=None) it `become_controller`s over the spawner (same-identity local re-take) →
65	the **driver** is the throttling controller, and its PACED read (the 20ms+5ms
66	anti-CPU-storm pacing, Unix-only) is slow enough under the forkpty `yes` flood to
67	keep its channel full. forkpty-only because forkpty floods harder than Windows
68	ConPTY's paced output.
69
70	Regression verdict: NOT a new class. Pre-W1 a full controller wedged the drain
71	PERMANENTLY; W1 (8b5583e) bounded it to 5s episodes but STILL couples drain
72	throughput to the controller. The NEW gates are the first to ASSERT the
73	viewer-not-starved-by-a-busy-session property — a LEGITIMATE product property
74	(rc --view of a noisy session must work) → REAL FIX, not pathological-expectation.
75
76	## Fix direction — RESOLVED design (doyle-gated reshape, sent for final gate)
77
78	doyle's constraint 2 collapsed the design tension: a slow-but-alive controller does
79	NOT need gapless live delivery — it degrades to the EXISTING ring-fall-behind +
80	resume-from-floor (RESTORATION-D4 clamp / snap-above render cursor), the same as the
81	reconnect case. So the controller becomes viewer-like (try_send drop-on-full) + a
82	staleness-evict. No ring-replay-on-recovery machinery needed.
83
84	THE RESHAPE: controller backpressure becomes a SINGLE non-blocking `try_send`,
85	identical to viewers; `ControllerJob::deliver()`'s 5s sleep-poll is DELETED. A single
86	try_send never blocks, so doing it UNDER the log lock is safe (viewers already do
87	exactly that in append). So move the controller send INTO append() (under the lock,
88	beside viewer fan-out); the drain closure loses the deliver() call entirely.
89
90	- `ControllerSink` += `last_ok: Instant` (stateful staleness in the sink under the
91	  log, survives across chunks; set = now at `become_controller`).
92	- `OutputLog::append` (under log lock), controller arm:
93	    match c.tx.try_send(CtrlMsg::Output(seq, frame)) {
94	      Ok(())               => c.last_ok = now,                                  // healthy
95	      Err(Full(_))         => if now - c.last_ok >= CONTROLLER_WRITE_DEADLINE { evict = Some(epoch) }
96	                              else { /* DROP — controller falls behind the ring */ },
97	      Err(Disconnected(_)) => evict = Some(epoch),
98	    }
99	  append returns `Option<evict-epoch>` (replaces the ControllerJob return). Drain
100	  off-lock: `if Some(epoch) => mark_controller_gone(epoch)`. NO deliver() loop, NO
101	  sleep — drain never blocks.
102
103	### Maps to doyle's 3 gate constraints (B2 gapless-handoff is load-bearing)
104
105	1. `delivered_through` advances ONLY on a real successful socket write — UNCHANGED:
106	   `controller_writer` advances it on write; a DROPPED frame never enters the channel
107	   → never written → never advances. Zero phantom-advance.
108	2. Slow-but-alive controller drops frames under flood → on-wire forward-jumps →
109	   operator snap-above cursor / RESTORATION-D4 clamp = existing fall-behind-ring case.
110	   No hard gap, no drain backpressure.
111	3. Truly-stalled controller: `last_ok` stops advancing → CONTROLLER_WRITE_DEADLINE of
112	   continuous-Full → evict. Bounded-wedge preserved; only the inline sleep dies.
113
114	### CONSTRAINT (a) — CONTIGUOUS delivered_through (doyle's gate; REQUIRED, prevents a B2 re-break)
115
116	THE FLAW in the bare reshape: `advanced_cursor` (broker.rs:263) is a HIGH-WATERMARK
117	(`current.max(seq+1)`). With drop-on-full, the drain drops seq N (Full) then later
118	enqueues N+1 (slot freed). `controller_writer` writes …N-1 (cursor=N), then N+1 →
119	`advanced_cursor(max(N, N+2)) = N+2`. The cursor JUMPS past the dropped N. Since
120	`delivered_through` IS the `resume_seq` a COLD brain reads (broker.rs:235-236), the
121	resumed brain skips N → incoherent screen → non-gapless / not-exactly-once resume =
122	the B2 load-bearing invariant (broker.rs:8-30) RE-BROKEN. "A dropped frame never
123	advances the cursor" is true PER-FRAME but the NEXT contiguous write jumps the
124	watermark past the gap.
125
126	FIX (a): make the cursor CONTIGUOUS. `controller_writer` advances `delivered_through`
127	ONLY when the frame it writes has `seq == cursor` (the next expected seq); a gap
128	(`seq > cursor`, from a drop) FREEZES the cursor at last-contiguous (the frame STILL
129	goes to the socket — the live operator sees it; only the durable resume point freezes).
130	A fallen-behind controller then resumes from the frozen cursor → ring replay catches
131	it up, or D4 floor-clamp if the frozen seq aged out of the ring + the controller was
132	evicted = exactly constraint-2 degradation, now CORRECT. Backward-compatible: today's
133	no-drop path has no gaps, so the advance is byte-identical. Implement in
134	`controller_writer` (broker.rs:705+): before/at the `advance_delivered` call, gate on
135	`seq == expected-next`; freeze (skip advance) on `seq > cursor`. Read `advance_delivered`
136	(688-703) + `advanced_cursor` (263) to do it minimally (likely a contiguity check
137	rather than the high-watermark `advanced_cursor`).
138
139	FINAL GATED RESHAPE = items 1-3 above (try_send/append/last_ok/evict, deliver-poll
140	deleted) + KH-unchanged (epoch/one-writer/monotonic) + (a) contiguous cursor. All of
141	doyle's 3 constraints + B2 hold. doyle adding a per-chunk deliver/try_send tally to v5
142	to empirically show the fix closes the gate.
143
144	RESOLVED (doyle's call): deadline semantic = "CONTROLLER_WRITE_DEADLINE since last
145	SUCCESSFUL send" (continuous-full window). Accepted — strictly better (a controller
146	making progress isn't evicted).
147
148	KH guardrails (preserved): REQ-HAZARD-INJECT-CONTROL-COEXIST,
149	REQ-HAZARD-CONTROLLER-WRITER-REORDER. Epoch gate unchanged (mark_controller_gone still
150	epoch-checked); one-live-writer-per-conn unchanged; monotonic on-wire seq unchanged.
151	Read before coding: `controller_writer` (broker.rs:705+), `advance_delivered` (688-703),
152	`become_controller` (358), `ControllerJob`/`deliver` (649-686), `append` (302+).
153
154	### Traceability (CLAUDE.md binding)
155
156	Likely a NEW req (e.g. REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE) or
157	fold into REQ-HAZARD-INJECT-CONTROL-COEXIST. Add to traceable-reqs.toml FIRST,
158	then satisfy (impl + unit). The 3 gates are the int evidence.
159
160	## Open items before coding (await doyle)
161
162	1. EMPIRICAL b4 confirm (doyle cutting): bump `CONTROLLER_CHANNEL_DEPTH` huge OR
163	   make the gate controller drain fast (un-paced pump) → if a_journaled greens, b4
164	   is THE trigger and the decouple fix is justified.
165	2. 3-gate counters from run 27898894089 (a_journaled / p0_paste / g2, c1/c2/c3 +
166	   EVICT). p0_paste fails BOTH platforms — does it ALSO fill the controller on
167	   Windows, or is it a 2nd mechanism? Splits whether one fix covers all three.
168	3. W1-vs-parent bisect (doyle) — confirm pre-W1 = permanent wedge (validates
169	   "not a regression").
170
171	## Already shipped this session (test-infra, both verified + keepable)
172
173	- `pump-carrier-fix` branch (origin), off v0.13.0-delivery-control@18416fd:
174	  - **7a1cc68** — both rc-attach gate attachers cold_start→cold_start_pump (kills
175	    the `subscribed=false` measurement artifact; facet-B itself already fixed by
176	    W1b e404aeb + P0 input-writer isolation).
177	  - **7c09819** — env-tunable registry busy_timeout (SPT_REGISTRY_BUSY_TIMEOUT_MS,
178	    default 5000; CI exports 30000) — ends the Phase-A SQLITE_BUSY load flake.
179	  - Both validated locally; traceable-reqs check exit 0. doyle cherry-picked onto
180	    PR #27 validation branch (his 25f60c1 p0_paste + 9dd7873 registry + e54bc51 CI env).
181
182	## Confirmed FIXED (not the blocker)
183
184	facet-B (W1b e404aeb + P0 + no-ack, all in HEAD; operator's 06-19 wedge was a
185	pre-W1b build 8b5583e). Carrier artifact (pump fix). Registry SQLITE_BUSY (knob).
186
187	## v4 diagnostic record (do-not-merge)
188
189	`wedge-trace-v4` (origin) carries the SPT_WEDGE_TRACE c1/c2/c3 + EVICT counters
190	(broker.rs append, attach.rs serve, nethost.rs StreamLog::append). Diagnostic only.
191
192	## End goal
193
194	Cut v0.13.0. Path = land the gated viewer-drain decouple fix → 3 gates green warm
195	on forkpty (kitsubito CI) → fold pump-carrier-fix + decouple to delivery-control →
196	doyle folds to delivery-control → deployah cuts the release. g2/InjectFloor
197	(commit-deadline/floor-release) is a SEPARATE item, still parked.
198

# V0.13.0 — viewer-drain decouple (b4 root) — JIT plan

**Status:** b4 fix IMPLEMENTED + verified + PUSHED → `origin/viewer-drain-decouple-b4` @ **677c3e7** (off delivery-control@18416fd). Local gates ALL GREEN: broker unit tests 10/10 (2 new b4 tests + regressions), workspace clippy -D warnings clean, traceable-reqs exit 0. AWAITING doyle's post-b4 forkpty re-measure (cherry-pick 677c3e7 onto PR #27 + v5 tally). Expect a_journaled GREEN, p0_paste still RED (next), g2 TBD (may close indirectly). NEXT items: p0_paste skip-to-live (gated, below) + g2 (re-measure post-b4).

## ⚠ v0.13.0 IS THREE ITEMS, NOT ONE (doyle's 3-gate counters, run 27898894089)

The 3 RED gates have THREE DISTINCT roots. b4 closes ONLY a_journaled. Do NOT ship
b4 and expect a clean board.

1. **a_journaled** — c1=0, EVICT=0, journaled_ops=13139, got_output=FALSE = **b4
   drain-throttle** (controller deliver blocks the drain inline; viewer never fanned).
   ✅ **THE b4 FIX (this plan) CLOSES THIS.**
2. **p0_paste** — EVICT FIRED ("dropped 1 viewer at seq 35584, drain_appends=35585"),
   pumped=35658, attach_received_output=FALSE, backpressured=true, BOTH platforms.
   = the drain ran FREE; the VIEWER (serve_attach's broker subscription) overflowed
   its 256 channel + was EVICTED because serve_attach forwards
   (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the
   drain fans out. **SEPARATE root. b4 does NOT touch it.** Triage lean (gate pending
   doyle): eviction-of-a-hopelessly-behind-viewer is correct session-protection; the
   bug is it's SILENT + PERMANENT. RIGHT product = SKIP-TO-LIVE — serve_attach
   re-subscribes from the current ring floor on eviction (like tail -f: see live, not
   die replaying an uncatchable backlog).
   **GATED (doyle): skip-to-live is REAL, not relax.** Eviction itself = correct
   (keep it); SILENT+PERMANENT eviction = the bug. VIEWER-only → B2-SAFE (viewers do
   NOT advance delivered_through / not authoritative; no cursor exposure). Design:
   (1) explicit broker→viewer EVICTION SIGNAL (unambiguous marker, DISTINCT from
   session-exit EOF — serve must not tear down a live attach on it) sent to the
   evicted viewer's conn; (2) serve_attach re-subscribes from the CURRENT ring floor
   (skip-to-live) on that signal; (3) HARD constraint: NO evict→resubscribe busy-loop
   — rate-limit / forward-progress so under max-flood the viewer sees intermittent
   LIVE bursts, never a CPU spin. Do NOT lead with serve stall/gap-detection (fragile
   fallback only). SEPARATE fix, AFTER b4.
3. **g2** — assert :1196 `assert!(delivered)` = the broker did NOT ACCEPT the injected
   endpoint event within 8s (NOT a got_output failure). forkpty-only. = inject-
   acceptance (dispatch_endpoint_input / inject path) starved by the output-flood
   CPU storm. **Distinct.** b4 frees drain-thread CPU (deletes the 5s inline poll), so
   b4 MAY close g2 indirectly — **RE-MEASURE g2 AFTER b4 lands before designing a
   separate fix.** If still red → real inject-acceptance-must-not-starve fix.

**v0.13.0 close = b4 (this plan) + p0_paste skip-to-live (doyle-gate pending) + g2
(re-measure post-b4, then decide).** This JIT plan covers item 1 (b4); items 2 & 3
are captured here but not yet designed/gated.

## The bug (b4 — confirmed by doyle's code read + todlando's verification)

The 3 inject_control_wedge gates fail WARM on forkpty (a_journaled/g2 Linux-only;
p0_paste both platforms) with subscribed=TRUE, got_output=FALSE. Localized via the
v4 counters: **c1=0, c2=0, c3=1, EVICT=0**.

Root: the session drain closure (`broker.rs:1450-1457`) runs `job.deliver()`
**inline**. `ControllerJob::deliver` (`broker.rs:669-685`) is a `try_send`
sleep-poll loop: on a FULL controller channel it sleeps `CONTROLLER_SEND_POLL`
and re-polls up to `CONTROLLER_WRITE_DEADLINE` (5s), **on the drain thread**. So a
controller that drains slower than the PTY floods → its `CONTROLLER_CHANNEL_DEPTH`
(4096) channel fills → `deliver()` throttles the drain → `append()` (and its viewer
`try_send` fan-out) doesn't run → **the viewer starves**. EVICT=0 because the
throttled drain is too slow to even fill the viewer's 256 channel.

No-recovery at 18.97s = steady-state-near-full: the controller socket drains JUST
enough that each `deliver()` gets a slot within 5s → returns Ok → controller is
NEVER deadline-evicted → drain throttled the WHOLE run (not 5s episodes).

Controller identity in a_journaled: after the driver's `w.attach(sid,0)` (Control,
by=None) it `become_controller`s over the spawner (same-identity local re-take) →
the **driver** is the throttling controller, and its PACED read (the 20ms+5ms
anti-CPU-storm pacing, Unix-only) is slow enough under the forkpty `yes` flood to
keep its channel full. forkpty-only because forkpty floods harder than Windows
ConPTY's paced output.

Regression verdict: NOT a new class. Pre-W1 a full controller wedged the drain
PERMANENTLY; W1 (8b5583e) bounded it to 5s episodes but STILL couples drain
throughput to the controller. The NEW gates are the first to ASSERT the
viewer-not-starved-by-a-busy-session property — a LEGITIMATE product property
(rc --view of a noisy session must work) → REAL FIX, not pathological-expectation.

## Fix direction — RESOLVED design (doyle-gated reshape, sent for final gate)

doyle's constraint 2 collapsed the design tension: a slow-but-alive controller does
NOT need gapless live delivery — it degrades to the EXISTING ring-fall-behind +
resume-from-floor (RESTORATION-D4 clamp / snap-above render cursor), the same as the
reconnect case. So the controller becomes viewer-like (try_send drop-on-full) + a
staleness-evict. No ring-replay-on-recovery machinery needed.

THE RESHAPE: controller backpressure becomes a SINGLE non-blocking `try_send`,
identical to viewers; `ControllerJob::deliver()`'s 5s sleep-poll is DELETED. A single
try_send never blocks, so doing it UNDER the log lock is safe (viewers already do
exactly that in append). So move the controller send INTO append() (under the lock,
beside viewer fan-out); the drain closure loses the deliver() call entirely.

- `ControllerSink` += `last_ok: Instant` (stateful staleness in the sink under the
  log, survives across chunks; set = now at `become_controller`).
- `OutputLog::append` (under log lock), controller arm:
    match c.tx.try_send(CtrlMsg::Output(seq, frame)) {
      Ok(())               => c.last_ok = now,                                  // healthy
      Err(Full(_))         => if now - c.last_ok >= CONTROLLER_WRITE_DEADLINE { evict = Some(epoch) }
                              else { /* DROP — controller falls behind the ring */ },
      Err(Disconnected(_)) => evict = Some(epoch),
    }
  append returns `Option<evict-epoch>` (replaces the ControllerJob return). Drain
  off-lock: `if Some(epoch) => mark_controller_gone(epoch)`. NO deliver() loop, NO
  sleep — drain never blocks.

### Maps to doyle's 3 gate constraints (B2 gapless-handoff is load-bearing)

1. `delivered_through` advances ONLY on a real successful socket write — UNCHANGED:
   `controller_writer` advances it on write; a DROPPED frame never enters the channel
   → never written → never advances. Zero phantom-advance.
2. Slow-but-alive controller drops frames under flood → on-wire forward-jumps →
   operator snap-above cursor / RESTORATION-D4 clamp = existing fall-behind-ring case.
   No hard gap, no drain backpressure.
3. Truly-stalled controller: `last_ok` stops advancing → CONTROLLER_WRITE_DEADLINE of
   continuous-Full → evict. Bounded-wedge preserved; only the inline sleep dies.

### CONSTRAINT (a) — CONTIGUOUS delivered_through (doyle's gate; REQUIRED, prevents a B2 re-break)

THE FLAW in the bare reshape: `advanced_cursor` (broker.rs:263) is a HIGH-WATERMARK
(`current.max(seq+1)`). With drop-on-full, the drain drops seq N (Full) then later
enqueues N+1 (slot freed). `controller_writer` writes …N-1 (cursor=N), then N+1 →
`advanced_cursor(max(N, N+2)) = N+2`. The cursor JUMPS past the dropped N. Since
`delivered_through` IS the `resume_seq` a COLD brain reads (broker.rs:235-236), the
resumed brain skips N → incoherent screen → non-gapless / not-exactly-once resume =
the B2 load-bearing invariant (broker.rs:8-30) RE-BROKEN. "A dropped frame never
advances the cursor" is true PER-FRAME but the NEXT contiguous write jumps the
watermark past the gap.

FIX (a): make the cursor CONTIGUOUS. `controller_writer` advances `delivered_through`
ONLY when the frame it writes has `seq == cursor` (the next expected seq); a gap
(`seq > cursor`, from a drop) FREEZES the cursor at last-contiguous (the frame STILL
goes to the socket — the live operator sees it; only the durable resume point freezes).
A fallen-behind controller then resumes from the frozen cursor → ring replay catches
it up, or D4 floor-clamp if the frozen seq aged out of the ring + the controller was
evicted = exactly constraint-2 degradation, now CORRECT. Backward-compatible: today's
no-drop path has no gaps, so the advance is byte-identical. Implement in
`controller_writer` (broker.rs:705+): before/at the `advance_delivered` call, gate on
`seq == expected-next`; freeze (skip advance) on `seq > cursor`. Read `advance_delivered`
(688-703) + `advanced_cursor` (263) to do it minimally (likely a contiguity check
rather than the high-watermark `advanced_cursor`).

FINAL GATED RESHAPE = items 1-3 above (try_send/append/last_ok/evict, deliver-poll
deleted) + KH-unchanged (epoch/one-writer/monotonic) + (a) contiguous cursor. All of
doyle's 3 constraints + B2 hold. doyle adding a per-chunk deliver/try_send tally to v5
to empirically show the fix closes the gate.

RESOLVED (doyle's call): deadline semantic = "CONTROLLER_WRITE_DEADLINE since last
SUCCESSFUL send" (continuous-full window). Accepted — strictly better (a controller
making progress isn't evicted).

KH guardrails (preserved): REQ-HAZARD-INJECT-CONTROL-COEXIST,
REQ-HAZARD-CONTROLLER-WRITER-REORDER. Epoch gate unchanged (mark_controller_gone still
epoch-checked); one-live-writer-per-conn unchanged; monotonic on-wire seq unchanged.
Read before coding: `controller_writer` (broker.rs:705+), `advance_delivered` (688-703),
`become_controller` (358), `ControllerJob`/`deliver` (649-686), `append` (302+).

### Traceability (CLAUDE.md binding)

Likely a NEW req (e.g. REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE) or
fold into REQ-HAZARD-INJECT-CONTROL-COEXIST. Add to traceable-reqs.toml FIRST,
then satisfy (impl + unit). The 3 gates are the int evidence.

## Open items before coding (await doyle)

1. EMPIRICAL b4 confirm (doyle cutting): bump `CONTROLLER_CHANNEL_DEPTH` huge OR
   make the gate controller drain fast (un-paced pump) → if a_journaled greens, b4
   is THE trigger and the decouple fix is justified.
2. 3-gate counters from run 27898894089 (a_journaled / p0_paste / g2, c1/c2/c3 +
   EVICT). p0_paste fails BOTH platforms — does it ALSO fill the controller on
   Windows, or is it a 2nd mechanism? Splits whether one fix covers all three.
3. W1-vs-parent bisect (doyle) — confirm pre-W1 = permanent wedge (validates
   "not a regression").

## Already shipped this session (test-infra, both verified + keepable)

- `pump-carrier-fix` branch (origin), off v0.13.0-delivery-control@18416fd:
  - **7a1cc68** — both rc-attach gate attachers cold_start→cold_start_pump (kills
    the `subscribed=false` measurement artifact; facet-B itself already fixed by
    W1b e404aeb + P0 input-writer isolation).
  - **7c09819** — env-tunable registry busy_timeout (SPT_REGISTRY_BUSY_TIMEOUT_MS,
    default 5000; CI exports 30000) — ends the Phase-A SQLITE_BUSY load flake.
  - Both validated locally; traceable-reqs check exit 0. doyle cherry-picked onto
    PR #27 validation branch (his 25f60c1 p0_paste + 9dd7873 registry + e54bc51 CI env).

## Confirmed FIXED (not the blocker)

facet-B (W1b e404aeb + P0 + no-ack, all in HEAD; operator's 06-19 wedge was a
pre-W1b build 8b5583e). Carrier artifact (pump fix). Registry SQLITE_BUSY (knob).

## v4 diagnostic record (do-not-merge)

`wedge-trace-v4` (origin) carries the SPT_WEDGE_TRACE c1/c2/c3 + EVICT counters
(broker.rs append, attach.rs serve, nethost.rs StreamLog::append). Diagnostic only.

## End goal

Cut v0.13.0. Path = land the gated viewer-drain decouple fix → 3 gates green warm
on forkpty (kitsubito CI) → fold pump-carrier-fix + decouple to delivery-control →
doyle folds to delivery-control → deployah cuts the release. g2/InjectFloor
(commit-deadline/floor-release) is a SEPARATE item, still parked.


Full picture now. Let me check the attach/subscribe path for a bounded ~5s wait — that would confirm my Windows hypothesis before I send it to doyle.


Found 116 files
crates\spt-daemon\src\broker.rs
crates\spt-daemon\tests\inject_control_wedge.rs
crates\spt-store\src\db.rs
crates\spt-daemon\src\nethost.rs
crates\spt-daemon\src\attach.rs
crates\spt-daemon\tests\broker.rs
crates\spt\tests\job_escape_e2e.rs
crates\spt-daemon\src\brain.rs
crates\spt-daemon\tests\attach.rs
crates\spt-daemon\tests\input_ack_deadlock.rs
crates\spt-daemon\src\msg.rs
crates\spt\src\rc.rs
crates\spt\tests\brain_survive.rs
crates\spt-runtime\src\manifest.rs
crates\spt\src\cli.rs
crates\spt\tests\resume_template_e2e.rs
crates\spt-daemon\tests\two_origin_spanning.rs
crates\spt-daemon\src\digest.rs
crates\spt\src\api\reporting.rs
crates\spt\tests\dummy_harness_e2e.rs
crates\spt-daemon\src\daemon.rs
crates\spt\tests\attach_wedge_e2e.rs
crates\spt-daemon\src\livehost.rs
crates\spt-daemon\src\effect.rs
crates\spt-daemon\tests\dispatch.rs
crates\spt-daemon\tests\handoff.rs
crates\spt-daemon\tests\resume.rs
crates\spt-daemon\tests\twohost.rs
crates\spt-daemon\src\applyhost.rs
crates\spt-daemon\src\shellhost.rs
crates\spt-daemon\src\lib.rs
crates\spt\tests\bind_cwd_project_e2e.rs
crates\spt-daemon\src\dispatch.rs
crates\spt\tests\ready_resume_ledger_e2e.rs
crates\spt\tests\brain_restart_psyche_dup_e2e.rs
crates\spt\tests\unhost_psyche_reap_e2e.rs
crates\spt-daemon\src\lifecycle.rs
crates\spt-live\src\psyche.rs
crates\spt-runtime\src\runtime.rs
crates\spt\tests\livehost_bootgate_e2e.rs
crates\spt-daemon\src\brainproc.rs
crates\spt-daemon\src\seedmap.rs
crates\spt-daemon\src\registryhost.rs
crates\spt-store\src\branchstore.rs
crates\spt\tests\live_resolve_e2e.rs
crates\spt-daemon\tests\pump.rs
crates\spt-daemon\tests\netbroker.rs
crates\spt-daemon\tests\daemon_lifecycle_real_brain.rs
crates\spt\tests\livehost_bootrace_e2e.rs
crates\spt\tests\livehost_nonresident_e2e.rs
crates\spt\tests\livehost_psyche_fail_e2e.rs
crates\spt-live\src\digest.rs
crates\spt\tests\tunnel_e2e.rs
crates\spt\tests\shell_actgate_e2e.rs
crates\spt\tests\live_bind_firsthost_e2e.rs
crates\spt\tests\live_firsthost_e2e.rs
crates\spt\tests\gateway_owner_shell_e2e.rs
crates\spt\tests\contract_e2e.rs
crates\spt\tests\drive_e2e.rs
crates\spt-daemon\src\tunnelhub.rs
crates\spt-daemon\src\linkhost.rs
crates\spt-daemon\src\endpoint.rs
crates\spt\tests\shell_e2e.rs
crates\spt\tests\poll_envelope_e2e.rs
crates\spt-net\src\net\wanmsg.rs
crates\spt-msg\src\ring.rs
crates\spt-msg\src\deliver.rs
crates\spt-daemon\src\psyrelay.rs
crates\spt-daemon\src\notif.rs
crates\spt-net\src\net\attach.rs
crates\spt-term\src\digest.rs
crates\spt-live\src\resume.rs
crates\spt-live\src\echo.rs
crates\spt-daemon\tests\digest.rs
crates\spt-daemon\src\digesthub.rs
crates\spt\tests\shell_sleepwake_e2e.rs
crates\spt\tests\gateway_e2e.rs
crates\spt-live\src\reconcile.rs
crates\spt-live\src\history.rs
crates\spt-daemon\tests\sync.rs
crates\spt-daemon\tests\wanmsg.rs
crates\spt-daemon\tests\replicate.rs
crates\spt-daemon\tests\access.rs
crates\spt-daemon\src\wan.rs
crates\spt-daemon\src\sync.rs
crates\spt\tests\brain_respawn_rename.rs
crates\spt-daemon\src\pump\mod.rs
crates\spt-daemon\src\propagate.rs
crates\xtask\src\main.rs
crates\spt-daemon\src\serveprobe.rs
crates\spt-daemon\tests\mesh.rs
crates\spt-daemon\src\pump\registry.rs
crates\spt\tests\quickstart_e2e.rs
crates\spt\tests\brain_split.rs
crates\spt\tests\n1_pairing.rs
crates\spt-daemon\tests\propagate.rs
crates\spt-daemon\src\update.rs
crates\spt\tests\notify_shell_e2e.rs
crates\spt-store\src\project.rs
crates\spt-store\src\gitrun.rs
crates\spt-net\src\net\pairing\rendezvous.rs
crates\spt-net\src\net\pairing\meet.rs
crates\spt-net\src\net\pairing\ntp.rs
crates\spt-net\src\net\endpoint.rs
crates\spt-live\src\turn.rs
crates\spt-live\src\pulse.rs
crates\spt-daemon\tests\xfer.rs
crates\spt-daemon\tests\shellchan.rs
crates\spt-daemon\tests\presence.rs
crates\spt-daemon\tests\notifsync.rs
crates\spt-daemon\tests\netstream.rs
crates\spt-daemon\src\xfer.rs
crates\spt-daemon\src\resthost.rs
crates\spt-daemon\src\pairhost.rs
crates\spt-daemon\src\config.rs
adapters\mock\src\shell.rs


Bash

grep -rn "concurrent_attach_subscribed\|journaled_ops_pumped\|attach_received" --include=*.rs | head -40

Find gate counter sites


crates/spt-daemon/tests/inject_control_wedge.rs:728:        "=== W1b JOURNAL-WEDGE GATE: journaled_ops_pumped={pumped_n} \
crates/spt-daemon/tests/inject_control_wedge.rs:729:         concurrent_attach_subscribed={subscribed} attach_received_pty_output={got_output} \
crates/spt-daemon/tests/inject_control_wedge.rs:1964:         concurrent_attach_subscribed={subscribed} attach_received_output={got_output} \
crates/spt-daemon/tests/input_ack_deadlock.rs:531:         concurrent_attach_subscribed={subscribed} attach_received_output={got_output} \


630	        let mut got_output = false;
631	        let deadline = std::time::Instant::now() + Duration::from_secs(8);
632	        while std::time::Instant::now() < deadline {
633	            match operator.read_event_until(Some(
634	                std::time::Instant::now() + Duration::from_millis(250),
635	            )) {
636	                Ok(BrokerEvent::NetStreamData { stream_id, bytes, .. }) if stream_id == stream_b => {
637	                    for rec in decoder.push(&bytes) {
638	                        if let AttachRecord::Output { data_b64, .. } = rec {
639	                            let chunk = decode_bytes(&data_b64).unwrap_or_default();
640	                            if count(&chunk, b"FLOOD") >= 1 {
641	                                got_output = true;
642	                            }
643	                        }
644	                    }
645	                    if got_output {
646	                        break;
647	                    }
648	                }
649	                Ok(_) => continue,
650	                Err(_) => continue, // slice timeout — keep waiting until the deadline
651	            }
652	        }
653
654	        // Report the result IMMEDIATELY (the main thread's watchdog can proceed),
655	        // THEN best-effort detach + drain. We deliberately do NOT block the result
656	        // on the serve thread: a controller's `serve_attach` forward can block on a
657	        // non-draining operator socket while the flood child keeps producing — the
658	        // main thread kills the child to unblock it. Reporting first then draining
659	        // keeps the watchdog free of that wedge.
660	        let _ = result_tx.send((subscribed, got_output));
661	        let _ = operator.net_stream_send(stream_b, &[], None, true);
662	        // Keep draining the operator so serve's forwards drain and serve_attach can
663	        // observe our detach + the child's eventual death and return cleanly.
664	        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);
665	        while std::time::Instant::now() < drain_deadline {
666	            match operator
667	                .read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))
668	            {
669	                Ok(BrokerEvent::NetStreamEof { .. }) => break,
670	                Ok(_) => continue,
671	                Err(_) => continue,
672	            }
673	        }
674	        let _ = server.join();
675	    });
676
677	    // Watchdog: a hard wedge surfaces as a recv timeout (a test FAILURE), never a
678	    // hang. The attacher reports its result before any blocking teardown, so this
679	    // recv is the hard ceiling on the whole gate.
680	    let (subscribed, got_output) = result_rx
681	        .recv_timeout(Duration::from_secs(30))
682	        .unwrap_or((false, false));
683
684	    // Tear down so nothing leaks AND so any backpressured helper thread unblocks:
685	    // kill the flood child FIRST (ends the output backing up serve's forwards),
686	    // then signal the driver to stop. The helper threads' own bounded drains let
687	    // them exit; we never block the GATE/assertions on a wedged thread.
688	    if let Some(pid) = session_pid_bounded(&_broker, sid, Duration::from_secs(5)) {
689	        kill_pid(pid);
690	    }
691	    stop.store(true, std::sync::atomic::Ordering::Relaxed);
692	    // Bound the teardown joins: on a REAL facet-B wedge the driver stays parked in
693	    // `send_effect` and the attacher in its subscribe — neither observes `stop`, so
694	    // a bare `.join()` would ride to the 240s nextest backstop and MASK the verdict
695	    // (the symptom doyle's kitsubito reliability run hit). The gate result is already
696	    // captured by the 30s watchdog above, so abandon a parked helper and let the
697	    // assertions fire fast (a true wedge then fails on `subscribed=false` in ~30s).
698	    let _ = join_bounded(driver, Duration::from_secs(8));
699	    let _ = join_bounded(attacher, Duration::from_secs(8));
700	    let pumped_n = pumped.load(std::sync::atomic::Ordering::Relaxed);
701
702	    // On Unix, fold the W1-missing park-(b) liveness probe HERE (before tearing the
703	    // broker down — the probe needs a live broker). On Windows the journaled write
704	    // is absorbed, so there is no park to assert liveness against.
705	    #[cfg(unix)]
706	    let alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
707
708	    // ── PROCESS-CLEAN-EXIT teardown (test-robustness, not the gate). ──
709	    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)
710	    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe
711	    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,
712	    // which drops each Brain's SendHalf → the broker's per-conn handler read errors
713	    // → the broker closes its end → the client RecvHalf read finally errors → the
714	    // reader thread exits. But that close CHAIN is asynchronous and the reader's
715	    // JoinHandle is only dropped, never joined (it is abandoned by design — KH 7.6).
716	    // On Windows, if the test returns before that chain completes, a reader thread
717	    // is still parked inside a named-pipe read syscall at process teardown, and a
718	    // thread blocked in that syscall on a half-closed handle aborts the WHOLE
719	    // process with exit 0xffffffff (the flaky ~50% abort under `--test-threads=1`,
720	    // where the next test / process exit races the chain). FIX: after the Brains
721	    // are dropped (helpers joined), settle briefly so the close chain completes and
722	    // every abandoned reader observes its read error and exits before the harness
723	    // moves on. Verified by `PUMP_IPC_READER: exited` printing within this window.
724	    // This is purely teardown hygiene; the assertions below are unchanged.
725	    thread::sleep(Duration::from_millis(750));
726
727	    eprintln!(
728	        "=== W1b JOURNAL-WEDGE GATE: journaled_ops_pumped={pumped_n} \
729	         concurrent_attach_subscribed={subscribed} attach_received_pty_output={got_output} \
730	         (fixed = both true; pre-fix on Unix: subscribed=false — attach wedged on the \
731	         held journal lock) ==="
732	    );
733
734	    // [int->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]
735	    // FACET B — the gate W1 lacked: a concurrent rc attach must be SERVICED even
736	    // while a journaled input effect is in-flight (and, on Unix, blocked holding
737	    // the journal lock). Pre-fix on a forkpty platform this subscribe wedges
738	    // ("brain IPC read deadline elapsed"); post-fix the effect runs off the lock.
739	    assert!(
740	        subscribed,
741	        "a concurrent `spt rc` attach must be SERVICED while a journaled PtyWrite \
742	         is in-flight: the attach's subscribe reply did not arrive within the \
743	         watchdog deadline — the journal lock is held across write_input (facet B, \
744	         REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE)."
745	    );
746	    // POSITIVE (W1 lacked): a real rc-client attach RECEIVES PTY output bytes.
747	    assert!(
748	        got_output,
749	        "the concurrent attach must actually RECEIVE PTY output from the child \
750	         (not just liveness): no FLOOD bytes rendered over the real attach pump."
751	    );
752
753	    // On Unix, ALSO fold the W1-missing park-(b) hard assertion: the broker PROCESS
754	    // must stay alive on a fresh connection even with the journaled write parked
755	    // (the park is per-effect under the lock, not a whole-process death). The probe
756	    // ran above (while the broker was still alive); assert its result here.
757	    #[cfg(unix)]
758	    assert!(
759	        alive,

        let mut got_output = false;
        let deadline = std::time::Instant::now() + Duration::from_secs(8);
        while std::time::Instant::now() < deadline {
            match operator.read_event_until(Some(
                std::time::Instant::now() + Duration::from_millis(250),
            )) {
                Ok(BrokerEvent::NetStreamData { stream_id, bytes, .. }) if stream_id == stream_b => {
                    for rec in decoder.push(&bytes) {
                        if let AttachRecord::Output { data_b64, .. } = rec {
                            let chunk = decode_bytes(&data_b64).unwrap_or_default();
                            if count(&chunk, b"FLOOD") >= 1 {
                                got_output = true;
                            }
                        }
                    }
                    if got_output {
                        break;
                    }
                }
                Ok(_) => continue,
                Err(_) => continue, // slice timeout — keep waiting until the deadline
            }
        }

        // Report the result IMMEDIATELY (the main thread's watchdog can proceed),
        // THEN best-effort detach + drain. We deliberately do NOT block the result
        // on the serve thread: a controller's `serve_attach` forward can block on a
        // non-draining operator socket while the flood child keeps producing — the
        // main thread kills the child to unblock it. Reporting first then draining
        // keeps the watchdog free of that wedge.
        let _ = result_tx.send((subscribed, got_output));
        let _ = operator.net_stream_send(stream_b, &[], None, true);
        // Keep draining the operator so serve's forwards drain and serve_attach can
        // observe our detach + the child's eventual death and return cleanly.
        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);
        while std::time::Instant::now() < drain_deadline {
            match operator
                .read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))
            {
                Ok(BrokerEvent::NetStreamEof { .. }) => break,
                Ok(_) => continue,
                Err(_) => continue,
            }
        }
        let _ = server.join();
    });

    // Watchdog: a hard wedge surfaces as a recv timeout (a test FAILURE), never a
    // hang. The attacher reports its result before any blocking teardown, so this
    // recv is the hard ceiling on the whole gate.
    let (subscribed, got_output) = result_rx
        .recv_timeout(Duration::from_secs(30))
        .unwrap_or((false, false));

    // Tear down so nothing leaks AND so any backpressured helper thread unblocks:
    // kill the flood child FIRST (ends the output backing up serve's forwards),
    // then signal the driver to stop. The helper threads' own bounded drains let
    // them exit; we never block the GATE/assertions on a wedged thread.
    if let Some(pid) = session_pid_bounded(&_broker, sid, Duration::from_secs(5)) {
        kill_pid(pid);
    }
    stop.store(true, std::sync::atomic::Ordering::Relaxed);
    // Bound the teardown joins: on a REAL facet-B wedge the driver stays parked in
    // `send_effect` and the attacher in its subscribe — neither observes `stop`, so
    // a bare `.join()` would ride to the 240s nextest backstop and MASK the verdict
    // (the symptom doyle's kitsubito reliability run hit). The gate result is already
    // captured by the 30s watchdog above, so abandon a parked helper and let the
    // assertions fire fast (a true wedge then fails on `subscribed=false` in ~30s).
    let _ = join_bounded(driver, Duration::from_secs(8));
    let _ = join_bounded(attacher, Duration::from_secs(8));
    let pumped_n = pumped.load(std::sync::atomic::Ordering::Relaxed);

    // On Unix, fold the W1-missing park-(b) liveness probe HERE (before tearing the
    // broker down — the probe needs a live broker). On Windows the journaled write
    // is absorbed, so there is no park to assert liveness against.
    #[cfg(unix)]
    let alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    // ── PROCESS-CLEAN-EXIT teardown (test-robustness, not the gate). ──
    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)
    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe
    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,
    // which drops each Brain's SendHalf → the broker's per-conn handler read errors
    // → the broker closes its end → the client RecvHalf read finally errors → the
    // reader thread exits. But that close CHAIN is asynchronous and the reader's
    // JoinHandle is only dropped, never joined (it is abandoned by design — KH 7.6).
    // On Windows, if the test returns before that chain completes, a reader thread
    // is still parked inside a named-pipe read syscall at process teardown, and a
    // thread blocked in that syscall on a half-closed handle aborts the WHOLE
    // process with exit 0xffffffff (the flaky ~50% abort under `--test-threads=1`,
    // where the next test / process exit races the chain). FIX: after the Brains
    // are dropped (helpers joined), settle briefly so the close chain completes and
    // every abandoned reader observes its read error and exits before the harness
    // moves on. Verified by `PUMP_IPC_READER: exited` printing within this window.
    // This is purely teardown hygiene; the assertions below are unchanged.
    thread::sleep(Duration::from_millis(750));

    eprintln!(
        "=== W1b JOURNAL-WEDGE GATE: journaled_ops_pumped={pumped_n} \
         concurrent_attach_subscribed={subscribed} attach_received_pty_output={got_output} \
         (fixed = both true; pre-fix on Unix: subscribed=false — attach wedged on the \
         held journal lock) ==="
    );

    // [int->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]
    // FACET B — the gate W1 lacked: a concurrent rc attach must be SERVICED even
    // while a journaled input effect is in-flight (and, on Unix, blocked holding
    // the journal lock). Pre-fix on a forkpty platform this subscribe wedges
    // ("brain IPC read deadline elapsed"); post-fix the effect runs off the lock.
    assert!(
        subscribed,
        "a concurrent `spt rc` attach must be SERVICED while a journaled PtyWrite \
         is in-flight: the attach's subscribe reply did not arrive within the \
         watchdog deadline — the journal lock is held across write_input (facet B, \
         REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE)."
    );
    // POSITIVE (W1 lacked): a real rc-client attach RECEIVES PTY output bytes.
    assert!(
        got_output,
        "the concurrent attach must actually RECEIVE PTY output from the child \
         (not just liveness): no FLOOD bytes rendered over the real attach pump."
    );

    // On Unix, ALSO fold the W1-missing park-(b) hard assertion: the broker PROCESS
    // must stay alive on a fresh connection even with the journaled write parked
    // (the park is per-effect under the lock, not a whole-process death). The probe
    // ran above (while the broker was still alive); assert its result here.
    #[cfg(unix)]
    assert!(
        alive,


This is the W1b journal-wedge gate (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE). Let me find where `subscribed` gets set — the actual subscribe call and its deadline.


560	    // park write_input under the journal lock).
561	    thread::sleep(Duration::from_secs(2));
562
563	    // ── The CONCURRENT `spt rc` attach: a SEPARATE operator connection requests a
564	    //    real attach over a loopback conn, subscribes, and (via serve_attach)
565	    //    receives the child's output. Run on its own thread so the OUTER
566	    //    recv_timeout watchdog bounds the whole gate — a wedged subscribe (facet B,
567	    //    the "brain IPC read deadline elapsed" symptom) surfaces as a clean test
568	    //    failure, never a hang. ──
569	    let (result_tx, result_rx) = std::sync::mpsc::channel::<(bool, bool)>();
570	    let attach_name = name.clone();
571	    let attacher = thread::spawn(move || {
572	        // The operator is a plain Whole conn (the proven attach-test carrier): its
573	        // request/subscribe are request-reply, so a blocking read that NEVER
574	        // returns IS the facet-B wedge — and the OUTER `recv_timeout(30s)` watchdog
575	        // converts that into a clean test FAILURE (subscribed never reported),
576	        // never a hang. No fragile pump-poison semantics on the servicing path.
577	        let mut operator = match Brain::cold_start(&attach_name, 1) {
578	            Ok(b) => b,
579	            Err(_) => {
580	                let _ = result_tx.send((false, false));
581	                return;
582	            }
583	        };
584	        let dialed = match operator.net_dial_loopback() {
585	            Ok(d) => d,
586	            Err(_) => {
587	                let _ = result_tx.send((false, false));
588	                return;
589	            }
590	        };
591	        let own_origin = dialed.remote_id_hex.clone();
592	        let stream_b = match request_attach(
593	            &mut operator,
594	            dialed.conn_id,
595	            sid,
596	            0,
597	            2,
598	            AttachIntent::Viewer, // a read-only rc --view: receives output, no journaled input
599	        ) {
600	            Ok(s) => s,
601	            Err(_) => {
602	                let _ = result_tx.send((false, false));
603	                return;
604	            }
605	        };
606	        // The subscribe reply is the first servicing proof: a wedged dispatch
607	        // never answers it; the outer recv_timeout watchdog then fails the gate.
608	        let subscribed = operator.net_stream_subscribe(stream_b, 0).is_ok();
609
610	        // The target serves the inbound attach stream (the real serve_attach pump).
611	        let mut target = connect_retry(&attach_name);
612	        let (stream_a, origin) = match wait_for_stream(&mut target) {
613	            Some(s) => s,
614	            None => {
615	                let _ = result_tx.send((subscribed, false));
616	                return;
617	            }
618	        };
619	        assert_eq!(origin, own_origin, "loopback origin is this node's own id");
620	        let serve_broker = attach_name.clone();
621	        let server = thread::spawn(move || {
622	            let _ = serve_attach(&mut target, &serve_broker, stream_a, "wedge-jrnl-ep", &origin, 0);
623	        });
624
625	        // The positive W1 LACKED: a real rc-client attach actually RECEIVES PTY
626	        // output bytes from the flooding child. Output delivery does NOT go through
627	        // the journal lock, so receiving bytes proves the dispatch serviced this
628	        // attach despite the wedged journaled input.
629	        let mut decoder = AttachDecoder::new();

    // park write_input under the journal lock).
    thread::sleep(Duration::from_secs(2));

    // ── The CONCURRENT `spt rc` attach: a SEPARATE operator connection requests a
    //    real attach over a loopback conn, subscribes, and (via serve_attach)
    //    receives the child's output. Run on its own thread so the OUTER
    //    recv_timeout watchdog bounds the whole gate — a wedged subscribe (facet B,
    //    the "brain IPC read deadline elapsed" symptom) surfaces as a clean test
    //    failure, never a hang. ──
    let (result_tx, result_rx) = std::sync::mpsc::channel::<(bool, bool)>();
    let attach_name = name.clone();
    let attacher = thread::spawn(move || {
        // The operator is a plain Whole conn (the proven attach-test carrier): its
        // request/subscribe are request-reply, so a blocking read that NEVER
        // returns IS the facet-B wedge — and the OUTER `recv_timeout(30s)` watchdog
        // converts that into a clean test FAILURE (subscribed never reported),
        // never a hang. No fragile pump-poison semantics on the servicing path.
        let mut operator = match Brain::cold_start(&attach_name, 1) {
            Ok(b) => b,
            Err(_) => {
                let _ = result_tx.send((false, false));
                return;
            }
        };
        let dialed = match operator.net_dial_loopback() {
            Ok(d) => d,
            Err(_) => {
                let _ = result_tx.send((false, false));
                return;
            }
        };
        let own_origin = dialed.remote_id_hex.clone();
        let stream_b = match request_attach(
            &mut operator,
            dialed.conn_id,
            sid,
            0,
            2,
            AttachIntent::Viewer, // a read-only rc --view: receives output, no journaled input
        ) {
            Ok(s) => s,
            Err(_) => {
                let _ = result_tx.send((false, false));
                return;
            }
        };
        // The subscribe reply is the first servicing proof: a wedged dispatch
        // never answers it; the outer recv_timeout watchdog then fails the gate.
        let subscribed = operator.net_stream_subscribe(stream_b, 0).is_ok();

        // The target serves the inbound attach stream (the real serve_attach pump).
        let mut target = connect_retry(&attach_name);
        let (stream_a, origin) = match wait_for_stream(&mut target) {
            Some(s) => s,
            None => {
                let _ = result_tx.send((subscribed, false));
                return;
            }
        };
        assert_eq!(origin, own_origin, "loopback origin is this node's own id");
        let serve_broker = attach_name.clone();
        let server = thread::spawn(move || {
            let _ = serve_attach(&mut target, &serve_broker, stream_a, "wedge-jrnl-ep", &origin, 0);
        });

        // The positive W1 LACKED: a real rc-client attach actually RECEIVES PTY
        // output bytes from the flooding child. Output delivery does NOT go through
        // the journal lock, so receiving bytes proves the dispatch serviced this
        // attach despite the wedged journaled input.
        let mut decoder = AttachDecoder::new();


`subscribed = net_stream_subscribe(...).is_ok()` — a request-reply to broker dispatch (line 608). My Windows hypothesis: b4 made the drain busy-cycle the OutputLog lock (removed the off-lock sleep window), starving the subscribe handler if it also needs that lock. Decisive check: does the subscribe dispatch path take the OutputLog log lock?


grep -rn "StreamSubscribe\|stream_subscribe\|Subscribe" crates/spt-daemon/src/broker.rs crates/spt-daemon/src/dispatch.rs crates/spt-daemon/src/nethost.rs 2>/dev/null | head -30

Find subscribe dispatch handler


crates/spt-daemon/src/broker.rs:56:    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,
crates/spt-daemon/src/broker.rs:57:    NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
crates/spt-daemon/src/broker.rs:59:    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,
crates/spt-daemon/src/broker.rs:472:    /// (REQ-RCVIEW-1 / REQ-KICK-1). Returns the [`SubscribeOutcome`]; performs
crates/spt-daemon/src/broker.rs:482:    ) -> SubscribeOutcome {
crates/spt-daemon/src/broker.rs:486:                return SubscribeOutcome::BusyControlled {
crates/spt-daemon/src/broker.rs:492:            return SubscribeOutcome::Viewer;
crates/spt-daemon/src/broker.rs:498:            return SubscribeOutcome::Controller;
crates/spt-daemon/src/broker.rs:506:            return SubscribeOutcome::Controller;
crates/spt-daemon/src/broker.rs:513:            return SubscribeOutcome::Viewer;
crates/spt-daemon/src/broker.rs:520:            return SubscribeOutcome::Controller;
crates/spt-daemon/src/broker.rs:535:                SubscribeOutcome::TookControl
crates/spt-daemon/src/broker.rs:537:            _ => SubscribeOutcome::BusyControlled {
crates/spt-daemon/src/broker.rs:1363:                KIND_NET_STREAM_SUBSCRIBE => match self.dispatch_net_stream_subscribe(env, &send) {
crates/spt-daemon/src/broker.rs:1567:    /// and replies the [`SubscribeOutcome`] so the serving brain can wake
crates/spt-daemon/src/broker.rs:1572:        let req: SubscribeReq = serde_json::from_value(env.payload)
crates/spt-daemon/src/broker.rs:2110:    fn dispatch_net_stream_subscribe(
crates/spt-daemon/src/broker.rs:2115:        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)
crates/spt-daemon/src/broker.rs:2132:        let req: NetPresenceSubscribeReq = serde_json::from_value(env.payload)
crates/spt-daemon/src/dispatch.rs:258:    brain.net_stream_subscribe(stream_id, 0)?;
crates/spt-daemon/src/dispatch.rs:444:    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
crates/spt-daemon/src/dispatch.rs:529:    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {
crates/spt-daemon/src/dispatch.rs:602:    if let Err(e) = brain.net_stream_subscribe(stream_id, 0) {


2110	    fn dispatch_net_stream_subscribe(
2111	        &self,
2112	        env: Envelope,
2113	        send: &SharedSend,
2114	    ) -> Result<u64, String> {
2115	        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)
2116	            .map_err(|e| format!("bad net-stream-subscribe payload: {e}"))?;
2117	        let host = self.net.get().ok_or("net disabled")?;
2118	        host.subscribe_stream(req.stream_id, Arc::clone(send), req.from_seq)
2119	            .map_err(|e| e.to_string())?;
2120	        Ok(req.stream_id)
2121	    }
2122
2123	    /// Attach a brain to the conn-liveness event log from a cursor (D4c) — the
2124	    /// PresenceChannel broker seam: gapless resume over connection presence,
2125	    /// same discipline as a stream subscribe.
2126	    // [impl->REQ-EP-4]
2127	    fn dispatch_net_presence_subscribe(
2128	        &self,
2129	        env: Envelope,
2130	        send: &SharedSend,
2131	    ) -> Result<(), String> {
2132	        let req: NetPresenceSubscribeReq = serde_json::from_value(env.payload)
2133	            .map_err(|e| format!("bad net-presence-subscribe payload: {e}"))?;
2134	        let host = self.net.get().ok_or("net disabled")?;
2135	        host.subscribe_presence(Arc::clone(send), req.from_seq);
2136	        Ok(())
2137	    }
2138
2139	    /// Terminate a session's child. The exit waiter emits the `exit` frame and

    fn dispatch_net_stream_subscribe(
        &self,
        env: Envelope,
        send: &SharedSend,
    ) -> Result<u64, String> {
        let req: NetStreamSubscribeReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad net-stream-subscribe payload: {e}"))?;
        let host = self.net.get().ok_or("net disabled")?;
        host.subscribe_stream(req.stream_id, Arc::clone(send), req.from_seq)
            .map_err(|e| e.to_string())?;
        Ok(req.stream_id)
    }

    /// Attach a brain to the conn-liveness event log from a cursor (D4c) — the
    /// PresenceChannel broker seam: gapless resume over connection presence,
    /// same discipline as a stream subscribe.
    // [impl->REQ-EP-4]
    fn dispatch_net_presence_subscribe(
        &self,
        env: Envelope,
        send: &SharedSend,
    ) -> Result<(), String> {
        let req: NetPresenceSubscribeReq = serde_json::from_value(env.payload)
            .map_err(|e| format!("bad net-presence-subscribe payload: {e}"))?;
        let host = self.net.get().ok_or("net disabled")?;
        host.subscribe_presence(Arc::clone(send), req.from_seq);
        Ok(())
    }

    /// Terminate a session's child. The exit waiter emits the `exit` frame and


Decisive finding: the flipping subscribe (`net_stream_subscribe`, inject_control_wedge.rs:608) dispatches via `dispatch_net_stream_subscribe` → `host.subscribe_stream` — the **nethost StreamLog**, NOT the session OutputLog. b4 touches only OutputLog/drain/controller_writer. No shared lock. And Windows pumped=249 « CONTROLLER_CHANNEL_DEPTH 4096 → controller channel never fills → b4's drop/evict arm never runs on Windows. b4 has no mechanism to break that subscribe. Reads as flake/load-artifact.

Sending doyle this exoneration + my plan.


cat <<'EOF' | spt send doyle
todlando → doyle. b4 read + Windows a_journaled analysis.

WINDOWS a_journaled — b4 STRUCTURALLY EXONERATED:
1. The subscribe that flips to false (inject_control_wedge.rs:608 net_stream_subscribe) dispatches via broker.rs:2118 dispatch_net_stream_subscribe -> host.subscribe_stream = the NETHOST StreamLog, NOT the session OutputLog. b4 touches ONLY OutputLog::append + the drain closure + controller_writer. Zero lock overlap with the subscribe path.
2. Windows pumped=249 << CONTROLLER_CHANNEL_DEPTH (4096) -> the controller channel NEVER fills on Windows -> b4's drop/evict/last_ok arm NEVER executes there. The ONLY non-full-path delta is the controller try_send moving UNDER the log lock (Ok arm, instant either way).
=> b4 has no mechanism to starve a nethost-StreamLog subscribe.

Verdict: likely FLAKE / load-artifact (5.59s, single run, Windows e2e known-slow under load). Your reproducibility re-run is the right gate: if subscribed=false is <100%, b4 is clean to fold. Expect WEDGE_TRACE to show the controller never fills + the drain starves nothing. (If it DOES repro 100%, only candidate left is dispatch-thread ordering on the operator conn — still not a lock b4 changed.)

AGREEMENT:
- b4 = keep.
- Don't block the fold on Windows a_journaled unless it repros.
- I'm starting skip-to-live now (closes a_journaled-Linux viewer-evict + p0_paste, one fix, unblocked per your gate). Holding the b4->delivery-control fold until your Windows repro verdict.
- g2 = real inject-acceptance fix, separate, after skip-to-live.

Need from you: repro count on Windows a_journaled (N runs, how many subscribed=false)?
EOF

Send doyle b4 Windows analysis + plan


Error: Exit code 1
NO_PERCH:doyle is not listening


doyle no perch. Check roster.


spt endpoint list 2>&1; echo "---agents---"; spt agent list 2>&1 | head -30

List reachable endpoints/agents


SUBNET SPT_DEV
  wall-a   HFENDULEAM (14efb80c…)  Dormant
  wall-aa  HFENDULEAM (14efb80c…)  Dormant
  wall-b   HFENDULEAM (14efb80c…)  Dormant
  wall-c   HFENDULEAM (14efb80c…)  Dormant
ENDPOINTS:4
LOCAL (this node)
  wall-a                       live_agent     -                      ready=true alive=true
  wall-aa                      live_agent     -                      ready=true alive=false
  wall-b                       live_agent     -                      ready=true alive=false
  wall-c                       live_agent     -                      ready=true alive=false
---agents---
error: unrecognized subcommand 'agent'

  tip: some similar subcommands exist: 'daemon', 'grant'

Usage: spt.exe [COMMAND]

For more information, try '--help'.


doyle unreachable — not on this node's subnet roster (only wall-* on HFENDULEAM; doyle's on his CI box, offline now running the Windows trace). Reply drafted, will resend when his perch returns via Monitor.

Proceeding with the unblocked high-value work: **skip-to-live** (closes a_journaled-Linux viewer-evict + p0_paste, one fix, doyle-gate already approved). Reading the relevant code to ground the design.


300	    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the
301	    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but
302	    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to
303	    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower
304	    /// than the PTY floods filled its channel, the inline poll throttled the drain,
305	    /// and the viewer fan-out below stopped running → a concurrent `rc --view`
306	    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`
307	    /// exactly like a viewer: the drain runs at full PTY speed regardless of
308	    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind
309	    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`
310	    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted
311	    /// (bounded wedge preserved). The writer still does the blocking socket write +
312	    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's
313	    /// single drain thread.
314	    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
315	    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
316	    #[must_use]
317	    fn append(&mut self, bytes: &[u8]) -> Option<u64> {
318	        let seq = self.next_seq;
319	        self.next_seq += 1;
320	        self.ring.push_back((seq, bytes.to_vec()));
321	        while self.ring.len() > self.cap_chunks {
322	            self.ring.pop_front();
323	        }
324	        let frame = output_envelope(self.session_id, seq, bytes);
325	        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.
326	        if !self.viewers.is_empty() {
327	            let mut evicted: Vec<u64> = Vec::new();
328	            for (vid, sink) in self.viewers.iter() {
329	                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
330	                    evicted.push(*vid);
331	                }
332	            }
333	            if !evicted.is_empty() {
334	                for vid in evicted {
335	                    self.viewers.remove(&vid);
336	                }
337	                self.stamp_viewer_count();
338	            }
339	        }
340	        // Controller: the SAME non-blocking discipline as a viewer (b4). One
341	        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a
342	        // slow controller falls behind the ring) unless it has been Full for the
343	        // whole deadline since its last success → EVICT. Disconnected (writer gone)
344	        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.
345	        let now = Instant::now();
346	        let epoch = self.controller_epoch.load(Ordering::Acquire);
347	        if let Some(c) = self.controller.as_mut() {
348	            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {
349	                Ok(()) => {
350	                    c.last_ok = now;
351	                    None
352	                }
353	                Err(TrySendError::Full(_)) => {
354	                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {
355	                        Some(epoch) // stalled past the deadline → bounded detach
356	                    } else {
357	                        None // drop this chunk; controller falls behind the ring
358	                    }
359	                }
360	                Err(TrySendError::Disconnected(_)) => Some(epoch),
361	            }
362	        } else {
363	            None
364	        }
365	    }
366
367	    /// The current controller's identity (`Some(node)` remote / `None` local),
368	    /// or `None` when undriven. The `by`-keyed ownership logic in
369	    /// [`resolve_subscribe`] reads this.
370	    fn controller_by(&self) -> Option<&str> {
371	        self.controller.as_ref().and_then(|c| c.by.as_deref())
372	    }
373
374	    /// Become (or silently re-take) the interactive controller from `from_seq`.

    /// b4 (REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE): W1 moved the
    /// controller SOCKET WRITE off the drain (the `controller_writer` thread) but
    /// left the HANDOFF as an inline sleep-poll (`ControllerJob::deliver`, up to
    /// `CONTROLLER_WRITE_DEADLINE`) ON the drain — so a controller draining slower
    /// than the PTY floods filled its channel, the inline poll throttled the drain,
    /// and the viewer fan-out below stopped running → a concurrent `rc --view`
    /// VIEWER starved. Now the controller handoff is ONE non-blocking `try_send`
    /// exactly like a viewer: the drain runs at full PTY speed regardless of
    /// controller speed. A slow-but-alive controller DROPS the chunk + falls behind
    /// the ring (resume-from-floor); a controller Full for `CONTROLLER_WRITE_DEADLINE`
    /// since its last successful handoff (`ControllerSink::last_ok`) is evicted
    /// (bounded wedge preserved). The writer still does the blocking socket write +
    /// advances the authoritative CONTIGUOUS cursor. Called only from the session's
    /// single drain thread.
    // [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    // [impl->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
    #[must_use]
    fn append(&mut self, bytes: &[u8]) -> Option<u64> {
        let seq = self.next_seq;
        self.next_seq += 1;
        self.ring.push_back((seq, bytes.to_vec()));
        while self.ring.len() > self.cap_chunks {
            self.ring.pop_front();
        }
        let frame = output_envelope(self.session_id, seq, bytes);
        // Viewers: non-blocking fan-out; a full / disconnected queue = eviction.
        if !self.viewers.is_empty() {
            let mut evicted: Vec<u64> = Vec::new();
            for (vid, sink) in self.viewers.iter() {
                if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
                    evicted.push(*vid);
                }
            }
            if !evicted.is_empty() {
                for vid in evicted {
                    self.viewers.remove(&vid);
                }
                self.stamp_viewer_count();
            }
        }
        // Controller: the SAME non-blocking discipline as a viewer (b4). One
        // `try_send`, never a sleep. Ok → record the handoff time. Full → DROP (a
        // slow controller falls behind the ring) unless it has been Full for the
        // whole deadline since its last success → EVICT. Disconnected (writer gone)
        // → EVICT. The drain off-lock turns `Some(epoch)` into `mark_controller_gone`.
        let now = Instant::now();
        let epoch = self.controller_epoch.load(Ordering::Acquire);
        if let Some(c) = self.controller.as_mut() {
            match c.tx.try_send(CtrlMsg::Output(seq, frame)) {
                Ok(()) => {
                    c.last_ok = now;
                    None
                }
                Err(TrySendError::Full(_)) => {
                    if now.duration_since(c.last_ok) >= CONTROLLER_WRITE_DEADLINE {
                        Some(epoch) // stalled past the deadline → bounded detach
                    } else {
                        None // drop this chunk; controller falls behind the ring
                    }
                }
                Err(TrySendError::Disconnected(_)) => Some(epoch),
            }
        } else {
            None
        }
    }

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

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


Let me read the viewer machinery (ViewerSink, viewer_writer, eviction helper) and serve_attach to design the eviction-signal + skip-to-live resubscribe.