mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(agent-hooks): the WSL hook relay stays down while its distro is stopped (E9) (#34195)
* fix(agent-hooks): the WSL hook relay stays down while its distro is stopped (E9)
verify-15b: after `wsl --terminate`, the relay link died ("mux disposed
(connection_lost); scheduling restart") and about 10 s later a `wsl.exe … launch.sh`
restart booted the stopped distro. The restart already asked whether the distro
runs, but any running-list answer would do, including one from a read that started
before the link died. Now:
- only a confirmed running-list read started after the link died can allow the
restart (the shared startedAfter rule; a failed read counts as stopped);
- while the distro is stopped the relay stays down, and it comes back on its own once
something else confirms the distro runs again (deferUntilWslDistroRunning), or when
a WSL terminal re-ensures it as before. Nothing in recovery boots a distro.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(agent-hooks): keep the Codex home and pi/omp setups across a deferred relay restart
A stopped verdict dropped the relay state before the deferred restart ran, so the
relay came back without the managed Codex home or the pi/omp launch setups.
Capture them first and pass them through. Also re-probe once after 30 s so a
failed running-list read while the distro is up does not leave the relay down.
* fix(agent-hooks): anchor the relay restart check at check time and keep all relay config
The running-list read must start when the restart check runs, not when the link
died: a read begun right after a terminate can still list the distro. A deferred
restart now seeds every relay config field (Codex home, Claude agent view and
config dirs, OpenCode overlays, pi/omp setups) so ensure brings them back.
---------
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,7 @@ import {
|
||||
} from './wsl-hook-relay-launch'
|
||||
import { waitForWslRelaySentinel } from './wsl-hook-relay-sentinel'
|
||||
import { listWslDistrosAsync } from '../wsl'
|
||||
import { isWslDistroRunning } from '../wsl-background-distro-gate'
|
||||
import { deferUntilWslDistroRunning, isWslDistroRunning } from '../wsl-background-distro-gate'
|
||||
import { isWslAgentHookRelayEnabled } from '../../shared/agent-hook-relay'
|
||||
|
||||
// Concurrent wsl.exe spawns can trigger transient E_UNEXPECTED failures.
|
||||
@@ -49,6 +49,7 @@ export type WslHookRelayManagerDeps = {
|
||||
listDistros: () => Promise<string[]>
|
||||
/** Recovery must not boot a distro the user shut down. */
|
||||
isDistroRunning: typeof isWslDistroRunning
|
||||
deferUntilRunning: typeof deferUntilWslDistroRunning
|
||||
spawnRelay: typeof spawnWslRelayProcess
|
||||
runInstall: typeof runWslInstallProcess
|
||||
waitForSentinel: typeof waitForWslRelaySentinel
|
||||
@@ -91,6 +92,7 @@ export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = {
|
||||
readBundle: (jsPath) => readFileSync(jsPath),
|
||||
listDistros: () => listWslDistrosAsync(),
|
||||
isDistroRunning: isWslDistroRunning,
|
||||
deferUntilRunning: deferUntilWslDistroRunning,
|
||||
spawnRelay: spawnWslRelayProcess,
|
||||
runInstall: runWslInstallProcess,
|
||||
waitForSentinel: waitForWslRelaySentinel,
|
||||
|
||||
@@ -236,6 +236,7 @@ describe('WslHookRelayManager', () => {
|
||||
readBundle: () => Buffer.from('// bundle'),
|
||||
listDistros: async () => ['Ubuntu'],
|
||||
isDistroRunning: vi.fn(async () => true),
|
||||
deferUntilRunning: vi.fn(),
|
||||
spawnRelay: vi.fn(() => fakeChild()),
|
||||
runInstall: vi.fn(async () => ({ code: 0, stderr: '' })),
|
||||
waitForSentinel: vi.fn(async () => guestTransport()),
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type WslHookRelayManagerDeps
|
||||
} from './wsl-hook-relay-deps'
|
||||
import { wireWslRelayLink } from './wsl-hook-relay-link'
|
||||
import { WslRelayRecovery } from './wsl-hook-relay-recovery'
|
||||
import type { WslRelayRecovery } from './wsl-hook-relay-recovery'
|
||||
import { wslHookRelayStateKey } from './wsl-hook-relay-state-key'
|
||||
import { SshChannelMultiplexer, type MultiplexerTransport } from '../ssh/ssh-channel-multiplexer'
|
||||
import { AGENT_HOOK_REQUEST_REPLAY_METHOD } from '../../shared/agent-hook-relay'
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
wslRuntimeHomePathsEqual
|
||||
} from '../codex/managed-wsl-codex-home-registry'
|
||||
import {
|
||||
createWslRelayRecoveryForManager,
|
||||
markWslRelayFailed,
|
||||
resolveWslDefaultDistro,
|
||||
resumeWslStoppedRelays
|
||||
@@ -44,18 +45,11 @@ export class WslHookRelayManager {
|
||||
private warnedBundleMissing = false
|
||||
constructor(deps: Partial<WslHookRelayManagerDeps> = {}) {
|
||||
this.deps = { ...defaultWslHookRelayDeps, ...deps }
|
||||
this.recovery = new WslRelayRecovery({
|
||||
isDistroRunning: (distro) => this.deps.isDistroRunning(distro),
|
||||
warn: (message) => this.deps.warn(message),
|
||||
this.recovery = createWslRelayRecoveryForManager({
|
||||
deps: () => this.deps,
|
||||
states: this.states,
|
||||
isDisposed: () => this.disposed,
|
||||
isCurrent: (state) => this.states.get(wslHookRelayStateKey(state.distro)) === state,
|
||||
restart: (distro) => this.ensureForDistro(distro, this.stateFor(distro)?.codexHomePath),
|
||||
dropState: (state) => {
|
||||
const key = wslHookRelayStateKey(state.distro)
|
||||
if (this.states.get(key) === state) {
|
||||
this.states.delete(key)
|
||||
}
|
||||
}
|
||||
ensure: (...args) => this.ensureForDistro(...args)
|
||||
})
|
||||
}
|
||||
setManagedHookSettingsResolver(resolve: WslHookRelayManagerDeps['managedHookSettings']): void {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { WslRelayRecovery, type WslRelayRecoveryState } from './wsl-hook-relay-recovery'
|
||||
import type { WslRelayDistroState, WslRelayRestartSetup } from './wsl-hook-relay-state'
|
||||
import { wslHookRelayStateKey } from './wsl-hook-relay-state-key'
|
||||
import { captureWslRelaySetup, restartWslRelay } from './wsl-hook-relay-state-machine'
|
||||
|
||||
beforeEach(() => vi.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }))
|
||||
|
||||
@@ -19,6 +22,8 @@ describe('WslRelayRecovery', () => {
|
||||
const restart = vi.fn()
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: async () => true,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
@@ -29,7 +34,7 @@ describe('WslRelayRecovery', () => {
|
||||
recovery.scheduleRestart(state)
|
||||
await vi.advanceTimersByTimeAsync(250)
|
||||
await waitFor(() => restart.mock.calls.length === 1)
|
||||
expect(restart).toHaveBeenCalledWith('Ubuntu')
|
||||
expect(restart).toHaveBeenCalledWith('Ubuntu', { launchKinds: [] })
|
||||
})
|
||||
|
||||
it('drops the state instead of booting a stopped distro', async () => {
|
||||
@@ -38,6 +43,8 @@ describe('WslRelayRecovery', () => {
|
||||
const warn = vi.fn()
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: async () => false,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn,
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
@@ -52,11 +59,116 @@ describe('WslRelayRecovery', () => {
|
||||
expect(warn).toHaveBeenCalledWith(expect.stringContaining('distro not running'))
|
||||
})
|
||||
|
||||
it('after a terminate, stays down and comes back only once the distro runs again (E9)', async () => {
|
||||
const restart = vi.fn()
|
||||
const dropState = vi.fn()
|
||||
const deferred: (() => void)[] = []
|
||||
const isDistroRunning = vi.fn(async (_distro: string, _startedAfter: number) => false)
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning,
|
||||
deferUntilRunning: (_distro, task) => deferred.push(task),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
restart,
|
||||
dropState
|
||||
})
|
||||
const state = makeState()
|
||||
recovery.scheduleRestart(state)
|
||||
await vi.advanceTimersByTimeAsync(250)
|
||||
await waitFor(() => dropState.mock.calls.length === 1)
|
||||
expect(restart).not.toHaveBeenCalled()
|
||||
expect(deferred).toHaveLength(1)
|
||||
// Something else (the user) started the distro: only now does the relay come back.
|
||||
deferred[0]()
|
||||
expect(restart).toHaveBeenCalledExactlyOnceWith('Ubuntu', { launchKinds: [] })
|
||||
})
|
||||
|
||||
it('restarts with the relay config captured before dropState discarded it', async () => {
|
||||
const restart = vi.fn()
|
||||
const deferred: (() => void)[] = []
|
||||
let live: WslRelayRestartSetup | undefined = {
|
||||
codexHomePath: '/home/u/.orca-codex',
|
||||
claudeConfigDir: '/home/u/.orca/claude',
|
||||
launchKinds: ['pi', 'omp']
|
||||
}
|
||||
const expected = live
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: async () => false,
|
||||
deferUntilRunning: (_distro, task) => deferred.push(task),
|
||||
captureSetup: () => live ?? { launchKinds: [] },
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
restart,
|
||||
dropState: () => {
|
||||
live = undefined
|
||||
}
|
||||
})
|
||||
recovery.scheduleRestart(makeState())
|
||||
await vi.advanceTimersByTimeAsync(250)
|
||||
await waitFor(() => deferred.length === 1)
|
||||
deferred[0]()
|
||||
expect(restart).toHaveBeenCalledExactlyOnceWith('Ubuntu', expected)
|
||||
})
|
||||
|
||||
it('accepts only a running-list read started at check time, not one begun at the terminate', async () => {
|
||||
const now = vi.spyOn(performance, 'now').mockReturnValue(1_000)
|
||||
const isDistroRunning = vi.fn(async (_distro: string, _startedAfter: number) => false)
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
restart: vi.fn(),
|
||||
dropState: vi.fn()
|
||||
})
|
||||
recovery.scheduleRestart(makeState())
|
||||
// A read begun just after the terminate may still list the distro; the check runs later.
|
||||
now.mockReturnValue(11_000)
|
||||
await vi.advanceTimersByTimeAsync(250)
|
||||
await waitFor(() => isDistroRunning.mock.calls.length === 1)
|
||||
expect(isDistroRunning.mock.calls[0]?.[1]).toBe(11_000)
|
||||
now.mockRestore()
|
||||
})
|
||||
|
||||
it('re-probes once after a stopped verdict and restarts only once', async () => {
|
||||
const restart = vi.fn()
|
||||
const deferred: (() => void)[] = []
|
||||
const isDistroRunning = vi.fn(async () => false)
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning,
|
||||
deferUntilRunning: (_distro, task) => deferred.push(task),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
restart,
|
||||
dropState: vi.fn()
|
||||
})
|
||||
recovery.scheduleRestart(makeState())
|
||||
await vi.advanceTimersByTimeAsync(250)
|
||||
await waitFor(() => deferred.length === 1)
|
||||
// The first read was wrong (the distro was still running): the re-probe recovers it.
|
||||
isDistroRunning.mockResolvedValue(true)
|
||||
await vi.advanceTimersByTimeAsync(30_000)
|
||||
await waitFor(() => restart.mock.calls.length === 1)
|
||||
deferred[0]()
|
||||
await vi.advanceTimersByTimeAsync(60_000)
|
||||
expect(isDistroRunning).toHaveBeenCalledTimes(2)
|
||||
expect(restart).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does nothing when the state was replaced before the restart probe', async () => {
|
||||
const restart = vi.fn()
|
||||
const probe = vi.fn(async () => true)
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: probe,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => false,
|
||||
@@ -78,6 +190,8 @@ describe('WslRelayRecovery', () => {
|
||||
const dropState = vi.fn()
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: probe,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => current,
|
||||
@@ -102,6 +216,8 @@ describe('WslRelayRecovery', () => {
|
||||
const restart = vi.fn()
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: probe,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => current,
|
||||
@@ -122,6 +238,8 @@ describe('WslRelayRecovery', () => {
|
||||
const run = vi.fn()
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: async () => true,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => true,
|
||||
isCurrent: () => true,
|
||||
@@ -139,6 +257,8 @@ describe('WslRelayRecovery', () => {
|
||||
const restart = vi.fn()
|
||||
const recovery = new WslRelayRecovery({
|
||||
isDistroRunning: async () => true,
|
||||
deferUntilRunning: vi.fn(),
|
||||
captureSetup: () => ({ launchKinds: [] }),
|
||||
warn: vi.fn(),
|
||||
isDisposed: () => false,
|
||||
isCurrent: () => true,
|
||||
@@ -157,3 +277,66 @@ describe('WslRelayRecovery', () => {
|
||||
expect(state.reinstallTimer).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('restartWslRelay', () => {
|
||||
function relayState(overrides: Partial<WslRelayDistroState> = {}): WslRelayDistroState {
|
||||
return {
|
||||
distro: 'Ubuntu',
|
||||
phase: 'running',
|
||||
failures: 0,
|
||||
cooldownUntil: 0,
|
||||
launchKinds: new Set(),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
it('seeds a dropped relay so ensure inherits every captured config field', async () => {
|
||||
const states = new Map<string, WslRelayDistroState>()
|
||||
const key = wslHookRelayStateKey('Ubuntu')
|
||||
const source = relayState({
|
||||
codexHomePath: '/h',
|
||||
claudeAgentViewDir: '/av',
|
||||
claudeConfigDir: '/cc',
|
||||
opencodeOverlayDir: '/oc',
|
||||
opencode2OverlayDir: '/oc2',
|
||||
piAgentDir: '/pi',
|
||||
ompStatusExtension: '/omp',
|
||||
launchKinds: new Set(['pi', 'omp'])
|
||||
})
|
||||
const setup = captureWslRelaySetup(source)
|
||||
const seen: (WslRelayDistroState | undefined)[] = []
|
||||
const ensure = vi.fn(async () => {
|
||||
seen.push(states.get(key))
|
||||
states.set(key, relayState({ ...states.get(key), phase: 'starting' }))
|
||||
})
|
||||
await restartWslRelay(states, ensure, 'Ubuntu', setup)
|
||||
const { phase: _p, ...expectedConfig } = source
|
||||
expect(seen[0]).toMatchObject({ ...expectedConfig, phase: 'failed', cooldownUntil: 0 })
|
||||
expect(ensure.mock.calls).toEqual([
|
||||
['Ubuntu'],
|
||||
['Ubuntu', undefined, 'pi'],
|
||||
['Ubuntu', undefined, 'omp']
|
||||
])
|
||||
})
|
||||
|
||||
it('removes the seed when ensure bails without replacing it', async () => {
|
||||
const states = new Map<string, WslRelayDistroState>()
|
||||
await restartWslRelay(
|
||||
states,
|
||||
vi.fn(async () => {}),
|
||||
'Ubuntu',
|
||||
{ codexHomePath: '/h', launchKinds: [] }
|
||||
)
|
||||
expect(states.size).toBe(0)
|
||||
})
|
||||
|
||||
it('leaves a relay a terminal already re-ensured in place', async () => {
|
||||
const live = relayState({ codexHomePath: '/new' })
|
||||
const states = new Map([[wslHookRelayStateKey('Ubuntu'), live]])
|
||||
const ensure = vi.fn(async () => {})
|
||||
await restartWslRelay(states, ensure, 'Ubuntu', { codexHomePath: '/old', launchKinds: ['pi'] })
|
||||
expect(states.get(wslHookRelayStateKey('Ubuntu'))).toBe(live)
|
||||
expect(live.codexHomePath).toBe('/new')
|
||||
expect(ensure.mock.calls).toEqual([['Ubuntu'], ['Ubuntu', undefined, 'pi']])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { WslRelayRestartSetup } from './wsl-hook-relay-state'
|
||||
|
||||
export type WslRelayRecoveryState = {
|
||||
distro: string
|
||||
cooldownUntil: number
|
||||
@@ -6,14 +8,22 @@ export type WslRelayRecoveryState = {
|
||||
}
|
||||
|
||||
export type WslRelayRecoveryIo = {
|
||||
isDistroRunning: (distro: string) => Promise<boolean>
|
||||
/** A confirmed running list read started after `startedAfter`; a failed read counts as stopped. */
|
||||
isDistroRunning: (distro: string, startedAfter: number) => Promise<boolean>
|
||||
/** Runs `task` once something else confirms the distro runs again; never boots it. */
|
||||
deferUntilRunning: (distro: string, task: () => void) => void
|
||||
warn: (message: string) => void
|
||||
isDisposed: () => boolean
|
||||
isCurrent: (state: WslRelayRecoveryState) => boolean
|
||||
restart: (distro: string) => void
|
||||
/** Snapshot of the relay config; read before `dropState` discards it. */
|
||||
captureSetup: (state: WslRelayRecoveryState) => WslRelayRestartSetup
|
||||
restart: (distro: string, setup: WslRelayRestartSetup) => void
|
||||
dropState: (state: WslRelayRecoveryState) => void
|
||||
}
|
||||
|
||||
// A failed running-list read is not proof the distro stopped; look once more after WSL's backoff.
|
||||
const REPROBE_AFTER_STOPPED_MS = 30_000
|
||||
|
||||
export class WslRelayRecovery {
|
||||
constructor(private io: WslRelayRecoveryIo) {}
|
||||
|
||||
@@ -56,18 +66,37 @@ export class WslRelayRecovery {
|
||||
if (this.io.isDisposed() || !this.io.isCurrent(state)) {
|
||||
return
|
||||
}
|
||||
const running = await this.io.isDistroRunning(state.distro)
|
||||
// Captured now: once the state is dropped, a later restart would otherwise come back bare.
|
||||
const setup = this.io.captureSetup(state)
|
||||
// Anchored at check time: a read begun right after the terminate may still list the distro.
|
||||
const running = await this.io.isDistroRunning(state.distro, performance.now())
|
||||
// The probe await may outlive state replacement; never drop or restart the replacement relay.
|
||||
if (this.io.isDisposed() || !this.io.isCurrent(state)) {
|
||||
return
|
||||
}
|
||||
if (!running) {
|
||||
this.io.warn(
|
||||
`[agent-hooks] WSL hook relay (${state.distro}): distro not running (or probe failed); restart skipped (next WSL terminal re-ensures)`
|
||||
`[agent-hooks] WSL hook relay (${state.distro}): distro not running (or probe failed); staying down until it runs again or a WSL terminal re-ensures`
|
||||
)
|
||||
this.io.dropState(state)
|
||||
// E9: come back when the distro runs again (started by the user or another act), never boot it.
|
||||
let restarted = false
|
||||
const restartOnce = (): void => {
|
||||
if (!restarted && !this.io.isDisposed()) {
|
||||
restarted = true
|
||||
this.io.restart(state.distro, setup)
|
||||
}
|
||||
}
|
||||
this.io.deferUntilRunning(state.distro, restartOnce)
|
||||
const reprobe = setTimeout(() => {
|
||||
void this.io
|
||||
.isDistroRunning(state.distro, performance.now())
|
||||
.then((running) => running && restartOnce())
|
||||
.catch(() => {})
|
||||
}, REPROBE_AFTER_STOPPED_MS)
|
||||
reprobe.unref?.()
|
||||
return
|
||||
}
|
||||
this.io.restart(state.distro)
|
||||
this.io.restart(state.distro, setup)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { WslRelayRecovery } from './wsl-hook-relay-recovery'
|
||||
import type { WslHookRelayManagerDeps } from './wsl-hook-relay-deps'
|
||||
import type { WslRelayDistroState, WslRelayRestartSetup } from './wsl-hook-relay-state'
|
||||
import { wslHookRelayStateKey } from './wsl-hook-relay-state-key'
|
||||
import { FAILURE_COOLDOWN_MAX_MS } from './wsl-hook-relay-deps'
|
||||
|
||||
type RelayFailureState = {
|
||||
@@ -70,3 +74,85 @@ export async function resolveWslDefaultDistro(
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
type WslRelayEnsure = (
|
||||
distro: string,
|
||||
codexHomePath?: string,
|
||||
launchKind?: 'pi' | 'omp'
|
||||
) => Promise<void>
|
||||
|
||||
export function captureWslRelaySetup(state: WslRelayDistroState): WslRelayRestartSetup {
|
||||
return {
|
||||
codexHomePath: state.codexHomePath,
|
||||
claudeAgentViewDir: state.claudeAgentViewDir,
|
||||
claudeConfigDir: state.claudeConfigDir,
|
||||
opencodeOverlayDir: state.opencodeOverlayDir,
|
||||
opencode2OverlayDir: state.opencode2OverlayDir,
|
||||
piAgentDir: state.piAgentDir,
|
||||
ompStatusExtension: state.ompStatusExtension,
|
||||
launchKinds: [...state.launchKinds]
|
||||
}
|
||||
}
|
||||
|
||||
/** Brings a relay back with everything it served before, even after its state was dropped. */
|
||||
export async function restartWslRelay(
|
||||
states: Map<string, WslRelayDistroState>,
|
||||
ensure: WslRelayEnsure,
|
||||
distro: string,
|
||||
setup: WslRelayRestartSetup
|
||||
): Promise<void> {
|
||||
const key = wslHookRelayStateKey(distro)
|
||||
let seed: WslRelayDistroState | undefined
|
||||
if (!states.has(key)) {
|
||||
// A cooled-down failed state: ensure replaces it with a starting relay that inherits its config.
|
||||
seed = {
|
||||
distro,
|
||||
...setup,
|
||||
launchKinds: new Set(setup.launchKinds),
|
||||
phase: 'failed',
|
||||
failures: 0,
|
||||
cooldownUntil: 0
|
||||
}
|
||||
states.set(key, seed)
|
||||
}
|
||||
// No Codex home argument: ensure keeps the seed's, and never overwrites a newer relay's.
|
||||
const ensures = [ensure(distro)]
|
||||
for (const launchKind of setup.launchKinds) {
|
||||
ensures.push(ensure(distro, undefined, launchKind))
|
||||
}
|
||||
await Promise.all(ensures)
|
||||
// An ensure that bailed (gate closed, no bundle) must not leave the seed behind.
|
||||
if (seed && states.get(key) === seed) {
|
||||
states.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
/** The relay manager's recovery: restarts only into a distro confirmed running, never boots it. */
|
||||
export function createWslRelayRecoveryForManager(manager: {
|
||||
deps: () => WslHookRelayManagerDeps
|
||||
states: Map<string, WslRelayDistroState>
|
||||
isDisposed: () => boolean
|
||||
ensure: WslRelayEnsure
|
||||
}): WslRelayRecovery {
|
||||
const { states } = manager
|
||||
return new WslRelayRecovery({
|
||||
isDistroRunning: (distro, startedAfter) =>
|
||||
manager.deps().isDistroRunning(distro, { startedAfter }),
|
||||
deferUntilRunning: (distro, task) =>
|
||||
manager.deps().deferUntilRunning(distro, 'wsl-hook-relay-restart', task),
|
||||
warn: (message) => manager.deps().warn(message),
|
||||
isDisposed: manager.isDisposed,
|
||||
isCurrent: (state) => states.get(wslHookRelayStateKey(state.distro)) === state,
|
||||
captureSetup: (state) => {
|
||||
const current = states.get(wslHookRelayStateKey(state.distro))
|
||||
return current ? captureWslRelaySetup(current) : { launchKinds: [] }
|
||||
},
|
||||
restart: (distro, setup) => void restartWslRelay(states, manager.ensure, distro, setup),
|
||||
dropState: (state) => {
|
||||
const key = wslHookRelayStateKey(state.distro)
|
||||
if (states.get(key) === state) {
|
||||
states.delete(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,3 +27,15 @@ export type WslRelayDistroState = {
|
||||
lastAttemptOpenCodeSettings?: string
|
||||
lastInstallMux?: SshChannelMultiplexer
|
||||
}
|
||||
|
||||
/** The config a relay served, kept across a stopped-distro drop so its restart brings it all back. */
|
||||
export type WslRelayRestartSetup = Pick<
|
||||
WslRelayDistroState,
|
||||
| 'codexHomePath'
|
||||
| 'claudeAgentViewDir'
|
||||
| 'claudeConfigDir'
|
||||
| 'opencodeOverlayDir'
|
||||
| 'opencode2OverlayDir'
|
||||
| 'piAgentDir'
|
||||
| 'ompStatusExtension'
|
||||
> & { launchKinds: readonly ('pi' | 'omp')[] }
|
||||
|
||||
@@ -13,8 +13,10 @@ function distroKey(distro: string): string {
|
||||
}
|
||||
|
||||
/** Require a fresh running-list result; last-known-good fallback could boot a distro that has since stopped. */
|
||||
export async function listConfirmedRunningWslDistros(): Promise<string[]> {
|
||||
return listRunningWslDistrosAsync({ requireConfirmed: true }).catch(() => [])
|
||||
export async function listConfirmedRunningWslDistros(
|
||||
options: { startedAfter?: number } = {}
|
||||
): Promise<string[]> {
|
||||
return listRunningWslDistrosAsync({ requireConfirmed: true, ...options }).catch(() => [])
|
||||
}
|
||||
|
||||
export type WslRunningDistrosRead =
|
||||
@@ -35,9 +37,13 @@ export async function readConfirmedRunningWslDistros(
|
||||
}
|
||||
}
|
||||
|
||||
export async function isWslDistroRunning(distro: string): Promise<boolean> {
|
||||
/** `startedAfter`: only a running-list read started after it counts, e.g. after a link died. */
|
||||
export async function isWslDistroRunning(
|
||||
distro: string,
|
||||
options: { startedAfter?: number } = {}
|
||||
): Promise<boolean> {
|
||||
const wanted = distroKey(distro)
|
||||
const running = await listConfirmedRunningWslDistros()
|
||||
const running = await listConfirmedRunningWslDistros(options)
|
||||
return running.some((candidate) => distroKey(candidate) === wanted)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user