Skip to content

Commit 744a3da

Browse files
d-csclaude
andcommitted
fix(webapp): readFallback batch + parent/root friendlyId from snapshot
Addresses two Devin findings on PR #3753 (readFallback.server.ts:206 comments 2026-05-28). 1) batchId field-name mismatch: #buildEngineTriggerInput in triggerTask.server.ts writes batch info as a nested object 'batch: { id, index }', not as a flat 'batchId'. readFallback was reading the non-existent snapshot.batchId so SyntheticRun.batchId was always undefined for buffered runs. Now reads snapshot.batch.id (the internal cuid, matching what PG stores in TaskRun.batchId). 2) parentTaskRunFriendlyId / rootTaskRunFriendlyId structurally unfillable: the snapshot carries the INTERNAL parent/root ids (parentTaskRunId / rootTaskRunId, what engine.trigger consumes), not friendlyIds. Convert internal to friendly via RunId.toFriendlyId so consumers do not have to special-case the buffered path. Four regression tests in mollifierReadFallback.test.ts: batchId extracted from nested snapshot.batch.id; a flat snapshot.batchId key is ignored (belt-and-braces); parent/root friendly conversion round-trips through RunId.generate(); parent/root undefined when the snapshot has no parent context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4f6335f commit 744a3da

2 files changed

Lines changed: 123 additions & 3 deletions

File tree

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ function asDate(value: unknown): Date | undefined {
109109
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
110110
}
111111

