From f9c35730f5ca4a85c76c87650916f294a6fc51be Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Mar 2026 10:16:20 +0000 Subject: [PATCH 1/5] feat(mcp): add get_span_details tool Returns the fully detailed span with attributes and AI enrichment data --- .../api.v1.runs.$runId.spans.$spanId.ts | 132 ++++++++++++++++++ packages/cli-v3/src/mcp/config.ts | 6 + packages/cli-v3/src/mcp/formatters.ts | 94 ++++++++++++- packages/cli-v3/src/mcp/schemas.ts | 10 ++ packages/cli-v3/src/mcp/tools.ts | 2 + packages/cli-v3/src/mcp/tools/runs.ts | 49 ++++++- packages/core/src/v3/apiClient/index.ts | 13 ++ packages/core/src/v3/schemas/api.ts | 49 +++++++ 8 files changed, 352 insertions(+), 3 deletions(-) create mode 100644 apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts new file mode 100644 index 00000000000..825a82e9705 --- /dev/null +++ b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts @@ -0,0 +1,132 @@ +import { json } from "@remix-run/server-runtime"; +import { BatchId } from "@trigger.dev/core/v3/isomorphic"; +import { z } from "zod"; +import { $replica } from "~/db.server"; +import { extractAISpanData } from "~/components/runs/v3/ai"; +import { createLoaderApiRoute } from "~/services/routeBuilders/apiBuilder.server"; +import { resolveEventRepositoryForStore } from "~/v3/eventRepository/index.server"; +import { getTaskEventStoreTableForRun } from "~/v3/taskEventStore.server"; + +const ParamsSchema = z.object({ + runId: z.string(), + spanId: z.string(), +}); + +export const loader = createLoaderApiRoute( + { + params: ParamsSchema, + allowJWT: true, + corsStrategy: "all", + findResource: (params, auth) => { + return $replica.taskRun.findFirst({ + where: { + friendlyId: params.runId, + runtimeEnvironmentId: auth.environment.id, + }, + }); + }, + shouldRetryNotFound: true, + authorization: { + action: "read", + resource: (run) => ({ + runs: run.friendlyId, + tags: run.runTags, + batch: run.batchId ? BatchId.toFriendlyId(run.batchId) : undefined, + tasks: run.taskIdentifier, + }), + superScopes: ["read:runs", "read:all", "admin"], + }, + }, + async ({ params, resource: run, authentication }) => { + const eventRepository = resolveEventRepositoryForStore(run.taskEventStore); + const eventStore = getTaskEventStoreTableForRun(run); + + const span = await eventRepository.getSpan( + eventStore, + authentication.environment.id, + params.spanId, + run.traceId, + run.createdAt, + run.completedAt ?? undefined + ); + + if (!span) { + return json({ error: "Span not found" }, { status: 404 }); + } + + const durationMs = span.duration / 1_000_000; + + const aiData = + span.properties && typeof span.properties === "object" + ? extractAISpanData(span.properties as Record, durationMs) + : undefined; + + const triggeredRuns = await $replica.taskRun.findMany({ + select: { + friendlyId: true, + taskIdentifier: true, + status: true, + createdAt: true, + }, + where: { + parentSpanId: params.spanId, + }, + }); + + const properties = + span.properties && + typeof span.properties === "object" && + Object.keys(span.properties as Record).length > 0 + ? (span.properties as Record) + : undefined; + + return json( + { + spanId: span.spanId, + parentId: span.parentId, + runId: run.friendlyId, + message: span.message, + isError: span.isError, + isPartial: span.isPartial, + isCancelled: span.isCancelled, + level: span.level, + startTime: span.startTime, + duration: span.duration, + durationMs, + properties, + events: span.events?.length ? span.events : undefined, + entityType: span.entity.type ?? undefined, + ai: aiData + ? { + model: aiData.model, + provider: aiData.provider, + operationName: aiData.operationName, + inputTokens: aiData.inputTokens, + outputTokens: aiData.outputTokens, + totalTokens: aiData.totalTokens, + cachedTokens: aiData.cachedTokens, + reasoningTokens: aiData.reasoningTokens, + inputCost: aiData.inputCost, + outputCost: aiData.outputCost, + totalCost: aiData.totalCost, + tokensPerSecond: aiData.tokensPerSecond, + msToFirstChunk: aiData.msToFirstChunk, + durationMs: aiData.durationMs, + finishReason: aiData.finishReason, + responseText: aiData.responseText, + } + : undefined, + triggeredRuns: + triggeredRuns.length > 0 + ? triggeredRuns.map((r) => ({ + runId: r.friendlyId, + taskIdentifier: r.taskIdentifier, + status: r.status, + createdAt: r.createdAt, + })) + : undefined, + }, + { status: 200 } + ); + } +); diff --git a/packages/cli-v3/src/mcp/config.ts b/packages/cli-v3/src/mcp/config.ts index bab3473a145..d878d881e7d 100644 --- a/packages/cli-v3/src/mcp/config.ts +++ b/packages/cli-v3/src/mcp/config.ts @@ -70,6 +70,12 @@ export const toolsMetadata = { description: "Get the details and trace of a run. Trace events are paginated — the first call returns run details and the first page of trace lines. Pass the returned cursor to fetch subsequent pages without re-fetching the trace. The run ID starts with run_.", }, + get_span_details: { + name: "get_span_details", + title: "Get Span Details", + description: + "Get detailed information about a specific span within a run trace. Use get_run_details first to see the trace and find span IDs (shown as [spanId] in the trace output). Returns timing, properties/attributes, error info, and for AI spans: model, tokens, cost, and response data.", + }, wait_for_run_to_complete: { name: "wait_for_run_to_complete", title: "Wait for Run to Complete", diff --git a/packages/cli-v3/src/mcp/formatters.ts b/packages/cli-v3/src/mcp/formatters.ts index 22e3191fff8..595e034311c 100644 --- a/packages/cli-v3/src/mcp/formatters.ts +++ b/packages/cli-v3/src/mcp/formatters.ts @@ -3,6 +3,7 @@ import { ListRunResponseItem, RetrieveRunResponse, RetrieveRunTraceResponseBody, + RetrieveSpanDetailResponseBody, } from "@trigger.dev/core/v3/schemas"; import type { CursorPageResponse } from "@trigger.dev/core/v3/zodfetch"; @@ -238,7 +239,7 @@ function formatSpan( const duration = formatDuration(span.data.duration); const startTime = formatDateTime(span.data.startTime); - lines.push(`${indent}${prefix} ${span.data.message} ${statusIndicator}`); + lines.push(`${indent}${prefix} [${span.id}] ${span.data.message} ${statusIndicator}`); lines.push(`${indent} Duration: ${duration}`); lines.push(`${indent} Started: ${startTime}`); @@ -459,3 +460,94 @@ export function formatQueryResults(rows: Record[]): string { return [header, separator, ...body].join("\n"); } + +export function formatSpanDetail(span: RetrieveSpanDetailResponseBody): string { + const lines: string[] = []; + + const statusIndicator = span.isCancelled + ? "[CANCELLED]" + : span.isError + ? "[ERROR]" + : span.isPartial + ? "[IN PROGRESS]" + : "[COMPLETED]"; + + lines.push(`## Span: ${span.message} ${statusIndicator}`); + lines.push(`Span ID: ${span.spanId}`); + if (span.parentId) lines.push(`Parent ID: ${span.parentId}`); + lines.push(`Run ID: ${span.runId}`); + lines.push(`Level: ${span.level}`); + lines.push(`Started: ${formatDateTime(span.startTime)}`); + lines.push(`Duration: ${formatDuration(span.durationMs)}`); + if (span.entityType) lines.push(`Entity Type: ${span.entityType}`); + + if (span.ai) { + lines.push(""); + lines.push("### AI Details"); + lines.push(`Model: ${span.ai.model}`); + lines.push(`Provider: ${span.ai.provider}`); + lines.push(`Operation: ${span.ai.operationName}`); + lines.push( + `Tokens: ${span.ai.inputTokens} in / ${span.ai.outputTokens} out (${span.ai.totalTokens} total)` + ); + if (span.ai.cachedTokens) { + lines.push(`Cached tokens: ${span.ai.cachedTokens}`); + } + if (span.ai.reasoningTokens) { + lines.push(`Reasoning tokens: ${span.ai.reasoningTokens}`); + } + if (span.ai.totalCost !== undefined) { + lines.push(`Cost: $${span.ai.totalCost.toFixed(6)}`); + if (span.ai.inputCost !== undefined && span.ai.outputCost !== undefined) { + lines.push( + ` Input: $${span.ai.inputCost.toFixed(6)}, Output: $${span.ai.outputCost.toFixed(6)}` + ); + } + } + if (span.ai.tokensPerSecond !== undefined) { + lines.push(`Speed: ${span.ai.tokensPerSecond} tokens/sec`); + } + if (span.ai.msToFirstChunk !== undefined) { + lines.push(`Time to first chunk: ${span.ai.msToFirstChunk.toFixed(0)}ms`); + } + if (span.ai.finishReason) { + lines.push(`Finish reason: ${span.ai.finishReason}`); + } + if (span.ai.responseText) { + lines.push(""); + lines.push("### AI Response"); + lines.push(span.ai.responseText); + } + } + + if (span.properties && Object.keys(span.properties).length > 0) { + lines.push(""); + lines.push("### Properties"); + lines.push(JSON.stringify(span.properties, null, 2)); + } + + if (span.events && span.events.length > 0) { + lines.push(""); + lines.push(`### Events (${span.events.length})`); + const maxEvents = 10; + for (let i = 0; i < Math.min(span.events.length, maxEvents); i++) { + const event = span.events[i]; + if (typeof event === "object" && event !== null) { + lines.push(JSON.stringify(event, null, 2)); + } + } + if (span.events.length > maxEvents) { + lines.push(`... and ${span.events.length - maxEvents} more events`); + } + } + + if (span.triggeredRuns && span.triggeredRuns.length > 0) { + lines.push(""); + lines.push("### Triggered Runs"); + for (const run of span.triggeredRuns) { + lines.push(`- ${run.runId} (${run.taskIdentifier}) - ${run.status.toLowerCase()}`); + } + } + + return lines.join("\n"); +} diff --git a/packages/cli-v3/src/mcp/schemas.ts b/packages/cli-v3/src/mcp/schemas.ts index df1370bc75b..b5b7d7518da 100644 --- a/packages/cli-v3/src/mcp/schemas.ts +++ b/packages/cli-v3/src/mcp/schemas.ts @@ -152,6 +152,16 @@ export const GetRunDetailsInput = CommonRunsInput.extend({ export type GetRunDetailsInput = z.output; +export const GetSpanDetailsInput = CommonRunsInput.extend({ + spanId: z + .string() + .describe( + "The span ID to get details for. You can find span IDs in the trace output from get_run_details — they appear as [spanId] before each span message." + ), +}); + +export type GetSpanDetailsInput = z.output; + export const ListRunsInput = CommonProjectsInput.extend({ cursor: z.string().describe("The cursor to use for pagination, starts with run_").optional(), limit: z diff --git a/packages/cli-v3/src/mcp/tools.ts b/packages/cli-v3/src/mcp/tools.ts index ca7b514dba0..fd013b77644 100644 --- a/packages/cli-v3/src/mcp/tools.ts +++ b/packages/cli-v3/src/mcp/tools.ts @@ -15,6 +15,7 @@ import { getQuerySchemaTool, queryTool } from "./tools/query.js"; import { cancelRunTool, getRunDetailsTool, + getSpanDetailsTool, listRunsTool, waitForRunToCompleteTool, } from "./tools/runs.js"; @@ -56,6 +57,7 @@ export function registerTools(context: McpContext) { triggerTaskTool, listRunsTool, getRunDetailsTool, + getSpanDetailsTool, waitForRunToCompleteTool, cancelRunTool, deployTool, diff --git a/packages/cli-v3/src/mcp/tools/runs.ts b/packages/cli-v3/src/mcp/tools/runs.ts index 7619886d8a4..16c2ed7ca5a 100644 --- a/packages/cli-v3/src/mcp/tools/runs.ts +++ b/packages/cli-v3/src/mcp/tools/runs.ts @@ -3,8 +3,8 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { toolsMetadata } from "../config.js"; -import { formatRun, formatRunList, formatRunShape, formatRunTrace } from "../formatters.js"; -import { CommonRunsInput, GetRunDetailsInput, ListRunsInput, WaitForRunInput } from "../schemas.js"; +import { formatRun, formatRunList, formatRunShape, formatRunTrace, formatSpanDetail } from "../formatters.js"; +import { CommonRunsInput, GetRunDetailsInput, GetSpanDetailsInput, ListRunsInput, WaitForRunInput } from "../schemas.js"; import { respondWithError, toolHandler } from "../utils.js"; // Cache formatted traces in temp files keyed by runId. @@ -156,6 +156,51 @@ export const getRunDetailsTool = { }), }; +export const getSpanDetailsTool = { + name: toolsMetadata.get_span_details.name, + title: toolsMetadata.get_span_details.title, + description: toolsMetadata.get_span_details.description, + inputSchema: GetSpanDetailsInput.shape, + handler: toolHandler(GetSpanDetailsInput.shape, async (input, { ctx }) => { + ctx.logger?.log("calling get_span_details", { input }); + + if (ctx.options.devOnly && input.environment !== "dev") { + return respondWithError( + `This MCP server is only available for the dev environment. You tried to access the ${input.environment} environment. Remove the --dev-only flag to access other environments.` + ); + } + + const projectRef = await ctx.getProjectRef({ + projectRef: input.projectRef, + cwd: input.configPath, + }); + + const apiClient = await ctx.getApiClient({ + projectRef, + environment: input.environment, + scopes: [`read:runs:${input.runId}`], + branch: input.branch, + }); + + const spanDetail = await apiClient.retrieveSpan(input.runId, input.spanId); + const formatted = formatSpanDetail(spanDetail); + + const runUrl = await ctx.getDashboardUrl( + `/projects/v3/${projectRef}/runs/${input.runId}` + ); + + const content = [formatted]; + if (runUrl) { + content.push(""); + content.push(`[View run in dashboard](${runUrl})`); + } + + return { + content: [{ type: "text", text: content.join("\n") }], + }; + }), +}; + export const waitForRunToCompleteTool = { name: toolsMetadata.wait_for_run_to_complete.name, title: toolsMetadata.wait_for_run_to_complete.title, diff --git a/packages/core/src/v3/apiClient/index.ts b/packages/core/src/v3/apiClient/index.ts index bc94ff38fd8..c0f179c97d0 100644 --- a/packages/core/src/v3/apiClient/index.ts +++ b/packages/core/src/v3/apiClient/index.ts @@ -54,6 +54,7 @@ import { PromptOverrideCreatedResponseBody, RetrieveRunResponse, RetrieveRunTraceResponseBody, + RetrieveSpanDetailResponseBody, ScheduleObject, SendInputStreamResponseBody, StreamBatchItemsResponse, @@ -602,6 +603,18 @@ export class ApiClient { ); } + retrieveSpan(runId: string, spanId: string, requestOptions?: ZodFetchOptions) { + return zodfetch( + RetrieveSpanDetailResponseBody, + `${this.baseUrl}/api/v1/runs/${runId}/spans/${spanId}`, + { + method: "GET", + headers: this.#getHeaders(false), + }, + mergeRequestOptions(this.defaultRequestOptions, requestOptions) + ); + } + listRuns( query?: ListRunsQueryParams, requestOptions?: ZodFetchOptions diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index bb6623b3f3c..17ac85bd65c 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -1624,6 +1624,55 @@ export const RetrieveRunTraceResponseBody = z.object({ export type RetrieveRunTraceResponseBody = z.infer; +export const RetrieveSpanDetailResponseBody = z.object({ + spanId: z.string(), + parentId: z.string().nullable(), + runId: z.string(), + message: z.string(), + isError: z.boolean(), + isPartial: z.boolean(), + isCancelled: z.boolean(), + level: z.string(), + startTime: z.coerce.date(), + duration: z.number(), + durationMs: z.number(), + properties: z.record(z.any()).optional(), + events: z.array(z.any()).optional(), + entityType: z.string().optional(), + ai: z + .object({ + model: z.string(), + provider: z.string(), + operationName: z.string(), + inputTokens: z.number(), + outputTokens: z.number(), + totalTokens: z.number(), + cachedTokens: z.number().optional(), + reasoningTokens: z.number().optional(), + inputCost: z.number().optional(), + outputCost: z.number().optional(), + totalCost: z.number().optional(), + tokensPerSecond: z.number().optional(), + msToFirstChunk: z.number().optional(), + durationMs: z.number(), + finishReason: z.string().optional(), + responseText: z.string().optional(), + }) + .optional(), + triggeredRuns: z + .array( + z.object({ + runId: z.string(), + taskIdentifier: z.string(), + status: z.string(), + createdAt: z.coerce.date(), + }) + ) + .optional(), +}); + +export type RetrieveSpanDetailResponseBody = z.infer; + export const CreateStreamResponseBody = z.object({ version: z.string(), }); From c2fdff7019eeca164bf1330812b045fecb01232c Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Mar 2026 10:37:13 +0000 Subject: [PATCH 2/5] Some fixes and changesets --- .changeset/mcp-get-span-details.md | 11 +++++++++++ .server-changes/mcp-get-span-details.md | 6 ++++++ .../app/routes/api.v1.runs.$runId.spans.$spanId.ts | 6 ++++-- packages/core/src/v3/schemas/api.ts | 1 - 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .changeset/mcp-get-span-details.md create mode 100644 .server-changes/mcp-get-span-details.md diff --git a/.changeset/mcp-get-span-details.md b/.changeset/mcp-get-span-details.md new file mode 100644 index 00000000000..e69b7979b07 --- /dev/null +++ b/.changeset/mcp-get-span-details.md @@ -0,0 +1,11 @@ +--- +"@trigger.dev/core": patch +"trigger.dev": patch +--- + +Add `get_span_details` MCP tool for inspecting individual spans within a run trace. + +- New `get_span_details` tool returns full span attributes, timing, events, and AI enrichment (model, tokens, cost, speed) +- Span IDs now shown in `get_run_details` trace output for easy discovery +- New API endpoint `GET /api/v1/runs/:runId/spans/:spanId` +- New `retrieveSpan()` method on the API client diff --git a/.server-changes/mcp-get-span-details.md b/.server-changes/mcp-get-span-details.md new file mode 100644 index 00000000000..336595d2203 --- /dev/null +++ b/.server-changes/mcp-get-span-details.md @@ -0,0 +1,6 @@ +--- +area: webapp +type: feature +--- + +Add API endpoint `GET /api/v1/runs/:runId/spans/:spanId` that returns detailed span information including properties, events, AI enrichment (model, tokens, cost), and triggered child runs. diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts index 825a82e9705..c987f339dd2 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts @@ -54,7 +54,10 @@ export const loader = createLoaderApiRoute( return json({ error: "Span not found" }, { status: 404 }); } - const durationMs = span.duration / 1_000_000; + // Postgres eventRepository returns duration in ms, ClickHouse returns nanoseconds + const isClickhouse = + run.taskEventStore === "clickhouse" || run.taskEventStore === "clickhouse_v2"; + const durationMs = isClickhouse ? span.duration / 1_000_000 : span.duration; const aiData = span.properties && typeof span.properties === "object" @@ -91,7 +94,6 @@ export const loader = createLoaderApiRoute( isCancelled: span.isCancelled, level: span.level, startTime: span.startTime, - duration: span.duration, durationMs, properties, events: span.events?.length ? span.events : undefined, diff --git a/packages/core/src/v3/schemas/api.ts b/packages/core/src/v3/schemas/api.ts index 17ac85bd65c..cf2ddfb26b8 100644 --- a/packages/core/src/v3/schemas/api.ts +++ b/packages/core/src/v3/schemas/api.ts @@ -1634,7 +1634,6 @@ export const RetrieveSpanDetailResponseBody = z.object({ isCancelled: z.boolean(), level: z.string(), startTime: z.coerce.date(), - duration: z.number(), durationMs: z.number(), properties: z.record(z.any()).optional(), events: z.array(z.any()).optional(), From 922c34f52f06df4598c593c2f89630a1cd1f20e2 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Mar 2026 10:51:11 +0000 Subject: [PATCH 3/5] Fix for coderabbit suggestion --- apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts index c987f339dd2..3f2665d0f33 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts @@ -65,6 +65,7 @@ export const loader = createLoaderApiRoute( : undefined; const triggeredRuns = await $replica.taskRun.findMany({ + take: 50, select: { friendlyId: true, taskIdentifier: true, @@ -72,6 +73,7 @@ export const loader = createLoaderApiRoute( createdAt: true, }, where: { + runtimeEnvironmentId: authentication.environment.id, parentSpanId: params.spanId, }, }); From 9a5e7c07706712611cb2936d506121a6e4a7cb01 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Mar 2026 11:59:18 +0000 Subject: [PATCH 4/5] fixed the duration formatter to work with nanoseconds --- packages/cli-v3/src/mcp/formatters.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli-v3/src/mcp/formatters.ts b/packages/cli-v3/src/mcp/formatters.ts index 595e034311c..cafd10d4a51 100644 --- a/packages/cli-v3/src/mcp/formatters.ts +++ b/packages/cli-v3/src/mcp/formatters.ts @@ -236,7 +236,11 @@ function formatSpan( // Format span header const statusIndicator = getStatusIndicator(span.data); - const duration = formatDuration(span.data.duration); + // Trace durations from ClickHouse are nanoseconds, from Postgres are milliseconds. + // Normalize: values over 1e6 are nanoseconds (1e6 ns = 1ms; 1e6 ms = 16min). + const durationMs = + span.data.duration > 1_000_000 ? span.data.duration / 1_000_000 : span.data.duration; + const duration = formatDuration(durationMs); const startTime = formatDateTime(span.data.startTime); lines.push(`${indent}${prefix} [${span.id}] ${span.data.message} ${statusIndicator}`); From 6d9e3868ef871ce11c5a69fcea5529356829e24e Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 24 Mar 2026 12:54:15 +0000 Subject: [PATCH 5/5] just treat duration as nanoseconds, don't worry about the postgresql store as its deprecated --- apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts | 6 ++---- packages/cli-v3/src/mcp/formatters.ts | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts index 3f2665d0f33..7c093efd960 100644 --- a/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts +++ b/apps/webapp/app/routes/api.v1.runs.$runId.spans.$spanId.ts @@ -54,10 +54,8 @@ export const loader = createLoaderApiRoute( return json({ error: "Span not found" }, { status: 404 }); } - // Postgres eventRepository returns duration in ms, ClickHouse returns nanoseconds - const isClickhouse = - run.taskEventStore === "clickhouse" || run.taskEventStore === "clickhouse_v2"; - const durationMs = isClickhouse ? span.duration / 1_000_000 : span.duration; + // Duration is nanoseconds from ClickHouse (Postgres store is deprecated) + const durationMs = span.duration / 1_000_000; const aiData = span.properties && typeof span.properties === "object" diff --git a/packages/cli-v3/src/mcp/formatters.ts b/packages/cli-v3/src/mcp/formatters.ts index cafd10d4a51..c90ae0570df 100644 --- a/packages/cli-v3/src/mcp/formatters.ts +++ b/packages/cli-v3/src/mcp/formatters.ts @@ -236,11 +236,8 @@ function formatSpan( // Format span header const statusIndicator = getStatusIndicator(span.data); - // Trace durations from ClickHouse are nanoseconds, from Postgres are milliseconds. - // Normalize: values over 1e6 are nanoseconds (1e6 ns = 1ms; 1e6 ms = 16min). - const durationMs = - span.data.duration > 1_000_000 ? span.data.duration / 1_000_000 : span.data.duration; - const duration = formatDuration(durationMs); + // Trace durations are nanoseconds from ClickHouse + const duration = formatDuration(span.data.duration / 1_000_000); const startTime = formatDateTime(span.data.startTime); lines.push(`${indent}${prefix} [${span.id}] ${span.data.message} ${statusIndicator}`);