    fn enqueue(&self, bytes: InputRecord) -> bool {
        match self.tx.try_send(bytes) {
            Ok(()) => {
                // Heal: input is flowing again — clear a prior backpressure stamp.
                if self.backpressure.swap(false, Ordering::AcqRel) {
                    self.stamp_backpressure(false);
                }
                true
            }
            Err(TrySendError::Full(_)) => {
                // DROP + surface, but stamp only on the rising edge.
                if !self.backpressure.swap(true, Ordering::AcqRel) {
                    self.stamp_backpressure(true);
                }
                false
            }
            // The writer thread is gone (session teardown) — nothing to do.
            Err(TrySendError::Disconnected(_)) => false,
        }
    }

    fn enqueue(&self, bytes: InputRecord) -> bool {
        let dbg = broker_dbg_input();
        let n = bytes.len();
        match self.tx.try_send(bytes) {
            Ok(()) => {
                // Heal: input is flowing again — clear a prior backpressure stamp.
                if self.backpressure.swap(false, Ordering::AcqRel) {
                    self.stamp_backpressure(false);
                }
                if dbg {
                    eprintln!("BROKER_INPUT: enqueue ACCEPTED n={n}");
                }
                true
            }
            Err(TrySendError::Full(_)) => {
                // DROP + surface, but stamp only on the rising edge.
                if !self.backpressure.swap(true, Ordering::AcqRel) {
                    self.stamp_backpressure(true);
                }
                if dbg {
                    eprintln!("BROKER_INPUT: enqueue DROPPED (FIFO full, depth {}) n={n} -> INPUT_BACKPRESSURE", input_queue_depth());
                }
                false
            }
            // The writer thread is gone (session teardown) — nothing to do.
            Err(TrySendError::Disconnected(_)) => {
                if dbg {
                    eprintln!("BROKER_INPUT: enqueue DISCONNECTED n={n} (writer gone)");
                }
                false
            }
        }
    }

/// thread never blocks here, however stuck the harness is.

+                    eprintln!("BROKER_INPUT: enqueue ACCEPTED n={n}");

+                    eprintln!("BROKER_INPUT: enqueue DROPPED (FIFO full, depth {}) n={n} -> INPUT_BACKPRESSURE", input_queue_depth());

-            Err(TrySendError::Disconnected(_)) => false,

+            Err(TrySendError::Disconnected(_)) => {

+                    eprintln!("BROKER_INPUT: enqueue DISCONNECTED n={n} (writer gone)");


Add output-chunk volume log in the drain closure.


1416	                    }
1417	                }
1418	                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),
1419	                // An unknown kind is tolerated (forward-compat): report, don't die.
1420	                other => send_error(&send, &format!("unknown command kind {other:?}")),
1421	            }
1422	        }
1423
1424	        // Brain gone: detach as subscriber but leave the sessions running.
1425	        let sessions = self.sessions.lock().unwrap();
1426	        for id in &my_subs {
1427	            if let Some(h) = sessions.get(id) {
1428	                h.log.lock().unwrap().detach_if(&send);
1429	            }
1430	        }
1431	        drop(sessions);
1432	        // Same for net streams: the streams + their rings persist (D4b).
1433	        if let Some(host) = self.net.get() {
1434	            for id in &my_stream_subs {
1435	                host.detach_stream_sub(*id, &send);

                    }
                }
                KIND_BRAIN_RESTART => self.dispatch_brain_restart(&send),
                // An unknown kind is tolerated (forward-compat): report, don't die.
                other => send_error(&send, &format!("unknown command kind {other:?}")),
            }
        }

        // Brain gone: detach as subscriber but leave the sessions running.
        let sessions = self.sessions.lock().unwrap();
        for id in &my_subs {
            if let Some(h) = sessions.get(id) {
                h.log.lock().unwrap().detach_if(&send);
            }
        }
        drop(sessions);
        // Same for net streams: the streams + their rings persist (D4b).
        if let Some(host) = self.net.get() {
            for id in &my_stream_subs {
                host.detach_stream_sub(*id, &send);


crates\spt-daemon\src\broker.rs:1501:            .drain(move |chunk| {


1499	        let log_drain = Arc::clone(&log);
1500	        let drain = session
1501	            .drain(move |chunk| {
1502	                let job = log_drain.lock().unwrap().append(chunk);
1503	                if let Some(job) = job {
1504	                    if let Err(epoch) = job.deliver() {
1505	                        log_drain.lock().unwrap().mark_controller_gone(epoch);
1506	                    }
1507	                }
1508	            })
1509	            .map_err(|e| format!("drain failed: {e}"))?;
1510
1511	        // Exit waiter → one `exit` frame when the child is reaped, then the dead
1512	        // session is removed from the table. Runs on its own thread; `wait`

        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                let job = log_drain.lock().unwrap().append(chunk);
                if let Some(job) = job {
                    if let Err(epoch) = job.deliver() {
                        log_drain.lock().unwrap().mark_controller_gone(epoch);
                    }
                }
            })
            .map_err(|e| format!("drain failed: {e}"))?;

        // Exit waiter → one `exit` frame when the child is reaped, then the dead
        // session is removed from the table. Runs on its own thread; `wait`


Edit

        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                let job = log_drain.lock().unwrap().append(chunk);
                if let Some(job) = job {
                    if let Err(epoch) = job.deliver() {
                        log_drain.lock().unwrap().mark_controller_gone(epoch);
                    }
                }
            })
            .map_err(|e| format!("drain failed: {e}"))?;

        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                if broker_dbg_output() {
                    eprintln!("BROKER_OUTPUT: drain chunk n={} (session output)", chunk.len());
                }
                let job = log_drain.lock().unwrap().append(chunk);
                if let Some(job) = job {
                    if let Err(epoch) = job.deliver() {
                        if broker_dbg_output() {
                            eprintln!("BROKER_OUTPUT: drain mark_controller_gone epoch={epoch} (deliver evicted)");
                        }
                        log_drain.lock().unwrap().mark_controller_gone(epoch);
                    }
                }
            })
            .map_err(|e| format!("drain failed: {e}"))?;


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

        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                let job = log_drain.lock().unwrap().append(chunk);
                if let Some(job) = job {
                    if let Err(epoch) = job.deliver() {
                        log_drain.lock().unwrap().mark_controller_gone(epoch);
                    }
                }
            })
            .map_err(|e| format!("drain failed: {e}"))?;

        let log_drain = Arc::clone(&log);
        let drain = session
            .drain(move |chunk| {
                if broker_dbg_output() {
                    eprintln!("BROKER_OUTPUT: drain chunk n={} (session output)", chunk.len());
                }
                let job = log_drain.lock().unwrap().append(chunk);
                if let Some(job) = job {
                    if let Err(epoch) = job.deliver() {
                        if broker_dbg_output() {
                            eprintln!("BROKER_OUTPUT: drain mark_controller_gone epoch={epoch} (deliver evicted)");
                        }
                        log_drain.lock().unwrap().mark_controller_gone(epoch);
                    }
                }
            })
            .map_err(|e| format!("drain failed: {e}"))?;

+                if broker_dbg_output() {

+                    eprintln!("BROKER_OUTPUT: drain chunk n={} (session output)", chunk.len());

+                        if broker_dbg_output() {

+                            eprintln!("BROKER_OUTPUT: drain mark_controller_gone epoch={epoch} (deliver evicted)");


Build the diagnostic binary.


Bash

Build diagnostic spt binary


    |
288 | fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {
    |    ^^^^^^^^^^^^^^^
    |
    = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default

warning: `spt` (bin "spt") generated 1 warning
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 37.02s


Build OK (one dead-code warning: `clipboard_paste` now only used by tests since `traced_paste` inlines the read). Suppress cleanly for the diag.


Edit

// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {

// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
#[cfg_attr(not(test), allow(dead_code))] // diag branch: the event loop uses `traced_paste`; this stays the unit-tested decision kernel
fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {


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 clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {

// [impl->REQ-RC-WIN-PASTE]
#[cfg(windows)]
#[cfg_attr(not(test), allow(dead_code))] // diag branch: the event loop uses `traced_paste`; this stays the unit-tested decision kernel
fn clipboard_paste(read: impl FnOnce() -> Option<Vec<u8>>) -> Option<Vec<u8>> {

/// ⇒ a clean no-op (`None`) — never inject garbage, never panic.

+#[cfg_attr(not(test), allow(dead_code))] // diag branch: the event loop uses `traced_paste`; this stays the unit-tested decision kernel


Bash

cargo build -p spt 2>&1 | tail -4 && echo "===bin path===" && ls -la target/debug/spt.exe 2>&1 | tail -1

Rebuild diagnostic, confirm clean


   Compiling spt v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.84s
===bin path===
-rwxr-xr-x 2 decid 197609 56914432 Jun 19 20:42 target/debug/spt.exe


Clean diagnostic build. Commit to throwaway branch + surface to doyle with run instructions.


git add crates/spt/src/rc.rs crates/spt-daemon/src/broker.rs && git commit -F - <<'EOF'
diag(p1-ctrlv): throwaway instrumentation + SPT_RC_CTRLV isolation for the ctrl+V paste wedge

NOT a fix — confirm-first diagnostic for the v0.13.0 P1 ctrl+V-wedges-broker
re-open (HITL). Throwaway branch v0.13.0-p1-ctrlv-diag off f7cf380.

Isolation (rc.rs): SPT_RC_CTRLV=0 disables OUR ctrl+V interception arm, leaving
Windows Terminal to handle ctrl+V natively — probes whether the wedge persists
with our handler off.

rc-side trace (SPT_RC_DEBUG_PASTE): "CTRLV/RMOUSE arm fired", "read_clipboard
START/END len=N" (missing END = read blocked), "sent N bytes", "Event::Paste
len=N" (WT native-paste detection), ctrl+V-fell-through-to-key-path.

broker-side trace:
- SPT_BROKER_DEBUG_INPUT: dispatch_input "recv session/op/n", "enqueue-done +
  ack send START/END" (START w/o END = per-conn handler blocked on the rc socket),
  enqueue ACCEPTED/DROPPED(FIFO full, depth)/DISCONNECTED, input_writer
  write_input START/END (P0 PTY park).
- SPT_BROKER_DEBUG_OUTPUT: drain chunk volume, become_controller START/END,
  controller_writer CTRL_WRITE start/end + cumulative bytes (start w/o end =
  controller socket block), ControllerJob::deliver FULL/poll/DEADLINE-EVICT
  (channel depth), mark_controller_gone.

Pins, at the ~10-char wedge boundary: how many input frames arrived, FIFO
drops, output bytes written before stall, and WHICH call blocks (per-conn
handler ack vs controller socket vs PTY write).

Co-authored by: todlando
EOF

Commit diagnostic instrumentation


[v0.13.0-p1-ctrlv-diag b39e8a0] diag(p1-ctrlv): throwaway instrumentation + SPT_RC_CTRLV isolation for the ctrl+V paste wedge
 2 files changed, 158 insertions(+), 6 deletions(-)


Diagnostic committed @b39e8a0. Surface to doyle with run matrix.


$OWL send doyle <<'EOF'
DIAGNOSTIC BUILD READY — branch v0.13.0-p1-ctrlv-diag @b39e8a0 (throwaway, NOT the gate; off f7cf380). Built clean: target/debug/spt.exe (cargo build -p spt). Unpushed — say the word if you want it on origin for the operator, or relay these run instructions.

ENV MATRIX (operator runs daemon + rc with envs, pastes a LARGE clipboard string ~10+ chars via ctrl+V, captures BOTH daemon stderr and rc stderr):

Daemon (broker) side — set before launching the daemon:
  SPT_BROKER_DEBUG_INPUT=1   SPT_BROKER_DEBUG_OUTPUT=1

rc client side — set before `spt rc <ep>`:
  SPT_RC_DEBUG_PASTE=1
  (run A) SPT_RC_CTRLV unset/1  = our ctrl+V handler ON
  (run B) SPT_RC_CTRLV=0        = our handler OFF, WT handles ctrl+V natively  <-- ISOLATION probe

WHAT THE LOGS PIN:
- rc: "CTRLV arm fired" + "read_clipboard START/END len=" (missing END = read_clipboard blocks) + "sent N bytes" + "Event::Paste len=" (WT native paste). Run B: "ctrl+V fell through to key path".
- broker INPUT: "recv session/op/n" per frame (COUNT the frames) + enqueue ACCEPTED|DROPPED(depth 4096)|DISCONNECTED + "enqueue-done; ack send START" then "ack send END" (START WITHOUT END = the per-conn handler is blocked on the rc socket write = the wedge) + write_input START/END.
- broker OUTPUT: "drain chunk n=" (output volume) + CTRL_WRITE start/end + CUMULATIVE bytes (start WITHOUT end = controller socket blocked) + deliver FULL/DEADLINE-EVICT (controller channel depth 4096).

READS AT A GLANCE:
- Run B still wedges => NOT our handler (WT native ctrl+V flood is the trigger; our interception is innocent).
- Run B does NOT wedge => our ctrl+V arm is implicated.
- "ack send START" with no "END" => per-conn handler blocked writing the applied-ack back to a non-draining rc socket (input-side IPC backpressure).
- "CTRL_WRITE start" with no "end" => controller output socket blocked (output-side — your symmetric-P0-for-output hypothesis).
- enqueue DROPPED at ~10 => would contradict the 4096 default (shouldn't happen; if it does, an env/size override is in play).

Relay capture instructions to the operator; I'll analyze the stderr dump and we design the fix from the pinned call. Standing by.
EOF

Surface diagnostic build to doyle


➜ SENT:doyle


Diagnostic surfaced. Logging the investigation state.


TaskCreate

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.


Task #13 created successfully: P1 ctrl+V wedge — diagnose then fix

P1 ctrl+V wedge — diagnose then fix


Diagnostic delivered. Current state:

**P1 ctrl+V wedge re-opened (HITL).** doyle's refined read: ~10-char ctrl+V wedges, right-click fine → WT's ctrl+V paste-accelerator floods N per-char key events vs our single right-click bracketed frame.

**Built:** throwaway diagnostic @b39e8a0 (branch `v0.13.0-p1-ctrlv-diag` off f7cf380, clean build). Confirm-first, no guess-fix. Instruments:
- **Isolation:** `SPT_RC_CTRLV=0` disables our ctrl+V arm (does it still wedge with our handler off?).
- **rc:** arm-fired / read_clipboard START·END / sent-bytes / WT-native-Paste.
- **broker input:** dispatch_input recv→enqueue→**ack START/END** (START-without-END = per-conn handler blocked on rc socket) · enqueue ACCEPTED/DROPPED(depth 4096) · write_input START/END.
- **broker output:** drain volume · controller_writer CTRL_WRITE start/end + cumulative bytes · deliver FULL/DEADLINE-EVICT.

Surfaced to doyle with the full env matrix + read-at-a-glance guide; doyle relays capture instructions to the operator. **Code flag raised:** input FIFO default is 4096 (not ~10), and W1 already made the controller output write async — so the probe will distinguish whether the real block is the controller socket (doyle's symmetric-P0-for-output hypothesis) or a per-conn-handler/sessions-lock path.

Awaiting the operator stderr capture (via doyle) to pin the exact blocking call, then design + build the real fix on a gated branch. Standing by — reachable.


<task-notification>
<task-id>b4cwwiglf</task-id>
<summary>Monitor event: "« spt event »"</summary>
<event><EVENT-PART seq="1/2" id="68de7c90" type="msg" from="doyle">Probe LGTM — run-A/run-B isolation + START/END counts will pin it cleanly. PUSH the diag branch v0.13.0-p1-ctrlv-diag @b39e8a0 to origin so I can build+deploy it from a known commit in the spt-core-opbuild worktree (keeps it off your shared main tree). Confirm pushed + I'll deploy it to the operator's install path (af</EVENT-PART>
<EVENT-PART seq="2/2" id="68de7c90">ter reaping the wedged spt-core daemons) and relay the capture run. Standing by to deploy the moment it's on origin.</EVENT-PART></event>
</task-notification>


<owl_reachability_notice>
You have an active owl perch. When spawning subagents or doing work, use run_in_background: true so you stay reachable for incoming messages.
</owl_reachability_notice>


CAVEMAN MODE ACTIVE (full). Drop articles/filler/pleasantries/hedging. Fragments OK. Code/commits/security: write normal.


doyle wants the diag branch on origin. Pushing.


Bash

git push -u origin v0.13.0-p1-ctrlv-diag 2>&1 | tail -8

Push diagnostic branch to origin


remote:
remote: Create a pull request for 'v0.13.0-p1-ctrlv-diag' on GitHub by visiting:
remote:      https://github.com/SaberMage/spt-core/pull/new/v0.13.0-p1-ctrlv-diag
remote:
branch 'v0.13.0-p1-ctrlv-diag' set up to track 'origin/v0.13.0-p1-ctrlv-diag'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      v0.13.0-p1-ctrlv-diag -> v0.13.0-p1-ctrlv-diag


$OWL send doyle <<'EOF'
PUSHED — origin/v0.13.0-p1-ctrlv-diag @b39e8a0 (tracking set). Off f7cf380, isolated from main tree. Build: cargo build -p spt (clean; one dead_code on clipboard_paste already suppressed for the diag). Deploy it + relay the run matrix (run A: SPT_RC_CTRLV on / run B: =0; both with SPT_RC_DEBUG_PASTE + SPT_BROKER_DEBUG_INPUT + SPT_BROKER_DEBUG_OUTPUT). Reap the wedged daemons first. Standing by for the operator stderr capture — I'll analyze + design the fix from the pinned call.
EOF

Confirm push to doyle


Pushed `origin/v0.13.0-p1-ctrlv-diag @b39e8a0`, confirmed to doyle.

doyle now builds+deploys from the spt-core-opbuild worktree (off my shared tree), reaps the wedged daemons, and relays the run matrix to the operator (run A handler-on / run B `SPT_RC_CTRLV=0` handler-off, both with the three debug envs).

Awaiting the operator stderr capture via doyle — it'll pin the exact blocking call (per-conn handler ack vs controller socket vs PTY write) at the ~10-char boundary. Then I analyze + design/build the real fix on a gated branch. Standing by, reachable.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/6" id="8b07decc" type="msg" from="doyle">CAPTURE RESULT + PROBE GAP. Operator pressed ctrl+V (single-char non-wedge THEN long-string wedge) in BOTH run A (handler ON) and run B (handler OFF). RESULT:
- BOTH runs WEDGED on the long ctrl+V. =&gt; isolation says OUR ctrl+V handler is INNOCENT (off still wedges); WT's NATIVE ctrl+V paste is the trigger.
- rc</EVENT-PART>
<EVENT-PART seq="2/6" id="8b07decc">-A logged ONLY &quot;RMOUSE arm fired&quot; (a right-click), NEVER &quot;CTRLV arm fired&quot; — despite the operator pressing ctrl+V. =&gt; our is_ctrl_v arm NEVER FIRED. Most likely WT CONSUMES Ctrl+V (paste accelerator) so the key event never reaches our Event::Key arm; WT instead injects the clipboard as input.

PROBE WAS INSUFFICI</EVENT-PART>
<EVENT-PART seq="3/6" id="8b07decc">ENT (only logs on is_ctrl_v match, so &quot;invisible ctrl+V&quot; is ambiguous). ENHANCE — log EVERY event in spawn_stdin_reader_events, gated by SPT_RC_DEBUG_PASTE:
- EVERY Event: Key (code + modifiers + kind), Mouse (kind + button), Paste (len), Resize. So we SEE exactly what WT delivers when the operator hits ctrl+V on a ~15-char clip</EVENT-PART>
<EVENT-PART seq="4/6" id="8b07decc">board: nothing? a Key('v')+CTRL? a burst of Key char events (WT inject)? an Event::Paste?
- broker side: log the INPUT FRAME BURST — recv frames with a running count + a tight timestamp delta, so we see if WT's paste = N rapid key-event frames + WHERE the burst stalls (the existing ack/CTRL_WRITE START/END stays).

This capture will</EVENT-PART>
<EVENT-PART seq="5/6" id="8b07decc"> show whether WT injects the long paste as a key-event FLOOD (N frames) and exactly which broker call stops returning. Rebuild the diag (same branch v0.13.0-p1-ctrlv-diag, bump it), I'll redeploy + re-capture. The &quot;both wedge&quot; already tells us the FIX is NOT our handler — it's handling WT's native paste injection / the broker's res</EVENT-PART>
<EVENT-PART seq="6/6" id="8b07decc">ponse to it. Top priority.</EVENT-PART></event>
</task-notification>


Capture pins it: BOTH runs wedge → our handler innocent; our CTRLV arm NEVER fired → WT consumes ctrl+V (paste accelerator) and injects clipboard as input. Probe gap: only logged on is_ctrl_v match. Enhancing per doyle — log EVERY event + broker frame-burst count/timestamp.

rc.rs: log every event under SPT_RC_DEBUG_PASTE.


Edit

        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard

        let mut armed = false;
        while let Ok(ev) = event::read() {
            // DIAGNOSTIC: log EVERY event verbatim so an "invisible ctrl+V" (WT
            // consuming the accelerator + injecting the clipboard) is unambiguous —
            // we see exactly what WT delivers on ctrl+V of a ~15-char clipboard:
            // nothing, a Key('v')+CTRL, a burst of char Key events, or an Event::Paste.
            if dbg_paste {
                match &ev {
                    Event::Key(ke) => eprintln!(
                        "RC_EVENT: Key code={:?} mods={:?} kind={:?}",
                        ke.code, ke.modifiers, ke.kind
                    ),
                    Event::Mouse(me) => {
                        eprintln!("RC_EVENT: Mouse kind={:?} col={} row={}", me.kind, me.column, me.row)
                    }
                    Event::Paste(s) => eprintln!("RC_EVENT: Paste len={}", s.len()),
                    Event::Resize(c, r) => eprintln!("RC_EVENT: Resize {c}x{r}"),
                    other => eprintln!("RC_EVENT: {other:?}"),
                }
            }
            match ev {
                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard


        let mut armed = false;
        while let Ok(ev) = event::read() {
            match ev {
                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard

        let mut armed = false;
        while let Ok(ev) = event::read() {
            // DIAGNOSTIC: log EVERY event verbatim so an "invisible ctrl+V" (WT
            // consuming the accelerator + injecting the clipboard) is unambiguous —
            // we see exactly what WT delivers on ctrl+V of a ~15-char clipboard:
            // nothing, a Key('v')+CTRL, a burst of char Key events, or an Event::Paste.
            if dbg_paste {
                match &ev {
                    Event::Key(ke) => eprintln!(
                        "RC_EVENT: Key code={:?} mods={:?} kind={:?}",
                        ke.code, ke.modifiers, ke.kind
                    ),
                    Event::Mouse(me) => {
                        eprintln!("RC_EVENT: Mouse kind={:?} col={} row={}", me.kind, me.column, me.row)
                    }
                    Event::Paste(s) => eprintln!("RC_EVENT: Paste len={}", s.len()),
                    Event::Resize(c, r) => eprintln!("RC_EVENT: Resize {c}x{r}"),
                    other => eprintln!("RC_EVENT: {other:?}"),
                }
            }
            match ev {
                // ctrl+V paste gesture (REQ-RC-WIN-PASTE): read the LOCAL clipboard

+            // DIAGNOSTIC: log EVERY event verbatim so an "invisible ctrl+V" (WT

+            // consuming the accelerator + injecting the clipboard) is unambiguous —

+            // we see exactly what WT delivers on ctrl+V of a ~15-char clipboard:

+            // nothing, a Key('v')+CTRL, a burst of char Key events, or an Event::Paste.

+                    Event::Key(ke) => eprintln!(

+                        "RC_EVENT: Key code={:?} mods={:?} kind={:?}",

+                        ke.code, ke.modifiers, ke.kind

+                    Event::Mouse(me) => {

+                        eprintln!("RC_EVENT: Mouse kind={:?} col={} row={}", me.kind, me.column, me.row)

+                    Event::Paste(s) => eprintln!("RC_EVENT: Paste len={}", s.len()),

+                    Event::Resize(c, r) => eprintln!("RC_EVENT: Resize {c}x{r}"),

+                    other => eprintln!("RC_EVENT: {other:?}"),

// and inject a BRACKETED paste, BEFORE the detach SM / key


Now broker: frame-burst running count + timestamp. Add diag globals + enhance dispatch_input recv log.


fn broker_dbg_input() -> bool {
    std::env::var_os("SPT_BROKER_DEBUG_INPUT").is_some()
}

fn broker_dbg_input() -> bool {
    std::env::var_os("SPT_BROKER_DEBUG_INPUT").is_some()
}

/// DIAGNOSTIC: monotonic input-frame counter + a process-relative clock, so the
/// HITL capture shows whether WT's ctrl+V paste arrives as a BURST of N rapid
/// input frames and exactly where (which frame #) the burst stalls.
static DIAG_INPUT_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
static DIAG_CLOCK_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
fn diag_elapsed_ms() -> u128 {
    DIAG_CLOCK_START.get_or_init(Instant::now).elapsed().as_millis()
}


fn broker_dbg_input() -> bool {
    std::env::var_os("SPT_BROKER_DEBUG_INPUT").is_some()
}

/// DIAGNOSTIC: monotonic input-frame counter + a process-relative clock, so the
/// HITL capture shows whether WT's ctrl+V paste arrives as a BURST of N rapid
/// input frames and exactly where (which frame #) the burst stalls.
static DIAG_INPUT_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
static DIAG_CLOCK_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();
fn diag_elapsed_ms() -> u128 {
    DIAG_CLOCK_START.get_or_init(Instant::now).elapsed().as_millis()
}

+/// DIAGNOSTIC: monotonic input-frame counter + a process-relative clock, so the

+/// HITL capture shows whether WT's ctrl+V paste arrives as a BURST of N rapid

+/// input frames and exactly where (which frame #) the burst stalls.

+static DIAG_INPUT_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);

+static DIAG_CLOCK_START: std::sync::OnceLock<Instant> = std::sync::OnceLock::new();

+    DIAG_CLOCK_START.get_or_init(Instant::now).elapsed().as_millis()

/// DIAGNOSTIC (v0.13.0 P1 ctrl+V wedge, throwaway): `SPT_BROKER_DEBUG_OUTPUT` traces

/// the controller OUTPUT-write path (the flood suspect): drain volume, the bounded

/// `ControllerJob::deliver` (poll/evict), `become_controller`, and the


        if broker_dbg_input() {
            eprintln!(
                "BROKER_INPUT: recv session={} op={:?} n={}",
                req.session_id,
                req.op_id,
                bytes.len()
            );
        }

        if broker_dbg_input() {
            let c = DIAG_INPUT_FRAME_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
            eprintln!(
                "BROKER_INPUT: recv #{c} t={}ms session={} op={:?} n={}",
                diag_elapsed_ms(),
                req.session_id,
                req.op_id,
                bytes.len()
            );
        }


        if broker_dbg_input() {
            eprintln!(
                "BROKER_INPUT: recv session={} op={:?} n={}",
                req.session_id,
                req.op_id,
                bytes.len()
            );
        }

        if broker_dbg_input() {
            let c = DIAG_INPUT_FRAME_COUNT.fetch_add(1, Ordering::Relaxed) + 1;
            eprintln!(
                "BROKER_INPUT: recv #{c} t={}ms session={} op={:?} n={}",
                diag_elapsed_ms(),
                req.session_id,
                req.op_id,
                bytes.len()
            );
        }

serde_json::from_value(env.payload).map_err(|e| format!("bad input payload: {e}"))?;

let bytes = decode_bytes(&req.data_b64).map_err(|e| format!("bad input base64: {e}"))?;

+            let c = DIAG_INPUT_FRAME_COUNT.fetch_add(1, Ordering::Relaxed) + 1;

-                "BROKER_INPUT: recv session={} op={:?} n={}",

+                "BROKER_INPUT: recv #{c} t={}ms session={} op={:?} n={}",


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.


Add timestamps to ack + write_input + CTRL_WRITE for burst-stall correlation.


                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: enqueue-done op={op} outcome={outcome:?}; ack send START");
                }
                // Ack exactly-once: both Applied and Deduped mean "accepted + ordered".
                send_frame(
                    send,
                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
                );
                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: ack send END op={op}");
                }

                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: enqueue-done op={op} outcome={outcome:?}; ack send START t={}ms", diag_elapsed_ms());
                }
                // Ack exactly-once: both Applied and Deduped mean "accepted + ordered".
                send_frame(
                    send,
                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
                );
                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: ack send END op={op} t={}ms", diag_elapsed_ms());
                }


                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: enqueue-done op={op} outcome={outcome:?}; ack send START");
                }
                // Ack exactly-once: both Applied and Deduped mean "accepted + ordered".
                send_frame(
                    send,
                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
                );
                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: ack send END op={op}");
                }

                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: enqueue-done op={op} outcome={outcome:?}; ack send START t={}ms", diag_elapsed_ms());
                }
                // Ack exactly-once: both Applied and Deduped mean "accepted + ordered".
                send_frame(
                    send,
                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
                );
                if broker_dbg_input() {
                    eprintln!("BROKER_INPUT: ack send END op={op} t={}ms", diag_elapsed_ms());
                }

.map_err(|e| format!("input enqueue failed: {e}"))?;

-                    eprintln!("BROKER_INPUT: enqueue-done op={op} outcome={outcome:?}; ack send START");

+                    eprintln!("BROKER_INPUT: enqueue-done op={op} outcome={outcome:?}; ack send START t={}ms", diag_elapsed_ms());

-                    eprintln!("BROKER_INPUT: ack send END op={op}");

+                    eprintln!("BROKER_INPUT: ack send END op={op} t={}ms", diag_elapsed_ms());


        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input START n={}", bytes.len());
        }
        let _ = session.write_input(&bytes);
        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input END n={}", bytes.len());
        }

        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input START n={} t={}ms", bytes.len(), diag_elapsed_ms());
        }
        let _ = session.write_input(&bytes);
        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input END n={} t={}ms", bytes.len(), diag_elapsed_ms());
        }


        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input START n={}", bytes.len());
        }
        let _ = session.write_input(&bytes);
        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input END n={}", bytes.len());
        }

        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input START n={} t={}ms", bytes.len(), diag_elapsed_ms());
        }
        let _ = session.write_input(&bytes);
        if broker_dbg_input() {
            eprintln!("BROKER_INPUT: write_input END n={} t={}ms", bytes.len(), diag_elapsed_ms());
        }

fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {

-            eprintln!("BROKER_INPUT: write_input START n={}", bytes.len());

+            eprintln!("BROKER_INPUT: write_input START n={} t={}ms", bytes.len(), diag_elapsed_ms());

-            eprintln!("BROKER_INPUT: write_input END n={}", bytes.len());

+            eprintln!("BROKER_INPUT: write_input END n={} t={}ms", bytes.len(), diag_elapsed_ms());


        if dbg {
            eprintln!(
                "BROKER_OUTPUT: CTRL_WRITE start (live) sid={sid} seq={seq:?} total={total}"
            );
        }
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if dbg {
            eprintln!("BROKER_OUTPUT: CTRL_WRITE end (live) sid={sid} seq={seq:?} ok={ok} total={total}");
        }

        if dbg {
            eprintln!(
                "BROKER_OUTPUT: CTRL_WRITE start (live) sid={sid} seq={seq:?} total={total} t={}ms",
                diag_elapsed_ms()
            );
        }
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if dbg {
            eprintln!("BROKER_OUTPUT: CTRL_WRITE end (live) sid={sid} seq={seq:?} ok={ok} total={total} t={}ms", diag_elapsed_ms());
        }


        if dbg {
            eprintln!(
                "BROKER_OUTPUT: CTRL_WRITE start (live) sid={sid} seq={seq:?} total={total}"
            );
        }
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if dbg {
            eprintln!("BROKER_OUTPUT: CTRL_WRITE end (live) sid={sid} seq={seq:?} ok={ok} total={total}");
        }

        if dbg {
            eprintln!(
                "BROKER_OUTPUT: CTRL_WRITE start (live) sid={sid} seq={seq:?} total={total} t={}ms",
                diag_elapsed_ms()
            );
        }
        let ok = match send.lock() {
            Ok(mut s) => write_frame(&mut *s, frame).is_ok(),
            Err(_) => false,
        };
        if dbg {
            eprintln!("BROKER_OUTPUT: CTRL_WRITE end (live) sid={sid} seq={seq:?} ok={ok} total={total} t={}ms", diag_elapsed_ms());
        }

-                "BROKER_OUTPUT: CTRL_WRITE start (live) sid={sid} seq={seq:?} total={total}"

+                "BROKER_OUTPUT: CTRL_WRITE start (live) sid={sid} seq={seq:?} total={total} t={}ms",

-            eprintln!("BROKER_OUTPUT: CTRL_WRITE end (live) sid={sid} seq={seq:?} ok={ok} total={total}");

+            eprintln!("BROKER_OUTPUT: CTRL_WRITE end (live) sid={sid} seq={seq:?} ok={ok} total={total} t={}ms", diag_elapsed_ms());


Build the enhanced diag.


Bash

Build enhanced diagnostic


   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 26.53s


Clean. Commit (bump branch) + push.


git add crates/spt/src/rc.rs crates/spt-daemon/src/broker.rs && git commit -F - <<'EOF'
diag(p1-ctrlv): log EVERY rc event + broker input-frame burst count/clock

Capture showed BOTH run A (handler on) and run B (SPT_RC_CTRLV=0) wedge on a
long ctrl+V, and run A NEVER logged "CTRLV arm fired" — so our is_ctrl_v arm
never fired; WT consumes ctrl+V (paste accelerator) and injects the clipboard
as input. Our handler is innocent. Enhance the probe to SEE what WT delivers.

- rc.rs (SPT_RC_DEBUG_PASTE): RC_EVENT log of EVERY crossterm event verbatim —
  Key (code+mods+kind), Mouse (kind+col/row), Paste (len), Resize, other. So a
  ctrl+V on a ~15-char clipboard shows exactly: nothing, a Key('v')+CTRL, a burst
  of char Key events (WT inject), or an Event::Paste.
- broker.rs (SPT_BROKER_DEBUG_INPUT): dispatch_input recv now stamps a monotonic
  frame #N + a process-relative t=..ms (DIAG_INPUT_FRAME_COUNT / DIAG_CLOCK), and
  ack-START/END, write_input START/END, controller CTRL_WRITE start/end carry
  t=..ms — so the capture shows whether WT's paste = N rapid input frames and
  WHICH frame # / call timestamp the burst stalls on.

Same branch (bump). Confirm-first; still NOT a fix.

Co-authored by: todlando
EOF
git push 2>&1 | tail -4

Commit + push enhanced diag


[v0.13.0-p1-ctrlv-diag 4b5bf30] diag(p1-ctrlv): log EVERY rc event + broker input-frame burst count/clock
 2 files changed, 37 insertions(+), 7 deletions(-)
To https://github.com/SaberMage/spt-core.git
   b39e8a0..4b5bf30  v0.13.0-p1-ctrlv-diag -> v0.13.0-p1-ctrlv-diag


$OWL send doyle <<'EOF'
ENHANCED DIAG PUSHED — origin/v0.13.0-p1-ctrlv-diag @4b5bf30 (bumped, same branch). Rebuild+redeploy.

WHAT'S NEW:
- rc (SPT_RC_DEBUG_PASTE): "RC_EVENT:" logs EVERY crossterm event verbatim — Key(code,mods,kind) / Mouse(kind,col,row) / Paste(len) / Resize / other. So a ctrl+V on a ~15-char clipboard shows EXACTLY what WT delivers: nothing at all (WT swallowed it), a Key('v')+CTRL, a BURST of char Key events (WT inject), or an Event::Paste.
- broker (SPT_BROKER_DEBUG_INPUT): every recv now stamps "#N t=..ms" (monotonic frame count + process clock); ack-START/END, write_input START/END, CTRL_WRITE start/end all carry t=..ms. So we see if the paste = N rapid input frames and WHICH frame#/timestamp the burst stalls on.

SAME RUN MATRIX (operator, ctrl+V a ~15-char clipboard):
  daemon: SPT_BROKER_DEBUG_INPUT=1 SPT_BROKER_DEBUG_OUTPUT=1
  rc: SPT_RC_DEBUG_PASTE=1   (run B add SPT_RC_CTRLV=0, though isolation already cleared our handler — run A alone may suffice now)

KEY QUESTION THE CAPTURE ANSWERS: does WT inject the long paste as a FLOOD of Key char events (RC_EVENT Key spam → N broker recv #1..#N) and at which #N does ack-START-without-END (per-conn handler blocks) or CTRL_WRITE-start-without-end (controller socket blocks) appear? That pins the exact failing call + the burst size at the ~10-15 char boundary. Redeploy + recapture; I'll analyze.
EOF

Surface enhanced diag to doyle


➜ SENT:doyle


Updated task #13 status


Enhanced diag pushed `@4b5bf30` + surfaced to doyle.

**Capture so far pinned:** both runs wedge regardless of our handler (`SPT_RC_CTRLV=0` still wedges) → our ctrl+V interception is **innocent**; our CTRLV arm never even fired → **WT consumes ctrl+V** (paste accelerator) and injects the clipboard as input. The fix is in how the broker handles WT's injected paste, not our handler.

**Probe gap closed:** now logs every crossterm event verbatim (`RC_EVENT:`) + broker input-frame burst count + process clock (`#N t=..ms`) on recv/ack/write_input/CTRL_WRITE — so the next capture shows exactly *what* WT delivers (key-event flood? Event::Paste?) and *which frame#/call* the burst stalls on at the ~10-15 char boundary.

doyle redeploys + recaptures. Awaiting the dump to pin the exact failing call, then design the real fix. Standing by, reachable.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/8" id="768cda68" type="msg" from="doyle">ROOT PINNED — broker-A capture (enhanced diag, ctrl+V ~15-char &quot;Reaping +deploying&quot; on WT):

THE WEDGE = INPUT-ACK IPC BACKPRESSURE DEADLOCK (NOT P0, NOT output):
- 11 input frames recv. write_input START/END = 11/11 (PTY write FINE — P0 holds).
- ack send START=11 / END=10. Frame #11: recv -&g</EVENT-PART>
<EVENT-PART seq="2/8" id="768cda68">t; write_input START/END -&gt; &quot;ack send START&quot; op=...895 -&gt; NEVER &quot;ack send END&quot;. Then DISPATCH:3:Failed(&quot;The pipe is being closed os 232&quot;) when the tab closed.
- So the per-conn handler BLOCKS on send_frame(applied_envelope) — writing the applied-ack back over the brain&lt;-&gt;broker IPC conn. The brain</EVENT-PART>
<EVENT-PART seq="3/8" id="768cda68"> isn't draining acks (busy pushing the keystroke flood on the SAME conn) -&gt; mutual backpressure deadlock -&gt; permanent broker wedge + controller-stuck (handler can't process the detach). Same class as v0.12.1 L0 (two-conn split). ~10 threshold = the IPC pipe buffer filling.

ALSO (two SEPARATE rc-side facts from rc-A):
1. our is_</EVENT-PART>
<EVENT-PART seq="4/8" id="768cda68">ctrl_v arm is DEAD: WT delivers Ctrl+V as `Key code=Char('v') mods=CONTROL kind=RELEASE` ONLY — no Press. Our arm guards `kind==Press` -&gt; NEVER fires. Operator's &quot;not wired right&quot; = correct.
2. WT SWALLOWS ctrl+V and injects the clipboard as a CHAR-BY-CHAR KEY FLOOD (I see &quot;Reaping +deploying&quot; as individual Char Pre</EVENT-PART>
<EVENT-PART seq="5/8" id="768cda68">ss/Release). So we CANNOT intercept ctrl+V on WT (WT consumes it + injects regardless). Our ctrl+V handler is broken AND redundant.

FIX DIRECTION (your call to confirm + design):
- PRIMARY (ship-blocker): kill the input-ack backpressure deadlock. Candidate: SKIP the applied-ack for EPHEMERAL PtyWrite (the rc pump is fire-and-forward,</EVENT-PART>
<EVENT-PART seq="6/8" id="768cda68"> mints op_id for dedup only, never gates on the ack; PtyWrite has no replay so the exactly-once ack is informational + unused). No ack send -&gt; no per-conn-handler block -&gt; no deadlock. Alt: non-blocking/separate-conn ack (two-conn split for the ack direction). This fixes ANY input flood, not just ctrl+V.
- SECONDARY: REMOVE our ctrl+V</EVENT-PART>
<EVENT-PART seq="7/8" id="768cda68"> interception (broken Press-guard + redundant; WT handles ctrl+V natively). Keep right-click (works via mouse-capture). Once the broker no longer wedges on a flood, WT's native ctrl+V paste lands as keystrokes without wedging (multiline may submit-storm — acceptable / document; bracketed paste only achievable via right-click which we keep).<</EVENT-PART>
<EVENT-PART seq="8/8" id="768cda68">br>
Confirm the ack-send-block reading + design the primary fix. This is THE v0.13.0 ship-blocker. Logs at C:/Users/decid/Documents/projects/{broker,rc}-A.log if you want them.</EVENT-PART></event>
</task-notification>


doyle pinned it: **input-ack IPC backpressure deadlock**, not P0/output. Confirming against code before designing the fix — need to verify the applied-ack is truly unused (safe to skip for PtyWrite). Reading send_effect + the ack consumer.


Grep

fn send_effect|applied_envelope|KIND_APPLIED|BrokerEvent::Applied|Applied \{|fn read_event


crates\spt-daemon\src\brainproc.rs:478:        let _ = cache.record_applied_state(&AppliedRecord::Applied { version });
crates\spt-daemon\src\brainproc.rs:1401:            *self.state.lock().unwrap() = Some(AppliedRecord::Applied { version });
crates\spt-daemon\src\broker.rs:53:    applied_envelope, decode_bytes, displaced_envelope, endpoint_injected_envelope,
crates\spt-daemon\src\broker.rs:1690:    /// with an [`crate::msg::KIND_APPLIED`] frame so the brain can retire the
crates\spt-daemon\src\broker.rs:1762:                    &applied_envelope(req.session_id, op, outcome == Outcome::Applied),
crates\spt-daemon\src\brain.rs:44:    KIND_APPLIED, KIND_BRAIN_RESTART,
crates\spt-daemon\src\brain.rs:124:    Applied {
crates\spt-daemon\src\brain.rs:447:    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
crates\spt-daemon\src\brain.rs:449:    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
crates\spt-daemon\src\brain.rs:521:    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {
crates\spt-daemon\src\brain.rs:532:    pub fn read_event_until(&mut self, deadline: Option<Instant>) -> io::Result<BrokerEvent> {
crates\spt-daemon\src\brain.rs:631:                KIND_APPLIED => {
crates\spt-daemon\src\brain.rs:634:                    return Ok(BrokerEvent::Applied {
crates\spt-daemon\src\msg.rs:69:pub const KIND_APPLIED: &str = "applied";
crates\spt-daemon\src\msg.rs:89:/// distinguishes, mirroring [`KIND_APPLIED`]). Carries the broker-minted conn id.
crates\spt-daemon\src\msg.rs:317:    /// replies [`KIND_APPLIED`]. `None` is the legacy direct write (no dedup) —
crates\spt-daemon\src\msg.rs:766:pub fn applied_envelope(session_id: u64, op_id: u64, applied_now: bool) -> Envelope {
crates\spt-daemon\src\msg.rs:768:        KIND_APPLIED,
crates\spt-daemon\src\propagate.rs:607:    Applied { version: u64 },
crates\spt-daemon\src\propagate.rs:627:        matches!(self, ConvergeState::Applied { .. })
crates\spt-daemon\src\propagate.rs:650:            return ConvergeState::Applied { version: v };
crates\spt-daemon\src\propagate.rs:726:            ConvergeState::Applied { version: 7 }
crates\spt-daemon\src\propagate.rs:730:            ConvergeState::Applied { version: 9 },
crates\spt-daemon\src\propagate.rs:770:        assert!(ConvergeState::Applied { version: 7 }.converged());
crates\spt-daemon\src\relcache.rs:101:    Applied { version: u64 },
crates\spt-daemon\src\relcache.rs:558:        let applied = AppliedRecord::Applied { version: 7 };
crates\spt-daemon\src\rollback_compat.rs:130:        assert_additive_n1_readable(&AppliedRecord::Applied { version: 7 }, &["phase", "version"]);
crates\spt-daemon\src\shellchan.rs:210:                BrokerEvent::Applied { op_id, .. } if op_id == row_id as u64 => break,


440	    }
441
442	    /// Inject input **idempotently**: the write carries the durable `op_id` so a
443	    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
444	    /// `op_id` must come from a durable source and be reused verbatim on replay —
445	    /// the same logical effect must always carry the same id, so the broker's
446	    /// applied-set recognizes the re-drive. The broker answers with a
447	    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
448	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
449	    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
450	        let sid = self.require_session()?;
451	        self.send(
452	            KIND_INPUT,
453	            serde_json::to_value(InputReq {
454	                session_id: sid,
455	                data_b64: encode_bytes(bytes),
456	                op_id: Some(op_id),
457	            })
458	            .expect("InputReq serializes"),
459	        )
460	    }
461
462	    /// Resize the current session's PTY.
463	    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {
464	        let sid = self.require_session()?;
465	        self.send(
466	            KIND_RESIZE,
467	            serde_json::to_value(ResizeReq {
468	                session_id: sid,
469	                rows,
470	                cols,
471	            })
472	            .expect("ResizeReq serializes"),
473	        )
474	    }
475
476	    /// Terminate the current session's child.
477	    pub fn kill_session(&mut self) -> io::Result<()> {
478	        let sid = self.require_session()?;
479	        self.send(
480	            KIND_KILL,
481	            serde_json::to_value(KillReq { session_id: sid }).expect("KillReq serializes"),
482	        )
483	    }
484
485	    /// Read output events until the accumulated bytes contain `needle`,
486	    /// appending all received output to `buf`. Returns the highest seq seen.
487	    /// A session `Exit` before the needle is found is an error (the child died).
488	    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {
489	        loop {
490	            if contains(buf, needle) {
491	                return Ok(self.next_seq.saturating_sub(1));
492	            }
493	            match self.read_event()? {
494	                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),
495	                BrokerEvent::Exit { .. } => {
496	                    return Err(io::Error::new(
497	                        io::ErrorKind::UnexpectedEof,
498	                        "session exited before the expected output arrived",
499	                    ));
500	                }
501	                BrokerEvent::Error { message } => {
502	                    return Err(io::Error::other(message));
503	                }
504	                _ => {}
505	            }
506	        }
507	    }
508
509	    /// Read and demux one broker event, enforcing exactly-once contiguous output:
510	    /// accept the next `seq` (advancing the cursor), drop an already-seen `seq`
511	    /// (replay duplicate), and reject a forward gap (lost chunk).
512	    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call
513	    /// entry (a whole `loop { read_event }` shares it, so unrelated events
514	    /// dripping in cannot reset it — the total-wait bound). `None` when not in
515	    /// pump mode (blocking, unbounded).
516	    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]
517	    pub fn call_deadline(&self) -> Option<Instant> {
518	        self.io_timeout.map(|d| Instant::now() + d)
519	    }
520
521	    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {
522	        self.read_event_until(None)
523	    }
524
525	    /// Like [`Brain::read_event`] but each frame read is bounded by an optional
526	    /// wall-clock `deadline` (pump mode passes one; everyone else passes
527	    /// `None`). A frame that does not arrive before `deadline` surfaces
528	    /// [`io::ErrorKind::TimedOut`] — the pump escalates that to a SUPERVISED
529	    /// RESTART (a timed-out IPC read may have desynced the stream; the client

    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
            })
            .expect("InputReq serializes"),
        )
    }

    /// Resize the current session's PTY.
    pub fn resize(&mut self, rows: u16, cols: u16) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_RESIZE,
            serde_json::to_value(ResizeReq {
                session_id: sid,
                rows,
                cols,
            })
            .expect("ResizeReq serializes"),
        )
    }

    /// Terminate the current session's child.
    pub fn kill_session(&mut self) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_KILL,
            serde_json::to_value(KillReq { session_id: sid }).expect("KillReq serializes"),
        )
    }

    /// Read output events until the accumulated bytes contain `needle`,
    /// appending all received output to `buf`. Returns the highest seq seen.
    /// A session `Exit` before the needle is found is an error (the child died).
    pub fn read_output_until(&mut self, needle: &[u8], buf: &mut Vec<u8>) -> io::Result<u64> {
        loop {
            if contains(buf, needle) {
                return Ok(self.next_seq.saturating_sub(1));
            }
            match self.read_event()? {
                BrokerEvent::Output { bytes, .. } => buf.extend_from_slice(&bytes),
                BrokerEvent::Exit { .. } => {
                    return Err(io::Error::new(
                        io::ErrorKind::UnexpectedEof,
                        "session exited before the expected output arrived",
                    ));
                }
                BrokerEvent::Error { message } => {
                    return Err(io::Error::other(message));
                }
                _ => {}
            }
        }
    }

    /// Read and demux one broker event, enforcing exactly-once contiguous output:
    /// accept the next `seq` (advancing the cursor), drop an already-seen `seq`
    /// (replay duplicate), and reject a forward gap (lost chunk).
    /// The deadline for ONE IPC call: `now + io_timeout`, computed at call
    /// entry (a whole `loop { read_event }` shares it, so unrelated events
    /// dripping in cannot reset it — the total-wait bound). `None` when not in
    /// pump mode (blocking, unbounded).
    // [impl->REQ-HAZARD-PUMP-IPC-DEADLINE]
    pub fn call_deadline(&self) -> Option<Instant> {
        self.io_timeout.map(|d| Instant::now() + d)
    }

    pub fn read_event(&mut self) -> io::Result<BrokerEvent> {
        self.read_event_until(None)
    }

    /// Like [`Brain::read_event`] but each frame read is bounded by an optional
    /// wall-clock `deadline` (pump mode passes one; everyone else passes
    /// `None`). A frame that does not arrive before `deadline` surfaces
    /// [`io::ErrorKind::TimedOut`] — the pump escalates that to a SUPERVISED
    /// RESTART (a timed-out IPC read may have desynced the stream; the client


