Repository navigation
Conversation
The crash message named one input (RIS, then ?47h) as the known cause, but ghostty-web 0.4.0 crashes on several inputs. It now says so and names coder/ghostty-web#215 as one known cause. A live render wait retried a crashed page for 10 polls. Each poll replays the log into a new page, so the wait took ten crashes and could return WAIT_TIMEOUT instead of the error. It now ends at the first crash with the same REPLAY_ERROR that a live snapshot gives. An offline REPLAY_ERROR and a WebM EXPORT_ERROR left out the failure that caused them. Their messages now end with its first line, cut at 300 characters.
|
@codex review |
|
@codex security review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
🛡️ Codex Security ReviewSecurity review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5f1939b40d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "ghostty-web crashed while it replayed this session's output. One known cause is a ghostty-web 0.4.0 bug: after a full reset (ESC c), the alternate screen (?47h or ?1047h) keeps the old cells, and output over a combining mark or other grapheme cluster there crashes the page. The libghostty-vt renderer and `record export --format svg` are not affected."; | ||
| "ghostty-web crashed while it replayed this session's output. This is a bug in the WASM module of ghostty-web 0.4.0, which several different inputs trigger. One known cause is https://github.com/coder/ghostty-web/issues/215 (a full reset, ESC c, then ?47h or ?1047h, then output over a combining mark or other grapheme cluster). The default libghostty-vt renderer (`snapshot` and `wait`), `record export --format svg` and `record export --format asciicast` do not use ghostty-web."; | ||
|
|
||
| const PAGE_CRASH_PATTERN = /Target crashed/u; |
There was a problem hiding this comment.
Qualify libghostty-vt as an optional workaround
When the optional native package is unavailable—or the user explicitly configured ghostty-web—semantic snapshot and wait commands use ghostty-web rather than “the default libghostty-vt renderer,” so following this crash message simply reproduces the same failure. Qualify this workaround as applying only when libghostty-vt is installed and selected, rather than implying that these commands universally avoid ghostty-web.
Useful? React with 👍 / 👎.
| isReplayCrashError(pollError) || | ||
| consecutiveFailures >= MAX_CONSECUTIVE_POLL_FAILURES |
There was a problem hiding this comment.
Preserve retries for unclassified Chromium crashes
When Chromium reports Target crashed because of a transient page/process failure such as resource pressure, describeReplayFailure classifies it as GhosttyWebCrashedError even though it cannot establish that the session output hit the deterministic WASM bug. The backend's crash handler marks it unbooted, so a later poll can recreate the page and recover, but this new branch now aborts on the first failure as a non-retryable REPLAY_ERROR; retain the existing retry allowance unless the failure can actually be tied to a deterministic input fault.
Useful? React with 👍 / 👎.
Why
Three things hide the real cause when the
ghostty-webrenderer fails.ESC c, then?47h). A plain print of 693 lines that hold combining marks crashes it too. For that input the old text pointed at a reset that was never there.wait --renderer ghostty-webhid the crash. The host retried every failed poll up to 10 times in a row. Each poll boots a page and replays the whole log, so the wait took ten crashes (about 2 s each on this host, longer on a loaded one). When--timeoutwas shorter than that, the wait ended asWAIT_TIMEOUT(exit 11) and the crash never showed. A livesnapshotandscreenshoton the same session gaveREPLAY_ERROR(exit 10).REPLAY_ERROR("Failed to boot or replay renderer for session ...") and arecord export --format webmEXPORT_ERRORended in a bare sentence for every failure that is not a page crash. TheRangeError: offset is out of boundsthat an empty output event gives was not visible.What
Crash text (
src/renderer/ghosttyWeb/crashError.ts)libghostty-vtrenderer forsnapshotandwait, andrecord export --format svgorasciicast.docs/TROUBLESHOOTING.md,docs/USAGE.mdand the READMEs ofsrc/renderer/ghosttyWeb,src/replay,src/cli/commands,src/hostandsrc/utilfollow the new text. No file underskills/quotes the crash text, so no public skill changes.Live render wait (
src/host/hostMain.ts,src/host/renderer.ts)MAX_CONSECUTIVE_POLL_FAILURES(10) failures in a row. I did not change this.isReplayCrashError(new, inrenderer.ts, next to the code that builds the error) recognizes theREPLAY_ERRORthatwithBackendthrows for a crash. The poll loop rejects with it at the first poll. The wait now gives the error thatsnapshotandscreenshotgive:REPLAY_ERROR, exit 10,retryable: false, noresult.--timeoutshorter than the first poll still ends asWAIT_TIMEOUT, because the timeout fires before the poll ends.Cause in offline errors (
src/util/errorSummary.ts,src/replay/offlineReplay.ts,src/cli/commands/record-export.ts)summarizeError(error): the first line of the error message, whitespace collapsed, at most 300 characters (ends in…when cut). It never carries a stack frame or the Playwright call log, which follow the first line.Failed to boot or replay renderer for session <id>: <cause>. Therecord exporterror readsFailed to export session "<id>" as <format>: <cause>. A page crash keeps its own explanation after a period, as before.detailskeep their shape and the JSON schemas do not change. Only themessagetext of those errors changes. No golden test pinned it.RPC_ERROR, so this adds no new kind of disclosure. Stack frames are never included.Not touched:
replayWithTiminginbackend.tsandwriteBatchinembeddedHarnessHtml.ts(PR #366). A trial merge with the head of #366 (074b3a31) has no conflicts.Validation
Machine: shared host, load 11 to 263 during the runs.
npm run format:check,npm run lint(0 warnings, 0 errors),npm run typecheck: clean.npx vitest run test/unit: 150 files, 2414 tests pass on the first commit (the second commit only splits one test into two, 2415 tests). See the load note below for the later run.check.sh --drift-only: "Covered code matches the sync record."--maxWorkers=1 --retry=0: 30 files (20 integration, 10 e2e: ghostty-web-crash, wait-render, renderer-failure-cause, host-renderer-rpc, record-export, empty-session-capture, e2e renderer-errors, record-diff, screenshot-out, screenshot-missing-glyphs, renderer-backend, batch, screen-hash, event-log, slow-replay, backend-selection, cli, run, mouse, artifact-manifest-race, pty-basics, and e2e export-fixtures, renderer-slice, unicode-grid, scrollback-demo, libghostty-vt-renderer, resize-demo, large-snapshot, hello-prompt, batch-fixture). 27 files passed on the first run. Three files had red tests. I judged each by its assertion, and each is a load timeout, not a result of this change:wait-render: "detects screen stability via waitForRender RPC" (Hook timed out in 30000msincreateSessionat load 69) and "prints the WAIT_TIMEOUT error to stderr when CLI --text times out without --json" (expected 1 to be 11:runClikilled the CLI at its 10 s limit, which gives status null). Both pass when run alone.mouse: the 5 "delivers ... bytes to a PTY" tests (Test timed out in 30000ms, each took 34 to 41 s at load 89). All 15 pass alone (those tests took 17 to 28 s).backend-selection: "defaults semantic snapshot rendering to libghostty-vt when available" (Test timed out in 5000ms). 7 of 7 pass with--testTimeout=60000. A copy of origin/main fails 7 of 7 at the 5 s default under the same load, so this is not from this change.Test timed out in 5000msor a timing bound (lineDiff: 22178 ms against 10000). All 247 tests of those 10 files pass with--testTimeout=60000 --hookTimeout=60000.New tests, and the mutant that turns each red (each mutant ran in a
git archivecopy, an identical-text control passed first):ghostty-web-crash.test.ts"names the crash of an input that has no reset, and ends a live ghostty-web wait at it", and unitghosttyWebCrash.test.ts"names the issue as one known cause ..." and "names the commands that do not use ghostty-web"isReplayCrashError(pollError) ||from the poll loopwait-render.test.ts"ends a render wait at the first poll that crashes the ghostty-web page" and the real-crash test aboverenderer.test.ts"tells the error of a crashed page from every other poll failure"wait-render.test.ts"retries a render poll that fails for another reason until the cap" and "keeps polling through fewer failed render polls than the cap and matches"offlineReplay.test.tsx2 (replayTo and boot) andrenderer-failure-cause.test.tsx3 (snapshot, screenshot, wait)record-export.test.tsx3 andrenderer-failure-cause.test.ts"... WebM export"summarizeErrorerrorSummary.test.tsandofflineReplay.test.ts"bounds the length ..." / "leaves the stack ... out of the message"Tests added: 8 in
errorSummary.test.ts, 2 inghosttyWebCrash.test.ts, 1 inrenderer.test.ts, 2 inofflineReplay.test.tsand 2 changed there, 2 changed and 1 added inrecord-export.test.ts(unit), and in integration 2 inwait-render.test.ts, 1 inghostty-web-crash.test.tsand the 4 tests of the newrenderer-failure-cause.test.ts.The wait tests count the host's render polls with
test/fixtures/inject-replay-failure.ts. A crash gives 1 poll, an ordinary failure gives 10 (MAX_CONSECUTIVE_POLL_FAILURES). Theghostty-web-crashtest uses a real Chromium crash and the same count, so it does not depend on how fast the host is.Dogfood with the real CLI (
node --import tsx src/cli/main.ts, isolatedAGENT_TTY_HOMEfolders under /tmp, sessions destroyed and folders removed). Input "crash A": 693 lines ofe\u0301x5 +abcdefghijx7 +abcde, 80x24, no RIS, no resize, no alternate screen. Wall time includes about 4 s oftsxstart under load.REPLAY_ERROR, 5.7 s, old text (blamesESC c)REPLAY_ERROR, 7.0 s, new textREPLAY_ERROR, 6.2 s, old text (blamesESC c)REPLAY_ERROR, 6.7 s, new textREPLAY_ERROR, 21.2 s, old text (blamesESC c)REPLAY_ERROR, 7.6 s, new textREPLAY_ERROR, 20.2 s, old text (blamesESC c)REPLAY_ERROR, 8.0 s, new textWAIT_TIMEOUT, 15.6 s, "Wait timed out before its condition was met."REPLAY_ERROR, 10.5 s, new textREPLAY_ERROR, 6.0 s, old text (blamesESC c)REPLAY_ERROR, 6.5 s, new textREPLAY_ERROR, 5.8 s, old text (blamesESC c)REPLAY_ERROR, 8.8 s, new textREPLAY_ERROR, 5.9 s, old text (blamesESC c)REPLAY_ERROR, 6.3 s, new textEXPORT_ERROR, 6.3 s, old text (blamesESC c)EXPORT_ERROR, 8.4 s, new textok, 4.8 sok, 5.1 sok, 4.8 sok, 5.7 sInput without a page crash: an exited session whose log holds an empty output event (
RangeError: offset is out of boundsin the page on main).REPLAY_ERROR, 23.2 s:Failed to boot or replay renderer for session <id>.REPLAY_ERROR, 5.9 s:Failed to boot or replay renderer for session <id>: page.evaluate: RangeError: offset is out of boundsREPLAY_ERROR, 17.9 s:Failed to boot or replay renderer for session <id>.REPLAY_ERROR, 6.3 s:Failed to boot or replay renderer for session <id>: page.evaluate: RangeError: offset is out of boundsREPLAY_ERROR, 8.8 s:Failed to boot or replay renderer for session <id>.REPLAY_ERROR, 6.8 s:Failed to boot or replay renderer for session <id>: page.evaluate: RangeError: offset is out of boundsEXPORT_ERROR, 4.8 s:Failed to export session "<id>" as webm.EXPORT_ERROR, 5.0 s:Failed to export session "<id>" as webm: page.evaluate: RangeError: offset is out of boundsThe wall times of the offline rows differ by load more than by the change. Only the live
waitrows are a before and after comparison: a crash used to cost one replay per poll, and now it costs one. Awaitwith a timeout shorter than ten polls ended asWAIT_TIMEOUTbefore the fix. The diagnosis run used--timeout 5000and my run above used--timeout 8000.Generated with
xum• Model:anthropic:claude-sonnet-5-5• Thinking:xhigh