/// A message from the stdin reader thread to the pump.
enum StdinMsg {
    /// Raw bytes to forward to the session as `AttachRecord::Input`.
    Bytes(Vec<u8>),
    /// The operator hit the detach keybind — end the viewport, leave the session.
    Detach,
}

/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET
/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for
/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel
/// events to us instead of scrolling its own buffer — so to keep scroll working we
/// must FORWARD wheel events to the harness, but ONLY when the harness has asked
/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these
/// (scanning output); the Windows stdin reader READS them to decide whether to
/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on
/// all platforms (the scan is cheap + keeps the model correct); only the Windows
/// reader acts on it (Unix terminals scroll natively, no capture).
#[derive(Default)]
struct MouseMode {
    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).
    enabled: std::sync::atomic::AtomicBool,
    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a
    /// scroll only when this is on (the modern encoding the harness will parse).
    sgr: std::sync::atomic::AtomicBool,
}

/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset
/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +
/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT
/// across two output chunks (the harness's mouse-enable can land across a decoder
/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each
/// `n` in {1000,1002,1003} sets `enabled = (final=='h')`, for 1006 sets `sgr`.
#[derive(Default)]
struct MouseModeScanner {
    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.
    carry: Vec<u8>,
}

/// The longest incomplete private-mode prefix we keep across a chunk boundary. A
/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about
/// (runaway) — dropped rather than buffered unboundedly.
const MOUSE_DECSET_CARRY_MAX: usize = 32;

impl MouseModeScanner {
    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to
    /// `mode`, carrying a trailing partial sequence to the next call.
    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {
        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.
        if self.carry.is_empty() && !chunk.contains(&0x1b) {
            return;
        }
        let mut buf = std::mem::take(&mut self.carry);
        buf.extend_from_slice(chunk);
        let mut i = 0;
        while i < buf.len() {
            if buf[i] != 0x1b {
                i += 1;
                continue;
            }
            match parse_decset_private(&buf[i..]) {
                DecsetParse::Complete { consumed, params, set } => {
                    for p in params {
                        apply_mouse_mode(mode, p, set);
                    }
                    i += consumed;
                }
                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the
                // next chunk; a too-long dangling run is treated as not-a-DECSET.
                DecsetParse::Incomplete => {
                    let tail = &buf[i..];
                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {
                        self.carry = tail.to_vec();
                    }
                    return;
                }
                // Not a private-mode sequence — skip this ESC, keep scanning.
                DecsetParse::NotDecset => i += 1,
            }
        }
    }
}

/// Result of trying to parse a private-mode set/reset at the start of `s` (which
/// begins with `ESC`).
enum DecsetParse {
    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric
    /// params, and `set` (`true` for `h`, `false` for `l`).
    Complete { consumed: usize, params: Vec<u16>, set: bool },
    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.
    Incomplete,
    /// `s` does not start a private-mode sequence (`ESC` of something else).
    NotDecset,
}

/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the
/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.
fn parse_decset_private(s: &[u8]) -> DecsetParse {
    // Need at least `ESC [ ?`.
    if s.len() < 3 {
        // Could be the very start of one split across the boundary.
        if s.first() == Some(&0x1b) && s.get(1).is_none_or(|&b| b == b'[')
            && s.get(2).is_none_or(|&b| b == b'?')
        {
            return DecsetParse::Incomplete;
        }
        return DecsetParse::NotDecset;
    }
    if s[1] != b'[' || s[2] != b'?' {
        return DecsetParse::NotDecset;
    }
    let mut params: Vec<u16> = Vec::new();
    let mut cur: Option<u32> = None;
    let mut j = 3;
    while j < s.len() {
        match s[j] {
            b'0'..=b'9' => {
                let d = (s[j] - b'0') as u32;
                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));
            }
            b';' => {
                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);
                cur = None;
            }
            b'h' | b'l' => {
                if let Some(v) = cur {
                    params.push(v.min(u16::MAX as u32) as u16);
                }
                return DecsetParse::Complete {
                    consumed: j + 1,
                    params,
                    set: s[j] == b'h',
                };
            }
            // Any other byte ⇒ this isn't a clean private-mode set/reset.
            _ => return DecsetParse::NotDecset,
        }
        j += 1;
    }
    // Ran out before a final `h`/`l` ⇒ split across the chunk boundary.
    DecsetParse::Incomplete
}

/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].
fn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {
    use std::sync::atomic::Ordering::Release;
    match param {
        1000 | 1002 | 1003 => mode.enabled.store(set, Release),
        1006 => mode.sgr.store(set, Release),
        _ => {}
    }
}

+/// The harness's MOUSE-REPORTING mode, tracked from its OUTPUT stream's DECSET

+/// sequences (REQ-RC-MOUSE-FORWARD). On Windows `spt rc` captures the mouse (for

+/// right-click paste, REQ-RC-WIN-PASTE), which means WT forwards scroll-wheel

+/// events to us instead of scrolling its own buffer — so to keep scroll working we

+/// must FORWARD wheel events to the harness, but ONLY when the harness has asked

+/// for mouse reporting (else the bytes are garbage). The pump thread WRITES these

+/// (scanning output); the Windows stdin reader READS them to decide whether to

+/// forward a scroll. Two atomics so the cross-thread read is lock-free. Tracked on

+/// all platforms (the scan is cheap + keeps the model correct); only the Windows

+/// reader acts on it (Unix terminals scroll natively, no capture).

+    /// The harness requested mouse reporting (DECSET 1000 / 1002 / 1003).

+    enabled: std::sync::atomic::AtomicBool,

+    /// The harness requested SGR extended coordinates (DECSET 1006). We forward a

+    /// scroll only when this is on (the modern encoding the harness will parse).

+    sgr: std::sync::atomic::AtomicBool,

+/// Scans a harness OUTPUT byte stream for the mouse-reporting DECSET set/reset

+/// sequences and updates a [`MouseMode`] (REQ-RC-MOUSE-FORWARD). Pump-local +

+/// single-threaded, so it owns a small `carry` buffer to survive a sequence SPLIT

+/// across two output chunks (the harness's mouse-enable can land across a decoder

+/// boundary). Recognizes `ESC[?<n>[;<n>...]h|l` private-mode sequences; for each

+    /// A partial `ESC[?…` left dangling at a chunk boundary, prepended next feed.

+/// The longest incomplete private-mode prefix we keep across a chunk boundary. A

+/// longer dangling `ESC[?…` with no final `h`/`l` is not a mode-set we care about

+/// (runaway) — dropped rather than buffered unboundedly.

+const MOUSE_DECSET_CARRY_MAX: usize = 32;

+    /// Feed one output chunk; apply any complete DECSET mouse-mode toggles to

+    /// `mode`, carrying a trailing partial sequence to the next call.

