# RC-RENDER-TRUTH — milestone dispatch (doyle triage 2026-07-18)

Operator GO 2026-07-18: plan + build + drive to release publish.

Three waves, one release. All derive from doyle-seam-verified hertz RCAs (2026-07-16..18).
Authorities: **ADR-0042** (rc attach truth), **ADR-0044** (controller lease truth),
**ADR-0043** (terminal render lifecycle), **KNOWN-HAZARDS 7.46 / 7.47 / 7.48**. All
thirteen REQs are minted in `traceable-reqs.toml` (`required_stages = []`); todlando
activates each wave's set at that wave's build start (rule 5, per-wave activation).
Wave rank: W2 (control integrity, live split-brain repro) outranks W3 (render); W1 first
because W2's revoke path composes with W1's session-confirmed attach machinery.

## W1 — rc attach truth (ADR-0042, KH 7.46)

Branch suggestion: `build/rc-render-truth-w1`.
Activate at build start: REQ-RC-HONEST-SESSION-AUTHORITY, REQ-RESUME-UNBOUND-STAMP,
REQ-RC-HARNESS-ONLY-REFUSAL, REQ-RC-QUALIFIED-TARGET-CANONICAL, REQ-HAZARD-RC-ATTACH-TRUTH.

| Leg | Seam (verified) | Fix shape (ratified — build to this) |
|---|---|---|
| 1. Honest-session authority | `rc.rs` offline fast-fail at ~1178-1187 gated on `!session_confirmed`; its doc-comment invariant ("offline never co-occurs with a live session") is field-falsified | Run the bounded `SessionProbe::has_live_session_honest` gate BEFORE the fast-fail; probe-true → `run_attach_session_confirmed`; probe-false → existing refusal; dead client tree → refuse/reap. Reuse `SessionProbe`, no new heuristic. Update the stale doc-comment same commit. |
| 2. Resume UNBOUND stamp | W2 skeleton-write short-circuits on existing perch → resume pre-bind row stays `offline` (rc.rs ~1117-1120, 1166-1174 "accepted" note — superseded) | Resume launch stamps existing offline perch → UNBOUND before/with broker spawn; generation/session-safe rollback to offline on spawn-fail/session death; bind owns UNBOUND→ONLINE. Remove/rewrite the superseded boundary comment. |
| 3. Harness-only refusal | rc never preflights known `harness_only`/`controllable=Some(false)`; maps empty refused attach to stale-row copy | Refuse BEFORE attach with truthful copy ("online but harness-hosted; spt does not own its terminal"). |
| 4. Qualified-target canonical | `establish_attach` (rc.rs ~1389) resolves via `Address::parse` but passes the ORIGINAL qualified string as wire `AttachRequest.endpoint_id` (~1435); target compares verbatim vs bare | Carry the resolver's canonical bare id separately from the user-facing qualified spelling; wire always bare. N-1-additive. |

Umbrella int matrix (REQ-HAZARD-RC-ATTACH-TRUTH): offline-row + honest session ⇒ rc
attaches; offline + no session ⇒ refusal; zombie ⇒ refuse/reap never attach;
resume-never-bound reads UNBOUND and is attachable; harness-only refuses truthfully
pre-stream; bare + `id@node` + `subnet:id` all attach with bare wire id.

W1 gate rulings (doyle, binding):
- Legs 1+2 BOTH land — reader-truth and writer-truth; neither substitutes for the other.
- Every daemon-tree-spawning test enters the HEAVY nextest group AT BIRTH (ci.yml Phase A
  + Phase B + nextest.toml override — three identical strings).
- ADR-0041 substrate stays green: the REGISTRY-LIFECYCLE W2/W3 lifecycle suites are
  sibling-seam tests for anything touching perch status writes — run them all.
- Refusal-copy changes: grep tests/ + ci/ for the OLD copy as assertions before renaming.
- Field evidence: hertz preserved the organic pre-edit perri row at
  `C:/Users/decid/.omp/state/spt-endpoint/perri-info.pre-unbound-manual.json`
  (status=offline over a live session) — the int repro's shape source.

## W2 — controller lease truth (ADR-0044, KH 7.48)

