import logManager from '@electron/service/logger'; import { LifecycleSupersededError } from './lifecycle-controller'; import { getGatewayStartupRecoveryAction } from './startup-recovery'; export interface ExistingGatewayInfo { port: number; externalToken?: string; } type StartupHooks = { port: number; shouldWaitForPortFree: boolean; maxStartAttempts?: number; hasOwnedProcess?: () => boolean; assertLifecycle?: (phase: string) => void; resetStartupStderrLines: () => void; getStartupStderrLines: () => string[]; findExistingGateway: (port: number) => Promise; connect: (port: number, externalToken?: string) => Promise; onConnectedToExistingGateway: () => void; waitForPortFree: (port: number) => Promise; startProcess: () => Promise; waitForReady: (port: number) => Promise; onConnectedToManagedGateway: () => void; runDoctorRepair?: () => Promise; onDoctorRepairSuccess?: () => void; delay: (ms: number) => Promise; }; export async function runGatewayStartupSequence(hooks: StartupHooks): Promise { let configRepairAttempted = false; let startAttempts = 0; const maxStartAttempts = hooks.maxStartAttempts ?? 3; while (true) { startAttempts += 1; hooks.assertLifecycle?.('start'); hooks.resetStartupStderrLines(); try { const existing = await hooks.findExistingGateway(hooks.port); hooks.assertLifecycle?.('start/find-existing'); if (existing) { logManager.debug(`Found existing Gateway on port ${existing.port}`); await hooks.connect(existing.port, existing.externalToken); hooks.assertLifecycle?.('start/connect-existing'); hooks.onConnectedToExistingGateway(); return; } if (hooks.hasOwnedProcess?.()) { logManager.info('Owned Gateway process still alive; waiting for it to become ready'); await hooks.waitForReady(hooks.port); hooks.assertLifecycle?.('start/wait-ready-owned'); await hooks.connect(hooks.port); hooks.assertLifecycle?.('start/connect-owned'); hooks.onConnectedToExistingGateway(); return; } if (hooks.shouldWaitForPortFree) { await hooks.waitForPortFree(hooks.port); hooks.assertLifecycle?.('start/wait-port'); } await hooks.startProcess(); hooks.assertLifecycle?.('start/start-process'); await hooks.waitForReady(hooks.port); hooks.assertLifecycle?.('start/wait-ready'); await hooks.connect(hooks.port); hooks.assertLifecycle?.('start/connect'); hooks.onConnectedToManagedGateway(); return; } catch (error) { if (error instanceof LifecycleSupersededError) { throw error; } const recoveryAction = getGatewayStartupRecoveryAction({ startupError: error, startupStderrLines: hooks.getStartupStderrLines(), configRepairAttempted, attempt: startAttempts, maxAttempts: maxStartAttempts, }); if (recoveryAction === 'repair' && hooks.runDoctorRepair) { configRepairAttempted = true; logManager.warn( 'Detected invalid OpenClaw config during Gateway startup; running doctor repair before retry', ); const repaired = await hooks.runDoctorRepair(); if (repaired) { logManager.info('OpenClaw doctor repair completed; retrying Gateway startup'); hooks.onDoctorRepairSuccess?.(); continue; } logManager.error('OpenClaw doctor repair failed; not retrying Gateway startup'); } if (recoveryAction === 'retry') { logManager.warn(`Transient start error: ${String(error)}. Retrying... (${startAttempts}/${maxStartAttempts})`); await hooks.delay(1000); continue; } throw error; } } }