+    fn feed(&mut self, mode: &MouseMode, chunk: &[u8]) {

+        // Fast path: no ESC anywhere and nothing carried ⇒ nothing to do.

+        if self.carry.is_empty() && !chunk.contains(&0x1b) {

+        let mut buf = std::mem::take(&mut self.carry);

+            match parse_decset_private(&buf[i..]) {

+                DecsetParse::Complete { consumed, params, set } => {

+                        apply_mouse_mode(mode, p, set);

+                // An incomplete `ESC[?…` at the tail: carry it (bounded) for the

+                // next chunk; a too-long dangling run is treated as not-a-DECSET.

+                DecsetParse::Incomplete => {

+                    let tail = &buf[i..];

+                    if tail.len() <= MOUSE_DECSET_CARRY_MAX {

+                        self.carry = tail.to_vec();

+                // Not a private-mode sequence — skip this ESC, keep scanning.

+                DecsetParse::NotDecset => i += 1,

+/// Result of trying to parse a private-mode set/reset at the start of `s` (which

+    /// A complete `ESC[?<params>h|l`: the byte length consumed, the numeric

+    /// params, and `set` (`true` for `h`, `false` for `l`).

+    Complete { consumed: usize, params: Vec<u16>, set: bool },

+    /// `ESC[?…` with no final `h`/`l` yet (chunk boundary) — carry + retry.

+    /// `s` does not start a private-mode sequence (`ESC` of something else).

+/// Parse a single `ESC[?<digits>[;<digits>…]h|l` private-mode sequence at the

+/// start of `s`. Pure, so the split-across-chunk behavior unit-tests directly.

+fn parse_decset_private(s: &[u8]) -> DecsetParse {

+        // Could be the very start of one split across the boundary.

+            return DecsetParse::Incomplete;

+    let mut params: Vec<u16> = Vec::new();

+                cur = Some(cur.unwrap_or(0).saturating_mul(10).saturating_add(d));

+                params.push(cur.unwrap_or(0).min(u16::MAX as u32) as u16);

+                    params.push(v.min(u16::MAX as u32) as u16);

+                return DecsetParse::Complete {

+            // Any other byte ⇒ this isn't a clean private-mode set/reset.

+            _ => return DecsetParse::NotDecset,

+    // Ran out before a final `h`/`l` ⇒ split across the chunk boundary.

+/// Apply one DECSET private-mode toggle to the tracked [`MouseMode`].

+fn apply_mouse_mode(mode: &MouseMode, param: u16, set: bool) {

+    use std::sync::atomic::Ordering::Release;

+        1000 | 1002 | 1003 => mode.enabled.store(set, Release),

+        1006 => mode.sgr.store(set, Release),

/// Mint operator op ids from a random 64-bit base so two `spt rc` sessions never

/// collide in the broker's net-op journal (a journaled `send_attach_input` op is

/// exactly-once at the PTY — a reused id from a prior session would be deduped,


Add `scroll_dir` + `scroll_sgr` (cfg windows) after `mouse_is_paste`.


Edit

// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn mouse_is_paste(me: &crossterm::event::MouseEvent) -> bool {
    use crossterm::event::{MouseButton, MouseEventKind};
    matches!(me.kind, MouseEventKind::Down(MouseButton::Right))
}

// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn mouse_is_paste(me: &crossterm::event::MouseEvent) -> bool {
    use crossterm::event::{MouseButton, MouseEventKind};
    matches!(me.kind, MouseEventKind::Down(MouseButton::Right))
}

/// The scroll-wheel direction of a mouse event, or `None` if it is not a wheel
/// scroll (REQ-RC-MOUSE-FORWARD). `Some(true)` = scroll UP, `Some(false)` = DOWN.
/// Pure so the classify unit-tests without a console.
// [impl->REQ-RC-MOUSE-FORWARD]
#[cfg(windows)]
fn scroll_dir(me: &crossterm::event::MouseEvent) -> Option<bool> {
    use crossterm::event::MouseEventKind;
    match me.kind {
        MouseEventKind::ScrollUp => Some(true),
        MouseEventKind::ScrollDown => Some(false),
        _ => None,
    }
}

/// Encode a scroll-wheel event as an xterm SGR mouse report
/// (REQ-RC-MOUSE-FORWARD): `ESC[<{btn};{col};{row}M`, button 64 = wheel-up /
/// 65 = wheel-down, coordinates 1-based (crossterm is 0-based → `+1`). Press-only
/// (wheel reports have no release). Forwarded to the harness so it scrolls, since
/// our mouse capture (for right-click paste) otherwise swallows the wheel.
// [impl->REQ-RC-MOUSE-FORWARD]
#[cfg(windows)]
fn scroll_sgr(up: bool, col: u16, row: u16) -> Vec<u8> {
    let btn = if up { 64 } else { 65 };
    format!(
        "\x1b[<{};{};{}M",
        btn,
        col.saturating_add(1),
        row.saturating_add(1)
    )
    .into_bytes()
}


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

// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn mouse_is_paste(me: &crossterm::event::MouseEvent) -> bool {
    use crossterm::event::{MouseButton, MouseEventKind};
    matches!(me.kind, MouseEventKind::Down(MouseButton::Right))
}

/// The scroll-wheel direction of a mouse event, or `None` if it is not a wheel
/// scroll (REQ-RC-MOUSE-FORWARD). `Some(true)` = scroll UP, `Some(false)` = DOWN.
/// Pure so the classify unit-tests without a console.
// [impl->REQ-RC-MOUSE-FORWARD]
#[cfg(windows)]
fn scroll_dir(me: &crossterm::event::MouseEvent) -> Option<bool> {
    use crossterm::event::MouseEventKind;
    match me.kind {
        MouseEventKind::ScrollUp => Some(true),
        MouseEventKind::ScrollDown => Some(false),
        _ => None,
    }
}

/// Encode a scroll-wheel event as an xterm SGR mouse report
/// (REQ-RC-MOUSE-FORWARD): `ESC[<{btn};{col};{row}M`, button 64 = wheel-up /
/// 65 = wheel-down, coordinates 1-based (crossterm is 0-based → `+1`). Press-only
/// (wheel reports have no release). Forwarded to the harness so it scrolls, since
/// our mouse capture (for right-click paste) otherwise swallows the wheel.
// [impl->REQ-RC-MOUSE-FORWARD]
#[cfg(windows)]
fn scroll_sgr(up: bool, col: u16, row: u16) -> Vec<u8> {
    let btn = if up { 64 } else { 65 };
    format!(
        "\x1b[<{};{};{}M",
        btn,
        col.saturating_add(1),
        row.saturating_add(1)
    )
    .into_bytes()
}

+/// The scroll-wheel direction of a mouse event, or `None` if it is not a wheel

+/// scroll (REQ-RC-MOUSE-FORWARD). `Some(true)` = scroll UP, `Some(false)` = DOWN.

+/// Pure so the classify unit-tests without a console.

+fn scroll_dir(me: &crossterm::event::MouseEvent) -> Option<bool> {

+    use crossterm::event::MouseEventKind;

+        MouseEventKind::ScrollUp => Some(true),

+        MouseEventKind::ScrollDown => Some(false),

+/// Encode a scroll-wheel event as an xterm SGR mouse report

+/// (REQ-RC-MOUSE-FORWARD): `ESC[<{btn};{col};{row}M`, button 64 = wheel-up /

+/// 65 = wheel-down, coordinates 1-based (crossterm is 0-based → `+1`). Press-only

+/// (wheel reports have no release). Forwarded to the harness so it scrolls, since

+/// our mouse capture (for right-click paste) otherwise swallows the wheel.

+fn scroll_sgr(up: bool, col: u16, row: u16) -> Vec<u8> {

+    let btn = if up { 64 } else { 65 };

/// Build the bracketed-paste bytes for a paste gesture from a clipboard reader.

/// `read` is injected so the decision unit-tests without a real clipboard: a

/// non-empty read ⇒ the wrapped paste; an empty read OR a read failure (`None`)


P1 ctrl+V wedge — diagnose then fix

HITL re-open: ctrl+V (~10+ chars) wedges broker; right-click fine. Diagnostic build @b39e8a0 (branch v0.13.0-p1-ctrlv-diag) surfaced to doyle with env matrix (SPT_RC_CTRLV isolation + SPT_RC_DEBUG_PASTE + SPT_BROKER_DEBUG_INPUT/OUTPUT). Awaiting operator stderr capture (via doyle) to pin input-dispatch vs controller-output-write block. Then design+build the real fix (doyle suspects symmetric-P0-for-output async controller writer) on a gated branch off f7cf380.

Mint REQ-HAZARD-INPUT-ACK-BACKPRESSURE + amend REQ-RC-WIN-PASTE

Registry-first: new hazard REQ [impl,unit,int] for the input-ack IPC deadlock. Amend REQ-RC-WIN-PASTE to right-click-only scope (ctrl+V = WT-native).

Primary fix: conditional ack

InputReq.ack serde-default=true; brain send_effect_no_ack for operator path (serve_attach); dispatch_input only send_frame(applied) when req.ack; shellchan keeps ack=true.

Secondary: remove dead ctrl+V interception

Remove is_ctrl_v arm + fn + unit; keep right-click bracketed paste + EnableMouseCapture. ctrl+V rides WT-native paste.

Docs: KNOWN-HAZARDS deadlock + amend 7.18

New KNOWN-HAZARDS entry for input-ack backpressure deadlock (+N-1 broker-resident caveat). Amend 7.18: ctrl+V WT-native, right-click bracketed only.

Tests (spt-test-engineer) + gates + commit + doyle

Delegate repro-first int (N=64 flood deadlock) + units (ack=true emits/false doesn't, exactly-once no-ack dedup). Gates clippy/traceable/docs-drift/real-bin; commit; surface green to doyle.

Scroll-forward: mouse_mode scan + scroll→SGR

REQ-RC-MOUSE-FORWARD: track CC mouse-mode via DECSET scan on output stream (Arc<AtomicBool> enabled+sgr); forward ScrollUp/Down as SGR reports when on; keep right-click paste; drop moved/clicks. Units: exact-byte SGR + DECSET scan.


Now wire it. Update `spawn_stdin_reader_events` signature + mouse arm (scroll handling).


571	    DetachParse {
572	        forward,
573	        detach: false,
574	    }
575	}
576
577	/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY
578	/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the
579	/// special keys the legacy console delivers as events — arrows / Home / End /
580	/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected
581	/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the
582	/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection
583	/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.
584	fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
585	    #[cfg(windows)]
586	    {
587	        use std::io::IsTerminal;
588	        if std::io::stdin().is_terminal() {
589	            spawn_stdin_reader_events(tx);
590	            return;
591	        }
592	    }
593	    spawn_stdin_reader_bytes(tx);
594	}
595
596	/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback
597	/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach
598	/// SM. No key translation — a non-tty stream carries bytes, not console key events.
599	fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {
600	    std::thread::spawn(move || {
601	        let mut stdin = std::io::stdin().lock();
602	        let mut buf = [0u8; 1024];
603	        let mut armed = false; // saw the detach prefix, awaiting the next byte
604	        loop {
605	            let n = match stdin.read(&mut buf) {
606	                Ok(0) => break, // stdin closed
607	                Ok(n) => n,
608	                Err(_) => break,
609	            };
610	            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
611	                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
612	                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
613	            }
614	            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
615	            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {
616	                break;
617	            }
618	            if parsed.detach {
619	                let _ = tx.send(StdinMsg::Detach);
620	                return;
621	            }
622	        }
623	    });
624	}
625

    DetachParse {
        forward,
        detach: false,
    }
}

/// Spawn the stdin reader. On a Windows INTERACTIVE console, read crossterm KEY
/// EVENTS and translate them to standard xterm VT ([`translate_key_event`]) so the
/// special keys the legacy console delivers as events — arrows / Home / End /
/// F-keys / … — actually reach the harness (REQ-RC-KEY-VT-TRANSLATE). A redirected
/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the
/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection
/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.
fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx);
            return;
        }
    }
    spawn_stdin_reader_bytes(tx);
}

/// The raw-byte stdin reader: Unix always (already VT), Windows non-tty fallback
/// (injected bytes — tests). Forwards bytes through the [`parse_stdin_chunk`] detach
/// SM. No key translation — a non-tty stream carries bytes, not console key events.
fn spawn_stdin_reader_bytes(tx: mpsc::Sender<StdinMsg>) {
    std::thread::spawn(move || {
        let mut stdin = std::io::stdin().lock();
        let mut buf = [0u8; 1024];
        let mut armed = false; // saw the detach prefix, awaiting the next byte
        loop {
            let n = match stdin.read(&mut buf) {
                Ok(0) => break, // stdin closed
                Ok(n) => n,
                Err(_) => break,
            };
            if std::env::var_os("SPT_RC_DEBUG_KEYS").is_some() {
                let hex: Vec<String> = buf[..n].iter().map(|b| format!("{b:02x}")).collect();
                eprintln!("RC_KEYS n={n} bytes=[{}]", hex.join(" "));
            }
            let parsed = parse_stdin_chunk(&mut armed, &buf[..n]);
            if !parsed.forward.is_empty() && tx.send(StdinMsg::Bytes(parsed.forward)).is_err() {
                break;
            }
            if parsed.detach {
                let _ = tx.send(StdinMsg::Detach);
                return;
            }
        }
    });
}


fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx);
            return;
        }
    }
    spawn_stdin_reader_bytes(tx);
}

fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx, mouse_mode);
            return;
        }
    }
    // Non-tty / Unix: the byte path doesn't consult mouse-mode (no capture).
    let _ = &mouse_mode;
    spawn_stdin_reader_bytes(tx);
}


fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx);
            return;
        }
    }
    spawn_stdin_reader_bytes(tx);
}

fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {
    #[cfg(windows)]
    {
        use std::io::IsTerminal;
        if std::io::stdin().is_terminal() {
            spawn_stdin_reader_events(tx, mouse_mode);
            return;
        }
    }
    // Non-tty / Unix: the byte path doesn't consult mouse-mode (no capture).
    let _ = &mouse_mode;
    spawn_stdin_reader_bytes(tx);
}

/// (piped / non-tty) Windows stdin — tests, non-interactive — and Unix both keep the

/// raw byte-read path: Unix already delivers proper VT, and the e2e byte-injection

/// relies on it. Detach is the `ctrl-b d` prefix on BOTH paths.

-fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>) {

+fn spawn_stdin_reader(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {

-            spawn_stdin_reader_events(tx);

+            spawn_stdin_reader_events(tx, mouse_mode);

+    // Non-tty / Unix: the byte path doesn't consult mouse-mode (no capture).


Now update `spawn_stdin_reader_events` signature + the mouse arm to forward scroll.


626	}
627
628	/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
629	/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
630	/// Release dropped to avoid double-input); a bracketed Paste forwards its content
631	/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
632	#[cfg(windows)]
633	fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
634	    use crossterm::event::{self, Event, KeyEventKind};
635	    std::thread::spawn(move || {
636	        let mut armed = false;
637	        while let Ok(ev) = event::read() {
638	            match ev {
639	                // Right-click paste gesture (REQ-RC-WIN-PASTE): read the LOCAL
640	                // clipboard and inject a BRACKETED paste (ESC[200~ … ESC[201~) — the
641	                // ONLY client-originated paste we can drive. (ctrl+V is NOT
642	                // intercepted: Windows Terminal consumes ctrl+V as its own paste
643	                // accelerator — it delivers only a Key RELEASE, never a Press, and
644	                // injects the clipboard as a char-by-char key flood — so an
645	                // interception arm could never fire. ctrl+V now rides WT's native
646	                // paste as keystrokes; the broker no longer wedges on the flood
647	                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Bracketed fidelity = right-click.)
648	                // Every OTHER mouse event is dropped — CC has no mouse features, so
649	                // capturing mouse deprives it of nothing.
650	                Event::Mouse(me) => {
651	                    if mouse_is_paste(&me) {
652	                        if let Some(bytes) = clipboard_paste(read_clipboard) {
653	                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
654	                                break;
655	                            }
656	                        }
657	                    }
658	                }
659	                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
660	                    match key_event_step(&mut armed, ke) {
661	                        KeyAction::Detach => {
662	                            let _ = tx.send(StdinMsg::Detach);
663	                            return;
664	                        }
665	                        KeyAction::Forward(bytes) => {
666	                            if !bytes.is_empty() && tx.send(StdinMsg::Bytes(bytes)).is_err() {
667	                                break;
668	                            }
669	                        }
670	                        KeyAction::Swallow => {}
671	                    }
672	                }
673	                Event::Paste(s) => {
674	                    if tx.send(StdinMsg::Bytes(s.into_bytes())).is_err() {
675	                        break;
676	                    }
677	                }
678	                _ => {}
679	            }
680	        }
681	    });
682	}
683
684	/// Attach a local terminal to the broker-held session for `endpoint_id` under
685	/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is

}

/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
    use crossterm::event::{self, Event, KeyEventKind};
    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // Right-click paste gesture (REQ-RC-WIN-PASTE): read the LOCAL
                // clipboard and inject a BRACKETED paste (ESC[200~ … ESC[201~) — the
                // ONLY client-originated paste we can drive. (ctrl+V is NOT
                // intercepted: Windows Terminal consumes ctrl+V as its own paste
                // accelerator — it delivers only a Key RELEASE, never a Press, and
                // injects the clipboard as a char-by-char key flood — so an
                // interception arm could never fire. ctrl+V now rides WT's native
                // paste as keystrokes; the broker no longer wedges on the flood
                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Bracketed fidelity = right-click.)
                // Every OTHER mouse event is dropped — CC has no mouse features, so
                // capturing mouse deprives it of nothing.
                Event::Mouse(me) => {
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                }
                Event::Key(ke) if ke.kind == KeyEventKind::Press => {
                    match key_event_step(&mut armed, ke) {
                        KeyAction::Detach => {
                            let _ = tx.send(StdinMsg::Detach);
                            return;
                        }
                        KeyAction::Forward(bytes) => {
                            if !bytes.is_empty() && tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                        KeyAction::Swallow => {}
                    }
                }
                Event::Paste(s) => {
                    if tx.send(StdinMsg::Bytes(s.into_bytes())).is_err() {
                        break;
                    }
                }
                _ => {}
            }
        }
    });
}

/// Attach a local terminal to the broker-held session for `endpoint_id` under
/// `intent` (REQ-RCVIEW-1 / REQ-KICK-1): `Control` drives a free endpoint (and is


/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
    use crossterm::event::{self, Event, KeyEventKind};
    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // Right-click paste gesture (REQ-RC-WIN-PASTE): read the LOCAL
                // clipboard and inject a BRACKETED paste (ESC[200~ … ESC[201~) — the
                // ONLY client-originated paste we can drive. (ctrl+V is NOT
                // intercepted: Windows Terminal consumes ctrl+V as its own paste
                // accelerator — it delivers only a Key RELEASE, never a Press, and
                // injects the clipboard as a char-by-char key flood — so an
                // interception arm could never fire. ctrl+V now rides WT's native
                // paste as keystrokes; the broker no longer wedges on the flood
                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Bracketed fidelity = right-click.)
                // Every OTHER mouse event is dropped — CC has no mouse features, so
                // capturing mouse deprives it of nothing.
                Event::Mouse(me) => {
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                }

