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

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

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

## Root

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

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

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

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

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

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

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

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

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

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

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

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

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

## Traceability (CLAUDE.md binding)

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

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

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

## Files

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

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

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


Plan written. Per CLAUDE.md rule 3, add the requirement to `traceable-reqs.toml` FIRST. Let me see the existing format.


Bash

grep -n "REQ-HAZARD-VIEWER-ISOLATION\|REQ-HAZARD-VIEWER-STARVE\|REQ-RCVIEW-1" traceable-reqs.toml

Find viewer reqs in toml


522:id = "REQ-RCVIEW-1"
621:id = "REQ-HAZARD-VIEWER-ISOLATION"
1195:id = "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE"


1195	id = "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE"
1196	title = "A SLOW controller must not starve a concurrent `rc --view` VIEWER. W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) moved the controller SOCKET WRITE off the drain thread onto controller_writer, but left the bounded HANDOFF (ControllerJob::deliver) as an INLINE try_send SLEEP-POLL on the drain (broker.rs:1450-1457 → deliver:669-685, up to CONTROLLER_WRITE_DEADLINE=5s). So when a controller drains slower than the PTY floods, its CONTROLLER_CHANNEL_DEPTH(4096) channel fills, deliver() polls inline, and the DRAIN THREAD is throttled to the controller's read rate → OutputLog::append's viewer fan-out (try_send) stops running → a concurrent VIEWER receives only the initial replay then nothing (root 'b4', warm forkpty: a_journaled c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery; forkpty-only, floods harder than Windows ConPTY). The viewer-not-starved-by-a-busy-session property is legitimate (rc --view of a noisy session must show LIVE output). FIX: the controller becomes a SINGLE NON-BLOCKING try_send (like a viewer), done IN append() under the log lock; deliver()'s sleep-poll DELETED; the drain NEVER sleeps. ControllerSink gains a stateful last_ok deadline → a TRULY-stalled controller (continuous-Full past CONTROLLER_WRITE_DEADLINE) is evicted (bounded-wedge preserved); a slow-but-alive controller DROPS frames + falls behind the ring (resume-from-floor, the existing reconnect case). B2 GAPLESS-HANDOFF PRESERVED via a CONTIGUOUS delivered_through: controller_writer advances the cursor ONLY when the written seq == cursor (next expected); a gap from a drop FREEZES the cursor at last-contiguous so a re-attaching brain's resume_seq never skips a dropped chunk (a high-watermark advance past the gap would be a not-exactly-once resume = B2 violation, doyle's gate). (v0.13.0)"
1197	required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 impl+unit (todlando, doyle-gated). impl = broker.rs OutputLog::append (single non-blocking controller try_send + ControllerSink.last_ok staleness, returns Option<evict-epoch>), drain closure (mark_controller_gone on signal, never sleeps), controller_writer live-path CONTIGUOUS cursor advance (contiguous_advance: compare_exchange seq→seq+1, freeze on gap), ControllerJob::deliver sleep-poll DELETED. unit = a_full_controller_does_not_starve_a_viewer_or_block_the_drain (in-process OutputLog, no PTY) + contiguous_advance_freezes_on_a_gap kernel. INT NOT YET ACTIVATED (rule 5, activate-don't-pre-fail): the int evidence is inject_control_wedge.rs a_journaled (warm forkpty, kitsubito CI), which only goes GREEN on the COMBINED branch (pump-carrier-fix carrier+registry + this b4) — not on this isolated b4 branch. Activate `int` + tag the a_journaled gate when b4 folds to delivery-control alongside pump-carrier-fix and the forkpty gate is green.
1198
1199	# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──
1200	[[requirements]]
1201	id = "REQ-MSG-IDLE-TRANSLATION-BINARY"
1202	title = "spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:\"init\",endpoint_id,node}` first · `{type:\"event\",envelope:\"<EVENT…>\"}` per inbound message (ADR-0020 envelope) · `{type:\"input\"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. The v0.11.0 raw inject is the degenerate `{text:payload}{key:enter}` case. (v0.13.0)"
1203	required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 W2 (todlando) — all layers landed. Layers A(@c73eb6a)+B(@611205c)+D(@7166f52)+C-kernel(@79f5a40)+C-proper/E(@f9fe5bb)+G(int). impl = translation.rs (ToBinary/KeyCmd/key_to_bytes/TranslationChild/InjectFloor) + broker.rs (Translation/run_inject_worker/flush_inject_floor/fault_translation + dispatch_spawn bring-up + dispatch_input floor-buffer&ping + dispatch_endpoint_input event routing); unit = translation.rs (wire round-trips, content-free ping, KeyCmd shapes, InjectFloor buffer/drain, send-keys map). int = inject_control_wedge.rs g1-g4 via the REAL src/bin/xlate_choreo_fixture.rs translation binary: g1 choreography happy-path (delivered + session serviceable + fresh attach served, all platforms; byte-receipt of XLATE_OK hard on cfg(unix)/gravity-linux, captured on cfg(windows) ConPTY per the file's banked park-(b) divergence stance) · g2 no-commit FAULT (SPT_INJECT_COMMIT_DEADLINE_MS=400: buffered operator keys flush after deadline = no wedge + raw-inject fallback, hard both platforms) · g3 content-free ping (secret reaches PTY but NEVER the binary stdin-log + saw_input_ping non-vacuous, hard both) · g4 no-zombie reap on session-down (hard both). Layer F (Unix park b/c bound) = gravity-linux follow-up, shared w/ W1b F3, tracked by the cfg(unix) park-(b) assert. — staged: doc DONE (manifest section row in MANIFEST.md inject-seam family + CONTEXT.md:39 delivery section reworded W0 + ADR-0022 wording table-not-scalar + manifest.schema.json regen); impl/unit/int bump in as each layer lands (Layer A manifest section + validate + schema = impl/unit foundation; B translation.rs protocol+driver; C atomic PTY apply w/ controller buffering; D lifecycle via SpawnReq.translation_binary; E routing; F Unix park (b)/(c) bound; G int gate). int gate: a real dummy translation-binary delivers an inbound message into an rc-attached spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant). ALSO OWNS the Unix-only park (b)/(c) PTY-write bound deferred from W1 (doyle ruling 2026-06-19): the 'spt-core owns every PTY write, applied atomically' substrate is where the forkpty input-queue park gets bounded — bounded/fail-fast write_input + DSR-answer must not hold the writer mutex across a blocking write; gate on gravity-linux (the cfg(unix) park-(b) assert in inject_control_wedge.rs proves the park is real + keeps it tracked).
1204
1205	# ── W3: a fresh spt-hosted endpoint shows under its own project (info.cwd on bind) ──
1206	[[requirements]]
1207	id = "REQ-HAZARD-BIND-CWD-UNSET"
1208	title = "A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)"
1209	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W3 (todlando). ROOT (doyle RCA): info.cwd NEVER set on bind. FIX: establish_perch gained a trailing cwd param + sets rec.cwd = cwd.or_else(prior.cwd) (revive carries forward); cmd_bind (spt-hosted) passes its own current_dir (broker spawned it in project_cwd); bind_from_seed (harness-hosted) passes seed.cwd (captured at api seed, previously DISCARDED). impl = api/startup.rs establish_perch/cmd_bind/bind_from_seed. unit = startup.rs bind_records_cwd_so_picker_can_group_by_project + revive_without_cwd_preserves_prior_cwd + seed_bind_records_seed_captured_cwd (mutation-verified — each guards a distinct branch). int = bind_cwd_project_e2e.rs (REAL detached daemon + endpoint run: live perch read_info().cwd is Some+non-empty AND appears under its project via project_id_for_dir membership). Delivers the v0.12.1 P1 that was REFUTED in the changelog. NOTE: cmd_bind stores raw current_dir (native slashes) while cmd_seed forward-slash-normalizes — slash-agnostic through project_id_for_dir, follow-up to unify.
1210
1211	# ── W4: `spt endpoint run` picker UX ──
1212	[[requirements]]
1213	id = "REQ-PICKER-UX-V013"
1214	title = "`spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)"
1215	required_stages = []  # RETIRED/REDIRECTED (doyle W4 gate 2026-06-19): the W4 picker UX scope was DELIVERED as refinements of the EXISTING picker reqs, NOT under this id — fixes (1) open-on-Pick-existing, (2) attach-by-default + `h` headless, (4) terse bringup line landed under REQ-RUN-PICKER (v0.13.0-W4 extension); fix (3) controlled-by node-name landed under REQ-PICKER-4 (v0.13.0-W4 extension, the D item). This seed carries no evidence by design (folded, not duplicated) — kept as a redirect breadcrumb, not an open requirement. Live keypress path = operator HITL.
1216
1217	# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
1218	[[requirements]]
1219	id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
1220	title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
1221	required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1/A2 characterization (inject_control_wedge.rs w5_a1/w5_a2). EMPIRICAL VERDICT (2026-06-19, real broker): controller_by==None is NOT a usable Gap-A signal — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None) AND the production wedged-remote case (A2) keeps controller_by==Some(origin)+driven_by latched on an IDLE session (the broker still thinks it is controlled). A clean controller disconnect ALREADY self-heals via detach_if→clear_controller; W1 bounds the active-output wedge. The residual A2 (idle wedged REMOTE controller) needs BROKER-SIDE liveness eviction, NOT a brain reconcile controller_by read → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.
1222
1223	# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
1224	[[requirements]]
1225	id = "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT"
1226	title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
1227	required_stages = []  # SEED — repro already exists (inject_control_wedge.rs w5_a2 characterization, under REQ-HAZARD-DRIVEN-BY-SELFHEAL int). doyle BOUNDARY RULING (2026-06-19): does NOT fold into W6 — W6 is the idle-DELIVERY consumer (translation binary injection), orthogonal to controller liveness; and a pump→clean-detach can't heal A2 either (A2 is a HALF-OPEN conn — remote gone, NO EOF — so serve_attach's read just blocks, no detach signal to propagate). Needs the broker-side D4c NetPresence oracle above. Schedule as a controller-liveness follow-up post W4/W6 (likely v0.13.x); doyle tracks. Activate [impl,unit,int] when the eviction lands.
1228
1229	# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
1230	[[requirements]]
1231	id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
1232	title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
1233	required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."
1234

id = "REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE"
title = "A SLOW controller must not starve a concurrent `rc --view` VIEWER. W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) moved the controller SOCKET WRITE off the drain thread onto controller_writer, but left the bounded HANDOFF (ControllerJob::deliver) as an INLINE try_send SLEEP-POLL on the drain (broker.rs:1450-1457 → deliver:669-685, up to CONTROLLER_WRITE_DEADLINE=5s). So when a controller drains slower than the PTY floods, its CONTROLLER_CHANNEL_DEPTH(4096) channel fills, deliver() polls inline, and the DRAIN THREAD is throttled to the controller's read rate → OutputLog::append's viewer fan-out (try_send) stops running → a concurrent VIEWER receives only the initial replay then nothing (root 'b4', warm forkpty: a_journaled c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery; forkpty-only, floods harder than Windows ConPTY). The viewer-not-starved-by-a-busy-session property is legitimate (rc --view of a noisy session must show LIVE output). FIX: the controller becomes a SINGLE NON-BLOCKING try_send (like a viewer), done IN append() under the log lock; deliver()'s sleep-poll DELETED; the drain NEVER sleeps. ControllerSink gains a stateful last_ok deadline → a TRULY-stalled controller (continuous-Full past CONTROLLER_WRITE_DEADLINE) is evicted (bounded-wedge preserved); a slow-but-alive controller DROPS frames + falls behind the ring (resume-from-floor, the existing reconnect case). B2 GAPLESS-HANDOFF PRESERVED via a CONTIGUOUS delivered_through: controller_writer advances the cursor ONLY when the written seq == cursor (next expected); a gap from a drop FREEZES the cursor at last-contiguous so a re-attaching brain's resume_seq never skips a dropped chunk (a high-watermark advance past the gap would be a not-exactly-once resume = B2 violation, doyle's gate). (v0.13.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 impl+unit (todlando, doyle-gated). impl = broker.rs OutputLog::append (single non-blocking controller try_send + ControllerSink.last_ok staleness, returns Option<evict-epoch>), drain closure (mark_controller_gone on signal, never sleeps), controller_writer live-path CONTIGUOUS cursor advance (contiguous_advance: compare_exchange seq→seq+1, freeze on gap), ControllerJob::deliver sleep-poll DELETED. unit = a_full_controller_does_not_starve_a_viewer_or_block_the_drain (in-process OutputLog, no PTY) + contiguous_advance_freezes_on_a_gap kernel. INT NOT YET ACTIVATED (rule 5, activate-don't-pre-fail): the int evidence is inject_control_wedge.rs a_journaled (warm forkpty, kitsubito CI), which only goes GREEN on the COMBINED branch (pump-carrier-fix carrier+registry + this b4) — not on this isolated b4 branch. Activate `int` + tag the a_journaled gate when b4 folds to delivery-control alongside pump-carrier-fix and the forkpty gate is green.

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──
[[requirements]]
id = "REQ-MSG-IDLE-TRANSLATION-BINARY"
title = "spt-hosted idle message delivery via an adapter TRANSLATION BINARY (ADR-0022). New opt-in manifest section `[message-idle-translation-binary]` = a TABLE carrying a `path` scalar (doyle OPT-B ruling: modeled as a table, not a bare top-level scalar, so a preceding section cannot silently absorb it + N+1 extensible; spt-core does NOT deny_unknown_fields, so a future key degrades gracefully); spt-core LIFECYCLE-manages it (spawn when the endpoint comes up, terminate when it goes down). The binary is a PURE stdin→stdout filter; spt-core owns EVERY PTY write. stdin (JSON-lines): `{type:\"init\",endpoint_id,node}` first · `{type:\"event\",envelope:\"<EVENT…>\"}` per inbound message (ADR-0020 envelope) · `{type:\"input\"}` content-free ping on each operator keystroke (binary tracks user-idle for its own idle-gated buffering; PTY input content NOT duplicated). stdout (JSON-lines): keystroke-commands `{key:…}`/`{delay_ms:…}`/`{text:…}` (extensible). spt-core applies the emitted sequence to the broker PTY ATOMICALLY (the W1 coordination — REQ-HAZARD-INJECT-CONTROL-COEXIST). The daemon poll feed is the ONE idle substrate for both topologies (Q1=A): harness-hosted consumer = the Monitor child, spt-hosted consumer = this binary; spt-core PREFERS a perch's poll listener if one exists (so spt-hosted can run a listener AND keep `spt rc`). Idle-only; busy/mid-turn = adapter hook-injection. Closes the current grounding gap: `api bind` registers no listener port → a listener-less spt-hosted perch SPOOLS inbound (only spooling+adapter-poll works today) → this delivers real inbound into the PTY. The v0.11.0 raw inject is the degenerate `{text:payload}{key:enter}` case. (v0.13.0)"
required_stages = ["doc", "impl", "unit", "int"]  # ACTIVATED v0.13.0 W2 (todlando) — all layers landed. Layers A(@c73eb6a)+B(@611205c)+D(@7166f52)+C-kernel(@79f5a40)+C-proper/E(@f9fe5bb)+G(int). impl = translation.rs (ToBinary/KeyCmd/key_to_bytes/TranslationChild/InjectFloor) + broker.rs (Translation/run_inject_worker/flush_inject_floor/fault_translation + dispatch_spawn bring-up + dispatch_input floor-buffer&ping + dispatch_endpoint_input event routing); unit = translation.rs (wire round-trips, content-free ping, KeyCmd shapes, InjectFloor buffer/drain, send-keys map). int = inject_control_wedge.rs g1-g4 via the REAL src/bin/xlate_choreo_fixture.rs translation binary: g1 choreography happy-path (delivered + session serviceable + fresh attach served, all platforms; byte-receipt of XLATE_OK hard on cfg(unix)/gravity-linux, captured on cfg(windows) ConPTY per the file's banked park-(b) divergence stance) · g2 no-commit FAULT (SPT_INJECT_COMMIT_DEADLINE_MS=400: buffered operator keys flush after deadline = no wedge + raw-inject fallback, hard both platforms) · g3 content-free ping (secret reaches PTY but NEVER the binary stdin-log + saw_input_ping non-vacuous, hard both) · g4 no-zombie reap on session-down (hard both). Layer F (Unix park b/c bound) = gravity-linux follow-up, shared w/ W1b F3, tracked by the cfg(unix) park-(b) assert. — staged: doc DONE (manifest section row in MANIFEST.md inject-seam family + CONTEXT.md:39 delivery section reworded W0 + ADR-0022 wording table-not-scalar + manifest.schema.json regen); impl/unit/int bump in as each layer lands (Layer A manifest section + validate + schema = impl/unit foundation; B translation.rs protocol+driver; C atomic PTY apply w/ controller buffering; D lifecycle via SpawnReq.translation_binary; E routing; F Unix park (b)/(c) bound; G int gate). int gate: a real dummy translation-binary delivers an inbound message into an rc-attached spt-hosted session via the choreography WITHOUT breaking control (composes with W1's invariant). ALSO OWNS the Unix-only park (b)/(c) PTY-write bound deferred from W1 (doyle ruling 2026-06-19): the 'spt-core owns every PTY write, applied atomically' substrate is where the forkpty input-queue park gets bounded — bounded/fail-fast write_input + DSR-answer must not hold the writer mutex across a blocking write; gate on gravity-linux (the cfg(unix) park-(b) assert in inject_control_wedge.rs proves the park is real + keeps it tracked).

# ── W3: a fresh spt-hosted endpoint shows under its own project (info.cwd on bind) ──
[[requirements]]
id = "REQ-HAZARD-BIND-CWD-UNSET"
title = "A bound endpoint's `info.cwd` is SET at bind so a freshly-created perch appears under its own project tab. ROOT (found, v0.13.0): `info.cwd` is NEVER set on bind — `cmd_bind` (spt-hosted) and `bind_from_seed` (harness-hosted) never thread cwd into `establish_perch`/`rec.cwd`. FIX: `cmd_bind` reads its own `current_dir` (the broker spawned it in `project_cwd`); `bind_from_seed` passes `seed.cwd` (already captured at seed time, currently DISCARDED). DISTINCT from REQ-PICKER-HISTORY-FRESH (v0.12.1) — that unioned cwd-origin into picker MEMBERSHIP but tested merge_origin_project with a PROVIDED origin; it never asserted `info.cwd` is actually set on bind, so a real `endpoint run` perch still had an empty cwd and the union had nothing to union. This is the v0.12.1 P1 'appears under its own project right away' claim that was REFUTED in the changelog — delivered for real here. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W3 (todlando). ROOT (doyle RCA): info.cwd NEVER set on bind. FIX: establish_perch gained a trailing cwd param + sets rec.cwd = cwd.or_else(prior.cwd) (revive carries forward); cmd_bind (spt-hosted) passes its own current_dir (broker spawned it in project_cwd); bind_from_seed (harness-hosted) passes seed.cwd (captured at api seed, previously DISCARDED). impl = api/startup.rs establish_perch/cmd_bind/bind_from_seed. unit = startup.rs bind_records_cwd_so_picker_can_group_by_project + revive_without_cwd_preserves_prior_cwd + seed_bind_records_seed_captured_cwd (mutation-verified — each guards a distinct branch). int = bind_cwd_project_e2e.rs (REAL detached daemon + endpoint run: live perch read_info().cwd is Some+non-empty AND appears under its project via project_id_for_dir membership). Delivers the v0.12.1 P1 that was REFUTED in the changelog. NOTE: cmd_bind stores raw current_dir (native slashes) while cmd_seed forward-slash-normalizes — slash-agnostic through project_id_for_dir, follow-up to unify.

# ── W4: `spt endpoint run` picker UX ──
[[requirements]]
id = "REQ-PICKER-UX-V013"
title = "`spt endpoint run` picker UX (v0.13.0 operator dogfooding): (1) SKIP the first screen — open directly on 'Pick existing'; `n` jumps to 'Create new'. (2) AUTO-ATTACH after both Start-new AND Resume-from-history (both currently don't attach and show no stdout); add an `h` shortcut to run headless (no attach). (3) 'controlled by' shows the node NAME (node_label_display), not the raw hex. (4) Clean up Start-new output — drop the Rust `pid=Some(142748)` leak and the 'harness binds its perch on startup' internals; user-friendly, not a process log. (v0.13.0)"
required_stages = []  # RETIRED/REDIRECTED (doyle W4 gate 2026-06-19): the W4 picker UX scope was DELIVERED as refinements of the EXISTING picker reqs, NOT under this id — fixes (1) open-on-Pick-existing, (2) attach-by-default + `h` headless, (4) terse bringup line landed under REQ-RUN-PICKER (v0.13.0-W4 extension); fix (3) controlled-by node-name landed under REQ-PICKER-4 (v0.13.0-W4 extension, the D item). This seed carries no evidence by design (folded, not duplicated) — kept as a redirect breadcrumb, not an open requirement. Live keypress path = operator HITL.

# ── W5: driven_by self-heal (ONLINE+CONTROLLED clears without the detach IPC) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-SELFHEAL"
title = "An spt-hosted endpoint's ONLINE+CONTROLLED state (`driven_by`) must CLEAR even when the detach IPC is lost — do NOT rely on the detach signal (same lesson as REQ-HAZARD-HOSTED-LIVENESS-RECONCILE B2): the reconcile loop clears `driven_by` when the endpoint has no live controller/session. Today a wedged or lost pump never delivers the detach, so the endpoint stays latched CONTROLLED forever. Composes with W1 (the wedge no longer blocks the detach) and rides the same pull-primary reconcile substrate as B2. (v0.13.0)"
required_stages = ["impl", "unit", "int"]  # ACTIVATED v0.13.0 W5 (repro-first, todlando). SCOPE landed = Gap B (no-session self-heal): reconcile_hosted_liveness, when it offlines a sessionless controllable perch (B2 keystone), ALSO clears a stale driven_by (set_driven_by None) — an OFFLINE endpoint can no longer render phantom CONTROLLED. RACE-FREE (no live session ⇒ no controller to re-stamp concurrently). impl = livehost.rs reconcile_hosted_liveness Gap-B clear + the additive SessionInfo.controller_by observability field (broker.rs KIND_SESSIONS). unit = pull_liveness unit extended (offlined sessionless perch clears driven_by; live/relay perches untouched). int = real-broker gates: driven_by_selfheal.rs gap_b (GREEN: reconcile offlines AND clears driven_by) + the A1/A2 characterization (inject_control_wedge.rs w5_a1/w5_a2). EMPIRICAL VERDICT (2026-06-19, real broker): controller_by==None is NOT a usable Gap-A signal — it is AMBIGUOUS (dispatch_spawn pre-attaches the spawner with by=None, so a LIVE LOCAL session also reads None) AND the production wedged-remote case (A2) keeps controller_by==Some(origin)+driven_by latched on an IDLE session (the broker still thinks it is controlled). A clean controller disconnect ALREADY self-heals via detach_if→clear_controller; W1 bounds the active-output wedge. The residual A2 (idle wedged REMOTE controller) needs BROKER-SIDE liveness eviction, NOT a brain reconcile controller_by read → DEFERRED to REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT. KNOWN-HAZARDS 7.15.

# ── W5 follow-up: the idle wedged-REMOTE-controller latch (A2, repro-proven, deferred) ──
[[requirements]]
id = "REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT"
title = "An spt-hosted endpoint driven by a REMOTE controller whose remote is gone but whose broker connection stays OPEN (a wedged/lost pump that never delivers the detach) AND whose session is IDLE (no output) stays latched ONLINE+CONTROLLED forever: the W1 drain-evict only fires on OUTPUT (CONTROLLER_WRITE_DEADLINE on a backed-up write), a clean disconnect self-heals via detach_if→clear_controller, but an idle session with a half-open/wedged controller connection produces neither signal. PROVED repro-first on a real broker (v0.13.0 W5, inject_control_wedge.rs w5_a2): controller_by STAYS Some(origin) and driven_by STAYS Some after the remote is abandoned without a clean EOF on an idle session — so the brain reconcile CANNOT detect it from KIND_SESSIONS controller_by (the broker still reports it controlled). FIX DIRECTION (doyle ruling 2026-06-19, broker-side single-writer — the broker owns driven_by/clear_controller): wire the EXISTING D4c NetPresence connection-disconnect event → clear_controller for any session whose controller identity == the dead origin (become_controller already stores Some(origin); presence events already exist — modest wiring, NOT a new probe). The liveness ORACLE is QUIC's own keepalive/idle-timeout: a presence-disconnect IS a real QUIC conn close, already tolerant of transient blips within the keepalive window, so NO heavy partition ADR is needed UNLESS the QUIC timeout proves too slow for the UX (then mint an ADR for a faster controller-heartbeat + its false-evict bound). Composes with W1 (output path) + W5 Gap B (no-session) — this is the third, idle-remote, leg. (v0.13.0 follow-up)"
required_stages = []  # SEED — repro already exists (inject_control_wedge.rs w5_a2 characterization, under REQ-HAZARD-DRIVEN-BY-SELFHEAL int). doyle BOUNDARY RULING (2026-06-19): does NOT fold into W6 — W6 is the idle-DELIVERY consumer (translation binary injection), orthogonal to controller liveness; and a pump→clean-detach can't heal A2 either (A2 is a HALF-OPEN conn — remote gone, NO EOF — so serve_attach's read just blocks, no detach signal to propagate). Needs the broker-side D4c NetPresence oracle above. Schedule as a controller-liveness follow-up post W4/W6 (likely v0.13.x); doyle tracks. Activate [impl,unit,int] when the eviction lands.

# ── W7 (operator /diagnose 2026-06-19): rc Backspace key forwards the wrong byte ──
[[requirements]]
id = "REQ-HAZARD-RC-INPUT-KEY-ENCODING"
title = "An `spt rc` session forwards the Backspace key as the VT DEL byte (0x7f), so the hosted TUI (Claude Code) deletes ONE character — never a whole word. SYMPTOM (operator dogfooding): Backspace in an rc session always behaves like ctrl+Backspace — deletes the entire last word. ROOT (doyle /diagnose, code-grounded, byte PENDING HITL confirm): rc is a RAW VERBATIM byte pump — spawn_stdin_reader (rc.rs:152) reads std::io::stdin() bytes under crossterm raw mode and forwards them unchanged (parse_stdin_chunk only intercepts the ctrl-b detach prefix); there is NO key-event encoding and NO 0x08↔0x7f normalization ANYWHERE in the tree (grep: zero SetConsoleMode / ENABLE_VIRTUAL_TERMINAL_INPUT). On Windows, crossterm enable_raw_mode does NOT set ENABLE_VIRTUAL_TERMINAL_INPUT, so the LEGACY console delivers ^H (0x08, ctrl+h) for Backspace instead of VT DEL (0x7f); Claude Code maps ^H → backward-kill-word → the observed whole-word delete. CONFIRM-FIRST (build the loop): an env-gated hexdump in spawn_stdin_reader (SPT_RC_DEBUG_KEYS) prints the forwarded byte; operator presses Backspace + ctrl+Backspace in a real rc session. FIX CANDIDATES: (a) enable ENABLE_VIRTUAL_TERMINAL_INPUT on the rc stdin console on Windows so the console emits proper VT (Backspace→0x7f, arrows/Home/End as CSI) — cleanest, fixes the whole key map not just Backspace; (b) narrow normalize bare 0x08→0x7f in the rc input path (riskier — a real ctrl+h is also 0x08). Prefer (a) unless it regresses other keys. Add a KNOWN-HAZARDS.md entry on landing. (v0.13.0)"
required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 W7 (todlando). BYTE CONFIRMED (operator HITL via SPT_RC_DEBUG_KEYS, real Windows Terminal): pre-fix Backspace=0x08 (^H), Ctrl+Backspace=0x7f — legacy console sends ^H, CC maps ^H → backward-kill-word = whole-word delete. REJECTED FIX (dc07c39, REVERTED): enabling ENABLE_VIRTUAL_TERMINAL_INPUT BACKFIRED — on Windows Terminal it yields win32-input-mode (6-field ESC[Vk;Sc;Uc;Kd;Cs;Rc_ records), NOT legacy xterm VT; it broke the ctrl-b detach (0x02 wrapped → parse_stdin_chunk never sees the raw prefix). (Operator note: CC itself DID understand win32-input-mode; the only regression was detach. But the VT flag is harness-SPECIFIC — a non-CC adapter could choke — so it's rejected on agnosticism grounds, not just the detach break.) SHIPPED FIX (doyle ruling, revert+narrow): stay a raw byte pump; normalize_key_byte maps a lone 0x08→0x7f in spawn_stdin_reader's forward path AFTER the detach state machine (0x02 + every other byte untouched). cfg(windows) only (Unix already VT; a real ctrl+h must stay 0x08). Accepted loss: ctrl+h conflated with Backspace; Ctrl+Backspace (0x7f here) stays char-delete (word-delete lost on the secondary key). impl = rc.rs normalize_key_byte (in spawn_stdin_reader). unit = normalize_maps_backspace_but_leaves_detach_and_others. doc = KNOWN-HAZARDS 7.13. SPT_RC_DEBUG_KEYS hexdump (f05566b) gated the root. FOLLOW-UP (not yet a stage): the IDEAL is clean xterm VT (Backspace→0x7f, arrows→ESC[, ctrl-b→0x02 RAW so detach survives) = agnostic + full key fidelity + working detach; narrow-normalize is backspace-ONLY (arrows/Home/End stay legacy). Investigate next session whether Windows Terminal can be driven to legacy xterm VT instead of win32-input-mode; mint a follow-up REQ if pursued. UPGRADE (2026-06-19, doyle-dispatched, operator-requested): one-way map → SWAP. normalize_key_byte now maps 0x08→0x7f AND 0x7f→0x08 (else passthrough; 0x02 detach still consumed upstream). Because CC reads ^H=backward-kill-word and DEL=char-delete, swapping the input bytes recovers Backspace=char-delete AND Ctrl+Backspace=word-delete (native Win11 — the operator rejected losing word-delete). unit assertions extended (0x7f→0x08). [impl,unit] unchanged (extend, not re-mint). SUPERSEDED on Windows by REQ-RC-KEY-VT-TRANSLATE (v0.13.0 bug 2): the byte-swap normalize_key_byte is REMOVED — the agnostic key-event→xterm-VT translator emits 0x7f for Backspace and 0x08 for Ctrl+Backspace NATIVELY, so the swap's intent is folded into translate_key_event. This req's [impl]/[unit] evidence RELOCATES there (translate_key_event's Backspace/Ctrl+Backspace arm + its mapping unit carry the [impl->...]/[unit->REQ-HAZARD-RC-INPUT-KEY-ENCODING] tags) — kept covered, not dropped. Unix is untouched (it was already cfg(windows)-only)."


Edit

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──

# ── b4 follow-up: an evicted VIEWER skips to live instead of dying silently ──
[[requirements]]
id = "REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT"
title = "A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 (todlando, doyle-gated). doc = V0.13.0-SKIP-TO-LIVE-JIT.md. impl = broker.rs (ViewerSink.evicted notifier + append evict arm sets it + viewer_writer emits KIND_VIEWER_EVICTED on an evicted rx-close), msg.rs (KIND_VIEWER_EVICTED + evicted_envelope), brain.rs (BrokerEvent::ViewerEvicted + decode arm + cold-cursor reset), attach.rs (serve_attach ViewerEvicted handling: re-subscribe-from-floor + rate-limit, no teardown). unit = viewer_writer emits the marker on evicted-close + nothing on normal close; serve re-subscribes from floor + does not tear down + rate-limits; cold-brain next_seq reset accepts the post-eviction forward jump. INT NOT YET ACTIVATED (rule 5): int evidence = inject_control_wedge.rs p0_paste (warm forkpty, kitsubito CI), green only on the COMBINED fold (pump-carrier-fix + b4 + this) — activate `int` + tag the p0_paste gate when this folds to delivery-control.

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──


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

# ── b4 follow-up: an evicted VIEWER skips to live instead of dying silently ──
[[requirements]]
id = "REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT"
title = "A `rc --view` VIEWER that overflows its broker subscription queue and is EVICTED (OutputLog::append try_send Full → viewers.remove, REQ-HAZARD-VIEWER-ISOLATION session-protection) must SKIP TO LIVE, not die silently. ROOT (v0.13.0, b4 JIT item 2 = p0_paste + post-b4 a_journaled-Linux, ONE root): serve_attach forwards each frame (read_event→b64decode→re-encode AttachRecord→net_stream_send) SLOWER than the drain fans out under flood → its VIEWER_CHANNEL_DEPTH(256) channel overflows → the drain evicts (viewers.remove drops the ViewerSink → drops tx → viewer_writer's rx.recv() Err → the writer returns WRITING NOTHING) → serve_attach's brain.read_event() just STOPS getting Output (no EOF, no error) → serve_attach blocks forever → the operator receives nothing (attach_received_output=FALSE). Eviction-of-a-hopelessly-behind-viewer is CORRECT session-protection (keep it); SILENT+PERMANENT eviction is the bug. VIEWER-only → B2-SAFE (a viewer never advances delivered_through / is not authoritative / exposes no resume cursor). FIX (doyle-gated, skip-to-live = tail -f reconnect): (1) explicit broker→viewer EVICTION SIGNAL (KIND_VIEWER_EVICTED, written in the viewer_writer thread OFF the log lock, DISTINCT from session-exit EOF so serve must NOT tear down on it); (2) serve_attach re-subscribes from the CURRENT ring floor (skip-to-live, replays nothing, sees the next live burst) — resetting the cold serve-brain's next_seq so the post-eviction forward-jump replay is accepted (the legacy reject-gap path, brain.rs:618-626, would otherwise FATAL the forward jump); (3) HARD constraint NO evict→resubscribe busy-loop: serve_attach rate-limits re-subscribes (RESUBSCRIBE_INTERVAL) so under max-flood the operator sees intermittent LIVE bursts, never a CPU spin. (v0.13.0)"
required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 (todlando, doyle-gated). doc = V0.13.0-SKIP-TO-LIVE-JIT.md. impl = broker.rs (ViewerSink.evicted notifier + append evict arm sets it + viewer_writer emits KIND_VIEWER_EVICTED on an evicted rx-close), msg.rs (KIND_VIEWER_EVICTED + evicted_envelope), brain.rs (BrokerEvent::ViewerEvicted + decode arm + cold-cursor reset), attach.rs (serve_attach ViewerEvicted handling: re-subscribe-from-floor + rate-limit, no teardown). unit = viewer_writer emits the marker on evicted-close + nothing on normal close; serve re-subscribes from floor + does not tear down + rate-limits; cold-brain next_seq reset accepts the post-eviction forward jump. INT NOT YET ACTIVATED (rule 5): int evidence = inject_control_wedge.rs p0_paste (warm forkpty, kitsubito CI), green only on the COMBINED fold (pump-carrier-fix + b4 + this) — activate `int` + tag the p0_paste gate when this folds to delivery-control.

# ── W2 KEYSTONE: the translation-binary idle-delivery redesign (ADR-0022) ──

title = "A SLOW controller must not starve a concurrent `rc --view` VIEWER. W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) moved the controller SOCKET WRITE off the drain thread onto controller_writer, but left the bounded HANDOFF (ControllerJob::deliver) as an INLINE try_send SLEEP-POLL on the drain (broker.rs:1450-1457 → deliver:669-685, up to CONTROLLER_WRITE_DEADLINE=5s). So when a controller drains slower than the PTY floods, its CONTROLLER_CHANNEL_DEPTH(4096) channel fills, deliver() polls inline, and the DRAIN THREAD is throttled to the controller's read rate → OutputLog::append's viewer fan-out (try_send) stops running → a concurrent VIEWER receives only the initial replay then nothing (root 'b4', warm forkpty: a_journaled c1=0/EVICT=0/got_output=FALSE; steady-state-near-full = no recovery; forkpty-only, floods harder than Windows ConPTY). The viewer-not-starved-by-a-busy-session property is legitimate (rc --view of a noisy session must show LIVE output). FIX: the controller becomes a SINGLE NON-BLOCKING try_send (like a viewer), done IN append() under the log lock; deliver()'s sleep-poll DELETED; the drain NEVER sleeps. ControllerSink gains a stateful last_ok deadline → a TRULY-stalled controller (continuous-Full past CONTROLLER_WRITE_DEADLINE) is evicted (bounded-wedge preserved); a slow-but-alive controller DROPS frames + falls behind the ring (resume-from-floor, the existing reconnect case). B2 GAPLESS-HANDOFF PRESERVED via a CONTIGUOUS delivered_through: controller_writer advances the cursor ONLY when the written seq == cursor (next expected); a gap from a drop FREEZES the cursor at last-contiguous so a re-attaching brain's resume_seq never skips a dropped chunk (a high-watermark advance past the gap would be a not-exactly-once resume = B2 violation, doyle's gate). (v0.13.0)"

required_stages = ["impl", "unit"]  # ACTIVATED v0.13.0 impl+unit (todlando, doyle-gated). impl = broker.rs OutputLog::append (single non-blocking controller try_send + ControllerSink.last_ok staleness, returns Option<evict-epoch>), drain closure (mark_controller_gone on signal, never sleeps), controller_writer live-path CONTIGUOUS cursor advance (contiguous_advance: compare_exchange seq→seq+1, freeze on gap), ControllerJob::deliver sleep-poll DELETED. unit = a_full_controller_does_not_starve_a_viewer_or_block_the_drain (in-process OutputLog, no PTY) + contiguous_advance_freezes_on_a_gap kernel. INT NOT YET ACTIVATED (rule 5, activate-don't-pre-fail): the int evidence is inject_control_wedge.rs a_journaled (warm forkpty, kitsubito CI), which only goes GREEN on the COMBINED branch (pump-carrier-fix carrier+registry + this b4) — not on this isolated b4 branch. Activate `int` + tag the a_journaled gate when b4 folds to delivery-control alongside pump-carrier-fix and the forkpty gate is green.

+# ── b4 follow-up: an evicted VIEWER skips to live instead of dying silently ──

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

+required_stages = ["doc", "impl", "unit"]  # ACTIVATED v0.13.0 (todlando, doyle-gated). doc = V0.13.0-SKIP-TO-LIVE-JIT.md. impl = broker.rs (ViewerSink.evicted notifier + append evict arm sets it + viewer_writer emits KIND_VIEWER_EVICTED on an evicted rx-close), msg.rs (KIND_VIEWER_EVICTED + evicted_envelope), brain.rs (BrokerEvent::ViewerEvicted + decode arm + cold-cursor reset), attach.rs (serve_attach ViewerEvicted handling: re-subscribe-from-floor + rate-limit, no teardown). unit = viewer_writer emits the marker on evicted-close + nothing on normal close; serve re-subscribes from floor + does not tear down + rate-limits; cold-brain next_seq reset accepts the post-eviction forward jump. INT NOT YET ACTIVATED (rule 5): int evidence = inject_control_wedge.rs p0_paste (warm forkpty, kitsubito CI), green only on the COMBINED fold (pump-carrier-fix + b4 + this) — activate `int` + tag the p0_paste gate when this folds to delivery-control.


Requirement registered. Now implement. Starting with `msg.rs` — the wire surface. Let me read the size_envelope + SizeEvent region to mirror it.


40	pub const KIND_KILL: &str = "kill";
41	/// Broker→brain: a chunk of a session's output (pushed as the child produces it).
42	pub const KIND_OUTPUT: &str = "output";
43	/// Broker→brain: a session's child exited.
44	pub const KIND_EXIT: &str = "exit";
45	/// Broker→brain: the outcome of a [`KIND_SUBSCRIBE`] under the W2.5
46	/// controller/viewer model (REQ-RCVIEW-1 / REQ-KICK-1). Carries the resolved
47	/// role — controller / viewer / busy-refused / took-control — so the serving
48	/// brain can wake (controller-only), stay wake-neutral (viewer), or refuse the
49	/// operator (busy). A normal reply, not a new wire frame (the busy refusal is
50	/// rendered client-side by `rc` from the endpoint's `driven_by`).
51	pub const KIND_SUBSCRIBED: &str = "subscribed";
52	/// Broker→brain: the current PTY size of a session, pushed to a VIEWER on
53	/// attach and to every viewer on a controller resize (REQ-RCVIEW-1). The
54	/// serving brain forwards it as a [`spt_net::net::attach::AttachRecord::Size`]
55	/// so the viewer letterboxes client-side (never resizing the PTY itself —
56	/// resize is controller-exclusive).
57	pub const KIND_SIZE: &str = "size";
58	/// Broker→brain: you (a controller) have been DISPLACED by a different operator's
59	/// explicit `--take`/`kick` (REQ-KICK-1). The serving brain forwards this as a
60	/// loud [`spt_net::net::attach::AttachRecord::Displaced`] on the wire, then
61	/// detaches. Fired ONLY on a genuine cross-operator control-ownership change —
62	/// NEVER on a same-origin re-serve after a brain restart (the gate-#7 self-kick
63	/// trap: a successor re-taking the controller slot with the SAME `by` is silent).
64	pub const KIND_DISPLACED: &str = "displaced";
65	/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably
66	/// applied **or** was already applied (deduped). The exactly-once ack: a brain
67	/// learns its effect landed, and a replay after a crash is acknowledged the same
68	/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).
69	pub const KIND_APPLIED: &str = "applied";

pub const KIND_KILL: &str = "kill";
/// Broker→brain: a chunk of a session's output (pushed as the child produces it).
pub const KIND_OUTPUT: &str = "output";
/// Broker→brain: a session's child exited.
pub const KIND_EXIT: &str = "exit";
/// Broker→brain: the outcome of a [`KIND_SUBSCRIBE`] under the W2.5
/// controller/viewer model (REQ-RCVIEW-1 / REQ-KICK-1). Carries the resolved
/// role — controller / viewer / busy-refused / took-control — so the serving
/// brain can wake (controller-only), stay wake-neutral (viewer), or refuse the
/// operator (busy). A normal reply, not a new wire frame (the busy refusal is
/// rendered client-side by `rc` from the endpoint's `driven_by`).
pub const KIND_SUBSCRIBED: &str = "subscribed";
/// Broker→brain: the current PTY size of a session, pushed to a VIEWER on
/// attach and to every viewer on a controller resize (REQ-RCVIEW-1). The
/// serving brain forwards it as a [`spt_net::net::attach::AttachRecord::Size`]
/// so the viewer letterboxes client-side (never resizing the PTY itself —
/// resize is controller-exclusive).
pub const KIND_SIZE: &str = "size";
/// Broker→brain: you (a controller) have been DISPLACED by a different operator's
/// explicit `--take`/`kick` (REQ-KICK-1). The serving brain forwards this as a
/// loud [`spt_net::net::attach::AttachRecord::Displaced`] on the wire, then
/// detaches. Fired ONLY on a genuine cross-operator control-ownership change —
/// NEVER on a same-origin re-serve after a brain restart (the gate-#7 self-kick
/// trap: a successor re-taking the controller slot with the SAME `by` is silent).
pub const KIND_DISPLACED: &str = "displaced";
/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably
/// applied **or** was already applied (deduped). The exactly-once ack: a brain
/// learns its effect landed, and a replay after a crash is acknowledged the same
/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).
pub const KIND_APPLIED: &str = "applied";


748	        serde_json::to_value(SubscribedReply {
749	            session_id,
750	            outcome,
751	        })
752	        .expect("SubscribedReply serializes"),
753	    )
754	}
755
756	/// Build a `size` event envelope for `session_id` (REQ-RCVIEW-1).
757	pub fn size_envelope(session_id: u64, rows: u16, cols: u16) -> Envelope {
758	    Envelope::new(
759	        KIND_SIZE,
760	        serde_json::to_value(SizeEvent {
761	            session_id,
762	            rows,
763	            cols,
764	        })
765	        .expect("SizeEvent serializes"),
766	    )
767	}
768
769	/// Build a `displaced` event envelope for the controller of `session_id` kicked
770	/// by `by` (REQ-KICK-1).
771	pub fn displaced_envelope(session_id: u64, by: &str) -> Envelope {
772	    Envelope::new(
773	        KIND_DISPLACED,
774	        serde_json::to_value(DisplacedEvent {
775	            session_id,
776	            by: by.to_string(),
777	        })
778	        .expect("DisplacedEvent serializes"),
779	    )
780	}
781
782	/// Build an `applied` ack envelope for `session_id` / `op_id`; `applied_now`

        serde_json::to_value(SubscribedReply {
            session_id,
            outcome,
        })
        .expect("SubscribedReply serializes"),
    )
}

