From ff802df65062b2efe86abd346760f66e13ee6eab Mon Sep 17 00:00:00 2001 From: JoaoMarcos44 Date: Mon, 21 Sep 2026 22:58:41 -0300 Subject: [PATCH] fix(desktop): retry failed supervisor respawns (cherry picked from commit c60e707f6c82418a94cf7d5be1fa7562e3052c6a) --- apps/desktop/electron/main.ts | 65 ++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index 3ba9eede68..b92338a471 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -13135,11 +13135,11 @@ function releaseHostSpawnReservation() { hostSpawnReservation = null } -function startHermes() { +function startHermes({ supervisorRecovery = false }: { supervisorRecovery?: boolean } = {}) { primaryRecoverySuppressed = false primaryStartsInFlight += 1 - const start = localBackendLifecycle.start(runHermesStart) + const start = localBackendLifecycle.start(() => runHermesStart({ supervisorRecovery })) const releaseStart = () => { primaryStartsInFlight -= 1 @@ -13156,6 +13156,43 @@ function startHermes() { // "log and return", and recovery then hinged on the renderer noticing its // socket drop — a 9 h engine-less window when it did not. Pool children are // deliberately not consulted: they never own the window backend. +function primaryRecoveryState() { + return { + hasCurrentOwner: backendConnectionState.getProcess() !== null || backendConnectionState.getPromise() !== null, + hasPendingStart: primaryStartsInFlight > 0, + intentionalTeardown: primaryRecoverySuppressed || isQuittingForHandoff || backendShutdown.hasStarted() + } +} + +function reportPrimaryRecoveryCrashLoop(code: number | null, signal: string | null): boolean { + if (!primaryExitRecovery.isCrashLooping()) { + return false + } + + const message = + 'Hermes backend keeps crashing right after it restarts; not restarting it again. Relaunch Hermes Desktop.' + + rememberLog(`[supervisor] ${message}`) + sendBackendExit({ code, signal, error: message }) + + return true +} + +function runPrimaryRecoverySpawn(code: number | null, signal: string | null) { + startHermes({ supervisorRecovery: true }).catch(respawnError => { + rememberLog(`[supervisor] backend respawn failed: ${respawnError.message}`) + + if (primaryExitRecovery.retryAfterFailedStart(primaryRecoveryState())) { + rememberLog('[supervisor] backend respawn failed before ready; retrying within crash-loop budget') + runPrimaryRecoverySpawn(code, signal) + + return + } + + reportPrimaryRecoveryCrashLoop(code, signal) + }) +} + function scheduleUnexpectedPrimaryRecovery({ code = null, signal = null, @@ -13166,34 +13203,20 @@ function scheduleUnexpectedPrimaryRecovery({ return false } - const claimed = primaryExitRecovery.claim({ - hasCurrentOwner: backendConnectionState.getProcess() !== null || backendConnectionState.getPromise() !== null, - hasPendingStart: primaryStartsInFlight > 0, - intentionalTeardown: primaryRecoverySuppressed || isQuittingForHandoff || backendShutdown.hasStarted() - }) + const claimed = primaryExitRecovery.claim(primaryRecoveryState()) if (!claimed) { - if (primaryExitRecovery.isCrashLooping()) { - const message = - 'Hermes backend keeps crashing right after it restarts; not restarting it again. Relaunch Hermes Desktop.' - - rememberLog(`[supervisor] ${message}`) - sendBackendExit({ code, signal, error: message }) - - return true - } - - return false + return reportPrimaryRecoveryCrashLoop(code, signal) } rememberLog('[supervisor] backend exit left no primary owner and no start in flight; respawning') sendBackendExit({ code, signal, ...(error ? { error } : {}) }) - startHermes().catch(respawnError => rememberLog(`[supervisor] backend respawn failed: ${respawnError.message}`)) + runPrimaryRecoverySpawn(code, signal) return true } -async function runHermesStart() { +async function runHermesStart({ supervisorRecovery = false }: { supervisorRecovery?: boolean } = {}) { // Only the single-instance lock holder may reap/spawn/claim the desktop // backend. A lock-losing instance must stay inert even if some path reaches // here (e.g. the deferred-quit window before `ready`): its reapOrphans() @@ -13671,7 +13694,7 @@ async function runHermesStart() { // child 'exit' handler to clear the cache — latching it would wedge the app // on "session expired" until a full restart, defeating reconnect, the // "Sign out & sign in" reload, and the wake-recovery revalidate path. - if (shouldLatchBackendStartFailure({ attemptedRemote })) { + if (!supervisorRecovery && shouldLatchBackendStartFailure({ attemptedRemote })) { backendStartFailure = error instanceof Error ? error : new Error(message) }