Skip to content
Merged
1 change: 1 addition & 0 deletions apps/webapp/app/env.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,7 @@ const EnvironmentSchema = z.object({
EVENT_LOOP_MONITOR_ENABLED: z.string().default("1"),
MAXIMUM_LIVE_RELOADING_EVENTS: z.coerce.number().int().default(1000),
MAXIMUM_TRACE_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(25_000),
MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT: z.coerce.number().int().default(10_000),
TASK_PAYLOAD_OFFLOAD_THRESHOLD: z.coerce.number().int().default(524_288), // 512KB
TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(3_145_728), // 3MB
BATCH_TASK_PAYLOAD_MAXIMUM_SIZE: z.coerce.number().int().default(1_000_000), // 1MB
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/app/v3/taskEventStore.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ export class TaskEventStore {
: Prisma.empty
}
ORDER BY "startTime" ASC
LIMIT ${env.MAXIMUM_TRACE_SUMMARY_VIEW_COUNT}
LIMIT ${env.MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT}
`;
} else {
return await this.readReplica.$queryRaw<DetailedTraceEvent[]>`
Expand Down Expand Up @@ -320,7 +320,7 @@ export class TaskEventStore {
: Prisma.empty
}
ORDER BY "startTime" ASC
LIMIT ${env.MAXIMUM_TRACE_SUMMARY_VIEW_COUNT}
LIMIT ${env.MAXIMUM_TRACE_DETAILED_SUMMARY_VIEW_COUNT}
`;
}
}
Expand Down
61 changes: 32 additions & 29 deletions packages/cli-v3/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,47 +107,50 @@ export function configureDevCommand(program: Command) {
export async function devCommand(options: DevCommandOptions) {
runtimeChecks();

const skipMCPInstall = typeof options.skipMCPInstall === "boolean" && options.skipMCPInstall;
// Only show these install prompts if the user is in a terminal (not in a Coding Agent)
if (process.stdout.isTTY) {
const skipMCPInstall = typeof options.skipMCPInstall === "boolean" && options.skipMCPInstall;

if (!skipMCPInstall) {
const hasSeenMCPInstallPrompt = readConfigHasSeenMCPInstallPrompt();
if (!skipMCPInstall) {
const hasSeenMCPInstallPrompt = readConfigHasSeenMCPInstallPrompt();

if (!hasSeenMCPInstallPrompt) {
const installChoice = await confirm({
message: "Would you like to install the Trigger.dev MCP server?",
initialValue: true,
});
if (!hasSeenMCPInstallPrompt) {
const installChoice = await confirm({
message: "Would you like to install the Trigger.dev MCP server?",
initialValue: true,
});

writeConfigHasSeenMCPInstallPrompt(true);
writeConfigHasSeenMCPInstallPrompt(true);

const skipInstall = isCancel(installChoice) || !installChoice;
const skipInstall = isCancel(installChoice) || !installChoice;

if (!skipInstall) {
log.step("Welcome to the Trigger.dev MCP server install wizard 🧙");
if (!skipInstall) {
log.step("Welcome to the Trigger.dev MCP server install wizard 🧙");

const [installError] = await tryCatch(
installMcpServer({
yolo: false,
tag: VERSION as string,
logLevel: options.logLevel,
})
);
const [installError] = await tryCatch(
installMcpServer({
yolo: false,
tag: VERSION as string,
logLevel: options.logLevel,
})
);

if (installError) {
log.error(`Failed to install MCP server: ${installError.message}`);
if (installError) {
log.error(`Failed to install MCP server: ${installError.message}`);
}
}
}
}
}

const skipRulesInstall =
typeof options.skipRulesInstall === "boolean" && options.skipRulesInstall;
const skipRulesInstall =
typeof options.skipRulesInstall === "boolean" && options.skipRulesInstall;

if (!skipRulesInstall) {
await initiateRulesInstallWizard({
manifestPath: options.rulesInstallManifestPath,
branch: options.rulesInstallBranch,
});
if (!skipRulesInstall) {
await initiateRulesInstallWizard({
manifestPath: options.rulesInstallManifestPath,
branch: options.rulesInstallBranch,
});
}
}

const authorization = await login({
Expand Down
18 changes: 7 additions & 11 deletions packages/cli-v3/src/commands/install-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,12 @@ export async function installMcpServer(
);

log.info("More examples:");
log.message(` • ${chalk.green('"List my Trigger.dev projects"')}`);
log.message(` • ${chalk.green('"Create a new Trigger.dev project called MyApp"')}`);
log.message(` • ${chalk.green('"Show me all tasks in my project"')}`);
log.message(` • ${chalk.green('"Trigger the email-notification task"')}`);
log.message(` • ${chalk.green('"Trigger the hello-world task"')}`);
log.message(` • ${chalk.green('"Can you help me debug the prod run run_1234"')}`);
log.message(` • ${chalk.green('"Deploy my trigger project to staging"')}`);
log.message(` • ${chalk.green('"What trigger task handles uploading files to S3?"')}`);
log.message(` • ${chalk.green('"How do I create a scheduled task in Trigger.dev?"')}`);
log.message(` • ${chalk.green('"Search Trigger.dev docs for webhook examples"')}`);
log.message(` • ${chalk.green('"Search Trigger.dev docs for ffmpeg examples"')}`);

log.info("Helpful links:");
log.message(` • ${cliLink("Trigger.dev docs", "https://trigger.dev/docs")}`);
Expand Down Expand Up @@ -318,17 +318,13 @@ async function installMcpServerForClient(
return;
}

const clientSpinner = spinner();

clientSpinner.start(`Installing in ${clientName}`);

const scope = await resolveScopeForClient(clientName, options);

clientSpinner.message(`Installing in ${scope.scope} scope at ${scope.location}`);
// clientSpinner.message(`Installing in ${scope.scope} scope at ${scope.location}`);

const configPath = await performInstallForClient(clientName, scope, options);

clientSpinner.stop(`Successfully installed in ${clientName} (${configPath})`);
// clientSpinner.stop(`Successfully installed in ${clientName} (${configPath})`);

return { configPath, clientName, scope };
}
Expand Down
36 changes: 28 additions & 8 deletions packages/cli-v3/src/mcp/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
} from "@trigger.dev/core/v3/schemas";
import type { CursorPageResponse } from "@trigger.dev/core/v3/zodfetch";

const DEFAULT_MAX_TRACE_LINES = 500;

export function formatRun(run: RetrieveRunResponse): string {
const lines: string[] = [];

Expand Down Expand Up @@ -170,23 +172,35 @@ function formatRelatedRuns(relatedRuns: RetrieveRunResponse["relatedRuns"]): str
return parts.length > 0 ? `Related: ${parts.join("; ")}` : null;
}

export function formatRunTrace(trace: RetrieveRunTraceResponseBody["trace"]): string {
export function formatRunTrace(
trace: RetrieveRunTraceResponseBody["trace"],
maxTraceLines: number = DEFAULT_MAX_TRACE_LINES
): string {
const lines: string[] = [];

lines.push(`Trace ID: ${trace.traceId}`);
lines.push("");

// Format the root span and its children recursively
formatSpan(trace.rootSpan, lines, 0);
const reachedMaxLines = formatSpan(trace.rootSpan, lines, 0, maxTraceLines);

if (reachedMaxLines) {
lines.push(`(truncated logs to ${maxTraceLines} lines)`);
}

return lines.join("\n");
}
Comment on lines +175 to 192
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Line-cap can be exceeded mid-span; enforce a hard cap and reserve room for the notice

Currently, a single large span can push many lines before the recursive call returns, resulting in outputs that exceed the intended cap. Enforce the cap after formatting and ensure the truncation notice is included deterministically.

Apply this diff:

 export function formatRunTrace(
   trace: RetrieveRunTraceResponseBody["trace"],
   maxTraceLines: number = DEFAULT_MAX_TRACE_LINES
 ): string {
   const lines: string[] = [];

   lines.push(`Trace ID: ${trace.traceId}`);
   lines.push("");

   // Format the root span and its children recursively
-  const reachedMaxLines = formatSpan(trace.rootSpan, lines, 0, maxTraceLines);
-
-  if (reachedMaxLines) {
-    lines.push(`(truncated logs to ${maxTraceLines} lines)`);
-  }
+  const reachedMaxLines = formatSpan(trace.rootSpan, lines, 0, maxTraceLines);
+
+  // Enforce a hard cap and include the truncation notice within the cap
+  const truncated = reachedMaxLines || lines.length > maxTraceLines;
+  if (truncated) {
+    if (lines.length >= maxTraceLines) {
+      // leave room for the truncation notice line
+      lines.length = Math.max(0, maxTraceLines - 1);
+    }
+    lines.push(`(truncated logs to ${maxTraceLines} lines)`);
+  }

   return lines.join("\n");
 }

As an optional follow-up, we can short-circuit earlier inside formatSpan after heavy sections (properties/output/events) to avoid extra work. Happy to provide that refinement if you want it.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function formatRunTrace(
trace: RetrieveRunTraceResponseBody["trace"],
maxTraceLines: number = DEFAULT_MAX_TRACE_LINES
): string {
const lines: string[] = [];
lines.push(`Trace ID: ${trace.traceId}`);
lines.push("");
// Format the root span and its children recursively
formatSpan(trace.rootSpan, lines, 0);
const reachedMaxLines = formatSpan(trace.rootSpan, lines, 0, maxTraceLines);
if (reachedMaxLines) {
lines.push(`(truncated logs to ${maxTraceLines} lines)`);
}
return lines.join("\n");
}
export function formatRunTrace(
trace: RetrieveRunTraceResponseBody["trace"],
maxTraceLines: number = DEFAULT_MAX_TRACE_LINES
): string {
const lines: string[] = [];
lines.push(`Trace ID: ${trace.traceId}`);
lines.push("");
// Format the root span and its children recursively
const reachedMaxLines = formatSpan(trace.rootSpan, lines, 0, maxTraceLines);
// Enforce a hard cap and include the truncation notice within the cap
const truncated = reachedMaxLines || lines.length > maxTraceLines;
if (truncated) {
if (lines.length >= maxTraceLines) {
// leave room for the truncation notice line
lines.length = Math.max(0, maxTraceLines - 1);
}
lines.push(`(truncated logs to ${maxTraceLines} lines)`);
}
return lines.join("\n");
}
🤖 Prompt for AI Agents
In packages/cli-v3/src/mcp/formatters.ts around lines 175 to 192, a single large
span can produce more lines than maxTraceLines because formatting happens
recursively without enforcing a hard cap or reserving space for the truncation
notice; after building the full lines array (or after formatSpan returns)
truncate the lines to at most (maxTraceLines - 1) entries to reserve one line
for the notice, detect if truncation occurred, and if so replace the tail with
the reserved notice "(truncated logs to X lines)"; ensure the final returned
string always respects the maxTraceLines limit and deterministically includes
the notice when any truncation happened.


function formatSpan(
span: RetrieveRunTraceResponseBody["trace"]["rootSpan"],
lines: string[],
depth: number
): void {
depth: number,
maxLines: number
): boolean {
if (lines.length >= maxLines) {
return true;
}

const indent = " ".repeat(depth);
const prefix = depth === 0 ? "└─" : "├─";

Expand Down Expand Up @@ -230,7 +244,7 @@ function formatSpan(
}

// Show output if it exists
if (span.data.output && Object.keys(span.data.output).length > 0) {
if (span.data.output) {
lines.push(
`${indent} Output: ${JSON.stringify(span.data.output, null, 2).replace(
/\n/g,
Expand Down Expand Up @@ -263,14 +277,20 @@ function formatSpan(

// Recursively format children
if (span.children) {
span.children.forEach((child, index) => {
formatSpan(child, lines, depth + 1);
const reachedMaxLines = span.children.some((child, index) => {
const reachedMaxLines = formatSpan(child, lines, depth + 1, maxLines);
// Add spacing between sibling spans (except for the last one)
if (index < span.children.length - 1) {
if (index < span.children.length - 1 && !reachedMaxLines) {
lines.push("");
}

return reachedMaxLines;
});

return reachedMaxLines;
}

return false;
}

function getStatusIndicator(
Expand Down
8 changes: 7 additions & 1 deletion packages/cli-v3/src/mcp/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,13 @@ export const CommonRunsInput = CommonProjectsInput.extend({

export type CommonRunsInput = z.output<typeof CommonRunsInput>;

export const GetRunDetailsInput = CommonRunsInput.extend({});
export const GetRunDetailsInput = CommonRunsInput.extend({
maxTraceLines: z
.number()
.int()
.describe("The maximum number of lines to show in the trace. Defaults to 500")
.optional(),
});
Comment on lines +126 to +132
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Constrain and coerce maxTraceLines to safe bounds

Good addition. To prevent pathological inputs (negative, zero, NaN, huge), coerce and bound the value. Also clarify the described default range.

Apply this diff:

 export const GetRunDetailsInput = CommonRunsInput.extend({
-  maxTraceLines: z
-    .number()
-    .int()
-    .describe("The maximum number of lines to show in the trace. Defaults to 500")
-    .optional(),
+  maxTraceLines: z
+    .coerce.number()
+    .int()
+    .min(1)
+    .max(10000)
+    .describe("The maximum number of lines to show in the trace. Defaults to 500 (range: 1–10,000)")
+    .optional(),
 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const GetRunDetailsInput = CommonRunsInput.extend({
maxTraceLines: z
.number()
.int()
.describe("The maximum number of lines to show in the trace. Defaults to 500")
.optional(),
});
export const GetRunDetailsInput = CommonRunsInput.extend({
maxTraceLines: z
.coerce.number()
.int()
.min(1)
.max(10000)
.describe("The maximum number of lines to show in the trace. Defaults to 500 (range: 1–10,000)")
.optional(),
});
🤖 Prompt for AI Agents
In packages/cli-v3/src/mcp/schemas.ts around lines 126 to 132, the maxTraceLines
schema currently allows pathological inputs; change it to coerce and constrain
the value (e.g. use z.coerce.number().int() with a sensible min and max such as
.min(1).max(10000) and set .default(500) so missing values get the default), and
update the .describe text to state the enforced range and default (e.g. "The
maximum number of lines to show in the trace; coerced to an integer between 1
and 10000, defaults to 500"). Ensure the schema still accepts omitted values by
using .default(500) (or .optional() with a transform to default) and rejects
NaN, non-numeric strings, zero, negatives, and excessively large numbers.


export type GetRunDetailsInput = z.output<typeof GetRunDetailsInput>;

Expand Down
1 change: 0 additions & 1 deletion packages/cli-v3/src/mcp/tools/deploys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ export const deployTool = {
cwd: cwd.cwd,
env: {
TRIGGER_MCP_SERVER: "1",
CI: "true",
},
},
});
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-v3/src/mcp/tools/runs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const getRunDetailsTool = {
]);

const formattedRun = formatRun(runResult);
const formattedTrace = formatRunTrace(traceResult.trace);
const formattedTrace = formatRunTrace(traceResult.trace, input.maxTraceLines);

Comment on lines +38 to 39
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Clamp user-supplied maxTraceLines before passing to formatter

Even with Zod validation, clamping at the call site makes the behavior robust and aligns with the schema bounds.

Apply this diff:

-    const formattedTrace = formatRunTrace(traceResult.trace, input.maxTraceLines);
+    const maxTraceLines =
+      typeof input.maxTraceLines === "number" && Number.isFinite(input.maxTraceLines)
+        ? Math.min(Math.max(Math.trunc(input.maxTraceLines), 1), 10_000)
+        : undefined;
+    const formattedTrace = formatRunTrace(traceResult.trace, maxTraceLines);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const formattedTrace = formatRunTrace(traceResult.trace, input.maxTraceLines);
const maxTraceLines =
typeof input.maxTraceLines === "number" && Number.isFinite(input.maxTraceLines)
? Math.min(Math.max(Math.trunc(input.maxTraceLines), 1), 10_000)
: undefined;
const formattedTrace = formatRunTrace(traceResult.trace, maxTraceLines);
🤖 Prompt for AI Agents
packages/cli-v3/src/mcp/tools/runs.ts lines 38-39: clamp input.maxTraceLines
before passing to formatRunTrace by parsing it to a number, bounding it with
Math.max/Math.min against the schema limits (e.g., min 0 and the schema's
MAX_TRACE_LINES constant or value), assign to a local variable like
clampedMaxTraceLines and pass that to formatRunTrace so the formatter always
receives a value within allowed bounds.

const runUrl = await ctx.getDashboardUrl(`/projects/v3/${projectRef}/runs/${runResult.id}`);

Expand Down
2 changes: 1 addition & 1 deletion packages/cli-v3/src/rules/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export class GithubRulesManifestLoader implements RulesManifestLoader {

async loadRulesFile(relativePath: string): Promise<string> {
const response = await fetch(
`https://raw.githubusercontent.com/triggerdotdev/trigger.dev/refs/heads/${this.branch}/${relativePath}`
`https://raw.githubusercontent.com/triggerdotdev/trigger.dev/refs/heads/${this.branch}/rules/${relativePath}`
);

if (!response.ok) {
Expand Down
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@
"execa": "^8.0.1",
"humanize-duration": "^3.27.3",
"jose": "^5.4.0",
"lodash.get": "^4.4.2",
"nanoid": "3.3.8",
"prom-client": "^15.1.0",
"socket.io": "4.7.4",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/v3/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,7 @@ export const RetrieveRunTraceSpanSchema = z.object({
runId: z.string(),
taskSlug: z.string().optional(),
taskPath: z.string().optional(),
events: z.array(z.any()),
events: z.array(z.any()).optional(),
startTime: z.coerce.date(),
duration: z.number(),
isError: z.boolean(),
Expand All @@ -1245,7 +1245,7 @@ export const RetrieveRunTraceSpanSchema = z.object({
queueName: z.string().optional(),
machinePreset: z.string().optional(),
properties: z.record(z.any()).optional(),
output: z.record(z.any()).optional(),
output: z.unknown().optional(),
}),
});

Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/v3/utils/ioSerialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { SemanticInternalAttributes } from "../semanticInternalAttributes.js";
import { TriggerTracer } from "../tracer.js";
import { zodfetch } from "../zodfetch.js";
import { flattenAttributes } from "./flattenAttributes.js";
import get from "lodash.get";
import { JSONHeroPath } from "@jsonhero/path";

export type IOPacket = {
data?: string | undefined;
Expand Down Expand Up @@ -536,7 +536,7 @@ export async function replaceSuperJsonPayload(original: string, newPayload: stri
.map(([key]) => key);

const overridenUndefinedKeys = originalUndefinedKeys.filter(
(key) => get(newPayloadObject, key) !== undefined
(key) => getKeyFromObject(newPayloadObject, key) !== undefined
);

Comment on lines 538 to 541
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: Path lookup returns a Node object, so !== undefined becomes true even when the value is undefined

getKeyFromObject() currently returns the result of jsonHeroPath.first(object), which (per library conventions) is a node wrapper, not the raw value. If the path exists but the value at the path is actually undefined, the node is still truthy, so this condition incorrectly treats it as “overridden”.

This would cause us to strip “undefined” metadata from SuperJSON even when the new payload still contains undefined at that path.

Apply this minimal fix (and optional typo fix) to the filter logic:

-    const overridenUndefinedKeys = originalUndefinedKeys.filter(
-      (key) => getKeyFromObject(newPayloadObject, key) !== undefined
-    );
+    const overriddenUndefinedKeys = originalUndefinedKeys.filter(
+      (key) => getKeyFromObject(newPayloadObject, key) !== undefined
+    );

Note: This relies on getKeyFromObject returning the raw value (undefined when the value is undefined). See suggested fix to getKeyFromObject in a separate comment.

Optionally fix the spelling: “overridden” instead of “overriden” for clarity.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const overridenUndefinedKeys = originalUndefinedKeys.filter(
(key) => get(newPayloadObject, key) !== undefined
(key) => getKeyFromObject(newPayloadObject, key) !== undefined
);
const overriddenUndefinedKeys = originalUndefinedKeys.filter(
(key) => getKeyFromObject(newPayloadObject, key) !== undefined
);
🤖 Prompt for AI Agents
In packages/core/src/v3/utils/ioSerialization.ts around lines 538 to 541, the
filter misdetects overrides because getKeyFromObject returns a jsonHero Node
(truthy even when its value is undefined); change the check to inspect the
node's actual value (e.g. use getKeyFromObject(newPayloadObject, key)?.value !==
undefined) so only truly defined values count as overrides, and optionally
correct the variable/name from "overriden" to "overridden".

overridenUndefinedKeys.forEach((key) => {
Expand All @@ -551,3 +551,9 @@ export async function replaceSuperJsonPayload(original: string, newPayload: stri

return superjson.deserialize(newSuperJson);
}

function getKeyFromObject(object: unknown, key: string) {
const jsonHeroPath = new JSONHeroPath(key);

return jsonHeroPath.first(object);
}
8 changes: 0 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading