From 36685faf867d0e7e3d0b79313e9edbc3dc02e516 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 15:36:14 -0400 Subject: [PATCH] refactor(orchestration): name the disposed close error once instead of in both predicates isTransient and isMissing both matched disposed, so the two read as disjoint while one silently shadowed the other. A disposed endpoint is genuinely both, and only the host observation decides which; pin the certified-exit outcome that had no coverage. --- .../worker/worker-release-close-error.ts | 16 ++++++++++++---- .../worker/worker-release-recovery.test.ts | 11 +++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) 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()