Branch suggestion: `build/rc-render-truth-w2` (off main after W1 merges).
Activate at build start: REQ-CONTROLLER-LEASE-IDENTITY, REQ-INPUT-CONTROLLER-FENCE,
REQ-HAZARD-CONTROLLER-LEASE.

| Leg | Seam (verified) | Fix shape (ratified) |
|---|---|---|
| 1. Lease identity + loud/authoritative Take (P0-A/B) | `resolve_subscribe` same-identity branch (broker.rs:1296-1318) silent re-take, intent never consulted; loud Displaced only different-remote+Take (1334-1347); the notice itself is `try_send` best-effort (1342-1345) and can drop on a Full queue while `become_controller` still replaces | Unique lease per rc invocation/stream, carried through subscribe/slot/Input/Resize; node identity = attribution only; silent successor = same-lease + equal-or-newer generation ONLY (ADR-0038 fix-6 preserved); distinct-lease Take revokes ATOMICALLY — old lease fenced + old attach stream FORCE-closed even if the notice enqueue fails; distinct-lease Control = Busy. Acceptable seam: `(by, nonzero attach gen)` as logical lease. N-1: additive field, absent lease → legacy semantics. |
| 2. Broker input fencing (P0-C, RC-scoped) | `dispatch_input` (broker.rs:3920-3935) session-addressed, zero controller validation — displaced window keeps typing into the PTY | RC-origin Input/Resize bound to the active lease (or originating conn as N-1 surrogate); stale-lease commands rejected/dropped after replacement. Do NOT globally gate generic KIND_INPUT — shell/system injection (Minter::Shell) legitimately sends non-controller InputReq; fence keys on an additive ownership token validated for token-bearing/Minter::Rc requests (or a dedicated guarded RC-input verb), default-none preserves generic injection. REQUIRED defense — makes the invariant true even when notification is delayed/lost. Unit matrix adds: tokenless shell injection UNCHANGED, lands mid-controlled-session. |

Umbrella int matrix (REQ-HAZARD-CONTROLLER-LEASE): the hertz 8-step two-loopback-client
regression rides verbatim (Displaced + terminal completion to A; output to B only; A's
post-takeover Input/Resize mutate nothing; B's apply; metadata identifies B, one slot;
equal-lease/equal-gen replay stays silent — dispatcher recovery never self-displaces).

W2 gate rulings (doyle, binding):
- ADR-0038 fix-6 generation semantics preserved EXACTLY — redispatch D1/D1b + the
  ownership/generation suites are substrate, all stay green.
- The revoke path must be proven with the notice DROPPED (full queue / failed enqueue):
  the closed stream alone must terminate the incumbent's rc (its existing
  `PumpEnd::Displaced`/EOF handling — client side already correct, do not touch it).
- Wire additions additive-only; mixed-version attach (absent lease) keeps today's exact
  semantics for that subscriber — N-1 leg in the unit matrix.

## W3 — terminal render lifecycle (ADR-0043, KH 7.47)

Branch suggestion: `build/rc-render-truth-w3` (off main after W2 merges).
Activate at build start: REQ-BROKER-OUTPUT-BEFORE-EXIT, REQ-RC-VT-TEARDOWN,
REQ-PICKER-PURGE-STRUCTURED, REQ-SCREENGRID-REPAINT-MODE-REPLAY,
REQ-HAZARD-RENDER-LIFECYCLE.

Ranked order (hertz, doyle-ratified): P0 legs 1-3, P1 leg 4.