send_effect is fire-and-forget (no ack read); the deadlock is real. But `shellchan.rs:210` *waits* on `BrokerEvent::Applied` — so I can't drop the ack globally. Checking shellchan's flow to design a conditional ack.


185	    if rows.is_empty() {
186	        return Ok(0);
187	    }
188	    let label = shell_session_label(owner, shell_id);
189	    let mut brain =
190	        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
191	    let sessions = brain.sessions().map_err(|e| format!("sessions: {e}"))?;
192	    let sid = sessions
193	        .sessions
194	        .iter()
195	        .find(|s| s.endpoint == label)
196	        .map(|s| s.session_id)
197	        .ok_or_else(|| format!("no broker session for {label} (shell offline?)"))?;
198	    brain.attach(sid, 0).map_err(|e| format!("attach: {e}"))?;
199
200	    let mut delivered = 0usize;
201	    for (row_id, _from, body, _at) in rows {
202	        let mut bytes = body.into_bytes();
203	        bytes.push(b'\n');
204	        brain
205	            .send_effect(row_id as u64, &bytes)
206	            .map_err(|e| format!("send row {row_id}: {e}"))?;
207	        // Drain interleaved output until this row's exactly-once ack.
208	        loop {
209	            match brain.read_event().map_err(|e| format!("read: {e}"))? {
210	                BrokerEvent::Applied { op_id, .. } if op_id == row_id as u64 => break,
211	                BrokerEvent::Exit { .. } => return Err("shell binary exited mid-delivery".into()),
212	                BrokerEvent::Error { message } => return Err(format!("broker error: {message}")),
213	                _ => {}
214	            }
215	        }
216	        spt_store::spool::mark_delivered_at(&perch, &[row_id])
217	            .map_err(|e| format!("mark delivered: {e}"))?;
218	        delivered += 1;
219	    }

    if rows.is_empty() {
        return Ok(0);
    }
    let label = shell_session_label(owner, shell_id);
    let mut brain =
        Brain::cold_start(broker_name, now_ms()).map_err(|e| format!("broker connect: {e}"))?;
    let sessions = brain.sessions().map_err(|e| format!("sessions: {e}"))?;
    let sid = sessions
        .sessions
        .iter()
        .find(|s| s.endpoint == label)
        .map(|s| s.session_id)
        .ok_or_else(|| format!("no broker session for {label} (shell offline?)"))?;
    brain.attach(sid, 0).map_err(|e| format!("attach: {e}"))?;

    let mut delivered = 0usize;
    for (row_id, _from, body, _at) in rows {
        let mut bytes = body.into_bytes();
        bytes.push(b'\n');
        brain
            .send_effect(row_id as u64, &bytes)
            .map_err(|e| format!("send row {row_id}: {e}"))?;
        // Drain interleaved output until this row's exactly-once ack.
        loop {
            match brain.read_event().map_err(|e| format!("read: {e}"))? {
                BrokerEvent::Applied { op_id, .. } if op_id == row_id as u64 => break,
                BrokerEvent::Exit { .. } => return Err("shell binary exited mid-delivery".into()),
                BrokerEvent::Error { message } => return Err(format!("broker error: {message}")),
                _ => {}
            }
        }
        spt_store::spool::mark_delivered_at(&perch, &[row_id])
            .map_err(|e| format!("mark delivered: {e}"))?;
        delivered += 1;
    }


