{
  "summary": "Verdict: the server-side `ScreenGrid` is capable of manufacturing the reported two-character left-margin scraps during a cold attach/reconnect repaint. The source seam is the width model, not byte loss in the daemon writer. `Cell` stores one Rust `char` per grid slot (`screen.rs:122-145`), and `put_char` advances exactly one column for every Unicode scalar (`screen.rs:327-344`). There is no wide-lead/wide-continuation representation, no combining/grapheme attachment, and no Unicode-width dependency in `spt-term`. Consequently, a width-2 glyph consumes one server cell but two cells in Windows Terminal. The synthesized repaint then emits the server row as a sequential Unicode string after an absolute CUP (`screen.rs:598-607`). If the server row is full according to scalar count but two columns overfull according to terminal display width, its final two characters wrap into the next physical row. When that next row is default-blank, `render_repaint` skips it, so nothing overwrites the newly wrapped characters. The leading `ESC[2J` does not refute this mechanism: it clears old residue before painting, while these scraps are created afterward by the repaint itself.\n\nDeterministic byte-level regression seam `[INFERENCE]`: use a 3×10 stateful reference terminal. Feed the authoritative/original terminal and `ScreenGrid` this byte trace: `ESC[1;1H` + UTF-8 `界界ABCDEFTo` + `ESC[2;1H ESC[2K ESC[3;1HSearch`. On a display-width-aware terminal, the two `界` glyphs occupy four columns; `ABCDEF` fills the rest of row 1; `To` legitimately wraps to row 2, and the following CUP+EL clears row 2. The correct final screen therefore has row 2 blank immediately above `Search`. `ScreenGrid` instead stores all ten Unicode scalars in row 1 because each `界` costs one cell; its EL sees row 2 already blank. Its repaint re-emits a 12-display-column row 1, falsely wraps `To` into physical row 2, skips modeled-blank row 2, and then CUP-paints `Search` on row 3. Substituting row suffixes `Wh` or `Es` produces the other reported fragments. A second deterministic seam is `界界Today ESC[1;5HNEW`: a real terminal overwrites the start of `Today` at display column 5, while the scalar-cell grid overwrites `day` and retains the false prefix `To`.\n\nThe regression must be differential and stateful: process the original trace in a real/reference VT grid, process `ScreenGrid::render_repaint()` in a fresh instance of the same emulator, and compare physical cells. Do not compare `ScreenGrid` to another `ScreenGrid`; the existing stateful-looking test at `screen.rs:915-934` uses the same faulty model on both sides and cannot detect a shared width bug. Add the corresponding broker seam by feeding the trace through `OutputLog::append`, cold-subscribing from sequence 0, decoding the one output frame, and applying it to the reference emulator. Current code should fail with `To` in row 2.\n\nSupported mechanisms:\n- **Wide/zero-width handling: implicated.** Wide glyphs, combining characters, variation selectors, and ZWJ sequences are all represented as independent one-column `char` cells. This directly supports both two-column row drift and two-character wrap scraps.\n- **ICH/DCH/ECH/ED/EL over wide glyphs: implicated as amplifiers.** `erase_display`, `erase_line`, `erase_chars`, `insert_chars`, and `delete_chars` operate on scalar slots (`screen.rs:347-439`). Their narrow-ASCII ranges are otherwise straightforward, but without lead/continuation invariants they cannot erase or move a whole width-2 glyph atomically. The absolute-overwrite trace above shows the same class without requiring ICH/DCH.\n- **Repaint trimming: not independently wrong, but exposes the width bug.** Trimming only trailing default blanks after a full clear is semantically safe for a width-correct model. It becomes the decisive persistence step when a preceding over-wide row wraps into a modeled-blank row that is skipped.\n- **DECSTBM: refuted for this horizontal two-column shape on current main.** The grid tracks margins (`screen.rs:208-209`, `808-817`) and the repaint now emits either the tracked region or explicit `ESC[r` before final cursor placement (`screen.rs:618-640`). Tests cover this at `screen.rs:891-934`. DECSTBM defects move/scroll rows, not columns.\n- **Origin mode/DECOM: unsupported, but a separate vertical divergence.** There is no origin-mode field. Private mode handling recognizes only 25, 47, 1047, and 1049 (`screen.rs:469-479`); `CSI ? 6 h/l` is ignored, while CUP/VPA coordinates remain absolute (`screen.rs:754-795`). Repaint never restores DECOM. This can make subsequent raw cursor addressing differ vertically relative to the scroll region, but it does not explain deterministic two-column left-margin fragments.\n- **Synchronized updates (`?2026h/l`): unsupported, but a separate snapshot-atomicity risk.** Mode 2026 is ignored by the same `set_mode` switch. `Drain` forwards arbitrary reads of up to 8192 bytes (`reader.rs:117-148`), and each read is advanced into the grid independently. A cold attach can therefore snapshot between chunks of a synchronized frame even though a physical terminal would keep the old committed frame visible until `?2026l`. This can expose a transient mixed frame, but it does not specifically generate a stable horizontal offset without an accompanying width/cursor error.\n- **Writer path: refuted as a byte-mutating source.** `OutputLog::append` base64-envelopes the original chunk for live controller/viewer fan-out and advances the grid only after that fan-out, under the same log lock (`broker.rs:935-994`). A cold `from_seq == 0` attach substitutes exactly one synthesized repaint (`broker.rs:836-851`, `1064-1088`, `1290-1303`); resume attaches keep raw ring bytes. `controller_writer` writes the initial repaint before draining the live FIFO (`broker.rs:1810-1937`), and `viewer_writer` does the analogous initial-then-live order (`broker.rs:1634-1681`). Thus the grid can affect only the cold repaint; an already-live attachment receives unchanged child bytes. If the screenshot was produced without a cold attach/reconnect, this particular ScreenGrid mechanism is refuted for that occurrence.\n- **Exit/final-output ordering: refuted in the healthy current path.** ADR-0043 is implemented by queueing `Exit` behind output through the same per-sink writer. The controller loop handles `CtrlMsg::Output` then `CtrlMsg::Exit` FIFO (`broker.rs:1914-1941`). There remains an explicitly loud three-second wedged-sink fallback that direct-writes Exit (`broker.rs:1745-1772`), but that is a terminal/wedge degradation, not a two-column grid transformation.\n- **Resize: a real secondary server-grid race, not needed for the deterministic wide-glyph repro.** `dispatch_resize` resizes the physical PTY first and only afterward locks and resizes the grid (`broker.rs:4447-4467`). `GridState::resize` merely copies the top-left overlap and resets margins (`screen.rs:557-567`). On ConPTY, resize-triggered repaint bytes may be drained between those operations and interpreted at the old grid width, creating wrap/crop debris in a later cold repaint. This should be closed or serialized, but the width repro already establishes the requested screenshot shape without relying on timing.\n\nRecommended fix: replace `Cell { ch: char, pen }` with a display-cell invariant that distinguishes blank, glyph lead (with display width and complete grapheme/combining payload), and wide continuation. Use an explicit, pinned terminal-width policy as a direct dependency; attach zero-width scalars to the preceding glyph rather than advancing the cursor. Printing must reserve both cells for a width-2 glyph and apply terminal-compatible right-margin behavior. Overwrite, ED/EL/ECH, ICH/DCH, scrolling, and resize must clear/move entire glyphs and repair orphan leads/continuations at operation boundaries; resize must sanitize a wide glyph clipped at the right edge. Repaint must emit glyph leads once, skip continuation cells, and guarantee that the display width emitted for a modeled row never exceeds `cols`. This fixes the state model at the source; inserting ad hoc clears after rows would only mask one symptom. Track/replay DECOM and synchronized-update commit state separately if captured Claude bytes prove those modes are present. No files were modified and no tests/commands were run, per the read-only/non-goal constraints.",
  "files": [
    {
      "path": "crates/spt-term/src/screen.rs",
      "description": "Primary defect seam. `Cell` is a single `char` (`122-145`); `put_char` consumes one column per scalar (`327-344`); erase/insert/delete operate scalar slots (`347-439`); mode handling omits DECOM and synchronized updates (`469-479`, `754-817`); resize top-left-copies cells (`557-567`); repaint clears, emits each nonblank row sequentially, skips blank rows, restores DECSTBM, then positions the cursor (`577-640`). Existing tests cover DECSTBM but not display width/continuations (`891-934`)."
    },
    {
      "path": "crates/spt-term/Cargo.toml",
      "description": "Confirms `spt-term` directly depends on `vte` only for byte-to-action parsing and has no Unicode-width/grapheme dependency (`12-17`), matching the absence of a display-width policy in `ScreenGrid`."
    },
    {
      "path": "crates/spt-term/src/reader.rs",
      "description": "PTY drain reads arbitrary chunks into an 8192-byte buffer and invokes the sink once per read (`117-148`). This preserves byte order but allows ignored synchronized-update regions to be snapshotted between reads."
    },
    {
      "path": "crates/spt-daemon/src/broker.rs",
      "description": "Owns the production grid and writer seams. `OutputLog.grid` and cold repaint contract (`686-691`, `836-851`); raw live fan-out followed by grid advance (`935-994`); cold controller/viewer initial batch creation (`1064-1088`, `1290-1303`); physical-PTY-then-grid resize ordering (`1442-1447`, `4447-4467`); initial-before-live writer loops and output-before-exit FIFO (`1634-1681`, `1810-1941`); loud wedged-sink Exit fallback (`1745-1772`)."
    },
    {
      "path": "docs/adr/0031-server-side-screen-grid-render-repaint.md",
      "description": "Defines the scope boundary: the grid models current visible screen and affects only attach repaint; live frames remain raw (`17-35`). This is why the confirmed defect requires a cold attach/reconnect repaint."
    },
    {
      "path": "docs/adr/0043-terminal-render-lifecycle.md",
      "description": "Separates four render-lifecycle failure classes and records that trailing blank omission after `ESC[2J` is not itself a bug (`13-41`); requires state-complete DECSTBM replay and output-before-exit ordering (`43-66`). The wide-row overflow finding is a new model-width failure that acts after the clear."
    },
    {
      "path": "docs/KNOWN-HAZARDS.md",
      "description": "Hazard 7.47 (`712-716`) records the current renderer ownership, ordered output/Exit, teardown, and DECSTBM invariants. It supports treating an rc identity/status banner as a separate client overlay rather than ScreenGrid-generated PTY content."
    }
  ],
  "architecture": "The relevant path is: child TUI → OS PTY/ConPTY byte stream → `Drain::spawn` ordered read chunks → `OutputLog::append`. `append` sends the original bytes unchanged to existing controller/viewer queues and independently interprets them into the broker-owned `ScreenGrid`. For a cold attach only, `repaint_initial(0)` snapshots that grid and synthesizes one ANSI repaint. The dedicated sink writer writes that repaint, then writes subsequent raw live frames FIFO. Therefore there are two renderer domains that must not be conflated: (1) the child/terminal protocol, whose live bytes Windows Terminal interprets directly, and (2) the broker's reconstructed current-screen model, used only to establish a cold client baseline. The confirmed defect is exclusively at the second domain's Unicode-scalar-to-display-cell boundary.\n\nThe physical client terminal is the final width authority. `render_repaint` first selects main/alt, resets SGR, clears, and homes; it then CUP-addresses every modeled nonblank row and emits its scalar contents. Because server and client disagree about display width, a logically full server row can physically wrap. Blank-row elision then preserves the just-created wrapped suffix. This cleanly explains why apparently stale `To`/`Wh`/`Es` can appear even though the repaint issued a full clear and no daemon writer dropped spaces.\n\nThe rc identity/status banner is outside this server grid: no `ScreenGrid` or broker writer symbol creates such prose. It is a client overlay/final-output concern and can independently invalidate a renderer baseline, as ADR-0043/KH 7.47 describe. Likewise, PTY resize is controller-owned: the OS surface changes geometry and may emit a ConPTY repaint, while the broker later changes its own grid geometry. That ordering is a separate race and can intensify wrap artifacts, but it is not required for the deterministic width failure. Final-output ordering is another independent axis: current healthy writer queues sequence Output before Exit, so an rc banner appearing at the bottom does not establish that ScreenGrid appended it or that Exit overtook bytes.\n\nThe correct acceptance test topology is a differential triplet: original child bytes → reference stateful terminal A; the same bytes → `ScreenGrid` → synthesized repaint → fresh reference stateful terminal B; assert A and B have identical physical cell grids, cursor, active buffer, modes, and margins. Then pass the synthesized repaint through the broker cold-attach socket and assert terminal C equals A. This catches display-width, continuation, erase/cursor, DECSTBM/DECOM, synchronized-update, resize, and writer-order defects at their actual boundaries without allowing `ScreenGrid` to validate itself."
}