From c2c242dd8a6f482f2e30197d034710f9eb68da89 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Fri, 4 Sep 2026 15:21:13 -0400 Subject: [PATCH] fix(orchestration): stop certifying an unproven process exit An unproven stop wrote stage=process_exited with termination_reason=unknown, and the fleet projection read that stage alone as a death certificate, so worker-list and worker-show published liveness=exited and nextAction=recover for a possibly-live agent. Only certify the stage when the cause was observed. --- .../orchestration-fleet-projection.test.ts | 37 +++++++++++++++++++ .../orchestration-fleet-worker-projection.ts | 5 ++- 2 files changed, 41 insertions(+), 1 deletion(-) 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'