mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
Keep stable workspace fingerprints compatible with existing task sessions. Bind legacy conversations to verified physical sandboxes, preserve Codex runtime files during config refresh, and retain failed preparations for retry. Authorize new native conversations separately from sandbox reuse without discarding prior state. Co-Authored-By: Paperclip <noreply@paperclip.ing>
40 lines
2.8 KiB
TypeScript
40 lines
2.8 KiB
TypeScript
import { createHash } from "node:crypto";
|
|
import { and, desc, eq, inArray, sql } from "drizzle-orm";
|
|
import { environmentLeases, workFolderRuns, type Db } from "@paperclipai/db";
|
|
|
|
export function workFolderSandboxKey(lease: { id: string; companyId: string; environmentId: string | null; provider: string | null; providerLeaseId: string | null }) {
|
|
return lease.providerLeaseId ? createHash("sha256").update(JSON.stringify([lease.companyId, lease.environmentId, lease.provider, lease.providerLeaseId])).digest("hex") : lease.id;
|
|
}
|
|
|
|
/**
|
|
* True means this physical resource must not be torn down by the caller.
|
|
* A transferred old lease is protected without mutating or resurrecting its row.
|
|
* A periodic checkpoint is not permission to discard edits made after it.
|
|
*/
|
|
export async function retainUnsavedWorkFolderLease(db: Db, lease: { id: string; companyId: string }, options: { runSaveFailed?: boolean } = {}) {
|
|
const [row] = await db.select().from(environmentLeases).where(and(eq(environmentLeases.id, lease.id), eq(environmentLeases.companyId, lease.companyId)));
|
|
if (!row) return false;
|
|
// This old row no longer owns the physical resource. Protect it from stale
|
|
// teardown callers without resurrecting the retired lease or changing its owner.
|
|
if (row.metadata?.reusableLeaseReplacedByRunId) return true;
|
|
const sandboxKey = workFolderSandboxKey(row);
|
|
const [run] = await db.select({ manifest: workFolderRuns.manifest, state: workFolderRuns.state })
|
|
.from(workFolderRuns).where(and(eq(workFolderRuns.companyId, lease.companyId),
|
|
sql`(${workFolderRuns.manifest}->>'sandboxKey' = ${sandboxKey} or ${workFolderRuns.manifest}->>'leaseId' = ${lease.id})`))
|
|
.orderBy(desc(workFolderRuns.updatedAt)).limit(1);
|
|
// A startup failure can precede this run's manifest. The previous run's final
|
|
// save cannot attest that partial hydration or the current working copy is safe.
|
|
if (!options.runSaveFailed && (!run || (run.state === "saved" && run.manifest.finalCheckpointAt))) return false;
|
|
await db.update(environmentLeases).set({ status: "retained", expiresAt: null,
|
|
failureReason: "work_folder_save_required", cleanupStatus: "failed",
|
|
metadata: sql`coalesce(${environmentLeases.metadata}, '{}'::jsonb) || '{"workFolderRecoveryRequired":true}'::jsonb`,
|
|
}).where(and(eq(environmentLeases.id, lease.id), eq(environmentLeases.companyId, lease.companyId),
|
|
inArray(environmentLeases.status, ["active", "released", "retained", "failed", "pending_cleanup", "expired"]),
|
|
sql`not (coalesce(${environmentLeases.metadata}, '{}'::jsonb) ? 'reusableLeaseReplacedByRunId')`,
|
|
sql`${environmentLeases.heartbeatRunId} is not distinct from ${row.heartbeatRunId}`,
|
|
)).returning({ id: environmentLeases.id });
|
|
// Losing the conditional update can mean a new owner won the handoff.
|
|
// The unsaved resource is still protected; never authorize stale teardown.
|
|
return true;
|
|
}
|