Fix windows serve wsl barrier (#8559)

* fix(startup): bound serve WSL reconciliation wait

* test(startup): cover WSL barrier fail-open on early reconciliation rejection

Co-authored-by: Orca <help@stably.ai>

* feat(serve): surface managed WSL reconciliation status to headless clients

Expose reconciliation state ('pending'|'settled'|'failed') in the
orca_server_ready payload and a wsl-cli-barrier startup milestone, so
headless/SSH agents can tell the fail-open barrier outlived its budget
and a WSL PTY launch may still race an un-migrated registration.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinjing 2026-07-13 05:40:09 -07:00 committed by GitHub
parent 6091995e75
commit 13ed697cc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 97 additions and 28 deletions

View File

@ -229,6 +229,10 @@ const recoveryReloadInFlight = createWebContentsTimedFlag()
let firstWindowStartupServicesReady: Promise<void> = Promise.resolve()
let managedWslCliReconciliationReady: Promise<void> = Promise.resolve()
let managedWslCliStartupBarrierReady: Promise<void> = 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<void> {
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,

View File

@ -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)')

View File

@ -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<void>((resolve) => {
resolveReconciliation = resolve
const reconciliation = new Promise<void>((_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<void>((resolve) => {
resolveWork = resolve
const reconciliation = new Promise<void>((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<void>((_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()
}

View File

@ -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<unknown>,
@ -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<void>((resolve) => {