fix(crash-reporting): disarm the recovery flag when auto-recovery gives up

The arm was set in one place and cancelled in none. When the renderer
recovery circuit breaker opens, the arm from the last allowed attempt
stays live for 30s, so the user's own reload from the "Orca keeps
failing to load" dialog consumes it — and the whole crash-on-load loop
is written dismissed with rendererAutoRecovered: true.

That flag is a direct lie there: the same code path had just recorded
renderer_recovery_circuit_breaker_open. It silences the highest-signal
crash class we have, repeated launch-failed before bootstrap.

Clearing on exhaustion leaves the manual retry unarmed, so the crash
prompts on its own evidence at the next launch.
This commit is contained in:
Neil
2026-08-01 05:21:07 -07:00
parent d85576e50b
commit 50e1b50b09
3 changed files with 35 additions and 2 deletions
@@ -13,6 +13,16 @@ export function noteRendererRecoveryReloadIssued(nowMs = Date.now()): void {
recoveryReloadIssuedAtMs = nowMs
}
/**
* Disarm the outcome check because auto-recovery gave up.
*
* Why: the next bootstrap then comes from the user's own retry, and a crash the
* breaker refused to keep reloading is the one they most need prompted about.
*/
export function clearRendererRecoveryReloadIssued(): void {
recoveryReloadIssuedAtMs = null
}
function takeRendererRecoveryReloadIssuedAt(nowMs: number): number | null {
const issuedAtMs = recoveryReloadIssuedAtMs
recoveryReloadIssuedAtMs = null
@@ -55,5 +65,5 @@ export function resolveRecoveredRendererCrashReports(
}
export function _resetRendererRecoveryOutcomeForTests(): void {
recoveryReloadIssuedAtMs = null
clearRendererRecoveryReloadIssued()
}
+7 -1
View File
@@ -281,7 +281,10 @@ import {
type ExpectedTeardownScope
} from './crash-reporting/process-gone-classification'
import { recordProcessGoneCrash as recordProcessGoneCrashEvent } from './crash-reporting/process-gone-recorder'
import { noteRendererRecoveryReloadIssued } from './crash-reporting/renderer-recovery-crash-outcome'
import {
clearRendererRecoveryReloadIssued,
noteRendererRecoveryReloadIssued
} from './crash-reporting/renderer-recovery-crash-outcome'
import {
advanceSyntheticTitleSpinnerEntries,
type SyntheticTitleSpinnerEntry
@@ -1228,6 +1231,9 @@ function openMainWindow(): BrowserWindow {
expectedTeardown: getExpectedTeardownScope(webContentsId)
}),
onRendererRecoveryExhausted: ({ details, recentRecoveryCount }) => {
// Why: the pending arm belongs to a reload that never brought the renderer
// back, so the user's retry below must not resolve these as auto-recovered.
clearRendererRecoveryReloadIssued()
recordDurableCrashBreadcrumb('renderer_recovery_circuit_breaker_open', {
reason: details.reason,
exitCode: details.exitCode ?? null,
@@ -56,6 +56,7 @@ vi.mock('../observability/tracer', () => ({
import { CrashReportStore } from '../crash-reporting/crash-report-store'
import {
_resetRendererRecoveryOutcomeForTests,
clearRendererRecoveryReloadIssued,
noteRendererRecoveryReloadIssued,
resolveRecoveredRendererCrashReports
} from '../crash-reporting/renderer-recovery-crash-outcome'
@@ -259,6 +260,22 @@ describe('auto-recovered renderer crash reporting', () => {
await expect(getLatestPending()).resolves.toMatchObject({ id: recorded.id, status: 'pending' })
})
it('still prompts after the recovery breaker opened and the user reloaded by hand', async () => {
const store = await createStore()
registerCrashReportingHandlers(store)
// The crash-on-load loop: every recovery reload dies before bootstrap.
const recorded = await store.record({ ...killedRendererCrash(), reason: 'launch-failed' })
noteRendererRecoveryReloadIssued()
// The breaker refuses the next attempt and Orca asks the user to retry.
clearRendererRecoveryReloadIssued()
// The user's own reload finally boots; it did not auto-recover anything.
emitRendererBootstrapRendered()
await expect(getLatestPending()).resolves.toMatchObject({ id: recorded.id, status: 'pending' })
expect((await getLatestReport())?.details.rendererAutoRecovered).toBeUndefined()
})
it('leaves an unresolved crash from a previous session pending', async () => {
const store = await createStore()
registerCrashReportingHandlers(store)