/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
/// `mouse_mode` (written by the pump from the harness's output DECSET) gates
/// scroll-wheel forwarding (REQ-RC-MOUSE-FORWARD).
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {
    use crossterm::event::{self, Event, KeyEventKind};
    use std::sync::atomic::Ordering::Acquire;
    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // Mouse, under our capture (enabled for right-click paste,
                // REQ-RC-WIN-PASTE). Two gestures we act on; everything else dropped.
                Event::Mouse(me) => {
                    // Right-click DOWN → bracketed clipboard paste (the ONLY
                    // client-originated paste we can drive; ctrl+V is consumed by WT
                    // and rides its native paste as keystrokes).
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    } else if let Some(up) = scroll_dir(&me) {
                        // Scroll wheel → forward to the harness as an SGR mouse
                        // report so it scrolls (our capture stole WT's native
                        // scroll). ONLY when the harness asked for mouse reporting +
                        // SGR (else the bytes are garbage it would render) — drop
                        // otherwise (REQ-RC-MOUSE-FORWARD).
                        if mouse_mode.enabled.load(Acquire) && mouse_mode.sgr.load(Acquire) {
                            let bytes = scroll_sgr(up, me.column, me.row);
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                    // Moved / drag / left / middle: dropped (CC has no use for them;
                    // forwarding risks garbage — scroll + right-click are the need).
                }


/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {
    use crossterm::event::{self, Event, KeyEventKind};
    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // Right-click paste gesture (REQ-RC-WIN-PASTE): read the LOCAL
                // clipboard and inject a BRACKETED paste (ESC[200~ … ESC[201~) — the
                // ONLY client-originated paste we can drive. (ctrl+V is NOT
                // intercepted: Windows Terminal consumes ctrl+V as its own paste
                // accelerator — it delivers only a Key RELEASE, never a Press, and
                // injects the clipboard as a char-by-char key flood — so an
                // interception arm could never fire. ctrl+V now rides WT's native
                // paste as keystrokes; the broker no longer wedges on the flood
                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Bracketed fidelity = right-click.)
                // Every OTHER mouse event is dropped — CC has no mouse features, so
                // capturing mouse deprives it of nothing.
                Event::Mouse(me) => {
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                }

/// The Windows interactive-console reader: crossterm key EVENTS → xterm VT, with the
/// event-sourced `ctrl-b d` detach SM ([`key_event_step`]). Press-only (Repeat /
/// Release dropped to avoid double-input); a bracketed Paste forwards its content
/// bytes. Resize is handled by the pump's own `size()` poll, so it is ignored here.
/// `mouse_mode` (written by the pump from the harness's output DECSET) gates
/// scroll-wheel forwarding (REQ-RC-MOUSE-FORWARD).
#[cfg(windows)]
fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {
    use crossterm::event::{self, Event, KeyEventKind};
    use std::sync::atomic::Ordering::Acquire;
    std::thread::spawn(move || {
        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // Mouse, under our capture (enabled for right-click paste,
                // REQ-RC-WIN-PASTE). Two gestures we act on; everything else dropped.
                Event::Mouse(me) => {
                    // Right-click DOWN → bracketed clipboard paste (the ONLY
                    // client-originated paste we can drive; ctrl+V is consumed by WT
                    // and rides its native paste as keystrokes).
                    if mouse_is_paste(&me) {
                        if let Some(bytes) = clipboard_paste(read_clipboard) {
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    } else if let Some(up) = scroll_dir(&me) {
                        // Scroll wheel → forward to the harness as an SGR mouse
                        // report so it scrolls (our capture stole WT's native
                        // scroll). ONLY when the harness asked for mouse reporting +
                        // SGR (else the bytes are garbage it would render) — drop
                        // otherwise (REQ-RC-MOUSE-FORWARD).
                        if mouse_mode.enabled.load(Acquire) && mouse_mode.sgr.load(Acquire) {
                            let bytes = scroll_sgr(up, me.column, me.row);
                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {
                                break;
                            }
                        }
                    }
                    // Moved / drag / left / middle: dropped (CC has no use for them;
                    // forwarding risks garbage — scroll + right-click are the need).
                }

+/// `mouse_mode` (written by the pump from the harness's output DECSET) gates

+/// scroll-wheel forwarding (REQ-RC-MOUSE-FORWARD).

-fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>) {

+fn spawn_stdin_reader_events(tx: mpsc::Sender<StdinMsg>, mouse_mode: std::sync::Arc<MouseMode>) {

+    use std::sync::atomic::Ordering::Acquire;

-                // Right-click paste gesture (REQ-RC-WIN-PASTE): read the LOCAL

-                // clipboard and inject a BRACKETED paste (ESC[200~ … ESC[201~) — the

-                // ONLY client-originated paste we can drive. (ctrl+V is NOT

-                // intercepted: Windows Terminal consumes ctrl+V as its own paste

-                // accelerator — it delivers only a Key RELEASE, never a Press, and

-                // injects the clipboard as a char-by-char key flood — so an

-                // interception arm could never fire. ctrl+V now rides WT's native

-                // paste as keystrokes; the broker no longer wedges on the flood

-                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). Bracketed fidelity = right-click.)

-                // Every OTHER mouse event is dropped — CC has no mouse features, so

-                // capturing mouse deprives it of nothing.

+                // Mouse, under our capture (enabled for right-click paste,

+                // REQ-RC-WIN-PASTE). Two gestures we act on; everything else dropped.

+                    // Right-click DOWN → bracketed clipboard paste (the ONLY

+                    // client-originated paste we can drive; ctrl+V is consumed by WT

+                    // and rides its native paste as keystrokes).

+                    } else if let Some(up) = scroll_dir(&me) {

+                        // Scroll wheel → forward to the harness as an SGR mouse

+                        // report so it scrolls (our capture stole WT's native

+                        // scroll). ONLY when the harness asked for mouse reporting +

+                        // otherwise (REQ-RC-MOUSE-FORWARD).

+                        if mouse_mode.enabled.load(Acquire) && mouse_mode.sgr.load(Acquire) {

+                            let bytes = scroll_sgr(up, me.column, me.row);

+                            if tx.send(StdinMsg::Bytes(bytes)).is_err() {

+                    // Moved / drag / left / middle: dropped (CC has no use for them;

+                    // forwarding risks garbage — scroll + right-click are the need).

Event::Key(ke) if ke.kind == KeyEventKind::Press => {


Now wire `mouse_mode` through `run_attach` + `pump`. Update run_attach.


    let _raw = RawGuard::enable();
    let rx = if view {
        None
    } else {
        let (tx, rx) = mpsc::channel();
        spawn_stdin_reader(tx);
        Some(rx)
    };

    let _raw = RawGuard::enable();
    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the
    // pump and read by the Windows stdin reader to gate scroll forwarding
    // (REQ-RC-MOUSE-FORWARD).
    let mouse_mode = std::sync::Arc::new(MouseMode::default());
    let rx = if view {
        None
    } else {
        let (tx, rx) = mpsc::channel();
        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));
        Some(rx)
    };


    let _raw = RawGuard::enable();
    let rx = if view {
        None
    } else {
        let (tx, rx) = mpsc::channel();
        spawn_stdin_reader(tx);
        Some(rx)
    };

    let _raw = RawGuard::enable();
    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the
    // pump and read by the Windows stdin reader to gate scroll forwarding
    // (REQ-RC-MOUSE-FORWARD).
    let mouse_mode = std::sync::Arc::new(MouseMode::default());
    let rx = if view {
        None
    } else {
        let (tx, rx) = mpsc::channel();
        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));
        Some(rx)
    };

.map_err(|e| format!("stream subscribe: {e}"))?;

+    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the

+    // pump and read by the Windows stdin reader to gate scroll forwarding

+    let mouse_mode = std::sync::Arc::new(MouseMode::default());

+        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));


    let outcome = pump(
        &mut brain,
        stream_id,
        &ops,
        rx.as_ref(),
        &mut decoder,
        &mut stdout,
        !view, // controller drives resize
    );

    let outcome = pump(
        &mut brain,
        stream_id,
        &ops,
        rx.as_ref(),
        &mut decoder,
        &mut stdout,
        !view, // controller drives resize
        &mouse_mode,
    );


    let outcome = pump(
        &mut brain,
        stream_id,
        &ops,
        rx.as_ref(),
        &mut decoder,
        &mut stdout,
        !view, // controller drives resize
    );

    let outcome = pump(
        &mut brain,
        stream_id,
        &ops,
        rx.as_ref(),
        &mut decoder,
        &mut stdout,
        !view, // controller drives resize
        &mouse_mode,
    );

// Out of raw mode (guard drops after) before the parting line.


Now the `pump` signature + the output-scan in its write path.


748
749	    let session_id = resolve_session(&mut brain, endpoint_id)
750	        .map_err(|e| format!("list sessions: {e}"))?
751	        .ok_or_else(|| format!("no live session for endpoint '{endpoint_id}'"))?;
752
753	    let ops = OpMinter::new();
754	    // Transport: a broker-minted in-process LOOPBACK connection (M12 W1.5, B1).
755	    // A QUIC self-dial is refused by iroh ("Connecting to ourself is not
756	    // supported"), so local attach cannot ride a self-dialed conn; instead the
757	    // broker wires an in-process loopback conn whose streams surface as the SAME
758	    // net-stream events the cross-node QUIC path produces. From `request_attach`
759	    // down this is byte-identical to a cross-node attach — the one-pump invariant
760	    // lives at the transport leaf inside the broker, never here. The loopback
761	    // peer's origin is this node's own id, so the target's serve gates same-node.
762	    let dialed = brain
763	        .net_dial_loopback()
764	        .map_err(|e| format!("loopback dial: {e}"))?;
765	    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next(), intent)
766	        .map_err(|e| format!("attach request: {e}"))?;
767	    // Subscribe our side of the stream so the target's `Output` records surface
768	    // as `NetStreamData` events (the open + Request do not auto-subscribe).
769	    brain
770	        .net_stream_subscribe(stream_id, 0)
771	        .map_err(|e| format!("stream subscribe: {e}"))?;
772
773	    let _raw = RawGuard::enable();
774	    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the
775	    // pump and read by the Windows stdin reader to gate scroll forwarding
776	    // (REQ-RC-MOUSE-FORWARD).
777	    let mouse_mode = std::sync::Arc::new(MouseMode::default());
778	    let rx = if view {
779	        None
780	    } else {
781	        let (tx, rx) = mpsc::channel();
782	        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));

    let session_id = resolve_session(&mut brain, endpoint_id)
        .map_err(|e| format!("list sessions: {e}"))?
        .ok_or_else(|| format!("no live session for endpoint '{endpoint_id}'"))?;

    let ops = OpMinter::new();
    // Transport: a broker-minted in-process LOOPBACK connection (M12 W1.5, B1).
    // A QUIC self-dial is refused by iroh ("Connecting to ourself is not
    // supported"), so local attach cannot ride a self-dialed conn; instead the
    // broker wires an in-process loopback conn whose streams surface as the SAME
    // net-stream events the cross-node QUIC path produces. From `request_attach`
    // down this is byte-identical to a cross-node attach — the one-pump invariant
    // lives at the transport leaf inside the broker, never here. The loopback
    // peer's origin is this node's own id, so the target's serve gates same-node.
    let dialed = brain
        .net_dial_loopback()
        .map_err(|e| format!("loopback dial: {e}"))?;
    let stream_id = request_attach(&mut brain, dialed.conn_id, session_id, 0, ops.next(), intent)
        .map_err(|e| format!("attach request: {e}"))?;
    // Subscribe our side of the stream so the target's `Output` records surface
    // as `NetStreamData` events (the open + Request do not auto-subscribe).
    brain
        .net_stream_subscribe(stream_id, 0)
        .map_err(|e| format!("stream subscribe: {e}"))?;

    let _raw = RawGuard::enable();
    // The harness's mouse-reporting mode, tracked from its output (DECSET) by the
    // pump and read by the Windows stdin reader to gate scroll forwarding
    // (REQ-RC-MOUSE-FORWARD).
    let mouse_mode = std::sync::Arc::new(MouseMode::default());
    let rx = if view {
        None
    } else {
        let (tx, rx) = mpsc::channel();
        spawn_stdin_reader(tx, std::sync::Arc::clone(&mouse_mode));


P1 ctrl+V wedge — diagnose then fix

Primary fix: conditional ack


800	        rx.as_ref(),
801	        &mut decoder,
802	        &mut stdout,
803	        !view, // controller drives resize
804	        &mouse_mode,
805	    );
806
807	    // Out of raw mode (guard drops after) before the parting line.
808	    drop(_raw);
809	    match outcome {
810	        Ok(PumpEnd::Exited(code)) => {
811	            let _ = writeln!(stdout, "\r\n[session '{endpoint_id}' ended ({code:?})]");
812	            Ok(())
813	        }
814	        Ok(PumpEnd::Detached) => {
815	            let _ = writeln!(stdout, "\r\n[detached — '{endpoint_id}' still running]");
816	            Ok(())
817	        }
818	        Ok(PumpEnd::Displaced(by)) => {
819	            // The loud kick notice (REQ-KICK-1): a different operator took control.
820	            let _ = writeln!(
821	                stdout,
822	                "\r\n[displaced — '{endpoint_id}' was taken over by {by}]"
823	            );
824	            Ok(())
825	        }
826	        Ok(PumpEnd::BrokerGone) => {
827	            // A severed broker stream is recoverable, not a crash (REQ-HAZARD-RC-EOF):
828	            // the broker is the daemon-lifetime anchor and comes back on the next
829	            // `spt api` call. Surface a CLEAR actionable line instead of the raw
830	            // "failed to fill whole buffer" that read as a PTY crash.
831	            let _ = writeln!(
832	                stdout,
833	                "\r\n[daemon connection dropped — the broker may have restarted \
834	                 (e.g. `spt daemon stop`); the session '{endpoint_id}' is unaffected, \
835	                 re-run `spt endpoint run`/attach to reconnect]"
836	            );
837	            Ok(())
838	        }
839	        Ok(PumpEnd::Stalled) => {
840	            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced
841	            // no output at all — a dead/wedged session, not a slow one. A clear
842	            // message, never the historical infinite blank.
843	            let _ = writeln!(
844	                stdout,
845	                "\r\n[no output from '{endpoint_id}' — the session may be dead or \
846	                 wedged. Detached. Try `spt endpoint list` for its state, or \
847	                 `spt endpoint run` to restart it]"
848	            );
849	            Ok(())
850	        }
851	        Err(e) => Err(e),
852	    }
853	}
854

        rx.as_ref(),
        &mut decoder,
        &mut stdout,
        !view, // controller drives resize
        &mouse_mode,
    );

    // Out of raw mode (guard drops after) before the parting line.
    drop(_raw);
    match outcome {
        Ok(PumpEnd::Exited(code)) => {
            let _ = writeln!(stdout, "\r\n[session '{endpoint_id}' ended ({code:?})]");
            Ok(())
        }
        Ok(PumpEnd::Detached) => {
            let _ = writeln!(stdout, "\r\n[detached — '{endpoint_id}' still running]");
            Ok(())
        }
        Ok(PumpEnd::Displaced(by)) => {
            // The loud kick notice (REQ-KICK-1): a different operator took control.
            let _ = writeln!(
                stdout,
                "\r\n[displaced — '{endpoint_id}' was taken over by {by}]"
            );
            Ok(())
        }
        Ok(PumpEnd::BrokerGone) => {
            // A severed broker stream is recoverable, not a crash (REQ-HAZARD-RC-EOF):
            // the broker is the daemon-lifetime anchor and comes back on the next
            // `spt api` call. Surface a CLEAR actionable line instead of the raw
            // "failed to fill whole buffer" that read as a PTY crash.
            let _ = writeln!(
                stdout,
                "\r\n[daemon connection dropped — the broker may have restarted \
                 (e.g. `spt daemon stop`); the session '{endpoint_id}' is unaffected, \
                 re-run `spt endpoint run`/attach to reconnect]"
            );
            Ok(())
        }
        Ok(PumpEnd::Stalled) => {
            // Path b backstop (REQ-HAZARD-RC-ATTACH-FAILFAST): the attach produced
            // no output at all — a dead/wedged session, not a slow one. A clear
            // message, never the historical infinite blank.
            let _ = writeln!(
                stdout,
                "\r\n[no output from '{endpoint_id}' — the session may be dead or \
                 wedged. Detached. Try `spt endpoint list` for its state, or \
                 `spt endpoint run` to restart it]"
            );
            Ok(())
        }
        Err(e) => Err(e),
    }
}


