Skip to content

chore: cover the mouse encoder in the lifecycle model - #32

Merged
ThomasK33 merged 3 commits into
mainfrom
verify-lifecycle-mouse-encoder
Oct 29, 2026
Merged

ThomasK33 merged 3 commits into
mainfrom
verify-lifecycle-mouse-encoder

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

This updates the verify-native-lifecycle model to describe the code that #12 added. The sync commit is 7366b7545170ad77f5151e445c60bbf5bb110d87, the squash commit of #12 on main.

Why

#12 adds a second libghostty handle (mouse_encoder_) and encodeMouse. The old model had neither. Its assumption "JS code cannot run inside a native method" is false for encodeMouse: Get() runs getters, including Object.prototype getters for keys that src/index.ts leaves out. That window let a getter dispose the terminal while EncodeMouse held copies of the handles (finding F2, fixed in #12 before it merged).

What changes

  • The model gets the encoder handle: allocation, the constructor's failure path, DisposeNative, and the finalizer.
  • encodeMouse is split at its JS windows: the TypeScript normalizers after #assertUsable (EncodeTs), the native Get() reads (EncodeEnter), and the libghostty calls (EncodeCall). EncodeThrows covers a getter that throws.
  • New rule EncoderFreedOnce. NoCallOnDeadHandle also covers the encoder handle.
  • New mutants:
    • mutant-handles-before-reads: the code before the F2 fix.
    • mutant-no-native-guard: no RequireTerminal or RequireMouseEncoder check.
    • mutant-keeps-encoder-pointer: DisposeNative does not clear mouse_encoder_.
  • SKILL.md records F2 and the new coverage rows, bounds and assumptions. It corrects one sentence: removing only the native null checks does break NoCallOnDeadHandle, because a getter can dispose the terminal after #assertUsable passed.
  • statechart-view.mjs labels the new steps and the encoder handle.

Result

run.sh exits 0. All 35 rows match EXPECT, and current.cfg has 8,661 distinct states (it had 2,121).

CONFIG RULE RESULT EXPECT
current CreateUsesOwnClass holds holds
current LoadDeletesOnlyOwnRef holds holds
current HandleFreedOnce holds holds
current EncoderFreedOnce holds holds
current NoCallOnDeadHandle holds holds
mutant-process-static CreateUsesOwnClass breaks breaks
mutant-process-static LoadDeletesOnlyOwnRef breaks breaks
mutant-process-static HandleFreedOnce holds holds
mutant-process-static EncoderFreedOnce holds holds
mutant-process-static NoCallOnDeadHandle holds holds
mutant-dispose-keeps-pointer CreateUsesOwnClass holds holds
mutant-dispose-keeps-pointer LoadDeletesOnlyOwnRef holds holds
mutant-dispose-keeps-pointer HandleFreedOnce breaks breaks
mutant-dispose-keeps-pointer EncoderFreedOnce holds holds
mutant-dispose-keeps-pointer NoCallOnDeadHandle holds holds
mutant-no-use-guards CreateUsesOwnClass holds holds
mutant-no-use-guards LoadDeletesOnlyOwnRef holds holds
mutant-no-use-guards HandleFreedOnce holds holds
mutant-no-use-guards EncoderFreedOnce holds holds
mutant-no-use-guards NoCallOnDeadHandle breaks breaks
mutant-no-native-guard CreateUsesOwnClass holds holds
mutant-no-native-guard LoadDeletesOnlyOwnRef holds holds
mutant-no-native-guard HandleFreedOnce holds holds
mutant-no-native-guard EncoderFreedOnce holds holds
mutant-no-native-guard NoCallOnDeadHandle breaks breaks
mutant-keeps-encoder-pointer CreateUsesOwnClass holds holds
mutant-keeps-encoder-pointer LoadDeletesOnlyOwnRef holds holds
mutant-keeps-encoder-pointer HandleFreedOnce holds holds
mutant-keeps-encoder-pointer EncoderFreedOnce breaks breaks
mutant-keeps-encoder-pointer NoCallOnDeadHandle holds holds
mutant-handles-before-reads CreateUsesOwnClass holds holds
mutant-handles-before-reads LoadDeletesOnlyOwnRef holds holds
mutant-handles-before-reads HandleFreedOnce holds holds
mutant-handles-before-reads EncoderFreedOnce holds holds
mutant-handles-before-reads NoCallOnDeadHandle breaks breaks

The mutant-handles-before-reads counterexample is the F2 bug: load the addon, create t1, call encodeMouse, copy the handles in native code, dispose t1 in a getter, then call libghostty on the freed handles. test/mouse-safety.test.ts on #12 replays it against the real addon. That test fails on the code before the fix and passes after it.

Not covered: the mouse argument checks, the encoded bytes and the mouse mode cache. #12's tests cover those.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

PR #12 adds a second libghostty handle (mouse_encoder_) and encodeMouse,
which reads caller values in two places where JS can run: the TypeScript
normalizers after #assertUsable, and the native Get() reads. The old model
had neither, and its assumption "no JS runs inside a native method" is false
for encodeMouse.

