---
name: v013-w1b-effect-journal-wedge
description: v0.13.0 W1b — the REAL operator wedge root (apply_once double-fsync + lock-across-PTY-write); W1 was a gate escape
metadata: 
  node_type: memory
  type: project
  originSessionId: 33cb9dc5-01bd-432b-a436-73f043e8656c
---

v0.13.0 operator dogfood (2026-06-19) on the new build (8b5583e): STILL wedges — but NOT the W1 park. doyle /diagnose root-caused + MEASURED on the real Windows box.

ROOT (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE, minted SEED): `EffectJournal::apply_once` (effect.rs:168-188) holds ONE global mutex across `write_line(PENDING)` → `effect()` → `write_line(DONE)`, and `write_line` (235-239) does flush()+sync_all() = full fsync. So every PtyWrite effect (each keystroke, broker.rs:1257 ← attach.rs:197 send_effect) = 2 fsync under a global lock with the blocking PTY write INSIDE the lock. Two facets, one root:
- (A) STUTTER: measured fsync on %LOCALAPPDATA%\spt-core = median 6.5ms, spike 198ms; ×2/keystroke serialized → "100s of ms per keypress, worsens with volume".
- (B) HARD PERMANENT WEDGE: a blocked PtyWrite (ConPTY input buffer full) holds the lock forever → single-threaded dispatch (dispatch.rs serve_attach, applies input AND opens attaches) can't progress → every `spt rc --view/--take` dies `attach request: brain IPC read deadline elapsed` (confirmed 2× identical). Broker control-plane (KIND queries: endpoint list, daemon status) still answers = different thread.

REFUTES the W2-deferred ruling that park-(b)/(c) is "Windows-benign because ConPTY absorbs 4MiB" — refuted on the real box. DISTINCT from W1 ([[v0121-l0-attach-deadlock-root]] / output drain @8b5583e, correctly fixed; output = broker.rs:1106 append, no fsync). This is the INPUT/effect-journal path W1 never touched.

GATE-ESCAPE LESSON: W1's int (inject_control_wedge.rs) only asserted KIND_SESSIONS stays alive under a backed-up controller — it NEVER asserted a REAL rc-client attach actually receives PTY bytes. Green-but-broken, same pattern as [[v0121-realharness-reopen]]. W1b's gate MUST extend the fixture to a real backed-up PTY consumer + a real rc attach that (a) stays serviceable (no brain-IPC-deadline) AND (b) actually receives PTY.

FIX (dispatched todlando, builds as the FOUNDATION of W2 Layer C — same substrate "own every PTY write atomically" + the park bound): ≥ (1) don't hold the journal lock across effect() (reserve key + fsync PENDING, release, run effect, re-acquire to fsync DONE) + (3) drop per-keystroke fsync (PtyWrite is EPHEMERAL — lost keystroke on broker-crash is retyped, PTY state isn't rebuilt from keystroke replay; in-memory applied-set dedup suffices, broker-survives-brain is the anchor), keep fsync for NetSend/NetDial/Registry/Spool. (2) bound/fail-fast PtyWrite.

ALSO confirmed-but-secondary: stale controller latch (W5, REQ-HAZARD-DRIVEN-BY-SELFHEAL) — wall-a stuck "controlled by <thisnode>" after tab-close-while-wedged, plain rc refused; AND endpoint list shows driven_by '-' while rc sees it latched (state/display disagreement). Operator clarified the latch is NOT the perceived wedge (they --take/--view past it into the real data-plane wedge).

Operator recovery (temporary, re-wedges under load until W1b ships): `spt daemon stop && spt daemon start`, or kill the brain pid (broker survives + respawns). Operator daemon = new opbuild binary (C:\Users\decid\Documents\projects\spt-core-opbuild\target\release\spt.exe).

