From 7207ab16be5d5da51672f84dd567de7f97811acf Mon Sep 17 00:00:00 2001 From: Dotta Date: Fri, 30 Oct 2026 21:41:40 -0500 Subject: [PATCH] fix: retain sandbox attachment run scope for backup cleanup Use the locked creation operation run identity when notifying environment cleanup. The existing backup dependency index can then reconcile one affected session and ignore unrelated company roots, while nested lease acquisition hints coalesce. Co-Authored-By: OpenAI Codex --- .../native-harness-backup-expiration.test.ts | 39 +++++++++++++++++++ server/src/services/environments.ts | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/server/src/__tests__/native-harness-backup-expiration.test.ts b/server/src/__tests__/native-harness-backup-expiration.test.ts index 9d16ec519ff..25827def7b5 100644 --- a/server/src/__tests__/native-harness-backup-expiration.test.ts +++ b/server/src/__tests__/native-harness-backup-expiration.test.ts @@ -24,6 +24,8 @@ import { companies, createDb, environmentLeases, + environments, + sandboxCreationOperations, heartbeatRuns, issues, nativeRunFinalizations, @@ -768,6 +770,43 @@ describeEmbeddedPostgres("native harness backup expiration", () => { await quiet(); expect(await exists(own.current)).toBe(true); }); + it.each([false, true])("sandbox attachment reconciles only its retained root using durable run authority (explicit run: %s)", async explicitRun => { + const own = await seedSession({ runStatus: "running" }); + const unrelated = []; + for (let index = 0; index < 16; index++) unrelated.push(await seedSession({ companyId: own.companyId, + agentId: own.agentId, issueStatus: "todo", runStatus: index % 2 ? "running" : "succeeded" })); + const [environment] = await db.insert(environments).values({ name: randomUUID(), driver: "sandbox" }).returning(); + async function attach(runId: string) { + const operationId = randomUUID(); + await db.insert(sandboxCreationOperations).values({ id: operationId, companyId: own.companyId, + environmentId: environment!.id, originalEnvironmentId: environment!.id, runId, + provider: "controlled", driverKey: "controlled", config: {}, metadata: {}, nextAttemptAt: new Date() }); + return environmentService(db).acquireLease({ creationOperationId: operationId, companyId: own.companyId, + environmentId: environment!.id, ...(explicitRun ? { heartbeatRunId: runId } : {}) }); + } + await startBackups(); await settled(); + const reads = vi.spyOn(fs, "readFile"); + try { + const lease = await attach(own.runId); await settled(); + expect(reads.mock.calls.map(call => String(call[0]))) + .toEqual([path.join(own.root, "control-plane", "control-plane-state.json")]); + expect((await db.select().from(sandboxCreationOperations).where(eq(sandboxCreationOperations.leaseId, lease.id)))[0]) + .toMatchObject({ state: "attached", runId: own.runId }); + reads.mockClear(); + const [run] = await db.insert(heartbeatRuns).values({ companyId: own.companyId, agentId: own.agentId, status: "running" }).returning(); + await attach(run!.id); await settled(); + expect(reads).not.toHaveBeenCalled(); + expect(await exists(own.current)).toBe(true); + for (const other of unrelated) expect(await exists(other.current)).toBe(true); + await quiet(); + } finally { + reads.mockRestore(); + await db.delete(sandboxCreationOperations).where(eq(sandboxCreationOperations.companyId, own.companyId)); + await db.delete(environmentLeases).where(eq(environmentLeases.environmentId, environment!.id)); + await db.delete(environments).where(eq(environments.id, environment!.id)); + } + }); + it("indexes protected session relations so a new run, historical task close and exact process release reactivate only its backup", async () => { const own = await seedSession({ closedDaysAgo: 2 }); const other = await seedSession({ companyId: own.companyId, agentId: own.agentId, issueStatus: "todo" }); diff --git a/server/src/services/environments.ts b/server/src/services/environments.ts index a9985370ae8..68fe46ede14 100644 --- a/server/src/services/environments.ts +++ b/server/src/services/environments.ts @@ -68,7 +68,7 @@ export async function attachSandboxCreation(db: Db, id: string, scope: { company } if (!operation || operation.state !== "pending") throw new Error("Sandbox creation ownership has already transferred"); const lease = await attach(tx as unknown as Db); - await notifyEnvironmentCleanup(tx, { companyId: scope.companyId }); + await notifyEnvironmentCleanup(tx, { companyId: scope.companyId, runId: operation.runId }); await tx.update(sandboxCreationOperations).set({ state: "attached", leaseId: lease.id, updatedAt: new Date() }) .where(eq(sandboxCreationOperations.id, id)); return lease;