Skip to content

sdk: a plugin's panic message reaches its crash report - #360

Merged
foxnne merged 1 commit into
mainfrom
sdk/plugin-panic-report
Oct 10, 2026
Merged

foxnne merged 1 commit into
mainfrom
sdk/plugin-panic-report

Conversation

@foxnne

@foxnne foxnne commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Part of plans/RESTART_AND_CRASHES_PLAN.md, step 3 (crash reports). Follows #351.

What changes

A plugin's panic now reaches fizzy's crash report, message included. Before, a plugin dylib panicked through its own copy of std. That printed the message to stderr, which the report never saw, and called abort(). On macOS and Linux, #351's abort handler then wrote crashed in <plugin>: Abort with no message. On Windows, std's abort is RtlExitUserProcess(3), which no handler sees, so no report at all.

Now the report reads:

fizzy crashed in crashy 0.3.1: panic: crashy was asked to panic
…
frames:
  crashy+0x1b6ea3
  crashy+0x1b6fbf
  fizzy+0x1a046f3
  …

The frames start at the plugin's panic site (crashy+0x1b6ea3 is 0x12a2eaea3 in register in std's trace), with no abort/libsystem frames in front.

How:

  • Plugin side: the generated dylib root (sdk/plugin_sdk.zig, and plugins/shared/build/helpers.zig for built-ins) declares pub const panic = sdk.dylib.panic. That is FullPanic over a forwarder, so every safety panic goes through it too.
  • The handoff: the forwarder hands the message and the first trace address to the host through a new EditorAPI.panic.
  • Host side: fizzy raises it again as its own panic (std.debug.panicExtra). Natively that is app: a crash report says whose code crashed #351's crash.panic; on the web it is the web backend's handler, which logs to the console where a side module used to trap silently.
  • Fallback: before the host is installed (sdk.runtime.installedHost(), new; the runtime's host pointer is now optional rather than undefined), the dylib falls back to std's own handler.

SDK impact

  • None
  • Core-only or additive
  • Fingerprint moved: EditorAPI gains panic; recorded 0x65096e9408ddb32f in sdk/src/version.zig, sdk_version untouched. Plugins get the forwarding at the next SDK release, once they rebuild against it. A plugin built against an older SDK keeps the old behavior, and the fingerprint keeps it from loading into a fizzy built from this commit.
  • SDK release

Verified

  • macOS (arm64), sandbox profile, throwaway plugin that panics in register on request:
    • Debug: the report above, with the message and frames from the panic site.
    • ReleaseFast fizzy and plugin: fizzy crashed in crashy 0.3.1: panic: crashy was asked to panic, first frame crashy+0x1d173.
    • A plugin segfault (ReleaseFast) is unchanged: crashed in crashy 0.3.1: Segmentation fault at address 0x10.
  • Gates: zig build, zig build test (87/87 steps, 531/532, 1 skipped), test-integration (46/46 steps, 371/371), check-web, test-sdk-version.
  • Web: plugins/image builds as a wasm side module (-Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall) with the new root. Not run in a browser.
  • Windows: CI cross-compile only. This is the case it newly covers (a plugin's panic reports instead of exiting silently); not run.
  • Linux: CI.

Follow-ups

  • The rest of step 3: the short paste-able report string, a notice at the next launch (which also says when step 4's checkpoint recovered unsaved work, KeptDocuments.recoveredFromCheckpoint()), symbols by build id, and safe mode.

🤖 Generated with Claude Code

A plugin dylib panicked through its own std: the message went to a stderr no
report read, then abort() (on Windows RtlExitUserProcess, which no handler sees).
The generated dylib root now declares `panic = sdk.dylib.panic`, which hands the
message and first trace address to the host through the new EditorAPI.panic;
fizzy raises it again as its own panic, so the crash report (or the web console)
gets the message and frames from the panic site. Fingerprint recorded.

Part of plans/RESTART_AND_CRASHES_PLAN.md, step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@foxnne foxnne added the sdk Moves the plugin boundary's fingerprint; ships at the next SDK release label Oct 10, 2026
@foxnne
foxnne merged commit ba0d5c5 into main Oct 10, 2026
9 of 11 checks passed
@foxnne
foxnne deleted the sdk/plugin-panic-report branch October 10, 2026 22:16
foxnne added a commit that referenced this pull request Oct 11, 2026
## What changes

`sdk_version` goes from 0.2.22 to 0.2.23. Merging tags `sdk-v0.2.23`,
publishes its tarball, and asks the store plugins to repin.

**Why now.** The plugin boundary's fingerprint moved since `sdk-v0.2.22`
(`0x1bf417f6…` → `0x65096e94…`, by #360), so store plugins built against
0.2.22 don't load in a fizzy built from `main`, and fizzyed.it/app is
built from `main`. Also, #355 moved the `plugins` service to version 2,
and the agent plugin's `fizzy_build` asks for version 1 until it repins.

## SDK impact

- [ ] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [x] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

**The `sdk` PRs since `sdk-v0.2.22`:**
- #360: a plugin's panic message reaches its crash report. This moves
the fingerprint.

**Additive, reaching plugins with this release:**
- #355: the `plugins` service is version 2, with `list` (each plugin's
identity, link, version, registration counts and status). A caller built
for version 1 is refused until it rebuilds, which this release's repin
does.
- #351: a crash report says whose code crashed (`sdk/plugin_sdk.zig`).

**Also reaching plugins (`core/`):**
- `core.viz`, graphics for live numbers: `line`, `bar`, `stat`, `Table`
(#345); `Pie`, `keyColor`, `palette` (#347); the table's rounded header
and shaded rows (#359).
- `core.profile`:
- `lookback()`: 30 s of costs and the frames over budget, plus
`ReportOptions.over_ms` / `.hitches` and `selfTimes` (#353).
- `frameCauses()` and `ReportOptions.causes`: why each frame happened
(#364).
- `phase` / `Phases` / `ReportOptions.startup`: startup timing (#365,
which landed inside #364's squash commit).
  - The profiler's `abi` stays 4.

## Verified

- [x] macOS:
  - `zig build` and `test-sdk-version` on this head.
  - `scripts/pack-sdk.sh` packs `fizzy-sdk-v0.2.23.tar.gz`.
- Every store plugin (fresh pulls of pixi, zig, drive, atlas and ghostty
`main`) builds against this tree's `sdk/` with `zig build --fork=<sdk>`
into a throwaway profile (31/31, and 28/28 for each of the other four).
The fingerprint moved, so each repin PR is a real rebuild, but none
should open as a draft for a broken build. The agent and chat plugins
build too (22/22, 19/19).
- [ ] Windows:
- [ ] Linux: CI.
- [ ] Web:

## Follow-ups

- Each repin PR merged and its plugin tagged (the plugin repos' tags
stay the maintainer's).
- `fizzyedit/agent` and `fizzyedit/chat` repin to the `sdk-v0.2.23`
tarball.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
foxnne added a commit that referenced this pull request Oct 11, 2026
## What changes

**On the web, a plugin that fails to load now shows up as failed in the
store's Installed pane, with the reason, as it does on the desktop.**
Before, a refused build was only logged to the browser console. After
#360 moved the SDK fingerprint, fizzyed.it/app refused every store
plugin with `AbiMismatch`. The Installed pane listed each one as an
undecided build with a **Load** button, and pressing Load failed
silently in the same way.

The desktop routes every load failure through `App.recordLoadFailure`,
which feeds the failed card (reason, probed version,
Retry/Reinstall/Uninstall) and the failed branch in Settings. The web's
arrival path (`WebPluginRequest.arrived`) never did. It now records a
failure on each of its three ways out:

- the page could not fetch or link the module (offline, hash mismatch,
not a side module);
- `PluginLoader.prepare` refused it (fingerprint, SDK version, declared
id, missing entry points);
- its `register()` was rejected.

When no build of that id is running (a remembered build coming back, a
first install, a repair), the failure is recorded as a failed plugin.
The card's detail line ("plugin x.y.z, min SDK a.b.c") comes from the
module's own version getters through a new
`PluginLoader_web.versionInfo`, which is the web's counterpart of
`probeVersionInfo`. When a running build stays in front of a refused
update, the failure is only noted for the `plugins` service, as a failed
reload is on the desktop. The update window's row already turns to Retry
in that case.

A web load that succeeds now clears any earlier failure record, and so
does a web uninstall, as both already do on the desktop.

`App.pluginLoadFailureReason` now works with either loader's error set.
The two sets share names where they mean the same thing, so the reasons
live in a table keyed by error name and the switch is `inline else`. An
error that either loader gains without a reason is still a compile
error.

The fingerprint problem itself needs an SDK release (0.2.23) and repins.
That is separate from this PR.

## SDK impact

- [x] None
- [ ] Core-only or additive: reaches plugins at the next SDK release
- [ ] Fingerprint moved: recorded in `sdk/src/version.zig`,
`sdk_version` untouched, PR labelled `sdk`
- [ ] SDK release: bumps `sdk_version`, lists the `sdk` PRs since the
last `sdk-v*` tag

## Verified

- [x] macOS: CI tests.
- [x] Windows: CI tests and the cross-compiled build.
- [x] Linux: CI tests, integration tests and verdict run. The Linux job
also runs `zig build check-web`, so the wasm side compiles with the web
loader's error set.
- [ ] Web: not seen in a browser. I could not build the wasm app in the
cloud session that wrote this, because its network policy blocks SDL's
Linux dependency hosts. Still to check: load a store plugin built
against SDK 0.2.22 on this build. Its card in the Installed tab should
read "built against an incompatible Fizzy SDK" with
Retry/Reinstall/Uninstall.

## Follow-ups

- A refused build that the page never remembered (a first install this
host refuses) has no URL to retry, so its card's Retry reports
`NotUnloadable`. Reinstall from the store is the way back, as it is for
any failed card with a store build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_012tKz7NTwCwAPSmfg3DjHDX

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sdk Moves the plugin boundary's fingerprint; ships at the next SDK release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant