        self.session_id = Some(session_id);
        self.next_seq = from_seq;
        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))
    }

    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —
    /// skip-to-live (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The viewer overflowed its
    /// bounded subscription queue (it forwards slower than the drain fans out) and
    /// the broker evicted it (correct session-protection); rather than dying it
    /// jumps to live and skips the uncatchable backlog (tail -f reconnect).
    ///
    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)
    /// EMPTY, so the viewer receives only the size frame + live output going
    /// forward. The next live frame carries the current ring tail's seq — a
    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup
    /// (the cold serve brain's default) would FATAL that jump as an "output gap",
    /// and a `next_seq = MAX` would DEDUP-DROP every live frame (seq < MAX); so we
    /// arm the snap-above dedup path (`session_cursors` non-empty) seeded at 0,
    /// which accepts the jump and snaps. VIEWER-only → B2-safe (a viewer never
    /// advances `delivered_through` / is not authoritative). The subscribe is sent
    /// FIRST (a cold brain's empty-map skips `subscribe_with`'s `from_seq` reset),
    /// THEN the cursor is armed, so neither path stamps `MAX`.
    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
    pub fn attach_skip_to_live(&mut self, session_id: u64, by: Option<&str>) -> io::Result<()> {
        self.session_id = Some(session_id);
        self.subscribe_with(session_id, u64::MAX, AttachIntent::Viewer, by.map(str::to_string))?;
        self.session_cursors.insert(session_id, 0);
        self.next_seq = 0;
        Ok(())
    }

+    /// Re-subscribe a VIEWER from the LIVE TAIL after a broker eviction —

+    /// skip-to-live (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The viewer overflowed its

+    /// bounded subscription queue (it forwards slower than the drain fans out) and

+    /// the broker evicted it (correct session-protection); rather than dying it

+    /// jumps to live and skips the uncatchable backlog (tail -f reconnect).

+    /// `from_seq = u64::MAX` makes the broker's ring replay (`s >= from_seq`)

+    /// EMPTY, so the viewer receives only the size frame + live output going

+    /// forward. The next live frame carries the current ring tail's seq — a

+    /// FORWARD JUMP from where this brain was evicted. The legacy `next_seq` dedup

+    /// (the cold serve brain's default) would FATAL that jump as an "output gap",

+    /// and a `next_seq = MAX` would DEDUP-DROP every live frame (seq < MAX); so we

+    /// arm the snap-above dedup path (`session_cursors` non-empty) seeded at 0,

+    /// which accepts the jump and snaps. VIEWER-only → B2-safe (a viewer never

+    /// advances `delivered_through` / is not authoritative). The subscribe is sent

+    /// FIRST (a cold brain's empty-map skips `subscribe_with`'s `from_seq` reset),

+    /// THEN the cursor is armed, so neither path stamps `MAX`.

+    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]

+    pub fn attach_skip_to_live(&mut self, session_id: u64, by: Option<&str>) -> io::Result<()> {

+        self.session_id = Some(session_id);

+        self.subscribe_with(session_id, u64::MAX, AttachIntent::Viewer, by.map(str::to_string))?;

+        self.session_cursors.insert(session_id, 0);

/// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the

/// broker for **every** hosted session and re-attach **each** in resume mode

/// from the broker's per-session delivered cursor (`resume_seq`). This is the


Now the broker side: `ViewerSink.evicted` notifier, `append` sets it before removing, and `viewer_writer` emits the marker on an evicted close. Let me check broker.rs imports.


Bash

grep -n "use std::sync\|AtomicBool\|AtomicU64\|use crate::msg\|size_envelope\|displaced_envelope\|use std::sync::atomic" crates/spt-daemon/src/broker.rs | head

Check broker imports for atomics + msg builders


36:use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
37:use std::sync::mpsc::{channel, sync_channel, Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError};
38:use std::sync::{Arc, Mutex};
52:use crate::msg::{
53:    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
55:    size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,
218:    /// thread (`Arc<AtomicU64>`). Bumped on every [`become_controller`]; the
225:    controller_epoch: Arc<AtomicU64>,
254:    /// W1: now an `Arc<AtomicU64>` shared with the controller's writer thread,
257:    delivered_through: Arc<AtomicU64>,


`AtomicBool`/`Arc`/`Ordering` already imported. Add `evicted_envelope` to the msg import.


52	use crate::msg::{
53	    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
54	    net_dialed_envelope, output_envelope,
55	    size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,
56	    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,
57	    NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
58	    NetStreamsReply, PairJoinReply, PairJoinReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,
59	    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,
60	    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_SUBSCRIBE,
61	    KIND_NET_SENT,
62	    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
63	    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,

use crate::msg::{
    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
    net_dialed_envelope, output_envelope,
    size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,
    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,
    NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,
    NetStreamsReply, PairJoinReply, PairJoinReq, ResizeReq, SessionInfo, SessionsReply, SpawnReq,
    Spawned, SubscribeOutcome, SubscribeReq, UnsubscribeReq, KIND_BRAIN_RESTART, KIND_BRAIN_RESTARTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT,
    KIND_INPUT, KIND_KILL, KIND_NET_DIAL, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_SUBSCRIBE,
    KIND_NET_SENT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
    KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED, KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE,


Edit

    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
    net_dialed_envelope, output_envelope,

    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
    evicted_envelope, net_dialed_envelope, output_envelope,


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

    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
    net_dialed_envelope, output_envelope,

    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
    evicted_envelope, net_dialed_envelope, output_envelope,

-    net_dialed_envelope, output_envelope,

+    evicted_envelope, net_dialed_envelope, output_envelope,

size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,

ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,

NetStatusReply, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq, NetStreamSubscribeReq,


Now the `ViewerSink` struct — add the `evicted` notifier:


struct ViewerSink {
    /// The bounded live-frame queue to this viewer's writer thread.
    tx: SyncSender<Envelope>,
    /// The viewer's IPC send half — kept ONLY for identity matching on detach
    /// (a dropped brain connection removes its viewer rows); never written here.
    send: SharedSend,
    /// The writer thread handle (held for ownership; it exits when `tx` drops or
    /// a socket write fails).
    _writer: JoinHandle<()>,
}

struct ViewerSink {
    /// The bounded live-frame queue to this viewer's writer thread.
    tx: SyncSender<Envelope>,
    /// The viewer's IPC send half — kept ONLY for identity matching on detach
    /// (a dropped brain connection removes its viewer rows); never written here.
    send: SharedSend,
    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),
    /// distinguishing an eviction from a normal teardown (session end / conn drop).
    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,
    /// on its `rx.recv()` Err, reads this and — when true — emits a
    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so
    /// the serving brain skips to live instead of tearing the attach down.
    evicted: Arc<AtomicBool>,
    /// The writer thread handle (held for ownership; it exits when `tx` drops or
    /// a socket write fails).
    _writer: JoinHandle<()>,
}


struct ViewerSink {
    /// The bounded live-frame queue to this viewer's writer thread.
    tx: SyncSender<Envelope>,
    /// The viewer's IPC send half — kept ONLY for identity matching on detach
    /// (a dropped brain connection removes its viewer rows); never written here.
    send: SharedSend,
    /// The writer thread handle (held for ownership; it exits when `tx` drops or
    /// a socket write fails).
    _writer: JoinHandle<()>,
}

struct ViewerSink {
    /// The bounded live-frame queue to this viewer's writer thread.
    tx: SyncSender<Envelope>,
    /// The viewer's IPC send half — kept ONLY for identity matching on detach
    /// (a dropped brain connection removes its viewer rows); never written here.
    send: SharedSend,
    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),
    /// distinguishing an eviction from a normal teardown (session end / conn drop).
    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,
    /// on its `rx.recv()` Err, reads this and — when true — emits a
    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so
    /// the serving brain skips to live instead of tearing the attach down.
    evicted: Arc<AtomicBool>,
    /// The writer thread handle (held for ownership; it exits when `tx` drops or
    /// a socket write fails).
    _writer: JoinHandle<()>,
}

+    /// Set TRUE when this viewer is EVICTED for overflow (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT),

+    /// distinguishing an eviction from a normal teardown (session end / conn drop).

+    /// `append` stores `true` BEFORE dropping the sink (and thus `tx`); the writer,

+    /// on its `rx.recv()` Err, reads this and — when true — emits a

+    /// [`KIND_VIEWER_EVICTED`] marker (in the writer thread, OFF the log lock) so

+    /// the serving brain skips to live instead of tearing the attach down.


Now `add_viewer` — create the notifier, thread it to the writer:


        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
        self.viewers.insert(
            vid,
            ViewerSink {
                tx,
                send: sub,
                _writer: writer,
            },
        );
    }

        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let evicted = Arc::new(AtomicBool::new(false));
        let writer_evicted = Arc::clone(&evicted);
        let session_id = self.session_id;
        let writer =
            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));
        self.viewers.insert(
            vid,
            ViewerSink {
                tx,
                send: sub,
                evicted,
                _writer: writer,
            },
        );
    }


        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));
        self.viewers.insert(
            vid,
            ViewerSink {
                tx,
                send: sub,
                _writer: writer,
            },
        );
    }

        let (tx, rx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
        let writer_send = Arc::clone(&sub);
        let evicted = Arc::new(AtomicBool::new(false));
        let writer_evicted = Arc::clone(&evicted);
        let session_id = self.session_id;
        let writer =
            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));
        self.viewers.insert(
            vid,
            ViewerSink {
                tx,
                send: sub,
                evicted,
                _writer: writer,
            },
        );
    }