920	}
921
922	/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound
923	/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.
924	#[allow(clippy::too_many_arguments)]
925	fn pump(
926	    brain: &mut Brain,
927	    stream_id: u64,
928	    ops: &OpMinter,
929	    rx: Option<&mpsc::Receiver<StdinMsg>>,
930	    decoder: &mut AttachDecoder,
931	    stdout: &mut std::io::Stdout,
932	    controller: bool,
933	) -> Result<PumpEnd, String> {
934	    // Finish our send side so the target's `serve_attach` sees EOF → Detached
935	    // (the session outlives the viewport). Best-effort: an already-finished or
936	    // exited stream send is harmless to attempt.
937	    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {
938	        let _ = brain.net_stream_send(stream_id, &[], None, true);
939	        PumpEnd::Detached
940	    }
941	    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;
942	    // drop any `seq` already rendered (the operator render cursor, matching the
943	    // local `OutputLog` replay discipline).
944	    let mut cursor = 0u64;
945	    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size
946	    // each slice and forward a Resize when it changes (cross-platform without
947	    // hooking SIGWINCH / the crossterm event loop, which would conflict with the
948	    // raw-stdin reader). `None` until the first read.
949	    let mut last_size: Option<(u16, u16)> = None;
950	    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown
951	    // as a one-line indicator when it differs from our own terminal.
952	    let mut pty_size: Option<(u16, u16)> = None;
953	    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged
954	    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE
955	    // rather than an infinite blank. Any broker event clears it.
956	    let started = Instant::now();
957	    let mut seen_any = false;
958	    loop {
959	        // ── controller window-change → resize ──────────────────────────────

}

/// The full-duplex pump: drain stdin → `send_attach_input`; render inbound
/// `AttachRecord::Output` → stdout; end on `Exit`, EOF, or detach.
#[allow(clippy::too_many_arguments)]
fn pump(
    brain: &mut Brain,
    stream_id: u64,
    ops: &OpMinter,
    rx: Option<&mpsc::Receiver<StdinMsg>>,
    decoder: &mut AttachDecoder,
    stdout: &mut std::io::Stdout,
    controller: bool,
) -> Result<PumpEnd, String> {
    // Finish our send side so the target's `serve_attach` sees EOF → Detached
    // (the session outlives the viewport). Best-effort: an already-finished or
    // exited stream send is harmless to attempt.
    fn detach(brain: &mut Brain, stream_id: u64) -> PumpEnd {
        let _ = brain.net_stream_send(stream_id, &[], None, true);
        PumpEnd::Detached
    }
    // Dedup cursor: a post-restart re-serve may re-transmit `Output` records;
    // drop any `seq` already rendered (the operator render cursor, matching the
    // local `OutputLog` replay discipline).
    let mut cursor = 0u64;
    // Controller window-change tracking (REQ-RCVIEW-1): poll the terminal size
    // each slice and forward a Resize when it changes (cross-platform without
    // hooking SIGWINCH / the crossterm event loop, which would conflict with the
    // raw-stdin reader). `None` until the first read.
    let mut last_size: Option<(u16, u16)> = None;
    // The PTY size last advertised to us as a VIEWER (the letterbox feed); shown
    // as a one-line indicator when it differs from our own terminal.
    let mut pty_size: Option<(u16, u16)> = None;
    // First-event backstop (REQ-HAZARD-RC-ATTACH-FAILFAST path b): a dead/wedged
    // attach produces nothing — bail with a clear message past FIRST_EVENT_GRACE
    // rather than an infinite blank. Any broker event clears it.
    let started = Instant::now();
    let mut seen_any = false;
    loop {
        // ── controller window-change → resize ──────────────────────────────


    decoder: &mut AttachDecoder,
    stdout: &mut std::io::Stdout,
    controller: bool,
) -> Result<PumpEnd, String> {
    // Finish our send side so the target's `serve_attach` sees EOF → Detached

    decoder: &mut AttachDecoder,
    stdout: &mut std::io::Stdout,
    controller: bool,
    mouse_mode: &MouseMode,
) -> Result<PumpEnd, String> {
    // Track the harness's mouse-reporting mode from its output DECSET sequences so
    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The
    // scanner is pump-local (single producer) and survives a sequence split across
    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.
    let mut mouse_scanner = MouseModeScanner::default();
    // Finish our send side so the target's `serve_attach` sees EOF → Detached


    decoder: &mut AttachDecoder,
    stdout: &mut std::io::Stdout,
    controller: bool,
) -> Result<PumpEnd, String> {
    // Finish our send side so the target's `serve_attach` sees EOF → Detached

    decoder: &mut AttachDecoder,
    stdout: &mut std::io::Stdout,
    controller: bool,
    mouse_mode: &MouseMode,
) -> Result<PumpEnd, String> {
    // Track the harness's mouse-reporting mode from its output DECSET sequences so
    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The
    // scanner is pump-local (single producer) and survives a sequence split across
    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.
    let mut mouse_scanner = MouseModeScanner::default();
    // Finish our send side so the target's `serve_attach` sees EOF → Detached

+    // Track the harness's mouse-reporting mode from its output DECSET sequences so

+    // the stdin reader knows whether to forward scroll (REQ-RC-MOUSE-FORWARD). The

+    // scanner is pump-local (single producer) and survives a sequence split across

+    // output chunks; it writes the shared `mouse_mode` atomics the reader observes.

+    let mut mouse_scanner = MouseModeScanner::default();

// (the session outlives the viewport). Best-effort: an already-finished or

// exited stream send is harmless to attempt.


Now the output write path — scan before/at write_all. Read around it.


1015	                    match rec {
1016	                        AttachRecord::Output { seq, data_b64 } => {
1017	                            if seq < cursor {
1018	                                continue; // already rendered — re-serve dedup
1019	                            }
1020	                            cursor = seq + 1;
1021	                            let bytes = decode_bytes(&data_b64)
1022	                                .map_err(|e| format!("decode output: {e}"))?;
1023	                            stdout.write_all(&bytes).map_err(|e| format!("write: {e}"))?;
1024	                            stdout.flush().map_err(|e| format!("flush: {e}"))?;
1025	                        }
1026	                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),
1027	                        AttachRecord::Displaced { by } => {
1028	                            return Ok(PumpEnd::Displaced(by));
1029	                        }
1030	                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,
1031	                        // show a one-line indicator when our terminal differs from
1032	                        // the new PTY size (rc is a byte pump — true clip/pad needs
1033	                        // a grid model, deferred; the honest size cue renders here).
1034	                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {
1035	                            pty_size = Some((rows, cols));
1036	                            match crossterm::terminal::size() {
1037	                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {
1038	                                    let _ = write!(
1039	                                        stdout,

                    match rec {
                        AttachRecord::Output { seq, data_b64 } => {
                            if seq < cursor {
                                continue; // already rendered — re-serve dedup
                            }
                            cursor = seq + 1;
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| format!("decode output: {e}"))?;
                            stdout.write_all(&bytes).map_err(|e| format!("write: {e}"))?;
                            stdout.flush().map_err(|e| format!("flush: {e}"))?;
                        }
                        AttachRecord::Exit { code } => return Ok(PumpEnd::Exited(code)),
                        AttachRecord::Displaced { by } => {
                            return Ok(PumpEnd::Displaced(by));
                        }
                        // Viewer letterbox feed (REQ-RCVIEW-1): on a size change,
                        // show a one-line indicator when our terminal differs from
                        // the new PTY size (rc is a byte pump — true clip/pad needs
                        // a grid model, deferred; the honest size cue renders here).
                        AttachRecord::Size { rows, cols } if pty_size != Some((rows, cols)) => {
                            pty_size = Some((rows, cols));
                            match crossterm::terminal::size() {
                                Ok((mycols, myrows)) if (myrows, mycols) != (rows, cols) => {
                                    let _ = write!(
                                        stdout,


                            cursor = seq + 1;
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| format!("decode output: {e}"))?;
                            stdout.write_all(&bytes).map_err(|e| format!("write: {e}"))?;
                            stdout.flush().map_err(|e| format!("flush: {e}"))?;

                            cursor = seq + 1;
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| format!("decode output: {e}"))?;
                            // Track the harness's mouse-reporting mode from its output
                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.
                            mouse_scanner.feed(mouse_mode, &bytes);
                            stdout.write_all(&bytes).map_err(|e| format!("write: {e}"))?;
                            stdout.flush().map_err(|e| format!("flush: {e}"))?;


                            cursor = seq + 1;
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| format!("decode output: {e}"))?;
                            stdout.write_all(&bytes).map_err(|e| format!("write: {e}"))?;
                            stdout.flush().map_err(|e| format!("flush: {e}"))?;

                            cursor = seq + 1;
                            let bytes = decode_bytes(&data_b64)
                                .map_err(|e| format!("decode output: {e}"))?;
                            // Track the harness's mouse-reporting mode from its output
                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.
                            mouse_scanner.feed(mouse_mode, &bytes);
                            stdout.write_all(&bytes).map_err(|e| format!("write: {e}"))?;
                            stdout.flush().map_err(|e| format!("flush: {e}"))?;

+                            // Track the harness's mouse-reporting mode from its output

+                            // (REQ-RC-MOUSE-FORWARD) before rendering it verbatim.

+                            mouse_scanner.feed(mouse_mode, &bytes);


Compile the bundle.


Bash

Build the full P1b bundle


   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 9.94s


Bundle compiles. Clippy spt.


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

Clippy spt all-targets


    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Checking spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 7.56s


Clippy clean. Writing the rc pure units myself (scroll + DECSET scan), delegating the broker deadlock int + ack units to spt-test-engineer. Find the rc test module.


Grep

fn clipboard_paste_wraps_nonempty|fn mouse_is_paste_only_right|mod tests


crates\spt\src\rc.rs:1064:mod tests {
crates\spt\src\rc.rs:1406:    fn mouse_is_paste_only_right_button_down() {
crates\spt\src\rc.rs:1428:    fn clipboard_paste_wraps_nonempty_and_noops_on_empty_or_failure() {


1404	    #[cfg(windows)]
1405	    #[test]
1406	    fn mouse_is_paste_only_right_button_down() {
1407	        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
1408	        let me = |kind| MouseEvent {
1409	            kind,
1410	            column: 0,
1411	            row: 0,
1412	            modifiers: KeyModifiers::NONE,
1413	        };
1414	        assert!(mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Right))));
1415	        assert!(!mouse_is_paste(&me(MouseEventKind::Up(MouseButton::Right))));
1416	        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Left))));
1417	        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Middle))));
1418	        assert!(!mouse_is_paste(&me(MouseEventKind::Moved)));
1419	        assert!(!mouse_is_paste(&me(MouseEventKind::Drag(MouseButton::Right))));
1420	        assert!(!mouse_is_paste(&me(MouseEventKind::ScrollDown)));
1421	    }
1422
1423	    // [unit->REQ-RC-WIN-PASTE] the gesture→paste decision with an INJECTED clipboard
1424	    // reader (no real clipboard): a non-empty read ⇒ the bracketed paste; an empty
1425	    // read OR a read failure (`None`) ⇒ a clean no-op (never inject garbage).
1426	    #[cfg(windows)]
1427	    #[test]
1428	    fn clipboard_paste_wraps_nonempty_and_noops_on_empty_or_failure() {
1429	        // Non-empty clipboard → bracketed paste of the verbatim content.
1430	        assert_eq!(
1431	            clipboard_paste(|| Some(b"pasted".to_vec())),
1432	            Some(b"\x1b[200~pasted\x1b[201~".to_vec())
1433	        );
1434	        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).
1435	        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);
1436	        // Failed read → no-op.
1437	        assert_eq!(clipboard_paste(|| None), None);
1438	    }
1439	}
1440

    #[cfg(windows)]
    #[test]
    fn mouse_is_paste_only_right_button_down() {
        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
        let me = |kind| MouseEvent {
            kind,
            column: 0,
            row: 0,
            modifiers: KeyModifiers::NONE,
        };
        assert!(mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Right))));
        assert!(!mouse_is_paste(&me(MouseEventKind::Up(MouseButton::Right))));
        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Left))));
        assert!(!mouse_is_paste(&me(MouseEventKind::Down(MouseButton::Middle))));
        assert!(!mouse_is_paste(&me(MouseEventKind::Moved)));
        assert!(!mouse_is_paste(&me(MouseEventKind::Drag(MouseButton::Right))));
        assert!(!mouse_is_paste(&me(MouseEventKind::ScrollDown)));
    }

    // [unit->REQ-RC-WIN-PASTE] the gesture→paste decision with an INJECTED clipboard
    // reader (no real clipboard): a non-empty read ⇒ the bracketed paste; an empty
    // read OR a read failure (`None`) ⇒ a clean no-op (never inject garbage).
    #[cfg(windows)]
    #[test]
    fn clipboard_paste_wraps_nonempty_and_noops_on_empty_or_failure() {
        // Non-empty clipboard → bracketed paste of the verbatim content.
        assert_eq!(
            clipboard_paste(|| Some(b"pasted".to_vec())),
            Some(b"\x1b[200~pasted\x1b[201~".to_vec())
        );
        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).
        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);
        // Failed read → no-op.
        assert_eq!(clipboard_paste(|| None), None);
    }
}