Model the encoder handle (create, the constructor's failure path, dispose,
finalize), split encodeMouse at the JS windows, and add EncoderFreedOnce.
Three new mutants: the code before the F2 fix (handles taken before the
reads), no native null checks, and DisposeNative keeping mouse_encoder_.
Record F2, move the sync commit to the PR #12 head, and update the view.

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: I2581fa76d309060eedd375edc79d0f9a2a3575a3
@ThomasK33
ThomasK33 changed the base branch from main to feat/mouse-encoder October 29, 2026 22:21
@ThomasK33
ThomasK33 changed the base branch from feat/mouse-encoder to main October 29, 2026 22:37
Change-Id: I78cc6ff2143014924571060acda28920218fceea
Signed-off-by: Thomas Kosiewski <tk@coder.com>
7366b75, the covered code changed only in PositiveUint16 and
NonNegativeSize (argument checks from #30, which the model does not
describe) and in how test/mouse-safety.test.ts loads src/. run.sh still
matches EXPECT in all 35 rows.

Signed-off-by: Thomas Kosiewski <tk@coder.com>

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_

Change-Id: Id8a32feb7af0e8da355aa2ebeaaaa327fd7924be
@ThomasK33
ThomasK33 marked this pull request as ready for review October 29, 2026 22:39
@ThomasK33
ThomasK33 merged commit 7b6c56c into main Oct 29, 2026
6 checks passed
@ThomasK33
ThomasK33 deleted the verify-lifecycle-mouse-encoder branch October 29, 2026 22:44
ThomasK33 added a commit that referenced this pull request Oct 30, 2026
Closes #37

## Why
Terminals ignored the DECSTR soft reset (`ESC[!p`), so a program that reset with it left the cursor hidden and the pen styled in every later snapshot. This moves the Ghostty pin to the `v1.4.0` release, which includes upstream DECSTR support.

## What changed
- The build pins Ghostty `v1.4.0` and Zig 0.17.0 (`mise.toml`, `mise.lock`, the build script and its docs).
- `native/terminal.cc` follows two C API changes. Terminal creation now sets scrollback with `ghostty_terminal_set`, and the UTF-8 mouse mode read uses `ghostty_terminal_get`.
- The DECSTR test now asserts the reset, and the README drops the soft-reset limit.
- `resize` does a narrower and taller resize in two steps, rows first. Ghostty v1.4.0 skips the scrollback pull when one call does both, which left the prompt up with blank rows below it.

## Scope
- The lifecycle model's coverage record still names its sync commit's code. I will move its sync commit to this PR's squash commit in a follow-up, as #32 did after #12.

## Tradeoffs
- `scrollbackLimit` maps to the byte limit, not the new line limit. The old Ghostty counted `max_scrollback` in bytes, and only the byte setter keeps 0 as "no history". The unit was never documented, so changing it would be a silent behavior change.
- I pinned the `v1.4.0` release instead of `f473f31`, the first commit with DECSTR. It is a supported release, and it needs Zig 0.17.0 instead of 0.16.0.

## Blast Radius
Every terminal now runs on Ghostty `v1.4.0` (2,472 upstream commits). An independent verifier diffed main against this branch through the packed package. The differences it traced to upstream fixes include reflow cursor placement, C1 and C0 control characters no longer printing as cells, Unicode 18 width tables, and `formatHtml` no longer adding a newline at each page. Building from source now needs Zig 0.17.0.

## Upstream changes consumers can see
These come from Ghostty `v1.4.0`, not from this binding:
- Reflow keeps the cursor at the last write position, and a pending wrap is repaired after a resize.
- C0 (0x10 to 0x1F) and C1 (0x80 to 0x9F) control characters no longer print as cells. Unicode 18 width tables change the width of 1,021 newly assigned codepoints.
- CHA, VPA, HPR and VPR are origin-relative, BS stops at the left margin, and TBC changed.
- With OSC 133, ED2 at a prompt moves the screen into scrollback. A `scrollbackLimit` of 0 no longer keeps rows from a scroll region at the top.
- In mouse mode 1005, buttons eight and nine now send bytes (`c2a0`, `c2a1`).
- `formatHtml` no longer adds a newline at each page.

## Verification
- The full local gate passes: libghostty build, native build, typecheck, 73 tests, smoke, `npm pack`, the UBSan build and tests, and the TLA+ lifecycle model.
- Scrollback, old build against new build, for 20,000 lines: an unset limit and 0 keep no history on both. A limit of 1 to 100,000 keeps 969 lines on the old build and 903 on the new one. Both are Ghostty's page minimum. A new test fails when the code uses the line limit instead.
- In the verifier's 400-scenario seeded fuzz, the resize fix brings 16 more scenarios back to main's exact state, and no scenario diverges earlier than before.
- The Zig 0.17.0 checksums in `mise.lock` match ziglang.org's `index.json` for linux-x64, linux-arm64 and macos-arm64.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
@ThomasK33 ThomasK33 mentioned this pull request Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant