From 64b3a58fffa0b142923423be3ceb72d7259402bd Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 04:11:17 -0400 Subject: [PATCH] fix(orchestration): stop projecting absence and a self-loop for a proven exit --- src/shared/orchestration-fleet-attention.ts | 7 +- .../orchestration-fleet-projection.test.ts | 64 +++++++++++++++++++ .../orchestration-fleet-worker-projection.ts | 10 +++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/src/shared/orchestration-fleet-attention.ts b/src/shared/orchestration-fleet-attention.ts index 940a22cd802..d77e4c25191 100644 --- a/src/shared/orchestration-fleet-attention.ts +++ b/src/shared/orchestration-fleet-attention.ts @@ -61,7 +61,12 @@ export function projectOrchestrationFleetAttention( if (facts.liveness.verdict === 'unverifiable') { categories.push(facts.liveness.reason === 'stale_status' ? 'stale' : 'unverifiable') } - if (facts.outcome === 'outcome_unknown' || facts.outcome === 'finished_unverified') { + // A proven exit is evidence, not absence: `unverifiable` beside an `exited` verdict told a + // reader to keep waiting on a worker the execution host had already reported gone. + if ( + facts.liveness.verdict !== 'exited' && + (facts.outcome === 'outcome_unknown' || facts.outcome === 'finished_unverified') + ) { if (!categories.includes('unverifiable')) { categories.push('unverifiable') } diff --git a/src/shared/orchestration-fleet-projection.test.ts b/src/shared/orchestration-fleet-projection.test.ts index 1ebb0bd83a7..780ca86c734 100644 --- a/src/shared/orchestration-fleet-projection.test.ts +++ b/src/shared/orchestration-fleet-projection.test.ts @@ -439,4 +439,68 @@ describe('fleet liveness and attention after a host verdict', () => { }) expect(projected.workers[0]!.nextAction.kind).toBe('inspect') }) + + // The live worker-list row from a stopped worker: the same receipt proved the exit, + // called it absence, and pointed back at the command that reported the settlement. + it('never contradicts a proven exit on a stopped worker still owning its terminal', () => { + const projected = projectOrchestrationFleet({ + workers: [ + worker('1', { + workerState: 'stopped', + dispatchStatus: 'completed', + workerStage: 'process_stopped', + outcome: 'outcome_unknown', + terminalState: 'retained', + resource: { + id: 'resource-1', + ownerDispatchId: '1', + worktreeId: 'workspace-1', + paneKey: 'tab-1:leaf-1', + hostScope: null, + ownershipState: 'owned', + releaseState: 'active', + updatedAt: '2026-09-04T00:00:00.000Z' + } + }) + ], + statuses: [], + now: 10_000 + }) + const row = projected.workers[0]! + + expect(row.liveness.verdict).toBe('exited') + expect(row.attention.categories).not.toContain('unverifiable') + expect(row.attention.requiresAction).toBe(false) + expect(row.nextAction).toEqual({ + kind: 'release', + argv: ['orchestration', 'worker-release', '--dispatch', '1'] + }) + }) + + it('asks nothing more of a settled worker whose terminal is already released', () => { + const projected = projectOrchestrationFleet({ + workers: [ + worker('1', { + workerState: 'stopped', + dispatchStatus: 'completed', + outcome: 'outcome_unknown', + terminalState: 'retained', + resource: { + id: 'resource-1', + ownerDispatchId: '1', + worktreeId: 'workspace-1', + paneKey: 'tab-1:leaf-1', + hostScope: null, + ownershipState: 'user_owned', + releaseState: 'active', + updatedAt: '2026-09-04T00:00:00.000Z' + } + }) + ], + statuses: [], + now: 10_000 + }) + + expect(projected.workers[0]!.nextAction).toEqual({ kind: 'none', argv: [] }) + }) }) diff --git a/src/shared/orchestration-fleet-worker-projection.ts b/src/shared/orchestration-fleet-worker-projection.ts index e1a8d9c72c8..cfcc4963876 100644 --- a/src/shared/orchestration-fleet-worker-projection.ts +++ b/src/shared/orchestration-fleet-worker-projection.ts @@ -121,6 +121,16 @@ function nextAction(worker: FleetDurableWorker, liveness: FleetLiveness): FleetN ) { return { kind: 'none', argv: [] } } + // A settled worker still owning its terminal owes the release decision. Pointing it at + // worker-show was a self-loop: the command that reported the settlement. + if (SETTLED_WORKER_STATES.has(worker.workerState) && worker.resource) { + return worker.resource.ownershipState === 'owned' && worker.resource.releaseState !== 'released' + ? { + kind: 'release', + argv: ['orchestration', 'worker-release', '--dispatch', worker.dispatchId] + } + : { kind: 'none', argv: [] } + } // A proven exit under a worker that never settled is a stall; worker-show would // only restate it. Read the transcript, then stop or abandon. `unverifiable` is // absence and must never land here.