Skip to content

fix(core): pre-check deployment affinity before the lazy resume write - #3374

Merged
karthikscale3 merged 1 commit into
mainfrom
kk/lazy-resume-deployment-affinity
Aug 6, 2026
Merged

fix(core): pre-check deployment affinity before the lazy resume write#3374
karthikscale3 merged 1 commit into
mainfrom
kk/lazy-resume-deployment-affinity

Conversation

@karthikscale3

Copy link
Copy Markdown
Contributor

What

Lazy hook resume messages now carry the run's pinned deployment (hookInput.deploymentId, stamped by the producer from its resume context). Before the consumer's hoisted hook_received write, the runtime compares it against the ambient deployment id: a match continues with no run fetch; a mismatch fetches the authoritative run and hands it to the existing deployment-affinity guard, re-enqueuing with the complete hookInput (payload included). The field is optional — older messages skip the pre-check and rely on the authoritative guard as before.

Why

#3345 hoisted the consumer's hook_received write above #2960's deployment-affinity guard, so a misrouted lazy resume wrote its event before the guard could re-route (the misrouted-lazy-resume unit test fails on main). With the pre-check, a misrouted modern resume re-routes with zero event writes, while correctly routed resumes pay no extra fetch or latency.

🤖 Generated with Claude Code

The lazy hook fast path (#3345) hoisted the consumer's hook_received
write above the deployment-affinity guard (#2960), so a misrouted lazy
resume wrote its event before the guard could re-route the delivery.

Stamp the run's pinned deployment on the resume message
(hookInput.deploymentId, from the producer's resume context) and, on
the consumer, compare it against the ambient deployment id immediately
before the fast path: a match continues with no run fetch, a mismatch
fetches the authoritative run and hands it to the existing guard —
which keeps sole ownership of re-route/fail policy and remains the
authoritative protection before replay and step execution. The
re-routed message preserves the complete hookInput (it may hold the
only copy of the resume payload). Older messages without the field, and
worlds without deployment affinity, are unchanged: they skip the
pre-check and rely on the authoritative guard, the pre-guard write
staying convergent per (runId, resumeId).

Fixes the misrouted-lazy-resume unit test broken by the #2960/#3345
ordering: a modern misrouted resume now re-routes with zero event
writes, asserted for both hook_received and run_started.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@karthikscale3
karthikscale3 requested a review from a team as a code owner August 6, 2026 19:43
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Aug 6, 2026 7:46pm
example-nextjs-workflow-webpack Ready Ready Preview Aug 6, 2026 7:46pm
example-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-astro-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-express-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-fastify-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-hono-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-nestjs-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-nitro-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-nuxt-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-python-workflow Error Error Aug 6, 2026 7:46pm
workbench-sveltekit-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-tanstack-start-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workbench-vite-workflow Ready Ready Preview Aug 6, 2026 7:46pm
workflow-docs Ready Ready Preview, v0 Aug 6, 2026 7:46pm
workflow-swc-playground Ready Ready Preview Aug 6, 2026 7:46pm
workflow-tarballs Ready Ready Preview Aug 6, 2026 7:46pm
workflow-web Ready Ready Preview Aug 6, 2026 7:46pm

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3a6dc5b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@workflow/world Minor
@workflow/core Patch
@workflow/cli Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-testing Patch
@workflow/world-vercel Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
workflow Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

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

@TooTallNate TooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at 3a6dc5b (based on current main, 0 behind). This is exactly the right resolution to the #2960#3345 interaction — the "carry the pin on the message" option, implemented with the guard kept authoritative.

Verified locally:

  • The red test on main (re-routes a misrouted lazy hook resume with its payload intact) passes on this branch, and the full core suite is green: 90 files, 1945 passed / 3 expected fail.
  • Provenance of the stamp is correct — this was my main worry. hookInput.deploymentId comes from the hook's resume context, which derives from run.deploymentId (the immutable pin), not the producer's ambient id — it's the same field the producer already trusts for encryption-key derivation (getEncryptionKeyForRun(runId, { deploymentId })). Since a run's pin never changes, the stamp can't go stale.
  • The pre-check is advisory-only and can't make a wrong decision on its own: a mismatch only pays one runs.get and hands the authoritative run to the existing guardDeploymentAffinity, which re-verifies against run.deploymentId — so even a hypothetically wrong stamp costs a fetch, never a false re-route. A match, getDeploymentId() failure, or an older message all fall through to the post-setup guard, which remains the protection before any replay or step execution.
  • Correctly capability-gated: without deploymentAffinity, version-tagged ids (dpl_local@<version>) would make every local resume pay a spurious run fetch — the gate mirrors the guard's own eligibility. And the test asserting runsGet is never called on a correctly routed modern message pins down the zero-cost claim.
  • The re-enqueue payload spreads the complete hookInput (stamp included), so the re-routed message re-checks for free on arrival at the pinned deployment, and the payload — potentially the only copy — survives the hop.
  • The rewritten comment block on the flow-replay guard does what I asked on #3345: the "writes nothing" invariant is now scoped deliberately (restored for modern messages, consciously waived for older ones whose idempotent (runId, resumeId) write stays convergent) instead of being silently false.
  • Single producer publish site, and it's the one stamped ✓. Changeset: @workflow/world minor for the schema field — thank you for applying the convention unprompted.

CI: the workbench-python-workflow deployment failure appears on every currently-open PR (3368/3374/3375) — baseline infra breakage, not this change.

Fast turnaround on a main breakage, with the cheap path staying free and the authoritative path staying authoritative. Approving.

@karthikscale3
karthikscale3 merged commit 439a495 into main Aug 6, 2026
23 of 24 checks passed
@karthikscale3
karthikscale3 deleted the kk/lazy-resume-deployment-affinity branch August 6, 2026 21:22
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.

2 participants