-        let writer = thread::spawn(move || viewer_writer(writer_send, initial, rx));

+        let evicted = Arc::new(AtomicBool::new(false));

+        let writer_evicted = Arc::clone(&evicted);

+        let session_id = self.session_id;

+            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));


Now the `append` viewer-eviction arm — set the flag before removing:


            if !evicted.is_empty() {
                for vid in evicted {
                    self.viewers.remove(&vid);
                }
                self.stamp_viewer_count();
            }

            if !evicted.is_empty() {
                for vid in evicted {
                    // Mark eviction BEFORE dropping the sink: the writer's
                    // `rx.recv()` then returns Err (tx dropped) and reads this flag
                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the
                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).
                    if let Some(sink) = self.viewers.remove(&vid) {
                        sink.evicted.store(true, Ordering::Release);
                    }
                }
                self.stamp_viewer_count();
            }


            if !evicted.is_empty() {
                for vid in evicted {
                    self.viewers.remove(&vid);
                }
                self.stamp_viewer_count();
            }

            if !evicted.is_empty() {
                for vid in evicted {
                    // Mark eviction BEFORE dropping the sink: the writer's
                    // `rx.recv()` then returns Err (tx dropped) and reads this flag
                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the
                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).
                    if let Some(sink) = self.viewers.remove(&vid) {
                        sink.evicted.store(true, Ordering::Release);
                    }
                }
                self.stamp_viewer_count();
            }

-                    self.viewers.remove(&vid);

+                    // Mark eviction BEFORE dropping the sink: the writer's

+                    // `rx.recv()` then returns Err (tx dropped) and reads this flag

+                    // to emit a KIND_VIEWER_EVICTED marker (skip-to-live, off the

+                    // log lock) instead of dying silently (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).

