diff --git a/src/main/crash-reporting/renderer-recovery-crash-outcome.ts b/src/main/crash-reporting/renderer-recovery-crash-outcome.ts index 20d47b3d330..4fb52b3c151 100644 --- a/src/main/crash-reporting/renderer-recovery-crash-outcome.ts +++ b/src/main/crash-reporting/renderer-recovery-crash-outcome.ts @@ -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() } diff --git a/src/main/index.ts b/src/main/index.ts index 857211b0111..d3a88ce2e8b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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, diff --git a/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts b/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts index b3454423ecc..a80db0398aa 100644 --- a/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts +++ b/src/main/ipc/crash-reporting-recovered-renderer-prompt.test.ts @@ -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)