| Leg | Seam (verified) | Fix shape (ratified) |
|---|---|---|
| 1. Output-before-Exit (P0) | Exit waiter (broker.rs ~3757-3792) direct-writes KIND_EXIT via `all_sinks()`; output queues via `controller_writer` (~1675-1778); per-conn gate serializes bytes not producer order; test `broker.rs:375-407` already documents Exit-before-Output and compensates — production rc doesn't | PTY drain/output writer = sole FIFO sequencer; Exit enqueued behind all prior output per sink (drain completion first). A mutex alone is insufficient. |
| 2. rc VT teardown (P0) | `RawGuard::drop` (rc.rs:841-856) restores raw/mouse/console-mode only — zero VT display reset; every exit path can leave the terminal dirty | Separate display RAII guard from input guard; on EVERY exit path incl. unwind: SGR reset + scroll-region reset + cursor show + leave alt screen + clear/home, while VT processing enabled, THEN mode restore, THEN prose. Idempotent. |
| 3. Picker structured purge (P0) | Purge runs inline under live ratatui alt screen (picker/mod.rs:165-173); `cmd_endpoint_purge` writes stderr (cli.rs ~12707-12826) → diff-baseline desync | Purge core returns structured outcome, writes NOTHING to the terminal in TUI mode; picker sole renderer via `model.flash`. Regression needs a stateful/recording backend — pure TestBackend snapshots cannot catch baseline desync. |
| 4. Repaint mode replay (P1) | ScreenGrid tracks DECSTBM (screen.rs:789-793) but `render_repaint` (577-623) never replays it | Replay every tracked render-affecting mode (DECSTBM minimum) before final cursor placement; stateful emulator contract: dirty screen + repaint + next raw frame ≡ server grid. |

W3 gate rulings (doyle, binding):
- Prior art (herdr v0.7.4, AGPL): design ideas only — NO code copy without license review.
  Do not import its multiplexer stack; do not replicate its ignore-write-errors-and-commit
  baseline bug.
- Deferred P2 (seeds, NOT this milestone): `Exit{after_seq}` watermark; per-client
  semantic baseline.
- Leg 1 touches the broker write path — KH 7.43/7.44 substrate suites (redispatch D1/D1b,
  registry-stall legs) must stay green.
- The leg-1 int regression must ride the PRODUCTION broker→attach→rc path (the existing
  test's post-exit drain compensation is the anti-pattern — do not copy it into the new
  test).

## Sequence

1. todlando: W1 (activate W1 REQs at build start; branch; build; preflight
   clippy --workspace + targeted suites + traceable check).
2. doyle: gate W1 (isolated worktree under `.worktrees/`, fresh CARGO_TARGET_DIR, nextest,
   cross-platform leg if path semantics move — not expected).
3. todlando: W2 (controller lease) — same discipline. 4. doyle: gate W2.
5. todlando: W3 (render lifecycle). 6. doyle: gate W3.
7. deployah: release (bump-in-PR, CHANGELOG end-user voice, counter from published
   metadata).
8. hertz: field-verify (owns the live repro boxes, the split-brain two-terminal repro,
   and the stale-glyphs screenshots).

Evidence index: hertz RCA messages 2026-07-17/18 (doyle transcript), organic perri row
(path above), `.claude/reports/2026-07-16-gaki-n-elevated-rc/hertz-corrected-report.md`
(harness-only + qualified-target legs' original filing).

## Second reopen — the presentation barrier (doyle triage 2026-07-21)

Field red on a fully-0.39.3 node (CLI + broker, `broker_stale=false`) with the
geometry-epoch fix shipped and its gate green. Root, source-certain: the shipped
barrier is **parse-model only**. `broker.rs` says so itself — the `ResizeTransition`
doc (:699-704) states append "still rings + fans out every chunk exactly as before",
and `OutputLog::append` (:1287-1290) gates only `grid.advance`; ring, controller
`try_send` and viewer fan-out all run during a transition. Live attached terminals
receive old+new-geometry differentials across an already-resized viewport.
**Requirement-scope defect (doyle owns it): the gate held against documented design;
the REQ scoped the barrier to the wrong surface.** The cold-attach oracle is
definitionally blind to the live path — which is why green gate + red field
coexisted.

Authority: **ADR-0031 Amendment II** (the presentation contract, the non-grid
byte-class disposition table, the carried constraints). Succession REQ:
**REQ-RC-RESIZE-PRESENTATION-BARRIER** (minted registry-first, `required_stages = []`;
todlando activates `["doc","impl","unit","int"]` on the build branch).
REQ-RC-RESIZE-GEOMETRY-EPOCH stands as the satisfied parse contract — its evidence
is real; the new REQ owns the delivery surface.

Branch suggestion: `fix/rc-resize-presentation-barrier` (off main).

| Leg | Seam (verified) | Fix shape (ratified — build to this) |
|---|---|---|
| 1. Delivery suppression | `append` :1250-1274 hands off to controller + viewers unconditionally during a transition | Suppress controller handoff + viewer fan-out while a transition is open; still assign `seq` + ring every chunk. A skip, never a block (KH 7.12). Suppressed window exempt from the controller Full-eviction deadline (no handoff was attempted — reset `last_ok` at commit or gate the deadline check on the transition). |
| 2. Commit-time sync frame | `commit_resize` :1779-1788 replays into the grid + pushes `size` to viewers only; sinks that lived through the window get nothing but resumed raw | Under the same lock acquisition that commits: viewers get `size` FIRST, then EVERY attached sink (controller included) gets ONE synthesized sync frame = `render_repaint()` at target geometry ++ deferred non-grid bytes. No raw frame interleaves. Abort = same path minus `size`, at the old geometry (uniform mechanism; if a raw-flush-from-ring on abort looks trivially better, come back for a ruling — don't build it). |
| 3. Cursor-of-record | `delivered_through` advances only on successful controller socket writes — suppressed seqs would replay on resume | Controller sync frame carries watermark `seq` = highest suppressed seq (the `repaint_initial` shape); one successful write advances the cursor past the whole range as-if-written. |
| 4. Presentation floor on resume | The ring deliberately keeps raw mixed-geometry bytes; a resume-from-floor below the transition would replay the corruption back in | Log records the presentation floor (seq after last commit/abort). Resume asking below it → cold-attach shape (sync repaint + skip to live), never raw replay across the boundary. Existing irrecoverable-behind/repaint machinery pointed at a new predicate. |
| 5. Non-grid byte disposition | The sync frame substitutes for raw delivery; everything the repaint doesn't re-emit would silently drop | Per-class rulings in ADR-0031 Amendment II + the REQ: tracked state SUPERSEDED (unit-pinned per class); untracked stateful + one-shot events + queries DEFERRED verbatim inside the sync frame; hyperlinks/DCS DROPPED balanced. Classification in the ONE parser authority (ScreenGrid `vte::Perform` capture mode at replay) — never a second scanner. |

Gate legs (binding, hertz-enumerated + doyle-ratified):
- **Non-grid disposition**: every class in the table has an explicit ruling AND a
  unit pin — a title change, cursor-visibility toggle, DECSTBM change, alt-screen
  switch, and a bracketed-paste/mouse toggle inside the suppressed window each
  surface in/after the sync frame; a hyperlink pair does not.
- **Cursor past suppressed frames**: a resumed controller never re-receives a
  suppressed seq; monotonic CAS untouched.
- **Resume-across-resize**: controller detaches pre-resize, resumes post-commit →
  zero mixed-geometry raw bytes on its socket, exact target-geometry screen.
- **Int drives the REAL live controller writer**: attached controller, socket feed
  parsed by an independent authority (avt) at the CLIENT TARGET geometry across the
  whole transition; full untrimmed row equality, every row, every cell incl.
  expected-blank interiors, after the sync frame. Viewer leg: `size` precedes sync
  frame; zero raw frames between commit and sync. Fixture rules carry verbatim from
  REQ-RC-RESIZE-GEOMETRY-EPOCH (readable prose, repeated words, no reflow-crossing
  at the resize instant). **The cold oracle does not satisfy this stage.**
- **NO whitespace special-casing, NO extra clears** — a fix that passes by clearing
  harder fails gate (standing constraint).

Fenced SEPARATE (do not build unless the field stays red after this repair): the
heuristic epoch-split strengthening (`mark_resize_issued` :4934 precedes
`session.resize` :4935; the 20ms/250ms settle split is a heuristic —
`RESIZE_SETTLE_CAP` admits old-geometry bytes may land in the new epoch).

Absorbed by construction (record when building): the commit-time sync frame IS the
seeded "repaint transition-era attachers at commit" follow-up — update the KNOWN
ACCEPTED RESIDUAL text at `repaint_initial` and in REQ-RC-RESIZE-GEOMETRY-EPOCH
(the "not bounded in VISIBILITY" caveat dies; the stale window now ends at commit).

Release-note correction rides the shipping cut (deployah executes, doyle reviews):
the v0.39.0 and v0.39.1 Fixed claims for rc resize corruption get quote/gap/closure
treatment per the v0.39.3 template — quote the shipped claim, state the gap (the
shipped barrier gated the parse model only; live attached terminals still received
mixed-geometry output), state the closure.
