Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/retry-middleware-errors.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 8 additions & 5 deletions packages/core/src/v3/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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.`
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: User-facing error text contains a garbled replacement character (), which degrades message readability.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/src/v3/errors.ts, line 647:

<comment>User-facing error text contains a garbled replacement character (`�`), which degrades message readability.</comment>

<file context>
@@ -641,7 +644,7 @@ export class ChatChunkTooLargeError extends Error {
         `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";
</file context>
Suggested change
`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";
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/test/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
Loading