Confirmed against code. The deadlock is real and I can see exactly why:

- `send_effect` (brain.rs:449) is **fire-and-forget** — writes KIND_INPUT, never reads the ack.
- `serve_attach` processes a whole NetStreamData batch of N input records in its inner `for rec in decoder.push()` loop, calling `send_effect` N times **without returning to `read_event()`** — so the brain writes N frames back-to-back and drains nothing.
- broker per-conn handler: each KIND_INPUT → `dispatch_input` → `send_frame(applied_envelope)` back on the **same** brain↔broker conn. Brain not reading → broker→brain buffer fills (~10 frames) → `send_frame` blocks → handler stops reading → brain's writes block too → **mutual full-duplex deadlock**. Permanent; controller never released (handler can't process detach).
- `BrokerEvent::Applied` is consumed **only** by `shellchan.rs:210` (one-at-a-time, drains between, needs it). `serve_attach` ignores it → the operator-path ack is written-then-discarded. rc never sees it.

Design (primary + secondary) to doyle, then implement on confirm.


Bash

$OWL send doyle <<'EOF'
CONFIRMED the ack-send-block reading against code — your RCA is exactly right:

- send_effect (brain.rs:449) is FIRE-AND-FORGET (writes KIND_INPUT, never reads the ack).
- serve_attach processes a whole NetStreamData batch in its inner `for rec in decoder.push()` loop, calling send_effect N times WITHOUT returning to read_event() — brain writes N frames, drains nothing.
- broker per-conn handler: each KIND_INPUT -> dispatch_input -> send_frame(applied_envelope) on the SAME brain<->broker conn -> brain not reading -> broker->brain buffer fills (~10) -> send_frame BLOCKS -> handler stops reading -> brain writes block -> full-duplex deadlock. Permanent, controller stuck (handler can't process detach). ~10 = IPC pipe buffer. v0.12.1 L0 class. CONFIRMED.
- BrokerEvent::Applied consumed ONLY by shellchan.rs:210 (one-at-a-time, drains between, NEEDS it). serve_attach IGNORES it. rc never sees it. So the operator-path ack is written-then-discarded.

FIX DESIGN:

PRIMARY (ship-blocker) — CONDITIONAL ACK (your skip-the-ack, made shellchan-safe):
- shellchan ALSO uses send_effect + WAITS on Applied (load-bearing) — so I can't drop the ack globally. Make it opt-in: add `ack: bool` to InputReq (serde default=true => N-1/back-compat: old callers + shellchan keep acking). serve_attach's operator path calls a no-ack variant (send_effect_no_ack -> InputReq{ack:false}); dispatch_input only send_frame(applied) when req.ack. shellchan unchanged (ack=true).
- Why it cures it: with no ack, the per-conn handler NEVER writes the brain conn while servicing operator input -> never blocks -> drains all N KIND_INPUT -> brain writes never block -> brain returns to read_event -> drains output. Controller output (controller_writer, separate thread) may transiently fill but unblocks the instant the brain reads; bounded by CONTROLLER_WRITE_DEADLINE. No permanent wedge. Fixes ANY input flood, not just ctrl+V.
- Exactly-once PRESERVED: op_id still dedups at apply_once; the ack was only informational on the operator path.
- N-1 caveat: an OLD resident broker (self-update window) ignores ack=false -> still acks -> deadlock persists until broker restart (inherent KH 7.9 broker-resident-wire-change; the fix is broker-side). Note in KNOWN-HAZARDS.

SECONDARY — REMOVE our ctrl+V interception (your call, confirmed dead): WT delivers Ctrl+V as Key kind=RELEASE only (our Press guard never matches) AND swallows+injects the clipboard as a char-by-char Key flood. So is_ctrl_v is dead + redundant. Remove the ctrl+V arm + is_ctrl_v + its unit; KEEP right-click (mouse-capture -> our bracketed paste, works). Document: ctrl+V on Windows = WT-native paste landing as keystrokes (multiline may submit-storm; bracketed paste only via right-click). Update KNOWN-HAZARDS 7.18 + REQ-RC-WIN-PASTE scope.

NEW REQ: mint REQ-HAZARD-INPUT-ACK-BACKPRESSURE (stages impl,unit,int) for the primary — distinct hazard from P0 (PTY park) and W1 (output drain). int = repro-first: a flood of journaled operator-input frames must NOT wedge the broker (a concurrent attach stays serviced + the controller detach still registers). REQ-RC-WIN-PASTE gets the secondary (ctrl+V removal + doc).

Confirm the design (esp. conditional-ack vs your skip, + minting REQ-HAZARD-INPUT-ACK-BACKPRESSURE) and I build on a fresh gated branch off f7cf380. This is THE ship-blocker — ready to implement on your nod.
EOF

Send confirmed RCA + fix design to doyle


Design sent to doyle, awaiting confirmation before building.

**Root confirmed (input-ack IPC backpressure deadlock):**
- `dispatch_input` writes the applied-ack back on the same brain↔broker conn the brain is flooding with KIND_INPUT → both directions fill (~10 frames = pipe buffer) → mutual deadlock → permanent broker wedge. P0 holds (write_input 11/11); the ack is the block (START 11 / END 10).
- `Applied` is load-bearing only for `shellchan` (waits on it); `serve_attach` discards it.

**Proposed fix:**
- **Primary** (ship-blocker): conditional ack — add `ack: bool` to `InputReq` (default true, N-1 safe), operator path sends no-ack, shellchan keeps it. No ack write → per-conn handler never blocks → deadlock gone. Fixes any input flood. Exactly-once preserved (op_id dedups at the journal regardless). New REQ `REQ-HAZARD-INPUT-ACK-BACKPRESSURE`.
- **Secondary**: remove the dead ctrl+V interception (WT delivers only kind=RELEASE → our Press guard never fires; WT injects clipboard as a key flood anyway). Keep right-click bracketed paste. Document ctrl+V = WT-native keystrokes (may submit-storm; bracketed only via right-click). Under REQ-RC-WIN-PASTE.

Ready to implement on a fresh gated branch off f7cf380 the moment doyle confirms the design (esp. conditional-ack approach + minting the new REQ). Standing by.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/7" id="899635e0" type="msg" from="doyle">DESIGN APPROVED — conditional-ack is clean, targeted, and preserves the two load-bearing invariants (shellchan ack-wait + exactly-once op_id dedup). Build it. This is the real ship-blocker fix (cures ANY input-flood deadlock, not just ctrl+V).

GATE CRITERIA (mine):
1. REPRO-FIRST int (non-negotiable, the key</EVENT-PART>
<EVENT-PART seq="2/7" id="899635e0">stone): a flood of N operator input frames through serve_attach on ONE conn where N &gt; the IPC pipe buffer (~10+, make it deterministic e.g. 64) -&gt; PRE-FIX deadlocks (ack-send-START-without-END / handler stops draining; assert a concurrent attach or the detach can't be serviced) -&gt; POST-FIX drains all N + the session stays live + a con</EVENT-PART>
<EVENT-PART seq="3/7" id="899635e0">current attach still opens. Real broker+brain, NO mocks. This is the exact gate the wedge needed.
2. shellchan PATH UNCHANGED: a shellchan send_effect still acks (ack=true) and shellchan.rs:210 still receives BrokerEvent::Applied (its one-at-a-time drain). Add/keep a unit proving ack=true still emits the applied frame and ack=false does NOT</EVENT-PART>
<EVENT-PART seq="4/7" id="899635e0">.
3. Exactly-once: a replayed/duplicate op_id on the no-ack path still dedups at apply_once (no double PTY write) — unit.
4. N-1 caveat: KNOWN-HAZARDS note that an OLD resident broker ignores ack=false (deadlock persists until a broker restart — KH 7.9 broker-resident-wire-change class). InputReq.ack serde-default=true so an old brok</EVENT-PART>
<EVENT-PART seq="5/7" id="899635e0">er + new brain, and new broker + old brain, both stay safe.
5. SECONDARY (same branch): REMOVE the is_ctrl_v arm + its units (dead Press-guard + redundant — WT swallows ctrl+V Release-only and injects the clipboard as a key flood we can't intercept). KEEP right-click (mouse-capture path) + EnableMouseCapture. Revise REQ-RC-WIN-PASTE scope</EVENT-PART>
<EVENT-PART seq="6/7" id="899635e0"> to right-click-only + the KNOWN-HAZARDS entry (ctrl+V rides WT-native paste; the broker fix makes it non-wedging; bracketed fidelity = right-click only). Consider dropping Mouse::Moved early (267-event noise in the capture) — minor, your call.
6. REQ: mint REQ-HAZARD-INPUT-ACK-BACKPRESSURE (or similar) for the deadlock [impl,unit,int]; a</EVENT-PART>
<EVENT-PART seq="7/7" id="899635e0">mend REQ-RC-WIN-PASTE. clippy/traceable/docs-drift.

Surface green for gate. Then re-deploy + operator HITL (ctrl+V large = no wedge; right-click = bracketed paste; both no broker wedge). Excellent root-cause work — this is a deeper bug than P1, well caught.</EVENT-PART></event>
</task-notification>


Design approved. Building on a fresh gated branch off f7cf380 (clean base, no diag spam). Let me create it and set up tasks.


git checkout -b v0.13.0-p1b-ack-deadlock f7cf380 2>&1 | tail -2 && git --no-pager log --oneline -2

Branch the fix off clean f7cf380


Switched to a new branch 'v0.13.0-p1b-ack-deadlock'
f7cf380 feat(rc): Windows ctrl+V + right-click paste via local clipboard → bracketed paste (REQ-RC-WIN-PASTE)
8b36b03 docs(harness-contract): publish [message-idle-translation-binary] + checklist both seams


P1 ctrl+V wedge — diagnose then fix


TaskCreate

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


Task #14 created successfully: Mint REQ-HAZARD-INPUT-ACK-BACKPRESSURE + amend REQ-RC-WIN-PASTE


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.


Task #15 created successfully: Primary fix: conditional ack

Primary fix: conditional ack


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.


Task #16 created successfully: Secondary: remove dead ctrl+V interception


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.


Task #17 created successfully: Docs: KNOWN-HAZARDS deadlock + amend 7.18


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.


Task #18 created successfully: Tests (spt-test-engineer) + gates + commit + doyle


Task 14 — mint REQ + amend. Read InputReq struct + registry P1 region.


300	/// (REQ-RCVIEW-1).
301	#[derive(Debug, Clone, Serialize, Deserialize)]
302	pub struct UnsubscribeReq {
303	    /// Target session.
304	    pub session_id: u64,
305	}
306
307	/// `input` payload — bytes to inject into a session's stdin (base64).
308	#[derive(Debug, Clone, Serialize, Deserialize)]
309	pub struct InputReq {
310	    /// Target session.
311	    pub session_id: u64,
312	    /// The raw input bytes, base64-encoded.
313	    pub data_b64: String,
314	    /// The durable op id for exactly-once delivery (B5). `Some(id)` routes the
315	    /// write through the broker's [`crate::effect::EffectJournal`]: a replay of
316	    /// the same `(session_id, id)` after a brain crash is deduped, and the broker
317	    /// replies [`KIND_APPLIED`]. `None` is the legacy direct write (no dedup) —
318	    /// kept for callers that don't carry a durable source id. Defaulted so an
319	    /// older brain's `input` (no `op_id`) still decodes (2.3 forward-compat).
320	    #[serde(default, skip_serializing_if = "Option::is_none")]
321	    pub op_id: Option<u64>,
322	}
323
324	/// `resize` payload.
325	#[derive(Debug, Clone, Serialize, Deserialize)]
326	pub struct ResizeReq {
327	    /// Target session.
328	    pub session_id: u64,
329	    /// New rows.
330	    pub rows: u16,
331	    /// New cols.
332	    pub cols: u16,
333	}
334

/// (REQ-RCVIEW-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnsubscribeReq {
    /// Target session.
    pub session_id: u64,
}

/// `input` payload — bytes to inject into a session's stdin (base64).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputReq {
    /// Target session.
    pub session_id: u64,
    /// The raw input bytes, base64-encoded.
    pub data_b64: String,
    /// The durable op id for exactly-once delivery (B5). `Some(id)` routes the
    /// write through the broker's [`crate::effect::EffectJournal`]: a replay of
    /// the same `(session_id, id)` after a brain crash is deduped, and the broker
    /// replies [`KIND_APPLIED`]. `None` is the legacy direct write (no dedup) —
    /// kept for callers that don't carry a durable source id. Defaulted so an
    /// older brain's `input` (no `op_id`) still decodes (2.3 forward-compat).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op_id: Option<u64>,
}

/// `resize` payload.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResizeReq {
    /// Target session.
    pub session_id: u64,
    /// New rows.
    pub rows: u16,
    /// New cols.
    pub cols: u16,
}


