From ab651f8d39edaa4ddf6906e837fcf9e3dfccd246 Mon Sep 17 00:00:00 2001 From: Deepshekhar Das <144223923+deepshekhardas@users.noreply.github.com> Date: Sun, 24 May 2026 17:46:44 +0530 Subject: [PATCH] fix(core): retry TASK_MIDDLEWARE_ERROR under the task's retry policy --- .changeset/retry-middleware-errors.md | 5 +++++ packages/core/src/v3/errors.ts | 13 ++++++++----- packages/core/test/errors.test.ts | 6 ++++++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 .changeset/retry-middleware-errors.md diff --git a/.changeset/retry-middleware-errors.md b/.changeset/retry-middleware-errors.md new file mode 100644 index 00000000000..bed1752d077 --- /dev/null +++ b/.changeset/retry-middleware-errors.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/core": patch +--- + +Retry TASK_MIDDLEWARE_ERROR under the task's retry policy instead of failing the run on the first attempt. The error was already classified as retryable by shouldRetryError, but shouldLookupRetrySettings did not include it, so the retry flow fell through to fail_run. Fixes #3231. \ No newline at end of file diff --git a/packages/core/src/v3/errors.ts b/packages/core/src/v3/errors.ts index d32e32f91c9..c6c6732b2b2 100644 --- a/packages/core/src/v3/errors.ts +++ b/packages/core/src/v3/errors.ts @@ -320,7 +320,7 @@ export function sanitizeError(error: TaskRunError): TaskRunError { case "CUSTOM_ERROR": { // CUSTOM_ERROR.raw holds JSON.stringify(error) which is later parsed by // JSON.parse in createErrorTaskError. Naive truncation would cut mid-token - // and produce invalid JSON — wrap the preview in a valid JSON envelope. + // and produce invalid JSON wrap the preview in a valid JSON envelope. const clean = error.raw.replace(/\0/g, ""); const safeRaw = clean.length > MAX_MESSAGE_LENGTH @@ -332,7 +332,7 @@ export function sanitizeError(error: TaskRunError): TaskRunError { }; } case "INTERNAL_ERROR": { - // message and stackTrace are optional for INTERNAL_ERROR — preserve + // message and stackTrace are optional for INTERNAL_ERROR preserve // `undefined` so the `error.message ?? "Internal error (CODE)"` fallback // in createErrorTaskError still kicks in (empty string is not nullish). return { @@ -429,6 +429,9 @@ export function shouldLookupRetrySettings(error: TaskRunError): boolean { case "TASK_RUN_UNCAUGHT_EXCEPTION": return true; + case "TASK_MIDDLEWARE_ERROR": + return true; + default: return false; } @@ -641,7 +644,7 @@ export class ChatChunkTooLargeError extends Error { `chat.agent chunk${chunkType ? ` of type "${chunkType}"` : ""} is ${chunkSize} bytes, ` + `over the realtime stream's per-record cap of ${maxSize} bytes. ` + `For oversized payloads (e.g. large tool outputs), write the value to your own store and ` + - `emit only an id/url through the chat stream — see https://trigger.dev/docs/ai-chat/patterns/large-payloads.` + `emit only an id/url through the chat stream see https://trigger.dev/docs/ai-chat/patterns/large-payloads.` ); this.name = "ChatChunkTooLargeError"; } @@ -744,7 +747,7 @@ const prettyInternalErrors: Partial< href: links.docs.troubleshooting.stalledExecution, }, }, - // Link only — we deliberately do NOT set `message`, so the original + // Link only we deliberately do NOT set `message`, so the original // error message (e.g. "read ECONNRESET") is preserved in the dashboard. // Common cause: an EventEmitter (node-redis, pg, etc.) emitted "error" // with no listener attached, which Node escalates to uncaughtException. @@ -1152,7 +1155,7 @@ export function createTaskMetadataFailedErrorStack( } stack.push("\n"); - stack.push(` ❯ ${taskWithIssues.exportName} in ${taskWithIssues.filePath}`); + stack.push(` ? ${taskWithIssues.exportName} in ${taskWithIssues.filePath}`); for (const issue of taskWithIssues.issues) { if (issue.path) { diff --git a/packages/core/test/errors.test.ts b/packages/core/test/errors.test.ts index 9a94366d845..40e53df3981 100644 --- a/packages/core/test/errors.test.ts +++ b/packages/core/test/errors.test.ts @@ -263,6 +263,12 @@ describe("shouldRetryError + shouldLookupRetrySettings", () => { expect(shouldLookupRetrySettings(err)).toBe(true); }); + it("retries TASK_MIDDLEWARE_ERROR using the task's retry settings", () => { + const err = internal("TASK_MIDDLEWARE_ERROR"); + expect(shouldRetryError(err)).toBe(true); + expect(shouldLookupRetrySettings(err)).toBe(true); + }); + it("still does not retry SIGKILL timeout", () => { expect(shouldRetryError(internal("TASK_PROCESS_SIGKILL_TIMEOUT"))).toBe(false); });