550	            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY
551	            // ABSORBS the journaled write, so there is no storm to pace and the sleep
552	            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —
553	            // skip it there; Windows p0 was green pre-pacing.
554	            #[cfg(unix)]
555	            std::thread::sleep(Duration::from_millis(5));
556	        }
557	    });

            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY
            // ABSORBS the journaled write, so there is no storm to pace and the sleep
            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —
            // skip it there; Windows p0 was green pre-pacing.
            #[cfg(unix)]
            std::thread::sleep(Duration::from_millis(5));
        }
    });


doyle (high-priority) acked: pacing removal + non-vacuity + g2-separate all confirmed; push when green, he validates both isolated + full binary. Proceeding with the approved edits.

a_journaled driver — swap to `send_effect_no_ack`, remove vestigial pacing, correct the refuted-mechanism comments, tag the requirement:


Edit

        // Sustain journaled input: keep ops in flight (and, on Unix, parked under
        // the journal lock) until teardown. Each send_effect writes a frame; the
        // pump reader drains replies concurrently so the write never deadlocks.
        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {
            if w.send_effect(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            op += 1;
            // Drain queued replies (acks/output) so the carrier stays healthy, then
            // PACE with an explicit floor sleep. The doc above says "sustain a write
            // in-flight", but the bare `while !stop` pumped ~6094 ops and pegged the
            // core, out-competing the SIBLING attacher thread for scheduling under
            // contention so its subscribe was never serviced in 30s (the kitsubito
            // reliability victim, subscribed=false). A small sleep keeps a journaled
            // write continuously in-flight (facet-B's load is intact) while yielding
            // the CPU the attacher needs — sustained, not storming.
            let _ = w.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20)));
            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY
            // ABSORBS the journaled write, so there is no storm to pace and the sleep
            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —
            // skip it there; Windows p0 was green pre-pacing.
            #[cfg(unix)]
            std::thread::sleep(Duration::from_millis(5));
        }

        // Sustain journaled input in-flight until teardown, via the REAL operator/rc
        // seam: send_effect_NO_ACK. The operator/rc drive floods input fire-and-
        // forward (attach.rs:205 `brain.send_effect_no_ack`) precisely so the broker
        // never writes an ACK back onto the very conn it is flooding — one ACK per op
        // self-contends with the controller flood-OUTPUT on that conn's Mutex<SendHalf>
        // and wedges the per-conn dispatch handler, starving a concurrent attach's
        // subscribe (root-caused via gdb: the dispatch thread parks in the ACK
        // send_frame on the SendHalf mutex, NOT the OutputLog mutex). The earlier
        // acked `send_effect` here was a TEST MIS-MODEL: no production path sustain-
        // floods the acked seam (its acked callers are bounded one-shot / one-at-a-
        // time), and acked-flood self-wedging is the documented hazard. no_ack stays
        // JOURNALED + exactly-once (op_id => the broker's journal.apply_once runs
        // regardless of ack), so facet B is non-vacuous: still a sustained journaled
        // flood, just via the seam the real operator actually uses.
        // [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]
        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {
            if w.send_effect_no_ack(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            op += 1;
            // Keep the carrier healthy by draining session output; with no_ack there
            // is no ACK frame to read and none to self-contend on the SendHalf.
            let _ = w.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20)));
        }


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

        // Sustain journaled input: keep ops in flight (and, on Unix, parked under
        // the journal lock) until teardown. Each send_effect writes a frame; the
        // pump reader drains replies concurrently so the write never deadlocks.
        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {
            if w.send_effect(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            op += 1;
            // Drain queued replies (acks/output) so the carrier stays healthy, then
            // PACE with an explicit floor sleep. The doc above says "sustain a write
            // in-flight", but the bare `while !stop` pumped ~6094 ops and pegged the
            // core, out-competing the SIBLING attacher thread for scheduling under
            // contention so its subscribe was never serviced in 30s (the kitsubito
            // reliability victim, subscribed=false). A small sleep keeps a journaled
            // write continuously in-flight (facet-B's load is intact) while yielding
            // the CPU the attacher needs — sustained, not storming.
            let _ = w.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20)));
            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY
            // ABSORBS the journaled write, so there is no storm to pace and the sleep
            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —
            // skip it there; Windows p0 was green pre-pacing.
            #[cfg(unix)]
            std::thread::sleep(Duration::from_millis(5));
        }

        // Sustain journaled input in-flight until teardown, via the REAL operator/rc
        // seam: send_effect_NO_ACK. The operator/rc drive floods input fire-and-
        // forward (attach.rs:205 `brain.send_effect_no_ack`) precisely so the broker
        // never writes an ACK back onto the very conn it is flooding — one ACK per op
        // self-contends with the controller flood-OUTPUT on that conn's Mutex<SendHalf>
        // and wedges the per-conn dispatch handler, starving a concurrent attach's
        // subscribe (root-caused via gdb: the dispatch thread parks in the ACK
        // send_frame on the SendHalf mutex, NOT the OutputLog mutex). The earlier
        // acked `send_effect` here was a TEST MIS-MODEL: no production path sustain-
        // floods the acked seam (its acked callers are bounded one-shot / one-at-a-
        // time), and acked-flood self-wedging is the documented hazard. no_ack stays
        // JOURNALED + exactly-once (op_id => the broker's journal.apply_once runs
        // regardless of ack), so facet B is non-vacuous: still a sustained journaled
        // flood, just via the seam the real operator actually uses.
        // [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]
        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {
            if w.send_effect_no_ack(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            op += 1;
            // Keep the carrier healthy by draining session output; with no_ack there
            // is no ACK frame to read and none to self-contend on the SendHalf.
            let _ = w.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20)));
        }

-        // Sustain journaled input: keep ops in flight (and, on Unix, parked under

-        // the journal lock) until teardown. Each send_effect writes a frame; the

-        // pump reader drains replies concurrently so the write never deadlocks.

+        // Sustain journaled input in-flight until teardown, via the REAL operator/rc

+        // seam: send_effect_NO_ACK. The operator/rc drive floods input fire-and-

+        // forward (attach.rs:205 `brain.send_effect_no_ack`) precisely so the broker

+        // never writes an ACK back onto the very conn it is flooding — one ACK per op

+        // self-contends with the controller flood-OUTPUT on that conn's Mutex<SendHalf>

+        // and wedges the per-conn dispatch handler, starving a concurrent attach's

+        // subscribe (root-caused via gdb: the dispatch thread parks in the ACK

+        // send_frame on the SendHalf mutex, NOT the OutputLog mutex). The earlier

+        // acked `send_effect` here was a TEST MIS-MODEL: no production path sustain-

+        // floods the acked seam (its acked callers are bounded one-shot / one-at-a-

+        // time), and acked-flood self-wedging is the documented hazard. no_ack stays

+        // JOURNALED + exactly-once (op_id => the broker's journal.apply_once runs

+        // regardless of ack), so facet B is non-vacuous: still a sustained journaled

+        // flood, just via the seam the real operator actually uses.

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

-            if w.send_effect(op, &chunk).is_err() {

+            if w.send_effect_no_ack(op, &chunk).is_err() {

-            // Drain queued replies (acks/output) so the carrier stays healthy, then

-            // PACE with an explicit floor sleep. The doc above says "sustain a write

-            // in-flight", but the bare `while !stop` pumped ~6094 ops and pegged the

-            // core, out-competing the SIBLING attacher thread for scheduling under

-            // contention so its subscribe was never serviced in 30s (the kitsubito

-            // reliability victim, subscribed=false). A small sleep keeps a journaled

-            // write continuously in-flight (facet-B's load is intact) while yielding

-            // the CPU the attacher needs — sustained, not storming.

+            // Keep the carrier healthy by draining session output; with no_ack there

+            // is no ACK frame to read and none to self-contend on the SendHalf.

-            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY

-            // ABSORBS the journaled write, so there is no storm to pace and the sleep

-            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —

-            // skip it there; Windows p0 was green pre-pacing.

-            std::thread::sleep(Duration::from_millis(5));


Fixing the intro comment's refuted "parks the journal lock" mechanism:


    // ── The WEDGE driver: on its OWN connection, pump journaled input
    //    (op_id-carrying send_effect → broker dispatch_input → journal.apply_once
    //    runs write_input INSIDE the global journal lock). On Unix the full input
    //    buffer parks write_input, holding the lock; on Windows ConPTY absorbs it.
    //    The driver fires a BOUNDED burst of MODEST-sized ops (so a backed-up
    //    broker IPC never blocks the driver's own socket write — the burst always
    //    completes and the thread exits cleanly, never hanging the test process)
    //    and then idles, checking the stop flag. ──

    // ── The WEDGE driver: on its OWN connection, pump journaled input via the REAL
    //    operator seam (op_id-carrying send_effect_no_ack → broker dispatch_input →
    //    journal.apply_once enqueues the PtyWrite exactly-once). A sustained flood of
    //    MODEST-sized ops keeps a journaled write in-flight; the pump carrier drains
    //    the driver's own socket so a backed-up broker never blocks its writes, and
    //    the thread exits cleanly on the stop flag. ──


    // ── The WEDGE driver: on its OWN connection, pump journaled input
    //    (op_id-carrying send_effect → broker dispatch_input → journal.apply_once
    //    runs write_input INSIDE the global journal lock). On Unix the full input
    //    buffer parks write_input, holding the lock; on Windows ConPTY absorbs it.
    //    The driver fires a BOUNDED burst of MODEST-sized ops (so a backed-up
    //    broker IPC never blocks the driver's own socket write — the burst always
    //    completes and the thread exits cleanly, never hanging the test process)
    //    and then idles, checking the stop flag. ──

    // ── The WEDGE driver: on its OWN connection, pump journaled input via the REAL
    //    operator seam (op_id-carrying send_effect_no_ack → broker dispatch_input →
    //    journal.apply_once enqueues the PtyWrite exactly-once). A sustained flood of
    //    MODEST-sized ops keeps a journaled write in-flight; the pump carrier drains
    //    the driver's own socket so a backed-up broker never blocks its writes, and
    //    the thread exits cleanly on the stop flag. ──

-    // ── The WEDGE driver: on its OWN connection, pump journaled input

-    //    (op_id-carrying send_effect → broker dispatch_input → journal.apply_once

-    //    runs write_input INSIDE the global journal lock). On Unix the full input

-    //    buffer parks write_input, holding the lock; on Windows ConPTY absorbs it.

-    //    The driver fires a BOUNDED burst of MODEST-sized ops (so a backed-up

-    //    broker IPC never blocks the driver's own socket write — the burst always

-    //    completes and the thread exits cleanly, never hanging the test process)

-    //    and then idles, checking the stop flag. ──

+    // ── The WEDGE driver: on its OWN connection, pump journaled input via the REAL

+    //    operator seam (op_id-carrying send_effect_no_ack → broker dispatch_input →

+    //    journal.apply_once enqueues the PtyWrite exactly-once). A sustained flood of

+    //    MODEST-sized ops keeps a journaled write in-flight; the pump carrier drains

+    //    the driver's own socket so a backed-up broker never blocks its writes, and

+    //    the thread exits cleanly on the stop flag. ──

let stop = Arc::new(AtomicBool::new(false));

let pumped = Arc::new(AtomicU64::new(0));


p0 driver — reading its current region for the precise swap.


1772	    let driver_stop = Arc::clone(&stop);
1773	    let driver_pumped = Arc::clone(&pumped);
1774	    let driver = thread::spawn(move || {
1775	        let mut w = match Brain::cold_start_pump(&driver_name, 1, Duration::from_secs(2)) {
1776	            Ok(b) => b,
1777	            Err(_) => return,
1778	        };
1779	        if w.attach(sid, 0).is_err() {
1780	            return;
1781	        }
1782	        // Paste-shaped: modest records, MANY of them — far more than the depth-4
1783	        // FIFO holds, so saturation (and DROP) is guaranteed once the writer parks.
1784	        let chunk = vec![b'P'; 4 * 1024];
1785	        let mut op = 1u64;
1786	        while !driver_stop.load(Ordering::Relaxed) {
1787	            if w.send_effect(op, &chunk).is_err() {
1788	                break;
1789	            }
1790	            driver_pumped.fetch_add(1, Ordering::Relaxed);
1791	            op += 1;
1792	            // Pace the pump (see the journaled-wedge driver): keep a paste write
1793	            // in-flight without a CPU storm that starves the concurrent attacher.
1794	            let _ = w.read_event_until(Some(Instant::now() + Duration::from_millis(15)));
1795	            // Unix-only (see the journaled-wedge driver): Windows ConPTY absorbs the
1796	            // paste, so there is no CPU storm to pace here.
1797	            #[cfg(unix)]

    let driver_stop = Arc::clone(&stop);
    let driver_pumped = Arc::clone(&pumped);
    let driver = thread::spawn(move || {
        let mut w = match Brain::cold_start_pump(&driver_name, 1, Duration::from_secs(2)) {
            Ok(b) => b,
            Err(_) => return,
        };
        if w.attach(sid, 0).is_err() {
            return;
        }
        // Paste-shaped: modest records, MANY of them — far more than the depth-4
        // FIFO holds, so saturation (and DROP) is guaranteed once the writer parks.
        let chunk = vec![b'P'; 4 * 1024];
        let mut op = 1u64;
        while !driver_stop.load(Ordering::Relaxed) {
            if w.send_effect(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, Ordering::Relaxed);
            op += 1;
            // Pace the pump (see the journaled-wedge driver): keep a paste write
            // in-flight without a CPU storm that starves the concurrent attacher.
            let _ = w.read_event_until(Some(Instant::now() + Duration::from_millis(15)));
            // Unix-only (see the journaled-wedge driver): Windows ConPTY absorbs the
            // paste, so there is no CPU storm to pace here.
            #[cfg(unix)]


        while !driver_stop.load(Ordering::Relaxed) {
            if w.send_effect(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, Ordering::Relaxed);
            op += 1;
            // Pace the pump (see the journaled-wedge driver): keep a paste write
            // in-flight without a CPU storm that starves the concurrent attacher.
            let _ = w.read_event_until(Some(Instant::now() + Duration::from_millis(15)));
            // Unix-only (see the journaled-wedge driver): Windows ConPTY absorbs the
            // paste, so there is no CPU storm to pace here.
            #[cfg(unix)]
            std::thread::sleep(Duration::from_millis(5));
        }

        // Flood paste-shaped input via the REAL operator seam (send_effect_no_ack):
        // the rc/operator paste drive is fire-and-forward (attach.rs:205) so the
        // broker never ACKs back onto the flooded conn. The earlier acked send_effect
        // was a test mis-model — one ACK per op self-contends with the controller
        // flood-output on that conn's Mutex<SendHalf> and starves the concurrent
        // attach's subscribe (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). no_ack stays
        // journaled+exactly-once (op_id => journal.apply_once), so the paste-wedge
        // gate is unchanged: still a sustained journaled paste flood that must NOT
        // wedge the concurrent attach.
        // [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]
        while !driver_stop.load(Ordering::Relaxed) {
            if w.send_effect_no_ack(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, Ordering::Relaxed);
            op += 1;
            // Drain session output to keep the carrier healthy; no ACK frame with no_ack.
            let _ = w.read_event_until(Some(Instant::now() + Duration::from_millis(15)));
        }


        while !driver_stop.load(Ordering::Relaxed) {
            if w.send_effect(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, Ordering::Relaxed);
            op += 1;
            // Pace the pump (see the journaled-wedge driver): keep a paste write
            // in-flight without a CPU storm that starves the concurrent attacher.
            let _ = w.read_event_until(Some(Instant::now() + Duration::from_millis(15)));
            // Unix-only (see the journaled-wedge driver): Windows ConPTY absorbs the
            // paste, so there is no CPU storm to pace here.
            #[cfg(unix)]
            std::thread::sleep(Duration::from_millis(5));
        }

        // Flood paste-shaped input via the REAL operator seam (send_effect_no_ack):
        // the rc/operator paste drive is fire-and-forward (attach.rs:205) so the
        // broker never ACKs back onto the flooded conn. The earlier acked send_effect
        // was a test mis-model — one ACK per op self-contends with the controller
        // flood-output on that conn's Mutex<SendHalf> and starves the concurrent
        // attach's subscribe (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). no_ack stays
        // journaled+exactly-once (op_id => journal.apply_once), so the paste-wedge
        // gate is unchanged: still a sustained journaled paste flood that must NOT
        // wedge the concurrent attach.
        // [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE]
        while !driver_stop.load(Ordering::Relaxed) {
            if w.send_effect_no_ack(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, Ordering::Relaxed);
            op += 1;
            // Drain session output to keep the carrier healthy; no ACK frame with no_ack.
            let _ = w.read_event_until(Some(Instant::now() + Duration::from_millis(15)));
        }

// FIFO holds, so saturation (and DROP) is guaranteed once the writer parks.

+        // Flood paste-shaped input via the REAL operator seam (send_effect_no_ack):

+        // the rc/operator paste drive is fire-and-forward (attach.rs:205) so the

+        // broker never ACKs back onto the flooded conn. The earlier acked send_effect

+        // was a test mis-model — one ACK per op self-contends with the controller

+        // flood-output on that conn's Mutex<SendHalf> and starves the concurrent

+        // attach's subscribe (REQ-HAZARD-INPUT-ACK-BACKPRESSURE). no_ack stays

+        // journaled+exactly-once (op_id => journal.apply_once), so the paste-wedge

+        // gate is unchanged: still a sustained journaled paste flood that must NOT

+        // wedge the concurrent attach.

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

-            if w.send_effect(op, &chunk).is_err() {

+            if w.send_effect_no_ack(op, &chunk).is_err() {

-            // Pace the pump (see the journaled-wedge driver): keep a paste write

-            // in-flight without a CPU storm that starves the concurrent attacher.

+            // Drain session output to keep the carrier healthy; no ACK frame with no_ack.

-            // Unix-only (see the journaled-wedge driver): Windows ConPTY absorbs the

-            // paste, so there is no CPU storm to pace here.

-            std::thread::sleep(Duration::from_millis(5));


Both drivers swapped to `send_effect_no_ack` (pacing removed, comments corrected, REQ tagged). Verifying: full inject_control_wedge binary (catches any sibling regression) + clippy + traceability on Windows.


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && git branch --show-current && cargo nextest run -p spt-daemon --test inject_control_wedge 2>&1 | tail -8

Confirm branch + run full inject_control_wedge binary


Command running in background with ID: bbf58f3t4. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bbf58f3t4.output. You will be notified when it completes. To check interim output, use Read on that file path.


cd "C:\Users\decid\Documents\projects\spt-core" && cargo clippy -p spt-daemon --tests 2>&1 | tail -3; echo "[clippy $?]"; traceable-reqs check >/tmp/tr4.out 2>&1; echo "[trace $?]"

Clippy + traceability


Command running in background with ID: bl14405pi. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bl14405pi.output. You will be notified when it completes. To check interim output, use Read on that file path.


Both verification runs in flight. Awaiting full-binary result + clippy/traceability.


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


<task-notification>
<task-id>bl14405pi</task-id>
<tool-use-id>toolu_01JF48jtekHi8mgcFfejy1YH</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bl14405pi.output</output-file>
<status>completed</status>
<summary>Background command "Clippy + traceability" completed (exit code 0)</summary>
</task-notification>


Compiling. Awaiting both completions.


1	    Blocking waiting for file lock on package cache
2	    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
3	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.13s
4	[clippy 0]
5	[trace 0]
6

    Blocking waiting for file lock on package cache
    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 6.13s
[clippy 0]
[trace 0]


Prompt:
<task-notification>
<task-id>bl14405pi</task-id>
<tool-use-id>toolu_01JF48jtekHi8mgcFfejy1YH</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bl14405pi.output</output-file>
<status>completed</status>
<summary>Background command "Clippy + traceability" completed (exit code 0)</summary>
</task-notification>


Clippy clean, traceability green. Awaiting the full-binary run.


1	v0.13.0-p1b-ack-deadlock
2


On the correct branch. Full binary (10 tests) still running. Awaiting.


ScheduleWakeup

Full inject_control_wedge binary running (~30s serial) to confirm no sibling regression from the no_ack seam swap before pushing; its completion re-invokes me.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 22:48:00 (in 156s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Edits applied (both drivers → `send_effect_no_ack`, pacing removed, comments corrected, REQ tagged); clippy + traceability green; on the right branch. Full inject_control_wedge binary running to confirm no sibling regression, then I push p0+a_journaled for doyle's kitsubito validation. g2 dig follows (held as the one possible real product issue).


<task-notification>
<task-id>bbf58f3t4</task-id>
<tool-use-id>toolu_014ZNnWh3KRKxfwXP5NwxT2T</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\bbf58f3t4.output</output-file>
<status>completed</status>
<summary>Background command "Confirm branch + run full inject_control_wedge binary" completed (exit code 0)</summary>
</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.


Bash

Read full binary result


        PASS [   0.802s] ( 5/10) spt-daemon::inject_control_wedge g3_content_free_ping_never_duplicates_operator_keystrokes_into_binary
        PASS [   0.654s] ( 6/10) spt-daemon::inject_control_wedge g4_translation_child_is_reaped_no_zombie_after_session_down
        PASS [   0.468s] ( 7/10) spt-daemon::inject_control_wedge injecting_a_large_payload_into_a_stdin_ignoring_child
        PASS [   3.449s] ( 8/10) spt-daemon::inject_control_wedge p0_paste_wedge_parked_write_does_not_starve_attach_or_wedge_broker
        PASS [   0.530s] ( 9/10) spt-daemon::inject_control_wedge w5_a1_locally_driven_session_reports_controller_by_none
        PASS [   4.278s] (10/10) spt-daemon::inject_control_wedge w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some
────────────
     Summary [  29.455s] 10 tests run: 10 passed, 0 skipped


10/10 PASS on Windows — p0 + a_journaled + all siblings green, no regression from the seam swap. (g2 passes on Windows; it only fails on Linux — untouched, held for separate root-cause.) Clippy + traceability green. Committing + pushing.


cd "C:\Users\decid\Documents\projects\spt-core" && git add crates/spt-daemon/tests/inject_control_wedge.rs && git commit -q -F - <<'EOF'
test(inject-wedge): drive the sustained-input gates via the REAL ackless operator seam (send_effect_no_ack) — fixes p0 + a_journaled

RUN#1 of the two-phase split isolated 3 real Phase-B failures on the quiet Linux
box. Root-caused two of them (p0_paste_wedge, a_journaled_input_wedge) to a TEST
MIS-MODEL, not a product bug — triangulated by gdb + SPT_WEDGE_TRACE + source:

- gdb (kitsubito): the broker's per-conn DISPATCH thread parks in a futex on the
  conn's Mutex<SendHalf> inside send_frame (broker.rs:2150) <- dispatch_input's ACK
  write (broker.rs:1690). NOT resolve_subscribe / NOT the OutputLog mutex.
- SPT_WEDGE_TRACE numbers: 566k drain appends but OutputLog acquire_wait 0.43s
  total / 41ms max = not the bottleneck. The wait is the per-op ACK on the SendHalf.
- source: dispatch_input sends the ACK send_frame ONLY when req.ack==true. The
  driver attached as controller, so the broker fans flood OUTPUT to that same conn's
  SendHalf; ~5000 per-op ACKs then self-contend on it (unfair std Mutex) and wedge
  the dispatch handler, starving the concurrent attach's subscribe (subscribed=false).

This is exactly REQ-HAZARD-INPUT-ACK-BACKPRESSURE, which the PRODUCT already avoids:
the real rc/operator sustained drive is fire-and-forward ACKLESS (attach.rs:205
`brain.send_effect_no_ack`). No production path sustain-floods the ACKED seam — its
acked callers are bounded one-shot (attach.rs:240 pending flush) or one-at-a-time
(shellchan). The gates drove the flood with `send_effect` (ack=true), re-triggering
the documented hazard the real path dodges.

Fix (test only, product UNTOUCHED): both wedge drivers `send_effect` ->
`send_effect_no_ack` (the seam the real operator uses) + comment citing the hazard.
Non-vacuity preserved: journaling is independent of ack (dispatch_input runs
journal.apply_once on op_id==Some regardless of req.ack; ack only gates the reply
frame), so no_ack stays journaled + exactly-once — still a sustained JOURNALED flood,
still asserts the concurrent attach is serviced. Also removed the now-vestigial
cfg(unix) driver pacing (added under the refuted CPU-storm hypothesis; the 566k-
append/0.43s data shows a fast ackless flood is fine).

g2_no_commit_deadline is NOT this root (no flood; op_flushed=false is the
translation::InjectFloor commit-deadline/floor-release path) — held untouched for
separate root-cause; could be the one genuine product issue in the cluster.

Verified local (Windows): inject_control_wedge 10/10; clippy clean; traceable-reqs
green. doyle validates p0 + a_journaled isolated on kitsubito (the boxes where they
actually failed).

Co-authored by: todlando
EOF
git push origin v0.13.0-p1b-ack-deadlock 2>&1 | tail -3

Commit + push the no_ack seam fix


warning: in the working copy of 'crates/spt-daemon/tests/inject_control_wedge.rs', LF will be replaced by CRLF the next time Git touches it
To https://github.com/SaberMage/spt-core.git
   18416fd..71378d8  v0.13.0-p1b-ack-deadlock -> v0.13.0-p1b-ack-deadlock


Pushed 71378d8 (p0 + a_journaled fix). Reporting to doyle + starting the g2 root-cause dig.


$OWL send doyle <<'EOF'
PUSHED -> 71378d8 on v0.13.0-p1b-ack-deadlock. p0 + a_journaled drivers now send_effect_no_ack (real operator seam), pacing removed, comments corrected + [int->REQ-HAZARD-INPUT-ACK-BACKPRESSURE] tagged. Assertions untouched, product untouched. g2 NOT touched (held).
VERIFIED Windows: inject_control_wedge 10/10; clippy clean; traceable-reqs green. Validate p0 + a_journaled isolated on kitsubito (expect subscribed=true, got_output=true) + full binary for sibling regressions.

NOW DIGGING g2 per your hold (root-cause first, no change). g2_no_commit_deadline: binary emits payload+key but never {commit} -> 400ms commit deadline must fire -> InjectFloor RELEASES -> buffered operator keys flush to PTY -> echo back over ring. op_flushed=false = buffered keys never reached the ring within 8s on quiet Linux. Reading translation::InjectFloor + the commit-deadline worker now to localize the floor-release/flush path, then I'll instrument it (same SPT_WEDGE_TRACE rigor, floor-open/deadline-fire/release/flush timestamps) for you to run on kitsubito — to show flush-but-slow (timing) vs never-flush (real forkpty bug). Findings before any change. -todlando
EOF

Report push + announce g2 dig to doyle


➜ SENT:doyle


Digging g2. Mapping the InjectFloor commit-deadline/floor-release/flush path.


Grep

struct InjectFloor|impl InjectFloor|fn take_or_release|fn open|fn release|commit|deadline|COMMIT_DEADLINE|fn flush|buffered|floor


58:/// the ADR-0022 wire (`{key}` / `{text}` / `{delay_ms}` / `{commit}`). Unknown
74:    /// emits `{"commit":true}` after the last keystroke of its response to an
76:    /// the buffered controller input race-free. The degenerate v0.11.0 raw-inject
77:    /// is `{text:payload}{key:"enter"}{commit:true}`.
78:    Commit { commit: bool },
154:/// The per-session controller-input buffer + inject-floor flag (W2 Layer C,
157:/// While the floor is **held** (an injected keystroke sequence is mid-flight,
158:/// between a `{type:event}` dispatch and its `{commit}`), the operator's
159:/// keystrokes from `dispatch_input` are **buffered here** instead of hitting the
160:/// PTY; on commit (or the commit deadline) the buffer is flushed to the PTY
169:pub struct InjectFloor {
174:impl InjectFloor {
175:    /// Open the floor for a new injected sequence (clears any stale buffer — a
180:    pub fn open(&mut self) {
185:    /// Whether the floor is currently held (a sequence is mid-flight).
190:    /// Route one operator-input chunk: if the floor is held, buffer it and return
203:    /// One flush step — call in a loop after `{commit}` / the deadline. If the
204:    /// buffer holds bytes, take them to write **off-lock** while the floor STAYS
206:    /// buffer is empty, RELEASE the floor and return `None`. Looping until `None`
207:    /// drains everything and leaves the floor free.
208:    pub fn take_or_release(&mut self) -> Option<Vec<u8>> {
318:        let deadline = Instant::now() + REAP_GRACE;
326:                    if Instant::now() >= deadline {
399:            serde_json::from_str::<KeyCmd>(r#"{"commit":true}"#).unwrap(),
400:            KeyCmd::Commit { commit: true }
411:    // [unit->REQ-MSG-IDLE-TRANSLATION-BINARY] the inject floor: while held, operator
413:    // floor; input arriving mid-flush keeps buffering so order is preserved; a free
414:    // floor passes input straight through.
416:    fn inject_floor_buffers_while_held_then_drains_in_order() {
418:        // free floor → input passes through (caller writes), nothing buffered.
419:        assert!(!f.buffer_if_held(b"a"), "free floor does not buffer");
425:        assert!(f.buffer_if_held(b"hel"), "held floor buffers");
426:        assert!(f.buffer_if_held(b"lo"), "held floor keeps buffering");
428:        // flush step 1 takes the buffered bytes (floor STAYS held) ...
430:        assert!(f.is_held(), "floor stays held mid-flush so order is preserved");
434:        // ... and the empty step RELEASES the floor.
436:        assert!(!f.is_held(), "drained empty → floor released");
437:        assert!(!f.buffer_if_held(b"z"), "released floor passes through again");


InjectFloor: `take_or_release` (208) drains the buffer to PTY then releases; the flush must fire on `{commit}` OR the commit deadline. g2 (nocommit) needs the **deadline** to trigger the flush. Finding the deadline worker (broker.rs ~940-1000, calls `take_or_release`/`open`).


126:/// momentary slow consumer never trips the deadline.
137:/// Poll interval for the controller `try_send` deadline loop (std `SyncSender`
150:const INJECT_COMMIT_DEADLINE: Duration = Duration::from_secs(5);
152:/// The effective commit deadline. Production is the const above; the int gate
153:/// (Layer G no-commit FAULT test) shrinks it via `SPT_INJECT_COMMIT_DEADLINE_MS`
156:/// the deadline's PRESENCE is the invariant, not its exact length.
157:fn inject_commit_deadline() -> Duration {
158:    match std::env::var("SPT_INJECT_COMMIT_DEADLINE_MS") {
162:            .unwrap_or(INJECT_COMMIT_DEADLINE),
163:        Err(_) => INJECT_COMMIT_DEADLINE,
212:    /// drain's deadline-evict ([`mark_controller_gone`]) carries the epoch it
352:    /// additionally makes a stale deadline-evict a no-op AND stops a superseded
360:        // tx drops), and bump the generation so (a) an in-flight deadline-evict
390:    /// by W1's deadline-evict ([`mark_controller_gone`]), [`detach_if`], and (per
658:    /// deadline. `Ok(())` = queued (the writer does the blocking socket write);
661:    /// root). A live-but-slow controller drains a slot before the deadline and is
671:        let deadline = Instant::now() + CONTROLLER_WRITE_DEADLINE;
677:                    if Instant::now() >= deadline {
786:/// sequence misses its [`INJECT_COMMIT_DEADLINE`] or the binary dies — after which
797:    /// Raised by the worker on FAULT (commit-deadline miss / binary death): the
949:        let chunk = floor.lock().unwrap().take_or_release();
966:/// `{commit}` — bounded by [`INJECT_COMMIT_DEADLINE`]. On commit: flush the
968:/// deadline-miss or binary death: flush+release anyway (operator input is never
975:/// each other (REQ-HAZARD-INJECT-CONTROL-COEXIST), and the deadline FAULT path is
979:fn run_inject_worker(
994:        floor.lock().unwrap().open();
1002:        let deadline = Instant::now() + inject_commit_deadline();
1006:            let remaining = deadline.saturating_duration_since(now);
1008:                break; // deadline → fault below
1010:            match cmd_rx.recv_timeout(remaining) {
1020:                    // Clamp the pause to the remaining deadline so a huge/hostile
1023:                    let cap = deadline.saturating_duration_since(Instant::now());
1041:            fault_translation(&faulted, &child, "no {commit} within INJECT_COMMIT_DEADLINE");
1443:        // then deliver to the controller OFF the lock with a bounded deadline
1447:        // deadline (bounded wedge, never permanent).
2273:    /// the deadline (detach), instead of blocking the drain forever.
2278:        // Room → queued instantly (the common path; no deadline wait).
2303:        // Full channel that never drains → Err(epoch) after the deadline (bounded),
2317:            "a fully-stalled controller detaches after the deadline"
2321:            "deliver waits the full deadline before declaring a stalled controller gone"

[Showing results with pagination = limit: 40]


Found `run_inject_worker` (979): opens floor (994), sets 400ms deadline (1002), loops on `cmd_rx.recv_timeout` (1010), on deadline breaks (1008) → fault (1041). The flush `take_or_release` is at 949 (separate fn). Reading the full worker to see whether the deadline-fault path actually flushes+releases the buffered keys.


934	/// when the last `tx` clone drops at session teardown.
935	fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {
936	    while let Ok(bytes) = rx.recv() {
937	        let _ = session.write_input(&bytes);
938	    }
939	}
940
941	/// Drain the inject floor's buffered controller input to the PTY **after** an
942	/// injected sequence, then release the floor. Each step takes the buffered bytes
943	/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never
944	/// hold a lock across a PTY write — now never block on it either); input arriving
945	/// mid-flush keeps buffering, so strict order is preserved until the buffer
946	/// drains empty and the floor frees.
947	fn flush_inject_floor(floor: &Mutex<InjectFloor>, input: &InputWriter) {
948	    loop {
949	        let chunk = floor.lock().unwrap().take_or_release();
950	        match chunk {
951	            Some(bytes) => {
952	                input.enqueue(bytes);
953	            }
954	            None => break,
955	        }
956	    }
957	}
958
959	/// The inject worker (W2 Layer C): one thread per translation-bearing session. It
960	/// drives inbound `<EVENT>`s through the binary and applies the binary's keystroke
961	/// commands to the PTY ATOMICALLY w.r.t. the operator's live controller.
962	///
963	/// Per queued event: drop any stray pre-event commands, OPEN the floor (operator
964	/// input now buffers in `dispatch_input`), feed the binary the `{type:event}`,
965	/// then drain `{key}`/`{text}`/`{delay_ms}` into `session.write_input` until
966	/// `{commit}` — bounded by [`INJECT_COMMIT_DEADLINE`]. On commit: flush the
967	/// buffered controller bytes AFTER the injected sequence and release the floor. On
968	/// deadline-miss or binary death: flush+release anyway (operator input is never
969	/// stranded), then FAULT — terminate the binary, set `faulted`, and exit (the
970	/// session degrades to raw inject). The loop ends when `event_tx` is dropped
971	/// (session teardown), reaping the child.
972	///
973	/// The atomic open→drain→commit→flush is the W1 coexistence mechanism: an injected
974	/// sequence and the operator's live controller share one PTY without clobbering
975	/// each other (REQ-HAZARD-INJECT-CONTROL-COEXIST), and the deadline FAULT path is
976	/// the "never park forever" bound applied to injection.
977	// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]
978	// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
979	fn run_inject_worker(
980	    input: Arc<InputWriter>,
981	    child: Arc<TranslationChild>,
982	    floor: Arc<Mutex<InjectFloor>>,
983	    event_rx: Receiver<String>,
984	    cmd_rx: Receiver<KeyCmd>,
985	    faulted: Arc<AtomicBool>,
986	) {
987	    // `event_rx` blocks here; events queue → one sequence at a time.
988	    for envelope in event_rx {
989	        // A well-behaved filter only emits in response to an event, but drop any
990	        // unsolicited commands left in the channel so they can't be mis-attributed
991	        // to this event's sequence.
992	        while cmd_rx.try_recv().is_ok() {}
993
994	        floor.lock().unwrap().open();
995	        if child.send(&ToBinary::Event { envelope }).is_err() {
996	            // stdin write failed → the binary is gone; release the floor + fault.
997	            flush_inject_floor(&floor, &input);
998	            fault_translation(&faulted, &child, "stdin write failed");
999	            return;

/// when the last `tx` clone drops at session teardown.
fn input_writer(session: Arc<PtySession>, rx: Receiver<InputRecord>) {
    while let Ok(bytes) = rx.recv() {
        let _ = session.write_input(&bytes);
    }
}

/// Drain the inject floor's buffered controller input to the PTY **after** an
/// injected sequence, then release the floor. Each step takes the buffered bytes
/// OFF the lock before ENQUEUEING it to the lone input-writer (the W1 rule: never
/// hold a lock across a PTY write — now never block on it either); input arriving
/// mid-flush keeps buffering, so strict order is preserved until the buffer
/// drains empty and the floor frees.
fn flush_inject_floor(floor: &Mutex<InjectFloor>, input: &InputWriter) {
    loop {
        let chunk = floor.lock().unwrap().take_or_release();
        match chunk {
            Some(bytes) => {
                input.enqueue(bytes);
            }
            None => break,
        }
    }
}

/// The inject worker (W2 Layer C): one thread per translation-bearing session. It
/// drives inbound `<EVENT>`s through the binary and applies the binary's keystroke
/// commands to the PTY ATOMICALLY w.r.t. the operator's live controller.
///
/// Per queued event: drop any stray pre-event commands, OPEN the floor (operator
/// input now buffers in `dispatch_input`), feed the binary the `{type:event}`,
/// then drain `{key}`/`{text}`/`{delay_ms}` into `session.write_input` until
/// `{commit}` — bounded by [`INJECT_COMMIT_DEADLINE`]. On commit: flush the
/// buffered controller bytes AFTER the injected sequence and release the floor. On
/// deadline-miss or binary death: flush+release anyway (operator input is never
/// stranded), then FAULT — terminate the binary, set `faulted`, and exit (the
/// session degrades to raw inject). The loop ends when `event_tx` is dropped
/// (session teardown), reaping the child.
///
/// The atomic open→drain→commit→flush is the W1 coexistence mechanism: an injected
/// sequence and the operator's live controller share one PTY without clobbering
/// each other (REQ-HAZARD-INJECT-CONTROL-COEXIST), and the deadline FAULT path is
/// the "never park forever" bound applied to injection.
// [impl->REQ-MSG-IDLE-TRANSLATION-BINARY]
// [impl->REQ-HAZARD-INJECT-CONTROL-COEXIST]
fn run_inject_worker(
    input: Arc<InputWriter>,
    child: Arc<TranslationChild>,
    floor: Arc<Mutex<InjectFloor>>,
    event_rx: Receiver<String>,
    cmd_rx: Receiver<KeyCmd>,
    faulted: Arc<AtomicBool>,
) {
    // `event_rx` blocks here; events queue → one sequence at a time.
    for envelope in event_rx {
        // A well-behaved filter only emits in response to an event, but drop any
        // unsolicited commands left in the channel so they can't be mis-attributed
        // to this event's sequence.
        while cmd_rx.try_recv().is_ok() {}

        floor.lock().unwrap().open();
        if child.send(&ToBinary::Event { envelope }).is_err() {
            // stdin write failed → the binary is gone; release the floor + fault.
            flush_inject_floor(&floor, &input);
            fault_translation(&faulted, &child, "stdin write failed");
            return;


Need the deadline→fault path (1002-1041) to confirm it flushes. Reading.


1000	        }
1001
1002	        let deadline = Instant::now() + inject_commit_deadline();
1003	        let mut committed = false;
1004	        loop {
1005	            let now = Instant::now();
1006	            let remaining = deadline.saturating_duration_since(now);
1007	            if remaining.is_zero() {
1008	                break; // deadline → fault below
1009	            }
1010	            match cmd_rx.recv_timeout(remaining) {
1011	                Ok(KeyCmd::Key { key }) => {
1012	                    if let Some(bytes) = key_to_bytes(&key) {
1013	                        input.enqueue(bytes);
1014	                    }
1015	                }
1016	                Ok(KeyCmd::Text { text }) => {
1017	                    input.enqueue(text.into_bytes());
1018	                }
1019	                Ok(KeyCmd::Delay { delay_ms }) => {
1020	                    // Clamp the pause to the remaining deadline so a huge/hostile
1021	                    // delay can't park the floor past the bound.
1022	                    let want = Duration::from_millis(delay_ms);
1023	                    let cap = deadline.saturating_duration_since(Instant::now());
1024	                    thread::sleep(want.min(cap));
1025	                }
1026	                Ok(KeyCmd::Commit { .. }) => {
1027	                    committed = true;
1028	                    break;
1029	                }
1030	                // Deadline with no commit, OR the child's stdout closed (binary
1031	                // died): both end the sequence; `committed` stays false → fault.
1032	                Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => break,
1033	            }
1034	        }
1035
1036	        // Flush buffered controller input AFTER the injected bytes + release the
1037	        // floor — in EVERY exit path, so operator keystrokes are never stranded.
1038	        flush_inject_floor(&floor, &input);
1039
1040	        if !committed {
1041	            fault_translation(&faulted, &child, "no {commit} within INJECT_COMMIT_DEADLINE");
1042	            return;
1043	        }
1044	    }
1045	    // `event_tx` dropped → session teardown. Reap the child (no zombie).
1046	    child.terminate();
1047	}
1048
1049	/// FAULT a translation binary: mark the session degraded (so inbound delivery

        }

        let deadline = Instant::now() + inject_commit_deadline();
        let mut committed = false;
        loop {
            let now = Instant::now();
            let remaining = deadline.saturating_duration_since(now);
            if remaining.is_zero() {
                break; // deadline → fault below
            }
            match cmd_rx.recv_timeout(remaining) {
                Ok(KeyCmd::Key { key }) => {
                    if let Some(bytes) = key_to_bytes(&key) {
                        input.enqueue(bytes);
                    }
                }
                Ok(KeyCmd::Text { text }) => {
                    input.enqueue(text.into_bytes());
                }
                Ok(KeyCmd::Delay { delay_ms }) => {
                    // Clamp the pause to the remaining deadline so a huge/hostile
                    // delay can't park the floor past the bound.
                    let want = Duration::from_millis(delay_ms);
                    let cap = deadline.saturating_duration_since(Instant::now());
                    thread::sleep(want.min(cap));
                }
                Ok(KeyCmd::Commit { .. }) => {
                    committed = true;
                    break;
                }
                // Deadline with no commit, OR the child's stdout closed (binary
                // died): both end the sequence; `committed` stays false → fault.
                Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => break,
            }
        }

        // Flush buffered controller input AFTER the injected bytes + release the
        // floor — in EVERY exit path, so operator keystrokes are never stranded.
        flush_inject_floor(&floor, &input);

        if !committed {
            fault_translation(&faulted, &child, "no {commit} within INJECT_COMMIT_DEADLINE");
            return;
        }
    }
    // `event_tx` dropped → session teardown. Reap the child (no zombie).
    child.terminate();
}

/// FAULT a translation binary: mark the session degraded (so inbound delivery


g2 code review: the flush logic looks **correct** — `flush_inject_floor` (1038) runs on **every exit path including the deadline** (comment: "in EVERY exit path, so operator keystrokes are never stranded"), draining buffered keys to the input writer → PTY. So it's likely NOT a "never-flush" worker-logic bug; leans timing or a downstream forkpty echo issue. Reporting to doyle + proposing the targeted instrumentation, since for a timing/echo issue a trace localizes better than gdb.


Bash

$OWL send doyle <<'EOF'
g2 CODE-REVIEW (before instrumenting — narrows it):
The floor-release/flush logic looks CORRECT in the worker. run_inject_worker (broker.rs:979): per event -> floor.open() (994) -> feed binary -> loop on cmd_rx.recv_timeout(remaining) bounded by the 400ms deadline (1002-1034) -> on deadline `Err(Timeout)=>break` (1032) -> then **flush_inject_floor(1038) runs on EVERY exit path** (commit, deadline, binary-death) -> only THEN fault (1040). flush_inject_floor (947-956) loops take_or_release: drains buffered bytes via input.enqueue (off-lock) until empty, then releases the floor. So the deadline path DOES flush buffered operator keys to the input-writer -> PTY. This is NOT an obvious never-flush worker bug.

=> g2's op_flushed=false is most likely one of:
  (a) TIMING: 400ms deadline vs the test's 4x send_input @150ms spacing (sends straddle the deadline); on a forkpty box the flushed write->PTY->child-echo->ring round-trip may exceed the 8s output_contains window, OR
  (b) DOWNSTREAM: the flushed keys enqueue but don't ECHO — e.g. after the binary FAULTS (degrade-to-raw), is the PTY child still echoing stdin on forkpty? OR write_input lands but the echo doesn't drain to the ring.
NOT the SendHalf/ack family at all — separate subsystem (InjectFloor).

OPEN QUESTION that decides (a) vs (b): does the operator's "OPERATOR_KEYS_FLUSHED" actually reach the PTY child's stdin + get echoed post-fault on forkpty?

PROPOSED INSTRUMENTATION (I'll add to the wedge-trace branch): SPT_WEDGE_TRACE timestamps at floor.open(994), deadline-fire(1032), flush_inject_floor byte-count+enqueue(947-956), and dispatch_input's buffer_if_held (buffered-vs-passthrough per operator key). That shows: were the keys buffered, did the flush drain N bytes, at what t, vs the echo arrival. flush-with-bytes-but-no-echo => (b) downstream; flush-late => (a) timing.

ALT (maybe faster on your box, like the SendHalf gdb): strace -f the broker on kitsubito during g2 for the write() to the PTY master fd carrying OPERATOR_KEYS_FLUSHED — present => flush works, issue is echo/ring (b); absent => floor never flushed (worker bug). Your call: want me to write the trace, or strace it first? -todlando
EOF

Report g2 code-review + instrumentation plan to doyle


g2 narrowed to timing-vs-downstream (flush logic is correct on every exit path); proposed instrumentation + a faster strace alternative, asked doyle's preference. Both threads now await doyle: his kitsubito validation of p0+a_journaled (71378d8), and his g2 approach call.

Standing by.


ScheduleWakeup

Pushed p0+a_journaled fix + reported g2 root-cause narrowing; awaiting doyle's kitsubito validation + his g2 approach decision (trace vs strace) via the armed Monitor — fallback heartbeat.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 23:06:00 (in 1036s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Progress this session: root-caused the entire Phase-B failure cluster via gdb + SPT_WEDGE_TRACE + source triangulation. **p0 + a_journaled fixed + pushed (71378d8)** — test mis-model (acked-flood vs the real ackless operator seam, REQ-HAZARD-INPUT-ACK-BACKPRESSURE); product untouched, non-vacuity preserved, 10/10 local + clippy + traceability green. **g2 held + narrowed** to timing-vs-downstream-echo (flush logic is correct on every exit path), instrumentation/strace proposed.

Awaiting doyle's kitsubito validation of the fix + his g2 approach call.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/3" id="1414eb6c" type="msg" from="doyle">STOP — the no_ack fix did NOT work. Empirical on kitsubito (71378d8 patch applied, isolated):
- a_journaled: subscribed=false, journaled_ops_pumped=41704 (was 5217)
- p0: subscribed=false, pumped=42689 (was 6009)
Both STILL FAIL. Removing the ACK + pacing made the driver pump 8x MORE but the attach is STILL n</EVENT-PART>
<EVENT-PART seq="2/3" id="1414eb6c">ot serviced. So the ack=true ACK-send was a SYMPTOM at the gdb instant, NOT the root — the real wedge persists without the ACK. Good thing I validate on kitsubito before folding.

HOLD g2 dig. Re-diagnosing the no_ack version now with a fresh gdb full-thread dump to find where dispatch/subscribe is parked WITHOUT the ACK in play. The a</EVENT-PART>
<EVENT-PART seq="3/3" id="1414eb6c">ttacher uses net_dial_loopback (QUIC), the flood is UDS — need to see if the broker core serializes both or the subscribe stalls elsewhere. Don't change anything; stand by for the new stack evidence. Our root-cause was incomplete — re-opening it with data. -doyle</EVENT-PART></event>
</task-notification>


doyle (high-priority): **STOP — my no_ack fix did NOT work.** Empirical on kitsubito: a_journaled + p0 still `subscribed=false`, and the driver pumped **8x MORE** (41704 vs 5217) without the ACK — so the ACK-send was a SYMPTOM at the gdb instant, not the root. The wedge persists on the **real ackless seam** → this reframes to a likely **real product issue**, not a test mis-model. Good thing he validates before folding. He's re-diagnosing the no_ack version with a fresh gdb dump; HOLD everything, no changes.

Acking + giving him a concrete lead for the new dump (the attacher is QUIC net-stream, a different subscribe handler than I instrumented).


Bash

$OWL send doyle <<'EOF'
ACK — my "test mis-model" call was WRONG. no_ack (the REAL operator seam) STILL wedges, 8x more pump = the ACK was a symptom at the gdb instant, not root. Critical reframe: since the REAL ackless operator flood DOES starve a concurrent attach on forkpty, this is a GENUINE PRODUCT issue (or limitation), and the test's invariant is VALID + currently violated. Holding everything, no changes, standing by for your stacks. Your validate-before-fold caught my incomplete root-cause — exactly right.

LEAD FOR THE NEW DUMP (where I steered wrong): the attacher subscribes over net_dial_loopback = QUIC, so it's KIND_NET_STREAM_SUBSCRIBE -> Broker::dispatch_net_stream_subscribe (broker.rs:1357), NOT the UDS KIND_SUBSCRIBE/dispatch_subscribe(1584) I instrumented + reasoned about. The flood is UDS (driver's Whole conn). So the two cross a transport boundary. Things to catch in your `thread apply all bt`:
- The NET-host / iroh accept+serve thread(s): is the QUIC subscribe even being ACCEPTED/served, or is the broker's net-endpoint pump thread starved/blocked by the UDS flood?
- dispatch_net_stream_subscribe's path: does it take a DIFFERENT lock than dispatch_subscribe (the net stream registry, or the same OutputLog/SendHalf)? Where is its thread parked?
- The driver's UDS dispatch thread (Thread 6 last time): with no_ack it no longer send_frames the ACK — so what's IT doing now at 42k pumped? If it's spinning in dispatch_input enqueue, is it starving a SHARED resource the net-subscribe needs (sessions lock? a global broker mutex? the input writer)?
- input_writer / the single drain: 42k ops -> is one global serialization point (input writer queue, or the session drain) blocking the net-subscribe servicing?

So the probe pivots from dispatch_subscribe/OutputLog to the NET-stream subscribe path + any UDS-vs-QUIC shared serialization. Send the stacks + who-holds-what and I'll re-analyze from the real park. No code touched. -todlando
EOF

Ack reframe + net-stream-subscribe lead to doyle


Reframe + net-stream-subscribe lead sent. Standing by for doyle's fresh gdb stacks (the park location without the ACK in play) before any further analysis — not reading speculatively since the park could be the net-accept thread, input writer, or a shared serialization point, and his stacks will direct it. No code changes per his hold.

The honest state: my "test mis-model" root-cause was incomplete — the ACK was a gdb-instant symptom; the real ackless operator flood still wedges the attach, so this is likely a genuine product issue. doyle's validate-before-fold caught it.


ScheduleWakeup

no_ack fix disproven on kitsubito; awaiting doyle's fresh full-thread gdb dump of the no_ack version to relocate the real wedge, via the armed Monitor — fallback heartbeat.

Prompt: <<autonomous-loop-dynamic>>


Next wakeup scheduled for 23:08:00 (in 1050s). Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.


Standing by for doyle's fresh gdb dump. Current state: the no_ack change is pushed (71378d8) but disproven as the fix — the wedge persists on the real ackless seam, reframing this as a genuine product issue. doyle is relocating the real park point; I gave him the net-stream-subscribe (QUIC) vs UDS-flood transport-boundary lead to direct it. No code changes pending his data.


<task-notification>
<task-id>b4cwwiglf</task-id>
Monitor event: "« spt event »"
<event><EVENT-PART seq="1/4" id="da0ed37c" type="msg" from="doyle">g2 code-review noted + good (floor-release looks correct; g2 likely timing/echo-downstream). But g2 is NOW LOWER PRIORITY — the p0/a_journaled no_ack REFUTATION is the fire. New gdb on the no_ack version: NO thread is lock-blocked. Broker threads are all actively processing the flood (Thread5 dispatch_input-&gt;decode</EVENT-PART>
<EVENT-PART seq="2/4" id="da0ed37c">_bytes@1630, Thread9 ControllerJob::deliver@681, Thread4 write_frame), net side IDLE (epoll). The ACK was a red herring — removing it didn't help. Driver pumped op_id=25812+ at the dump instant.

DECISIVE NEW CLUE: in your earlier instrumented run, the WEDGE_TRACE *subscribe* line NEVER PRINTED (only drain lines). That suggests the att</EVENT-PART>
<EVENT-PART seq="3/4" id="da0ed37c">acher's subscribe may not even REACH resolve_subscribe under flood — stuck in transport (it dials net_dial_loopback = QUIC) or never serviced, NOT a lock. Running the no_ack version WITH your instrumentation now + capturing FULL output to grep whether the subscribe reaches the broker at all. That distinguishes transport-stuck vs broker-slow.</EVENT-PART>
<EVENT-PART seq="4/4" id="da0ed37c"> Hold g2; this reopened root is the priority. Evidence incoming. -doyle</EVENT-PART></event>
</task-notification>


doyle (high-priority): new gdb (no_ack) — **NO thread lock-blocked**; all broker threads actively churning the flood (Thread5 dispatch_input→decode_bytes@1630, Thread9 deliver@681, Thread4 write_frame), net side idle-epoll, driver at op 25812+. ACK was a red herring. **Decisive clue: my WEDGE_TRACE *subscribe* line NEVER PRINTED on kitsubito** → the attacher's QUIC subscribe doesn't even reach resolve_subscribe under flood = transport-stuck or never-serviced, NOT a lock. He's capturing the instrumented no_ack run to confirm subscribe-reaches-broker-or-not. g2 lower priority now.

Acking + reading the net-stream-subscribe bridge (how a QUIC subscribe crosses into the broker, and whether it queues behind the UDS flood) to contribute concrete analysis.


Bash

$OWL send doyle <<'EOF'
ACK — huge clue. The subscribe NEVER reaching resolve_subscribe (no WEDGE_TRACE subscribe line on kitsubito) means it stalls BEFORE dispatch_subscribe — transport or the net->broker bridge, not a lock, not OutputLog, not the ACK. And all broker threads CPU-busy on flood decode (16KB chunk x 25k+ ops = decode_bytes@1630 is heavy) while net sits idle-epoll. Two candidate roots your capture will separate:
  (A) CPU-SATURATION: the flood's per-op base64/JSON decode (decode_bytes, 16KB x tens-of-thousands) pins the broker's cores; the attacher's QUIC subscribe IS delivered but its servicing work is starved for CPU and never finishes in the 30s watchdog. (Flood-volume-driven -> worse on the fast quiet box, matches the inversion + the 8x-more-pump-still-fails.)
  (B) BRIDGE HEAD-OF-LINE: the net host forwards the QUIC subscribe into the SAME single broker dispatch path / work-channel that's draining the UDS flood serially -> the subscribe queues behind ~25k flood ops and never gets its turn.

READING NOW (no code change): dispatch_net_stream_subscribe (broker.rs:1357) + how NetHost hands a QUIC net-stream subscribe to the broker core — is there a shared mpsc/work-queue or single dispatch thread that the UDS flood also feeds (=B), or per-conn threads that just lose the CPU race (=A)? I'll report the bridge structure so we know if the fix is "don't let one conn's flood monopolize broker dispatch CPU/queue" (fairness/yield/bounded-batch) vs a transport accept issue.

For your capture: grep WEDGE_TRACE subscribe (does it appear at all?) + add the driver op_id at the moment + whether dispatch_net_stream_subscribe is even entered (if you can trace/break there). That nails A vs B. Standing by + reading. -todlando
EOF

Ack + A/B hypotheses + announce bridge read


➜ SENT:doyle


Reading the net-stream-subscribe handler + the NetHost→broker bridge to determine A (CPU starvation) vs B (shared-dispatch head-of-line).


1240	    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
1241	        let (mut recv, send) = conn.split();
1242	        let send: SharedSend = Arc::new(Mutex::new(send));
1243
1244	        // Version handshake: this connection must be a brain (2.3).
1245	        recv_hello(&mut recv, Role::Brain)?;
1246
1247	        let mut my_subs: Vec<u64> = Vec::new();
1248	        let mut my_stream_subs: Vec<u64> = Vec::new();
1249	        let mut my_presence_sub = false;
1250
1251	        loop {
1252	            let env = match read_frame(&mut recv) {
1253	                Ok(e) => e,
1254	                // Any read failure (EOF included) ends the connection.
1255	                Err(_) => break,
1256	            };
1257	            match env.kind.as_str() {
1258	                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
1259	                    Ok(id) => my_subs.push(id),
1260	                    Err(msg) => send_error(&send, &msg),
1261	                },
1262	                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
1263	                    Ok(id) => my_subs.push(id),
1264	                    Err(msg) => send_error(&send, &msg),
1265	                },
1266	                KIND_UNSUBSCRIBE => {
1267	                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {
1268	                        send_error(&send, &msg);
1269	                    }
1270	                }
1271	                KIND_INPUT => {
1272	                    if let Err(msg) = self.dispatch_input(env, &send) {
1273	                        send_error(&send, &msg);
1274	                    }
1275	                }
1276	                KIND_RESIZE => {
1277	                    if let Err(msg) = self.dispatch_resize(env, &send) {
1278	                        send_error(&send, &msg);
1279	                    }

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            match env.kind.as_str() {
                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
                    Ok(id) => my_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_SUBSCRIBE => match self.dispatch_subscribe(env, &send) {
                    Ok(id) => my_subs.push(id),
                    Err(msg) => send_error(&send, &msg),
                },
                KIND_UNSUBSCRIBE => {
                    if let Err(msg) = self.dispatch_unsubscribe(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_INPUT => {
                    if let Err(msg) = self.dispatch_input(env, &send) {
                        send_error(&send, &msg);
                    }
                }
                KIND_RESIZE => {
                    if let Err(msg) = self.dispatch_resize(env, &send) {
                        send_error(&send, &msg);
                    }


handle_conn (1240) is per-conn (each conn its own thread); `KIND_NET_STREAM_SUBSCRIBE` is dispatched from it too (1357). The attacher is cross-broker (QUIC), so the question is how the target broker's NetHost bridges an incoming QUIC net-stream into dispatch — separate thread (A: CPU-starved) or shared queue behind the UDS flood (B: head-of-line). Reading the NetHost→broker bridge.


1://! The broker-owned network host (D4a) — ADR-0004 §B made real for the wire.
3://! [`NetHost`] is the broker's ownership of the node's live network presence:
6://! kind of resource ADR-0004 §B assigns to the broker — a consumer would lose
10://! and may detach/reattach freely; connections persist in the broker's table.
14://! The broker is otherwise sync/thread-based; iroh requires tokio. So the host
16://! (`spt-broker-net`): QUIC objects live in structures only the runtime's tasks
18://! host (broker teardown stops all net tasks). Spike #3's broker binary proved
19://! exactly this shape — a broker-owned endpoint surviving brain kill/restart.
24://! iroh endpoint, so broker ownership of the endpoint closes the ADR-0004 §B
44:use crate::broker::SharedSend;
52:/// The reserved [`crate::effect::EffectKey`] session namespace for net-scoped
55:pub const NET_EFFECT_SESSION: u64 = 0;
60:/// the broker replies, never an unbounded await that wedges the single-threaded
65:/// fires FIRST — the timeout then surfaces to the pump as a normal broker error
73:/// Everything needed to stand the broker's endpoint up: the node's identity
107:/// kind an inbound handshake can produce), or a broker-minted `Loopback` —
109:/// established BY CONSTRUCTION. A loopback conn can ONLY be minted broker-side
112:/// what lets a LOCAL `spt rc` ride the byte-identical `serve_attach` pump (M12
114:/// supported"), so local attach rides a broker loopback conn instead.
119:    /// A broker-minted in-process loopback (no iroh handle, no handshake). Its
125:/// One live connection the broker holds (QUIC or loopback).
130:    /// handshake; for loopback, THIS node's own id (the broker-minted identity).
165:    /// `retentive_cap = 0` (a slow/absent brain must not OOM the broker via an
265:/// A bounded, sequenced log of connection liveness events — the broker-owned
338:/// `serve_attach`/operator pump and the `StreamLog` read discipline are
379:/// One live stream the broker holds (QUIC or loopback): the send half (brain
385:    /// (accept loop). The inbound dispatcher (D9-1) claims only
387:    /// it holds locally-opened outbound streams too (gotcha: a dispatcher
388:    /// that served its own requester stream would deadlock on itself).
425:/// Register a live connection: mint its broker id and spawn its inbound-stream
427:/// `block_on` future) — it spawns tasks.
446:    // Presence: announce the row BEFORE spawning the closed-watcher, so the
462:    tokio::spawn(async move {
477:    tokio::spawn(async move {
493:/// Register a live stream (opened or accepted): mint its broker id and spawn

[Showing results with pagination = limit: 35]


Structural finding: `KIND_NET_STREAM_SUBSCRIBE` is dispatched **per-conn in handle_conn (1357)** — so on the local side it's the attacher's own conn thread, NOT shared with the driver's flood conn thread (weakens simple shared-dispatch head-of-line). The broker is sync-threaded + a separate tokio/iroh net runtime that's idle-epoll (has capacity). So a subscribe that *arrived* at dispatch would be served — pointing to it stalling in the loopback/net bridge before reaching dispatch, or CPU-starved. doyle's capture (does WEDGE_TRACE subscribe print at all) is decisive. Reporting + standing by — not rabbit-holing the bridge without his data.


$OWL send doyle <<'EOF'
BRIDGE STRUCTURE (from source, pending your capture):
- KIND_NET_STREAM_SUBSCRIBE is dispatched PER-CONN inside handle_conn (broker.rs:1357), i.e. on the ATTACHER conn's OWN thread — NOT the driver-flood conn's thread. So a naive "subscribe queued behind flood on one shared dispatch thread" (B) is NOT the obvious shape locally; each brain conn has its own handle_conn loop.
- Broker is sync/thread-per-conn; the iroh net endpoint is a SEPARATE tokio runtime, and your gdb shows it IDLE in epoll = it has spare capacity. So if the subscribe reached the net runtime / the broker dispatch, it would be served. Net-idle + subscribe-never-logged => it's NOT being delivered INTO dispatch under flood.
- net_dial_loopback = a broker-minted in-process LOOPBACK conn (nethost.rs:112-125, no iroh/handshake), riding serve_attach. So attacher+driver are on the SAME broker; the loopback subscribe still has to cross into the broker's stream handling.

So leading candidates now, your capture decides:
1. The attacher's loopback-conn handle_conn THREAD exists but loses the CPU race to the N flood-busy threads (decode_bytes@1630 on 16KB x 25k+) -> it's runnable-but-starved, subscribe arrives late >30s. (A, CPU.) -> gdb would show the attacher's handle_conn thread RUNNABLE (not parked), just not scheduled.
2. The loopback conn isn't fully established / its read side starves so read_frame never returns the SUBSCRIBE frame under flood. (transport/bridge.) -> attacher handle_conn parked in read_frame.

DECISIVE GREP on your instrumented no_ack capture:
- Does `WEDGE_TRACE subscribe` appear AT ALL? (never = stalls before dispatch_subscribe.)
- In your gdb, find the ATTACHER's handle_conn thread: is it RUNNABLE (=A, CPU starvation, fix=fairness/yield/bounded-batch in dispatch_input so one conn's flood can't monopolize cores) or PARKED in read_frame on the loopback recv (=transport/bridge, fix there)?
That A-vs-B is the whole fix direction. Standing by for the capture + that one thread's state. No code touched. -todlando
EOF

Send bridge structure + decisive A/B grep to doyle


<owl_messages>
<owl_message from="doyle" priority="high">INSTRUMENTED no_ack DATA changes the picture — and confirms your reframe (genuine product issue, test invariant VALID):
- subscribe sid=1 #1: drain_appends_so_far=4 (start), resolve_us=434, reply_send_us=30. fine.
- subscribe sid=1 #2 (the attacher): drain_appends_so_far=42464 (≈FLOOD END, ~t=30s), resolve_us=47498, reply_send_us=621, sessions_lock_wait=1us, log_lock_wait=1us.
So the subscribe RESOLVES cheap (47ms) with negligible lock waits — but it only gets DISPATCHED at flood-end, AFTER the 30s watchdog already declared subscribed=false. = HEAD-OF-LINE STARVATION: the attacher's subscribe REQUEST can't reach dispatch_subscribe for ~30s while the 40k-op flood monopolizes a shared serialization point. NOT lock contention (waits ~1us), NOT resolve cost — the request waits ~30s for its TURN.

Wait — these traced subscribes are the UDS dispatch_subscribe(1584) you instrumented. So net_dial_loopback is using the UDS FALLBACK (matches loopback_self_dial_is_refused_local_uses_fallback_transport), meaning the attacher IS on a UDS conn. So the HOL is on the UDS side.

NEXT PROBE (pin the HOL point, not resolve): timestamp (1) when the attacher's subscribe FRAME is READ by its conn handler (handle_conn read_frame) vs (2) when it reaches dispatch_subscribe. If read-late -> the attacher's conn isn't being READ (its handle_conn thread starved or the conn is shared w/ the flood = HOL on one conn). If read-early-dispatched-late -> a dispatch serialization point. ALSO: is there ONE shared lock/thread between dispatch_input (flood, 40k) and dispatch_subscribe? gdb showed Thread5 in dispatch_input actively; is the attacher's handle_conn even getting CPU/turns? Add a per-conn "frame read at t / dispatched at t" probe + identify any global dispatch mutex. Send the probe; I run it. We pin the HOL serialization point with data before any fix. -doyle</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


doyle (high-priority): **instrumented data pins it as HEAD-OF-LINE STARVATION, not locks.** The attacher's subscribe resolves cheap (47ms, sessions/log lock waits ~1µs) but is only DISPATCHED at flood-end (drain_appends 42464 ≈ t=30s), after the watchdog already fired. So the request waits ~30s for its TURN to even reach dispatch_subscribe. And it's the UDS dispatch_subscribe (net_dial_loopback fell back to UDS). My reframe holds: genuine product issue, test invariant valid.

Key tension: gdb showed only ~3-4 busy threads on a multi-core box — so simple CPU starvation shouldn't block the attacher's thread for 30s. Something gates when its frame is *read/dispatched*. doyle's probe (frame-read-time vs dispatch-time) is exactly right. Acking + building it.


$OWL send doyle <<'EOF'
ACK — HOL starvation confirmed by your numbers: subscribe resolves 47ms / locks ~1us, but only dispatched at drain_appends=42464 (flood-end, ~30s) = it waited ~30s for its TURN, AFTER the watchdog. Not lock, not resolve. UDS dispatch_subscribe (loopback->UDS fallback). Genuine product issue, invariant valid — agreed.

PUZZLE to resolve with the probe: your gdb showed only ~3-4 broker threads busy on a MULTI-CORE box, so the attacher's handle_conn thread shouldn't be CPU-starved for 30s if it's merely runnable — something gates WHEN its subscribe frame is read/dispatched. Candidates:
(A) the SERVE ACCEPT loop is single-threaded + starved/blocked, so the attacher's conn handler thread isn't SPAWNED until flood-end.
(B) the handler thread spawned, but recv_hello/read_frame on the attacher conn blocks (conn setup / a write-back serialized behind the flood).
(C) a global per-broker serialization in the hot dispatch path I haven't spotted.

PROBE (building now, pushing as an updated branch so you don't hand-patch): two cheap eprintlns gated to SPT_WEDGE_TRACE + only the low-volume frames (NOT the 42k inputs):
1. handle_conn ENTRY: `WEDGE_TRACE conn_open drain_appends=<N>` — when does the attacher's conn get its handler thread vs flood progress? (late = accept-starved = A)
2. handle_conn reads a SUBSCRIBE/NET_STREAM_SUBSCRIBE frame: `WEDGE_TRACE conn_read kind=.. drain_appends_at_read=<N>` — conn_open early but read late = handler thread blocked/starved reading (B); read≈resolve (both ~42k) pins the wait to pre-dispatch.

ALSO grab in gdb: the ATTACHER's handle_conn thread specifically — RUNNABLE (CPU-starved, but then why on multi-core?) or PARKED (in read_frame / recv_hello / accept)? That + the probe nails A/B/C.

Pushing branch wedge-trace-v2 (off 71378d8 = no_ack HEAD, so it matches what you're running) with these probes in ~2 min. -todlando
EOF

Ack + conn-open/read probe plan to doyle


Building `wedge-trace-v2` off 71378d8 (no_ack HEAD) with the existing instrumentation + new handle_conn probes. Creating the branch + cherry-picking the instrumentation commit.


cd "C:\Users\decid\Documents\projects\spt-core" && INSTR=$(git rev-parse wedge-trace-instrumentation) && echo "instr commit: $INSTR" && git checkout -b wedge-trace-v2 2>&1 | tail -1 && git cherry-pick "$INSTR" 2>&1 | tail -4

Create wedge-trace-v2 + cherry-pick instrumentation


instr commit: ce48da961a0eb4393cadace7b3b0478cab311bfb
Switched to a new branch 'wedge-trace-v2'
[wedge-trace-v2 2d8bc49] DIAGNOSTIC (do not merge): SPT_WEDGE_TRACE instrumentation for the sustained-input-wedge root-cause
 Date: Sat Jun 20 22:34:29 2026 -0700
 1 file changed, 81 insertions(+), 7 deletions(-)


Cherry-pick clean (no conflict). Now adding the handle_conn probes — conn-open + post-hello + subscribe-frame-read, all vs flood progress.


Edit

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // DIAGNOSTIC (SPT_WEDGE_TRACE): when does THIS conn's handler thread start +
        // clear the handshake, vs flood progress (drain appends)? If the attacher's
        // conn_open/conn_hello only print near flood-end, the SERVE accept loop or the
        // handshake is starved behind the flood — that is the head-of-line point.
        if wedge_trace_on() {
            eprintln!(
                "WEDGE_TRACE conn_open drain_appends={}",
                WT_DRAIN_APPENDS.load(Ordering::Relaxed)
            );
        }
        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;
        if wedge_trace_on() {
            eprintln!(
                "WEDGE_TRACE conn_hello drain_appends={}",
                WT_DRAIN_APPENDS.load(Ordering::Relaxed)
            );
        }


<tool_use_error>File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.</tool_use_error>

Error: File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.


Cherry-pick shifted lines. Re-reading handle_conn on this branch.


Grep

fn handle_conn|recv_hello\(&mut recv|let env = match read_frame\(&mut recv\)|match env.kind.as_str\(\)


1256:    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
1261:        recv_hello(&mut recv, Role::Brain)?;
1268:            let env = match read_frame(&mut recv) {
1273:            match env.kind.as_str() {


1256	    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
1257	        let (mut recv, send) = conn.split();
1258	        let send: SharedSend = Arc::new(Mutex::new(send));
1259
1260	        // Version handshake: this connection must be a brain (2.3).
1261	        recv_hello(&mut recv, Role::Brain)?;
1262
1263	        let mut my_subs: Vec<u64> = Vec::new();
1264	        let mut my_stream_subs: Vec<u64> = Vec::new();
1265	        let mut my_presence_sub = false;
1266
1267	        loop {
1268	            let env = match read_frame(&mut recv) {
1269	                Ok(e) => e,
1270	                // Any read failure (EOF included) ends the connection.
1271	                Err(_) => break,
1272	            };
1273	            match env.kind.as_str() {
1274	                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
1275	                    Ok(id) => my_subs.push(id),

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            match env.kind.as_str() {
                KIND_SPAWN => match self.dispatch_spawn(env, &send) {
                    Ok(id) => my_subs.push(id),


Edit

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            match env.kind.as_str() {

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // DIAGNOSTIC (SPT_WEDGE_TRACE): when does THIS conn's handler thread start +
        // clear the handshake, vs flood progress (drain appends)? If the attacher's
        // conn_open/conn_hello only print near flood-end, the SERVE accept loop or the
        // handshake is starved behind the flood — that is the head-of-line point.
        if wedge_trace_on() {
            eprintln!(
                "WEDGE_TRACE conn_open drain_appends={}",
                WT_DRAIN_APPENDS.load(Ordering::Relaxed)
            );
        }
        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;
        if wedge_trace_on() {
            eprintln!(
                "WEDGE_TRACE conn_hello drain_appends={}",
                WT_DRAIN_APPENDS.load(Ordering::Relaxed)
            );
        }

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            // DIAGNOSTIC (SPT_WEDGE_TRACE): a subscribe FRAME was just read off this
            // conn — at what flood progress? read-late (≈flood-end appends) means this
            // handler thread was starved from reading for the whole flood (the HOL).
            if wedge_trace_on()
                && (env.kind == KIND_SUBSCRIBE || env.kind == KIND_NET_STREAM_SUBSCRIBE)
            {
                eprintln!(
                    "WEDGE_TRACE conn_read kind={} drain_appends_at_read={}",
                    env.kind,
                    WT_DRAIN_APPENDS.load(Ordering::Relaxed)
                );
            }
            match env.kind.as_str() {


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)

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            match env.kind.as_str() {

    fn handle_conn(self: &Arc<Self>, conn: Stream) -> io::Result<()> {
        let (mut recv, send) = conn.split();
        let send: SharedSend = Arc::new(Mutex::new(send));

        // DIAGNOSTIC (SPT_WEDGE_TRACE): when does THIS conn's handler thread start +
        // clear the handshake, vs flood progress (drain appends)? If the attacher's
        // conn_open/conn_hello only print near flood-end, the SERVE accept loop or the
        // handshake is starved behind the flood — that is the head-of-line point.
        if wedge_trace_on() {
            eprintln!(
                "WEDGE_TRACE conn_open drain_appends={}",
                WT_DRAIN_APPENDS.load(Ordering::Relaxed)
            );
        }
        // Version handshake: this connection must be a brain (2.3).
        recv_hello(&mut recv, Role::Brain)?;
        if wedge_trace_on() {
            eprintln!(
                "WEDGE_TRACE conn_hello drain_appends={}",
                WT_DRAIN_APPENDS.load(Ordering::Relaxed)
            );
        }

        let mut my_subs: Vec<u64> = Vec::new();
        let mut my_stream_subs: Vec<u64> = Vec::new();
        let mut my_presence_sub = false;

        loop {
            let env = match read_frame(&mut recv) {
                Ok(e) => e,
                // Any read failure (EOF included) ends the connection.
                Err(_) => break,
            };
            // DIAGNOSTIC (SPT_WEDGE_TRACE): a subscribe FRAME was just read off this
            // conn — at what flood progress? read-late (≈flood-end appends) means this
            // handler thread was starved from reading for the whole flood (the HOL).
            if wedge_trace_on()
                && (env.kind == KIND_SUBSCRIBE || env.kind == KIND_NET_STREAM_SUBSCRIBE)
            {
                eprintln!(
                    "WEDGE_TRACE conn_read kind={} drain_appends_at_read={}",
                    env.kind,
                    WT_DRAIN_APPENDS.load(Ordering::Relaxed)
                );
            }
            match env.kind.as_str() {

+        // DIAGNOSTIC (SPT_WEDGE_TRACE): when does THIS conn's handler thread start +

+        // conn_open/conn_hello only print near flood-end, the SERVE accept loop or the

+        // handshake is starved behind the flood — that is the head-of-line point.

+                "WEDGE_TRACE conn_open drain_appends={}",

+                WT_DRAIN_APPENDS.load(Ordering::Relaxed)

+                "WEDGE_TRACE conn_hello drain_appends={}",

+            // DIAGNOSTIC (SPT_WEDGE_TRACE): a subscribe FRAME was just read off this

+            // conn — at what flood progress? read-late (≈flood-end appends) means this

+            // handler thread was starved from reading for the whole flood (the HOL).

+                && (env.kind == KIND_SUBSCRIBE || env.kind == KIND_NET_STREAM_SUBSCRIBE)

+                    "WEDGE_TRACE conn_read kind={} drain_appends_at_read={}",

KIND_SPAWN => match self.dispatch_spawn(env, &send) {


Compile-check + validate the new probe lines on Windows, then push.


Bash

cd "C:\Users\decid\Documents\projects\spt-core" && cargo check -p spt-daemon 2>&1 | tail -3; echo "[check $?]"

Compile-check wedge-trace-v2 probes


Command running in background with ID: b3begjis1. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b3begjis1.output. You will be notified when it completes. To check interim output, use Read on that file path.


<task-notification>
<task-id>b3begjis1</task-id>
<tool-use-id>toolu_01W1XsZH6JfSFv2NoSLd8rcj</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b3begjis1.output</output-file>
<status>completed</status>
<summary>Background command "Compile-check wedge-trace-v2 probes" completed (exit code 0)</summary>
</task-notification>


1	    Checking spt-daemon v0.12.1 (C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon)
2	    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.65s
3	[check 0]
4

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


Prompt:
<task-notification>
<task-id>b3begjis1</task-id>
<tool-use-id>toolu_01W1XsZH6JfSFv2NoSLd8rcj</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b3begjis1.output</output-file>
<status>completed</status>
<summary>Background command "Compile-check wedge-trace-v2 probes" completed (exit code 0)</summary>
</task-notification>


Compiles clean. Validating the new probe lines on Windows.


cd "C:\Users\decid\Documents\projects\spt-core" && SPT_WEDGE_TRACE=1 cargo nextest run -p spt-daemon --test inject_control_wedge -E 'test(a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach)' --no-capture 2>&1 | grep -E "WEDGE_TRACE (conn_open|conn_hello|conn_read|subscribe)" | head -20

Validate new probe lines on Windows


Command running in background with ID: b0r460ojj. Output is being written to: C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b0r460ojj.output. You will be notified when it completes. To check interim output, use Read on that file path.


<system-reminder>Warning: the file exists but is shorter than the provided offset (1). The file has 1 lines.</system-reminder>


<task-notification>
<task-id>b0r460ojj</task-id>
<tool-use-id>toolu_01HxcomAUoLaarCcUvUfDHaY</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b0r460ojj.output</output-file>
<status>completed</status>
<summary>Background command "Validate new probe lines on Windows" completed (exit code 0)</summary>
</task-notification>


Compiling the test. Awaiting the probe-format validation.


1	WEDGE_TRACE conn_open drain_appends=0
2	WEDGE_TRACE conn_hello drain_appends=0
3	WEDGE_TRACE conn_open drain_appends=0
4	WEDGE_TRACE conn_hello drain_appends=0
5	WEDGE_TRACE conn_read kind=subscribe drain_appends_at_read=0
6	WEDGE_TRACE subscribe sid=1 sessions_lock_wait_us=0
7	WEDGE_TRACE subscribe sid=1 log_lock_wait_us=0 resolve_us=406 drain_appends_so_far=0
8	WEDGE_TRACE subscribe sid=1 reply_send_us=40
9	WEDGE_TRACE conn_open drain_appends=222
10	WEDGE_TRACE conn_hello drain_appends=222
11	WEDGE_TRACE conn_read kind=net-stream-subscribe drain_appends_at_read=224
12	WEDGE_TRACE conn_open drain_appends=224
13	WEDGE_TRACE conn_hello drain_appends=224
14	WEDGE_TRACE conn_read kind=net-stream-subscribe drain_appends_at_read=224
15	WEDGE_TRACE conn_open drain_appends=224
16	WEDGE_TRACE conn_hello drain_appends=224
17	WEDGE_TRACE conn_read kind=subscribe drain_appends_at_read=224
18	WEDGE_TRACE subscribe sid=1 sessions_lock_wait_us=0
19	WEDGE_TRACE subscribe sid=1 log_lock_wait_us=0 resolve_us=879 drain_appends_so_far=224
20	WEDGE_TRACE subscribe sid=1 reply_send_us=9638
21

WEDGE_TRACE conn_open drain_appends=0
WEDGE_TRACE conn_hello drain_appends=0
WEDGE_TRACE conn_open drain_appends=0
WEDGE_TRACE conn_hello drain_appends=0
WEDGE_TRACE conn_read kind=subscribe drain_appends_at_read=0
WEDGE_TRACE subscribe sid=1 sessions_lock_wait_us=0
WEDGE_TRACE subscribe sid=1 log_lock_wait_us=0 resolve_us=406 drain_appends_so_far=0
WEDGE_TRACE subscribe sid=1 reply_send_us=40
WEDGE_TRACE conn_open drain_appends=222
WEDGE_TRACE conn_hello drain_appends=222
WEDGE_TRACE conn_read kind=net-stream-subscribe drain_appends_at_read=224
WEDGE_TRACE conn_open drain_appends=224
WEDGE_TRACE conn_hello drain_appends=224
WEDGE_TRACE conn_read kind=net-stream-subscribe drain_appends_at_read=224
WEDGE_TRACE conn_open drain_appends=224
WEDGE_TRACE conn_hello drain_appends=224
WEDGE_TRACE conn_read kind=subscribe drain_appends_at_read=224
WEDGE_TRACE subscribe sid=1 sessions_lock_wait_us=0
WEDGE_TRACE subscribe sid=1 log_lock_wait_us=0 resolve_us=879 drain_appends_so_far=224
WEDGE_TRACE subscribe sid=1 reply_send_us=9638


Prompt:
<task-notification>
<task-id>b0r460ojj</task-id>
<tool-use-id>toolu_01HxcomAUoLaarCcUvUfDHaY</tool-use-id>
<output-file>C:\Users\decid\AppData\Local\Temp\claude\C--Users-decid-Documents-projects-spt-core\97787e5e-64b2-4afd-b3fe-becaa2ea8030\tasks\b0r460ojj.output</output-file>
<status>completed</status>
<summary>Background command "Validate new probe lines on Windows" completed (exit code 0)</summary>
</task-notification>


Probes validated on Windows — `conn_open`/`conn_hello`/`conn_read` (subscribe + net-stream-subscribe) all print with flood progress; everything early (low appends) → passes. On Linux under flood, the appends-at-each-stage will pin the HOL point. Committing + pushing wedge-trace-v2.


cd "C:\Users\decid\Documents\projects\spt-core" && git add crates/spt-daemon/src/broker.rs && git commit -q -F - <<'EOF'
DIAGNOSTIC (do not merge): add handle_conn HOL probes to SPT_WEDGE_TRACE (on no_ack base)

wedge-trace-v2: cherry-picks the SPT_WEDGE_TRACE instrumentation onto 71378d8 (the
no_ack base doyle is running) + adds handle_conn probes to pin the head-of-line
serialization that delays the attacher's subscribe ~30s (resolve is cheap 47ms /
locks ~1us, but it only dispatches at flood-end):
- conn_open: handler-thread entry vs flood progress (drain appends). Late => SERVE
  accept loop starved behind the flood.
- conn_hello: after recv_hello. open-early/hello-late => handshake blocked.
- conn_read (subscribe / net-stream-subscribe only — NOT the 42k inputs): when the
  subscribe FRAME is actually read. read-late (≈flood-end) => the handler thread was
  starved from reading for the whole flood = the HOL point.
Compare conn_read drain_appends vs the existing `subscribe ... drain_appends_so_far`
to localize the wait to pre-read (thread/accept starvation) vs pre-dispatch.

Run on kitsubito: SPT_WEDGE_TRACE=1 cargo nextest run -p spt-daemon --test
inject_control_wedge -E 'test(a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach)'
--no-capture  (and p0). Off by default, behaviour-neutral, never ships.

Co-authored by: todlando
EOF
git push -u origin wedge-trace-v2 2>&1 | tail -3

Commit + push wedge-trace-v2


branch 'wedge-trace-v2' set up to track 'origin/wedge-trace-v2'.
To https://github.com/SaberMage/spt-core.git
 * [new branch]      wedge-trace-v2 -> wedge-trace-v2


Pushed wedge-trace-v2. Returning to delivery branch + reporting to doyle (with run command + the net-stream-subscribe-vs-subscribe nuance the probe surfaced).


cd "C:\Users\decid\Documents\projects\spt-core" && git checkout v0.13.0-p1b-ack-deadlock 2>&1 | tail -1; git status -sb | head -1

Return to delivery branch


<owl_messages>
<owl_message from="doyle" priority="high">DECISIVE: cap@5000 + idle STILL FAILS (subscribed=false, pumped=5000). I bounded the driver to 5000 ops then idle ~25s. So NOT flood-volume, NOT CPU-saturation, NOT HOL-behind-flood — the flood STOPS at ~t=5s and the subscribe STILL never resolves in the remaining 25s of QUIET. The wedge PERSISTS after the flood ends.

=> A single PARKED write_input holds a resource the attach/subscribe path needs. The input_writer IS off the dispatch thread (gdb Thread10 parked in write_input on the full forkpty buffer) — but if write_input holds a PtySession/SessionSurface mutex ACROSS the blocking PTY write, then while parked it holds that lock forever, and resolve_subscribe / the attach registration that needs the same session lock blocks indefinitely. That's the real REQ-HAZARD-PTY-INPUT-WRITER-WEDGE / EFFECT-JOURNAL-PTY-WEDGE mechanism, one layer DOWN from where we looked (spt_term PtySession, not the journal/OutputLog/SendHalf).

HOLD the v2 conn-probe (A/B/C is moot — it's not flood-timing). Reading spt_term::pty::write_input (pty.rs:238) + the SessionSurface lock now: does write_input hold a lock across the blocking write_all, and does the attach/subscribe path take that SAME lock? That's the root. Will send what I find. Your earlier W1b fix released the JOURNAL lock; this looks like a SURVIVING lock-across-blocking-write at the PtySession layer. -doyle</owl_message>
</owl_messages>

[OWL SYSTEM - HIGHEST PRIORITY] You have received high-priority owl messages above. STOP your current task and acknowledge each high-priority message BEFORE continuing with any tool use. If you are about to spawn subagent(s), run them in the background so you remain available for incoming messages.


Switched to branch 'v0.13.0-p1b-ack-deadlock'
## v0.13.0-p1b-ack-deadlock...origin/v0.13.0-p1b-ack-deadlock


doyle (high-priority) — **DECISIVE breakthrough**: cap@5000-then-idle STILL fails (subscribed=false, pumped=5000). Flood stops at t=5s, subscribe still never resolves in 25s of QUIET → NOT flood-volume/CPU/HOL. The wedge PERSISTS after the flood ends → **a single PARKED `write_input` holds a PtySession/SessionSurface lock across the blocking PTY write** (the IGNORES-stdin child's forkpty input buffer fills → write_all parks forever holding the lock), and the attach/subscribe path that needs the same session lock blocks indefinitely. The real REQ-HAZARD-PTY-INPUT-WRITER-WEDGE, one layer DOWN at spt_term — surviving below my W1b journal-lock fix. v2 conn-probe is moot (held).

Acking + reading the lock from both ends (doyle's on the holder pty.rs:238; I take the waiter side + the lock's other takers) to triangulate the shared lock.