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 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
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

+4 -1
View File
@@ -43,7 +43,7 @@ pub fn diagnostics(app: &SolveApp) -> String {
)
}
/// Run a Help menu item: `docs`, `about`, `shortcuts`, `report`.
/// Run a Help menu item: `docs`, `about`, `shortcuts`, `report`, `crashes`.
pub fn run(app: &mut SolveApp, ctx: &egui::Context, item: &str) {
app.help.menu = false;
match item {
@@ -53,6 +53,7 @@ pub fn run(app: &mut SolveApp, ctx: &egui::Context, item: &str) {
app.keymap.open = true;
app.keymap.read_only = true;
}
"crashes" => crate::crash_ui::open(app),
"report" => {
let dialog = app.dialog.clone();
let error = dialog.as_ref().and_then(|d| d.error.clone()).or_else(|| app.preview.error.clone());
@@ -74,6 +75,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
("about", "About SolidCraft"),
("shortcuts", "Keyboard Shortcuts"),
("report", "Report an Issue…"),
("crashes", "Crash Reports…"),
] {
if crate::context_menu::list_row(ui, &crate::context_menu::Item::action(id, label, "")).is_some() {
pick = Some(id);
@@ -92,6 +94,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context, anchor: egui::Pos2) {
ctx.data_mut(|d| d.insert_temp(egui::Id::new("sc_help_opened"), false));
}
about(app, ctx);
crate::crash_ui::show(app, ctx);
}
fn about(app: &mut SolveApp, ctx: &egui::Context) {