diff --git a/src/orchestrator/core.ts b/src/orchestrator/core.ts index e82f569c..bc4abfa2 100644 --- a/src/orchestrator/core.ts +++ b/src/orchestrator/core.ts @@ -462,6 +462,7 @@ export class OrchestratorCore { const terminalStates = toNormalizedStateSet( this.config.tracker.terminalStates, ); + const refreshedIds = new Set(refreshed.map((snapshot) => snapshot.id)); for (const snapshot of refreshed) { const runningEntry = this.state.running[snapshot.id]; @@ -492,6 +493,21 @@ export class OrchestratorCore { ); } + for (const runningId of runningIds) { + if (refreshedIds.has(runningId)) { + continue; + } + + const runningEntry = this.state.running[runningId]; + if (runningEntry === undefined) { + continue; + } + + stopRequests.push( + await this.requestStop(runningEntry, false, "inactive_state"), + ); + } + return { stopRequests, reconciliationFetchFailed: false, diff --git a/tests/orchestrator/core.test.ts b/tests/orchestrator/core.test.ts index 48ade6fb..6e3c12e4 100644 --- a/tests/orchestrator/core.test.ts +++ b/tests/orchestrator/core.test.ts @@ -141,6 +141,25 @@ describe("orchestrator core", () => { ]); }); + it("requests stop when reconciliation no longer returns a running issue", async () => { + const tracker = createTracker({ + statesById: [], + }); + const orchestrator = createOrchestrator({ tracker }); + + await orchestrator.pollTick(); + const result = await orchestrator.pollTick(); + + expect(result.stopRequests).toEqual([ + { + issueId: "1", + issueIdentifier: "ISSUE-1", + cleanupWorkspace: false, + reason: "inactive_state", + }, + ]); + }); + it("treats reconciliation with no running issues as a no-op", async () => { const tracker = createTracker({ candidates: [],