import { and, eq } from "drizzle-orm"; import { environmentLeases, heartbeatRuns, type Db } from "@paperclipai/db"; import { hasNativeLocalProcessStop } from "./native-local-process-stop.js"; import { hasRemoteTerminationReceipt } from "./remote-execution-termination.js"; /** A server-recorded run-only Stop must not manufacture a recovery incident. */ function hasAcknowledgedNativeRunStopReceipt(run: { id: string; companyId: string; status: string; nativeIssueId: string | null; resultJson: Record | null; }): boolean { const result = run.resultJson; const intent = result?.nativeCancellation as Record | undefined; return intent?.schema === "paperclip.native-cancellation.v1" && intent.runId === run.id && intent.companyId === run.companyId && intent.issueId === run.nativeIssueId && intent.scope === "run" && intent.reasonCode === "cancellation_run_only" && intent.dispatchState === "acknowledged" && intent.dispatched === true && typeof intent.intentAuditId === "string" && typeof intent.acknowledgementAuditId === "string"; } export function hasAcknowledgedNativeStopIntent(run: Parameters[0]): boolean { return run.resultJson?.cancelledByActorType === "user" && typeof run.resultJson.cancelledByUserId === "string" && Boolean(run.resultJson.cancelledByUserId) && hasAcknowledgedNativeRunStopReceipt(run); } /** An intentional handoff retains task state and deferred work for its successor. */ export function hasAcknowledgedNativeReassignmentStopIntent(run: Parameters[0]): boolean { return run.resultJson?.reassignmentStopRequested === true && hasAcknowledgedNativeRunStopReceipt(run); } export function isAcknowledgedNativeReassignmentStop(run: Parameters[0]): boolean { return run.status === "cancelled" && hasAcknowledgedNativeReassignmentStopIntent(run); } export function isAcknowledgedNativeStop(run: Parameters[0]): boolean { return run.status === "cancelled" && hasAcknowledgedNativeStopIntent(run); } /** A Stop acknowledgement alone does not prove provider cleanup completed. */ export async function acknowledgedNativeStopExecutionHasStopped(db: Db, run: typeof heartbeatRuns.$inferSelect) { if (!isAcknowledgedNativeStop(run)) return false; const leases = await db.select().from(environmentLeases).where(and( eq(environmentLeases.companyId, run.companyId), eq(environmentLeases.heartbeatRunId, run.id), )); // Provider PIDs are meaningful only on their own host. if (leases.some(lease => lease.provider !== "local")) return leases.every(hasRemoteTerminationReceipt); if (leases.some(lease => !lease.releasedAt || lease.cleanupStatus === "failed")) return false; if (!run.processPid && !run.processGroupId) return hasNativeLocalProcessStop(db, run.companyId, run.id); const absent = (pid: number) => { try { process.kill(pid, 0); return false; } catch (error) { return (error as NodeJS.ErrnoException).code === "ESRCH"; } }; return (!run.processPid || absent(run.processPid)) && (!run.processGroupId || absent(-run.processGroupId)); }