From a8f3501647f10816b4f35759fd45e083173d66b1 Mon Sep 17 00:00:00 2001 From: Harold Kim Date: Fri, 11 Sep 2026 22:39:34 +0000 Subject: [PATCH] fix: scope graceful legacy shutdown to the owning controller Co-Authored-By: Paperclip --- server/src/services/heartbeat.ts | 4 ++++ server/src/services/legacy-controller-lease.test.ts | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index b88aeb8535f..103c1067f64 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -14557,6 +14557,10 @@ export function heartbeatService( const restartSuspendedRunIds: string[] = []; for (const { run, agent } of activeRuns) { + // Shutdown owns only this boot's legacy executions. Expired foreign + // owners belong to the reaper, not another container's drain. + if (run.runtimeMode === "legacy" && run.controllerBootId && + run.controllerBootId !== legacyControllerBootId) continue; if (isNativeRunnerOwnershipHeld(run)) continue; if ( run.runtimeMode === "native" && diff --git a/server/src/services/legacy-controller-lease.test.ts b/server/src/services/legacy-controller-lease.test.ts index 0eb33ccec9e..6bceee0c4c6 100644 --- a/server/src/services/legacy-controller-lease.test.ts +++ b/server/src/services/legacy-controller-lease.test.ts @@ -43,6 +43,15 @@ const support = await getEmbeddedPostgresTestSupport(); expect(saved.status).toBe("running"); expect(saved.errorCode).toBeNull(); }); + it.each([false, true])("shutdown preserves a foreign controller (expired: %s)", async expired => { + const run = await seed(); + await db.update(heartbeatRuns).set({ controllerBootId: randomUUID() }).where(eq(heartbeatRuns.id, run.id)); + if (expired) await expire(run.id); + const result = await heartbeatService(db).drainRunningRunsForShutdown("SIGTERM", new Date(), [run.id]); + expect(result.interruptedRunIds).toEqual([]); + const [saved] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, run.id)); + expect(saved.status).toBe("running"); + }); it("a current controller renews and records the dispatch boundary", async () => { const run = await seed(); expect(await renewLegacyControllerLease(db, run, "dispatching")).toBe(true);