112+
// Snapshot ids are written by engine.trigger as INTERNAL ids (cuids); the
113+
// SyntheticRun contract exposes friendlyIds. `RunId.toFriendlyId` is
114+
// already used for the synthetic run's own id (line 155); reuse it for
115+
// parent/root so consumers see the same shape as the PG path.
116+
function internalRunIdToFriendlyId(internalId: string | undefined): string | undefined {
117+
if (!internalId) return undefined;
118+
return RunId.toFriendlyId(internalId);
119+
}
120+
112121
export async function findRunByIdWithMollifierFallback(
113122
input: ReadFallbackInput,
114123
deps: ReadFallbackDeps = {},
@@ -201,9 +210,23 @@ export async function findRunByIdWithMollifierFallback(
201210
annotations: snapshot.annotations,
202211
traceContext: snapshot.traceContext,
203212
scheduleId: asString(snapshot.scheduleId),
204-
batchId: asString(snapshot.batchId),
205-
parentTaskRunFriendlyId: asString(snapshot.parentTaskRunFriendlyId),
206-
rootTaskRunFriendlyId: asString(snapshot.rootTaskRunFriendlyId),
213+
// The engine.trigger input embeds the batch as `{ id, index }` (see
214+
// triggerTask.server.ts #buildEngineTriggerInput), not as a flat
215+
// `batchId`. The nested `id` is the batch's internal cuid — the same
216+
// value PG stores in `TaskRun.batchId` — so callers reconstruct the
217+
// friendly id via `BatchId.toFriendlyId` exactly as the PG path does.
218+
batchId: asString((snapshot.batch as { id?: unknown } | undefined)?.id),
219+
// The snapshot only carries the INTERNAL parent/root ids
220+
// (`parentTaskRunId` / `rootTaskRunId` — what engine.trigger consumes),
221+
// not the friendlyIds the SyntheticRun contract expects. Convert
222+
// internal → friendly here so consumers don't have to special-case
223+
// the buffered path.
224+
parentTaskRunFriendlyId: internalRunIdToFriendlyId(
225+
asString(snapshot.parentTaskRunId)
226+
),
227+
rootTaskRunFriendlyId: internalRunIdToFriendlyId(
228+
asString(snapshot.rootTaskRunId)
229+
),
207230

208231
error: entry.lastError,
209232
};

apps/webapp/test/mollifierReadFallback.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ vi.mock("~/db.server", () => ({
77

88
import { findRunByIdWithMollifierFallback } from "~/v3/mollifier/readFallback.server";
99
import type { MollifierBuffer, BufferEntry } from "@trigger.dev/redis-worker";
10+
import { RunId } from "@trigger.dev/core/v3/isomorphic";
1011

1112
function fakeBuffer(entry: BufferEntry | null): MollifierBuffer {
1213
return {
@@ -329,4 +330,100 @@ describe("findRunByIdWithMollifierFallback", () => {
329330
expect(result!.workerQueue).toBeUndefined();
330331
expect(result!.queue).toBeUndefined();
331332
});
333+
334+
it("extracts batchId from the nested snapshot.batch object (not the flat key)", async () => {
335+
// Regression for the field-name mismatch Devin flagged:
336+
// #buildEngineTriggerInput writes batch info as
337+
// `batch: { id, index }`, never as a flat `batchId`. readFallback
338+
// must read the nested key, otherwise SyntheticRun.batchId is always
339+
// undefined for buffered runs.
340+
const entry: BufferEntry = {
341+
runId: "run_1",
342+
envId: "env_a",
343+
orgId: "org_1",
344+
payload: JSON.stringify({
345+
taskIdentifier: "t",
346+
batch: { id: "batch_internal_xyz", index: 3 },
347+
}),
348+
status: "QUEUED",
349+
attempts: 0,
350+
createdAt: NOW,
351+
};
352+
const result = await findRunByIdWithMollifierFallback(
353+
{ runId: "run_1", environmentId: "env_a", organizationId: "org_1" },
354+
{ getBuffer: () => fakeBuffer(entry) },
355+
);
356+
expect(result!.batchId).toBe("batch_internal_xyz");
357+
});
358+
359+
it("does NOT read a flat `batchId` key — only the nested batch.id", async () => {
360+
// Belt-and-braces: a payload with the wrong-shaped flat key should
361+
// resolve to undefined, not silently pick up the bogus value.
362+
const entry: BufferEntry = {
363+
runId: "run_1",
364+
envId: "env_a",
365+
orgId: "org_1",
366+
payload: JSON.stringify({
367+
taskIdentifier: "t",
368+
batchId: "should-be-ignored",
369+
}),
370+
status: "QUEUED",
371+
attempts: 0,
372+
createdAt: NOW,
373+
};
374+
const result = await findRunByIdWithMollifierFallback(
375+
{ runId: "run_1", environmentId: "env_a", organizationId: "org_1" },
376+
{ getBuffer: () => fakeBuffer(entry) },
377+
);
378+
expect(result!.batchId).toBeUndefined();
379+
});
380+
381+
it("converts internal parent/root IDs in the snapshot to friendlyIds", async () => {
382+
// Regression for Devin's structural-unfillable finding: the snapshot
383+
// only carries INTERNAL parent/root ids (engine.trigger consumes the
384+
// internal shape), while SyntheticRun exposes friendlyIds. Convert
385+
// here so consumers don't have to special-case the buffered path.
386+
// The conversion is deterministic via RunId.toFriendlyId — we drive
387+
// it through `RunId.generate()` to get a matching internal+friendly
388+
// pair and assert the round-trip.
389+
const parent = RunId.generate();
390+
const root = RunId.generate();
391+
const entry: BufferEntry = {
392+
runId: "run_1",
393+
envId: "env_a",
394+
orgId: "org_1",
395+
payload: JSON.stringify({
396+
taskIdentifier: "t",
397+
parentTaskRunId: parent.id,
398+
rootTaskRunId: root.id,
399+
}),
400+
status: "QUEUED",
401+
attempts: 0,
402+
createdAt: NOW,
403+
};
404+
const result = await findRunByIdWithMollifierFallback(
405+
{ runId: "run_1", environmentId: "env_a", organizationId: "org_1" },
406+
{ getBuffer: () => fakeBuffer(entry) },
407+
);
408+
expect(result!.parentTaskRunFriendlyId).toBe(parent.friendlyId);
409+
expect(result!.rootTaskRunFriendlyId).toBe(root.friendlyId);
410+
});
411+
412+
it("leaves parent/root friendlyIds undefined when the snapshot carries no parent context", async () => {
413+
const entry: BufferEntry = {
414+
runId: "run_1",
415+
envId: "env_a",
416+
orgId: "org_1",
417+
payload: JSON.stringify({ taskIdentifier: "t" }),
418+
status: "QUEUED",
419+
attempts: 0,
420+
createdAt: NOW,
421+
};
422+
const result = await findRunByIdWithMollifierFallback(
423+
{ runId: "run_1", environmentId: "env_a", organizationId: "org_1" },
424+
{ getBuffer: () => fakeBuffer(entry) },
425+
);
426+
expect(result!.parentTaskRunFriendlyId).toBeUndefined();
427+
expect(result!.rootTaskRunFriendlyId).toBeUndefined();
428+
});
332429
});

0 commit comments

Comments
 (0)