diff --git a/src/main/daemon/daemon-launch-paths.ts b/src/main/daemon/daemon-launch-paths.ts index 92601e9e3b4..9b5b2c3dca8 100644 --- a/src/main/daemon/daemon-launch-paths.ts +++ b/src/main/daemon/daemon-launch-paths.ts @@ -46,8 +46,14 @@ export function daemonLogArgs(): string[] { return disabled === '1' || disabled === 'true' ? [] : ['--log-file', getDaemonLogFilePath()] } +/** Named so a caller clamping this probe to a deadline cannot silently decouple from its default. */ +export const DAEMON_SOCKET_PROBE_TIMEOUT_MS = 1_000 + // Why: a socket that accepts a connection proves a daemon survived a previous app session and can be reused. -export function probeDaemonSocket(socketPath: string, timeoutMs = 1000): Promise { +export function probeDaemonSocket( + socketPath: string, + timeoutMs = DAEMON_SOCKET_PROBE_TIMEOUT_MS +): Promise { const { promise, resolve } = Promise.withResolvers() if (process.platform !== 'win32' && !existsSync(socketPath)) { resolve(false) diff --git a/src/main/daemon/daemon-recovery-budget.ts b/src/main/daemon/daemon-recovery-budget.ts index 3f8370002cd..ea6f6ebb76f 100644 --- a/src/main/daemon/daemon-recovery-budget.ts +++ b/src/main/daemon/daemon-recovery-budget.ts @@ -42,6 +42,11 @@ export const DAEMON_RECOVERY_PROBE_MS = 8_000 * timeout, and 5s for the adoption lease and adapter connects — those two run against a daemon * that has just reported ready over IPC, so they get a realistic allowance, not the client's * unbudgeted 4x5s. 60 - 25.5 leaves 34.5s; take 32s and keep the rest as margin. + * + * Outside that accounting by design: the launcher's outer-catch endpoint rescue, which only + * runs once the replacement has already failed. Clamping it to the remainder is what turned a + * recoverable degraded adoption into total daemon loss, so it keeps its own probe default and + * the gate may fail open ahead of it — no worse than not rescuing, and better whenever it wins. */ export const DAEMON_RECOVERY_BUDGET_MS = 32_000 diff --git a/src/main/daemon/daemon-replacement-preflight.ts b/src/main/daemon/daemon-replacement-preflight.ts index dc8d4c7619a..71e7303d174 100644 --- a/src/main/daemon/daemon-replacement-preflight.ts +++ b/src/main/daemon/daemon-replacement-preflight.ts @@ -3,7 +3,11 @@ import type { DaemonReplaceReason } from '../../shared/daemon-lifecycle-telemetr import { isDaemonStaleForCurrentBundle } from './daemon-bundle-staleness' import { DaemonEndpointOwnershipError } from './daemon-endpoint-adoption' import { checkDaemonHealth, getMacDaemonSystemResolverHealth } from './daemon-health' -import { getAliveDaemonSessionCount, probeDaemonSocket as probeSocket } from './daemon-launch-paths' +import { + DAEMON_SOCKET_PROBE_TIMEOUT_MS, + getAliveDaemonSessionCount, + probeDaemonSocket as probeSocket +} from './daemon-launch-paths' import { trackDaemonReplaced } from './daemon-lifecycle-event' import { getDaemonLaunchIdentity } from './daemon-pid-identity' import { cleanupDaemonForProtocol } from './daemon-protocol-cleanup' @@ -12,8 +16,9 @@ import { killStaleDaemon } from './daemon-stale-kill' import { getMacDaemonTccAttributionHealth } from './daemon-tcc-attribution' import { PROTOCOL_VERSION } from './types' -// Why a count on top of the wall clock: DAEMON_RECOVERY_BUDGET_MS ends the grace, but a probe -// that fails instantly (nothing listening) would spin hot for the whole budget without this. +// Why a count on top of the wall clock: DAEMON_RECOVERY_BUDGET_MS ends the grace, but a socket +// that accepts and then resets the hello answers both probes instantly, so without this the loop +// would spin hot for the whole budget. export const WEDGED_DAEMON_GRACE_RETRIES = 11 type PreserveDaemon = (mode?: 'degraded-new-pty-fallback') => Promise @@ -157,7 +162,10 @@ export async function prepareDaemonReplacement( health !== 'rejected' && graceRetry < WEDGED_DAEMON_GRACE_RETRIES && Date.now() < recoveryDeadlineMs && - (await probeSocket(socketPath, Math.max(1, Math.min(1000, recoveryDeadlineMs - Date.now())))) + (await probeSocket( + socketPath, + Math.max(1, Math.min(DAEMON_SOCKET_PROBE_TIMEOUT_MS, recoveryDeadlineMs - Date.now())) + )) ) { liveSessionCount = await getAliveDaemonSessionCount(socketPath, tokenPath, recoveryDeadlineMs) graceRetry++