Skip to content

feat(run-ops): webapp routes — friendlyId reads, cross-seam token resolution, co-location writes#4123

Draft
d-cs wants to merge 10 commits into
runops/pr08-presentersfrom
runops/pr09-routes
Draft

feat(run-ops): webapp routes — friendlyId reads, cross-seam token resolution, co-location writes#4123
d-cs wants to merge 10 commits into
runops/pr08-presentersfrom
runops/pr09-routes

Conversation

@d-cs

@d-cs d-cs commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

What

Wires the run-ops split into the webapp routes so requests are routed correctly across the store boundary. Builds on the presenter rework from the previous PR; this layer is the route/loader/action glue that calls those presenters and the run store.

  • Route loaders and actions that previously looked runs up by internal id now resolve by friendlyId through the run store, so a run is found regardless of which store holds it. This spans the dashboard run routes, the resource routes (resources.runs.*, resources.taskruns.* cancel/debug/replay, logs download), the API batch/run routes (api.v1/api.v2/api.v3), realtime routes, sync-traces, and the admin debugRun helper.
  • Public wait-token routes are updated to resolve tokens across the split boundary: the waitpoint-token complete / callback / retrieve / wait routes look the token up through the store, and the resolver re-checks auth after resolution rather than relying on a co-located join. Waitpoint writes go to the co-location store that owns them.
  • Waitpoint-token handling is moved onto an always-cuid contract, and the token tests are rewritten against that contract.

New tests cover token resolution across the seam (waitpointTokenResolve), the cross-seam guard on the complete route, the control-plane callback path, and the api.v1.waitpoints.tokens surface.

Why

Part of the run-ops database split. This is PR9 of the series (PR8/9/10): it connects the split-aware read presenters (PR8) to the actual HTTP/loader surface so reads and token resolution route correctly, without yet enabling the split. The behaviour-changing activation stays isolated in PR10 on top of this.

Tests

pnpm run test --filter webapp for the affected route/resolver suites, including the new cross-seam-guard and waitpoint-token suites.

Notes

Draft, stacked on #4122 (runops/pr08-presenters). Review that first; this diff is against it.

Server-change / changeset note to be added at stack-assembly time.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cbf736a

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

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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: 297872b1-cb5c-4baa-ace5-33f8cd769e81

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 runops/pr09-routes

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.

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

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.

Devin Review found 2 potential issues.

Open in Devin Review

Comment thread apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts Outdated
@d-cs d-cs force-pushed the runops/pr08-presenters branch from 9b6eee8 to 94eaef6 Compare July 2, 2026 18:02
@d-cs d-cs force-pushed the runops/pr09-routes branch from 4ba9267 to ec1aa6e Compare July 2, 2026 18:03
@pkg-pr-new

pkg-pr-new Bot commented Jul 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

@trigger.dev/build

npm i https://pkg.pr.new/@trigger.dev/build@cbf736a

trigger.dev

npm i https://pkg.pr.new/trigger.dev@cbf736a

@trigger.dev/core

npm i https://pkg.pr.new/@trigger.dev/core@cbf736a

@trigger.dev/python

npm i https://pkg.pr.new/@trigger.dev/python@cbf736a

@trigger.dev/react-hooks

npm i https://pkg.pr.new/@trigger.dev/react-hooks@cbf736a

@trigger.dev/redis-worker

npm i https://pkg.pr.new/@trigger.dev/redis-worker@cbf736a

@trigger.dev/rsc

npm i https://pkg.pr.new/@trigger.dev/rsc@cbf736a

@trigger.dev/schema-to-json

npm i https://pkg.pr.new/@trigger.dev/schema-to-json@cbf736a

@trigger.dev/sdk

npm i https://pkg.pr.new/@trigger.dev/sdk@cbf736a

commit: cbf736a

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx (1)

97-101: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The as unknown as PrismaClientOrTransaction double casts bypass structural type checking on the injected clients. If the runOps replica client shapes ever diverge from PrismaClientOrTransaction, this would silently compile. Consider aligning the presenter's parameter types (or the exported client types) so a single, checked cast — or no cast — suffices.

apps/webapp/app/routes/api.v1.waitpoints.tokens.ts (1)

12-17: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Type-erasing double-cast (as unknown as PrismaClientOrTransaction) repeated across presenters.

Same runOpsNewReplicaClient as unknown as PrismaClientOrTransaction / runOpsLegacyReplica as unknown as PrismaClientOrTransaction pattern appears here and in resources...waitpoints.tags.ts. Casting through unknown bypasses structural type-checking for the replica client wiring; if PrismaClientOrTransaction and the actual db.server client types diverge, this masks it at compile time. Consider aligning the exported client types with PrismaClientOrTransaction (or defining a narrower shared type) so the cast can be dropped.

Also applies to: 36-40

apps/webapp/test/api.v1.waitpoints.tokens.test.ts (1)

134-200: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Tag-write test doesn't exercise the real createWaitpointTag production path.

The assertion that "the tag landed on the control-plane client" is performed by writing directly via prisma.waitpointTag.upsert rather than calling createWaitpointTag from ~/models/waitpointTag.server (the function the actual api.v1.waitpoints.tokens.ts action uses). This makes the control-plane assertion trivially true by construction and wouldn't catch a regression if the production tag-write path were changed to route through the run-ops store. Consider invoking the real model function instead, if feasible without pulling in unrelated route dependencies.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 1b3db469-9a23-444a-b2fc-0576d2b798e7

📥 Commits

Reviewing files that changed from the base of the PR and between 9b6eee8 and 4ba9267.

📒 Files selected for processing (39)
  • apps/webapp/app/components/admin/debugRun.tsx
  • apps/webapp/app/routes/@.runs.$runParam.ts
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx
  • apps/webapp/app/routes/api.v1.batches.$batchId.ts
  • apps/webapp/app/routes/api.v1.runs.$runFriendlyId.input-streams.wait.ts
  • apps/webapp/app/routes/api.v1.runs.$runFriendlyId.session-streams.wait.ts
  • apps/webapp/app/routes/api.v1.waitpoints.tokens.$waitpointFriendlyId.callback.$hash.ts
  • apps/webapp/app/routes/api.v1.waitpoints.tokens.$waitpointFriendlyId.complete.ts
  • apps/webapp/app/routes/api.v1.waitpoints.tokens.$waitpointFriendlyId.ts
  • apps/webapp/app/routes/api.v1.waitpoints.tokens.ts
  • apps/webapp/app/routes/api.v2.batches.$batchId.ts
  • apps/webapp/app/routes/api.v2.tasks.batch.ts
  • apps/webapp/app/routes/api.v3.batches.ts
  • apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.wait.duration.ts
  • apps/webapp/app/routes/engine.v1.runs.$runFriendlyId.waitpoints.tokens.$waitpointFriendlyId.wait.ts
  • apps/webapp/app/routes/orgs.$organizationSlug.projects.$projectParam.runs.$runParam.ts
  • apps/webapp/app/routes/projects.v3.$projectRef.runs.$runParam.ts
  • apps/webapp/app/routes/realtime.v1.batches.$batchId.ts
  • apps/webapp/app/routes/realtime.v1.runs.$runId.ts
  • apps/webapp/app/routes/realtime.v1.runs.ts
  • apps/webapp/app/routes/realtime.v1.streams.$runId.$streamId.ts
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.idempotencyKey.reset.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tags.ts
  • apps/webapp/app/routes/resources.runs.$runParam.logs.download.ts
  • apps/webapp/app/routes/resources.runs.$runParam.ts
  • apps/webapp/app/routes/resources.taskruns.$runParam.cancel.ts
  • apps/webapp/app/routes/resources.taskruns.$runParam.debug.ts
  • apps/webapp/app/routes/resources.taskruns.$runParam.replay.ts
  • apps/webapp/app/routes/runs.$runParam.ts
  • apps/webapp/app/routes/sync.traces.runs.$traceId.ts
  • apps/webapp/app/v3/runOpsMigration/waitpointTokenResolve.server.test.ts
  • apps/webapp/test/api.v1.waitpoints.tokens.complete.crossSeamGuard.test.ts
  • apps/webapp/test/api.v1.waitpoints.tokens.test.ts
  • apps/webapp/test/crossSeamGuard.proof.test.ts
  • apps/webapp/test/waitpointCallback.controlPlane.test.ts

Comment thread apps/webapp/app/routes/resources.runs.$runParam.ts Outdated
Comment thread apps/webapp/app/routes/resources.taskruns.$runParam.debug.ts Outdated
@d-cs

d-cs commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

On the three maintainability nitpicks in the review summary:

  • The as unknown as PrismaClientOrTransaction double-casts on the injected run-ops replica clients are intentional and consistent across the waitpoint presenter wiring. The run-ops replica client types are structurally compatible at the call sites we use; aligning the exported client types to drop the cast is a broader typing change outside this PR's read-routing scope. Kept as-is for consistency (the newly-wired waitpoint detail route follows the same pattern).
  • The tag-write test asserting the write lands on the control-plane client: noted; the production tag path is covered by the route-level tests, and rewiring this assertion through the model function pulls in unrelated route deps for no additional coverage of the split behaviour under test.

No code changes for these; the security/correctness comments have been addressed in the latest commit.

@d-cs d-cs force-pushed the runops/pr08-presenters branch from 6e4c0de to 48c395b Compare July 2, 2026 19:25
@d-cs d-cs force-pushed the runops/pr09-routes branch from 30ccb91 to caebbbc Compare July 2, 2026 19:25
@d-cs d-cs force-pushed the runops/pr08-presenters branch from f8b64bd to e145839 Compare July 2, 2026 20:23
@d-cs d-cs force-pushed the runops/pr09-routes branch from e2cdc6d to 00f588c Compare July 2, 2026 20:23
@d-cs d-cs force-pushed the runops/pr08-presenters branch from e145839 to 6cd0085 Compare July 2, 2026 20:38
@d-cs d-cs force-pushed the runops/pr09-routes branch from 00f588c to 077917b Compare July 2, 2026 20:38
@d-cs d-cs force-pushed the runops/pr08-presenters branch from 6cd0085 to 5ccf63f Compare July 2, 2026 21:44
@d-cs d-cs force-pushed the runops/pr09-routes branch from 077917b to b74993f Compare July 2, 2026 21:44
@d-cs d-cs force-pushed the runops/pr08-presenters branch from 5ccf63f to 105ba1c Compare July 3, 2026 00:17
@d-cs d-cs force-pushed the runops/pr09-routes branch 2 times, most recently from e7056ff to fa76c1b Compare July 3, 2026 01:32
@d-cs d-cs force-pushed the runops/pr08-presenters branch from f7cf260 to d31f2e9 Compare July 3, 2026 08:51
@d-cs d-cs force-pushed the runops/pr09-routes branch from 3bee9ab to be4e08a Compare July 3, 2026 08:51
@d-cs d-cs force-pushed the runops/pr08-presenters branch from d31f2e9 to 4bfd808 Compare July 3, 2026 10:02
@d-cs d-cs force-pushed the runops/pr09-routes branch from be4e08a to d9ee04f Compare July 3, 2026 10:02
@d-cs d-cs force-pushed the runops/pr08-presenters branch from 4bfd808 to 9197016 Compare July 3, 2026 10:37
@d-cs d-cs force-pushed the runops/pr09-routes branch from d9ee04f to 495be7c Compare July 3, 2026 10:37
@d-cs d-cs force-pushed the runops/pr08-presenters branch from 9197016 to fa0d473 Compare July 3, 2026 10:44
@d-cs d-cs force-pushed the runops/pr09-routes branch from 495be7c to 0c5b193 Compare July 3, 2026 10:44
…ck, co-location writes

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
d-cs and others added 9 commits July 3, 2026 12:07
… contract

Waitpoint ids are always cuid; residency is decided by co-location routing,
not id-shape. Remove the removed setKsuidMintEnabled global and flip the
standalone-token assertions to cuid/LEGACY. Drive the NEW/cross-seam side from
a ksuid run co-locating its (cuid) token on #new instead of a ksuid token, and
keep the cross-seam guard consulted unconditionally on a plain cuid token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The public wait-token routes (complete, HTTP callback, retrieve) resolved the
waitpoint with a bare read-through that defaulted its new-side client to the
dedicated run-ops replica and gated on the async mint flag. A standalone wait
token has a cuid id and, having no owning run, is written to the control-plane
store, so under the split topology the run-ops replica does not hold it and the
routes returned 404 "Waitpoint not found".

Resolve these routes the same way the working waiter route does: fan out through
the run-ops replica first, then the control-plane replica, so both a co-located
(run-owned) waitpoint and a standalone control-plane token are found. Gate the
fan-out on the URL-presence read gate rather than the mint flag, so read
visibility spans both DBs whenever both are configured — including the window
where both database URLs are set but the mint flag is off. The retrieve route
hands the same fan-out clients and gate to ApiWaitpointPresenter.

Adds a two-database testcontainer regression proving a control-plane-resident
standalone token resolves via the legacy fallback under the read gate, and that
the passthrough branch (gate off) misses it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment/label/title cleanup only — no product logic or test behavior
changed. Removes leftover enumeration scaffolding across the PR10 files:
Test A-D and Leg 1-4 case markers, (A)/(B)/(D) parenthesized markers,
Step 1-3 prefixes, and --- comment framing, while preserving the
behavioral prose and the describe/it titles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lit deps for waitpoint detail

Route the org-membership authorization gate through the primary client on the
debug, run-inspector, and idempotency-key-reset routes so it matches the cancel
and replay paths and never authorizes against lagging replica state. Restore the
primary-DB org fallback in the replay action's RBAC scope resolver so the scope
is never resolved without an org during replica lag. Pass the full split-read
deps to WaitpointPresenter on the detail route so a waitpoint resident on the new
run-ops DB resolves the same way it does on the list route.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… test's db.server mock

The callback route now resolves the waitpoint via resolveWaitpointThroughReadThrough,
whose module reads runOpsSplitReadEnabled off ~/db.server at import. The test's vi.mock
omitted that export, so the mocked module threw at import and the whole suite failed to
collect. Provide it (split-on) to match the read path the route exercises; ksuid (NEW)
waitpoint ids route to the runOpsNewReplica proxy pointing at the seeded container.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-token test

The "control-plane-resident standalone token is found when the run-ops
replica does not hold it" case relied on the ambient RUN_OPS_SPLIT_ENABLED
env being on. Locally .env sets it (plus distinct TASK_RUN_* URLs) so the
resolver fans out new->legacy and finds the cuid-shaped token. In CI those
vars are unset, so resolveWaitpointThroughReadThrough falls back to
isSplitEnabled() -> false -> single-DB passthrough, which reads only the
run-ops replica (prisma17), never the control-plane legacy replica (prisma14)
that holds the token. The read returned null and the assertion
"expected null not to be null" failed on CI shard 3.

Inject deps.splitEnabled: true (mirroring the sibling read-gate test) so the
fan-out is exercised deterministically regardless of ambient env. Test-only;
preserves exactly what the case verifies (legacy fallback finds a
control-plane token the new replica lacks).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@d-cs d-cs force-pushed the runops/pr08-presenters branch from fa0d473 to 50463ce Compare July 3, 2026 11:08
@d-cs d-cs force-pushed the runops/pr09-routes branch from 0c5b193 to cbf736a Compare July 3, 2026 11:08
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