mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01:00
Read current native Stop identity, acknowledgement, and process evidence without saved execution inputs, profiles, excerpts, or unrelated result data. Preserve the complete nested acknowledgement and every existing lease, remote-termination, process, ownership, and admission check. The physical-stop helper body is unchanged. Verified with 101 tests across the affected suites, the full build, and typecheck. The review fix passed 62 focused tests. Large valid saved inputs stay below 2 KB at the raw driver boundary; large malformed string and array results stay below 1 KB and remain stored. CI is green and exact-head Greptile is 5/5 at Medium impact with no unresolved comments. Co-Authored-By: Paperclip <noreply@paperclip.ing>
56 lines
3.3 KiB
TypeScript
56 lines
3.3 KiB
TypeScript
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<string, unknown> | null;
|
|
}): boolean {
|
|
const result = run.resultJson;
|
|
const intent = result?.nativeCancellation as Record<string, unknown> | 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<typeof hasAcknowledgedNativeRunStopReceipt>[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<typeof hasAcknowledgedNativeRunStopReceipt>[0]): boolean {
|
|
return run.resultJson?.reassignmentStopRequested === true && hasAcknowledgedNativeRunStopReceipt(run);
|
|
}
|
|
|
|
export function isAcknowledgedNativeReassignmentStop(run: Parameters<typeof hasAcknowledgedNativeRunStopReceipt>[0]): boolean {
|
|
return run.status === "cancelled" && hasAcknowledgedNativeReassignmentStopIntent(run);
|
|
}
|
|
|
|
export function isAcknowledgedNativeStop(run: Parameters<typeof hasAcknowledgedNativeStopIntent>[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: Pick<typeof heartbeatRuns.$inferSelect,
|
|
"id" | "companyId" | "nativeIssueId" | "status" | "resultJson" | "processPid" | "processGroupId">) {
|
|
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));
|
|
}
|