/** * apps/worker — automation executor error types + classification (TASK-0112 §4). * * Every error an executor throws is classified retryable-vs-fatal. The run * pipeline (TASK-0113) consumes `retryable` only: * - FatalError → the run is marked FAILED immediately (no retry): * validation, permission, 4xx, ACTION_TIMEOUT, unknown * action/property, stubs, quota, SSRF. * - RetryableError → the whole run is re-enqueued (BullMQ attempts, max 8): * network I/O (ECONNRESET/ETIMEDOUT/EPIPE), HTTP 429/5xx, * Postgres transient (40P01 deadlock, 40001 serialization), * email-queue-full. * * `classifyError` is the single mapping the pipeline uses; each executor * declares its own classification by throwing the matching class (the HTTP * executor classifies by status code, the DB executors by Prisma error code). */ export class FatalError extends Error { constructor( public readonly code: string, message: string ) { super(message); this.name = "FatalError"; } } export class RetryableError extends Error { constructor( public readonly code: string, message: string ) { super(message); this.name = "RetryableError"; } } /** node errno codes that mean "the transport hiccuped — retry the call". */ const RETRYABLE_ERRNO = new Set(["ECONNRESET", "ETIMEDOUT", "EPIPE", "ECONNREFUSED", "EAI_AGAIN"]); /** Postgres SQLSTATE codes that are transient (safe to retry the statement). */ const RETRYABLE_PG = new Set(["40P01", "40001", "57P01", "55P03", "57014"]); /** * Map any thrown error to { retryable, code }. * - FatalError / RetryableError: use their declared code, * - Prisma errors: retryable only on the transient SQLSTATE set, * - network errors with a retryable errno → retryable, * - otherwise fatal (fail closed — an unknown error must not silently retry). */ export function classifyError(err: unknown): { retryable: boolean; code: string } { if (err instanceof FatalError) return { retryable: false, code: err.code }; if (err instanceof RetryableError) return { retryable: true, code: err.code }; const e = err as { code?: string; errno?: string; cause?: { errno?: string }; meta?: { driverError?: { code?: string } } }; const pgCode = e?.meta?.driverError?.code ?? (typeof e?.code === "string" && RETRYABLE_PG.has(e.code) ? e.code : undefined); if (pgCode && RETRYABLE_PG.has(pgCode)) return { retryable: true, code: `DB_TRANSIENT_${pgCode}` }; const errno = e?.errno ?? e?.cause?.errno; if (errno && RETRYABLE_ERRNO.has(errno)) return { retryable: true, code: `NET_${errno}` }; if (typeof e?.code === "string" && RETRYABLE_ERRNO.has(e.code)) return { retryable: true, code: `NET_${e.code}` }; return { retryable: false, code: (err as Error)?.name === "TimeoutError" ? "ACTION_TIMEOUT" : "UNEXPECTED" }; }