REFINEMENT 2026-06-21 (doyle settled todlando's fork on the W1b gate test `a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach`, inject_control_wedge.rs:489):
- FORK RESULT = (B) LIVE flood-volume, LOCKED. The committed flood child `yes FLOODFLOODFLOOD` (flood_spawn_req:126) floods stdout FOREVER — can't go idle without editing it. So drain thread is 100% serde-saturating at attach time. The child's stdout flood ALONE wedges a fresh rc --view attach; the journaled-input wedge is NOT necessary for the symptom. NO idle-at-attach repro exists → ZERO evidence for any (A) replay-path bug.
- "cap@5000" (my dossier) was WRONG number + wrong shape: real ring = DEFAULT_LOG_CHUNKS=4096 (broker.rs:80), an EVICTING VecDeque (append pop_front 306-307), never a "N-then-quiet" volume cap.
- CARRIER CONFOUND in the gate (must fix BEFORE wedge-trace-v4): attacher `operator = Brain::cold_start` = Whole conn (line 577), but got_output loop calls `read_event_until(now+250ms)` (633). File's OWN comment (833) says a Whole cold_start IGNORES the read deadline → read_frame blocks forever when no NetStreamData lands → 8s deadline never checked → result_tx.send (660) never runs → 30s watchdog → (false,false). So reported `subscribed=false` is a MEASUREMENT ARTIFACT not a subscribe failure (matches gdb: parked PAST net_stream_subscribe, broker ok=true 56us). Every other helper uses cold_start_pump; attacher is the lone Whole conn. FIX: switch attacher to cold_start_pump → true signal = subscribed=true, got_output=false → localizes wedge to OUTPUT delivery (6-stage serde fan-out, stages 1/3/5).
- PRODUCT BUG (crisp): a max-rate stdout child cannot be attached by a new rc --view — per-chunk serde fan-out can't push one frame through the pipe to the operator. Lead hypothesis: serve's viewer EVICTED at try_send past VIEWER_CHANNEL_DEPTH=256 (silent+permanent+no-resubscribe); 2nd: serve CPU-starved pre-forward. todlando cutting wedge-trace-v4 (c1/c2/c3 hop counters) + coalescing fix.

RESULT 2026-06-21 (wedge-trace-v4 @4ee7767, doyle ran on Windows kitsubito/HFENDULEAM, SPT_WEDGE_TRACE=1): GATE GREEN — pump fix ALONE flipped it. subscribed=true got_output=true, c1/c2/c3 all fired count=1 write_ok=true, NO c1-EVICT, journaled_ops_pumped=257, PASS 3.017s. Eviction lead NOT confirmed on Windows. (Benign `c3-DROP stream=1` = other direction, not operator's stream=2.) BUT this green is near-meaningless for the product wedge — DON'T re-baseline to "no bug":
- TRACK (B) RESOLVED: production rc attach uses the CORRECT carrier — rc.rs:742 `Brain::cold_start_pump(...,10s)` (its comment: "a Whole conn can't time out — nonblocking dead on Windows named pipes"). So the Whole-conn carrier artifact is TEST-ONLY; production rc is NOT affected → operator dogfood wedge is GENUINE (my track-B hypothesis refuted, good).
- SYMPTOM MAPS EXACTLY: operator "attach request: brain IPC read deadline elapsed" = rc.rs:766 map_err on `request_attach` (line 765) = the attach-OPEN ack, BEFORE subscribe (770). So broker dispatch never answers the open within 10s → single-threaded dispatch blocked at open = held journal lock (facet-B) parked in write_input.
- WHY GATE GREENS ON WINDOWS = UNDER-STRESS. Gate pumps 257×16KB paced (20ms read + sustained-not-storming) → ConPTY ABSORBS → input buffer never fills → write_input never parks → lock never held → attach-open serviced → green. Operator's real long session FILLED the ConPTY input buffer → parked → wedge. "ConPTY absorbs" is true for a bounded burst, FALSE for sustained real fill. The CPU-storm-fix pacing specifically AVOIDS filling the buffer = neutered the Windows repro (two needs in tension).
- VENUES: Linux/forkpty = tiny input queue → write_input parks easily → THE venue to prove the fix (run v4 on gravity-linux, expect pre-fix subscribed=false even with pump carrier). Windows needs a SEPARATE saturating variant (unpaced, fill-the-buffer) to repro the operator symptom. Pump fix is a keepable test-carrier bugfix but does NOT close the wedge; the real fix (lock off effect + drop per-keystroke fsync) stays UNPROVEN until Linux red→green.

MAJOR CORRECTION 2026-06-21 (doyle gater verdict — the facet-B fix is ALREADY SHIPPED + in HEAD; my "still required/unproven" framing above was WRONG, it conflated pre-fix code with current tree):
- The FIX SHIPPED @e404aeb (2026-06-19 05:34, "W1b — effect journal no longer wedges interactive input"), and e404aeb IS an ancestor of HEAD (wedge-trace-v4 / delivery / 18416fd). Verified in current-tree code: effect.rs apply_once = Phase1 reserve-under-lock (207-219) → RELEASE → Phase2 `effect()` OFF the lock (224) → Phase3 finalize (225-226); PtyWrite ephemeral (is_durable=false, 92-101) = no journal write, no fsync; UNIT-GATED by `apply_once_does_not_hold_the_journal_lock_across_effect` (effect.rs:523-572).
- SECOND layer (P0, REQ-HAZARD-PTY-INPUT-WRITER-WEDGE): broker.rs `InputWriter` (908) spawns a dedicated `input_writer` thread (927-930) = SOLE caller of blocking write_input; dispatch ENQUEUES via non-blocking try_send, bounded input_queue_depth(), DROP+INPUT_BACKPRESSURE on full (881,940-943). Dispatch thread cannot park on input. (Third layer = no-ack, REQ-HAZARD-INPUT-ACK-BACKPRESSURE.)
- ANCESTRY (load-bearing): operator's wedge build 8b5583e = 2026-06-19 03:16 ("W1 controller delivery off drain") had ONLY the W1 output fix, NOT W1b. e404aeb landed 2h LATER (05:34). `merge-base --is-ancestor 8b5583e e404aeb` = TRUE. So the OPERATOR WEDGED ON A PRE-W1b-FIX BUILD. No post-fix wedge evidence exists. The symptom→rc.rs:766 (attach-OPEN ack timeout) mapping was correct, but the lock-held MECHANISM was the PRE-fix one.
- RECONCILED, no unfixed product wedge: Windows gate red = carrier artifact (Whole conn ignores deadline) → FIXED by v4 pump switch. Linux gate red = (iii) load-starvation (ci.yml:93-100) → FIXED by two-phase split + pacing. facet-B (i) = already fixed (3 layers), never the cause of recent reds. output-throughput (ii) = real perf characteristic (gdb serde-saturation) but does NOT fail the gate in isolation = separate perf item.
- FALSIFIABLE TEST (isolated traced kitsubito run, PR #27): GREEN isolated → thesis CONFIRMED, v0.13.0 gate-side change = JUST the v4 pump-carrier fix, "lock-off-effect required" item CLOSED. RED + subscribed=false + c1/c2/c3 FLAT ZERO → a 4th serialization survives all layers, reopen. subscribed=true + got_output=false + counters CLIMB→PLATEAU → (ii) perf item, not a blocker. Awaiting tail.

RUN #27 RESULT 2026-06-21 (PR #27, wedge-trace-v4 @77378b1) = RED, but DECOMPOSES into 3 distinct NON-facet-B failures. THESIS HOLDS (zero facet-B reproduction; every subscribed=false is a carrier artifact). NO clean green yet:
1. Windows p0_paste_wedge subscribed=false/no-output = CARRIER ARTIFACT — p0_paste's attach operator is `Brain::cold_start` (Whole conn) at inject_control_wedge.rs:1837; 4ee7767 only pump-fixed a_journaled (line 580), p0_paste untouched. Fixed by todlando's 7a1cc68 ("pump-mode carrier on BOTH gates"). NOT product.
2. Linux registry::tests::concurrent_registration_never_locks = SqliteFailure(DatabaseBusy code 5) at spt-store/src/registry.rs:184 — PASSED on one runner, FAILED on other = busy_timeout(5000) exceeded under Phase A full-parallel load = SQLite load flake, unrelated to wedge. It failed the Phase A STEP → Linux job stopped → Phase B NEVER RAN on Linux → no warm Linux datapoint for either wedge gate. (Separate item; will keep masking Linux Phase B until quarantined / busy_timeout raised.)
3. Linux a_journaled (my isolated diagnostic) = subscribed=TRUE (NOT facet-B), c3 stream=2 live-send count=1 (ONE frame then nothing), drain appends=186000 acquire_wait_max_us=51493 (51ms drain OutputLog-lock waits). PASSED warm on Windows Phase B in 3.08s. = COLD-START artifact: I placed the diagnostic as the literal FIRST step (cold caches + fresh-build IO) — "quiet" but COLD, worst case for the `yes`-flood lock contention. No warm Linux confirmation (registry killed Phase B).
TWO METHODOLOGY LESSONS (mine): (a) cold-isolated ≠ warm-steady-state — a diagnostic must run WARM (add `cargo nextest --no-run` warmup before it, continue-on-error, decoupled from the flaky Phase A). (b) CI grep filter `'W1b GATE'` does NOT match the real summary line `W1b JOURNAL-WEDGE GATE` → LOST the GATE summary + assertion; correct capture = grep 'JOURNAL-WEDGE GATE|PASTE-WEDGE GATE|subscribed=|got_output=|assertion|panic|c1:|c2:|c3:|EVICT' AND exclude 'drain appends='.
NEXT: validate on 7a1cc68 (both gates pump-fixed, local/unpushed in shared checkout — coordinating with todlando, NOT git-operating on his branch) with the corrected warm diagnostic. Expected: both gates GREEN warm on Linux; one warm confirmation needed before calling (ii) output-throughput a non-issue on forkpty. SHARED-CHECKOUT NOTE: todlando operates in the same spt-core working tree (reflog shows his checkouts/commits); avoid branch-switching collisions — use a worktree if I must build.

WARM RUN RESULT 2026-06-21 — PIVOT (run 27898375946, PR #27, wedge-trace-v4 @e54bc51 = both gates pumped + registry knob + CI env + warm both-gate diagnostic). The "cold artifact" hypothesis is FALSIFIED — todlando was right to refuse the unverified dismissal. v0.13.0 is NOT a clean green:
- a_journaled WARM: subscribed=TRUE, attach_received_pty_output=FALSE → panic inject_control_wedge.rs:750 (got_output assert), FAIL 18.97s. Reproduced in BOTH warm diagnostic AND Phase B = steady-state.
- FIXED + VALIDATED this run: facet-B (subscribed=TRUE everywhere, never flat-zero); carrier (both gates pumped); registry SQLITE_BUSY (knob worked — NO registry failure, Phase A survived, Phase B ran). All keepable.
- NEW REAL FAILURE: 3 inject gates RED on attach OUTPUT-RECEIPT (subscribed=TRUE, got_output/attach_received=FALSE), warm + reproducible. PLATFORM MATRIX (Phase B): a_journaled Linux-FAIL/Windows-PASS(3.0s); g2_no_commit Linux-FAIL/Windows-PASS; p0_paste Linux-FAIL/Windows-FAIL (BOTH!); a_backed_up_controller PASS both (W1 output-off-drain CORE intact); g1_choreography PASS both.
- LOCALIZATION (a_journaled warm, c1/c2/c3 counters): c1=0, c2=0, c3=1. Only ONE c3 = stream=2 live-send seq=0 = the INITIAL ring-replay batch. c1 (serve_attach RECEIVED live Output) + c2 (serve FORWARDED) NEVER fired → serve_attach gets the seq=0 replay then ZERO live output for 18s under `yes` flood. Broken hop = UPSTREAM of c1 = the LIVE viewer fan-out (drain → viewer channel → viewer_writer → serve subscription). c1-EVICT counter did NOT print (so either viewer_writer starved or eviction uncounted).
- TWO DISTINCT BUGS likely: (1) a_journaled/g2 forkpty-only live-output-to-attach starvation under flood (a_backed_up_controller passing = W1 core fine, so narrow to the live-output-to-attach path; regression-vs-pre-existing UNRESOLVED — doyle offered to bisect W1 8b5583e vs parent). (2) p0_paste both-platform attach_received_output=false = DISTINCT cause, likely the paste scenario's output-receipt expectation (parked-write child output dynamics) — assert may be wrong for that case.
- OPEN: is got_output under INFINITE `yes` flood a fair gate or pathological/over-strict? todlando (impl owner) drives per-gate root-cause; doyle gates. GATER POSITION: will NOT sign v0.13.0 green with its own new gates RED warm+reproducible. Path = per-gate resolution (real output-delivery fix if regression, or corrected gate expectation if pathological). Keep c1/c2/c3 counters on the diagnostic branch until closed. Awaiting todlando's regression-vs-pre-existing read.

ROOT CAUSE b4 (doyle, code-grounded at HEAD 2026-06-21) — THE DRAIN IS COUPLED TO THE CONTROLLER'S DRAIN RATE:
- broker.rs:1450-1457 drain closure: `let job = log.lock().append(chunk)` (viewer fan-out try_send happens INSIDE append) THEN `job.deliver()` INLINE. deliver() (broker.rs:669) is a try_send POLL LOOP: on a FULL controller channel it thread::sleep(CONTROLLER_SEND_POLL) and re-polls until CONTROLLER_WRITE_DEADLINE (5s). It runs INLINE in the drain closure → when the controller channel is full the drain thread is STUCK in deliver() and processes NO further chunks → append()/viewer try_send doesn't run → VIEWER STARVES. Drain throughput is rate-limited to the controller's drain speed.
- WHY a_journaled hits it: controller = the SPAWNER conn (dispatch_spawn become_controller by=None); the spawner is a Whole `cold_start` that spawn_sessions then NEVER READS again → its controller channel fills under the `yes` flood → deliver() blocks → drain throttled → attacher(viewer) c1≈0. EVICT=0 because the viewer try_send, when it rarely runs, SUCCEEDS (viewer channel not full — drain too slow to fill it).
- FITS ALL SYMPTOMS: c1=0/c2=0/c3=1(seq0 replay)/EVICT=0, AND forkpty-only for a_journaled/g2 (forkpty `yes` floods harder than Windows ConPTY's paced output → controller channel fills on Linux, keeps up on Windows → a_journaled PASSES Windows). Reframes todlando's b1: drain IS try_sending to viewer, but the drain LOOP is throttled by the inline controller deliver.
- REGRESSION VERDICT: viewer try_send loop UNCHANGED by W1 (8b5583e restructured only the controller path; viewer fan-out predates it). But W1 INTRODUCED the inline per-chunk job.deliver() in the drain closure (pre-W1 = a single inline controller write_frame, also blocking → pre-W1 a full controller wedged the drain PERMANENTLY; W1 bounded to 5s episodes but STILL couples drain throughput to the controller). Not a new bug class; W1's bounded-coupling throttles viewers when a controller backs up; a_journaled amplifies with a deliberately non-draining spawner-controller.
- FIX DIRECTION (doyle suggested, todlando owns): decouple deliver() from the drain loop — hand the controller job to its writer thread WITHOUT the drain blocking, or make the drain's controller hand-off truly non-blocking (try_send + evict-on-full like the viewer path) instead of poll-to-5s-deadline. v5 probe: measure time-in-deliver()/Full-poll-count per chunk to confirm b4 (not just viewers.len()+try_send tally).

FIX RESHAPE + DOYLE GATE BLOCK 2026-06-21 (b4 confirmed by todlando, credited doyle). todlando's reshape: delete ControllerJob::deliver()'s 5s inline sleep-poll; move controller send INTO append() under the log lock as ONE non-blocking try_send (viewers already do this); ControllerSink += last_ok:Instant; on Full → DROP unless now-last_ok>=CONTROLLER_WRITE_DEADLINE → evict; on Disconnected → evict; append returns Option<evict-epoch>; drain does NOT block. GREEN parts: delete sleep-poll, single try_send under lock, last_ok bounded-detach, epoch gate/one-live-writer/monotonic-seq unchanged.
DOYLE GATE BLOCK (B2 gapless-handoff re-break): the reshape as written advances delivered_through PAST a dropped frame. delivered_through (broker.rs:235-236) IS the resume_seq a COLD brain reads (KIND_SESSIONS). advanced_cursor (broker.rs:263) = current.max(seq+1) = HIGH-WATERMARK. controller_writer FIFO: drain drops seq N (Full), enqueues N+1 later → writer writes N+1 → cursor jumps to N+2, SKIPPING N → cold-brain resume from N+2 never replays N → incoherent screen = non-gapless/not-exactly-once = B2 violation (load-bearing, broker.rs:8-30, KNOWN-HAZARD). todlando's "dropped frame never advances" is true per-frame but the NEXT write advances the watermark past the gap.
REQUIRED FIX (doyle gate, todlando picks): (a) PREFERRED — make delivered_through CONTIGUOUS: controller_writer advances ONLY when writing seq==cursor (next expected); a gap (from drop) FREEZES cursor at last-contiguous → resume always gapless, fallen-behind controller catches up via ring replay / D4 floor-clamp = exactly the constraint-2 degradation, now correct; backward-compatible (no-drop case has no gaps). (b) flag controller "behind" → resume ignores watermark, replays from ring floor (messier). Ship reshape + (a) = all 3 doyle constraints + B2 hold. Awaiting todlando (a)-vs-(b). Run #3 (27898894089) counters still QUEUED (shared runner saturated) = corroboration only, b4 locked from code.

GATE RESOLVED + 3-MECHANISM FINDING 2026-06-21 (run #3 counters landed). todlando ACCEPTED the B2 gate, took (a) contiguous cursor. FINAL GATED-GREEN reshape (todlando cutting on fresh context after commune+self-clear; design in V0.13.0-VIEWER-DRAIN-DECOUPLE-JIT.md): (1) delete deliver()'s inline 5s sleep-poll; (2) single non-blocking try_send under the log lock in append(); (3) ControllerSink.last_ok staleness → CONTROLLER_WRITE_DEADLINE continuous-Full → evict; (4) epoch/one-live-writer/monotonic-seq unchanged; (5) (a) CONTIGUOUS delivered_through (advance only seq==cursor, freeze on gap). + a b4 in-proc unit test + traceable req + v5 per-chunk tally (doyle wires AFTER push, on the diagnostic branch, to avoid colliding with the rewrite).
CRITICAL — the b4 fix is NECESSARY but NOT SUFFICIENT. The 3 RED forkpty gates have 3 DISTINCT signatures; b4 closes ONLY a_journaled:
- a_journaled: c1=0/EVICT=0/got_output=false = b4 drain-throttle (controller deliver blocks drain; viewer never fanned). ✅ b4 fix closes.
- p0_paste: EVICT FIRED ("dropped 1 viewer at seq 35584, drain_appends=35585", pumped=35658, attach_received_output=false, backpressured=true), FAILS BOTH PLATFORMS = drain ran FREE, the VIEWER (serve_attach's broker subscription) overflowed its 256 channel + was EVICTED because serve couldn't FORWARD fast enough (read→b64→net_stream_send < drain fan-out). ❌ b4 does NOT touch it. Separate root: serve_attach forward throughput. Triage: re-subscribe-on-evict / deeper-coalescing viewer feed / OR relax assert (evicting a ~35k-behind viewer = correct session-protection).
- g2: assert :1196 = `assert!(delivered)` = broker DID NOT ACCEPT the injected endpoint event in 8s (NOT got_output). Forkpty-only. Likely inject-acceptance/dispatch path CPU-starved by the output-flood serde-storm. ❌ distinct; b4 may help indirectly (unconfirmed). Triage: likely a REAL fix ("must be able to message a busy session"), not a gate relax.
v0.13.0 CLOSE = b4 (locked+gated) + p0_paste decision + g2 fix/decision = THREE items, not one. Counters saved /tmp/run29.log. doyle wires v5 tally on todlando's resume/push.

TRIAGE RESOLVED 2026-06-21 (todlando triage, doyle gate):
- a_journaled = b4. todlando CUTTING b4+(a) now.
- p0_paste = SKIP-TO-LIVE (doyle gate: silent-permanent eviction is a REAL bug, NOT relax). Eviction itself = correct session-protection; the bug is it's silent+permanent. Right product = tail -f / skip-to-live: on eviction serve_attach re-subscribes from the current ring floor → sees LIVE output, doesn't die replaying an uncatchable backlog. B2-SAFE (viewers don't advance delivered_through / not authoritative — B2 is controller/brain-restart only). Design gate: PREFER explicit broker→viewer eviction SIGNAL (deterministic) over serve stall-detection (fragile); eviction marker unambiguous vs session-EOF; re-subscribe must NOT busy-loop under sustained flood (forward-progress/rate-limit, degrade to intermittent live bursts). KEEP the gate assertion (it asserts a legit property: view of a busy session must show live output).
- g2 = RE-MEASURE POST-b4 before designing. b4 deletes the 5s inline deliver-poll hogging the drain thread; g2's inject-acceptance (delivered=false, :1196) is plausibly starved by exactly that CPU storm → b4 may close it indirectly. If still red post-b4 → real inject-acceptance fix (dispatch_endpoint_input must not starve under output load). Don't design blind.
NEXT (doyle, on todlando b4 push): wire v5 per-chunk tally on the diagnostic branch + re-measure all 3 gates post-b4 (expect a_journaled GREEN, g2 maybe GREEN, p0_paste still RED until skip-to-live). PR #27 / worktree spt-core-wedgeval stay live for this.

POST-b4 RE-MEASURE 2026-06-21 (b4=677c3e7 cherry-picked onto validation branch wedge-trace-v4 @eb73da7; doyle resolved the drain-closure conflict keeping b4 + the drain-trace, added v5 ctrl tally; compile-clean):
- b4 WORKS (drain decoupled, PROVEN): Linux a_journaled drain_appends=88902 (pre-b4 throttled to ~0), c1-EVICT now FIRES. Controller no longer throttles the drain.
- a_journaled CONVERTED (not closed): Linux now subscribed=true, drain FREE, VIEWER EVICTED (c1-EVICT @ seq88901), attach_received=false = the SAME signature as p0_paste. So a_journaled + p0_paste are now ONE root → SKIP-TO-LIVE closes BOTH.
- g2 STILL RED Linux post-b4 (delivered=false, 16.7s). b4 did NOT close it → real inject-acceptance fix needed (dispatch_endpoint_input must not starve under output load).
- ⚠️ WINDOWS a_journaled FLIPPED pass→fail post-b4: pre-b4 PASS 2.99s subscribed=true; post-b4 FAIL 5.59s, journaled_ops_pumped=249 (Linux 11398), subscribed=FALSE, attach_received=false. subscribed=false = UPSTREAM of output (attach setup, NOT viewer-eviction); failed at 5.59s « 30s watchdog → EARLY assertion/return (request_attach/net_dial/subscribe failing fast). POSSIBLE b4 Windows interaction (controller try_send now under the log lock; todlando's local unit tests are in-process, not real-PTY-on-Windows). NOT dismissing as flake without proof. Windows p0_paste now PASSES (was fail) — b4 helped there.
- v5 ctrl tally didn't print (likely controller evicted to None early → try_send arm stopped; drain_appends+c1-EVICT already prove decouple; non-blocking).
DOYLE ACTION: enabled WEDGE_TRACE diagnostic on BOTH platforms (@6a27b0b) + re-running to localize the Windows subscribed=false + confirm reproducibility. GATE HOLD: do NOT fold b4 to delivery-control until the Windows a_journaled subscribed=false is resolved (flake vs b4-regression). v0.13.0 remaining = skip-to-live (closes a_journaled+p0_paste) + g2 inject-acceptance + Windows-subscribed=false resolution.

RESOLUTION 2026-06-21 (2 more runs):
- Windows a_journaled = FLAKE, NOT b4-regression. Re-run: Windows a_journaled PASS 4.7s, p0_paste PASS, g2 PASS. Prior subscribed=false/pumped=249 = one-off shared-hfenduleam load-contention flake. b4 does NOT regress Windows. (My both-platform diagnostic parse-errored on Windows: pwsh can't parse the bash `\` heredoc body → reverted diagnostic to Linux-only @c240643.)
- ⚠️ REAL b4 REGRESSION (deterministic, proven 3 runs): w5_a2_abandoned_remote_controller_idle_session_keeps_controller_by_some. run29 PRE-b4 Linux PASS 4.062s → run30 POST-b4 Linux FAIL → run31 POST-b4 Linux FAIL. Pass→fail→fail across b4 boundary. ASSERTS (inject_control_wedge.rs:1671-1676): an abandoned remote controller on an IDLE session must STILL read controller_by==Some ("slot does NOT self-clear without output-drain-evict or clean EOF"). Post-b4 it SELF-CLEARS → fails. NUANCE: w5_a2's OWN comment (1664-1670) says the production fix "must EVICT the dead controller broker-side" — b4 ADDS exactly a staleness eviction (last_ok past CONTROLLER_WRITE_DEADLINE), so b4 may have SERENDIPITOUSLY closed the W5 abandoned-controller gap this test characterized → update the obsolete test. COUNTER: confirm HOW an IDLE session triggers b4's output-gated evict (if it evicts with no output pressure = too aggressive). EITHER WAY b4 CANNOT FOLD with w5_a2 red — reconcile (fix b4 evict OR update w5_a2). Awaiting todlando's read.
- v0.13.0 BOARD: b4 works (drain decouple proven) + 4 reconciles before ship: skip-to-live (closes a_journaled+p0_paste, ONE fix), g2 inject-acceptance, w5_a2 reconcile (b4 side-effect), Windows shared-runner flakiness (separate CI-hygiene, not b4). Validation branch wedge-trace-v4 @c240643, worktree spt-core-wedgeval.
