Skip to content

feat(desktop): preserve bounded main-process recovery evidence - #3543

Merged
M4n5ter merged 3 commits into
mainfrom
feat/desktop-main-process-recovery
Aug 23, 2026
Merged

M4n5ter merged 3 commits into
mainfrom
feat/desktop-main-process-recovery

Conversation

@M4n5ter

@M4n5ter M4n5ter commented Aug 23, 2026

Copy link
Copy Markdown
Member

Summary

English
  • Preserve one local, redacted main-process log tail so the next launch can offer Copy Diagnostics before the Renderer or Runtime Host starts.
  • Treat a missing clean-shutdown marker only as an interrupted prior run. An uncaught JavaScript failure gets a best-effort immediate snapshot; V8 OOM, native crashes, OS kills, and power loss remain indistinguishable and use the latest periodic snapshot on macOS, Windows, and Linux.
  • Keep the persistence contract bounded: first dirty snapshot after 2 seconds, at most one dirty rewrite per 5 minutes, 256 KiB of newest logs plus 16 KiB metadata allowance, one pending run, and seven-day expiry from when an interrupted run is discovered. This caps the continuous worst case at about 72 MiB of logical writes per day.
  • Redact secrets in the existing log buffer and collapse home paths before persistence. Store the journal under an owner-only directory/file boundary, atomically replace it, reject symlink/non-file reads, discard corrupt or incompatible records, and delete pending evidence after the recovery prompt finishes.
  • Mark normal application quits and Windows session-end as clean. The recovery dialog never uploads data and continues startup even if journal or clipboard handling fails.
简体中文
  • 在本地保留一份经过脱敏的 main process 最新日志尾部,让下次启动能在 Renderer 和 Runtime Host 启动前提供复制诊断信息
  • 缺少 clean-shutdown 标记只表示上一次运行未完成正常退出。JavaScript 未捕获异常会尽力立即刷新快照;V8 OOM、原生崩溃、OS kill 与断电仍不可区分,并在 macOS、Windows 和 Linux 上共同使用最近一次周期快照。
  • 持久化契约保持有界:首次 dirty 后 2 秒写入、持续产生日志时最多每 5 分钟重写一次、最新日志上限 256 KiB、元数据预留 16 KiB、仅保留一次 pending run,并从发现上一次运行中断时起保留七天。持续最坏情况下的逻辑写入量约为每天 72 MiB。
  • 复用现有日志缓冲区的敏感信息脱敏,并在持久化前折叠 home path。journal 使用仅 owner 可访问的目录和文件、原子替换、拒绝 symlink/非普通文件、丢弃损坏或不兼容记录,并在恢复弹窗结束后删除 pending evidence。
  • 正常应用退出和 Windows session-end 会标记为 clean。恢复弹窗不会上传数据,journal 或剪贴板失败也不会阻塞启动。

Fixes #3491

Verification

  • npm run lint
  • npm run format:check
  • npm run build
  • npm run typecheck
  • Desktop main test suite: 1,195 passed
  • npx knip --workspace apps/desktop
  • Not run manually: physical Windows shutdown/session-end flow

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex assisted with design analysis, implementation, tests, and review. The human contributor remains responsible for the contribution.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

Persist a redacted latest-log snapshot at a bounded cadence so the next launch can offer copyable diagnostics when the prior main process did not complete a clean shutdown. Keep the signal intentionally cause-agnostic, local-only, and disposable after the first recovery prompt.

Generated-by: Codex
Treat the active journal file as the sole unclean-run marker, removing it synchronously on clean shutdown. Keep pending evidence when the native recovery dialog cannot be presented so a later launch can retry.

Generated-by: Codex
@M4n5ter
M4n5ter force-pushed the feat/desktop-main-process-recovery branch from d07b8d6 to 6d6d30d Compare August 23, 2026 03:20
Start the seven-day retention window when an interrupted run becomes pending, using one metadata update instead of a periodic heartbeat. Keep corrupt high-cardinality records bounded and centralize the present-before-discard policy so failed native prompts remain retryable.

Generated-by: Codex
@M4n5ter
M4n5ter marked this pull request as ready for review August 23, 2026 03:59

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving ff576a384d16056efd22ac92565f1fd12c8aa5fe. Both required checks are completed / success bound to that exact SHA. No P0–P3.

An independent line reviewed this with one question up front — "the PR title says bounded, so where is the bound, who enforces it, and what happens at the edge" — and produced the enforcement table before looking for defects. I then re-derived the load-bearing pieces myself at the gate rather than taking them on report. All four held:

  • boundedLogTail keeps the newest, not the oldest. It walks logs from the last index backwards, accumulating until the next entry would cross MAIN_PROCESS_RECOVERY_LOG_MAX_BYTES, then reverses. This is the one that had to be right — the last few lines before a crash are the whole point of the artifact, and a naive slice(0, n) would have thrown away exactly the evidence worth keeping. The test pinning entry 399 present and entry 0 absent is the right assertion.
  • The write path is genuinely atomic and hostile-input aware: writeFileSync with flag: 'wx' and mode: 0o600 to a temp path, then renameSync, with the temp removed in finally. Reads go through lstatSync rejecting symlinks and O_NOFOLLOW, so a symlink planted at the evidence path is refused twice rather than followed.
  • RUN_MAX_BYTES is headroom, not an unenforced limit. It only contributes to EVIDENCE_MAX_BYTES, and I checked why that is safe: every field on recoveryRunSchema is a boundedStringSchema capped at 1 KiB, so the run record cannot approach 16 KiB by construction. Worth knowing the reason, since "a constant that is never checked directly" usually is a finding.
  • The 7-day clock starts at discovery, not at the crashutimesSync(activePath, promotedAt, promotedAt) immediately before the rename to pending. That matches what the PR says, and it is the right choice: a crash that happened while the machine was off should not expire before anyone has seen it.

On the single pending slot: a newer crash overwrites an unviewed pending record. That is a deliberate design choice, not an oversight, and it is the correct one here — the alternative is unbounded growth in exactly the situation where the app is already failing repeatedly. Worth keeping in mind if crash-loop diagnosis ever becomes a goal, since only the most recent loop iteration survives.

On scope: this is one journal, not a second crash-collection system, and the -30 is mostly app.exit(0)app.quit() so that a normal quit can mark itself clean. A failed write disables the journal for the remainder of the process rather than retrying into a broken disk, and appendUncaughtMainProcessError swallows its own errors so the evidence path cannot become the thing that kills the process. Diagnostic code that can take down the process it is diagnosing is the classic failure of this feature, and it is avoided here.

Seven distinct failure hypotheses were raised and each withdrawn against specific evidence — wrong-end truncation, write failure as a new crash source, torn JSON or symlink substitution, double-recording and replay, the age bound interacting with the slot, session-end listener target, and post-failure disabling. No P3s were manufactured to look thorough, which I appreciate more than a padded list.

Coverage disclosure: one independent review line plus my own verification at the gate. For a change of this size I would normally want a second independent line; I am approving on one because the line's claims were reproducible and I checked the load-bearing ones directly. Stated so the record is accurate rather than flattering.

Reviewed at 2026-08-23 12:50 UTC.

@M4n5ter
M4n5ter merged commit 1d06330 into main Aug 23, 2026
2 checks passed
@M4n5ter
M4n5ter deleted the feat/desktop-main-process-recovery branch August 23, 2026 04:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

discussion(desktop): define bounded next-launch evidence for hard main-process crashes

2 participants