diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-close-error.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-close-error.ts index d03737f3220..e0a6ac82066 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-close-error.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-close-error.ts @@ -1,11 +1,18 @@ function isTransientWorkerTerminalCloseError(reason: string): boolean { - return /disposed|not connected|unavailable/i.test(reason) + return /not connected|unavailable/i.test(reason) } /** The close found nothing to close. Against a host-certified exit that is the goal state, not new * doubt: a retry only aims the same dead handle at the same absent terminal, forever. */ function isMissingWorkerTerminalCloseError(reason: string): boolean { - return /handle_stale|stale handle|disposed|not found|no such terminal/i.test(reason) + return /handle_stale|stale handle|not found|no such terminal/i.test(reason) +} + +/** A disposed endpoint is genuinely both: nothing is left to close, and it may return on + * reconnect. Only the host observation can say which, so it is named once here instead of + * being spelled into two predicates that then read as if they were disjoint. */ +function isDisposedWorkerTerminalCloseError(reason: string): boolean { + return /disposed/i.test(reason) } export function classifyWorkerTerminalCloseError(error: unknown): { @@ -14,10 +21,11 @@ export function classifyWorkerTerminalCloseError(error: unknown): { alreadyGone: boolean } { const reason = error instanceof Error ? error.message : String(error) + const disposed = isDisposedWorkerTerminalCloseError(reason) return { reason, - transient: isTransientWorkerTerminalCloseError(reason), - alreadyGone: isMissingWorkerTerminalCloseError(reason) + transient: disposed || isTransientWorkerTerminalCloseError(reason), + alreadyGone: disposed || isMissingWorkerTerminalCloseError(reason) } } diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts index b9e80cf16e9..792656025b9 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-release-recovery.test.ts @@ -212,6 +212,17 @@ describe('orchestration worker release recovery', () => { expect(runtime.closeTerminal).not.toHaveBeenCalled() }) + it('settles a disposed endpoint as released once the host certified the exit', async () => { + setup() + const { dispatchId } = await startSettledWorker() + vi.spyOn(runtime, 'getTerminalLivenessVerdict').mockReturnValue({ status: 'exited' }) + vi.mocked(runtime.closeTerminal).mockRejectedValueOnce(new Error('Multiplexer disposed')) + + await expect( + call('orchestration.workerRelease', { dispatch: dispatchId }) + ).resolves.toMatchObject({ state: 'released', processAction: 'closed_exited_terminal' }) + }) + it('defers instead of settling unknown while inventory is incomplete', async () => { setup() const { dispatchId } = await startSettledWorker()