Repository navigation
[core] Treat events after an entity's terminal event as inert in replay - #4819
Conversation
Once a step outcome, wait_completed, hook_disposed or attr_set has been consumed, a later event under the same correlation id has no consumer left and is now skipped instead of being reported as replay divergence. Covers stragglers such as a step_started or step_retrying written by a stalled step invocation after the outcome, and a hook_received committed behind the hook's disposal. The observability duplicate fold applies the same rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 9ba1eb8 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
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 |
📊 Workflow Benchmarkscommit Backend:
📈 STSO distribution vs main (inline / queue-hop histograms)1020 steps (inline) Cumulative STSO time: main 140168ms → this run 105874ms (Δ -34294ms, -24%) ℹ️ Metric definitions & methodologyThe collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). Best/P75/P90/P99 deltas compare against the most recent benchmark run on Metrics — TTFS: time to first step body (in-deployment start() → first step body) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out 🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor. |
🧪 E2E Test Results✅ All tests passed
|
| Passed | Failed | Skipped | Total | |
|---|---|---|---|---|
| ✅ ▲ Vercel Production | 3982 | 0 | 878 | 4860 |
| ✅ 💻 Local Development | 4669 | 0 | 551 | 5220 |
| ✅ 📦 Local Production | 4669 | 0 | 551 | 5220 |
| ✅ 🐘 Local Postgres | 4669 | 0 | 551 | 5220 |
| ✅ 🪟 Windows | 348 | 0 | 12 | 360 |
| ✅ 🌐 Cross-language Conformance | 68 | 0 | 87 | 155 |
| ✅ dynamic-runs | 0 | 0 | 0 | 0 |
| ✅ vercel-http-transport | 897 | 0 | 183 | 1080 |
| ✅ vercel-multi-region | 27 | 0 | 0 | 27 |
| ✅ vercel-ws-transport | 607 | 0 | 113 | 720 |
| Total | 19936 | 0 | 2926 | 22862 |
Details by Category
✅ ▲ Vercel Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-node | 145 | 0 | 35 |
| ✅ astro-quickjs | 145 | 0 | 35 |
| ✅ example-node | 145 | 0 | 35 |
| ✅ example-quickjs | 145 | 0 | 35 |
| ✅ express-node | 145 | 0 | 35 |
| ✅ express-quickjs | 145 | 0 | 35 |
| ✅ fastify-node | 145 | 0 | 35 |
| ✅ fastify-quickjs | 145 | 0 | 35 |
| ✅ hono-node | 145 | 0 | 35 |
| ✅ hono-quickjs | 145 | 0 | 35 |
| ✅ nest-node | 145 | 0 | 35 |
| ✅ nest-quickjs | 145 | 0 | 35 |
| ✅ nextjs-turbopack-node | 172 | 0 | 8 |
| ✅ nextjs-turbopack-quickjs | 172 | 0 | 8 |
| ✅ nextjs-webpack-node | 172 | 0 | 8 |
| ✅ nextjs-webpack-quickjs | 172 | 0 | 8 |
| ✅ nitro-node | 145 | 0 | 35 |
| ✅ nitro-quickjs | 145 | 0 | 35 |
| ✅ nuxt-node | 145 | 0 | 35 |
| ✅ nuxt-quickjs | 145 | 0 | 35 |
| ✅ python-node | 66 | 0 | 114 |
| ✅ sveltekit-node | 164 | 0 | 16 |
| ✅ sveltekit-quickjs | 164 | 0 | 16 |
| ✅ tanstack-start-node | 145 | 0 | 35 |
| ✅ tanstack-start-quickjs | 145 | 0 | 35 |
| ✅ vite-node | 145 | 0 | 35 |
| ✅ vite-quickjs | 145 | 0 | 35 |
✅ 💻 Local Development
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 151 | 0 | 29 |
| ✅ astro-stable-quickjs | 151 | 0 | 29 |
| ✅ express-stable-node | 151 | 0 | 29 |
| ✅ express-stable-quickjs | 151 | 0 | 29 |
| ✅ fastify-stable-node | 151 | 0 | 29 |
| ✅ fastify-stable-quickjs | 151 | 0 | 29 |
| ✅ hono-stable-node | 151 | 0 | 29 |
| ✅ hono-stable-quickjs | 151 | 0 | 29 |
| ✅ nest-stable-node | 151 | 0 | 29 |
| ✅ nest-stable-quickjs | 151 | 0 | 29 |
| ✅ nextjs-turbopack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-turbopack-quickjs-snapshot | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 179 | 0 | 1 |
| ✅ nitro-stable-node | 151 | 0 | 29 |
| ✅ nitro-stable-quickjs | 151 | 0 | 29 |
| ✅ nuxt-stable-node | 151 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 151 | 0 | 29 |
| ✅ sveltekit-stable-node | 170 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 170 | 0 | 10 |
| ✅ tanstack-start-node | 151 | 0 | 29 |
| ✅ tanstack-start-quickjs | 151 | 0 | 29 |
| ✅ vite-stable-node | 151 | 0 | 29 |
| ✅ vite-stable-quickjs | 151 | 0 | 29 |
✅ 📦 Local Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 151 | 0 | 29 |
| ✅ astro-stable-quickjs | 151 | 0 | 29 |
| ✅ express-stable-node | 151 | 0 | 29 |
| ✅ express-stable-quickjs | 151 | 0 | 29 |
| ✅ fastify-stable-node | 151 | 0 | 29 |
| ✅ fastify-stable-quickjs | 151 | 0 | 29 |
| ✅ hono-stable-node | 151 | 0 | 29 |
| ✅ hono-stable-quickjs | 151 | 0 | 29 |
| ✅ nest-stable-node | 151 | 0 | 29 |
| ✅ nest-stable-quickjs | 151 | 0 | 29 |
| ✅ nextjs-turbopack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-turbopack-quickjs-snapshot | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 179 | 0 | 1 |
| ✅ nitro-stable-node | 151 | 0 | 29 |
| ✅ nitro-stable-quickjs | 151 | 0 | 29 |
| ✅ nuxt-stable-node | 151 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 151 | 0 | 29 |
| ✅ sveltekit-stable-node | 170 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 170 | 0 | 10 |
| ✅ tanstack-start-node | 151 | 0 | 29 |
| ✅ tanstack-start-quickjs | 151 | 0 | 29 |
| ✅ vite-stable-node | 151 | 0 | 29 |
| ✅ vite-stable-quickjs | 151 | 0 | 29 |
✅ 🐘 Local Postgres
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 151 | 0 | 29 |
| ✅ astro-stable-quickjs | 151 | 0 | 29 |
| ✅ express-stable-node | 151 | 0 | 29 |
| ✅ express-stable-quickjs | 151 | 0 | 29 |
| ✅ fastify-stable-node | 151 | 0 | 29 |
| ✅ fastify-stable-quickjs | 151 | 0 | 29 |
| ✅ hono-stable-node | 151 | 0 | 29 |
| ✅ hono-stable-quickjs | 151 | 0 | 29 |
| ✅ nest-stable-node | 151 | 0 | 29 |
| ✅ nest-stable-quickjs | 151 | 0 | 29 |
| ✅ nextjs-turbopack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-turbopack-quickjs-snapshot | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-node | 179 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 179 | 0 | 1 |
| ✅ nitro-stable-node | 151 | 0 | 29 |
| ✅ nitro-stable-quickjs | 151 | 0 | 29 |
| ✅ nuxt-stable-node | 151 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 151 | 0 | 29 |
| ✅ sveltekit-stable-node | 170 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 170 | 0 | 10 |
| ✅ tanstack-start-node | 151 | 0 | 29 |
| ✅ tanstack-start-quickjs | 151 | 0 | 29 |
| ✅ vite-stable-node | 151 | 0 | 29 |
| ✅ vite-stable-quickjs | 151 | 0 | 29 |
✅ 🪟 Windows
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 174 | 0 | 6 |
| ✅ nextjs-turbopack-quickjs | 174 | 0 | 6 |
✅ 🌐 Cross-language Conformance
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ python | 68 | 0 | 87 |
✅ dynamic-runs
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-vercel | 0 | 0 | 0 |
| ✅ example-vercel | 0 | 0 | 0 |
| ✅ express-vercel | 0 | 0 | 0 |
| ✅ fastify-vercel | 0 | 0 | 0 |
| ✅ hono-vercel | 0 | 0 | 0 |
| ✅ nest-vercel | 0 | 0 | 0 |
| ✅ nextjs-turbopack-vercel | 0 | 0 | 0 |
| ✅ nextjs-webpack-vercel | 0 | 0 | 0 |
| ✅ nitro-vercel | 0 | 0 | 0 |
| ✅ nuxt-vercel | 0 | 0 | 0 |
| ✅ sveltekit-vercel | 0 | 0 | 0 |
| ✅ tanstack-start-vercel | 0 | 0 | 0 |
| ✅ vite-vercel | 0 | 0 | 0 |
✅ vercel-http-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 145 | 0 | 35 |
| ✅ express | 145 | 0 | 35 |
| ✅ hono | 145 | 0 | 35 |
| ✅ nextjs-turbopack | 172 | 0 | 8 |
| ✅ nitro | 145 | 0 | 35 |
| ✅ vite | 145 | 0 | 35 |
✅ vercel-multi-region
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack | 27 | 0 | 0 |
✅ vercel-ws-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 145 | 0 | 35 |
| ✅ express | 145 | 0 | 35 |
| ✅ nextjs-turbopack | 172 | 0 | 8 |
| ✅ vite | 145 | 0 | 35 |
Sim WorldSimulated world deterministic testing for races. Traces 🟢 world-sim scenario book — 0 fail of 42 total
Full trace: |
About these numbersSizes are gzip; parentheses show the change against
|
karthikscale3
left a comment
There was a problem hiding this comment.
AI Review: No blocking issues found. Reviewed and tested commit 9ba1eb8.
Built and packed this SDK into a standalone Next.js 16.3.8 production application on Node.js 24.13.1. With both Node and QuickJS workflow engines using the local World, checked 12 parallel steps, an intentional retry, durable sleep, caught FatalError, hook receipt/disposal, and restart replay. Filesystem audits confirmed completed steps were not executed again.
Created paused runs with the base SDK, stopped the app, and injected late step_started/step_retrying events after a completed step plus hook_received after hook disposal. The base Node SDK rejected this history with CORRUPTED_EVENT_LOG. Restored the identical pre-replay snapshot, upgraded to this PR, and verified that the pending hook resumed, the original hook payload/result stayed intact, and completed-step side effects were not repeated. Also checked the late-event history with QuickJS and passed 105 focused EventsConsumer, terminal-replay and duplicate-classification tests.
CI currently has 164 successful checks and 6 skipped checks. Scope: local World with controlled persisted-history injection; deployed Vercel delivery and performance benchmarks were not tested.
|
No backport to This is a genuine correctness fix (it stops To override, re-run the Backport to stable workflow manually via |
Catches the docs up with SDK changes that shipped in the last day without (complete) docs updates. | Change | Source PR | Docs edit | | --- | --- | --- | | QuickJS runs with snapshotting are exempt from the default per-run event limit on the Local and Vercel Worlds, and `MAX_EVENTS_EXCEEDED` / `REPLAY_TIMEOUT` now have their own error pages | #4870 | The error-code table in Errors and retries, the log-growth note in Event sourcing, and the event-limit row in What's new now mention the QuickJS exemption and link to the new error pages. #4870 updated the errors index, Runtime tuning, Worlds, and Vercel World pages but not these | I reviewed the other PRs from the last day and they need no further docs: removing the experimental resilient step dispatch (#4818) updated its own docs and the variable no longer appears outside changelogs, the `setAttributes()` delivery-order fix (#4861) and terminal-inert replay (#4819) are bug fixes, reading step creation from `step_started` (#4866) is reader-side forward compatibility with no user-facing change yet, and the devalue upgrade (#4856), dependency bumps, and Version Packages PRs aren't documented behavior. The Workflows pricing and limits page in `vercel/front` gets the matching event-limit note in vercel/front#91076. ## Docs Preview | Page | Preview | | --- | --- | | Errors and retries | [Error codes](https://workflow-docs-git-workflow-docs-cleanup-wed-14-oct.vercel.sh/docs/foundations/errors-and-retries) | | Event sourcing | [How fast a log grows](https://workflow-docs-git-workflow-docs-cleanup-wed-14-oct.vercel.sh/docs/how-it-works/event-sourcing#how-fast-a-log-grows) | | What's new | [Breaking changes: Application code](https://workflow-docs-git-workflow-docs-cleanup-wed-14-oct.vercel.sh/docs/whats-new#application-code) | 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @workflow/astro@5.0.6 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.6 - @workflow/rollup@5.0.6 - @workflow/vite@5.0.6 ## @workflow/builders@5.0.6 ### Patch Changes - Updated dependencies [[`91297ab`](91297ab), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917)]: - @workflow/core@5.2.3 - @workflow/errors@5.0.5 ## @workflow/cli@5.0.6 ### Patch Changes - [#4758](#4758) [`0bc17d7`](0bc17d7) Thanks [@pranaygp](https://github.com/pranaygp)! - Resolve a run's encryption key from metadata only when decrypting in `workflow inspect --decrypt` and the web UI, instead of making the server resolve the run's whole input and output for each lookup. - Updated dependencies [[`91297ab`](91297ab), [`5170f8f`](5170f8f), [`0bc17d7`](0bc17d7), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917), [`a067ef7`](a067ef7)]: - @workflow/core@5.2.3 - @workflow/world-local@5.1.2 - @workflow/web@5.0.6 - @workflow/world@5.0.5 - @workflow/world-vercel@5.2.3 - @workflow/errors@5.0.5 - @workflow/builders@5.0.6 ## @workflow/core@5.2.3 ### Patch Changes - [#4335](#4335) [`91297ab`](91297ab) Thanks [@pranaygp](https://github.com/pranaygp)! - Security: remove the `nanoid` dependency (pinned at 5.1.6, affected by [GHSA-28wg-ghj8-5hjv](GHSA-28wg-ghj8-5hjv)). Default hook tokens now come from a built-in copy of the same generator, so tokens and seeded random values stay identical for runs recorded on earlier versions. Upgrading `nanoid` itself would have changed them, because `nanoid@5.1.16` draws a different number of random bytes per id. - [#4758](#4758) [`0bc17d7`](0bc17d7) Thanks [@pranaygp](https://github.com/pranaygp)! - Resolve a run's encryption key from metadata only, so `Run#getReadable()` and resuming a hook stored without a resume context no longer make the server resolve the run's whole input and output first. - [#4916](#4916) [`42077f2`](42077f2) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Fix cross-deployment `start()` to 4.x deployments being misread as a run invocation, and keep replayed runs in the original run's region. - [#4870](#4870) [`59b76f8`](59b76f8) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Stamp the default snapshot threshold into QuickJS runs at `start()` so they qualify for the event limit exemption, warn when a run approaches the event limit, and recommend `WORKFLOW_VM=quickjs` in `MAX_EVENTS_EXCEEDED` and `REPLAY_TIMEOUT` errors - [#4866](#4866) [`73a7efa`](73a7efa) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Read event logs where a step's creation is recorded on its `step_started` alone, with no separate `step_created`. - [#4874](#4874) [`2163290`](2163290) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Preserve `workflowVm` and `snapshotThreshold` from `executionContext` in `hydrateResourceIO` for observability UIs - [#4895](#4895) [`0d434fc`](0d434fc) Thanks [@karthikscale3](https://github.com/karthikscale3)! - Avoid redundant history reads when independent hooks register concurrently. Keep existing single-event requests parallel and reconcile their committed events together before continuing, with the normal history reload when the responses do not cover a complete consecutive suffix. - [#4819](#4819) [`cd2f917`](cd2f917) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Ignore events a step or hook writes after its outcome during replay instead of failing the run - Updated dependencies [[`5170f8f`](5170f8f), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`0d434fc`](0d434fc), [`a067ef7`](a067ef7)]: - @workflow/world-local@5.1.2 - @workflow/world@5.0.5 - @workflow/world-vercel@5.2.3 - @workflow/errors@5.0.5 ## @workflow/errors@5.0.5 ### Patch Changes - [#4870](#4870) [`59b76f8`](59b76f8) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Add a QuickJS snapshotting hint and docs link to `MaxEventsExceededError` - Updated dependencies [[`42077f2`](42077f2), [`59b76f8`](59b76f8), [`0d434fc`](0d434fc)]: - @workflow/world@5.0.5 ## @workflow/nest@5.1.4 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.6 ## @workflow/next@5.0.6 ### Patch Changes - Updated dependencies [[`91297ab`](91297ab), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917)]: - @workflow/core@5.2.3 - @workflow/builders@5.0.6 ## @workflow/nitro@5.0.6 ### Patch Changes - Updated dependencies [[`91297ab`](91297ab), [`0bc17d7`](0bc17d7), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917)]: - @workflow/core@5.2.3 - @workflow/web@5.0.6 - @workflow/builders@5.0.6 - @workflow/rollup@5.0.6 - @workflow/vite@5.0.6 ## @workflow/nuxt@5.0.6 ### Patch Changes - Updated dependencies []: - @workflow/nitro@5.0.6 ## @workflow/rollup@5.0.6 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.6 ## @workflow/sveltekit@5.0.6 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.6 - @workflow/rollup@5.0.6 - @workflow/vite@5.0.6 ## @workflow/vite@5.0.6 ### Patch Changes - Updated dependencies []: - @workflow/builders@5.0.6 ## @workflow/vitest@5.0.6 ### Patch Changes - Updated dependencies [[`91297ab`](91297ab), [`5170f8f`](5170f8f), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917), [`a067ef7`](a067ef7)]: - @workflow/core@5.2.3 - @workflow/world-local@5.1.2 - @workflow/world@5.0.5 - @workflow/builders@5.0.6 - @workflow/rollup@5.0.6 ## @workflow/web@5.0.6 ### Patch Changes - [#4758](#4758) [`0bc17d7`](0bc17d7) Thanks [@pranaygp](https://github.com/pranaygp)! - Resolve a run's encryption key from metadata only when decrypting in `workflow inspect --decrypt` and the web UI, instead of making the server resolve the run's whole input and output for each lookup. - [#4874](#4874) [`2163290`](2163290) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Recommend the QuickJS VM with snapshotting on runs that failed with `MAX_EVENTS_EXCEEDED` or `REPLAY_TIMEOUT` - Updated dependencies [[`5170f8f`](5170f8f), [`59b76f8`](59b76f8), [`a067ef7`](a067ef7)]: - @workflow/world-local@5.1.2 ## @workflow/web-shared@5.0.6 ### Patch Changes - [#4866](#4866) [`73a7efa`](73a7efa) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Read event logs where a step's creation is recorded on its `step_started` alone, with no separate `step_created`. - [#4874](#4874) [`2163290`](2163290) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Add `getRunLimitGuidance()` for runs that failed with `MAX_EVENTS_EXCEEDED` or `REPLAY_TIMEOUT`, and show the run's workflow VM in the sidebar - [#4819](#4819) [`cd2f917`](cd2f917) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Ignore events a step or hook writes after its outcome during replay instead of failing the run - Updated dependencies [[`91297ab`](91297ab), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917)]: - @workflow/core@5.2.3 - @workflow/world@5.0.5 ## workflow@5.2.3 ### Patch Changes - Updated dependencies [[`91297ab`](91297ab), [`0bc17d7`](0bc17d7), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917)]: - @workflow/core@5.2.3 - @workflow/cli@5.0.6 - @workflow/errors@5.0.5 - @workflow/next@5.0.6 - @workflow/nitro@5.0.6 - @workflow/typescript-plugin@5.0.0 - @workflow/astro@5.0.6 - @workflow/nest@5.1.4 - @workflow/rollup@5.0.6 - @workflow/sveltekit@5.0.6 - @workflow/nuxt@5.0.6 ## @workflow/world@5.0.5 ### Patch Changes - [#4916](#4916) [`42077f2`](42077f2) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Fix cross-deployment `start()` to 4.x deployments being misread as a run invocation, and keep replayed runs in the original run's region. - [#4870](#4870) [`59b76f8`](59b76f8) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Exempt QuickJS runs with a positive snapshot threshold from the default per-run event limit, matching the Vercel World - [#4895](#4895) [`0d434fc`](0d434fc) Thanks [@karthikscale3](https://github.com/karthikscale3)! - Avoid redundant history reads when independent hooks register concurrently. Keep existing single-event requests parallel and reconcile their committed events together before continuing, with the normal history reload when the responses do not cover a complete consecutive suffix. ## @workflow/world-local@5.1.2 ### Patch Changes - [#4922](#4922) [`5170f8f`](5170f8f) Thanks [@NishitaSikka](https://github.com/NishitaSikka)! - Restore missing `run_created` events for interrupted local run creation. - [#4870](#4870) [`59b76f8`](59b76f8) Thanks [@TooTallNate](https://github.com/TooTallNate)! - Exempt QuickJS runs with a positive snapshot threshold from the default per-run event limit, matching the Vercel World - [#4924](#4924) [`a067ef7`](a067ef7) Thanks [@AndrewBarba](https://github.com/AndrewBarba)! - Fix an edge case in `createHook({ experimental_force: true })` where a racing `resumeHook` could be dropped during the handover - Updated dependencies [[`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`0d434fc`](0d434fc)]: - @workflow/world@5.0.5 - @workflow/errors@5.0.5 ## @workflow/world-postgres@5.0.5 ### Patch Changes - [#4883](#4883) [`93396c5`](93396c5) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Write `run_started`, `run_completed`, `run_failed`, `run_cancelled`, `attr_set`, `step_retrying`, `wait_created` and `wait_completed` in one transaction with the entity change they record. A process that died between the two used to leave an entity in a state its event log did not explain; a completed wait with no `wait_completed` made the run retry the wait forever. A wait left in that state by an earlier version now gets its missing event on the next attempt. - [#4882](#4882) [`ecaa047`](ecaa047) Thanks [@dretanacr](https://github.com/dretanacr)! - Write a step's `step_completed` or `step_failed` row update and its event in one transaction, as `step_started` already does. A process that died between the two used to leave a terminal step with no terminal event, so the replayed `step_started` was refused as terminal and the run never advanced. - [#4812](#4812) [`32f213e`](32f213e) Thanks [@pranaygp](https://github.com/pranaygp)! - Keep a checked-out client in the World's own `pg` pool from ending the process when the database drops its connection. - [#4757](#4757) [`7882c81`](7882c81) Thanks [@pranaygp](https://github.com/pranaygp)! - Store timestamps as `timestamp with time zone` so `createdAt` is correct when the Postgres server or session is not in UTC. Migration 0027 converts the columns and repairs `created_at` values written by earlier versions. - Updated dependencies [[`5170f8f`](5170f8f), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`0d434fc`](0d434fc), [`a067ef7`](a067ef7)]: - @workflow/world-local@5.1.2 - @workflow/world@5.0.5 - @workflow/errors@5.0.5 ## @workflow/world-testing@5.0.6 ### Patch Changes - Updated dependencies [[`91297ab`](91297ab), [`0bc17d7`](0bc17d7), [`0bc17d7`](0bc17d7), [`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`73a7efa`](73a7efa), [`2163290`](2163290), [`0d434fc`](0d434fc), [`cd2f917`](cd2f917)]: - @workflow/core@5.2.3 - @workflow/cli@5.0.6 - @workflow/world@5.0.5 - workflow@5.2.3 ## @workflow/world-vercel@5.2.3 ### Patch Changes - [#4916](#4916) [`42077f2`](42077f2) Thanks [@VaguelySerious](https://github.com/VaguelySerious)! - Fix cross-deployment `start()` to 4.x deployments being misread as a run invocation, and keep replayed runs in the original run's region. - [#4895](#4895) [`0d434fc`](0d434fc) Thanks [@karthikscale3](https://github.com/karthikscale3)! - Avoid redundant history reads when independent hooks register concurrently. Keep existing single-event requests parallel and reconcile their committed events together before continuing, with the normal history reload when the responses do not cover a complete consecutive suffix. - Updated dependencies [[`42077f2`](42077f2), [`59b76f8`](59b76f8), [`59b76f8`](59b76f8), [`0d434fc`](0d434fc)]: - @workflow/world@5.0.5 - @workflow/errors@5.0.5 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
(AI) Once an entity's terminal event (
step_completed/step_failed,wait_completed,hook_disposed,attr_set; never the run entity) has been consumed in replay, every later event under the same correlation id is inert, whatever its class.Why. A step invocation that stalled past its queue lease can write a
step_startedorstep_retryingafter another invocation recorded the step's outcome, and ahook_receivedcan commit behind the hook'shook_disposed. Onmainsuch an event repeats no class the walk has seen, so nothing claims it and the node:vm engine reportsReplay could not consume event(CORRUPTED_EVENT_LOG). Now it is skipped like any other straggler. The observability duplicate fold (@workflow/web-shared) and the shared fixtures in@workflow/world/test-supportapply the same rule; the fold no longer stops at what used to be a divergence point.Determinism for existing runs.
EventsConsumerconsults the closed-entity map only after every registered callback declined the event, and only after the existing first-of-class check. Any event a consumer claims, and any repeat already skipped today, is handled exactly as before; a retry sequence (step_started,step_retrying,step_started,step_completed) is all claimed by the still-open step consumer.maineither. The only way it could still be claimed is a later consumer registering under the same correlation id, and correlation ids are minted per body call from a monotonic ULID with a kind prefix (step_/wait_/hook_/attr_), so a closed id never gets a second consumer. Onmainsuch an event therefore always ended in divergence (directly, or as a stranded parked event once the workflow returned); the change only turns that failure into continued replay. Skipped events do not move the replay clock, as with every other skip.QuickJS. The QuickJS engine has no divergence detection. On
mainit already reads past all three straggler shapes (they only mark a pending op created, or buffer a payload nobody reads), so this change aligns the node:vm engine with what QuickJS already does. No QuickJS change is made here. #4661 also skips post-terminal events insideprocessEvents, but its per-VM closed set persists acrossprocessEvents' fixed-point re-scans, so a re-scan skips a closed step'sstep_createdand leaves the step's pending op unmarked when its terminal was buffered before the VM called it (checked locally: the completed step resurfaces as pending-to-create). Left out for that reason. Remaining pre-existing differences, not changed here: QuickJS advances its clock on every event it reads, including stragglers, and a payload buffered afterhook_disposedcan still be returned by anawait hookwritten afterdispose().Composes with #4683: that classification already treats a disposed hook as closed and counts other step executors'
step_retryingas possibly path-changing. With this rule a post-outcome step event can no longer change a decision (onmainit failed the run).Tests.
terminal-inert-replay.test.tsreplays the same logs through both engines: start and retry after a completed step, start after a failed step, payload after disposal, wait event after completion, an unchanged retry sequence, and an unrelated entity that still diverges. Againstmain'sEventsConsumerthe three node:vm straggler cases fail and QuickJS passes. PlusEventsConsumerunit tests and the updated shared fixtures.Extracted from #4661.
🤖 Generated with Claude Code