Edit

        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).
        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);
        // Failed read → no-op.
        assert_eq!(clipboard_paste(|| None), None);
    }
}

        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).
        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);
        // Failed read → no-op.
        assert_eq!(clipboard_paste(|| None), None);
    }

    // [unit->REQ-RC-MOUSE-FORWARD] the scroll-wheel classifier: ScrollUp/Down only,
    // every other mouse event is a non-scroll (None → dropped or paste, not forwarded).
    #[cfg(windows)]
    #[test]
    fn scroll_dir_classifies_wheel_only() {
        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
        let me = |kind| MouseEvent {
            kind,
            column: 0,
            row: 0,
            modifiers: KeyModifiers::NONE,
        };
        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollUp)), Some(true));
        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollDown)), Some(false));
        assert_eq!(scroll_dir(&me(MouseEventKind::Down(MouseButton::Right))), None);
        assert_eq!(scroll_dir(&me(MouseEventKind::Moved)), None);
        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollLeft)), None);
    }

    // [unit->REQ-RC-MOUSE-FORWARD] the scroll→SGR encoder emits EXACT xterm bytes:
    // ESC[<btn;col+1;row+1M, btn 64=up/65=down, 0-based crossterm → 1-based xterm.
    #[cfg(windows)]
    #[test]
    fn scroll_sgr_emits_exact_xterm_bytes() {
        assert_eq!(scroll_sgr(true, 0, 0), b"\x1b[<64;1;1M".to_vec());
        assert_eq!(scroll_sgr(false, 0, 0), b"\x1b[<65;1;1M".to_vec());
        // 0-based (col=4,row=9) → 1-based (5,10).
        assert_eq!(scroll_sgr(true, 4, 9), b"\x1b[<64;5;10M".to_vec());
        assert_eq!(scroll_sgr(false, 79, 23), b"\x1b[<65;80;24M".to_vec());
    }

    // [unit->REQ-RC-MOUSE-FORWARD] the DECSET scan tracks the harness's mouse-report
    // mode from its output: 1000/1002/1003 toggle `enabled`, 1006 toggles `sgr`,
    // a combined `ESC[?1002;1006h` sets both, and `l` resets — over a mixed stream.
    #[test]
    fn mouse_mode_decset_scan_tracks_enable_and_sgr() {
        use std::sync::atomic::Ordering::Acquire;
        let mode = MouseMode::default();
        let mut sc = MouseModeScanner::default();
        // Plain output, no DECSET → unchanged.
        sc.feed(&mode, b"hello world\x1b[0m");
        assert!(!mode.enabled.load(Acquire));
        assert!(!mode.sgr.load(Acquire));
        // Combined enable (button-event tracking + SGR), amid other output.
        sc.feed(&mode, b"before\x1b[?1002;1006hafter");
        assert!(mode.enabled.load(Acquire));
        assert!(mode.sgr.load(Acquire));
        // 1003 (any-event) also enables; a non-mouse private mode (25 cursor) is ignored.
        sc.feed(&mode, b"\x1b[?25h\x1b[?1003h");
        assert!(mode.enabled.load(Acquire));
        // Reset mouse but keep SGR.
        sc.feed(&mode, b"\x1b[?1003l");
        assert!(!mode.enabled.load(Acquire));
        assert!(mode.sgr.load(Acquire));
        // Reset SGR.
        sc.feed(&mode, b"\x1b[?1006l");
        assert!(!mode.sgr.load(Acquire));
    }

    // [unit->REQ-RC-MOUSE-FORWARD] a DECSET sequence SPLIT across two output chunks
    // (the harness's enable can land across a decoder boundary) is still tracked —
    // the scanner carries the partial `ESC[?…` to the next feed.
    #[test]
    fn mouse_mode_decset_scan_survives_split_across_chunks() {
        use std::sync::atomic::Ordering::Acquire;
        let mode = MouseMode::default();
        let mut sc = MouseModeScanner::default();
        // `ESC[?1006h` split right in the middle of the digits.
        sc.feed(&mode, b"tail\x1b[?100");
        assert!(!mode.sgr.load(Acquire), "incomplete half must not toggle yet");
        sc.feed(&mode, b"6hmore");
        assert!(mode.sgr.load(Acquire), "the completed sequence toggles sgr");
        // Split at the very ESC/[/? boundary too.
        sc.feed(&mode, b"x\x1b");
        sc.feed(&mode, b"[?1002h");
        assert!(mode.enabled.load(Acquire));
    }
}


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

        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).
        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);
        // Failed read → no-op.
        assert_eq!(clipboard_paste(|| None), None);
    }
}

        // Empty clipboard → no-op (not an empty bracketed frame — nothing to paste).
        assert_eq!(clipboard_paste(|| Some(Vec::new())), None);
        // Failed read → no-op.
        assert_eq!(clipboard_paste(|| None), None);
    }

    // [unit->REQ-RC-MOUSE-FORWARD] the scroll-wheel classifier: ScrollUp/Down only,
    // every other mouse event is a non-scroll (None → dropped or paste, not forwarded).
    #[cfg(windows)]
    #[test]
    fn scroll_dir_classifies_wheel_only() {
        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
        let me = |kind| MouseEvent {
            kind,
            column: 0,
            row: 0,
            modifiers: KeyModifiers::NONE,
        };
        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollUp)), Some(true));
        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollDown)), Some(false));
        assert_eq!(scroll_dir(&me(MouseEventKind::Down(MouseButton::Right))), None);
        assert_eq!(scroll_dir(&me(MouseEventKind::Moved)), None);
        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollLeft)), None);
    }

    // [unit->REQ-RC-MOUSE-FORWARD] the scroll→SGR encoder emits EXACT xterm bytes:
    // ESC[<btn;col+1;row+1M, btn 64=up/65=down, 0-based crossterm → 1-based xterm.
    #[cfg(windows)]
    #[test]
    fn scroll_sgr_emits_exact_xterm_bytes() {
        assert_eq!(scroll_sgr(true, 0, 0), b"\x1b[<64;1;1M".to_vec());
        assert_eq!(scroll_sgr(false, 0, 0), b"\x1b[<65;1;1M".to_vec());
        // 0-based (col=4,row=9) → 1-based (5,10).
        assert_eq!(scroll_sgr(true, 4, 9), b"\x1b[<64;5;10M".to_vec());
        assert_eq!(scroll_sgr(false, 79, 23), b"\x1b[<65;80;24M".to_vec());
    }

    // [unit->REQ-RC-MOUSE-FORWARD] the DECSET scan tracks the harness's mouse-report
    // mode from its output: 1000/1002/1003 toggle `enabled`, 1006 toggles `sgr`,
    // a combined `ESC[?1002;1006h` sets both, and `l` resets — over a mixed stream.
    #[test]
    fn mouse_mode_decset_scan_tracks_enable_and_sgr() {
        use std::sync::atomic::Ordering::Acquire;
        let mode = MouseMode::default();
        let mut sc = MouseModeScanner::default();
        // Plain output, no DECSET → unchanged.
        sc.feed(&mode, b"hello world\x1b[0m");
        assert!(!mode.enabled.load(Acquire));
        assert!(!mode.sgr.load(Acquire));
        // Combined enable (button-event tracking + SGR), amid other output.
        sc.feed(&mode, b"before\x1b[?1002;1006hafter");
        assert!(mode.enabled.load(Acquire));
        assert!(mode.sgr.load(Acquire));
        // 1003 (any-event) also enables; a non-mouse private mode (25 cursor) is ignored.
        sc.feed(&mode, b"\x1b[?25h\x1b[?1003h");
        assert!(mode.enabled.load(Acquire));
        // Reset mouse but keep SGR.
        sc.feed(&mode, b"\x1b[?1003l");
        assert!(!mode.enabled.load(Acquire));
        assert!(mode.sgr.load(Acquire));
        // Reset SGR.
        sc.feed(&mode, b"\x1b[?1006l");
        assert!(!mode.sgr.load(Acquire));
    }

    // [unit->REQ-RC-MOUSE-FORWARD] a DECSET sequence SPLIT across two output chunks
    // (the harness's enable can land across a decoder boundary) is still tracked —
    // the scanner carries the partial `ESC[?…` to the next feed.
    #[test]
    fn mouse_mode_decset_scan_survives_split_across_chunks() {
        use std::sync::atomic::Ordering::Acquire;
        let mode = MouseMode::default();
        let mut sc = MouseModeScanner::default();
        // `ESC[?1006h` split right in the middle of the digits.
        sc.feed(&mode, b"tail\x1b[?100");
        assert!(!mode.sgr.load(Acquire), "incomplete half must not toggle yet");
        sc.feed(&mode, b"6hmore");
        assert!(mode.sgr.load(Acquire), "the completed sequence toggles sgr");
        // Split at the very ESC/[/? boundary too.
        sc.feed(&mode, b"x\x1b");
        sc.feed(&mode, b"[?1002h");
        assert!(mode.enabled.load(Acquire));
    }
}