/// Build a `size` event envelope for `session_id` (REQ-RCVIEW-1).
pub fn size_envelope(session_id: u64, rows: u16, cols: u16) -> Envelope {
    Envelope::new(
        KIND_SIZE,
        serde_json::to_value(SizeEvent {
            session_id,
            rows,
            cols,
        })
        .expect("SizeEvent serializes"),
    )
}

/// Build a `displaced` event envelope for the controller of `session_id` kicked
/// by `by` (REQ-KICK-1).
pub fn displaced_envelope(session_id: u64, by: &str) -> Envelope {
    Envelope::new(
        KIND_DISPLACED,
        serde_json::to_value(DisplacedEvent {
            session_id,
            by: by.to_string(),
        })
        .expect("DisplacedEvent serializes"),
    )
}

/// Build an `applied` ack envelope for `session_id` / `op_id`; `applied_now`


Bash

grep -n "struct SizeEvent\|struct DisplacedEvent" crates/spt-daemon/src/msg.rs

Find SizeEvent struct


281:pub struct SizeEvent {
292:pub struct DisplacedEvent {


278
279	/// `size` event payload — the current PTY size pushed to a viewer (REQ-RCVIEW-1).
280	#[derive(Debug, Clone, Serialize, Deserialize)]
281	pub struct SizeEvent {
282	    /// The session whose PTY size this is.
283	    pub session_id: u64,
284	    /// Current rows.
285	    pub rows: u16,
286	    /// Current cols.
287	    pub cols: u16,
288	}
289
290	/// `displaced` event payload — a controller was kicked by `by` (REQ-KICK-1).
291	#[derive(Debug, Clone, Serialize, Deserialize)]
292	pub struct DisplacedEvent {
293	    /// The session whose controller seat was taken.
294	    pub session_id: u64,
295	    /// The node that took control.
296	    pub by: String,
297	}
298
299	/// `unsubscribe` payload — release this connection's role on `session_id`
300	/// (REQ-RCVIEW-1).
301	#[derive(Debug, Clone, Serialize, Deserialize)]
302	pub struct UnsubscribeReq {

/// `size` event payload — the current PTY size pushed to a viewer (REQ-RCVIEW-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SizeEvent {
    /// The session whose PTY size this is.
    pub session_id: u64,
    /// Current rows.
    pub rows: u16,
    /// Current cols.
    pub cols: u16,
}

/// `displaced` event payload — a controller was kicked by `by` (REQ-KICK-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplacedEvent {
    /// The session whose controller seat was taken.
    pub session_id: u64,
    /// The node that took control.
    pub by: String,
}

/// `unsubscribe` payload — release this connection's role on `session_id`
/// (REQ-RCVIEW-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnsubscribeReq {


Now I have all the patterns. Let me make the msg.rs changes — add the kind constant, payload struct, and envelope builder.


Edit

/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably
/// applied **or** was already applied (deduped). The exactly-once ack: a brain
/// learns its effect landed, and a replay after a crash is acknowledged the same
/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).
pub const KIND_APPLIED: &str = "applied";

