Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/ai/changelog/2026-10-14-simplex-desktop-crash-recovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Simplex Desktop crash recovery and local diagnostics

The dashboard reloads automatically after its first renderer crash. Later crashes offer
**Reload dashboard** or **Later**; a crash during a requested reload offers a fresh prompt
once that load finishes. **Show Simplex** also retries a crashed dashboard. Recovery waits
for Chromium teardown, stops during app exit or update installation, and leaves the
separate solver process running.

A rejected initial page load waits up to one second for a recoverable renderer-exit event.
This handles either event order; without that event, the original load error remains fatal.

OS secure-storage availability checks share one native request and return unavailable after
five seconds. While storage is unavailable, the locked login screen checks again every two
seconds, with one refresh in flight at a time. Refreshes pause during authentication and stop
on unlock or unmount. A completed native check can restore passkey access without restarting
the app. Password-protected config remains encrypted, and passkeys and background resume
require available OS storage.

Desktop health, status and control requests reject interrupted responses, enforce an absolute
deadline, and cap responses at 64 KiB. Renderer API streams retain their streaming behavior.
Unexpected solver disappearance triggers the native alert on every OS, including removal of
a Windows named pipe. Accepted desktop stop actions and an observed stopping state suppress
that alert. An external stop completed between probes can still appear unexpected.

Local diagnostics live under `<userData>/diagnostics`, accessible through **Open Data Directory**.
Desktop and solver diagnostics each retain five files of up to 1 MiB, with details limited to
16,384 characters and files created mode `0600` on Unix. The main process and its solver child
handles share one desktop logger, so solver exits do not create extra one-line files.
The shared secret scrubber runs before each write. Local logging works with remote reporting
disabled and does not upload files or memory dumps.

The solver loads `solver-diagnostics.js` in development or `desktop/solver-diagnostics.mjs`
in packaged builds through Node's `--import`. It records handled console errors and observes
fatal exceptions and unhandled rejections without suppressing process failure. Writes are
synchronous; logging failures do not stop filling. Solver stdio stays detached from Electron.
Native aborts and OS kills require OS crash records.

Renderer-exit notifications use the local log. Existing Sentry child-process reporting handles
remote crash events; recovery errors still use desktop error reporting. Build inputs, opt-out
and scrubbing are documented in [Sentry error reporting](2026-10-14-simplex-sentry-error-reporting.md).
29 changes: 22 additions & 7 deletions sdk/packages/simplex-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ a restored older backup also still accepts the passkey it recorded, so replaceme

Passkeys require secure OS storage. Mac native passkeys also require Apple signing, a provisioning
profile, and the configured associated domain; Windows requires browser platform WebAuthn support.
Storage availability checks return unavailable after five seconds, so a stalled OS check does not
indefinitely hide password login. Password-protected configuration remains encrypted; OS-backed
passkeys and background resume require a completed availability check. The locked login screen
refreshes unavailable storage every two seconds, pausing during authentication, so a slow successful
check can restore passkey access without restarting the app.
If the device prompt is unavailable or cancelled, retry or choose another sign-in method. A request expires
after two minutes and can also be cancelled from Simplex. Linux passkey integration is not
implemented.
Expand Down Expand Up @@ -104,12 +109,17 @@ The tray and application menus deliberately provide two separate exit commands:

The app polls the private socket every three seconds. Its tray icon, native menu, tooltip, and window
title distinguish setup, running, paused, stopping, stopped, and unreachable states. It requires two
consecutive failed probes before declaring a running solver unreachable. A transport failure raises
an operating-system notification; clean socket removal is shown as stopped without calling it a
crash, because it may be a deliberate dashboard stop. Restart is operator-controlled and remains
consecutive failed probes before declaring a running solver unreachable. Interrupted responses fail
within a bounded deadline so later polls continue. Unexpected disappearance raises an operating-system
notification, including Windows named-pipe removal. Accepted desktop dashboard and native-menu stop
requests, and an observed stopping state, suppress that notification. Restart is operator-controlled and remains
disabled while graceful shutdown drains in-flight work, so the shell cannot start a second filler on
the same signer.

If the dashboard renderer crashes, Electron reloads it once without restarting the solver. Another
renderer crash in that window offers **Reload dashboard** or **Later**. **Show Simplex** also retries
a crashed dashboard. Recovery is disabled while quitting or installing an update.

While the solver is actively filling and Electron remains open, its `powerSaveBlocker` prevents app
suspension. The native menu says whether sleep prevention is on. Pausing or stopping the solver
releases it. This protects an active session from ordinary system sleep, but it cannot survive logout
Expand Down Expand Up @@ -140,9 +150,13 @@ stable/beta channel selector while preserving the platform Edit and Window roles
shortcuts. Update controls are available only in an installed build; development launches never
contact the release feed.

Simplex does not upload crash reports. Solver diagnostics remain in rotating NDJSON launch logs under
`<userData>/logs`; five launches are retained, and **Open Current Log** opens the newest one. This
avoids a remote crash-reporting path that could accidentally capture config contents or key material.
Solver activity remains in NDJSON launch logs under `<userData>/logs`; five launches are retained,
and **Open Current Log** opens the newest one. Scrubbed desktop errors and solver fatal errors are
written separately under `<userData>/diagnostics`, accessible through **Open Data Directory**.
Each source retains five files of up to 1 MiB. Local diagnostics work without a Sentry DSN and with
`SIMPLEX_ERROR_REPORTING=off`. Builds with a DSN may also send scrubbed error events as described in
[Sentry error reporting](../../../docs/ai/changelog/2026-10-14-simplex-sentry-error-reporting.md).
No native memory dumps are collected.

