docs: record the GPU terminal input fix in the three required docs

COHERENCE.md section 25 and the handoff/ledger update protocols make
these ride the PR.

COHERENCE.md:
- Section 6 named one optimistic key classifier and attributed it to the
  GPU. There are two, one per replica frontend:
  crate::optimistic::classify_key belongs to the pmacs --attach TUI
  replica, and pmacs-gpu has its own unrelated optimistic_insert_text /
  optimistic_crdt_insert. The section's "kept honest by
  dispatch_idle_for" claim is confirmed for both, which this
  investigation verified rather than assumed.
- Section 16 graded per-frontend degradation strong on the evidence of
  per-frontend fold projection. That grade stands, but the practice is
  enforced by convention rather than structure, and this defect is the
  counter-example; the note says so and points at what is now structural.
- Section 2 step 8 records that the terminal was broken outright on the
  GPU frontend, not merely undiscoverable.

docs/agent-handoff.md section 5 gains four lessons: adjacency does not
make two operations alternatives (and two individually sound idempotence
guards can be jointly useless); bite against every pre-image the fix
could have taken, since the obvious guard here fixes the storm and
introduces a controller leak; a quiet child is an instrument, because a
frame storm hides inside a chatty fixture and a geometric readout is
satisfied by an oscillating geometry; and TerminalMode::Raw makes
sh-based input fixtures useless because there is no ICRNL.

docs/active-work.md gains the lane entry with the branch, the bite
matrix, the named out-of-scope items, and the gate results.
This commit is contained in:
Levi Neuwirth 2026-07-25 15:28:47 -04:00
parent b775f1703a
commit adf90e5351
3 changed files with 109 additions and 4 deletions

View File

@ -363,7 +363,7 @@ Full verdict table:
| 5 | Edit | **Works** | Full CUA + Emacs keymap in 161 lines (`builtin/keymaps/default.lua`); isearch, query-replace, kill ring, undo/redo, auto-indent/pair/comment, atomic save. Genuinely excellent zero-config | | 5 | Edit | **Works** | Full CUA + Emacs keymap in 161 lines (`builtin/keymaps/default.lua`); isearch, query-replace, kill ring, undo/redo, auto-indent/pair/comment, atomic save. Genuinely excellent zero-config |
| 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua:44-52`) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose | | 6 | Language intelligence | **Partial** | Rust grammar bundled and auto-attaches; rust-analyzer preconfigured (`builtin/runtime/lsp.lua:44-52`) — but a missing binary fails silently (§1.2) and highlighting masks it. No LSP status command exists to diagnose |
| 7 | Find symbol / file | **File: missing → in flight (PR #162). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit; `M-.`/`M-?`/`C-c o` bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI | | 7 | Find symbol / file | **File: missing → in flight (PR #162). Symbol: works but undiscoverable** | No find-file/dired/picker existed at audit; `M-.`/`M-?`/`C-c o` bound but advertised nowhere and server-gated; no workspace-symbol command; `pmacs.index.*` has no UI |
| 8 | Open terminal | **Works but undiscoverable** | Full PTY with scrollback + modeline segment — reachable only as `M-x terminal`, no keybinding | | 8 | Open terminal | **Works but undiscoverable** | Full PTY with scrollback + modeline segment — reachable only as `M-x terminal`, no keybinding. *Was broken outright on the GPU frontend until the double terminal-layout sync was fixed: the child took a `SIGWINCH` storm at tick cadence, so typing into it was impossible while output still flowed.* |
| 9 | Build / test | **Partial** | `M-x compile.run` works, defaults cwd to detected project root, parses Rust `-->` errors — but no keybinding, an **empty first prompt** (`initial = last and last.cmdline or ""`, `builtin/runtime/compile.lua:1134-1138`), and no `cargo build`/`cargo test` suggestion despite `ProjectKind::Cargo` existing (`src/project.rs:77`) | | 9 | Build / test | **Partial** | `M-x compile.run` works, defaults cwd to detected project root, parses Rust `-->` errors — but no keybinding, an **empty first prompt** (`initial = last and last.cmdline or ""`, `builtin/runtime/compile.lua:1134-1138`), and no `cargo build`/`cargo test` suggestion despite `ProjectKind::Cargo` existing (`src/project.rs:77`) |
| 10 | Inspect error | **Partial (good once reached)** | `E:n W:n` modeline counts, underlines, `M-g n/p` + ``C-x ` `` walking a unified compile/grep/diag source, message echo, `RET` visits. Gated entirely on step 6 or 9 succeeding first | | 10 | Inspect error | **Partial (good once reached)** | `E:n W:n` modeline counts, underlines, `M-g n/p` + ``C-x ` `` walking a unified compile/grep/diag source, message echo, `RET` visits. Gated entirely on step 6 or 9 succeeding first |
| 11 | See background work | **Works but undiscoverable** | `*workers*` view via `M-x editor.list-workers`; `C-c C-k` cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9) | | 11 | See background work | **Works but undiscoverable** | `*workers*` view via `M-x editor.list-workers`; `C-c C-k` cancel-at-point. No keybinding, no statusline spinner/progress indicator anywhere (§9) |
@ -671,9 +671,18 @@ Facts that define the gap:
optimistic-apply correctness), and `dispatch_paste` optimistic-apply correctness), and `dispatch_paste`
(`editor.rs:1129-1140`). (`editor.rs:1129-1140`).
- Off-path hardcodes: client-side **F12 detach** (`is_detach_key`, - Off-path hardcodes: client-side **F12 detach** (`is_detach_key`,
`src/attach.rs:997-1006`) and the GPU **optimistic key classifier** `src/attach.rs:997-1006`) and the replica frontends' **optimistic key
(`crate::optimistic::classify_key`) — the latter is classification, classifiers** — classification, not routing, and kept honest by
not routing, and is kept honest by `dispatch_idle_for`. `dispatch_idle_for`. There are **two, one per replica frontend**, and the
original audit named only one: `crate::optimistic::classify_key` belongs to
the **`pmacs --attach` TUI** replica (`src/attach.rs:843` is its only
consumer), while `pmacs-gpu` has its own, unrelated
`optimistic_insert_text` / `optimistic_crdt_insert`
(`pmacs-gpu/src/main.rs:2694`/`3306`). The "kept honest by
`dispatch_idle_for`" claim was **verified for both** while investigating the
GPU terminal input defect: a focused terminal buffer is in
`round_trip_buffers` (`src/terminal/session.rs:338`), so `dispatch_idle_for`
reports false and neither classifier can fire there.
**The counter-example that proves the idiom:** the entire picker/panel **The counter-example that proves the idiom:** the entire picker/panel
family — listview (references, outline), project-search, buffer-list, family — listview (references, outline), project-search, buffer-list,
@ -1245,6 +1254,14 @@ its asks are already practiced.**
selected from the negotiated `semantic_render` bit) so a grid selected from the negotiated `semantic_render` bit) so a grid
frontend collapses folds while a simultaneous GPU session does not frontend collapses folds while a simultaneous GPU session does not
skip lines (#149/#148). skip lines (#149/#148).
- **But it is enforced by convention, not by structure.** The GPU terminal
input defect was a per-frontend-kind operation applied to *both* kinds:
the dispatcher's grid and semantic terminal-layout syncs were written as
twins and executed as siblings, so a GPU session's PTY was resized twice
per tick forever. `sync_terminal_layouts_for_tick` now makes that one
exclusive by construction; every other per-frontend-kind pair in the
dispatcher remains two adjacent `if`s that a reader must notice are
alternatives.
- The GPU frontend exceeds the TUI (minimap, squiggles, typography) - The GPU frontend exceeds the TUI (minimap, squiggles, typography)
without the TUI losing the model — the "no privileged frontend" rule without the TUI losing the model — the "no privileged frontend" rule
is holding under real divergence pressure. is holding under real divergence pressure.

View File

@ -227,6 +227,58 @@ If it does not, stop and repair the remote/fetch configuration.
buffer a directory should resolve *to*, and `pmacs .` should route into buffer a directory should resolve *to*, and `pmacs .` should route into
it rather than growing a second directory surface. it rather than growing a second directory surface.
## GPU terminal input lane — IN REVIEW
- Portable branch: `githubsucks/gpu-terminal-input`, worktree
`../pmacs-gui-term-input`, based on `githubsucks/main` @ `46a1b8f`.
- Approved framing: `docs/gpu-terminal-input-framing.md` revision 2,
committed as the branch's first commit (`9a0df21`). Bug fix, not a
feature; **no protocol change (stays v20)**.
- Reported as "text input within the terminal doesn't work on GUI, this is
fine in TUI". Root cause: the dispatcher applied **both** terminal-layout
syncs to **every** attached frontend, and a semantic session satisfies both
conditions (a `term_sizes` entry from `AttachRequest` *and* a terminal
declaration). Its PTY was resized twice per tick forever — grid arm installs
the TUI placement size, semantic arm installs the declared content
rectangle, each arm's idempotence guard seeing only what the other just
wrote — so the child took a `SIGWINCH` storm at tick cadence.
- **The fix is a split, not a guard.** The grid arm is also the only per-tick
controller-liveness release a semantic frontend gets, and
`sync_semantic_terminal_layout` cannot take that over: the buffer-follow
snapshot clears the viewport declaration (`on_buffer_snapshot_sent`), so
that arm stops running in exactly the switch-away case that needs the
release. `sync_terminal_layout` is therefore split into a
frontend-kind-neutral half (panel reconcile + liveness) and a grid-only
geometry half, with the loop body extracted to
`sync_terminal_layouts_for_tick` so the exclusivity is structural and tests
drive the real thing.
- **Trap for anyone touching this again:** the release at the "no
`window_placements` entry" arm reads like liveness and is grid geometry. A
semantic frontend has no placement entry at all, so moving it into the
neutral half releases a GPU controller every tick.
- Bite-verified against **two** pre-images, because the naive guard fixes the
storm and introduces the leak:
| pin | `main` | naive guard | the split |
|---|---|---|---|
| settle (acc 2+3) | FAIL | pass | pass |
| controller release (acc 6) | pass | FAIL | pass |
| grid still resizes (acc 5) | pass | pass | pass |
- Real-path evidence: a quiet child trapping `SIGWINCH` reports **144 frames
in 4 s and `WINCH 1..12` on screen** against the pre-fix tree, versus a
settled screen with the fix.
- **Deliberately out of scope, named:** interactive-shell echo on a raw-mode
PTY (Q#GT5 — reproduces in-process too, so it is not the GUI/TUI
asymmetry), and a geometry change appearing to clear the visible screen
(reproduces pre-fix; why acceptance 4 latches its observation across
frames).
- Verification on this branch: `cargo fmt --check` clean; strict workspace
Clippy clean; 1,829 default + 2,006 CRDT library tests; vterm Stage 1/2/3
10 / 6 / 9 CRDT; bottom-panel Stage 1 46; M4 121; required GPU 155;
**isolated-config workspace sweep 3,177 across 92 suites, zero failures**;
`git diff --check` clean. Gates were run against the committed tree.
## Bottom-panel lane (window placement + side windows) — Stage 1 IN REVIEW ## Bottom-panel lane (window placement + side windows) — Stage 1 IN REVIEW
- Portable branch: `githubsucks/bottom-panel`, worktree - Portable branch: `githubsucks/bottom-panel`, worktree

View File

@ -704,6 +704,42 @@ final variant — its own round-trip cannot detect a discriminant shift.
## 5. Hard-won ops lessons ## 5. Hard-won ops lessons
- **Two operations that must be alternatives are not made alternatives by
being adjacent.** The dispatcher applied its grid and semantic
terminal-layout syncs to every attached frontend; a semantic session
satisfies both conditions, so its PTY was resized twice per tick forever
and the child took a `SIGWINCH` storm that made a GPU terminal untypable
while output still flowed. Each arm had a correct `old_size == size`
idempotence guard — **individually sound, jointly useless**, because each
saw only the size the other had just written. Write mutually exclusive
per-frontend-kind work as one `if`/`else` keyed on the same fact session
establishment uses, and extract the loop body so a test can drive the real
thing.
- **Bite against every pre-image the fix could plausibly have taken, not just
`main`.** For the same defect, the obvious one-line guard (skip the grid arm
for semantic frontends) *does* fix the storm — and silently introduces a
controller leak, because that arm was also the only per-tick
controller-liveness release a semantic frontend got. A single revert would
have scored the fix complete. The pin that catches it (`acc 6`) deliberately
**passes on `main`** and fails only against the naive guard: today's defect
supplies the release by the accident of running an arm it should not.
- **A quiet child is an instrument.** A frame storm is invisible against a
fixture that legitimately emits hundreds of frames, and an assertion like
`frames >= 2` cannot see one. The same applies to geometry: a
"did a frame at the new width arrive" readout is satisfied by a geometry
*oscillating through* that width. Assert upper bounds over a fixed window
against a child that produces nothing, and let the child self-report the
signal you care about (a `SIGWINCH` trap printing a **fresh distinct**
breadcrumb per signal — repeated identical markers paint nothing, because
`cell::diff` skips already-matching cells).
- **`TerminalMode::Raw` makes `sh`-based input fixtures useless.** There is no
`ICRNL`, so Enter delivers CR and a `read -r` loop waits forever for a LF
that never comes — the test then "proves" input never arrived. Use
`exec cat`, which copies stdin to stdout byte by byte. It is also the right
echo instrument for the opposite reason people assume: termios `ECHO` is
*off* in raw mode, so nothing double-echoes and one keystroke yields exactly
one cell.
- **The checkout may be shared with the user.** Check `git status` for - **The checkout may be shared with the user.** Check `git status` for
foreign uncommitted work before any stash/checkout/branch surgery; foreign uncommitted work before any stash/checkout/branch surgery;
never assume dirty files are yours. (Their uncommitted fix was nearly never assume dirty files are yours. (Their uncommitted fix was nearly