diff --git a/src/main/index.ts b/src/main/index.ts index b76964ae8..5196b7eac 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -229,6 +229,10 @@ const recoveryReloadInFlight = createWebContentsTimedFlag() let firstWindowStartupServicesReady: Promise = Promise.resolve() let managedWslCliReconciliationReady: Promise = Promise.resolve() let managedWslCliStartupBarrierReady: Promise = Promise.resolve() +// Why: the serve barrier fails open at its budget, so headless clients need the +// reconciliation state at ready time to know a WSL PTY launch may still race an +// un-migrated registration. 'settled' covers the off-Windows no-op fast path. +let managedWslCliReconciliationStatus: 'pending' | 'settled' | 'failed' = 'settled' // Why: GPU child crashes clustered right after launch indicate a broken driver; // track them so Orca can move this build onto software rendering. const gpuLaunchTimeMs = Date.now() @@ -1384,6 +1388,9 @@ async function printServeReady(options: ServeOptions): Promise { type: 'orca_server_ready', runtimeId: runtime.getRuntimeId(), endpoint, + // Why: the WSL reconciliation barrier fails open, so 'pending' warns clients + // it outlived the startup budget and a WSL PTY launch may still race a repair. + managedWslCliReconciliation: managedWslCliReconciliationStatus, pairing: pairing.available ? { url: pairing.pairingUrl, @@ -1610,6 +1617,7 @@ app.whenReady().then(async () => { // Why: managed WSL launchers live outside the Windows app bundle, so keep // their launcher and bridge contract synchronized across app updates. + managedWslCliReconciliationStatus = 'pending' managedWslCliReconciliationReady = reconcileManagedWslCliRegistrations({ isPackaged: app.isPackaged, userDataPath: getCanonicalUserDataPath(), @@ -1625,8 +1633,10 @@ app.whenReady().then(async () => { console.log(`[wsl-cli] Repaired managed registration in ${result.distro}.`) } } + managedWslCliReconciliationStatus = 'settled' }) .catch((error) => { + managedWslCliReconciliationStatus = 'failed' console.warn( '[wsl-cli] Managed registration reconciliation discovery failed:', error instanceof Error ? error.message : String(error) @@ -2090,9 +2100,13 @@ app.whenReady().then(async () => { } if (serveOptions) { - // Why: headless serve has no renderer startup barrier, so settle managed - // WSL command reconciliation before exposing its runtime transport. - await managedWslCliReconciliationReady + // Why: give managed WSL launchers a brief chance to migrate before headless + // PTYs become reachable without letting slow repairs withhold all RPC readiness. + logStartupMilestone('wsl-cli-barrier-start') + await managedWslCliStartupBarrierReady + logStartupMilestone('wsl-cli-barrier-resolved', { + reconciliation: managedWslCliReconciliationStatus + }) await startServeAgentHookServer() registerHeadlessPtyRuntime( runtime, diff --git a/src/main/startup/desktop-startup-ordering.test.ts b/src/main/startup/desktop-startup-ordering.test.ts index d99177285..b9adfc6b8 100644 --- a/src/main/startup/desktop-startup-ordering.test.ts +++ b/src/main/startup/desktop-startup-ordering.test.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'node:fs' import { join } from 'node:path' import { describe, expect, it } from 'vitest' -describe('desktop startup ordering', () => { +describe('startup ordering', () => { it('passes the startup barrier into PTY handlers without blocking window creation', () => { const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8') const attachStart = source.indexOf('attachMainWindowServices(') @@ -24,7 +24,7 @@ describe('desktop startup ordering', () => { expect(Math.max(rpcStartIndex, legacyRpcStartIndex)).toBeGreaterThanOrEqual(0) }) - it('shows the desktop window without waiting for WSL registration reconciliation', () => { + it('bounds WSL reconciliation before serve RPC while leaving desktop startup independent', () => { const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8') const barrierStart = source.indexOf("ipcMain.handle('app:awaitFirstWindowStartupServices'") const barrierEnd = source.indexOf("ipcMain.handle(\n 'app:startupDiagnostic'", barrierStart) @@ -43,12 +43,32 @@ describe('desktop startup ordering', () => { expect(serveStart).toBeGreaterThan(reconciliationStart) expect(serveEnd).toBeGreaterThan(serveStart) expect(desktopWindowStart).toBeGreaterThan(reconciliationStart) - expect(serveStartup).toContain('await managedWslCliReconciliationReady') + expect(serveStartup).toContain('await managedWslCliStartupBarrierReady') + expect(serveStartup).not.toContain('await managedWslCliReconciliationReady') + expect(serveStartup.indexOf('await managedWslCliStartupBarrierReady')).toBeLessThan( + serveStartup.indexOf('await runtimeRpc.start()') + ) expect(desktopStartup).not.toContain('await managedWslCliReconciliationReady') expect(barrier).toContain('managedWslCliStartupBarrierReady') expect(barrier).not.toContain('managedWslCliReconciliationReady') }) + it('exposes managed WSL reconciliation status to headless serve clients and diagnostics', () => { + const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8') + + // Why: the barrier fails open, so the serve-ready payload must carry the + // reconciliation state and the bounded wait must be traceable via a milestone. + const readyStart = source.indexOf("type: 'orca_server_ready'") + const readyEnd = source.indexOf('pairing: pairing.available', readyStart) + const readyPayload = source.slice(readyStart, readyEnd) + expect(readyPayload).toContain('managedWslCliReconciliation: managedWslCliReconciliationStatus') + + expect(source).toContain("managedWslCliReconciliationStatus = 'pending'") + expect(source).toContain("managedWslCliReconciliationStatus = 'settled'") + expect(source).toContain("managedWslCliReconciliationStatus = 'failed'") + expect(source).toContain("logStartupMilestone('wsl-cli-barrier-resolved'") + }) + it('does not run the rate-limit quota fetch before the first window can show results', () => { const source = readFileSync(join(process.cwd(), 'src/main/index.ts'), 'utf8') const attachIndex = source.indexOf('rateLimits.attach(window)') diff --git a/src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts b/src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts index e59201e6a..b2a56a415 100644 --- a/src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts +++ b/src/main/startup/wsl-cli-reconciliation-startup-barrier.test.ts @@ -30,13 +30,13 @@ describe('createWslCliReconciliationStartupBarrier', () => { } }) - it('fails open when reconciliation exceeds the startup budget', async () => { + it('fails open immediately when reconciliation rejects before the budget expires', async () => { vi.useFakeTimers() - let resolveReconciliation!: () => void + let rejectReconciliation!: (error: Error) => void try { - const reconciliation = new Promise((resolve) => { - resolveReconciliation = resolve + const reconciliation = new Promise((_resolve, reject) => { + rejectReconciliation = reject }) const barrier = createWslCliReconciliationStartupBarrier(reconciliation) let barrierSettled = false @@ -44,38 +44,73 @@ describe('createWslCliReconciliationStartupBarrier', () => { barrierSettled = true }) - await vi.advanceTimersByTimeAsync(WSL_CLI_RECONCILIATION_STARTUP_BUDGET_MS - 1) + await vi.advanceTimersByTimeAsync(1) expect(barrierSettled).toBe(false) - await vi.advanceTimersByTimeAsync(1) + // Why: a fast WSL discovery failure should release the barrier via its catch + // branch without waiting out the budget, and must clear the pending timer. + rejectReconciliation(new Error('WSL discovery failed')) await expect(barrier).resolves.toBeUndefined() - resolveReconciliation() - await reconciliation + expect(vi.getTimerCount()).toBe(0) } finally { vi.useRealTimers() } }) - it('leaves reconciliation running after the startup budget expires', async () => { + it('lets serve reach RPC readiness at budget while reconciliation remains pending', async () => { vi.useFakeTimers() - let resolveWork!: () => void - let completed = false + let resolveReconciliation!: () => void + let reconciliationCompleted = false try { - const work = new Promise((resolve) => { - resolveWork = resolve + const reconciliation = new Promise((resolve) => { + resolveReconciliation = resolve }).then(() => { - completed = true + reconciliationCompleted = true }) - const barrier = createWslCliReconciliationStartupBarrier(work, { timeoutMs: 10 }) + const barrier = createWslCliReconciliationStartupBarrier(reconciliation) + let rpcReady = false + const serveRpcReadiness = barrier.then(() => { + rpcReady = true + }) + + await vi.advanceTimersByTimeAsync(WSL_CLI_RECONCILIATION_STARTUP_BUDGET_MS - 1) + expect(rpcReady).toBe(false) + expect(reconciliationCompleted).toBe(false) + + await vi.advanceTimersByTimeAsync(1) + await expect(serveRpcReadiness).resolves.toBeUndefined() + expect(rpcReady).toBe(true) + expect(reconciliationCompleted).toBe(false) + + resolveReconciliation() + await reconciliation + expect(reconciliationCompleted).toBe(true) + } finally { + vi.useRealTimers() + } + }) + + it('preserves eventual reconciliation error reporting after the budget expires', async () => { + vi.useFakeTimers() + let rejectReconciliation!: (error: Error) => void + const reportedErrors: string[] = [] + + try { + const reconciliation = new Promise((_resolve, reject) => { + rejectReconciliation = reject + }).catch((error) => { + reportedErrors.push(error instanceof Error ? error.message : String(error)) + }) + const barrier = createWslCliReconciliationStartupBarrier(reconciliation, { timeoutMs: 10 }) await vi.advanceTimersByTimeAsync(10) await expect(barrier).resolves.toBeUndefined() - expect(completed).toBe(false) + expect(reportedErrors).toEqual([]) - resolveWork() - await work - expect(completed).toBe(true) + rejectReconciliation(new Error('WSL discovery failed')) + await reconciliation + expect(reportedErrors).toEqual(['WSL discovery failed']) } finally { vi.useRealTimers() } diff --git a/src/main/startup/wsl-cli-reconciliation-startup-barrier.ts b/src/main/startup/wsl-cli-reconciliation-startup-barrier.ts index 5f70ad7ee..056f225b8 100644 --- a/src/main/startup/wsl-cli-reconciliation-startup-barrier.ts +++ b/src/main/startup/wsl-cli-reconciliation-startup-barrier.ts @@ -5,7 +5,7 @@ type WslCliReconciliationStartupBarrierOptions = { } /** - * Briefly gates restored terminals while managed WSL registrations reconcile. + * Briefly gates terminal startup while managed WSL registrations reconcile. */ export function createWslCliReconciliationStartupBarrier( reconciliation: Promise, @@ -22,8 +22,8 @@ export function createWslCliReconciliationStartupBarrier( } }) - // Why: reconciliation may outlive a slow or unavailable WSL distro; restored - // terminals should wait briefly without turning WSL discovery into an app hang. + // Why: reconciliation may outlive a slow or unavailable WSL distro; terminal + // startup should wait briefly without turning WSL discovery into an app hang. return Promise.race([ settled, new Promise((resolve) => {