import { isHardBillingToolHttpError, ToolHttpError } from './tool-http-errors'; import { RuntimeReceiptLeaseLostError } from './durable-receipt-execution'; import { isRuntimePersistenceCircuitOpenError } from './persistence-latch'; import { isWorkspaceStorageNotReadyFailure } from './run-failure'; /** * Thrown by runner Adapters when a Play Run is externally cancelled. Row * isolation must let this escape so in-flight user code stops cooperatively. */ export class WorkflowAbortError extends Error { override readonly name = 'WorkflowAbort'; constructor(message = 'Play run cancelled.') { super(message); } } export function isAbortLikeError(error: unknown): boolean { if (!error) return false; if (error instanceof WorkflowAbortError) return true; if (error instanceof Error) { return error.name === 'WorkflowAbort' || error.name === 'AbortError'; } return false; } /** * Errors that must stay run-fatal even under default map row failure isolation. * * Provider/tool HTTP failures, including exhausted 429/5xx retries, are row * outcomes after the tool-call Adapter spends its local retry budget. Runtime * persistence failures are not row data failures: once receipts or sheet writes * are unreliable, completing the run would hide paid/side-effecting work that * no durable ledger can prove. */ export function isRowIsolationExemptError(error: unknown): boolean { if (isAbortLikeError(error)) return true; // Receipt ownership loss is a control-plane uncertainty, not a provider row // result. Propagate it to the one fenced Play Run task so its normal retry // path can reconcile/replay the durable invocation before any redelivery. if (error instanceof RuntimeReceiptLeaseLostError) return true; // A remote tool can return arbitrary provider/database wording. Its message // must never be mistaken for a failure of the runner's own receipt or sheet // persistence plane. Hard Deepline billing denials are the sole typed tool // failure that intentionally remains run-fatal. if (error instanceof ToolHttpError) { return isHardBillingToolHttpError(error); } const nestedErrors = error && typeof error === 'object' ? (error as { errors?: unknown }).errors : undefined; if ( Array.isArray(nestedErrors) && nestedErrors.some(isRowIsolationExemptError) ) { return true; } if (error instanceof Error && error.name === 'GovernorBudgetError') { return true; } if (isRuntimeReceiptPersistenceError(error)) return true; if (isRuntimeStoragePersistenceError(error)) return true; if (isRuntimePersistenceCircuitOpenError(error)) return true; if (isWorkspaceStorageNotReadyFailure(error)) return true; return isHardBillingToolHttpError(error); } function isRuntimeReceiptPersistenceError(error: unknown): boolean { if (!error || typeof error !== 'object') return false; if ( error instanceof Error && error.name === 'RuntimeReceiptPersistenceError' ) { return true; } const nestedErrors = (error as { errors?: unknown }).errors; return ( Array.isArray(nestedErrors) && nestedErrors.some(isRuntimeReceiptPersistenceError) ); } function isRuntimeStoragePersistenceError(error: unknown): boolean { if (!error || typeof error !== 'object') return false; if (error instanceof ToolHttpError) return false; const nestedErrors = (error as { errors?: unknown }).errors; if (Array.isArray(nestedErrors) && nestedErrors.length > 0) { // AggregateError.message is caller-controlled summary text. Once typed // children exist, classify those children and never fall through to a // misleading container message. return nestedErrors.some(isRuntimeStoragePersistenceError); } const message = error instanceof Error ? error.message : String((error as object) ?? ''); return ( /Runtime Postgres connection timed out/i.test(message) || /\bNeonDbError\b/i.test(message) || /timeout exceeded when trying to connect/i.test(message) || /connection terminated unexpectedly/i.test(message) || /permission denied (?:for|to grant) (?:schema|table|relation|database|sequence|role)\b/i.test( message, ) || /must be owner of (?:table|relation|sequence|schema|database|function|procedure|routine|type)\b/i.test( message, ) ); }