Skip to content

feat(webapp): mollifier API GET read-fallback — synthetic primitives + route wiring#3755

Draft
d-cs wants to merge 3 commits into
mollifier-phase-3-replayfrom
mollifier-phase-3-reads
Draft

feat(webapp): mollifier API GET read-fallback — synthetic primitives + route wiring#3755
d-cs wants to merge 3 commits into
mollifier-phase-3-replayfrom
mollifier-phase-3-reads

Conversation

@d-cs
Copy link
Copy Markdown
Collaborator

@d-cs d-cs commented May 26, 2026

Summary

Synthesise QUEUED/FAILED responses from the mollifier buffer when a TaskRun row hasn't landed in Postgres yet. Wires the synthesis into:

  • ApiRetrieveRunPresenter
  • v1 trace GET route
  • v1 spans GET route
  • attempts route gains a GET loader (fixes pre-existing Remix "no loader" 400)

The readFallback infra itself lives on the trigger PR (consumed by IdempotencyKeyConcern); this PR adds the route-level synthetic-rendering primitives.

Stacked on the replay PR.

Test plan

  • `pnpm run typecheck --filter webapp` passes
  • `pnpm run test --filter webapp test/mollifierSyntheticRedirectInfo.test.ts` passes
  • `pnpm run test --filter webapp test/mollifierSyntheticSpanRun.test.ts` passes

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 26, 2026

⚠️ No Changeset found

Latest commit: e57bc5e

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fdd25663-4c87-4e1f-baae-1ba6b94e2ba0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mollifier-phase-3-reads

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/webapp/app/v3/mollifier/syntheticRedirectInfo.server.ts
@d-cs d-cs self-assigned this May 26, 2026
@d-cs d-cs force-pushed the mollifier-phase-3-replay branch from 31f4726 to b05929b Compare May 26, 2026 11:12
@d-cs d-cs force-pushed the mollifier-phase-3-reads branch from 1838229 to af0aeeb Compare May 26, 2026 11:12
@d-cs d-cs force-pushed the mollifier-phase-3-replay branch from b05929b to b89da52 Compare May 26, 2026 13:24
@d-cs d-cs force-pushed the mollifier-phase-3-reads branch 3 times, most recently from 0919f7a to f36c576 Compare May 26, 2026 15:12
@d-cs d-cs force-pushed the mollifier-phase-3-replay branch from 74fdf6d to c6fa61f Compare May 26, 2026 16:20
@d-cs d-cs force-pushed the mollifier-phase-3-reads branch from c8ab214 to 047b240 Compare May 26, 2026 16:20
d-cs and others added 3 commits May 26, 2026 17:32
…+ route wiring

Synthesise QUEUED/FAILED responses from the mollifier buffer when a
TaskRun row hasn't landed in Postgres yet. Wires the synthesis into:
- ApiRetrieveRunPresenter
- v1 trace GET route
- v1 spans GET route
- attempts route gains a GET loader (fixes pre-existing Remix 400)

Stacked on the trigger-time decisions PR. The readFallback infra
itself lives on the trigger PR (consumed by IdempotencyKeyConcern);
this PR adds the route-level synthetic-rendering primitives.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the ad-hoc \`as Record<string, unknown>\` + \`typeof ===
"string"\` checks in \`findBufferedRunRedirectInfo\` with a Zod
\`safeParse\` against a schema for the subset of fields the redirect
needs (envSlug / projectSlug / orgSlug / optional spanId). Wrong-typed
or missing fields now collapse into a single parse-fail branch that
logs the structured issue list and returns null.

Adds a regression test for the structural-vs-typeof distinction:
\`environment.slug: 42\` (number) is now rejected, where the previous
\`typeof slug === "string"\` chain would silently accept any string-
typed value but had no defence against shape drift in other fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l enum

\`SyntheticRun.machinePreset\` is a plain string sourced from the
mollifier snapshot, but \`SpanRun.machinePreset\` is the typed
\`MachinePresetName\` enum (micro / small-1x / small-2x / medium-1x /
medium-2x / large-1x / large-2x). The direct assignment failed
\`tsc --noEmit\` and CI typecheck. Validate via \`MachinePresetName.safeParse\`
and collapse unknown values to \`undefined\` so a stale preset
returned by the buffer doesn't bleed into the UI as a typed-but-unknown
value.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@d-cs d-cs force-pushed the mollifier-phase-3-reads branch from 047b240 to e57bc5e Compare May 26, 2026 16:32
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

typeof buffered.metadata === "string" ? buffered.metadata : null;

return {
id: buffered.friendlyId,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 synthesiseFoundRunFromBuffer uses friendlyId for internal id field

In synthesiseFoundRunFromBuffer, line 551 sets id: buffered.friendlyId instead of id: buffered.id. The SyntheticRun distinguishes between id (the internal cuid derived via RunId.fromFriendlyId() at apps/webapp/app/v3/mollifier/readFallback.server.ts:155) and friendlyId (the user-facing run_xxx identifier). The FoundRun type extends CommonRelatedRun, whose id corresponds to the Prisma TaskRun.id column (an internal cuid). By assigning the friendly ID to id, downstream logging in call() (e.g., taskRunId: taskRun.id at lines 176 and 205) will record the friendly ID instead of the internal ID, breaking log correlation with other systems that use the internal ID.

Suggested change
id: buffered.friendlyId,
id: buffered.id,
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

startTime: buffered.createdAt,
duration: 0,
isError: false,
isPartial: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 isPartial hardcoded to true in trace route for cancelled buffered runs

In the buffered-run branch of api.v1.runs.$runId.trace.ts, isPartial is hardcoded to true (line 98). For cancelled runs, this is incorrect — cancelled runs have reached a terminal state and should have isPartial: false. The sibling span route (api.v1.runs.$runId.spans.$spanId.ts:96) correctly uses isPartial: resolved.run.status !== "CANCELED", and syntheticTrace.server.ts:27 correctly uses isPartial: !isCancelled. This inconsistency means the trace API returns a "still in progress" signal for cancelled buffered runs, which could confuse SDK consumers or dashboard components that rely on isPartial to determine if the span is finalized.

Suggested change
isPartial: true,
isPartial: buffered.status !== "CANCELED",
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +577 to +578
payload: typeof buffered.payload === "string" ? buffered.payload : "",
payloadType: buffered.payloadType ?? "application/json",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Buffered payload silently dropped when non-string

In synthesiseFoundRunFromBuffer at line 577, the payload is set to typeof buffered.payload === "string" ? buffered.payload : "". The SyntheticRun.payload is typed unknown and is assigned from snapshot.payload (readFallback.server.ts:164). If the snapshot stores the payload as a parsed JSON object rather than a serialized string, it gets replaced with "". This is then processed by conditionallyImportPacket and parsePacketAsJson, which returns undefined for empty strings (since !"" is truthy at ioSerialization.ts:52). So the API response will show no payload. This appears intentional (the PG column is String? so only strings are expected), but it means buffered runs with object-typed payloads in Redis will silently lose their payload in the API response.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +24 to +36
export async function loader({ request, params }: LoaderFunctionArgs) {
const authenticationResult = await authenticateApiRequest(request);
if (!authenticationResult) {
return json({ error: "Invalid or Missing API Key" }, { status: 401 });
}

const parsed = ParamsSchema.safeParse(params);
if (!parsed.success) {
return json({ error: "Invalid or missing run ID" }, { status: 400 });
}

return json({ attempts: [] }, { status: 200 });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 Attempts loader returns 200 for any run without ownership verification

The new loader in api.v1.runs.$runParam.attempts.ts authenticates the API key but does not verify that the run exists or that the authenticated environment owns it. Any valid API key holder can GET /api/v1/runs/<any-run-id>/attempts and receive { attempts: [] } with status 200. Since the response contains no sensitive data (just an empty array), this is not a security leak, but it deviates from the authorization pattern used by other run endpoints (spans, trace, retrieve) which verify run ownership. The PR comments indicate this is intentional for the parity script contract, but it means the endpoint cannot distinguish between "run exists with no attempts" and "run doesn't exist / belongs to another environment."

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@@ -0,0 +1,112 @@
import { deserialiseSnapshot, type MollifierBuffer } from "@trigger.dev/redis-worker";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚩 syntheticRedirectInfo uses deserialiseSnapshot while readFallback uses deserialiseMollifierSnapshot

The syntheticRedirectInfo.server.ts imports deserialiseSnapshot from @trigger.dev/redis-worker (line 1), while readFallback.server.ts imports deserialiseMollifierSnapshot from ./mollifierSnapshot.server (line 4). These are different deserialisation paths for the same entry.payload data. If the two functions have different behaviour (e.g., one parses JSON while the other uses msgpack, or they handle errors differently), the two modules could disagree on the snapshot contents. Worth verifying that both deserialise functions are equivalent or intentionally different for their use cases.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant