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:
24 files changed
+1998
-9
No files matched your search
@@ -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 |
|
||||
|
||||
@@ -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.
|
||||
@@ -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`). |
|
||||
|
||||
Reference in new issue
Block a user