Desktop removal leaves Electron's user-data directory in place. Neither this shell nor its installer
contract deletes operator databases, logs, or configuration.
Expand Down Expand Up @@ -228,12 +242,13 @@ The staging script follows Node's binary-verification procedure: it verifies the
and installs only `node` or `node.exe`. `darwin-universal` verifies both macOS slices independently
before merging them with `lipo`.

The desktop process requires the three solver resources plus the status variants derived from the
The desktop process requires the runtime resources plus the status variants derived from the
PWA logo, and fails before creating a window if any is missing:

- `resources/node/<platform>-<arch>/node` in development, or `node.exe` on Windows;
- `@hyperbridge/simplex/dist/bin/simplex.js`;
- `@hyperbridge/simplex/dist/ui/index.html`.
- `dist/solver-diagnostics.js` in development, or `desktop/solver-diagnostics.mjs` in packaged resources.
- `resources/tray/<state>.png` in development, with 18px macOS `Template` and 36px `Template@2x`
variants; packaged builds place them under `desktop/tray` in Electron resources.

Expand Down
4 changes: 4 additions & 0 deletions sdk/packages/simplex-desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ files:
- package.json

extraResources:
- from: dist/solver-diagnostics.js
to: desktop/solver-diagnostics.mjs
- from: dist/solver-diagnostics.js.map
to: desktop/solver-diagnostics.js.map
- from: build/package/simplex
to: simplex
- from: build/package/runtime-deps/node_modules
Expand Down
2 changes: 1 addition & 1 deletion sdk/packages/simplex-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"stage:solver": "node scripts/stage-solver.mjs",
"test": "pnpm run test:unit && pnpm run test:integration",
"test:e2e": "pnpm run ensure:electron && node scripts/e2e/assert-artifacts.mjs && tsx --tsconfig ../simplex/tsconfig.json --test --test-timeout=300000 scripts/e2e/desktop.e2e.mjs",
"test:unit": "vitest --watch=false src/tests/passkeys.test.ts src/tests/mac-native-passkeys.test.ts src/tests/device-key-store.test.ts src/tests/vault.test.ts src/tests/blackhole-server.test.ts src/tests/bootstrap.test.ts src/tests/desktop-paths.test.ts src/tests/daemon.test.ts src/tests/desktop-security.test.ts src/tests/electron-launch.test.ts src/tests/login-item.test.ts src/tests/notification-client.test.ts src/tests/release-provider.test.ts src/tests/session-marker.test.ts src/tests/solver-supervisor.test.ts src/tests/tray-icon.test.ts src/tests/tray-menu.test.ts src/tests/update-authenticity.test.ts src/tests/update-coordinator.test.ts src/tests/update-store.test.ts scripts/ensure-electron.test.ts scripts/package-release.test.ts scripts/release-signing.test.ts scripts/mac-passkey-signing.test.ts scripts/stage-node.test.ts scripts/stage-solver.test.ts scripts/workspace-policy.test.ts",
"test:unit": "vitest --watch=false src/tests/passkeys.test.ts src/tests/mac-native-passkeys.test.ts src/tests/device-key-store.test.ts src/tests/vault.test.ts src/tests/blackhole-server.test.ts src/tests/bootstrap.test.ts src/tests/diagnostic-log.test.ts src/tests/renderer-recovery.test.ts src/tests/error-reporting.test.ts src/tests/socket-request.test.ts src/tests/desktop-paths.test.ts src/tests/daemon.test.ts src/tests/desktop-security.test.ts src/tests/electron-launch.test.ts src/tests/login-item.test.ts src/tests/notification-client.test.ts src/tests/release-provider.test.ts src/tests/session-marker.test.ts src/tests/solver-supervisor.test.ts src/tests/tray-icon.test.ts src/tests/tray-menu.test.ts src/tests/update-authenticity.test.ts src/tests/update-coordinator.test.ts src/tests/update-store.test.ts scripts/ensure-electron.test.ts scripts/package-release.test.ts scripts/release-signing.test.ts scripts/mac-passkey-signing.test.ts scripts/stage-node.test.ts scripts/stage-solver.test.ts scripts/workspace-policy.test.ts",
"test:integration": "vitest --watch=false src/tests/protocol.test.ts",
"lint": "biome lint src scripts",
"format": "prettier --write \"{src,scripts}/**/*.{ts,mjs}\"",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const requiredArtifacts = [
resolve(desktopRoot, "dist/main.js"),
resolve(desktopRoot, "dist/main.js.map"),
resolve(desktopRoot, "dist/bootstrap.js"),
resolve(desktopRoot, "dist/solver-diagnostics.js"),
resolve(desktopRoot, "THIRD-PARTY-NOTICES.md"),
resolve(desktopRoot, "resources/icon.icns"),
resolve(desktopRoot, "resources/icon.ico"),
Expand Down
Loading
Loading