crates\spt-daemon\src\msg.rs:309:pub struct InputReq {
crates\spt-daemon\src\msg.rs:518:    /// dedup, no ack) — mirrors [`InputReq::op_id`].
crates\spt-daemon\src\msg.rs:620:/// hosts `endpoint` (REQ-SEND-SPT-HOSTED). Base64 like [`InputReq`].
crates\spt-daemon\src\msg.rs:622:pub struct EndpointInputReq {
crates\spt-daemon\src\msg.rs:629:/// `endpoint-injected` reply payload — the outcome of an [`EndpointInputReq`].
crates\spt-daemon\src\msg.rs:938:        let req = EndpointInputReq {
crates\spt-daemon\src\msg.rs:942:        let back: EndpointInputReq =
crates\spt-daemon\src\broker.rs:55:    size_envelope, subscribed_envelope, BrainRestarted, EndpointInputReq,
crates\spt-daemon\src\broker.rs:56:    ErrorEvent, ExitEvent, InputReq, KillReq, NetDialReq, NetPresenceSubscribeReq, NetSent,
crates\spt-daemon\src\broker.rs:1590:        let req: InputReq =
crates\spt-daemon\src\broker.rs:1676:        let req: EndpointInputReq = serde_json::from_value(env.payload)
crates\spt-daemon\src\brain.rs:38:    EndpointInputReq, ErrorEvent, ExitEvent,
crates\spt-daemon\src\brain.rs:39:    InputReq,
crates\spt-daemon\src\brain.rs:433:            serde_json::to_value(InputReq {
crates\spt-daemon\src\brain.rs:438:            .expect("InputReq serializes"),
crates\spt-daemon\src\brain.rs:453:            serde_json::to_value(InputReq {
crates\spt-daemon\src\brain.rs:458:            .expect("InputReq serializes"),
crates\spt-daemon\src\brain.rs:1022:            serde_json::to_value(EndpointInputReq {
crates\spt-daemon\src\brain.rs:1026:            .expect("EndpointInputReq serializes"),
crates\spt\tests\brain_survive.rs:73:use spt_daemon::msg::{encode_bytes, InputReq, SpawnReq, KIND_INPUT};
crates\spt\tests\brain_survive.rs:358:        let input = InputReq {
crates\spt\tests\brain_survive.rs:365:            &Envelope::new(KIND_INPUT, serde_json::to_value(input).expect("InputReq serializes")),
crates\spt-daemon\tests\broker.rs:28:    decode_bytes, encode_bytes, EndpointInjected, EndpointInputReq, ExitEvent, InputReq, KillReq,
crates\spt-daemon\tests\broker.rs:118:        serde_json::to_value(InputReq {
crates\spt-daemon\tests\broker.rs:231:        serde_json::to_value(EndpointInputReq {
crates\spt-daemon\tests\broker.rs:270:        serde_json::to_value(EndpointInputReq {
crates\spt-daemon\tests\broker.rs:602:        serde_json::to_value(EndpointInputReq {
crates\spt-daemon\tests\inject_control_wedge.rs:50:    encode_bytes, EndpointInputReq, SpawnReq, Spawned, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT,
crates\spt-daemon\tests\inject_control_wedge.rs:297:            serde_json::to_value(EndpointInputReq {
crates\spt-daemon\tests\inject_control_wedge.rs:879:            serde_json::to_value(EndpointInputReq {
traceable-reqs.toml:553:[Omitted long matching line]


420	                _ => continue,
421	            }
422	        }
423	    }
424
425	    /// Inject input bytes into the current session (send-keys), the **legacy
426	    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery
427	    /// of an effect that originates from a durable source (a spool row, a
428	    /// sequenced injection), use [`Brain::send_effect`].
429	    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
430	        let sid = self.require_session()?;
431	        self.send(
432	            KIND_INPUT,
433	            serde_json::to_value(InputReq {
434	                session_id: sid,
435	                data_b64: encode_bytes(bytes),
436	                op_id: None,
437	            })
438	            .expect("InputReq serializes"),
439	        )
440	    }
441
442	    /// Inject input **idempotently**: the write carries the durable `op_id` so a
443	    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
444	    /// `op_id` must come from a durable source and be reused verbatim on replay —
445	    /// the same logical effect must always carry the same id, so the broker's
446	    /// applied-set recognizes the re-drive. The broker answers with a
447	    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
448	    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
449	    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
450	        let sid = self.require_session()?;
451	        self.send(
452	            KIND_INPUT,
453	            serde_json::to_value(InputReq {
454	                session_id: sid,
455	                data_b64: encode_bytes(bytes),
456	                op_id: Some(op_id),
457	            })
458	            .expect("InputReq serializes"),
459	        )
460	    }
461

                _ => continue,
            }
        }
    }

    /// Inject input bytes into the current session (send-keys), the **legacy
    /// direct** path: no durable id, no dedup, no ack. For exactly-once delivery
    /// of an effect that originates from a durable source (a spool row, a
    /// sequenced injection), use [`Brain::send_effect`].
    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: None,
            })
            .expect("InputReq serializes"),
        )
    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
            })
            .expect("InputReq serializes"),
        )
    }


Now implement primary. msg.rs — add `ack` field + default.


Edit

    /// older brain's `input` (no `op_id`) still decodes (2.3 forward-compat).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op_id: Option<u64>,
}

    /// older brain's `input` (no `op_id`) still decodes (2.3 forward-compat).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op_id: Option<u64>,
    /// Whether the broker should reply a [`KIND_APPLIED`] ack for this op
    /// (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). `true` — the ack is sent (the
    /// `shellchan` one-at-a-time drain WAITS on it). `false` — NO ack frame is
    /// written back, so a FLOOD of operator input on one brain↔broker conn cannot
    /// fill the return direction and deadlock the per-conn handler (the rc/operator
    /// path is fire-and-forward — `op_id` is for dedup only, it never gates on the
    /// ack). Exactly-once is unaffected: dedup happens at the broker's applied-set
    /// regardless of whether the ack is emitted. `default = true` so an OLDER
    /// brain's `input` (no `ack` field) still acks — today's behavior, N-1-safe.
    #[serde(default = "default_true")]
    pub ack: bool,
}

/// serde default for [`InputReq::ack`] — absent on the wire ⇒ `true` (the
/// pre-REQ-HAZARD-INPUT-ACK-BACKPRESSURE behavior: every `input` was acked).
fn default_true() -> bool {
    true
}


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

    /// older brain's `input` (no `op_id`) still decodes (2.3 forward-compat).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op_id: Option<u64>,
}

    /// older brain's `input` (no `op_id`) still decodes (2.3 forward-compat).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub op_id: Option<u64>,
    /// Whether the broker should reply a [`KIND_APPLIED`] ack for this op
    /// (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). `true` — the ack is sent (the
    /// `shellchan` one-at-a-time drain WAITS on it). `false` — NO ack frame is
    /// written back, so a FLOOD of operator input on one brain↔broker conn cannot
    /// fill the return direction and deadlock the per-conn handler (the rc/operator
    /// path is fire-and-forward — `op_id` is for dedup only, it never gates on the
    /// ack). Exactly-once is unaffected: dedup happens at the broker's applied-set
    /// regardless of whether the ack is emitted. `default = true` so an OLDER
    /// brain's `input` (no `ack` field) still acks — today's behavior, N-1-safe.
    #[serde(default = "default_true")]
    pub ack: bool,
}

/// serde default for [`InputReq::ack`] — absent on the wire ⇒ `true` (the
/// pre-REQ-HAZARD-INPUT-ACK-BACKPRESSURE behavior: every `input` was acked).
fn default_true() -> bool {
    true
}

+    /// Whether the broker should reply a [`KIND_APPLIED`] ack for this op

+    /// (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). `true` — the ack is sent (the

+    /// `shellchan` one-at-a-time drain WAITS on it). `false` — NO ack frame is

+    /// written back, so a FLOOD of operator input on one brain↔broker conn cannot

+    /// fill the return direction and deadlock the per-conn handler (the rc/operator

+    /// path is fire-and-forward — `op_id` is for dedup only, it never gates on the

+    /// ack). Exactly-once is unaffected: dedup happens at the broker's applied-set

+    /// regardless of whether the ack is emitted. `default = true` so an OLDER

+    /// brain's `input` (no `ack` field) still acks — today's behavior, N-1-safe.

+/// serde default for [`InputReq::ack`] — absent on the wire ⇒ `true` (the

+/// pre-REQ-HAZARD-INPUT-ACK-BACKPRESSURE behavior: every `input` was acked).

#[derive(Debug, Clone, Serialize, Deserialize)]


Now brain.rs — update both constructors + add `send_effect_no_ack`.


    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: None,
            })
            .expect("InputReq serializes"),
        )
    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
            })
            .expect("InputReq serializes"),
        )
    }

    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: None,
                ack: true,
            })
            .expect("InputReq serializes"),
        )
    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    ///
    /// ACKED (`ack=true`): the caller waits on the [`BrokerEvent::Applied`] one at a
    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward
    /// operator/rc path — which never reads the ack and can send a FLOOD on one
    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure
    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        self.send_effect_inner(op_id, bytes, true)
    }

    /// Inject input idempotently like [`Brain::send_effect`] but request NO
    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc
    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is
    /// discarded by `serve_attach` — and it can flood N input frames on ONE
    /// brain↔broker conn without returning to read. With no ack the broker's
    /// per-conn handler never writes back while servicing that flood, so the return
    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the
    /// broker still dedups by `(session, op_id)` at its applied-set.
    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]
    pub fn send_effect_no_ack(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        self.send_effect_inner(op_id, bytes, false)
    }

    /// Shared body for [`Brain::send_effect`] / [`Brain::send_effect_no_ack`]: the
    /// only difference is whether the broker replies a `KIND_APPLIED` ack.
    fn send_effect_inner(&mut self, op_id: u64, bytes: &[u8], ack: bool) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
                ack,
            })
            .expect("InputReq serializes"),
        )
    }


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

    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: None,
            })
            .expect("InputReq serializes"),
        )
    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
            })
            .expect("InputReq serializes"),
        )
    }

    pub fn send_input(&mut self, bytes: &[u8]) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: None,
                ack: true,
            })
            .expect("InputReq serializes"),
        )
    }

    /// Inject input **idempotently**: the write carries the durable `op_id` so a
    /// replay after a brain crash is deduped at the broker (exactly-once, B5).
    /// `op_id` must come from a durable source and be reused verbatim on replay —
    /// the same logical effect must always carry the same id, so the broker's
    /// applied-set recognizes the re-drive. The broker answers with a
    /// [`BrokerEvent::Applied`] for `(session, op_id)`.
    ///
    /// ACKED (`ack=true`): the caller waits on the [`BrokerEvent::Applied`] one at a
    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward
    /// operator/rc path — which never reads the ack and can send a FLOOD on one
    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure
    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).
    // [impl->REQ-HAZARD-RESTART-IDEMPOTENT]
    pub fn send_effect(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        self.send_effect_inner(op_id, bytes, true)
    }

    /// Inject input idempotently like [`Brain::send_effect`] but request NO
    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc
    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is
    /// discarded by `serve_attach` — and it can flood N input frames on ONE
    /// brain↔broker conn without returning to read. With no ack the broker's
    /// per-conn handler never writes back while servicing that flood, so the return
    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the
    /// broker still dedups by `(session, op_id)` at its applied-set.
    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]
    pub fn send_effect_no_ack(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {
        self.send_effect_inner(op_id, bytes, false)
    }

    /// Shared body for [`Brain::send_effect`] / [`Brain::send_effect_no_ack`]: the
    /// only difference is whether the broker replies a `KIND_APPLIED` ack.
    fn send_effect_inner(&mut self, op_id: u64, bytes: &[u8], ack: bool) -> io::Result<()> {
        let sid = self.require_session()?;
        self.send(
            KIND_INPUT,
            serde_json::to_value(InputReq {
                session_id: sid,
                data_b64: encode_bytes(bytes),
                op_id: Some(op_id),
                ack,
            })
            .expect("InputReq serializes"),
        )
    }

+    /// ACKED (`ack=true`): the caller waits on the [`BrokerEvent::Applied`] one at a

+    /// time (the `shellchan` spool-delivery drain). For the fire-and-forward

+    /// operator/rc path — which never reads the ack and can send a FLOOD on one

+    /// conn — use [`Brain::send_effect_no_ack`] to avoid the ack-backpressure

+    /// deadlock (REQ-HAZARD-INPUT-ACK-BACKPRESSURE).

+        self.send_effect_inner(op_id, bytes, true)

+    /// Inject input idempotently like [`Brain::send_effect`] but request NO

+    /// [`KIND_APPLIED`] ack (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). The operator/rc

+    /// attach path is fire-and-forward — `op_id` is for dedup only and the ack is

+    /// discarded by `serve_attach` — and it can flood N input frames on ONE

+    /// brain↔broker conn without returning to read. With no ack the broker's

+    /// per-conn handler never writes back while servicing that flood, so the return

+    /// direction can't fill and deadlock the handler. Exactly-once is preserved: the

+    /// broker still dedups by `(session, op_id)` at its applied-set.

+    // [impl->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]

+    pub fn send_effect_no_ack(&mut self, op_id: u64, bytes: &[u8]) -> io::Result<()> {

+        self.send_effect_inner(op_id, bytes, false)

+    /// Shared body for [`Brain::send_effect`] / [`Brain::send_effect_no_ack`]: the

+    /// only difference is whether the broker replies a `KIND_APPLIED` ack.

+    fn send_effect_inner(&mut self, op_id: u64, bytes: &[u8], ack: bool) -> io::Result<()> {


Now serve_attach → use `send_effect_no_ack` for operator input.


                            match role {
                                // Confirmed controller — exactly-once at the PTY
                                // (the operator-minted id rides the B5 journal).
                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,

                            match role {
                                // Confirmed controller — exactly-once at the PTY
                                // (the operator-minted id rides the B5 journal). NO-ACK
                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is
                                // fire-and-forward (the ack is discarded here) and can
                                // flood N input frames on this one conn; an acked write
                                // back would fill the return direction and deadlock the
                                // broker's per-conn handler. Dedup still holds at the
                                // broker (op_id applied-set).
                                Some(ServeRole::Controller) => {
                                    brain.send_effect_no_ack(op_id, &bytes)?
                                }


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

                            match role {
                                // Confirmed controller — exactly-once at the PTY
                                // (the operator-minted id rides the B5 journal).
                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,

                            match role {
                                // Confirmed controller — exactly-once at the PTY
                                // (the operator-minted id rides the B5 journal). NO-ACK
                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is
                                // fire-and-forward (the ack is discarded here) and can
                                // flood N input frames on this one conn; an acked write
                                // back would fill the return direction and deadlock the
                                // broker's per-conn handler. Dedup still holds at the
                                // broker (op_id applied-set).
                                Some(ServeRole::Controller) => {
                                    brain.send_effect_no_ack(op_id, &bytes)?
                                }

-                                // (the operator-minted id rides the B5 journal).

-                                Some(ServeRole::Controller) => brain.send_effect(op_id, &bytes)?,

+                                // (the operator-minted id rides the B5 journal). NO-ACK

+                                // (REQ-HAZARD-INPUT-ACK-BACKPRESSURE): this path is

+                                // fire-and-forward (the ack is discarded here) and can

+                                // flood N input frames on this one conn; an acked write

+                                // back would fill the return direction and deadlock the

+                                // broker's per-conn handler. Dedup still holds at the

+                                // broker (op_id applied-set).

+                                Some(ServeRole::Controller) => {

+                                    brain.send_effect_no_ack(op_id, &bytes)?

// Outcome not yet known — buffer; flushed on a

// controller confirmation, discarded on busy/viewer.

None => pending_input.push((op_id, bytes)),


P1 ctrl+V wedge — diagnose then fix

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.

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

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.

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.


Now broker `dispatch_input` — gate the ack on `req.ack` (clean branch, no diag).