+                    if let Some(sink) = self.viewers.remove(&vid) {

+                        sink.evicted.store(true, Ordering::Release);


Now `viewer_writer` — capture the exit reason and emit the marker on an evicted close:


fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
    for frame in &initial {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    while let Ok(frame) = rx.recv() {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
}

fn viewer_writer(
    send: SharedSend,
    session_id: u64,
    initial: Vec<Envelope>,
    rx: Receiver<Envelope>,
    evicted: Arc<AtomicBool>,
) {
    for frame in &initial {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    while let Ok(frame) = rx.recv() {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    // `tx` dropped. If this was an EVICTION for overflow (append set the flag
    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from
    // a session-exit EOF — so the serving brain skips to live (re-subscribes from
    // the ring floor) instead of dying silently. A NORMAL teardown (session end /
    // conn drop) leaves the flag false → no marker, exit quietly. This write is in
    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4
    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.
    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
    if evicted.load(Ordering::Acquire) {
        let marker = evicted_envelope(session_id);
        if let Ok(mut s) = send.lock() {
            let _ = write_frame(&mut *s, &marker);
        }
    }
}


fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {
    for frame in &initial {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    while let Ok(frame) = rx.recv() {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
}

fn viewer_writer(
    send: SharedSend,
    session_id: u64,
    initial: Vec<Envelope>,
    rx: Receiver<Envelope>,
    evicted: Arc<AtomicBool>,
) {
    for frame in &initial {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    while let Ok(frame) = rx.recv() {
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, &frame).is_ok(),
            Err(_) => false,
        };
        if !ok {
            return;
        }
    }
    // `tx` dropped. If this was an EVICTION for overflow (append set the flag
    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from
    // a session-exit EOF — so the serving brain skips to live (re-subscribes from
    // the ring floor) instead of dying silently. A NORMAL teardown (session end /
    // conn drop) leaves the flag false → no marker, exit quietly. This write is in
    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4
    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.
    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
    if evicted.load(Ordering::Acquire) {
        let marker = evicted_envelope(session_id);
        if let Ok(mut s) = send.lock() {
            let _ = write_frame(&mut *s, &marker);
        }
    }
}

/// `rx` with a BLOCKING socket write each. A failed write (dead viewer) ends the

/// thread; the drain thread's next `try_send` then sees `Disconnected` and

/// evicts the row. The drain thread itself never blocks on this socket.

-fn viewer_writer(send: SharedSend, initial: Vec<Envelope>, rx: Receiver<Envelope>) {

+    // `tx` dropped. If this was an EVICTION for overflow (append set the flag

+    // before dropping the sink), emit a KIND_VIEWER_EVICTED marker — DISTINCT from

+    // a session-exit EOF — so the serving brain skips to live (re-subscribes from

+    // the ring floor) instead of dying silently. A NORMAL teardown (session end /

+    // conn drop) leaves the flag false → no marker, exit quietly. This write is in

+    // the dedicated writer thread, OFF the log lock / off the drain (the W1/b4

+    // isolation is preserved). REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT.

+    // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]

+    if evicted.load(Ordering::Acquire) {

+        let marker = evicted_envelope(session_id);

+        if let Ok(mut s) = send.lock() {

+            let _ = write_frame(&mut *s, &marker);

/// A message to the CONTROLLER writer thread (W1). `Output` carries a sequenced


Now fix any direct `ViewerSink` literals (the b4 test constructs one) and check for other callers:


Bash

grep -rn "ViewerSink {\|viewer_writer(\|viewers.remove\|viewers.retain" crates/spt-daemon/src/broker.rs

Find ViewerSink literals and viewer removals


110:struct ViewerSink {
346:                    if let Some(sink) = self.viewers.remove(&vid) {
477:            thread::spawn(move || viewer_writer(writer_send, session_id, initial, rx, writer_evicted));
480:            ViewerSink {
578:                self.viewers.remove(&vid);
598:        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
663:fn viewer_writer(
2345:            .insert(0, ViewerSink { tx: vtx, send: vsend, _writer: thread::spawn(|| {}) });


562	    /// letterbox feed). Called on a controller resize. Viewer pushes are
563	    /// non-blocking with eviction, like [`OutputLog::append`].
564	    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {
565	        self.size = (rows, cols);
566	        if self.viewers.is_empty() {
567	            return;
568	        }
569	        let frame = size_envelope(self.session_id, rows, cols);
570	        let mut evicted: Vec<u64> = Vec::new();
571	        for (vid, sink) in self.viewers.iter() {
572	            if viewer_send_evicts(&sink.tx.try_send(frame.clone())) {
573	                evicted.push(*vid);
574	            }
575	        }
576	        if !evicted.is_empty() {
577	            for vid in evicted {
578	                self.viewers.remove(&vid);
579	            }
580	            self.stamp_viewer_count();
581	        }
582	    }
583
584	    /// Detach `sub` from whatever role it held: clear the controller slot iff it
585	    /// is the current controller (a no-op if a newer brain took over — a late
586	    /// disconnect can't unseat the live controller), and remove any viewer rows
587	    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps
588	    /// `driven_by` / `viewer_count` when a role actually changed.
589	    fn detach_if(&mut self, sub: &SharedSend) {
590	        let is_ctrl = self
591	            .controller
592	            .as_ref()
593	            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));
594	        if is_ctrl {
595	            self.clear_controller(); // drops the sink (ends its writer) + unlatches
596	        }
597	        let before = self.viewers.len();
598	        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
599	        if self.viewers.len() != before {
600	            self.stamp_viewer_count();
601	        }
602	    }
603

    /// letterbox feed). Called on a controller resize. Viewer pushes are
    /// non-blocking with eviction, like [`OutputLog::append`].
    fn set_size_and_notify(&mut self, rows: u16, cols: u16) {
        self.size = (rows, cols);
        if self.viewers.is_empty() {
            return;
        }
        let frame = size_envelope(self.session_id, rows, cols);
        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();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it
    /// is the current controller (a no-op if a newer brain took over — a late
    /// disconnect can't unseat the live controller), and remove any viewer rows
    /// owned by `sub`. Leaves `delivered_through` intact (D4-1). Re-stamps
    /// `driven_by` / `viewer_count` when a role actually changed.
    fn detach_if(&mut self, sub: &SharedSend) {
        let is_ctrl = self
            .controller
            .as_ref()
            .is_some_and(|c| Arc::ptr_eq(&c.send, sub));
        if is_ctrl {
            self.clear_controller(); // drops the sink (ends its writer) + unlatches
        }
        let before = self.viewers.len();
        self.viewers.retain(|_, sink| !Arc::ptr_eq(&sink.send, sub));
        if self.viewers.len() != before {
            self.stamp_viewer_count();
        }
    }


Line 576-579 in `set_size_and_notify` is the *same* overflow-eviction (a size push overflowing the viewer queue) — it should also skip-to-live, not die silently. Apply the same flag:


Edit

        if !evicted.is_empty() {
            for vid in evicted {
                self.viewers.remove(&vid);
            }
            self.stamp_viewer_count();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it

        if !evicted.is_empty() {
            for vid in evicted {
                // Same overflow eviction as `append` — signal skip-to-live so the
                // viewer re-subscribes rather than dying silently on a size push
                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).
                if let Some(sink) = self.viewers.remove(&vid) {
                    sink.evicted.store(true, Ordering::Release);
                }
            }
            self.stamp_viewer_count();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it


        if !evicted.is_empty() {
            for vid in evicted {
                self.viewers.remove(&vid);
            }
            self.stamp_viewer_count();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it

        if !evicted.is_empty() {
            for vid in evicted {
                // Same overflow eviction as `append` — signal skip-to-live so the
                // viewer re-subscribes rather than dying silently on a size push
                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).
                if let Some(sink) = self.viewers.remove(&vid) {
                    sink.evicted.store(true, Ordering::Release);
                }
            }
            self.stamp_viewer_count();
        }
    }

    /// Detach `sub` from whatever role it held: clear the controller slot iff it

-                self.viewers.remove(&vid);

+                // Same overflow eviction as `append` — signal skip-to-live so the

+                // viewer re-subscribes rather than dying silently on a size push

+                // (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).

+                if let Some(sink) = self.viewers.remove(&vid) {

+                    sink.evicted.store(true, Ordering::Release);


Now fix the b4 test's direct `ViewerSink` literal (line ~2345):


2340	            tx: ctx,
2341	            send: csend,
2342	            by: None,
2343	            last_ok: Instant::now(),
2344	            _writer: thread::spawn(|| {}),
2345	        });
2346	        // A VIEWER with a generous channel we CAN drain — to prove it stays fed.
2347	        let (vsend, _vclient, _vrecv) = controller_socket_pair();
2348	        let (vtx, vrx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);
2349	        log.viewers

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

            .insert(
                0,
                ViewerSink {
                    tx: vtx,
                    send: vsend,
                    evicted: Arc::new(AtomicBool::new(false)),
                    _writer: thread::spawn(|| {}),
                },
            );


            .insert(
                0,
                ViewerSink {
                    tx: vtx,
                    send: vsend,
                    evicted: Arc::new(AtomicBool::new(false)),
                    _writer: thread::spawn(|| {}),
                },
            );

let (vsend, _vclient, _vrecv) = controller_socket_pair();

let (vtx, vrx) = sync_channel::<Envelope>(VIEWER_CHANNEL_DEPTH);

-            .insert(0, ViewerSink { tx: vtx, send: vsend, _writer: thread::spawn(|| {}) });

+                    evicted: Arc::new(AtomicBool::new(false)),

+                    _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


Now the serve_attach side — the ViewerEvicted handler with rate-limiting. Check attach.rs imports/consts first.


1	//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
2	//! onto a local session for an operator on another node.
3	//!
4	//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
5	//! [`AttachRecord::Request`]; this module is the target-side pump between that
6	//! stream and the local broker-hosted session:
7	//!
8	//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
9	//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
10	//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
11	//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
12	//!   journal (a replayed input record cannot double-type);
13	//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
14	//!
15	//! ## Exactly-once lives at the EDGES, not the wire sends
16	//!
17	//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
18	//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
19	//! and *discards* everything interleaved — inside a full-duplex pump loop that
20	//! eats the operator's `Input` records (both sides starve; found the hard way
21	//! in `tests/attach.rs`). Idempotency holds without it because each direction
22	//! dedups at its **effect**: the PTY write is journaled by the operator-minted
23	//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
24	//! session-log `seq` (the render cursor). A post-restart re-serve may
25	//! re-transmit output records on the wire — duplicates are dropped at the
26	//! render cursor, the same discipline the local `OutputLog` replay uses
27	//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
28	//!
29	//! **Compute + files stay here**; the operator is a viewport (CONTEXT
30	//! §Remote-control vs local operation). The access gate runs before anything
31	//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
32	//! [`access_check`] sees the **handshake-proven** origin from the broker's
33	//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
34	//! your own running instance is ungated by default (ADR-0009 default-open);
35	//! the D5d whitelist drops in at this same call.
36	//!
37	//! Restart survival composes from D4/B5 with **no new machinery**: the broker
38	//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
39	//! A restarted target brain re-attaches the session from its output cursor,
40	//! resubscribes the stream from its net cursor, and re-drives journaled sends;
41	//! a restarted operator brain resubscribes and dedups rendered chunks by
42	//! `Output.seq`.
43
44	use std::io;
45
46	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
47	use spt_net::net::ndjson;
48
49	use crate::access::{access_check, AccessDecision, InboundClass};
50	use crate::brain::{now_ms, Brain, BrokerEvent};
51	use crate::msg::{decode_bytes, encode_bytes};
52
53	/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
54	/// served viewport is a **driver** — attaching wakes the endpoint, the
55	/// viewport ending detaches it (active → dormant; the transition echo is
56	/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
57	/// [`mark_driven_by`]: a session served without a perch has no resting
58	/// record to move.
59	// [impl->REQ-INST-3]
60	fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {

//! Remote terminal attach (M4-D5b, REQ-INST-8) — serve a byte-stream viewport
//! onto a local session for an operator on another node.
//!
//! The operator's daemon opened a bidi QUIC stream to this node (D4) and sent
//! [`AttachRecord::Request`]; this module is the target-side pump between that
//! stream and the local broker-hosted session:
//!
//! - session `Output` chunks → [`AttachRecord::Output`] lines onto the wire
//!   (fire-and-forget sends; `Output.seq` carries the dedup authority);
//! - inbound [`AttachRecord::Input`] → [`Brain::send_effect`] with the
//!   operator-minted `op_id` — exactly-once at the PTY write through the B5
//!   journal (a replayed input record cannot double-type);
//! - session `Exit` → [`AttachRecord::Exit`] + stream finish.
//!
//! ## Exactly-once lives at the EDGES, not the wire sends
//!
//! Stream sends here are deliberately **unjournaled** (`op_id: None`): a
//! journaled [`Brain::net_stream_send`] blocks reading events until its ack
//! and *discards* everything interleaved — inside a full-duplex pump loop that
//! eats the operator's `Input` records (both sides starve; found the hard way
//! in `tests/attach.rs`). Idempotency holds without it because each direction
//! dedups at its **effect**: the PTY write is journaled by the operator-minted
//! `Input.op_id`, and the operator's viewport dedups `Output` records by their
//! session-log `seq` (the render cursor). A post-restart re-serve may
//! re-transmit output records on the wire — duplicates are dropped at the
//! render cursor, the same discipline the local `OutputLog` replay uses
//! (REQ-HAZARD-RESTART-IDEMPOTENT at the boundary that matters).
//!
//! **Compute + files stay here**; the operator is a viewport (CONTEXT
//! §Remote-control vs local operation). The access gate runs before anything
//! is served: the [`AttachRecord::Request`] is *unsolicited wire inbound*, so
//! [`access_check`] sees the **handshake-proven** origin from the broker's
//! stream table — never payload bytes (KNOWN-HAZARDS 7.5). Remote-drive of
//! your own running instance is ungated by default (ADR-0009 default-open);
//! the D5d whitelist drops in at this same call.
//!
//! Restart survival composes from D4/B5 with **no new machinery**: the broker
//! owns the QUIC stream, the session, its `OutputLog`, and the effect journal.
//! A restarted target brain re-attaches the session from its output cursor,
//! resubscribes the stream from its net cursor, and re-drives journaled sends;
//! a restarted operator brain resubscribes and dedups rendered chunks by
//! `Output.seq`.

use std::io;

use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
use spt_net::net::ndjson;

use crate::access::{access_check, AccessDecision, InboundClass};
use crate::brain::{now_ms, Brain, BrokerEvent};
use crate::msg::{decode_bytes, encode_bytes};

/// Feed the resting-state machine at the attach edges (D9-2, REQ-INST-3): a
/// served viewport is a **driver** — attaching wakes the endpoint, the
/// viewport ending detaches it (active → dormant; the transition echo is
/// gate-armed for the endpoint's own pulse loop). Best-effort exactly like
/// [`mark_driven_by`]: a session served without a perch has no resting
/// record to move.
// [impl->REQ-INST-3]
fn feed_rest(endpoint: &str, event: crate::resting::RestEvent) {


use std::io;

use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};

use std::io;
use std::time::{Duration, Instant};

use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};


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

use std::io;

use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};

use std::io;
use std::time::{Duration, Instant};

use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};


Now add the rate-limit const and the loop state. Let me add the const before `serve_attach` and wire the handler. First the const:


88	enum ServeRole {
89	    /// The interactive controller (input + resize; wakes the endpoint).
90	    Controller,
91	    /// A read-only viewer (output only; wake-neutral).
92	    Viewer,
93	}
94
95	/// Serve one attach stream against this brain's broker, blocking until the
96	/// session exits, the operator detaches, or the gate refuses.
97	///
98	/// `origin_node` MUST be the handshake-proven remote id from the broker's
99	/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
100	/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
101	/// is being attached, for the gate. `net_from_seq` is this brain's resume
102	/// cursor on the attach stream (0 cold — also the safe post-restart choice:
103	/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
104	/// journal, and re-pumped output dedups at the operator's render cursor).
105	// [impl->REQ-INST-8]
106	// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
107	// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
108	pub fn serve_attach(
109	    brain: &mut Brain,

enum ServeRole {
    /// The interactive controller (input + resize; wakes the endpoint).
    Controller,
    /// A read-only viewer (output only; wake-neutral).
    Viewer,
}

/// Serve one attach stream against this brain's broker, blocking until the
/// session exits, the operator detaches, or the gate refuses.
///
/// `origin_node` MUST be the handshake-proven remote id from the broker's
/// stream table (`NetStreamInfo::remote_id_hex`) — never payload bytes
/// (REQ-HAZARD-WAN-ORIGIN-AUTH). `endpoint` names the endpoint whose session
/// is being attached, for the gate. `net_from_seq` is this brain's resume
/// cursor on the attach stream (0 cold — also the safe post-restart choice:
/// the replayed `Request`/`Input` records re-gate and dedup at the PTY
/// journal, and re-pumped output dedups at the operator's render cursor).
// [impl->REQ-INST-8]
// [impl->REQ-HAZARD-WAN-ORIGIN-AUTH]
// [impl->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
pub fn serve_attach(
    brain: &mut Brain,


/// Serve one attach stream against this brain's broker, blocking until the
/// session exits, the operator detaches, or the gate refuses.

/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction
/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is
/// hopelessly behind under a sustained flood will be re-evicted immediately after
/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With
/// it, the operator sees an intermittent LIVE burst each interval (forward
/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.
const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);

/// Serve one attach stream against this brain's broker, blocking until the
/// session exits, the operator detaches, or the gate refuses.


/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction
/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is
/// hopelessly behind under a sustained flood will be re-evicted immediately after
/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With
/// it, the operator sees an intermittent LIVE burst each interval (forward
/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.
const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);

/// Serve one attach stream against this brain's broker, blocking until the
/// session exits, the operator detaches, or the gate refuses.

+/// Minimum spacing between skip-to-live re-subscribes after a viewer eviction

+/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). HARD constraint: an evicted viewer that is

+/// hopelessly behind under a sustained flood will be re-evicted immediately after

+/// each re-subscribe; without spacing that is an evict→resubscribe CPU spin. With

+/// it, the operator sees an intermittent LIVE burst each interval (forward

+/// progress), never a busy-loop. ~200ms = several bursts/sec, imperceptible lag.

+const RESUBSCRIBE_INTERVAL: Duration = Duration::from_millis(200);


Now add the loop state (`last_resubscribe`) and the `ViewerEvicted` handler arm:


    // The attached session — captured from the Request so the terminal paths can
    // explicitly release this worker's role (clearing driven_by / viewer_count
    // promptly, not only on connection drop).
    let mut attached_session: Option<u64> = None;

    // The attached session — captured from the Request so the terminal paths can
    // explicitly release this worker's role (clearing driven_by / viewer_count
    // promptly, not only on connection drop).
    let mut attached_session: Option<u64> = None;
    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the
    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees
    // intermittent live bursts, never a CPU spin.
    let mut last_resubscribe: Option<Instant> = None;


    // The attached session — captured from the Request so the terminal paths can
    // explicitly release this worker's role (clearing driven_by / viewer_count
    // promptly, not only on connection drop).
    let mut attached_session: Option<u64> = None;
    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the
    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees
    // intermittent live bursts, never a CPU spin.
    let mut last_resubscribe: Option<Instant> = None;

+    // Last skip-to-live re-subscribe (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT): bounds the

+    // evict→resubscribe rate so a hopelessly-behind viewer under flood sees

+    // intermittent live bursts, never a CPU spin.

+    let mut last_resubscribe: Option<Instant> = None;


Now the handler arm — after the `Displaced` arm, before `NetStreamEof`:


            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {

            // This VIEWER overflowed its broker subscription queue and was evicted
            // (it forwards slower than the drain fans out under flood). SKIP TO
            // LIVE — re-subscribe from the current ring floor — instead of tearing
            // the attach down: DISTINCT from a session-exit EOF. Rate-limited
            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained
            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU
            // spin. The next live Output carries a forward seq jump; the operator's
            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.
            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
            BrokerEvent::ViewerEvicted { session_id: sid }
                if attached && Some(sid) == attached_session =>
            {
                if let Some(prev) = last_resubscribe {
                    let since = prev.elapsed();
                    if since < RESUBSCRIBE_INTERVAL {
                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);
                    }
                }
                brain.attach_skip_to_live(sid, Some(origin_node))?;
                last_resubscribe = Some(Instant::now());
            }

            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {


            // This VIEWER overflowed its broker subscription queue and was evicted
            // (it forwards slower than the drain fans out under flood). SKIP TO
            // LIVE — re-subscribe from the current ring floor — instead of tearing
            // the attach down: DISTINCT from a session-exit EOF. Rate-limited
            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained
            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU
            // spin. The next live Output carries a forward seq jump; the operator's
            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.
            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
            BrokerEvent::ViewerEvicted { session_id: sid }
                if attached && Some(sid) == attached_session =>
            {
                if let Some(prev) = last_resubscribe {
                    let since = prev.elapsed();
                    if since < RESUBSCRIBE_INTERVAL {
                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);
                    }
                }
                brain.attach_skip_to_live(sid, Some(origin_node))?;
                last_resubscribe = Some(Instant::now());
            }

            // The operator closed its send side: the viewport is over.
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == stream_id => {

+            // This VIEWER overflowed its broker subscription queue and was evicted

+            // (it forwards slower than the drain fans out under flood). SKIP TO

+            // LIVE — re-subscribe from the current ring floor — instead of tearing

+            // the attach down: DISTINCT from a session-exit EOF. Rate-limited

+            // (RESUBSCRIBE_INTERVAL) so a hopelessly-behind viewer under sustained

+            // flood sees intermittent LIVE bursts, never an evict→resubscribe CPU

+            // spin. The next live Output carries a forward seq jump; the operator's

+            // render cursor snaps to it (tail -f reconnect). VIEWER-only → B2-safe.

+            // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]

+            BrokerEvent::ViewerEvicted { session_id: sid }

+                if attached && Some(sid) == attached_session =>

+                if let Some(prev) = last_resubscribe {

+                    let since = prev.elapsed();

+                    if since < RESUBSCRIBE_INTERVAL {

+                        std::thread::sleep(RESUBSCRIBE_INTERVAL - since);

+                brain.attach_skip_to_live(sid, Some(origin_node))?;

+                last_resubscribe = Some(Instant::now());

wire.net_stream_send(stream_id, &[], None, true)?;


Code complete across all 4 files. Let me compile-check the daemon crate.


Bash

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

Check spt-daemon compiles


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


Clean compile. Now clippy (CI form) + the broker unit tests to confirm no regression:


cargo clippy -p spt-daemon --all-targets -- -D warnings 2>&1 | tail -25

Clippy CI form on daemon


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


cargo test -p spt-daemon --lib broker:: 2>&1 | tail -30

Run broker lib unit tests


   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 16.61s
     Running unittests src\lib.rs (target\debug\deps\spt_daemon-6c41d016386d7ce3.exe)

running 10 tests
test broker::tests::advance_delivered_is_monotonic ... ok
test broker::tests::contiguous_advance_freezes_on_a_gap ... ok
test broker::tests::delivered_cursor_advances_only_on_ok_and_is_monotonic ... ok
test broker::tests::sessions_reply_resume_seq_defaults_when_absent ... ok
test broker::tests::viewer_overflow_or_disconnect_evicts_never_blocks ... ok
test broker::tests::input_enqueue_drops_and_stamps_on_full_then_heals_on_drain ... ok
test broker::tests::input_writer_preserves_strict_fifo_order_through_the_sole_writer ... ok
test broker::tests::a_full_controller_does_not_starve_a_viewer_or_block_the_drain ... ok
test broker::tests::superseded_controller_writer_is_epoch_gated_silent_one_live_writer_per_conn ... ok
test broker::tests::apply_once_enqueues_ptywrite_exactly_once_and_dedups_replay ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 340 filtered out; finished in 0.00s


All 10 broker tests pass (including the b4 tests + the existing viewer-eviction test). Now the traceability gate:


Run traceability check


  [OK] REQ-TERM-6  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Thread-spanning digest across session boundaries: a per-endpoint session ledger (`<perch>/sessions.log`) appended at first bind and by `api boundary` on `/clear`|`/compact` session rotation, the digest enumerating the last K sessions so its rolling window bridges a boundary, and a distinctive in-timeline boundary marker (DigestEntry::Boundary). The digest follows the live-agent thread, not a single session.
  [OK] REQ-TERM-7  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Two-origin digest merge: spt-owned context-injection entries (psyche_download | echo_mirror | owl_message) appended by spt to the endpoint `digest.log`, timestamp-interleaved with the adapter's extracted activity records into one ordered timeline, via a distinct context-injection record category. Data model only this milestone; GUI collapse/expand and the echo-reads-digest delta loop are deferred to the surfaces that consume them.
  [OK] REQ-UPD-1  required: [impl, unit, int]  stages: -doc +impl +unit +int
       Peer-propagated update over P2P
  [OK] REQ-UPD-2  required: [impl, unit]  stages: -doc +impl +unit -int
       All binaries signature-verified before handoff
  [OK] REQ-UPD-3  required: [impl, unit, int]  stages: -doc +impl +unit +int
       No endpoint process terminates/suspends during self-update
  [OK] REQ-UPD-4  required: [impl, unit]  stages: -doc +impl +unit -int
       Update gated on user confirmation by default; opt-in full-auto
  [OK] REQ-UPD-5  required: [impl, unit]  stages: -doc +impl +unit -int
       spt-core ripple-updates registered adapters
  [OK] REQ-UPD-6  required: [doc, impl, unit, int]  stages: +doc +impl +unit +int
       Platform-targeted update sets and debug rollout: signed multi-platform update metadata, recipient platform selection, channel-scoped monotonic counters, debug-channel opt-in via release-key overlay, local staging plus pull-based peer propagation, and maintainer-only convergence tooling (ADR-0016)
  [OK] REQ-UPD-7  required: [impl, unit]  stages: -doc +impl +unit -int
       Origin-source update bootstrap (`spt update fetch`): pull the latest signed release directly from the GitHub release origin (`SaberMage/spt-releases`) — the per-platform artifact + its `<asset>.release.json` SignedRelease metadata — and stage it through the EXISTING verify→stage pipeline (the same `plan_verified` gate: two-key signature + channel + monotonic rollback floor + SHA-256), after which the normal consent-notif / `spt update apply` flow is unchanged. Closes the peer-only-discovery gap (REQ-UPD-1): a first-in-fleet / isolated node can update with no peer to pull from. The signed-release anchor keeps the GitHub transport untrusted-but-verified.
  [OK] REQ-UPD-8  required: [impl, unit]  stages: -doc +impl +unit -int
       Platform-safe `spt update fetch` + apply platform-guard (v0.3.1 cross-OS brick fix): `spt update fetch` stages the signed multi-platform `SignedUpdateSet` (`update-set.json` + every platform artifact it names), never a platform-blind single `SignedRelease`, so local apply selects `current_platform()` and P2P re-serve lets each peer select ITS own platform. Defense-in-depth: `apply_staged` REFUSES a staged single-release artifact unless it is platform-stamped for THIS node (an unstamped pre-v0.3.2 single, or a single stamped for another OS, fail-safe refuses — the guard that alone prevents the v0.3.1 brick where a Linux ELF was applied as `spt.exe`). UX: a friendly post-apply message (`Updated spt-core to vX.Y.Z.` + changelog URL) driven by an additive `product_version` metadata field, with a release-counter fallback when absent.
  [OK] REQ-UPD-9  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `gh_release` adapter [update] avenue (optional signing): an adapter declares `[update] avenue = "gh_release", repo = "user/repo"` (+ optional `asset`, default `adapter.spt`; + optional Ed25519 `signing_key`); spt-core's ripple compares the repo's LATEST GitHub release version against the installed adapter version and, when newer, auto-updates by fetching the release `.spt` archive (the REQ-INSTALL-9 `--release` fetch primitive) → verifies the `.spt` against `signing_key` if declared, else HTTPS+GitHub first-acquisition trust → re-extracts + re-registers the adapter root. Lets a harness adapter ship updates from its own GitHub releases with NO signing tooling or plugin coupling (removes the perri file_pull/delegated avenue blockers). Acquisition-trust mirrors `--release` + the installer first-fetch; does not alter spt-core self-update (REQ-UPD-1..8).
  [--] REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT  required: [doc, impl, unit]  stages: -doc +impl -unit -int
       A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
       `spt whoami` is a thin ALIAS for `spt endpoint list` (full output: the SELF pin + the subnet roster) — the standalone bare-id command is dropped (the `id=$(spt whoami)` capture was never a real pattern: env vars don't persist between agent tool calls). The one new render: the `endpoint list` SELF pin carries the Self endpoint's authored `endpoint description` (info::read_info(...).resources) when present, inline after the liveness state. whoami stays a top-level hot-path verb (parse unchanged, REQ-MSG-9).

Findings:
  [must] missing_stage REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT stage=doc — Requirement is missing documentation evidence
  [must] missing_stage REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT stage=unit — Requirement is missing unit test evidence


Req registered, impl tagged. Two gaps: `doc` and `unit`. Let me add the doc tag to the JIT plan, then delegate unit tests to spt-test-engineer.


Edit

## Design (doyle's 3 constraints, b4-doc lines 24-32)

## Design (doyle's 3 constraints, b4-doc lines 24-32)

<!-- [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] -->
The skip-to-live contract: an evicted viewer is signalled (not silently dropped), re-subscribes
from the live ring tail (skips the uncatchable backlog), and is rate-limited so a hopelessly-
behind viewer under flood sees intermittent LIVE bursts rather than spinning. Eviction itself
stays — it is correct session-protection; only the SILENT+PERMANENT death is fixed.


The file C:\Users\decid\Documents\projects\spt-core\V0.13.0-SKIP-TO-LIVE-JIT.md has been updated successfully. (file state is current in your context — no need to Read it back)

## Design (doyle's 3 constraints, b4-doc lines 24-32)

<!-- [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] -->
The skip-to-live contract: an evicted viewer is signalled (not silently dropped), re-subscribes
from the live ring tail (skips the uncatchable backlog), and is rate-limited so a hopelessly-
behind viewer under flood sees intermittent LIVE bursts rather than spinning. Eviction itself
stays — it is correct session-protection; only the SILENT+PERMANENT death is fixed.

# V0.13.0 — viewer SKIP-TO-LIVE on eviction — JIT plan

**Status:** DESIGNED (this doc). Closes **p0_paste** + **a_journaled-Linux** (post-b4 these
collapsed to ONE root — doyle's post-b4 re-measure 2026-06-21). doyle-GATED: "skip-to-live
is REAL, not relax" (eviction itself is correct session-protection; SILENT+PERMANENT
eviction is the bug). Off `viewer-drain-decouple-b4@677c3e7`. Implement AFTER b4.

## Root

Viewer eviction in `OutputLog::append` (broker.rs:326-338, `try_send` Full / Disconnected
→ `viewers.remove`) is **SILENT + PERMANENT**:

- serve_attach (attach.rs) subscribes to the broker as a VIEWER → `add_viewer` (broker.rs:445)
  → `ViewerSink` (256-deep `VIEWER_CHANNEL_DEPTH`) + a `viewer_writer` thread.
- serve_attach forwards each frame `read_event → b64decode → re-encode AttachRecord →
  net_stream_send` — SLOWER than the drain fans out under flood. The 256 channel overflows.
- The drain (append) evicts: `viewers.remove(vid)` drops `ViewerSink` → drops `tx` →
  `viewer_writer`'s `rx.recv()` returns Err → the writer `return`s **writing nothing**
  (broker.rs:655-663). serve_attach's `brain.read_event()` then **just stops** getting
  `Output` — NO EOF, NO error → serve_attach blocks forever → operator receives nothing.
  `attach_received_output=FALSE`.

Post-b4 (doyle 2026-06-21): a_journaled-Linux now = subscribed=true, drain FREE (88902
appends), VIEWER EVICTED (c1-EVICT @ seq 88901), got_output=FALSE — the **identical
signature to p0_paste**. b4 unthrottled the drain; the remaining failure is this
viewer-eviction. ONE root → skip-to-live closes BOTH.

## Design (doyle's 3 constraints, b4-doc lines 24-32)

### (1) EVICTION SIGNAL — broker→viewer, DISTINCT from session-exit EOF
The evicted viewer must learn it was evicted (≠ the session ended). serve_attach must NOT
tear down on it (it tears down on real EOF/Exit).

- `ViewerSink` += an eviction notifier shared with its `viewer_writer`: `evicted: Arc<AtomicBool>`.
- `append` overflow/disconnect arm: set `evicted.store(true)` BEFORE `viewers.remove(vid)`
  (drop `tx`). (Still under the log lock — just an atomic store, never a blocking write.)
- `viewer_writer`: when `rx.recv()` returns Err, check `evicted`:
    - `true`  → write ONE `KIND_VIEWER_EVICTED` marker frame to `send`, then exit.
    - `false` → exit silently (normal teardown: session end / conn drop).
  The marker write happens **in the writer thread = OFF the log lock / off the drain** — same
  isolation as every viewer frame (a bounded, isolated, dedicated-writer write). No W1/b4
  hazard re-introduced.
- New wire surface:
    - `msg.rs`: `KIND_VIEWER_EVICTED` + `evicted_envelope(session_id)` (+ a tiny
      `ViewerEvictedEvent { session_id }` payload struct, mirror `SizeEvent`).
    - `brain.rs`: `BrokerEvent::ViewerEvicted { session_id }` + a decode arm (mirror `KIND_SIZE`).
  Additive + tagged → N-1 peers skip an unknown kind (degrade-don't-die, per AttachRecord posture).

### (2) serve_attach: re-subscribe from the CURRENT RING FLOOR (skip-to-live)
On `BrokerEvent::ViewerEvicted` (NOT EOF — does NOT return/teardown):
- Re-issue the subscribe at the live tail: `brain.attach_as(session, LIVE_TAIL, Viewer, origin)`.
- LIVE_TAIL semantics: `from_seq = u64::MAX` → `add_viewer`'s `s >= from_seq` ring filter
  (broker.rs:455) replays NOTHING → the viewer jumps to live, sees the next live burst, skips
  the uncatchable backlog (tail -f reconnect). (Confirm u64::MAX is safe vs any `from_seq`
  arithmetic; else add an explicit "tail" flag to the subscribe.)

### (3) NO evict→resubscribe busy-loop — RATE-LIMIT (HARD constraint)
serve_attach tracks `last_resubscribe: Instant`. Min `RESUBSCRIBE_INTERVAL` (~150-250ms).
If a `ViewerEvicted` arrives within the interval, sleep the remainder before re-subscribing.
Forward-progress guarantee: each re-subscribe delivers the live tail → under max-flood the
operator sees intermittent LIVE bursts, never a CPU spin. (Do NOT lead with serve
stall/gap-detection — fragile fallback only.)

## ⚠ RISK / open gate — COLD-BRAIN REJECT-GAP (must resolve, B2-adjacent)

serve_attach's `brain` is **cold-start** (brain.rs:1186-7: "the production dispatch serve
brain keeps the legacy `next_seq` path"; `session_cursors` EMPTY). The legacy path
(brain.rs:618-626) treats a forward seq jump as a **FATAL "output gap" Err**. The
post-eviction replay-from-floor (skip-to-live) IS a forward jump → serve_attach returns Err →
**tears down** → skip-to-live BREAKS at the cold-brain dedup.

FIX: on the skip-to-live re-subscribe, RESET serve_attach brain's `next_seq` to the new floor
so the replay is contiguous-by-construction. Mirror the seeded-map reset (brain.rs:1189-1190
`session_cursors.insert(session_id, from_seq)`) but for the legacy `next_seq` field. Add a
`Brain` method (e.g. `reset_output_cursor(from_seq)` / extend `attach_as` to reset `next_seq`
on the cold path too). The snap-above path (brain.rs:595-606) ALREADY documents "a
post-eviction ring-floor jump" as the only legitimate forward jump — so the resume-brain side
is ready; this only needs the cold serve-brain to accept it.

OPERATOR render cursor: serve_attach forwards `seq` verbatim; the rc operator must accept the
forward jump (skip-to-live) in its AttachRecord::Output dedup. VERIFY rc.rs render dedup is
snap-above, not reject-gap. If reject-gap, that is a 2nd reset point.

B2 / KH: both resets are VIEWER-only — a viewer never advances `delivered_through`, is not
authoritative, exposes no resume cursor → B2-SAFE (doyle's gate rationale, b4-doc line 24).
Intersects REQ-HAZARD-CONTROLLER-WRITER-REORDER only by adjacency; no controller path touched.
**CONFIRM with doyle before fold** (he owns the B2 invariant + the p0_paste forkpty verify).

## Traceability (CLAUDE.md binding)

New req `REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT` (or fold into REQ-HAZARD-VIEWER-ISOLATION). Add to
`traceable-reqs.toml` FIRST, then satisfy: `doc` (this file) / `impl` (broker+brain+attach) /
`unit` (below). INT evidence = p0_paste warm-forkpty gate (activates on fold, rule 5).

## Tests (delegate → spt-test-engineer)

- unit (broker): `append` overflow-evict sets the eviction notifier (not a silent remove);
  `viewer_writer` writes `KIND_VIEWER_EVICTED` on an evicted rx-close + NOTHING on a normal
  close.
- unit (attach/serve): on `ViewerEvicted` serve_attach re-subscribes from the floor, does NOT
  tear down (distinct from EOF), and rate-limits (≥2 rapid evictions ⇒ ≥1 interval of spacing,
  no busy-loop).
- unit (brain): cold-brain `next_seq` reset accepts the post-eviction forward jump (no
  "output gap" Err); a NON-reset forward jump still errors (guard unchanged elsewhere).
- int: p0_paste (warm forkpty, kitsubito) green — operator receives intermittent live bursts
  after eviction. (doyle's gate.)

## Files

- `crates/spt-daemon/src/msg.rs` — `KIND_VIEWER_EVICTED`, `evicted_envelope`, payload struct.
- `crates/spt-daemon/src/broker.rs` — `ViewerSink.evicted`, `append` evict arm, `add_viewer`,
  `viewer_writer`.
- `crates/spt-daemon/src/brain.rs` — `BrokerEvent::ViewerEvicted` + decode arm; cold-cursor reset.
- `crates/spt-daemon/src/attach.rs` — `serve_attach` ViewerEvicted handling + rate-limit +
  re-subscribe-from-floor.
- `traceable-reqs.toml`.

## Sequencing vs the board (doyle 2026-06-21)

- b4 = keep (works). Windows a_journaled flip = STRUCTURALLY EXONERATED (subscribe path =
  nethost StreamLog, not OutputLog; Windows ctrl channel never fills at pumped=249<<4096) →
  likely flake; doyle confirming repro. Don't block fold on it.
- THIS (skip-to-live) = closes p0_paste + a_journaled-Linux. NEXT.
- g2 = real inject-acceptance fix (dispatch_endpoint_input must not starve under output flood);
  still RED post-b4. SEPARATE, AFTER this.
- Fold order: b4 + pump-carrier-fix + skip-to-live → delivery-control → doyle folds → release.

+The skip-to-live contract: an evicted viewer is signalled (not silently dropped), re-subscribes

+from the live ring tail (skips the uncatchable backlog), and is rate-limited so a hopelessly-

+behind viewer under flood sees intermittent LIVE bursts rather than spinning. Eviction itself

+stays — it is correct session-protection; only the SILENT+PERMANENT death is fixed.


Bash

traceable-reqs check 2>&1 | grep -A2 "SKIP-TO-LIVE\|^Findings\|must\]" | head -20; echo "=== exit: ${PIPESTATUS[0]} ==="

Re-check traceability after doc tag


  [--] REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT  required: [doc, impl, unit]  stages: -doc +impl -unit -int
       A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)
  [OK] REQ-WHOAMI-1  required: [doc, impl, unit]  stages: +doc +impl +unit -int
--
Findings:
  [must] missing_stage REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT stage=doc — Requirement is missing documentation evidence
  [must] missing_stage REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT stage=unit — Requirement is missing unit test evidence
=== exit: 1 ===


grep -rln "doc->REQ" --include=*.md . | head; echo "=== config ==="; grep -n "doc\|scan\|include\|path\|glob\|src" traceable-reqs.toml | head -20

Find doc tag locations and config


./AGENTS.md
./CLAUDE.md
./CONTEXT.md
./docs/adr/0005-totp-seeded-spake2-node-pairing.md
./docs/adr/0010-immutable-home-subnet-fork-not-rehome.md
./docs/adr/0014-publish-target-repo-and-lapse-proof-canonical-url.md
./docs/adr/0016-platform-targeted-update-sets.md
./docs/adr/0020-event-envelope-sole-arriving-format-reply-to-removed.md
./docs/DEBUG-CONVERGE-PLAN.md
./docs/DEBUG-ROLLOUT.md
=== config ===
4:# KNOWN-HAZARDS invariant lives here as a REQ-* id (see docs/TRACEABILITY.md
12:[scan]
14:# carries doc-stage tags (e.g. §Installation). installer/ added M6-D2.
15:roots = ["src", "tests", "docs", "crates", "installer", "CONTEXT.md", "docs-site", ".github"]
18:# Default policy applied at activation: doc + impl + unit. Networking/lifecycle
63:title = "spt-store::BranchStore (git branch as versioned KV; commit=checkpoint/tip=resume, atomic multi-key, merge-native sync) is the substrate for coarse/durable/audited state (context, registry snapshot+distribution, daemon checkpoint); hot paths (B5 fsync journal) + indexed queries (SQLite spool) excluded (ADR-0011)"
64:required_stages = ["impl", "unit"]  # activated M4-D6a: spt_store::branchstore::BranchStore — bare seed repo + linked worktrees (relative-path config, repair fallback), ensure_branch = parentless empty-tree seed (independent KV roots; sync joins with --allow-unrelated-histories at first contact), commit_in_worktree = ONE commit per write incl. atomic multi-key (no-op mints no empty checkpoint), tip/read_at_tip = the recovery read. Git-CLI-backed per ADR-0013 (bounded runner gitrun.rs, hazard 5.3; identity pinned via env, never user config). First consumer: contextstore.rs two-tier tracked/ layout (a-<id>/p-<project> branches) + project.rs project_id derivation (remote-URL slug → toplevel folder → dir name). unit = init-idempotent + commit/read/tip + no-op-no-commit + one-commit-multi-key + independent-roots + tier layout/commits + p-branch-aggregates-agents + invalid-id-refused + url-normalization + derivation ladder
70:required_stages = ["doc", "impl", "unit"]
74:required_stages = ["doc", "impl", "unit"]
77:title = "Adapter strings — [strings] KV tree, dot-path get-string resolving through the profile leaf-replace overlay, set-string editing a local profile's [strings] only; data-only (nothing executes a string)"
78:required_stages = ["doc", "impl", "unit"]  # activated 2026-06-12: M9-T2 (CONTEXT.md §adapter strings, adapter-string get/set CLI + overlay resolution)
82:required_stages = ["doc", "impl", "unit"]  # activated 2026-06-12: M9-T3 (CONTEXT.md §keyword hints, api hint surface + per-session seen-set)
85:title = "File-backed adapter [strings] (M12-W3-T3.1): a [strings] dot-path value MAY be an inline-table FILE POINTER `key = { file = \"rel/path\" }` resolved to the file's contents at get-string time, keeping large bodies (skill-instructions, hint text) out of the manifest. A value-position table with a `file` key IS the pointer form (reserved — cannot double as data). Per-adapter aux storage `adapters/<adapter>/strings/`; pointers resolve relative to it with CONTAINMENT (reject `..`/absolute escaping the dir). UPDATE-SAFETY: a LOCAL profile's file-pointers resolve relative to the user-owned local-profile dir (NOT adapter-shipped strings/, which adapter updates overwrite), or the local profile inlines. Validate-at-register (fail-fast on a bad/escaping/missing pointer) + LAZY read at get-string (live file edits reflect, no re-register) + skip-diagnostics on missing-at-read (no hard-crash, mirrors [digest]). Rides the same leaf-replace profile overlay as the rest of [strings]."
86:required_stages = ["doc", "impl", "unit"]  # activated M12-W3
89:title = "Cross-adapter fallback target addressing (M12-W3-T3.2): a cross-adapter fallback target is addressed as `<adapter>:<profile>` (not just a bare adapter_name), resolved through the one composite-addressing resolver (registry::resolve_option) at every adapter-option read site so a fallback may select a shipped/local profile (e.g. a `ccs` profile). CONTEXT.md §cross-adapter-fallback reconciled (\"ccs is a profile; cross-adapter fallback may target <adapter>:<profile>\"). Contract-only this milestone: the node-wide fallback SETTING + its rate-limit invocation are deferred to the consuming milestone (the runtime path does not exist yet); this REQ guarantees the ADDRESSING resolves."
90:required_stages = ["doc", "unit"]  # activated M12-W3 (contract-only: no int)
93:title = "Adapter-declared shortcut basename (M12-W2 follow-on): an optional `[adapter] shortcut_basename` manifest field names the basename the `spt endpoint run` picker bakes into the generated `<basename>-<id>` launcher shortcut (REQ-RUN-SHORTCUT). Absent ⇒ the harness-agnostic default `spt` (→ `spt-<id>`); an adapter sets it to brand its shortcuts (claude-spt → `cc` → `cc-<id>`), so the Claude-Code-ness lives in the PUBLISHED adapter manifest, never hardcoded in spt-core. The picker reads it from the RESOLVED manifest of the selected adapter (registry::resolve_option), falling back to `spt` when absent/empty/unresolvable. Additive + N-1-safe (serde-default Option, omitted from serialization when absent; old manifests parse clean); manifest.schema.json regenerated from the derive (ADR-0001, CI drift-gated). Documented in docs/MANIFEST.md `[adapter]` section + the claude-spt worked example — the adapter-author contract perri builds spt-claude-code against."
94:required_stages = ["doc", "impl", "unit"]  # activated M12-W2 follow-on
98:required_stages = ["doc", "impl", "unit", "int"]  # activated v0.9.0 W2: doc = CONTEXT.md §179 host_binaries candidate rule + docs/MANIFEST.md + docs-site harness-contract host_binaries field. impl = the `[adapter] host_binaries` field (manifest.rs, serde-default omit-empty) + spt_store::proc::exe_basename (Win QueryFullProcessImageNameW / Linux /proc/pid/exe) + the resolver's host_binaries candidate filter (spt-runtime resolve). unit = host_binaries_optional_and_n1_safe (manifest) + exe_basename_resolves_current_process/dead_pid_is_none (proc) + no_candidate/normalize tests (resolve). int folds into REQ-START-5's final bringup E2E
152:required_stages = ["doc", "impl", "unit"]  # activate v0.8.0: doc = CONTEXT.md "Manifest resolution from --adapter (since v0.8.0)" §inbound-api-surface. impl = api/mod.rs run() — when args.manifest is None, resolve (record, manifest) via registry::resolve_option(perch::adapters_dir(), &args.adapter) (the same composite name:profile resolution digest.rs + the gh_release update use), setting manifest = Some + install_dir = record.source_dir (PRECISE — supersedes the --manifest-parent approximation for the resolved path, fixing REQ-INSTALL-11's copy-mode psyche edge); args.manifest = Some keeps the explicit-path override (install_dir = its parent). Backward-compatible: an unregistered/dev adapter with no --manifest still yields manifest=None (registry miss), unchanged. unit = registry-resolve-when-manifest-absent (registered name:profile → manifest+source_dir), --manifest-override-wins, unregistered-miss → None. int rides the existing api/contract E2E.


The scan roots don't include root-level `.md` files (only `CONTEXT.md`, `docs/`, etc.). The JIT doc isn't scanned. Skip-to-live is a real observable semantic in the remote-control model — CONTEXT.md is its authoritative home. Let me find the viewer section there.


grep -n "rc --view\|viewer\|Viewer\|evict\|RCVIEW\|read-only\|skip-to-live\|tail -f" CONTEXT.md | head -20

Find viewer section in CONTEXT.md


161:**`spt endpoint purge <id>`** (CLI, not `api`) — the standalone, formal **full teardown**: wipe an endpoint and *every* record keyed on it. It is the dev/CI sibling of `api session-end --erase` (which is adapter-triggered at session end); `purge` is the explicit operator/test command for clean setup-and-reset. **Deliberately NOT consent-gated** — a local dev/test op, never a peer-visible action. **Offline-only**: it refuses a live / daemon-hosted endpoint (deleting records out from under a running host would let the daemon re-create or re-host mid-purge); **`--force`** stops it first (→ the daemon reconcile un-hosts it and reaps its Psyche) and then purges. **`--yes`** skips the interactive confirm (the CI path); purge refuses removing the **caller's own running id**. It is **node-local** — purge reaches only *this* node's records; a remote endpoint's records are unreachable and its subnet-registry rows decay on their own via the epoch-lease eviction. It removes: the perch directory **tree** recursively (incl every nested `{id}-psyche` / `{id}-w*` / shell child — info.json, ready marker, the `sessions.log` ledger, spool, the idle/echo-gate sentinels, the auth token); the registry address; the **context store** (`a-<id>` branch + worktree and the `<id>/` rows in every `p-<project>` branch — the same path `endpoint fork --delete-source` uses); and the node-local trust rows keyed on the id (access + visibility). Implementation-wise it is `fork --delete-source` generalized (recursive perch-remove + unregister + context `remove_endpoint`) plus the trust-record cleanup, sharing `endpoint rename`'s record-set enumeration and offline-only gate.
339:The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.
340:_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.
342:<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->
343:**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)
438:Two granularities, both first-class. `send-keys` — raw byte stream including escape sequences (e.g. `Ctrl-C`). `send-line` — cooked line-level input flushed on newline (the common case; matches how user input reaches an agent prompt). **PTY input is single-writer (v0.13.0 P0, KNOWN-HAZARDS 7.17):** broker-side, every write to a hosted session's PTY goes through ONE per-session input-writer thread — the sole caller of the blocking `write_input`, fed by a bounded FIFO. All callers (operator keystrokes, message injection, the inject-floor flush) ENQUEUE and return immediately, so a paste burst that fills the harness input buffer parks only that thread, never the broker dispatch thread. A genuinely wedged harness (queue full) DROPS excess input + stamps the perch `input_backpressure` (a visible signal, healed on resume); the daemon never wedges. This is the input-side mirror of the output-side single-writer (controller/viewer writer threads, 7.12). **rc paste is client-originated on Windows (v0.13.0 P1/P1b, KNOWN-HAZARDS 7.18):** the harness runs daemon-side with no access to the operator's local clipboard, so `spt rc` itself reads the local clipboard on a **right-click** and injects a *bracketed* paste (`ESC[200~`…`ESC[201~`) — the harness's bracketed-paste mode lands a multi-line paste intact with no per-newline submit-storm, content verbatim. (ctrl+V is NOT intercepted: Windows Terminal consumes it as its own paste accelerator and injects the clipboard as keystrokes — which the broker no longer wedges on, 7.19.) Because that capture also steals WT's native scroll, rc forwards the **scroll wheel** to the harness as SGR mouse reports when the harness has mouse reporting on (7.20). cfg(windows) only; Unix terminals paste + scroll natively through the byte pump. **An operator input flood must not deadlock the broker (v0.13.0 P1b, KNOWN-HAZARDS 7.19):** the applied-ack is opt-in (`InputReq.ack`) — the fire-and-forward rc path sends no-ack so the broker's per-conn handler never blocks writing an ack back onto the same conn it is draining a flood from; `shellchan` (which waits on the ack) keeps it. Exactly-once is unaffected (dedup at the applied-set).
475:**pump worker**: a cadenced consumer module behind the pump's **worker seam** — one each for registry, notif, sync, update. A worker declares its cadence, optionally consumes its wake marker (`poll_wake` — the exactly-once filesystem take stays at the worker edge, the scheduling kernel stays pure), runs optional node-local **pre-round** work once per due round (e.g. the registry worker's eviction sweep → rotation fire → re-advertise ordering), and a per-peer **peer step** over the pump-provided connection. Workers receive per-round shared state through the pump-owned **round context** — the single-read invariant: push targets and the sync gate see the *same* roster load. A failed peer step aborts the remaining due workers for that peer and drops its connection (redial next tick). **LLM-bearing work is never a pump worker** (KNOWN-HAZARDS 7.4 — per-agent Psyche/pulse/echo runs off the shared scheduler, on its own thread).
590:A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt endpoint description set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).


CONTEXT.md:343 already documents the viewer model + eviction (tagged at :342). I'll extend it with the skip-to-live semantic and add the doc tag there — the honest home for this observable behavior.


Edit

each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`).

each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). An evicted viewer **skips to live** instead of dying silently: the broker signals the eviction (a marker distinct from session-exit EOF) and the viewer re-subscribes from the current ring floor — rate-limited so a hopelessly-behind `--view` under a sustained output flood sees intermittent live bursts (tail -f reconnect), never a frozen viewport or an evict→resubscribe CPU spin. Viewer-only, so it never touches the authoritative resume cursor (`REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT`).


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

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


342	<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->
343	**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)

<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->
**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)


<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->

<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] -->


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

The user-facing remote-terminal product surface: `spt rc [subnet:]<id>[@node]` attaches across the subnet to an spt-hosted session's broker surface — scrollback replay + live bytes down, keystrokes up. Address-gated like messaging, **plus a consent gate** (driving a session ≠ watching it). **Controller/viewer model:** one interactive **controller** at a time — its viewport sets the PTY size (resize fires on attach + controller window changes; ConPTY repaint cost is why resize stays controller-exclusive); any number of **`--view`** attachers (read-only, never resize, letterbox client-side). `spt rc kick <target>` displaces the incumbent controller (loud notice to them); `--take` = kick + attach in one motion. A web client (xterm.js + a local bridge speaking the same stream substrate) is the GUI's terminal pane and the future Android-gateway shape — the bridge is just another `rc`-class client.

_Avoid_: "screen share"; resize-per-viewer; silent controller displacement.

-<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] -->

+<!-- [doc->REQ-RCVIEW-1] [doc->REQ-KICK-1] [doc->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT] -->

**BUILT (M12 W2.5).** The controller/viewer model is implemented end-to-end. Attach intent is **three-valued** (`AttachIntent = Viewer | Control | Take`, wire-default `Control`): `Control` to a FREE endpoint becomes controller; `Control` to a CONTROLLED endpoint is **refused with guidance** (`--view` to watch, `--take` to control) — never auto-viewer, never silent-displace; `Take` (`spt rc --take` / picker "Kick") kicks the incumbent with a **loud `Displaced{by}` notice** and full detach (not demote). The broker's per-session `OutputLog` is the fan-out hub: ONE authoritative blocking **controller** (advances the brain-resume cursor `delivered_through`) plus ANY NUMBER of read-only **viewers**, each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive** (the broker rejects a viewer's resize). The **broker is the single writer** of the perch's `driven_by` (controller node) + `viewer_count`, resolving the displaced-controller clear-race. **Controller identity is keyed on the operator node (`by`)**: a same-`by` re-subscribe (a successor re-taking the slot after a brain restart) silently re-takes — `Displaced` fires ONLY on a genuine cross-operator `Take` (the gate-#7 self-kick guard; a brain update never kicks attached operators). **Dormancy keys on the controller only** (viewer attach/detach is wake-neutral; a viewer may watch a dormant endpoint as-is). **v1: viewing is gated identically to driving** (a viewer runs the same `access_check(Unsolicited)`; the lighter distinct watch-gate is the future seam). The picker is status-conditional: a CONTROLLED endpoint offers **View + Kick** only (no plain Attach), pinned `controlled by <node> (+N viewing)`; all of View/Attach/Kick ride the SAME rc dispatch (intent is a parameter — single-bringup-path). (rc viewer letterboxing is a one-line size indicator in v1; true clip/pad needs a client grid model — deferred.)


each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). Resize is **controller-exclusive**

each an isolated bounded queue + writer thread evicted on overflow (a wedged viewer never stalls the drain, controller, or child — `REQ-HAZARD-VIEWER-ISOLATION`). An evicted viewer **skips to live** instead of dying silently: the broker signals the eviction (a marker distinct from session-exit EOF) and the viewer re-subscribes from the current ring floor — rate-limited so a hopelessly-behind `--view` under a sustained output flood sees intermittent live bursts (tail -f reconnect), never a frozen viewport or an evict→resubscribe CPU spin. Viewer-only, so it never touches the authoritative resume cursor (`REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT`). Resize is **controller-exclusive**