mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-05 03:51:54 +01:00
Release false recovery holds for legacy dispatch cancellations whose retained evidence proves execution never started. Keep audit history, current admission gates, review-wait Retry, and retained workspace obligations. Share the execution-absence proof across cancellation policies and make the dispatcher error-code policy exhaustive. Add PostgreSQL coverage and stabilize the browser history fixture by waiting for pause cleanup before seeding it. Fixes #15483 Refs #18085 Co-authored-by: commitperclip[bot] <284812584+commitperclip[bot]@users.noreply.github.com> Co-Authored-By: Paperclip <noreply@paperclip.ing>
44 lines
2.9 KiB
TypeScript
44 lines
2.9 KiB
TypeScript
import { and, eq, sql } from "drizzle-orm";
|
|
import { heartbeatRuns, issueRecoveryActions, type Db } from "@paperclipai/db";
|
|
import { canonicalExecutionUuid } from "./execution-identity.js";
|
|
import { noRetainedExecutionPredicate } from "./pre-dispatch-execution-evidence.js";
|
|
import { LEGACY_WORKSPACE_RECOVERY_SCHEMA } from "./workspace-restore-recovery-state.js";
|
|
|
|
/** Admission denied by an existing hold is not a provider attempt. Require the
|
|
* dispatcher's receipt and absence of execution authority, resources and events. */
|
|
function preDispatchExecutionWaitPredicate() {
|
|
return sql`coalesce(
|
|
${heartbeatRuns.runtimeMode} = 'legacy' AND ${heartbeatRuns.status} = 'cancelled'
|
|
AND ${heartbeatRuns.finishedAt} IS NOT NULL
|
|
AND ${heartbeatRuns.errorCode} = 'execution_reconciliation_required'
|
|
AND ${heartbeatRuns.resultJson}->>'stopReason' = 'execution_reconciliation_required'
|
|
AND ${heartbeatRuns.resultJson}->>'timeoutSource' = 'stale_queued_run_gate'
|
|
AND ${canonicalExecutionUuid(sql`${heartbeatRuns.resultJson}->'executionWait'->>'issueId'`)}
|
|
= ${canonicalExecutionUuid(sql`${heartbeatRuns.contextSnapshot}->>'issueId'`)}
|
|
AND (${heartbeatRuns.issueId} IS NULL OR ${heartbeatRuns.issueId}
|
|
= ${canonicalExecutionUuid(sql`${heartbeatRuns.contextSnapshot}->>'issueId'`)})
|
|
AND ${canonicalExecutionUuid(sql`${heartbeatRuns.resultJson}->'executionWait'->>'recoveryActionId'`)} IS NOT NULL
|
|
AND ${noRetainedExecutionPredicate()}, false)`;
|
|
}
|
|
|
|
export async function isPreDispatchExecutionWaitVerified(db: Db, run: typeof heartbeatRuns.$inferSelect): Promise<boolean> {
|
|
if (run.errorCode !== "execution_reconciliation_required" || run.runtimeMode !== "legacy") return false;
|
|
const [verified] = await db.select({ id: heartbeatRuns.id }).from(heartbeatRuns).where(and(
|
|
eq(heartbeatRuns.companyId, run.companyId), eq(heartbeatRuns.id, run.id), preDispatchExecutionWaitPredicate(),
|
|
)).limit(1);
|
|
return Boolean(verified);
|
|
}
|
|
|
|
/** Re-evaluate old false incidents with the same proof; retain their audit rows
|
|
* and leave the original execution/workspace hold to its own recovery policy. */
|
|
export function preDispatchExecutionWaitRecoveryActionPredicate() {
|
|
return sql`(${issueRecoveryActions.cause} = 'legacy_execution_requires_reconciliation'
|
|
AND coalesce(${issueRecoveryActions.evidence}->>'workspaceRestoreFailure', '') <> 'restore_unsafe_archive'
|
|
AND coalesce(${issueRecoveryActions.evidence}->'workspaceRestoreRecovery'->>'schema', '') <> ${LEGACY_WORKSPACE_RECOVERY_SCHEMA}
|
|
AND EXISTS (SELECT 1 FROM ${heartbeatRuns}
|
|
WHERE ${heartbeatRuns.companyId} = ${issueRecoveryActions.companyId}
|
|
AND ${heartbeatRuns.id} = ${canonicalExecutionUuid(sql`coalesce(${issueRecoveryActions.evidence}->>'runId', ${issueRecoveryActions.evidence}->>'sourceRunId')`)}
|
|
AND ${issueRecoveryActions.sourceIssueId} = ${canonicalExecutionUuid(sql`${heartbeatRuns.contextSnapshot}->>'issueId'`)}
|
|
AND ${preDispatchExecutionWaitPredicate()}))`;
|
|
}
|