/// Broker→brain(VIEWER): this viewer's bounded subscription queue overflowed and
/// it was EVICTED (REQ-HAZARD-VIEWER-ISOLATION session-protection). DISTINCT from
/// a session-exit EOF: the serving brain must NOT tear the attach down — it
/// re-subscribes from the current ring floor (SKIP-TO-LIVE,
/// REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). Written by the viewer's dedicated writer
/// thread (OFF the log lock / off the drain) as its final frame.
pub const KIND_VIEWER_EVICTED: &str = "viewer_evicted";
/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably
/// applied **or** was already applied (deduped). The exactly-once ack: a brain
/// learns its effect landed, and a replay after a crash is acknowledged the same
/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).
pub const KIND_APPLIED: &str = "applied";


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

/// Broker→brain(VIEWER): this viewer's bounded subscription queue overflowed and
/// it was EVICTED (REQ-HAZARD-VIEWER-ISOLATION session-protection). DISTINCT from
/// a session-exit EOF: the serving brain must NOT tear the attach down — it
/// re-subscribes from the current ring floor (SKIP-TO-LIVE,
/// REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). Written by the viewer's dedicated writer
/// thread (OFF the log lock / off the drain) as its final frame.
pub const KIND_VIEWER_EVICTED: &str = "viewer_evicted";
/// Broker→brain: an idempotent effect (an `input` carrying an `op_id`) is durably
/// applied **or** was already applied (deduped). The exactly-once ack: a brain
/// learns its effect landed, and a replay after a crash is acknowledged the same
/// way so the brain can retire it (B5 / KNOWN-HAZARDS 7.2).
pub const KIND_APPLIED: &str = "applied";

