Crash Recovery & Safe Mode
How Daintree detects unclean exits, the safe-mode crash-loop guard, the recovery and auto-restore flow, the deadlock watchdog, and terminal service crashes.
Safe Mode
Safe mode activates on its own when Daintree detects a crash loop. There's no manual trigger and no menu entry for it.
What Triggers It
Daintree counts unclean exits in a sliding 30-minute window. The state lives in crash-loop-state.json under user data. Three unclean exits inside that window flip the next launch into safe mode. Five in the same window go a step further: auto-relaunch is disabled entirely, so the app won't try to come back on its own after a crash. You launch it yourself.
The window genuinely slides. Old launches expire lazily on each new launch, so a single crash followed by 30 minutes of clean operation never builds toward safe mode. If crash-loop-state.json itself is unreadable, Daintree moves it aside as crash-loop-state.json.corrupted.{timestamp} (POSIX 0o600 perms, up to three siblings retained), resets the counter, and boots normally.
The Safe Mode Banner
Safe mode pins an inline warning banner to the top of the app:
"Safe mode — panels weren't restored"
Beneath the title is a crash-count meta line in the form "3 crashes detected, last 2m ago". The right side of the banner carries two controls: a Show details popover button and a Restart normally primary action. The X icon at the far right dismisses the banner for the current session only. It comes back on the next boot until you restart normally.
Show Details Popover
Clicking Show details opens a popover listing every panel Daintree quarantined as a likely culprit. Each row shows the panel title and its working directory or worktree ID, with a per-row Restore panel button. The popover also repeats the crash-count meta line and, when relevant, a short note on what's been deferred.
Restoring a panel from the popover is a next-launch operation. The panel is dropped from the quarantine ledger so it comes back on the next boot, but it doesn't re-hydrate in the current session. Each row moves through visible states (idle → clearing → cleared or failed) so you can tell whether the request landed.
If there are no quarantined panels but Daintree still skipped some during boot, because they weren't safe to restore in a crash-loop state, the popover shows a simpler message: a count of skipped panels and a prompt to use Restart normally to reload them.
Restart Normally
Restart normally opens a destructive ConfirmDialog:
- Title: "Restart Daintree normally?"
- Description: "All running terminals and agent sessions will be killed. Scrollback and in-flight agent work will be lost."
- Confirm: Restart normally (destructive variant)
A small View logs text link sits at the bottom of the dialog. It opens the current log file in your system editor so you can check what's been happening before you commit to the restart. The link is in this dialog only. There's no separate View logs action on the banner itself.
Returning to Normal Without Restart
You don't have to use Restart normally to get out of safe mode. The 30-minute window decays on its own. Once the most recent unclean exit ages out, the next clean launch comes up without the banner. If you've been running stably for the rest of the window, relaunching the app eventually does the right thing.
Crash Recovery
Daintree detects unclean exits with a marker file. running.lock is written on launch with a JSON payload (session start time, app version, platform, crash log path, heartbeat timestamp, suspend-start timestamp), refreshed on every backup tick, and removed on a clean shutdown. If the marker is still there at the next start, Daintree knows the previous session ended unexpectedly and runs the recovery flow.
Session backups capture your panel list, working directories, last agent state per panel, and window layout. They write to disk on three triggers: a 60-second periodic timer, window blur (100 ms debounced so dragging the window doesn't cause write storms), and scheduled state changes from the app store (1.5 seconds debounced). The service is sleep/wake aware. onSuspend stops the timer and stamps the marker; onWake clears the suspend and restarts the timer.
Backups land in backups/session-state.json, with the previous generation rotated to backups/session-state.previous.json before each new write. That rolling pair means a corrupt write doesn't destroy your only snapshot. The payload is small: a capturedAt timestamp plus the serialized appState and windowStates.
Crash Classification
When Daintree runs the recovery flow, it classifies the previous crash. Causes are checked in priority order:
- uncaught-exception: Daintree wrote a crash log path before exiting
- native-crash: a Crashpad
.dmpdropped intocrashDumps/after this session started - suspended-then-lost: the marker recorded a suspend that never resumed (e.g. battery died during sleep)
- power-loss: system uptime is shorter than the wall-clock elapsed (system rebooted)
- external-kill: heartbeat is stale but the OS didn't reboot (Activity Monitor force-quit,
kill -9, OOM killer) - unknown: no signal pointed anywhere specific
If watchdog-kill.flag is present and fresh, the classifier overrides the cause to watchdog-deadlock and annotates the crash log with the killed PID, the missed-heartbeat count, and the kill timestamp. See Crash Watchdog for how the deadlock-detection path works.
Recovery Dialog
The recovery dialog opens before the workspace loads. The title is "Daintree closed unexpectedly", with a body line of "The previous session ended unexpectedly on {date}. Select which panels to restore:". When a backup is available, its timestamp shows below the dialog body (e.g. "Session backup from Tuesday at 14:32").
The panel list is a scrollable set of rows. Each row shows the panel kind icon (terminal, plugin, browser, dev preview, git ops), the panel title, the working directory, the last known agent state, and a checkbox. A Select all / Deselect all toggle at the top handles bulk selection, and the header tracks "X of N selected".
Panels created within 30 seconds of the crash are tagged as suspects and marked inline with an AlertTriangle icon. The rule is conservative: a panel that came into existence right before the crash is more likely to have caused it. On the second consecutive crash (when the persisted crash count is at least 1), suspect panels are pre-deselected, and the inline banner above the list reads "{N} panels deselected — created shortly before the crash". On the first crash, suspects are still highlighted but left selected, and the banner reads "{N} panels created shortly before the crash".
Two actions sit at the bottom of the dialog:
- Restore selected (N): restores only the checked panels
- Continue without restoring: discards the panel list and comes up with a clean layout
If the backup has no panel list, the dialog collapses to a card-style two-button layout: Restore previous session (with the backup date, or a fallback warning when the backup is missing) and Continue without restoring.
Continue Without Restoring
Because this action throws work away, v0.12 routes it through a second destructive ConfirmDialog before anything is discarded:
- Title: "Reset to clean layout?"
- Description (with panels): "All open panels listed below will be closed and their state will be discarded."
- Description (no panels, backup present): "Your session will start with a clean layout and the existing session backup will be discarded."
- Confirm: Reset to clean layout (destructive variant)
When you're discarding panels, the confirm dialog shows the panel icon list inline, so you know exactly what's going.
Auto-Restore
Auto-restore is on by default in v0.12. The recovery dialog shows a switch labeled "Restore automatically next time" with the subtitle "Skips this dialog. Shows again if Daintree crashes twice in a row." With it on, a single unclean exit silently restores all panels and shows a confirmation banner instead of the dialog (covered below).
After two consecutive crashes, auto-restore pauses itself so you get a chance to see what's going on. The dialog switches to static text: "Auto-restore paused — too many consecutive crashes." Once the crash counter decays and you're back to clean operation, the switch becomes interactive again.
To turn auto-restore off, or back on outside the dialog, go to Settings > Troubleshooting and toggle Auto-Restore on Crash.
Silent Auto-Restore Banner
When auto-restore handles a crash silently, Daintree confirms it with an inline banner at the top of the workspace. The banner has two forms:
- No suspects: "Session recovered after unexpected exit." Auto-dismisses after 10 seconds.
- With suspects: "Session recovered after unexpected exit — N panels created near the crash may be affected." Stays until you dismiss it.
The sticky variant gives you time to deal with the suspect panels, whether that means closing them, restarting them, or just acknowledging the warning, before the banner goes away. The auto-dismissing variant is a low-noise confirmation that the recovery happened.
Error Details and Crash Reporting
The recovery dialog has a collapsible Error Details section. Expand it to see the diagnostic context Daintree gathered at crash time: app version, platform and architecture, OS version, session duration (formatted as Xm Ys), Electron version, free and total memory, panel count, process uptime, and, when available, the captured error message and stack trace.
Three buttons sit below the details:
- Open log file: opens the crash log in your system text editor
- Copy stack: copies the stack trace to your clipboard
- Report this crash: two-step privacy flow described below
Report this crash uses a two-step disclosure. The first click reveals a privacy warning: the report includes platform info, app version, panel kinds, file paths, error message, and stack trace, and it will be publicly visible on GitHub. The second click copies the formatted markdown report to your clipboard and opens GitHub Issues in your browser, where you paste the report into a new issue body.
Crash Watchdog
Daintree runs an external watchdog in a separate Electron UtilityProcess. It has no UI of its own. Its job is to notice when the main process has frozen and force-kill it, so the recovery flow can take over on the next launch. The threshold is conservative on purpose: a synchronous garbage collection or a long native-module operation can pause the main process for several seconds, and the watchdog has to avoid mistaking that for a deadlock.
The watchdog expects a heartbeat from the main process every 5 seconds. Miss three in a row, around 15 seconds of an unresponsive main process, and the watchdog writes watchdog-kill.flag synchronously and sends SIGKILL to the main PID. On wake from sleep, a monotonic-clock grace window suppresses queued ticks so a burst of catch-up heartbeats doesn't trigger a false-positive kill.
On the next launch, Daintree consumes the flag (consumeWatchdogKillFlag) and annotates the crash classification as watchdog-deadlock with the killed PID, missed-beat count, and kill timestamp. The flag is then unlinked unconditionally, so a stale file from a long-ago kill can't poison later launches.
When the Watchdog Disables Itself
If the watchdog UtilityProcess can't stay running, because the watchdog itself crashes repeatedly or its IPC channel won't come back, Daintree gives up on restarting it and shows a warning banner:
- Title: "Crash watchdog disabled"
- Description: "Deadlock detection stopped after {N} restart attempts. Restart it to resume monitoring."
- Action: Restart watchdog
Restart watchdog dispatches watchdog.restart and re-launches the UtilityProcess. The banner clears once the restart succeeds.
Terminal Service Crashes
Daintree's pty-host runs as a separate Electron UtilityProcess. Every terminal panel sits on top of it: the agent panels (Claude Code, Gemini CLI, Codex, OpenCode) and any plain shells you've opened. When the pty-host exits, terminal I/O stops until the backend comes back. The rest of the app keeps running. The review hub, browser panels, and settings are unaffected.
The pty-host lifecycle handler restarts the process with full-jitter exponential backoff. The floor is 100 ms, the cap is 10 seconds, and the exponent doubles per attempt. Restarts are budgeted against the same 3-in-30-minutes sliding window the main crash-loop guard uses. Past that budget, automatic restarts stop and the banner switches to manual mode.
Backend Status Banner
The banner is driven by usePanelStore.backendStatus, which has three states:
- connected: no banner, normal operation
- recovering: spinner banner with the title "Terminal service restarting" and the description "The terminal backend stopped and is restarting automatically." A Doherty-threshold delay gates the banner so a sub-second blip never flashes UI
- exhausted: error banner with crash-type-specific copy and a red Restart service button (dangerFilled variant)
Crash-Type Variants
Once the restart budget is exhausted, the banner copy depends on how the pty-host exited. Classification uses the authoritative child-process-gone reason from Electron 37 and later, falling back to exit-code heuristics (137 = OOM / SIGKILL, 134 = SIGABRT, and so on).
| Crash Type | Banner Title | Suggested Action |
|---|---|---|
OUT_OF_MEMORY | "Terminal service ran out of memory" | Close unused terminals or agent panels, then click Restart service |
SIGNAL_TERMINATED | "Terminal service was terminated" | Check whether the OS or another tool is killing the pty-host. Click Restart service to bring it back |
ASSERTION_FAILURE | "Terminal service hit an assertion failure" | Click Restart service. If it keeps happening, capture a diagnostics bundle and file a bug |
CLEAN_EXIT | "Terminal service stopped unexpectedly" | Click Restart service. A clean exit usually means a plugin or signal handler triggered the shutdown |
UNKNOWN_CRASH | "Terminal service crashed" | Click Restart service. If the crash repeats, attach a diagnostics bundle to a bug report |
Workspace Host Crashes
The workspace host is a separate Electron UtilityProcess, distinct from the pty-host covered in Terminal Service Crashes. It owns git state, file watching, and worktree operations for an open project. It runs out of the renderer, so a crash there doesn't take down the UI, but it does interrupt worktree updates until the host comes back online.
When the workspace host exits unexpectedly, Daintree surfaces the crash to the sidebar within about 10 milliseconds. A Reconnecting… label with a spinning icon replaces the Worktrees header while Daintree restarts the host in the background. You don't need to do anything in the meantime. Daintree retries up to three times with exponential backoff, roughly 2, 4, and 8 seconds between attempts, and the spinner clears on its own once a fresh snapshot arrives.
If all three retries fail, the spinner clears and the sidebar shows a terminal error: "Workspace host crashed and could not recover. Please restart Daintree." Restart the app from the Dock or Start menu. If the crash repeats on every restart, capture a diagnostics report and file a bug with it attached.
Crash Files and Session Backups
The session backup captures your panel list, working directories, last agent state for each panel, and window layout. That's the data Daintree uses to restore your workspace after a crash.
Crash logs, backups, and flag files live in the Daintree user data directory:
~/Library/Application Support/Daintree
%APPDATA%\Daintree
~/.config/Daintree
The files that matter inside that directory:
running.lock: crash detection marker. Present while Daintree is running. Holds the session start time, app version, platform, crash log path, and a heartbeat timestamp refreshed on every backup tick.crashes/: crash log files in JSON format. Up to 10 retained; older logs are pruned automatically.backups/session-state.json: the most recent session backup.backups/session-state.previous.json: the previous generation, rotated fromsession-state.jsonbefore each new write. The rolling pair keeps a corrupt write from destroying your only snapshot.crash-loop-state.json: counter state for the safe-mode crash-loop guard.watchdog-kill.flag: written briefly after the watchdog SIGKILLs the main process. Consumed on the next launch to attribute the crash as watchdog-deadlock, then unlinked.gpu-disabled.flag: the nuclear hardware-acceleration disable. Persists until you re-enable HW acceleration from Settings.gpu-angle-fallback.flag: the Linux + Wayland soft fallback. Present until you toggle hardware acceleration off and back on, or until the nuclear path takes over and clears it.*.corrupted.{timestamp}: quarantined files. When per-project or global JSON stores (state.json,settings.json,recipes.json,config.json,crash-loop-state.json) fail to parse or carry a future schema version, Daintree moves the file aside with this suffix and resets the original. These are pruned automatically after 30 days. There's no UI for them. You'd only notice if you browse user data yourself.
For how Daintree persists session state during normal operation, see Session Management.