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:
24 files changed
+1998
-9
No files matched your search
@@ -29,6 +29,18 @@ fn tool(s: &mut Server, name: &str, args: Value) -> Value {
|
||||
if c["type"] == "text" { serde_json::from_str(c["text"].as_str().unwrap()).unwrap_or(Value::Null) } else { c.clone() }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crash_reports_are_listed_and_read_without_changing_the_design() {
|
||||
let mut s = server();
|
||||
let list = tool(&mut s, "crash_reports", json!({"limit": 3}));
|
||||
assert!(list["reports"].as_array().is_some_and(|r| r.len() <= 3), "{list}");
|
||||
assert!(list["dir"].is_string(), "{list}");
|
||||
let r = raw_tool(&mut s, "crash_reports", json!({"id": "../../etc"}));
|
||||
assert_eq!(r["result"]["isError"], json!(true), "{r}");
|
||||
let r = raw_tool(&mut s, "crash_reports", json!({"id": 5}));
|
||||
assert!(r.get("error").is_some() || r["result"]["isError"] == json!(true), "{r}");
|
||||
}
|
||||
|
||||
fn volume(s: &mut Server) -> f64 {
|
||||
tool(s, "measure", json!({}))["total"]["volume_mm3"].as_f64().unwrap()
|
||||
}
|
||||
@@ -67,6 +79,7 @@ fn handshake_and_tools_list() {
|
||||
"export",
|
||||
"undo",
|
||||
"redo",
|
||||
"crash_reports",
|
||||
"new_design",
|
||||
"open",
|
||||
"save",
|
||||
|
||||
Reference in new issue
Block a user