# V0.13.0 — viewer PRE-eviction RING-ROLL snap-to-live — JIT plan

**Status:** IMPLEMENTED (this doc + impl + unit). The **keystone** of v0.13.0 (doyle's
forkpty ground-truth, 2026-06-21). Off `viewer-drain-decouple-b4` (after b4 + skip-to-live).
doyle-GATED: clear-to-author+commit+push this session; FOLD to delivery-control stays
operator-gated (doyle re-runs the forkpty matrix before any fold). `REQ-HAZARD-VIEWER-RING-ROLL-SNAP`.

## Why this exists (the near-fold was refuted)

b4 (drain-decouple) + skip-to-live (post-eviction recovery) were expected to turn the
forkpty gates GREEN. doyle's **combined** forkpty run (wedge-trace-v4, Linux kitsubito)
**refuted** that: `a_journaled` (b4's OWN gate), `p0_paste`, and `attach.rs:1071
wedged_viewer` are all **RED on Linux** with the same cause —

```
ctrl event: Custom { InvalidData, "output gap: got seq 5968 want 4504" }
```

b4 DID its job (drain decoupled: `drain_appends`→98993, c1-EVICT @ seq 98992), but the
attach dies EARLY (~seq 4504), **long before** the eviction. Windows passed only because
ConPTY floods slower → the ring never rolls far enough to gap a still-subscribed viewer
= a **platform-masked false-green**. Linux was never actually green.

## Root (exact, brain.rs read)

`read_event`'s `KIND_OUTPUT` dedup has TWO paths:

- `session_cursors` **non-empty** → brain.rs:607 **dedup-below + snap-above** (accept any
  `seq >= cursor`, advance; drop `seq < cursor`; **NO reject-gap**).
- `session_cursors` **empty** → brain.rs:624 **legacy** accept-next / drop-dup /
  **REJECT-GAP** (:628 fatals on `seq > next_seq` with `output gap: got N want M`).

`serve_attach` subscribes a viewer via `brain.attach_as(intent=Viewer)` (attach.rs:199),
which leaves `session_cursors` **EMPTY** → the viewer serve-brain runs the LEGACY
reject-gap. Under a hard flood the serving brain falls behind the live ring and the ring
rolls the intervening frames OUT between reads → the next frame it reads carries a
**forward seq gap**. This gap happens **PRE-eviction** (eviction was at 98992), so **no
`KIND_VIEWER_EVICTED` marker precedes it** → `attach_skip_to_live` never arms snap-above
→ brain.rs:628 FATALS → `serve_attach` returns → forwarding stops →
`attach_received_pty_output = FALSE`.

This is **DISTINCT** from `REQ-VIEWER-SKIP-TO-LIVE-ON-EVICT` (the POST-eviction
re-subscribe-from-floor). This one is **PRE-eviction gap-tolerance while STILL subscribed**.

## Fix (additive, VIEWER-only, B2-safe)

doyle's correction to the first proposal: scope the snap-above to the **VIEWER, not the
controller**. A controller advances `delivered_through`; letting it skip rolled frames =
not-exactly-once resume = **B2 hazard**. The controller keeps its strict reject-gap. The
failing attaches are all `AttachIntent::Viewer`, and a viewer is never authoritative →
snapping it is B2-safe by construction.

1. **Arm snap-above at the INITIAL viewer attach.** New brain method:

   ```rust
   // [impl->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]
   pub fn attach_as_viewer_snap(&mut self, session_id: u64, from_seq: u64, by: Option<&str>) -> io::Result<()> {
       self.session_id = Some(session_id);
       self.next_seq = from_seq;
       self.subscribe_with(session_id, from_seq, AttachIntent::Viewer, by.map(str::to_string))?;
       self.session_cursors.insert(session_id, from_seq);  // → brain.rs:607 path active
       Ok(())
   }
   ```

   Mirrors `attach_skip_to_live` but seeds at the requested `from_seq` (the viewer asked
   from there — dedup BELOW it, snap ABOVE a forward gap) and subscribes from `from_seq`,
   NOT the MAX skip-replay, because this is the FIRST attach. Subscribe FIRST (an empty map
   skips `subscribe_with`'s `from_seq` reset), THEN insert the cursor.

2. **Wire it** in `serve_attach` at `intent == Viewer` (attach.rs:199): viewers call
   `attach_as_viewer_snap`; controller/take keep `attach_as` (strict reject-gap).

3. **Keep** the `KIND_VIEWER_EVICTED` marker + `attach_skip_to_live` UNCHANGED — they
   handle the POST-eviction case (the broker REMOVED the subscription; the viewer needs a
   fresh re-subscribe from floor to get any frames at all).

The two **COMPOSE**: (1) tolerates pre-eviction ring-roll gaps while subscribed,
(2) recovers post-eviction.

## Evidence (stages)

<!-- [doc->REQ-HAZARD-VIEWER-RING-ROLL-SNAP] -->

- **doc** — CONTEXT.md viewer paragraph (a viewer snaps a forward ring-roll gap, not just
  on eviction) + this JIT.
- **impl** — `Brain::attach_as_viewer_snap` (brain.rs) + `serve_attach` viewer wiring
  (attach.rs).
- **unit** — `viewer_snap_attach_accepts_a_forward_output_gap` (brain.rs): a viewer-armed
  brain ACCEPTS a forward Output gap via snap-above; `cold_brain_still_rejects_a_forward_output_gap`
  stays GREEN (a non-viewer cold brain keeps the legacy reject-gap).
- **int** — NOT YET ACTIVATED (rule 5): `inject_control_wedge.rs` `a_journaled` + `p0_paste`
  + `wedged_viewer` on the warm forkpty fold (kitsubito CI), GREEN only on the COMBINED
  delivery-control fold (pump-carrier-fix + b4 + skip-to-live + this). Activate `int` + tag
  those gates when this folds and the forkpty gate is green.