+    // [unit->REQ-RC-MOUSE-FORWARD] the scroll-wheel classifier: ScrollUp/Down only,

+    // every other mouse event is a non-scroll (None → dropped or paste, not forwarded).

+    fn scroll_dir_classifies_wheel_only() {

+        use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};

+            modifiers: KeyModifiers::NONE,

+        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollUp)), Some(true));

+        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollDown)), Some(false));

+        assert_eq!(scroll_dir(&me(MouseEventKind::Down(MouseButton::Right))), None);

+        assert_eq!(scroll_dir(&me(MouseEventKind::Moved)), None);

+        assert_eq!(scroll_dir(&me(MouseEventKind::ScrollLeft)), None);

+    // [unit->REQ-RC-MOUSE-FORWARD] the scroll→SGR encoder emits EXACT xterm bytes:

+    // ESC[<btn;col+1;row+1M, btn 64=up/65=down, 0-based crossterm → 1-based xterm.

+    fn scroll_sgr_emits_exact_xterm_bytes() {

+        assert_eq!(scroll_sgr(true, 0, 0), b"\x1b[<64;1;1M".to_vec());

+        assert_eq!(scroll_sgr(false, 0, 0), b"\x1b[<65;1;1M".to_vec());

+    // [unit->REQ-RC-MOUSE-FORWARD] the DECSET scan tracks the harness's mouse-report

+    // mode from its output: 1000/1002/1003 toggle `enabled`, 1006 toggles `sgr`,

+    // a combined `ESC[?1002;1006h` sets both, and `l` resets — over a mixed stream.

+    fn mouse_mode_decset_scan_tracks_enable_and_sgr() {

+        use std::sync::atomic::Ordering::Acquire;

+        let mode = MouseMode::default();

+        let mut sc = MouseModeScanner::default();

+        // Plain output, no DECSET → unchanged.

+        sc.feed(&mode, b"hello world\x1b[0m");

+        assert!(!mode.enabled.load(Acquire));

+        assert!(!mode.sgr.load(Acquire));

+        // Combined enable (button-event tracking + SGR), amid other output.

+        sc.feed(&mode, b"before\x1b[?1002;1006hafter");

+        assert!(mode.enabled.load(Acquire));

+        assert!(mode.sgr.load(Acquire));

+        // 1003 (any-event) also enables; a non-mouse private mode (25 cursor) is ignored.

+    // [unit->REQ-RC-MOUSE-FORWARD] a DECSET sequence SPLIT across two output chunks

+    // (the harness's enable can land across a decoder boundary) is still tracked —

+    // the scanner carries the partial `ESC[?…` to the next feed.

+    fn mouse_mode_decset_scan_survives_split_across_chunks() {

+        // `ESC[?1006h` split right in the middle of the digits.

+        sc.feed(&mode, b"tail\x1b[?100");

+        assert!(!mode.sgr.load(Acquire), "incomplete half must not toggle yet");

+        assert!(mode.sgr.load(Acquire), "the completed sequence toggles sgr");

+        // Split at the very ESC/[/? boundary too.


Bash

cargo test -p spt --bin spt rc::tests:: 2>&1 | tail -30

Run rc unit tests incl new scroll/DECSET


   Compiling iroh-relay v0.98.0
   Compiling iroh v0.98.2
   Compiling spt-net v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-net)
   Compiling spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 25.11s
     Running unittests src\main.rs (target\debug\deps\spt-00174d284f6e881b.exe)

running 15 tests
test rc::tests::bracketed_paste_framing_is_exact_and_content_verbatim ... ok
test rc::tests::detach_prefix_spans_chunks ... ok
test rc::tests::scroll_dir_classifies_wheel_only ... ok
test rc::tests::first_event_stall_decision ... ok
test rc::tests::mouse_mode_decset_scan_survives_split_across_chunks ... ok
test rc::tests::mouse_is_paste_only_right_button_down ... ok
test rc::tests::translate_key_event_emits_exact_xterm_vt ... ok
test rc::tests::clipboard_paste_wraps_nonempty_and_noops_on_empty_or_failure ... ok
test rc::tests::key_event_step_detach_sm ... ok
test rc::tests::detach_keybind_semantics ... ok
test rc::tests::classify_read_err_eof_is_graceful_not_fatal ... ok
test rc::tests::mouse_mode_decset_scan_tracks_enable_and_sgr ... ok
test rc::tests::scroll_sgr_emits_exact_xterm_bytes ... ok
test rc::tests::op_minter_distinct_and_monotonic ... ok
test rc::tests::attach_offline_endpoint_short_circuits_clean ... ok

test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 193 filtered out; finished in 0.01s


All 15 rc units pass (4 new scroll/DECSET incl split-chunk). Now mint REQ-RC-MOUSE-FORWARD in registry.


Edit

int = REPRO-FIRST (the keystone, the exact gate the wedge needed): a flood of N>pipe-buffer (deterministic, e.g. 64) operator input frames through serve_attach on ONE conn -> PRE-FIX deadlocks (ack-send-START-without-END, handler stops draining; a concurrent attach / the detach cannot be serviced) -> POST-FIX drains all N + the session stays live + a concurrent attach still OPENS. Real broker+brain, NO mocks. doc = KNOWN-HAZARDS entry (the deadlock + the N-1 broker-resident caveat). clippy/traceable/docs-drift."

int = REPRO-FIRST (the keystone, the exact gate the wedge needed): a flood of N>pipe-buffer (deterministic, e.g. 64) operator input frames through serve_attach on ONE conn -> PRE-FIX deadlocks (ack-send-START-without-END, handler stops draining; a concurrent attach / the detach cannot be serviced) -> POST-FIX drains all N + the session stays live + a concurrent attach still OPENS. Real broker+brain, NO mocks. doc = KNOWN-HAZARDS entry (the deadlock + the N-1 broker-resident caveat). clippy/traceable/docs-drift."

# ── P1b sibling (operator HITL 2026-06-19): rc scroll broken — capture-stolen wheel must forward to the harness ──
[[requirements]]
id = "REQ-RC-MOUSE-FORWARD"
title = "On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1b (todlando, operator-ruled scroll fix bundled with the ack-deadlock; doyle gates). NO int (live console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure translator + scan unit-tested). impl = (cfg(windows)) MouseMode{enabled,sgr atomics} + MouseModeScanner (carry-buffer DECSET scan, parse_decset_private/apply_mouse_mode) fed from the pump's output-render path + scroll_dir + scroll_sgr (SGR encode) + the spawn_stdin_reader_events scroll arm (forward when enabled&&sgr) threaded via Arc<MouseMode> (pump writes / reader reads). unit = scroll_dir classify (wheel-only) + scroll_sgr EXACT xterm bytes (up=64/down=65, col+1/row+1) + DECSET scan (1000/1002/1003/1006 set&reset, combined ESC[?1002;1006h, mixed stream, and SPLIT across two feeds — the carry-buffer edge). doc = KNOWN-HAZARDS entry (rc Windows scroll forwarded as SGR when the harness reports mouse; capture is for right-click paste). doyle gates; operator HITL (scroll works in a real CC rc session; right-click still pastes)."


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

int = REPRO-FIRST (the keystone, the exact gate the wedge needed): a flood of N>pipe-buffer (deterministic, e.g. 64) operator input frames through serve_attach on ONE conn -> PRE-FIX deadlocks (ack-send-START-without-END, handler stops draining; a concurrent attach / the detach cannot be serviced) -> POST-FIX drains all N + the session stays live + a concurrent attach still OPENS. Real broker+brain, NO mocks. doc = KNOWN-HAZARDS entry (the deadlock + the N-1 broker-resident caveat). clippy/traceable/docs-drift."

# ── P1b sibling (operator HITL 2026-06-19): rc scroll broken — capture-stolen wheel must forward to the harness ──
[[requirements]]
id = "REQ-RC-MOUSE-FORWARD"
title = "On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1b (todlando, operator-ruled scroll fix bundled with the ack-deadlock; doyle gates). NO int (live console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure translator + scan unit-tested). impl = (cfg(windows)) MouseMode{enabled,sgr atomics} + MouseModeScanner (carry-buffer DECSET scan, parse_decset_private/apply_mouse_mode) fed from the pump's output-render path + scroll_dir + scroll_sgr (SGR encode) + the spawn_stdin_reader_events scroll arm (forward when enabled&&sgr) threaded via Arc<MouseMode> (pump writes / reader reads). unit = scroll_dir classify (wheel-only) + scroll_sgr EXACT xterm bytes (up=64/down=65, col+1/row+1) + DECSET scan (1000/1002/1003/1006 set&reset, combined ESC[?1002;1006h, mixed stream, and SPLIT across two feeds — the carry-buffer edge). doc = KNOWN-HAZARDS entry (rc Windows scroll forwarded as SGR when the harness reports mouse; capture is for right-click paste). doyle gates; operator HITL (scroll works in a real CC rc session; right-click still pastes)."

title = "A FLOOD of operator input on one brain↔broker connection deadlocks the broker PERMANENTLY (entire broker — no new/existing attach; the controller stays latched because the per-conn handler can't process the detach). ROOT (doyle /diagnose, code-grounded + HITL capture, the v0.13.0 P1 ctrl+V re-open): `serve_attach` processes a whole `NetStreamData` batch of N operator `Input` records in its inner `for rec in decoder.push()` loop, calling `brain.send_effect(op_id, &bytes)` N times WITHOUT returning to `read_event()` — so the brain writes N `KIND_INPUT` frames back-to-back and drains nothing. The broker's single-threaded per-conn handler answers EACH with `send_frame(applied_envelope)` on the SAME conn (B5 exactly-once ack, KNOWN-HAZARDS 7.2). With the brain not reading, the broker→brain return direction fills (~10 frames = the IPC pipe buffer) → `send_frame` BLOCKS → the handler stops reading → the brain's writes block too → mutual full-duplex DEADLOCK. Capture pinned it: 11 input frames, write_input 11/11 (P0 holds — the PTY write is fine), ack send START=11 / END=10 (frame #11's applied-ack never returns). Same class as the v0.12.1 L0 two-conn split. Windows Terminal's ctrl+V paste accelerator was the trigger (injects the clipboard as a char-by-char key flood) but the deadlock is generic to ANY input flood, NOT ctrl+V-specific and NOT a P0 (PTY-write) or W1 (output-drain) regression. The applied-ack is load-bearing ONLY for `shellchan` (one-at-a-time spool delivery WAITS on `BrokerEvent::Applied`); `serve_attach` DISCARDS it (the operator/rc path is fire-and-forward, op_id for dedup only, never gates on the ack). FIX (doyle-approved): CONDITIONAL ACK — `InputReq` gains `ack: bool` (serde default = true, N-1-safe: an older brain's input still acks = today's behavior). `serve_attach`'s operator path calls `send_effect_no_ack` (ack=false) → `dispatch_input` writes NO applied frame → the per-conn handler never writes back while servicing the flood → it always drains → no deadlock (cures ANY input flood). `shellchan` keeps `send_effect` (ack=true) and its `Applied`-wait. Exactly-once PRESERVED: the broker still dedups by (session, op_id) at the applied-set regardless of the ack. N-1 caveat: an OLD resident broker (self-update window) ignores `ack=false` → still acks → the deadlock persists until a broker restart (inherent KNOWN-HAZARDS 7.9 broker-resident-wire-change class). (v0.13.0)"

required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 P1b (todlando, the real ship-blocker behind the ctrl+V wedge; doyle gates). impl = InputReq.ack (serde default_true) + Brain::send_effect_no_ack (send_effect_inner shared body) + serve_attach operator path -> send_effect_no_ack + dispatch_input gates send_frame(applied) on req.ack. unit = InputReq.ack serde default=true on an absent field (N-1) + dispatch_input emits the applied frame when ack=true and NONE when ack=false + exactly-once on the no-ack path (a replayed/duplicate op_id still dedups at apply_once, no double PTY write). int = REPRO-FIRST (the keystone, the exact gate the wedge needed): a flood of N>pipe-buffer (deterministic, e.g. 64) operator input frames through serve_attach on ONE conn -> PRE-FIX deadlocks (ack-send-START-without-END, handler stops draining; a concurrent attach / the detach cannot be serviced) -> POST-FIX drains all N + the session stays live + a concurrent attach still OPENS. Real broker+brain, NO mocks. doc = KNOWN-HAZARDS entry (the deadlock + the N-1 broker-resident caveat). clippy/traceable/docs-drift."

+# ── P1b sibling (operator HITL 2026-06-19): rc scroll broken — capture-stolen wheel must forward to the harness ──

+title = "On Windows, `spt rc` must FORWARD scroll-wheel events to the harness when the harness has mouse reporting on. ROOT (operator HITL): P1's RawGuard EnableMouseCapture (added for right-click paste, REQ-RC-WIN-PASTE) makes Windows Terminal forward ALL mouse — including the scroll wheel — to rc instead of scrolling its own buffer, but the rc mouse handler dropped everything except right-button-down → scroll DIED (and WT's native scrollback is stolen by the capture). Operator ruling: keep mouse capture + right-click bracketed paste AND forward scroll to the harness. FIX (doyle design, cfg(windows), folds into the rc mouse handler): TRACK the harness's mouse-reporting mode by scanning its OUTPUT stream for the DECSET set/reset — ESC[?1000h/1002h/1003h (mouse on) + ESC[?1006h (SGR ext) and their ...l (off) — into a shared MouseMode{enabled,sgr} (pump writes from output, stdin reader reads); the scan survives a sequence SPLIT across output chunks (a bounded carry buffer). The mouse handler: right-button-DOWN -> bracketed clipboard paste (unchanged, REQ-RC-WIN-PASTE); ScrollUp/Down -> translate to an xterm SGR mouse report (ESC[<64;col+1;row+1M up / ESC[<65;..M down, 0-based crossterm -> 1-based xterm) and forward ONLY when enabled && sgr (else DROP — a legacy X10 report the harness may not parse is garbage); Moved/drag/left/middle -> DROP (scroll is the operator's need; click-forward risks garbage, no click-to-position). Unix UNCHANGED (no capture; the terminal scrolls natively). (v0.13.0)"

+required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 P1b (todlando, operator-ruled scroll fix bundled with the ack-deadlock; doyle gates). NO int (live console mouse = HITL, REQ-RUN-PICKER/RC-1 precedent; the pure translator + scan unit-tested). impl = (cfg(windows)) MouseMode{enabled,sgr atomics} + MouseModeScanner (carry-buffer DECSET scan, parse_decset_private/apply_mouse_mode) fed from the pump's output-render path + scroll_dir + scroll_sgr (SGR encode) + the spawn_stdin_reader_events scroll arm (forward when enabled&&sgr) threaded via Arc<MouseMode> (pump writes / reader reads). unit = scroll_dir classify (wheel-only) + scroll_sgr EXACT xterm bytes (up=64/down=65, col+1/row+1) + DECSET scan (1000/1002/1003/1006 set&reset, combined ESC[?1002;1006h, mixed stream, and SPLIT across two feeds — the carry-buffer edge). doc = KNOWN-HAZARDS entry (rc Windows scroll forwarded as SGR when the harness reports mouse; capture is for right-click paste). doyle gates; operator HITL (scroll works in a real CC rc session; right-click still pastes)."


Now docs. Read current 7.18 (table row + narrative) on this branch.


docs\KNOWN-HAZARDS.md:448:### 7.18 `spt rc` paste is client-originated on Windows — read the LOCAL clipboard, inject a BRACKETED paste  `[REQ-RC-WIN-PASTE]`
docs\KNOWN-HAZARDS.md:505:[Omitted long matching line]