fix: cloud draft remains stuck after worker teardown (#107759)

* fix: clear destroyed failed cloud drafts

* chore: leave cloud fix notes to release flow
This commit is contained in:
Peter Steinberger
2026-07-14 14:05:10 -07:00
committed by GitHub
parent fd986ed3c2
commit 31cd030f16
2 changed files with 70 additions and 2 deletions
@@ -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;
}
@@ -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";