Skip to content

Commit 49b9d00

Browse files
d-csclaude
andcommitted
fix(webapp): make buffered API responses match SDK response shapes
Two SDK schemas were drifting from what the mollifier paths emitted: 1. ListRunResponseItem declares `idempotencyKey: z.string().optional()` (omit-or-string). The listing-merge synthesiser was emitting `idempotencyKey: null` for buffered runs, which old SDK versions reject with a validation error before surfacing the row. 2. RetrieveRunTraceResponseBody declares a non-nullable `rootSpan` matching the recursive RetrieveRunTraceSpan shape. The buffered branch of /api/v1/runs/{id}/trace returned `rootSpan: null` plus an `events: []` field that isn't in the schema. Synthesise a real partial span (task identifier as message, no children, isPartial: true) from the buffer snapshot so the response satisfies the schema the SDK validates against. Verified end-to-end by calling the MCP server's list_runs and get_run_details against a buffered run; both now succeed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a15566d commit 49b9d00

2 files changed

Lines changed: 24 additions & 8 deletions

File tree

apps/webapp/app/routes/api.v1.runs.$runId.trace.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -78,15 +78,31 @@ export const loader = createLoaderApiRoute(
7878
if (resolved.source === "buffer") {
7979
// Buffered runs have no events ingested yet — the drainer hasn't
8080
// materialised the PG row and the worker hasn't started executing.
81-
// Return an empty trace skeleton so the customer's SDK sees the same
82-
// 200 shape it would get from a freshly-triggered PG run that hasn't
83-
// had its first span recorded yet.
81+
// Synthesise a single partial span that satisfies the SDK's
82+
// RetrieveRunTraceResponseBody schema (rootSpan is non-nullable).
83+
const buffered = resolved.run;
8484
return json(
8585
{
8686
trace: {
87-
traceId: resolved.run.traceId ?? "",
88-
rootSpan: null,
89-
events: [],
87+
traceId: buffered.traceId ?? "",
88+
rootSpan: {
89+
id: buffered.spanId ?? "",
90+
runId: buffered.friendlyId,
91+
data: {
92+
message: buffered.taskIdentifier ?? "",
93+
taskSlug: buffered.taskIdentifier ?? undefined,
94+
events: [],
95+
startTime: buffered.createdAt,
96+
duration: 0,
97+
isError: false,
98+
isPartial: true,
99+
isCancelled: buffered.status === "CANCELED",
100+
level: "TRACE",
101+
queueName: buffered.queue ?? undefined,
102+
machinePreset: buffered.machinePreset ?? undefined,
103+
},
104+
children: [],
105+
},
90106
},
91107
},
92108
{ status: 200 }

apps/webapp/app/v3/mollifier/listingMerge.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export type ListDataItem = {
8383
id: string;
8484
status: string;
8585
taskIdentifier: string;
86-
idempotencyKey: string | null;
86+
idempotencyKey?: string;
8787
createdAt: Date;
8888
updatedAt: Date;
8989
startedAt?: Date;
@@ -122,7 +122,7 @@ export async function synthesiseBufferedListItem(input: {
122122
const taskIdentifier =
123123
typeof snapshot.taskIdentifier === "string" ? snapshot.taskIdentifier : "";
124124
const idempotencyKey =
125-
typeof snapshot.idempotencyKey === "string" ? snapshot.idempotencyKey : null;
125+
typeof snapshot.idempotencyKey === "string" ? snapshot.idempotencyKey : undefined;
126126
const tags =
127127
Array.isArray(snapshot.tags) && snapshot.tags.every((t) => typeof t === "string")
128128
? (snapshot.tags as string[])

0 commit comments

Comments
 (0)