Files
PaperClipAI/server/src/services/native-runtime/native-workspace-finalization-state.ts
T
DottaandPaperclip f5534717b0 fix(runtime): preserve unexported native work during lease cleanup (#16583)
Preserve the exact native workspace source until its export is finalized. Apply the same guard to cleanup sweeps and direct environment teardown. Keep stop-only recovery and healthy warm retention available.

Verified with 317 targeted tests, server typecheck, module boundary check, all current-head CI gates, and Greptile 5/5.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-16 12:56:09 -05:00

56 lines
3.1 KiB
TypeScript

import { environmentLeases, heartbeatRuns, nativeRunFinalizations, nativeRunResults, type Db } from "@paperclipai/db";
import { and, eq, exists, sql } from "drizzle-orm";
import { nativeRunHasNoWorkspaceAccess } from "../workspace-restore-recovery-state.js";
import { readNativeWorkspaceSyncReference } from "./native-workspace-sync.js";
type WorkspaceSourceLease = Pick<typeof environmentLeases.$inferSelect,
"id" | "companyId" | "heartbeatRunId" | "provider" | "providerLeaseId">;
export async function nativeWorkspaceExportPendingForLease(db: Db, lease: WorkspaceSourceLease): Promise<boolean> {
if (!lease.heartbeatRunId || !lease.provider || lease.provider === "local" || !lease.providerLeaseId) return false;
const input = sql`${heartbeatRuns.runnerProfileJson}->'nativeExecutionInput'`;
const [run] = await db.select({
id: heartbeatRuns.id, companyId: heartbeatRuns.companyId, agentId: heartbeatRuns.agentId,
issueId: heartbeatRuns.issueId, nativeIssueId: heartbeatRuns.nativeIssueId,
runtimeMode: heartbeatRuns.runtimeMode, driverKind: heartbeatRuns.driverKind,
reference: sql<unknown>`${heartbeatRuns.runnerProfileJson}->'nativeWorkspaceSync'`,
executionScope: sql<Record<string, unknown>>`jsonb_build_object(
'schema', ${input}->'schema', 'binding', ${input}->'binding', 'workspace', ${input}->'workspace',
'provider', jsonb_build_object('kind', ${input}#>'{provider,kind}'))`,
}).from(heartbeatRuns).where(and(
eq(heartbeatRuns.id, lease.heartbeatRunId), eq(heartbeatRuns.companyId, lease.companyId),
eq(heartbeatRuns.runtimeMode, "native"),
)).limit(1);
if (!run || nativeRunHasNoWorkspaceAccess({ ...run, runnerProfileJson: { nativeExecutionInput: run.executionScope } })) return false;
const reference = readNativeWorkspaceSyncReference(run.reference);
return reference?.state === "prepared" && reference.leaseId === lease.id
&& reference.providerLeaseId === lease.providerLeaseId;
}
/**
* An accepted result transfers recovery to the workspace finalizer. A dead
* provider PID cannot authorize discarding its unfinished workspace suffix,
* including the interval before physical copyback acquires its owner receipt.
* The heartbeat's display phase and arbitrary profile JSON are not evidence.
*/
export function pendingNativeWorkspaceFinalizationCondition(db: Db) {
return and(
eq(heartbeatRuns.runtimeMode, "native"),
exists(db.select({ runId: nativeRunFinalizations.runId })
.from(nativeRunFinalizations)
.innerJoin(nativeRunResults, and(
eq(nativeRunResults.id, nativeRunFinalizations.resultId),
eq(nativeRunResults.runId, nativeRunFinalizations.runId),
eq(nativeRunResults.companyId, nativeRunFinalizations.companyId),
eq(nativeRunResults.issueId, nativeRunFinalizations.issueId),
eq(nativeRunResults.schemaStatus, "accepted"),
))
.where(and(
eq(nativeRunFinalizations.runId, heartbeatRuns.id),
eq(nativeRunFinalizations.companyId, heartbeatRuns.companyId),
eq(nativeRunFinalizations.issueId, heartbeatRuns.nativeIssueId),
eq(nativeRunFinalizations.phase, "workspace_finalizing"),
))),
)!;
}