diff --git a/src/gateway/server-methods/sessions-shared.ts b/src/gateway/server-methods/sessions-shared.ts index 293ea864537f..90dbaa2e1f87 100644 --- a/src/gateway/server-methods/sessions-shared.ts +++ b/src/gateway/server-methods/sessions-shared.ts @@ -50,10 +50,18 @@ export function resolveSessionWorkerPlacementMutationError(params: { const placement = params.context.workerSessionPlacementService ?.getMany([params.sessionId]) .get(params.sessionId); + // Failed placement normally keeps destructive mutation fenced. Missing worker identity or an + // authoritative destroyed environment proves cleanup cannot orphan a live worker. + const failedPlacementCanDelete = + params.action === "delete" && + placement?.state === "failed" && + (placement.environmentId === null || + params.context.workerEnvironmentService?.get(placement.environmentId)?.state === "destroyed"); if ( !placement || placement.state === "local" || - (params.action === "delete" && placement.state === "reclaimed") + (params.action === "delete" && placement.state === "reclaimed") || + failedPlacementCanDelete ) { return undefined; } diff --git a/src/gateway/server.sessions.worker-placement-lifecycle.test.ts b/src/gateway/server.sessions.worker-placement-lifecycle.test.ts index 8bb3d1ff02d1..29b75f9a214c 100644 --- a/src/gateway/server.sessions.worker-placement-lifecycle.test.ts +++ b/src/gateway/server.sessions.worker-placement-lifecycle.test.ts @@ -172,7 +172,13 @@ test("sessions.delete rejects failed placement with unresolved worker ownership" "sessions.delete", { key: sessionKey }, { - context: { workerSessionPlacementService: placementReader }, + context: { + workerEnvironmentService: { + get: () => ({ state: "attached" }), + resolveInferenceSessionForRunId: () => undefined, + } as never, + workerSessionPlacementService: placementReader, + }, }, ); @@ -182,6 +188,60 @@ test("sessions.delete rejects failed placement with unresolved worker ownership" expect(embeddedRunMock.abortCalls).toEqual([]); }); +test("sessions.delete allows failed placement after its worker is destroyed", async () => { + await createSessionStoreDir(); + const sessionKey = "discord:group:destroyed-failed-worker-session"; + const sessionId = "sess-destroyed-failed-worker-delete"; + await writeSessionStore({ entries: { [sessionKey]: sessionStoreEntry(sessionId) } }); + const placementReader = sequencedPlacementReader([terminalPlacementRecord(sessionId, "failed")]); + + const deleted = await directSessionReq( + "sessions.delete", + { key: sessionKey }, + { + context: { + workerEnvironmentService: { + get: (environmentId: string) => { + expect(environmentId).toBe("worker-environment"); + return { state: "destroyed" }; + }, + hasInferenceForSession: () => false, + resolveInferenceSessionForRunId: () => undefined, + } as never, + workerSessionPlacementService: placementReader, + }, + }, + ); + + expect(deleted.ok).toBe(true); + expect(deleted.payload).toMatchObject({ ok: true, deleted: true }); + expect(loadSessionEntry(sessionKey).entry).toBeUndefined(); +}); + +test("sessions.delete allows failed placement that never acquired a worker", async () => { + await createSessionStoreDir(); + const sessionKey = "discord:group:unallocated-failed-worker-session"; + const sessionId = "sess-unallocated-failed-worker-delete"; + await writeSessionStore({ entries: { [sessionKey]: sessionStoreEntry(sessionId) } }); + const placement = terminalPlacementRecord(sessionId, "failed"); + if (placement.state !== "failed") { + throw new Error("expected failed placement fixture"); + } + placement.environmentId = null; + + const deleted = await directSessionReq( + "sessions.delete", + { key: sessionKey }, + { + context: { workerSessionPlacementService: sequencedPlacementReader([placement]) }, + }, + ); + + expect(deleted.ok).toBe(true); + expect(deleted.payload).toMatchObject({ ok: true, deleted: true }); + expect(loadSessionEntry(sessionKey).entry).toBeUndefined(); +}); + test("sessions.delete allows reclaimed placement with no live worker owner", async () => { await createSessionStoreDir(); const sessionKey = "discord:group:reclaimed-worker-session";