/// NEVER on a same-origin re-serve after a brain restart (the gate-#7 self-kick

/// trap: a successor re-taking the controller slot with the SAME `by` is silent).

pub const KIND_DISPLACED: &str = "displaced";

+/// Broker→brain(VIEWER): this viewer's bounded subscription queue overflowed and

+/// it was EVICTED (REQ-HAZARD-VIEWER-ISOLATION session-protection). DISTINCT from

+/// a session-exit EOF: the serving brain must NOT tear the attach down — it

+/// re-subscribes from the current ring floor (SKIP-TO-LIVE,

+/// REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). Written by the viewer's dedicated writer

+/// thread (OFF the log lock / off the drain) as its final frame.

+pub const KIND_VIEWER_EVICTED: &str = "viewer_evicted";


/// `displaced` event payload — a controller was kicked by `by` (REQ-KICK-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplacedEvent {

/// `viewer_evicted` event payload — this viewer overflowed and was evicted
/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The serving brain re-subscribes from the
/// ring floor rather than tearing the attach down.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViewerEvictedEvent {
    /// The session whose viewer subscription overflowed.
    pub session_id: u64,
}

/// `displaced` event payload — a controller was kicked by `by` (REQ-KICK-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplacedEvent {


/// `viewer_evicted` event payload — this viewer overflowed and was evicted
/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The serving brain re-subscribes from the
/// ring floor rather than tearing the attach down.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViewerEvictedEvent {
    /// The session whose viewer subscription overflowed.
    pub session_id: u64,
}

/// `displaced` event payload — a controller was kicked by `by` (REQ-KICK-1).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DisplacedEvent {

+/// `viewer_evicted` event payload — this viewer overflowed and was evicted

+/// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). The serving brain re-subscribes from the

+/// ring floor rather than tearing the attach down.

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

+    /// The session whose viewer subscription overflowed.


/// Build a `displaced` event envelope for the controller of `session_id` kicked
/// by `by` (REQ-KICK-1).
pub fn displaced_envelope(session_id: u64, by: &str) -> Envelope {

/// Build a `viewer_evicted` event envelope for `session_id` — the evicted
/// viewer's final frame, so the serving brain skips to live instead of tearing
/// the attach down (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).
// [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
pub fn evicted_envelope(session_id: u64) -> Envelope {
    Envelope::new(
        KIND_VIEWER_EVICTED,
        serde_json::to_value(ViewerEvictedEvent { session_id })
            .expect("ViewerEvictedEvent serializes"),
    )
}

/// Build a `displaced` event envelope for the controller of `session_id` kicked
/// by `by` (REQ-KICK-1).
pub fn displaced_envelope(session_id: u64, by: &str) -> Envelope {


/// Build a `viewer_evicted` event envelope for `session_id` — the evicted
/// viewer's final frame, so the serving brain skips to live instead of tearing
/// the attach down (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT).
// [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
pub fn evicted_envelope(session_id: u64) -> Envelope {
    Envelope::new(
        KIND_VIEWER_EVICTED,
        serde_json::to_value(ViewerEvictedEvent { session_id })
            .expect("ViewerEvictedEvent serializes"),
    )
}

/// Build a `displaced` event envelope for the controller of `session_id` kicked
/// by `by` (REQ-KICK-1).
pub fn displaced_envelope(session_id: u64, by: &str) -> Envelope {

+/// Build a `viewer_evicted` event envelope for `session_id` — the evicted

+/// viewer's final frame, so the serving brain skips to live instead of tearing

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

+pub fn evicted_envelope(session_id: u64) -> Envelope {

+        serde_json::to_value(ViewerEvictedEvent { session_id })

+            .expect("ViewerEvictedEvent serializes"),


Now brain.rs — the `BrokerEvent::ViewerEvicted` variant + decode arm. First check the msg imports.


Bash

grep -n "KIND_DISPLACED\|KIND_SIZE\|DisplacedEvent\|SizeEvent\|use crate::msg" crates/spt-daemon/src/brain.rs | head

Find brain.rs msg imports


36:use crate::msg::{
37:    decode_bytes, encode_bytes, AppliedEvent, BrainRestarted, DisplacedEvent, EndpointInjected,
43:    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
45:    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,
51:    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,
660:                KIND_SIZE => {
661:                    let ev: SizeEvent = serde_json::from_value(env.payload)
669:                KIND_DISPLACED => {
670:                    let ev: DisplacedEvent = serde_json::from_value(env.payload)


36	use crate::msg::{
37	    decode_bytes, encode_bytes, AppliedEvent, BrainRestarted, DisplacedEvent, EndpointInjected,
38	    EndpointInputReq, ErrorEvent, ExitEvent,
39	    InputReq,
40	    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,
41	    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,
42	    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, PairJoinReply, PairJoinReq, ResizeReq,
43	    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
44	    KIND_APPLIED, KIND_BRAIN_RESTART,
45	    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,
46	    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,
47	    KIND_NET_SENT,
48	    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
49	    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,
50	    KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_JOIN, KIND_PAIR_JOINED,
51	    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,
52	    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,
53	};
54	use spt_net::net::attach::AttachIntent;
55	use crate::transport::{send_hello, LocalSocketTransport};

use crate::msg::{
    decode_bytes, encode_bytes, AppliedEvent, BrainRestarted, DisplacedEvent, EndpointInjected,
    EndpointInputReq, ErrorEvent, ExitEvent,
    InputReq,
    KillReq, NetDialReq, NetDialed, NetPresenceEvent, NetPresenceSubscribeReq, NetSent,
    NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,
    NetStreamSubscribeReq, NetStreamsReply, OutputEvent, PairJoinReply, PairJoinReq, ResizeReq,
    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
    KIND_APPLIED, KIND_BRAIN_RESTART,
    KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,
    KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,
    KIND_NET_SENT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_NET_STREAMS, KIND_NET_STREAMS_REPLY,
    KIND_NET_STREAM_DATA, KIND_NET_STREAM_EOF, KIND_NET_STREAM_OPEN, KIND_NET_STREAM_OPENED,
    KIND_NET_STREAM_SEND, KIND_NET_STREAM_SUBSCRIBE, KIND_OUTPUT, KIND_PAIR_JOIN, KIND_PAIR_JOINED,
    KIND_RESIZE, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SIZE, KIND_SPAWN, KIND_SPAWNED,
    KIND_SUBSCRIBE, KIND_SUBSCRIBED, KIND_UNSUBSCRIBE, UnsubscribeReq,
};
use spt_net::net::attach::AttachIntent;
use crate::transport::{send_hello, LocalSocketTransport};


Edit

    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
    KIND_APPLIED, KIND_BRAIN_RESTART,

    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
    ViewerEvictedEvent,
    KIND_APPLIED, KIND_BRAIN_RESTART, KIND_VIEWER_EVICTED,


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

    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
    KIND_APPLIED, KIND_BRAIN_RESTART,

    SessionsReply, SizeEvent, SpawnReq, Spawned, SubscribeOutcome, SubscribeReq, SubscribedReply,
    ViewerEvictedEvent,
    KIND_APPLIED, KIND_BRAIN_RESTART, KIND_VIEWER_EVICTED,

NetStatusReply, NetStreamData, NetStreamEof, NetStreamOpenReq, NetStreamOpened, NetStreamSendReq,

NetStreamSubscribeReq, NetStreamsReply, OutputEvent, PairJoinReply, PairJoinReq, ResizeReq,

+    KIND_APPLIED, KIND_BRAIN_RESTART, KIND_VIEWER_EVICTED,

KIND_BRAIN_RESTARTED, KIND_DISPLACED, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT, KIND_ERROR, KIND_EXIT, KIND_INPUT, KIND_KILL, KIND_NET_DIAL,

KIND_NET_DIALED, KIND_NET_DIAL_LOOPBACK, KIND_NET_PRESENCE_EVENT, KIND_NET_PRESENCE_SUBSCRIBE,


    /// This controller was DISPLACED by a different operator's `--take`/`kick`
    /// (REQ-KICK-1). The serving brain forwards a loud notice and detaches.
    Displaced { session_id: u64, by: String },

    /// This controller was DISPLACED by a different operator's `--take`/`kick`
    /// (REQ-KICK-1). The serving brain forwards a loud notice and detaches.
    Displaced { session_id: u64, by: String },
    /// This VIEWER's bounded subscription queue overflowed and it was evicted
    /// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). DISTINCT from a session-exit EOF: the
    /// serving brain re-subscribes from the current ring floor (skip-to-live)
    /// rather than tearing the attach down.
    ViewerEvicted { session_id: u64 },


    /// This controller was DISPLACED by a different operator's `--take`/`kick`
    /// (REQ-KICK-1). The serving brain forwards a loud notice and detaches.
    Displaced { session_id: u64, by: String },
    /// This VIEWER's bounded subscription queue overflowed and it was evicted
    /// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). DISTINCT from a session-exit EOF: the
    /// serving brain re-subscribes from the current ring floor (skip-to-live)
    /// rather than tearing the attach down.
    ViewerEvicted { session_id: u64 },

+    /// This VIEWER's bounded subscription queue overflowed and it was evicted

+    /// (REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT). DISTINCT from a session-exit EOF: the

+    /// serving brain re-subscribes from the current ring floor (skip-to-live)

+    /// rather than tearing the attach down.

/// An idempotent effect is durably applied (or was deduped — `applied_now`

/// distinguishes). The exactly-once ack for a [`Brain::send_effect`] (B5).


                KIND_DISPLACED => {
                    let ev: DisplacedEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Displaced {
                        session_id: ev.session_id,
                        by: ev.by,
                    });
                }

                KIND_DISPLACED => {
                    let ev: DisplacedEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Displaced {
                        session_id: ev.session_id,
                        by: ev.by,
                    });
                }
                // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
                KIND_VIEWER_EVICTED => {
                    let ev: ViewerEvictedEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::ViewerEvicted {
                        session_id: ev.session_id,
                    });
                }


                KIND_DISPLACED => {
                    let ev: DisplacedEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::Displaced {
                        session_id: ev.session_id,
                        by: ev.by,
                    });
                }
                // [impl->REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT]
                KIND_VIEWER_EVICTED => {
                    let ev: ViewerEvictedEvent = serde_json::from_value(env.payload)
                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
                    return Ok(BrokerEvent::ViewerEvicted {
                        session_id: ev.session_id,
                    });
                }

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

+                KIND_VIEWER_EVICTED => {

+                    let ev: ViewerEvictedEvent = serde_json::from_value(env.payload)

+                        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

+                    return Ok(BrokerEvent::ViewerEvicted {

+                        session_id: ev.session_id,

let ev: AppliedEvent = serde_json::from_value(env.payload)


Now the cold-cursor reset — the critical risk. Let me read `attach_as` fully to design the skip-to-live re-subscribe correctly.


1155	    }
1156
1157	    /// Re-attach to a session's output from `from_seq` (handoff resume) — a
1158	    /// LOCAL re-take (intent `Control`, identity `None`). A `None` identity never
1159	    /// displaces a remote controller (the broker falls it back to a silent
1160	    /// viewer) — the gate-#7 local-resume guard.
1161	    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
1162	        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)
1163	    }
1164
1165	    /// Release this connection's controller/viewer role on `session_id`
1166	    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when
1167	    /// the viewport ends so the broker clears `driven_by` / `viewer_count`
1168	    /// promptly. Best-effort: a dropped connection clears the role anyway.
1169	    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {
1170	        self.send(
1171	            KIND_UNSUBSCRIBE,
1172	            serde_json::to_value(UnsubscribeReq { session_id })
1173	                .expect("UnsubscribeReq serializes"),
1174	        )
1175	    }
1176
1177	    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/
1178	    /// take, `by` carries the operator identity (`Some(node)` remote, `None`
1179	    /// local). The broker replies the resolved [`SubscribeOutcome`] as a
1180	    /// [`BrokerEvent::Subscribed`].
1181	    fn subscribe_with(
1182	        &mut self,
1183	        session_id: u64,
1184	        from_seq: u64,
1185	        intent: AttachIntent,
1186	        by: Option<String>,
1187	    ) -> io::Result<()> {
1188	        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain
1189	        // (seeded map) that subscribes from `from_seq` must RESET this session's
1190	        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then
1191	        // re-delivered (and the operator's render cursor dedups the overlap),
1192	        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,
1193	        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary
1194	        // BEFORE it is `attached` (the frame is dropped by the `if attached` gate
1195	        // but the snap-above cursor still advanced past K); `attach_as(sid, 0)`
1196	        // then re-subscribes from 0, and without this reset the broker's re-send of
1197	        // seq K arrives below the advanced cursor and is deduped → seq K never
1198	        // reaches the operator viewport → a forward gap. Resetting to `from_seq`
1199	        // makes the re-subscribe re-deliver everything from its floor. A cold-start
1200	        // brain (empty map — e.g. the production dispatch serve brain) keeps the
1201	        // legacy `next_seq` path; this only arms in resume mode.
1202	        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]
1203	        if !self.session_cursors.is_empty() {
1204	            self.session_cursors.insert(session_id, from_seq);

    }

    /// Re-attach to a session's output from `from_seq` (handoff resume) — a
    /// LOCAL re-take (intent `Control`, identity `None`). A `None` identity never
    /// displaces a remote controller (the broker falls it back to a silent
    /// viewer) — the gate-#7 local-resume guard.
    fn subscribe(&mut self, session_id: u64, from_seq: u64) -> io::Result<()> {
        self.subscribe_with(session_id, from_seq, AttachIntent::Control, None)
    }

    /// Release this connection's controller/viewer role on `session_id`
    /// (REQ-RCVIEW-1) — the explicit detach a `serve_attach` worker sends when
    /// the viewport ends so the broker clears `driven_by` / `viewer_count`
    /// promptly. Best-effort: a dropped connection clears the role anyway.
    pub fn detach_session(&mut self, session_id: u64) -> io::Result<()> {
        self.send(
            KIND_UNSUBSCRIBE,
            serde_json::to_value(UnsubscribeReq { session_id })
                .expect("UnsubscribeReq serializes"),
        )
    }

    /// The role-aware subscribe (REQ-RCVIEW-1): `intent` selects viewer/control/
    /// take, `by` carries the operator identity (`Some(node)` remote, `None`
    /// local). The broker replies the resolved [`SubscribeOutcome`] as a
    /// [`BrokerEvent::Subscribed`].
    fn subscribe_with(
        &mut self,
        session_id: u64,
        from_seq: u64,
        intent: AttachIntent,
        by: Option<String>,
    ) -> io::Result<()> {
        // P1c (operator-stream boundary, kitsubito RACEDIAG): a resume-mode brain
        // (seeded map) that subscribes from `from_seq` must RESET this session's
        // dedup cursor to `from_seq` — the broker's replay from `from_seq` is then
        // re-delivered (and the operator's render cursor dedups the overlap),
        // never lost. WHY THIS, NOT just `read_event`'s `or_insert`: on a re-serve,
        // `serve_attach` may CONSUME a handoff-replay frame at the K boundary
        // BEFORE it is `attached` (the frame is dropped by the `if attached` gate
        // but the snap-above cursor still advanced past K); `attach_as(sid, 0)`
        // then re-subscribes from 0, and without this reset the broker's re-send of
        // seq K arrives below the advanced cursor and is deduped → seq K never
        // reaches the operator viewport → a forward gap. Resetting to `from_seq`
        // makes the re-subscribe re-deliver everything from its floor. A cold-start
        // brain (empty map — e.g. the production dispatch serve brain) keeps the
        // legacy `next_seq` path; this only arms in resume mode.
        // [impl->REQ-HAZARD-CONTROLLER-WRITER-REORDER]
        if !self.session_cursors.is_empty() {
            self.session_cursors.insert(session_id, from_seq);


Bash

grep -n "pub fn attach_as\|fn attach_as\|pub fn net_stream_subscribe\|fn subscribe_with\|LIVE_TAIL\|u64::MAX" crates/spt-daemon/src/brain.rs

Find attach_as method


830:    pub fn attach_as(
1120:    pub fn net_stream_subscribe(&mut self, stream_id: u64, from_seq: u64) -> io::Result<()> {
1181:    fn subscribe_with(


820
821	    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a
822	    /// role-aware subscribe with `intent` + the operator identity `by` (the
823	    /// handshake-proven origin for a remote attach). The broker resolves the role
824	    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]
825	    /// loop) reads to learn whether it became controller / viewer / was refused.
826	    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the
827	    /// generation is untouched).
828	    ///
829	    /// [`serve_attach`]: crate::attach::serve_attach
830	    pub fn attach_as(
831	        &mut self,
832	        session_id: u64,
833	        from_seq: u64,
834	        intent: AttachIntent,
835	        by: Option<&str>,
836	    ) -> io::Result<()> {
837	        self.session_id = Some(session_id);
838	        self.next_seq = from_seq;
839	        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))
840	    }
841
842	    /// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the
843	    /// broker for **every** hosted session and re-attach **each** in resume mode
844	    /// from the broker's per-session delivered cursor (`resume_seq`). This is the
845	    /// production replacement for the retired single-session `BrainState` handoff
846	    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs
847	    /// all session continuity by querying the persistent side, never a brain→brain
848	    /// message. Returns the ids re-attached (empty when the broker hosts none —
849	    /// the supervised daemon brain's no-op-today case).
850	    ///
851	    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,
852	    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send
853	    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is
854	    /// at-least-once; input/effects stay exactly-once via the broker-owned
855	    /// `EffectJournal`. The generation is **not** touched — this is a cold start
856	    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.
857	    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
858	    // [impl->REQ-DAEMON-2]
859	    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {
860	        let reply = self.sessions()?;
861	        let mut resumed = Vec::with_capacity(reply.sessions.len());
862	        for info in reply.sessions {
863	            // Seed the dedup cursor at the broker's resume point BEFORE the
864	            // subscribe replay starts arriving, then re-attach from there.
865	            self.session_cursors
866	                .insert(info.session_id, info.resume_seq);
867	            self.subscribe(info.session_id, info.resume_seq)?;
868	            resumed.push(info.session_id);
869	        }
870	        Ok(resumed)
871	    }
872
873	    /// The per-session resume cursor (the next seq still needed for a session
874	    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.
875	    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {
876	        self.session_cursors.get(&session_id).copied()
877	    }
878
879
880	    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
881	    /// address, conn count — or `enabled: false` on a net-less broker. Reads
882	    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
883	    // [impl->REQ-DAEMON-2]
884	    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
885	        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
886	        loop {
887	            match self.read_event()? {
888	                BrokerEvent::NetStatus(s) => return Ok(s),
889	                BrokerEvent::Error { message } => return Err(io::Error::other(message)),

    /// Attach under the W2.5 controller/viewer model (REQ-RCVIEW-1): send a
    /// role-aware subscribe with `intent` + the operator identity `by` (the
    /// handshake-proven origin for a remote attach). The broker resolves the role
    /// and replies a [`BrokerEvent::Subscribed`] the caller (a [`serve_attach`]
    /// loop) reads to learn whether it became controller / viewer / was refused.
    /// Like [`Brain::attach`] this is a second reader, not a logic handoff (the
    /// generation is untouched).
    ///
    /// [`serve_attach`]: crate::attach::serve_attach
    pub fn attach_as(
        &mut self,
        session_id: u64,
        from_seq: u64,
        intent: AttachIntent,
        by: Option<&str>,
    ) -> io::Result<()> {
        self.session_id = Some(session_id);
        self.next_seq = from_seq;
        self.subscribe_with(session_id, from_seq, intent, by.map(str::to_string))
    }

    /// Cold-start multi-session resume (restoration D4-2, ADR-0018 Q6): query the
    /// broker for **every** hosted session and re-attach **each** in resume mode
    /// from the broker's per-session delivered cursor (`resume_seq`). This is the
    /// production replacement for the retired single-session `BrainState` handoff
    /// frame — a brain the supervisor respawns (crash *or* update) reconstructs
    /// all session continuity by querying the persistent side, never a brain→brain
    /// message. Returns the ids re-attached (empty when the broker hosts none —
    /// the supervised daemon brain's no-op-today case).
    ///
    /// Each session is seeded into [`Self::session_cursors`] at its `resume_seq`,
    /// so [`Brain::read_event`] dedups the broker's at-least-once boundary re-send
    /// and accepts a post-eviction ring-floor jump (no reject-gap). Output is
    /// at-least-once; input/effects stay exactly-once via the broker-owned
    /// `EffectJournal`. The generation is **not** touched — this is a cold start
    /// (generation custody is the broker's, ADR-0018 V2), not a `handoff`.
    // [impl->REQ-HAZARD-BROKER-PROCESS-ISOLATION]
    // [impl->REQ-DAEMON-2]
    pub fn resume_sessions(&mut self) -> io::Result<Vec<u64>> {
        let reply = self.sessions()?;
        let mut resumed = Vec::with_capacity(reply.sessions.len());
        for info in reply.sessions {
            // Seed the dedup cursor at the broker's resume point BEFORE the
            // subscribe replay starts arriving, then re-attach from there.
            self.session_cursors
                .insert(info.session_id, info.resume_seq);
            self.subscribe(info.session_id, info.resume_seq)?;
            resumed.push(info.session_id);
        }
        Ok(resumed)
    }

    /// The per-session resume cursor (the next seq still needed for a session
    /// re-attached by [`Brain::resume_sessions`]) — observability / tests.
    pub fn session_cursor(&self, session_id: u64) -> Option<u64> {
        self.session_cursors.get(&session_id).copied()
    }

    /// Query the broker-owned net endpoint's status (D4a): node id, dialable
    /// address, conn count — or `enabled: false` on a net-less broker. Reads
    /// until the reply (consuming interleaved events like [`Brain::spawn_session`]).
    // [impl->REQ-DAEMON-2]
    pub fn net_status(&mut self) -> io::Result<NetStatusReply> {
        self.send(KIND_NET_STATUS, serde_json::Value::Null)?;
        loop {
            match self.read_event()? {
                BrokerEvent::NetStatus(s) => return Ok(s),
                BrokerEvent::Error { message } => return Err(io::Error::other(message)),


Now I understand the cursor mechanics precisely. The skip-to-live re-subscribe needs: wire `from_seq=u64::MAX` (broker replays nothing — ring filter `s >= MAX` is empty), but the *local* dedup must accept the next live frame at the real tail seq. That requires the snap-above path (`session_cursors` non-empty), seeded at 0 — not the legacy next_seq path (which would gap-reject or dedup-drop). Let me add a dedicated Brain method.


Edit

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

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

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