Skip to content

feat(core): experimental in-process VM continuation for inline replay#2966

Draft
VaguelySerious wants to merge 2 commits into
mainfrom
peter/cache-vm-continuation
Draft

feat(core): experimental in-process VM continuation for inline replay#2966
VaguelySerious wants to merge 2 commits into
mainfrom
peter/cache-vm-continuation

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Adds in-process VM continuationon by default, kill switch WORKFLOW_VM_CONTINUATION=0. When active, the inline replay loop keeps a suspended workflow VM alive across iterations and feeds newly-appended events into it, instead of rebuilding the vm.Context and replaying the event log from event 0 on every pass.

Default-on so CI lanes and benchmarks actually exercise and measure the new path. The kill switch restores the previous rebuild-and-replay behavior exactly.

This is the "cache the VM" idea, scoped to the only place it's sound: within a single invocation's inline loop, where the same process owns the appended writes so the durable event log stays authoritative. It does not cache a VM across process/invocation boundaries (sleeps, hooks, redelivery) — there is no live VM to resume there and a snapshot would drift from the log.

How it works

  1. On a step-only suspension, runWorkflow attaches a continuation handle to the thrown WorkflowSuspension.
  2. The inline loop runs the step(s) inline as today, appending their terminal events to the log.
  3. Next iteration, instead of a fresh runWorkflow, it calls continuation.resume(events): EventsConsumer.resume() adopts the authoritative event array and re-drives the still-registered step consumers, resolving the pending step promise so the same live VM advances to its next checkpoint.

The step consumer already stays registered after hitting end-of-events (it only removes itself on terminal events), so resuming is just feeding it the events a fresh replay would have consumed next — the same deterministic call sequence, not restarted. RNG/clock/correlation-id state advance naturally in the live VM, exactly matching what a from-scratch replay reconstructs.

