From fc661a36477c93eedb1e4ae2179314e0964bdfe4 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 6 Oct 2026 16:36:56 -0500 Subject: [PATCH] Clean failed reusable leases when issues and workspaces close Include failed reusable leases in existing company-scoped closure cleanup. Preserve live-run protection. Validate both closure scopes and the company boundary with 282 environment tests and the server typecheck. Record the remaining build, qualification, browser CI, and review gates. Co-Authored-By: Paperclip --- .../2026-10-02-pi-production-readiness.md | 33 +++++++++++++++++++ .../src/__tests__/environment-runtime.test.ts | 25 +++++++++++--- server/src/services/environment-runtime.ts | 2 +- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/doc/plans/2026-10-02-pi-production-readiness.md b/doc/plans/2026-10-02-pi-production-readiness.md index cde411f4462..0589ea48689 100644 --- a/doc/plans/2026-10-02-pi-production-readiness.md +++ b/doc/plans/2026-10-02-pi-production-readiness.md @@ -40,6 +40,39 @@ The memory, steering and Runner terminal/tool-call failures still need their own proven corrections; this cleanup change does not authorize their retries. All 26 Product and seven Runner cases remain required on the final set. +Review also identifies the issue/workspace closure path, which omitted failed +reusable leases. The follow-up includes them there while retaining company and +issue/workspace scope and the live-run check. All 282 environment tests pass, +including failed-lease closure for each scope and rejection of another company. +The complete server typecheck passes with all corrected services and tests. +The corrected-suite log hash is +`9b965b9cf0acc1dc49340f85d4efb5588f6f59b3bc2522df7ec96d3bb515443a`. +The first filtered review regression reaches its unchanged database startup +hook timeout before any test executes. That failure stays preserved and its +owned Postgres process is independently confirmed absent. It proves no semantic +regression result. + +Two unchanged pinned-runtime controls also pass with no provider credentials: +native agent-file write/read preserves the exact final LF, and serialized RPC +warm recovery preserves that memory across both sessions and reaches both +terminal turns at low thinking. The synthetic server supplies six requests; +external socket connections are denied before connect. The log hash is +`b902ea76b2d0ee377dc28c42e7fe96d91e9e92671060380fe2dd87901b801797`. +These controls narrow the failing live memory/terminal investigation. They do +not attest its missing raw write arguments or regrade either live failure. + +The first cleanup revision `3f6b11a1e` passes full build, recursive typecheck and +Product harness typecheck. Its qualification-only Linux image is built and +published at +`ghcr.io/paperclipai/paperclip-daytona-runner@sha256:ab05e949af8ccff1a1f94317272ce5d934398a9548b6197de273bfb942bf74a4`. +The issue/workspace follow-up changes shipping inputs again, so that intermediate +image is historical and is not admitted or qualified. Preserve its receipts. +No new paid attempts run in either correction. Latest-head CI and review remain +required. The first cleanup head fails its connection-intent browser scenario: +its continuation reaches `cancelled` where the unchanged test requires +`succeeded`. Keep that failure and inspect the actual run before changing code. + + ## Linux process-birth correction — 2026-10-06 The prior-set local pending-question restart failed before controller shutdown because the diff --git a/server/src/__tests__/environment-runtime.test.ts b/server/src/__tests__/environment-runtime.test.ts index 14369ec4018..1943b9ee392 100644 --- a/server/src/__tests__/environment-runtime.test.ts +++ b/server/src/__tests__/environment-runtime.test.ts @@ -6662,13 +6662,24 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { }); }); - it("destroys scoped reusable plugin-backed sandbox leases", async () => { + it.each([ + { status: "active", scope: "workspace" }, + { status: "failed", scope: "workspace" }, + { status: "failed", scope: "issue" }, + ] as const)("destroys $status reusable plugin-backed sandbox leases scoped to an $scope", async ({ status, scope }) => { const { pluginId, companyId, runId, executionWorkspaceId, reusableLease } = await seedReusablePluginSandboxLease(); await db .update(heartbeatRuns) - .set({ status: "succeeded" }) + .set({ status: status === "failed" ? "failed" : "succeeded" }) .where(eq(heartbeatRuns.id, runId)); + if (status === "failed") await environmentService(db).releaseLease(reusableLease.id, "failed"); + const issueId = randomUUID(); + if (scope === "issue") { + await db.insert(issues).values({ id: issueId, companyId, title: "Failed reusable lease cleanup", status: "done", priority: "medium" }); + await db.update(environmentLeases).set({ issueId }).where(eq(environmentLeases.id, reusableLease.id)); + } + const selection = scope === "issue" ? { issueId } : { executionWorkspaceId }; const workerManager = { isRunning: vi.fn((id: string) => id === pluginId), @@ -6682,9 +6693,12 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { } as unknown as PluginWorkerManager; const runtimeWithPlugin = environmentRuntimeService(db, { pluginWorkerManager: workerManager }); + // A known issue/workspace id does not authorize another company's teardown. + expect(await runtimeWithPlugin.destroyReusableSandboxLeases({ companyId: randomUUID(), ...selection })).toEqual([]); + expect(workerManager.call).not.toHaveBeenCalled(); const destroyed = await runtimeWithPlugin.destroyReusableSandboxLeases({ companyId, - executionWorkspaceId, + ...selection, failureReason: "execution_workspace_closed", }); @@ -6707,9 +6721,10 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { }); }); - it("does not destroy a scoped reusable lease while its run is active", async () => { + it.each(["active", "failed"] as const)("does not destroy a %s scoped reusable lease while its run is active", async (status) => { const { pluginId, companyId, executionWorkspaceId, reusableLease } = await seedReusablePluginSandboxLease(); + if (status === "failed") await environmentService(db).releaseLease(reusableLease.id, "failed"); const workerManager = { isRunning: vi.fn((id: string) => id === pluginId), call: vi.fn(async () => undefined), @@ -6735,7 +6750,7 @@ describeEmbeddedPostgres("environmentRuntimeService", () => { expect(workerManager.call).not.toHaveBeenCalled(); await expect( environmentService(db).getLeaseById(reusableLease.id), - ).resolves.toMatchObject({ status: "active" }); + ).resolves.toMatchObject({ status }); }); it("destroys reusable plugin-backed sandbox leases scoped to an environment", async () => { diff --git a/server/src/services/environment-runtime.ts b/server/src/services/environment-runtime.ts index 7e473c7dfe0..73a11790885 100644 --- a/server/src/services/environment-runtime.ts +++ b/server/src/services/environment-runtime.ts @@ -4161,7 +4161,7 @@ export function environmentRuntimeService( and( eq(environmentLeases.companyId, input.companyId), eq(environmentLeases.leasePolicy, "reuse_by_environment"), - inArray(environmentLeases.status, ["active", "released", "retained", "pending_cleanup"]), + inArray(environmentLeases.status, ["active", "released", "retained", "failed", "pending_cleanup"]), ...scopeConditions, ), );