diff --git a/src/shared/orchestration-fleet-projection.test.ts b/src/shared/orchestration-fleet-projection.test.ts index 780ca86c734..67710929df6 100644 --- a/src/shared/orchestration-fleet-projection.test.ts +++ b/src/shared/orchestration-fleet-projection.test.ts @@ -420,6 +420,43 @@ describe('fleet liveness and attention after a host verdict', () => { }) }) + it('refuses to certify a process_exited stage whose cause was never observed', () => { + const projected = projectOrchestrationFleet({ + workers: [ + worker('1', { + workerStage: 'process_exited', + workerState: 'failed', + terminationReason: 'unknown' + }) + ], + statuses: [], + now: 10_000 + }) + expect(projected.workers[0]!.liveness).toEqual({ + verdict: 'unverifiable', + reason: 'missing_status' + }) + expect(projected.workers[0]!.nextAction.kind).toBe('inspect') + }) + + it('certifies a process_exited stage whose exit was observed', () => { + const projected = projectOrchestrationFleet({ + workers: [ + worker('1', { + workerStage: 'process_exited', + workerState: 'failed', + terminationReason: 'exited' + }) + ], + statuses: [], + now: 10_000 + }) + expect(projected.workers[0]!.liveness).toEqual({ + verdict: 'exited', + source: 'execution_host' + }) + }) + it('keeps an unverifiable worker on inspect: absence is never authority to stop', () => { const now = 10 * AGENT_STATUS_STALE_AFTER_MS const projected = projectOrchestrationFleet({ diff --git a/src/shared/orchestration-fleet-worker-projection.ts b/src/shared/orchestration-fleet-worker-projection.ts index cfcc4963876..e5ce3254351 100644 --- a/src/shared/orchestration-fleet-worker-projection.ts +++ b/src/shared/orchestration-fleet-worker-projection.ts @@ -26,7 +26,10 @@ const SETTLED_WORKER_STATES = new Set(['succeeded', 'failed', 'stopped', 'abando * `unknown` is a death certificate — regardless of which state the worker settled into. */ function hasCertifiedExit(worker: FleetLivenessSubject): boolean { return ( - worker.workerStage === 'process_exited' || + // `process_exited` is written from the same cause as the reason beside it, and + // `unknown` there means a stop was issued and no exit was ever observed. A null + // reason is a pre-v29 row whose stage write was the only exit record. + (worker.workerStage === 'process_exited' && worker.terminationReason !== 'unknown') || worker.terminationReason === 'operator_close' || worker.terminationReason === 'signaled' || worker.terminationReason === 'exited'