Safety (why this can't corrupt a run)

  • Scoped to step-only suspensions. Hooks / waits / attribute writes / aborts leave continuation unset → the loop falls back to a full replay. Those paths involve out-of-band resume/parallel invocations and delivery-barrier ordering that the replay path handles specially and that aren't validated for continuation here.
  • Prefix-divergence guard. resume() checks that the events already consumed by the live VM are a byte-stable prefix (by eventId) of the authoritative log. Any mismatch throws ReplayDivergenceError; the loop swallows it and falls back to a fresh replay.
  • Kill switch == today. WORKFLOW_VM_CONTINUATION=0 disables the continuation attach entirely; the suspend/complete/error control flow is byte-identical to before (verified by the full suite passing under the kill switch).
  • Worst case == today. Any bail-out reverts to a from-scratch replay in a fresh VM.

Scope / limitations (intentionally not in this PR)

  • Cross-process/invocation continuation (sleeps, hooks, redelivery) — not possible in-memory; excluded by design.
  • Fan-out where not all pending steps run inline still works (the VM re-suspends on the remaining ones, identical to a fresh replay) but gets no extra benefit beyond the inline batch.
  • Hook/wait/attribute suspensions deliberately fall back.

Verification

  • packages/core full suite green with the default (on) and under the kill switch (=0): 1488 passed, 3 pre-existing expected-fails.
  • vm-continuation.test.ts: proves resume() advances a live two-step workflow across appended events in one VM, plus the divergence guard.
  • vm-continuation-loop.test.ts: drives a 2-step sequential workflow through the real workflowEntrypoint loop and asserts createContext (VM construction) is called once by default (resumed per step) vs >1 under the kill switch (rebuilt per replay), and that the dehydrated run_completed output is byte-identical between the two modes.
  • pnpm --filter @workflow/core build + typecheck clean.

Notes

  • Default-on affects production too, not just CI — this env flag is read at runtime everywhere. WORKFLOW_VM_CONTINUATION=0 is the per-deployment kill switch.
  • The e2e / world-vercel lanes exercise continuation for the first time via this PR's default flip; CI results on those lanes are the real signal for the non-world-local paths.
  • Draft / proof-of-concept: demonstrates the mechanism and the sequential hot-path win while keeping the log authoritative and a kill switch away from the prior behavior.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8467bdf

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core Minor
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Minor
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/nuxt Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite 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

@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1453 0 230 1683
✅ 💻 Local Development 1617 0 219 1836
✅ 📦 Local Production 1617 0 219 1836
✅ 🐘 Local Postgres 1617 0 219 1836
✅ 🪟 Windows 153 0 0 153
✅ 📋 Other 894 0 177 1071
✅ vercel-multi-region 27 0 0 27
Total 7378 0 1064 8442

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 126 0 27
✅ example 126 0 27
✅ express 126 0 27
✅ fastify 126 0 27
✅ hono 126 0 27
✅ nextjs-turbopack 150 0 3
✅ nextjs-webpack 150 0 3
✅ nitro 126 0 27
✅ nuxt 126 0 27
✅ sveltekit 145 0 8
✅ vite 126 0 27
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 128 0 25
✅ express-stable 128 0 25
✅ fastify-stable 128 0 25
✅ hono-stable 128 0 25
✅ nextjs-turbopack-canary 134 0 19
✅ nextjs-turbopack-stable 153 0 0
✅ nextjs-webpack-canary 134 0 19
✅ nextjs-webpack-stable 153 0 0
✅ nitro-stable 128 0 25
✅ nuxt-stable 128 0 25
✅ sveltekit-stable 147 0 6
✅ vite-stable 128 0 25
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 153 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 25
✅ e2e-local-dev-tanstack-start- 128 0 25
✅ e2e-local-postgres-nest-stable 128 0 25
✅ e2e-local-postgres-tanstack-start- 128 0 25
✅ e2e-local-prod-nest-stable 128 0 25
✅ e2e-local-prod-tanstack-start- 128 0 25
✅ e2e-vercel-prod-tanstack-start 126 0 27
✅ vercel-multi-region
App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

Comment thread packages/core/src/workflow.ts
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Deployment failed with the following error:

Creating the Deployment Timed Out.

@vercel

vercel Bot commented Jul 16, 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, Comment Jul 17, 2026 10:28pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jul 17, 2026 10:28pm
example-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-astro-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-express-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-fastify-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-hono-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-nitro-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workbench-vite-workflow Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jul 17, 2026 10:28pm
workflow-swc-playground Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workflow-tarballs Ready Ready Preview, Comment Jul 17, 2026 10:28pm
workflow-web Ready Ready Preview, Comment Jul 17, 2026 10:28pm

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 8467bdf · Fri, 17 Jul 2026 22:36:10 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Avg (ms) P10 (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1349 (+22%) 1278 1357 🔴 1376 🔴 1700 🔴 30
TTFS stream 1387 (+26%) 1299 1432 🔴 1481 🔴 1724 🔴 30
TTFS hook + stream 1625 (+11%) 1542 1666 🔴 1729 🔴 1806 🔴 30
STSO 1020 steps (1-20) 210 (-26%) 120 192 🔴 524 🔴 623 🔴 19
STSO 1020 steps (101-120) 137 (-58%) 119 144 🔴 179 🔴 189 🔴 19
STSO 1020 steps (1001-1020) 147 (-75%) 122 142 🔴 197 🔴 358 🔴 19
WO 1020 steps 149007 (-65%) 149007 149007 149007 149007 1
SL stream latency 279 (+65%) 125 186 🔴 322 🔴 3298 🔴 30
📜 Previous results (2)

ff3959b

Fri, 17 Jul 2026 17:06:35 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P10 (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1789 (+65%) 1765 🔴 1890 🔴 2993 🔴 30
TTFS hook + stream 1989 (+51%) 2032 🔴 2148 🔴 2188 🔴 30
STSO 1020 steps (1-20) 181 (-35%) 189 🔴 252 🔴 335 🔴 19
STSO 1020 steps (101-120) 270 (-29%) 288 🔴 316 🔴 336 🔴 19
STSO 1020 steps (1001-1020) 170 (-86%) 191 🔴 202 🔴 227 🔴 19
WO stream 1789 (+65%) 1765 1890 2993 30
WO hook + stream 1989 (+51%) 2032 2148 2188 30
SL stream 3567 (+226%) 6078 🔴 6192 🔴 6349 🔴 30
SL hook + stream 5197 (+202%) 5832 🔴 6020 🔴 6853 🔴 30

ff3959b

Fri, 17 Jul 2026 16:25:18 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Avg (ms) P10 (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS stream 1292 (+19%) 1632 🔴 1692 🔴 1838 🔴 30
TTFS hook + stream 1681 (+27%) 1890 🔴 2035 🔴 2187 🔴 30
STSO 1020 steps (1-20) 168 (-40%) 178 🔴 190 🔴 265 🔴 19
STSO 1020 steps (101-120) 285 (-25%) 339 🔴 397 🔴 438 🔴 19
STSO 1020 steps (1001-1020) 267 (-78%) 254 🔴 261 🔴 728 🔴 19
WO stream 1292 (+19%) 1632 1692 1838 30
WO hook + stream 1681 (+27%) 1890 2035 2187 30
SL stream 3521 (+222%) 5763 🔴 5868 🔴 5956 🔴 30
SL hook + stream 4469 (+159%) 5681 🔴 5788 🔴 5891 🔴 30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

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 · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

NathanColosimo added a commit that referenced this pull request Jul 17, 2026
Combines the retained-session architecture from #2984 with the env kill
switch and loop-level single-VM test from #2966.

- executeWorkflow with discriminated request/result types and a
  WorkflowSession state machine (running/suspended/failed/replay/completed)
- EventsConsumer.append: only newly durable events feed the live VM
- WORKFLOW_RETAINED_VM=0 kill switch (default on)
- retained-vm-loop.test.ts: proves one VM per run and byte-identical
  output vs the from-scratch replay path
VaguelySerious and others added 2 commits July 17, 2026 15:23
Add an opt-in path (WORKFLOW_VM_CONTINUATION=1, off by default) that keeps a
suspended workflow VM alive across inline-loop iterations instead of rebuilding
the vm.Context and replaying the event log from scratch each pass.

On a step-only suspension, runWorkflow attaches a `continuation` handle to the
WorkflowSuspension. The inline loop, after appending the step's terminal events,
feeds them into the SAME live VM via EventsConsumer.resume(), resolving the
pending step promise so the workflow body advances to its next checkpoint.

Scope and safety:
- Only within a single invocation's inline loop (same process owns the writes,
  so the durable event log stays authoritative).
- Only for step-only suspensions; hooks/waits/attribute writes fall back to a
  full replay (they involve out-of-band invocations and delivery-barrier
  ordering the replay path handles specially).
- resume() prefix-checks the consumed events against the authoritative log by
  eventId and throws ReplayDivergenceError on any mismatch; the loop swallows
  it and falls back to a fresh replay. Worst case == today's behavior.

Verified: full core suite green with the flag both off and on (1488 passed);
a loop-level test proves the VM is constructed once (resumed per step) with the
flag on vs rebuilt per replay off, producing byte-identical output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flip the flag to default-on so CI and benchmarks exercise and measure the
continuation path. `WORKFLOW_VM_CONTINUATION=0` reverts to rebuilding the VM
and replaying from scratch. Updates the loop test to drive the baseline via the
kill switch and the ON case via the (now default) unset env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious force-pushed the peter/cache-vm-continuation branch from ff3959b to 8467bdf Compare July 17, 2026 22:24
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