Crash reports: replayable, bounded, local, reachable by agents (#384)

Every panic (in a command, in the kernel, anywhere else) and every unclean
exit (abort, stack overflow, kill) leaves a report in a local crash folder:
message, file:line, a trimmed backtrace, version, commit, system and renderer,
the command and recent commands, and when possible the design and replay.json
that reproduce it (solvecraft-cli crashes replay <id>).

- Cost: nothing until the app installs the hook (tests, CLI and web never
  do); then one small journal line per top-level command of the design on
  screen, taken back with a truncate when the command changed nothing.
- Unclean exits: each run holds a lock on its journal; a journal nobody holds
  at the next start belongs to an app that died. Autosaves restart the
  journal from the autosaved design.
- Bounded: a crash's signature (place + message, numbers ignored) is counted,
  not rewritten; at most 5 new reports per run, 20 reports, 50 MB, 90 days;
  recovered kernel panics are pruned first.
- Agents: diagnostics.crash_reports / crash_report / crash_reports.clear
  commands, the MCP crash_reports tool, solvecraft-cli crashes, solvecraft
  --crash-log; diagnostics.crash_test (with SOLVECRAFT_CRASH_TEST) checks it.
- People: the Crash Reports window opens after a crash (Help > Crash
  Reports…) with a reviewed issue draft per report; nothing is sent.
- Release builds keep line tables for file:line in backtraces.

Tests: child processes that really panic in a command, in the kernel, on a
thread, and abort; dedup counts, limits, hostile folders, journal parsing,
the UI window and draft, the MCP tool. docs/crash-reports.md.

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: bherbruck <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-12 16:50:49 -04:00
1 parent 55f2f29fff
commit 27415e4dd7
24 files changed
+1998 -9

No files matched your search

+1 -1
View File
@@ -75,7 +75,7 @@ unit regardless of the design, pass an expression with one: `"value": "50 mm"` o
| `ui.file` | `path`; `insert?`: bool | starts an interactive background Open or Insert; poll `ui.inspect.file_busy` for completion; the window stays responsive |
| `ui.home` | `open?`: bool; `sample?`: index | the start page: show or hide it, open a built-in sample (once built); returns `open` and the recent designs |
| `ui.shortcut` | `command`, `key?`, `replace?` | a command's key, or rebinds it (`""` clears; a key another command uses is refused unless `replace`) |
| `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (copies diagnostics) | the version, commit, build date and diagnostic text |
| `ui.help` | `item?`: `about`, `shortcuts` (read only), `report` (copies diagnostics), `crashes` (the Crash Reports window) | the version, commit, build date and diagnostic text |
| `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `tinkercad`, `openscad`), `zoom_reverse`, `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, pinch zooms), `orbit_cursor`, `look_at_anim`, `look_at_speed`, `msaa`, `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system`), `language` (`en`, `ja`; see [localization](localization.md)), `ui_scale`, `up_axis` (`z`, `y`), `offer_stand_up`; `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` |
| `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits |
| `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first |
+87
View File
@@ -0,0 +1,87 @@
# Crash reports
When the desktop app crashes or hits an internal error, it writes a crash report on this
computer. Each report holds enough to reproduce and fix the problem: in most cases the design
and the exact commands that trigger it. Reports are never sent anywhere. To share one, use
**Report…** in Help ▸ Crash Reports…, which opens a GitHub issue draft. Nothing is shared
until you review and submit it, and the design is not part of the draft.
## Where they are
`<data dir>/solvecraft/crashes/`, next to the recovery folder:
| System | Folder |
|---|---|
| Linux | `~/.local/share/solvecraft/crashes` (or `$XDG_DATA_HOME/solvecraft/crashes`) |
| Windows | `%LOCALAPPDATA%\SolveCraft\crashes` |
| macOS | `~/Library/Application Support/SolveCraft/crashes` |
`SOLVECRAFT_CRASH_DIR` overrides the folder. `solvecraft --crash-log` and
`solidcraft-cli crashes dir` print it.
## For agents
All of these read the same folder:
| From | List | One report | Delete |
|---|---|---|---|
| Engine command (MCP `execute`, control channel, scripts) | `diagnostics.crash_reports {limit?}` | `diagnostics.crash_report {id}` | `diagnostics.crash_reports.clear {id?}` |
| MCP tool | `crash_reports {limit?}` | `crash_reports {id}` | — |
| CLI | `solidcraft-cli crashes [--json]` | `solidcraft-cli crashes show <id>` | `solidcraft-cli crashes clear [<id>]` |
| Control channel (UI) | `ui.help {"item": "crashes"}` opens the window | | |
**Reproduce:** `solidcraft-cli crashes replay <id>` opens the report's design, runs its
commands, and reports whether the same internal error happens again (same place and message).
Without the CLI, run `solidcraft-cli run <report>/replay.json --in <report>/design.solvecraft`.
A fixed crash turns into a regression test with the same two files.
**Check that reporting works:** with `SOLVECRAFT_CRASH_TEST=1` set for the app, the command
`diagnostics.crash_test` has three modes:
- `panic`: an internal error in a command
- `kernel`: an internal error inside the geometry kernel
- `abort`: the app ends at once, and the report appears at the next start
## What a report holds
A folder `crashes/<YYYYMMDD-HHMMSS>-<signature>/`:
- `report.json` (`"format": "solvecraft-crash/1"`):
- `kind`: one of
- `command_panic`: a command hit an internal error; it was caught and the design was left unchanged
- `kernel_panic_recovered`: the third-party geometry kernel panicked and was caught. This is harmless on the desktop, but it would crash the web build.
- `panic`: an internal error outside any command
- `unclean_exit`: the app ended without a hook seeing it (an abort, a stack overflow, a segfault, or the process being killed). It is found at the next start.
- `message`, `location` (file:line), `thread`
- `backtrace`: frames `symbol (file:line)`, without the panic machinery, std internals or the user's home folder
- `app` (version, commit, build date), `system` (OS, architecture, renderer and GPU), `session.uptime_s`
- `command`: the command that crashed, with its parameters
- `recent_commands`: up to 32, with ok or error
- `base`: what the replay starts from (the design before the command, an autosave, a saved file or a new design)
- `count`, `first_seen`, `last_seen`: the same crash repeats only increase `count`
- `notes`: what is missing and why, for example a design too large to copy
- `design.solvecraft`: the design the replay starts from
- `replay.json`: `{"commands": [{command, params}, …]}`
## How it stays cheap and small
- **Before a crash:** the app keeps a journal of the top-level commands run on the design on
screen (`crashes/sessions/*.jsonl`, at most 512 KB). Each command adds two small writes,
with no flush to disk. Previews and other scratch sessions are not recorded. Each autosave
restarts the journal from the autosaved design. A clean exit deletes it. The journal file
is locked while its app runs, so at the next start, a journal nobody holds belongs to an
app that died.
- **A panic** captures its backtrace only the first time its signature is seen. The
signature is the place plus the message, with numbers ignored.
- **Limits:**
- at most 5 new reports per run
- 20 reports
- 50 MB in total
- 90 days
- a design over 10 MB is not copied; the report notes that
When over a limit, recovered kernel panics are deleted before real crashes, and the oldest first.
- **Where it's off:** the CLI, tests and the web build never install the hook. There,
commands pay only a check of one atomic flag.
Release builds keep line tables (`debug = "line-tables-only"`), so backtraces name the file
and line. Stripping a binary (`strip`) removes them, and the function names too.
+1
View File
@@ -58,6 +58,7 @@ Claude Desktop (`claude_desktop_config.json`), bridged to a running app:
| `screenshot` | `view?` (iso, top, front, back, bottom, left, right, home, fit), `path?`, `width?`, `height?`, `source?` (window, model) | A PNG image content block. Headless: CPU render fitted to the model. Bridged: the app window (default) or a model render with the app's camera. |
| `export` | `path`, `format?` (step, 3mf, stl, stla, obj), `bodies?` | STEP, 3MF or mesh export. |
| `undo`, `redo` | — | Undo / redo one change. |
| `crash_reports` | `id?`, `limit?` | The crash reports on this computer, newest first; with `id`, the whole report (backtrace, command, recent commands, `design_path`/`replay_path`). See [crash-reports.md](crash-reports.md). |
| `new_design` | `name?` | Start an empty design. |
| `open` | `path` | Open a `.solvecraft` design, a STEP file (`.step`/`.stp`) as a new design holding its bodies (an Import base feature), or a 3MF/STL file as mesh bodies. |
| `save` | `path?` | Save the design (to its current file without `path`). |