Repository navigation
[core] Allow run_started to record input in absence of run_created - #5270
Conversation
Forward-compat for spec 9, where a resilient start's run_started that creates the run can be written alone at the run's first slot, carrying the creation data, with no separate run_created. Reader-side support only: nothing in this SDK asks a World to write this shape yet. The Vercel backend writes it only for a run_started that opts in (vercel/workflow-server#1372). Readers recognize the creation by position, not by its input (isRunCreationEvent: a run_created, or a run_started at the first slot), since only a creation can hold slot 1 and a replay page may omit payloads. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Peter Wielander <29887157+VaguelySerious@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 8660cfc The changes in this PR will be included in the next version bump. This PR includes changesets to release 21 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 99488ms → this run 107573ms (Δ +8085ms, +8%) ℹ️ 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 | 4619 | 0 | 916 | 5535 |
| ✅ 💻 Local Development | 5379 | 0 | 566 | 5945 |
| ✅ 📦 Local Production | 5379 | 0 | 566 | 5945 |
| ✅ 🐘 Local Postgres | 5379 | 0 | 566 | 5945 |
| ✅ 🪟 Windows | 397 | 0 | 13 | 410 |
| ✅ 🌐 Cross-language Conformance | 68 | 0 | 112 | 180 |
| ✅ dynamic-runs | 0 | 0 | 0 | 0 |
| ✅ local-postgres-spec9 | 0 | 0 | 360 | 360 |
| ✅ vercel-http-transport | 1047 | 0 | 183 | 1230 |
| ✅ vercel-multi-region | 27 | 0 | 0 | 27 |
| ✅ vercel-spec9 | 391 | 0 | 19 | 410 |
| ✅ vercel-ws-transport | 707 | 0 | 113 | 820 |
| Total | 23393 | 0 | 3414 | 26807 |
Details by Category
✅ ▲ Vercel Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-node | 170 | 0 | 35 |
| ✅ astro-quickjs | 169 | 0 | 36 |
| ✅ example-node | 170 | 0 | 35 |
| ✅ example-quickjs | 169 | 0 | 36 |
| ✅ express-node | 170 | 0 | 35 |
| ✅ express-quickjs | 169 | 0 | 36 |
| ✅ fastify-node | 170 | 0 | 35 |
| ✅ fastify-quickjs | 169 | 0 | 36 |
| ✅ hono-node | 170 | 0 | 35 |
| ✅ hono-quickjs | 169 | 0 | 36 |
| ✅ nest-node | 170 | 0 | 35 |
| ✅ nest-quickjs | 169 | 0 | 36 |
| ✅ nextjs-turbopack-node | 197 | 0 | 8 |
| ✅ nextjs-turbopack-quickjs | 196 | 0 | 9 |
| ✅ nextjs-webpack-node | 197 | 0 | 8 |
| ✅ nextjs-webpack-quickjs | 196 | 0 | 9 |
| ✅ nitro-node | 170 | 0 | 35 |
| ✅ nitro-quickjs | 169 | 0 | 36 |
| ✅ nuxt-node | 170 | 0 | 35 |
| ✅ nuxt-quickjs | 169 | 0 | 36 |
| ✅ python-node | 66 | 0 | 139 |
| ✅ sveltekit-node | 189 | 0 | 16 |
| ✅ sveltekit-quickjs | 188 | 0 | 17 |
| ✅ tanstack-start-node | 170 | 0 | 35 |
| ✅ tanstack-start-quickjs | 169 | 0 | 36 |
| ✅ vite-node | 170 | 0 | 35 |
| ✅ vite-quickjs | 169 | 0 | 36 |
✅ 💻 Local Development
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 176 | 0 | 29 |
| ✅ astro-stable-quickjs | 175 | 0 | 30 |
| ✅ express-stable-node | 176 | 0 | 29 |
| ✅ express-stable-quickjs | 175 | 0 | 30 |
| ✅ fastify-stable-node | 176 | 0 | 29 |
| ✅ fastify-stable-quickjs | 175 | 0 | 30 |
| ✅ hono-stable-node | 176 | 0 | 29 |
| ✅ hono-stable-quickjs | 175 | 0 | 30 |
| ✅ nest-stable-node | 176 | 0 | 29 |
| ✅ nest-stable-quickjs | 175 | 0 | 30 |
| ✅ nextjs-turbopack-canary-node | 204 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 203 | 0 | 2 |
| ✅ nextjs-turbopack-quickjs-snapshot | 203 | 0 | 2 |
| ✅ nextjs-turbopack-stable-node | 204 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 203 | 0 | 2 |
| ✅ nextjs-webpack-canary-node | 204 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 203 | 0 | 2 |
| ✅ nextjs-webpack-stable-node | 204 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 203 | 0 | 2 |
| ✅ nitro-stable-node | 176 | 0 | 29 |
| ✅ nitro-stable-quickjs | 175 | 0 | 30 |
| ✅ nuxt-stable-node | 176 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 175 | 0 | 30 |
| ✅ sveltekit-stable-node | 195 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 194 | 0 | 11 |
| ✅ tanstack-start-node | 176 | 0 | 29 |
| ✅ tanstack-start-quickjs | 175 | 0 | 30 |
| ✅ vite-stable-node | 176 | 0 | 29 |
| ✅ vite-stable-quickjs | 175 | 0 | 30 |
✅ 📦 Local Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 176 | 0 | 29 |
| ✅ astro-stable-quickjs | 175 | 0 | 30 |
| ✅ express-stable-node | 176 | 0 | 29 |
| ✅ express-stable-quickjs | 175 | 0 | 30 |
| ✅ fastify-stable-node | 176 | 0 | 29 |
| ✅ fastify-stable-quickjs | 175 | 0 | 30 |
| ✅ hono-stable-node | 176 | 0 | 29 |
| ✅ hono-stable-quickjs | 175 | 0 | 30 |
| ✅ nest-stable-node | 176 | 0 | 29 |
| ✅ nest-stable-quickjs | 175 | 0 | 30 |
| ✅ nextjs-turbopack-canary-node | 204 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 203 | 0 | 2 |
| ✅ nextjs-turbopack-quickjs-snapshot | 203 | 0 | 2 |
| ✅ nextjs-turbopack-stable-node | 204 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 203 | 0 | 2 |
| ✅ nextjs-webpack-canary-node | 204 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 203 | 0 | 2 |
| ✅ nextjs-webpack-stable-node | 204 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 203 | 0 | 2 |
| ✅ nitro-stable-node | 176 | 0 | 29 |
| ✅ nitro-stable-quickjs | 175 | 0 | 30 |
| ✅ nuxt-stable-node | 176 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 175 | 0 | 30 |
| ✅ sveltekit-stable-node | 195 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 194 | 0 | 11 |
| ✅ tanstack-start-node | 176 | 0 | 29 |
| ✅ tanstack-start-quickjs | 175 | 0 | 30 |
| ✅ vite-stable-node | 176 | 0 | 29 |
| ✅ vite-stable-quickjs | 175 | 0 | 30 |
✅ 🐘 Local Postgres
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 176 | 0 | 29 |
| ✅ astro-stable-quickjs | 175 | 0 | 30 |
| ✅ express-stable-node | 176 | 0 | 29 |
| ✅ express-stable-quickjs | 175 | 0 | 30 |
| ✅ fastify-stable-node | 176 | 0 | 29 |
| ✅ fastify-stable-quickjs | 175 | 0 | 30 |
| ✅ hono-stable-node | 176 | 0 | 29 |
| ✅ hono-stable-quickjs | 175 | 0 | 30 |
| ✅ nest-stable-node | 176 | 0 | 29 |
| ✅ nest-stable-quickjs | 175 | 0 | 30 |
| ✅ nextjs-turbopack-canary-node | 204 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 203 | 0 | 2 |
| ✅ nextjs-turbopack-quickjs-snapshot | 203 | 0 | 2 |
| ✅ nextjs-turbopack-stable-node | 204 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 203 | 0 | 2 |
| ✅ nextjs-webpack-canary-node | 204 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 203 | 0 | 2 |
| ✅ nextjs-webpack-stable-node | 204 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 203 | 0 | 2 |
| ✅ nitro-stable-node | 176 | 0 | 29 |
| ✅ nitro-stable-quickjs | 175 | 0 | 30 |
| ✅ nuxt-stable-node | 176 | 0 | 29 |
| ✅ nuxt-stable-quickjs | 175 | 0 | 30 |
| ✅ sveltekit-stable-node | 195 | 0 | 10 |
| ✅ sveltekit-stable-quickjs | 194 | 0 | 11 |
| ✅ tanstack-start-node | 176 | 0 | 29 |
| ✅ tanstack-start-quickjs | 175 | 0 | 30 |
| ✅ vite-stable-node | 176 | 0 | 29 |
| ✅ vite-stable-quickjs | 175 | 0 | 30 |
✅ 🪟 Windows
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 199 | 0 | 6 |
| ✅ nextjs-turbopack-quickjs | 198 | 0 | 7 |
✅ 🌐 Cross-language Conformance
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ python | 68 | 0 | 112 |
✅ 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 |
✅ local-postgres-spec9
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-stable-node | 0 | 0 | 180 |
| ✅ nextjs-turbopack-stable-quickjs | 0 | 0 | 180 |
✅ vercel-http-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 170 | 0 | 35 |
| ✅ express | 170 | 0 | 35 |
| ✅ hono | 170 | 0 | 35 |
| ✅ nextjs-turbopack | 197 | 0 | 8 |
| ✅ nitro | 170 | 0 | 35 |
| ✅ vite | 170 | 0 | 35 |
✅ vercel-multi-region
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack | 27 | 0 | 0 |
✅ vercel-spec9
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 196 | 0 | 9 |
| ✅ nextjs-turbopack-quickjs | 195 | 0 | 10 |
✅ vercel-ws-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 170 | 0 | 35 |
| ✅ express | 170 | 0 | 35 |
| ✅ nextjs-turbopack | 197 | 0 | 8 |
| ✅ vite | 170 | 0 | 35 |
Sim WorldSimulated world deterministic testing for races. Traces 🟢 world-sim scenario book — 0 fail of 45 total
Full trace: |
About these numbersSizes are gzip; parentheses show the change against
|
karthikscale3
left a comment
There was a problem hiding this comment.
Reviewed the replay readers, run reconstruction, start-hook binding, and timeline changes. No actionable issues found. Affected packages build successfully and all 183 focused tests pass locally.
|
No backport to This is forward-compat feature work for spec 9 (single orchestrator). It adds a new To override, re-run the Backport to stable workflow manually via |
Forward-compat for spec 9, where a resilient start's
run_startedthat creates the run can be written alone at the run's first slot, carrying the creation data, with no separaterun_created. This is the run-level counterpart of #4866 (step_startedalone as a step's creation).This PR is reader-side only, like #4866: nothing in this SDK asks a World to write this shape yet. The Vercel backend side is vercel/workflow-server#1372. It writes the shape only for a
run_startedthat sendsmeta.createsRun, which no SDK sends today. Sending it, and teaching the local Worlds to write it, come in a follow-up once these readers have shipped.How a reader recognizes the creation:
isRunCreationEvent(new in@workflow/world) is true for arun_created, or for arun_startedat the first slot. It goes by position, not byinput, for two reasons: only a run's creation can hold slot 1 (every World pins or restores it there), and a replay page can be listed without payloads. A first-slotrun_startedwhose input was omitted falls back to the existing sources (queuerunInput, the run record), so the rule never makes a reader worse off.Readers changed:
@workflow/corequickjs-runtime.ts) takes the workflow arguments from the creation event. Today a log withoutrun_createdand a delivery withoutrunInput(any redelivery) throws "no run_created event found".spec9/delivery.ts):resolveRunStartHookreads the start Hook from the creation event.@workflow/world-vercel:reconstructRunFromReplayEvents(therun_started/hook_receivedreplay preloads) rebuilds the run from a creatingrun_started.@workflow/web-shared:run_failedas for any event-sourced run, instead of falling back to the V1 status-only bar.run_startedwith norun_createdas the creation.Turbo: a turbo first delivery holds a
run_createdit builds at slot 1 without loading the log. When the World answers therun_startedat slot 1 instead, the two replay alike. Merging by id keeps the placeholder, so nothing needs replacing; a test covers it.In-band count: a World that writes this shape keeps the count a log with
run_createdwould have. The creation holdsIN_BAND_SEQ_AT_RUN_CREATIONand the start the next position, and therun_startedwrite reportsallocated: 1, because the writer (turbo in particular) cannot infer that from the slot. This is documented onisRunCreationEventandIN_BAND_SEQ_AT_RUN_CREATION. vercel/workflow-server#1372 does this.Tests
world/src/slot-identity.test.ts:isRunCreationEvent.core/src/runtime/spec9/run-creation-shape.runtime.test.ts(node and QuickJS), against anAppendOnlyWorldwith a new test-onlycreationOnStartoption:run_created, whose run record holds no input;runInput, of a log holding only the creatingrun_started.The last two fail on both engines without the core change.
core/src/runtime/spec9/turbo.runtime.test.ts: a turbo resilient start in this shape.core/src/runtime/start-hook-binding.test.ts: the start Hook is read from a first-slotrun_started, and not from one above it.world-vercel/src/events.test.ts: the run is reconstructed from a creatingrun_started(fails without the change).web-shared: the run span start, and run-bar segments for a run withrun_failedand norun_created.Results:
pnpm testpasses in core (4440), world (303), world-vercel (1162), web-shared (252) and world-local (794). Typecheck passes for the touched packages.Not changed: world-local, world-postgres and world-sim always write
run_created, which stays valid.🤖 Generated with Claude Code