') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); core: QuickJS engine divergence-detection and write-fencing parity with node:vm by TooTallNate · Pull Request #3453 · vercel/workflow · GitHub
Skip to content

core: QuickJS engine divergence-detection and write-fencing parity with node:vm - #3453

Open
TooTallNate wants to merge 2 commits into
mainfrom
quickjs-divergence-parity
Open

core: QuickJS engine divergence-detection and write-fencing parity with node:vm#3453
TooTallNate wants to merge 2 commits into
mainfrom
quickjs-divergence-parity

Conversation

@TooTallNate

@TooTallNateTooTallNate commented Aug 11, 2026

Copy link
Copy Markdown
Member

Why

The QuickJS dry-run #3253 showed that QuickJS's clean CORRUPTED_EVENT_LOG record in the event-log-race repro is largely a classification artifact: the engine had no divergence machinery at all, so the same racing-writer corruption the node engine reports as CORRUPTED_EVENT_LOG surfaced on QuickJS as USER_ERROR (a self-HookConflictError — both hook-storm failures on #3253 decrypt to conflictingRunId == <own runId>), or worse, as a silently wrong completion: with no stepName validation, a step_completed for a different step's ordinal resolved the wrong call with the wrong payload, and run_completed was written over a log the replay demonstrably did not reproduce.

This PR gives the two engines equivalent functionality on both axes, so repro results (and production failures) are directly comparable across engines.

What

1. Replay-divergence arbitration (quickjs-divergence.ts + fixed-point sweeps)

At every replay fixed point (initial drain loop convergence, and each continueWithEvents burst), the host dumps the VM's draws (__pending + hook/abort registries) and arbitrates the full observed event log against them:

  • Orphaned events — a correlation id the replay never drew → ReplayDivergenceError (mirrors the node engine's unconsumed-event check).
  • Family mismatches — a step_* event for an id drawn as a wait, etc.
  • Identity mismatchesstepName (step.ts parity), hook token (hook.ts parity), wait_completedresumeAt (sleep.ts parity).

The error throws out of the entrypoint into runtime.ts's existing replay-loop catch, engaging the same recovery machinery as the node engine: bounded recovery replays (WORKFLOW_REPLAY_DIVERGENCE_MAX_RETRIES), then terminal CorruptedEventLogError. A workflow that already failed with a genuine user error is recorded as such — arbitration only applies to logs the replay claims to have reproduced.

Because the QuickJS host drains the VM's microtask queue synchronously, the fixed point is exact — none of the grace-window / delivery-idle heuristics the node engine needs under the shared event loop.

2. Precondition guard (closes the documented KNOWN GAP)

Every replay-context event write in the entrypoint now carries the optimistic-concurrency view snapshot (stateUpdatedAt / stateEventCount / stateCursor), maintained incrementally from every event the invocation observes:

  • dispatchPendingOps writes: step_created, hook_created, hook_received (aborts), hook_disposed, attr_set, wait_created
  • both elapsed-wait wait_completed passes
  • run_completed
  • inline lazy step claims (executeSteppreconditionSnapshot), with suppressOptimisticStart while a hook is open on guard-enforcing Worlds — so a 412-fenced step never executes user code (node parity)
  • run_failed deliberately unfenced, matching the node engine's terminal-failure asymmetry

A 412 propagates to runtime.ts, whose existing restartReplayInProcess recovery invalidates the cached log and re-dispatches the QuickJS replay over the corrected view.

Validation

  • New unit suites: quickjs-divergence.test.ts (pure arbitration semantics), quickjs-entrypoint.fencing.test.ts (snapshot on fenced writes, run_failed unfenced, guard kill-switch), plus VM-level integration tests in quickjs-runtime.test.ts (orphan rejection, wrong-stepName rejection, healthy replay unaffected, user-failure priority, live-continuation divergence).
  • Full packages/core suite: 95 files, 2052 tests passing. Build + typecheck clean.
  • The CI quickjs e2e lanes exercise the full replay paths; combining this with the event-log-race-repro label on a quickjs-default branch (à la [DO NOT MERGE] CI dry-run: QuickJS as the default workflow VM engine #3253) should now show the same CORRUPTED_EVENT_LOG classification as the node engine for the racing-writer class.

@TooTallNate
TooTallNate requested a review from a team as a code ownerAugust 11, 2026 07:57
CopilotAI lite review requested due to automatic review settings August 11, 2026 07:57
@changeset-bot

changeset-botBot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5bed21a

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

This PR includes changesets to release 16 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

@vercel

vercelBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewAug 11, 2026 8:43am
example-nextjs-workflow-webpackReadyReadyPreviewAug 11, 2026 8:43am
example-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-astro-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-express-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-fastify-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-hono-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-nestjs-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-nitro-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-nuxt-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-python-workflowErrorErrorAug 11, 2026 8:43am
workbench-sveltekit-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-tanstack-start-workflowReadyReadyPreviewAug 11, 2026 8:43am
workbench-vite-workflowReadyReadyPreviewAug 11, 2026 8:43am
workflow-docsReadyReadyPreview, v0Aug 11, 2026 8:43am
workflow-swc-playgroundReadyReadyPreviewAug 11, 2026 8:43am
workflow-tarballsReadyReadyPreviewAug 11, 2026 8:43am
workflow-webReadyReadyPreviewAug 11, 2026 8:43am

@github-actions

github-actionsBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (15 failed)

astro-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

example-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

express-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

fastify-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

hono-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

nest-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

nextjs-turbopack-quickjs (2 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability
  • pages router sleepingWorkflow via pages router

nextjs-webpack-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

nitro-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

nuxt-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

sveltekit-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

tanstack-start-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

vite-node (1 failed):

vite-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R | 🔍 observability

💻 Local Development (9 failed)

astro-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nest-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-turbopack-canary-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-turbopack-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-webpack-canary-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nuxt-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

sveltekit-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

tanstack-start-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

vite-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R
📦 Local Production (14 failed)

astro-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

express-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

fastify-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

hono-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nest-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-turbopack-canary-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-turbopack-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-webpack-canary-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-webpack-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nitro-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nuxt-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

sveltekit-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

tanstack-start-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

vite-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R
🐘 Local Postgres (14 failed)

astro-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

express-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

fastify-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

hono-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nest-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-turbopack-canary-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-turbopack-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-webpack-canary-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nextjs-webpack-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nitro-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

nuxt-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

sveltekit-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

tanstack-start-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

vite-stable-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

🪟 Windows (1 failed)

nextjs-turbopack-quickjs (1 failed):

  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running | wrun_41KZR0CZ3Y0GG9BVW3SJ56XG8R

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production3451155904056
❌ 💻 Local Development363995584206
❌ 📦 Local Production3796145584368
❌ 🐘 Local Postgres3796145584368
❌ 🪟 Windows31110312
✅ vercel-multi-region270027
Total1502053226417337
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro-node128028
❌ astro-quickjs127128
✅ example-node128028
❌ example-quickjs127128
✅ express-node128028
❌ express-quickjs127128
✅ fastify-node128028
❌ fastify-quickjs127128
✅ hono-node128028
❌ hono-quickjs127128
✅ nest-node128028
❌ nest-quickjs127128
✅ nextjs-turbopack-node15303
❌ nextjs-turbopack-quickjs15123
✅ nextjs-webpack-node15303
❌ nextjs-webpack-quickjs15213
✅ nitro-node128028
❌ nitro-quickjs127128
✅ nuxt-node128028
❌ nuxt-quickjs127128
✅ sveltekit-node14709
❌ sveltekit-quickjs14619
✅ tanstack-start-node128028
❌ tanstack-start-quickjs127128
❌ vite-node127128
❌ vite-quickjs127128

❌ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable-node130026
❌ astro-stable-quickjs129126
✅ express-stable-node130026
✅ express-stable-quickjs129026
✅ fastify-stable-node130026
✅ fastify-stable-quickjs129026
✅ hono-stable-node130026
✅ hono-stable-quickjs128026
✅ nest-stable-node130026
❌ nest-stable-quickjs129126
✅ nextjs-turbopack-canary-node137019
❌ nextjs-turbopack-canary-quickjs136119
✅ nextjs-turbopack-stable-node15600
❌ nextjs-turbopack-stable-quickjs15510
✅ nextjs-webpack-canary-node137019
❌ nextjs-webpack-canary-quickjs136119
✅ nextjs-webpack-stable-node15600
✅ nitro-stable-node130026
✅ nitro-stable-quickjs128026
✅ nuxt-stable-node130026
❌ nuxt-stable-quickjs129126
✅ sveltekit-stable-node14907
❌ sveltekit-stable-quickjs14817
✅ tanstack-start-node130026
❌ tanstack-start-quickjs129126
✅ vite-stable-node130026
❌ vite-stable-quickjs129126

❌ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable-node130026
❌ astro-stable-quickjs129126
✅ express-stable-node130026
❌ express-stable-quickjs129126
✅ fastify-stable-node130026
❌ fastify-stable-quickjs129126
✅ hono-stable-node130026
❌ hono-stable-quickjs129126
✅ nest-stable-node130026
❌ nest-stable-quickjs129126
✅ nextjs-turbopack-canary-node137019
❌ nextjs-turbopack-canary-quickjs136119
✅ nextjs-turbopack-stable-node15600
❌ nextjs-turbopack-stable-quickjs15510
✅ nextjs-webpack-canary-node137019
❌ nextjs-webpack-canary-quickjs136119
✅ nextjs-webpack-stable-node15600
❌ nextjs-webpack-stable-quickjs15510
✅ nitro-stable-node130026
❌ nitro-stable-quickjs129126
✅ nuxt-stable-node130026
❌ nuxt-stable-quickjs129126
✅ sveltekit-stable-node14907
❌ sveltekit-stable-quickjs14817
✅ tanstack-start-node130026
❌ tanstack-start-quickjs129126
✅ vite-stable-node130026
❌ vite-stable-quickjs129126

❌ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable-node130026
❌ astro-stable-quickjs129126
✅ express-stable-node130026
❌ express-stable-quickjs129126
✅ fastify-stable-node130026
❌ fastify-stable-quickjs129126
✅ hono-stable-node130026
❌ hono-stable-quickjs129126
✅ nest-stable-node130026
❌ nest-stable-quickjs129126
✅ nextjs-turbopack-canary-node137019
❌ nextjs-turbopack-canary-quickjs136119
✅ nextjs-turbopack-stable-node15600
❌ nextjs-turbopack-stable-quickjs15510
✅ nextjs-webpack-canary-node137019
❌ nextjs-webpack-canary-quickjs136119
✅ nextjs-webpack-stable-node15600
❌ nextjs-webpack-stable-quickjs15510
✅ nitro-stable-node130026
❌ nitro-stable-quickjs129126
✅ nuxt-stable-node130026
❌ nuxt-stable-quickjs129126
✅ sveltekit-stable-node14907
❌ sveltekit-stable-quickjs14817
✅ tanstack-start-node130026
❌ tanstack-start-quickjs129126
✅ vite-stable-node130026
❌ vite-stable-quickjs129126

❌ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack-node15600
❌ nextjs-turbopack-quickjs15510

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5bed21a · Tue, 11 Aug 2026 08:57:01 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1154 (+9.2%)1299 🔴 (+10%)1365 🔴 (+13%)1550 🔴 (-9.3%)30
TTFSstream1149 (+10%)1206 🔴 (+5.5%)1229 🔴 (+5.4%)1293 🔴 (+6.9%)30
TTFShook + stream1385 (+11%)1468 🔴 (+7.3%)1554 🔴 (+11%)1645 🔴 (+9.1%)30
STSO1020 steps (inline)96 (±0%)123 (-25%) 💚143 (-30%) 💚270 (-65%) 💚1019
WO1020 steps126086 (-26%) 💚126086 (-26%) 💚126086 (-26%) 💚126086 (-26%) 💚1
SLstream latency81 (-11%)100 🔴 (-35%) 💚109 🔴 (-37%) 💚232 🔴 (-58%) 💚30
SOstream overhead (text)95 (-28%) 💚135 (-51%) 💚150 (-55%) 💚229 (-71%) 💚30
SOstream overhead (structured)94 (-23%) 💚134 (-45%) 💚176 (-44%) 💚196 (-56%) 💚30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 170410ms → this run 124907ms (Δ -45503ms, -27%)

 50-100 ms ┃ main 1 this 18 +17
100-150 ms ████████████████░░░░░░░┃ main 627 this 919 +292
150-200 ms ┃██████ main 279 this 57 -222
200-250 ms ┃█ main 59 this 11 -48
250-300 ms ┃ main 17 this 6 -11
300-350 ms ┃ main 8 this 0 -8
350-400 ms ┃ main 3 this 1 -2
400-450 ms ┃ main 2 this 2 +0
450-500 ms ┃ main 1 this 0 -1
500-550 ms ┃ main 2 this 4 +2
600-650 ms ┃ main 2 this 0 -2
650-700 ms ┃ main 4 this 0 -4
700-750 ms ┃ main 3 this 0 -3
750-800 ms ┃ main 2 this 0 -2
800-850 ms ┃ main 3 this 1 -2
850-900 ms ┃ main 1 this 0 -1
950-1000 ms ┃ main 1 this 0 -1
1000-1050 ms ┃ main 1 this 0 -1
1250-1300 ms ┃ main 1 this 0 -1
1300-1350 ms ┃ main 1 this 0 -1
1650-1700 ms ┃ main 1 this 0 -1
📜 Previous results (1)

a8bff02

Tue, 11 Aug 2026 08:15:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep244 (-77%) 💚1444 🔴 (+23%) 🔻1532 🔴 (+27%) 🔻1582 🔴 (-7.4%)30
TTFSstream258 (-75%) 💚1434 🔴 (+25%) 🔻1498 🔴 (+28%) 🔻1587 🔴 (+31%) 🔻30
TTFShook + stream333 (-73%) 💚1697 🔴 (+24%) 🔻1756 🔴 (+26%) 🔻1996 🔴 (+32%) 🔻30
STSO1020 steps (inline)93 (-3.1%)151 (-7.9%)169 (-17%) 💚327 (-58%) 💚1019
WO1020 steps148479 (-13%)148479 (-13%)148479 (-13%)148479 (-13%)1
SLstream latency103 (+13%)141 🔴 (-8.4%)212 🔴 (+23%) 🔻458 🔴 (-18%) 💚30
SOstream overhead (text)126 (-4.5%)206 (-25%) 💚252 (-24%) 💚698 (-11%)30
SOstream overhead (structured)121 (-0.8%)308 🔴 (+26%) 🔻801 🔴 (+157%) 🔻963 (+114%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

CopilotAI 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.

Pull request overview

This PR brings the QuickJS workflow VM engine up to parity with the node:vm engine for (1) replay divergence detection and (2) optimistic-concurrency write fencing, so corrupted/diverged event logs are consistently classified and recovered across engines.

Changes:

  • Add fixed-point replay-divergence arbitration for QuickJS (orphaned events, family mismatches, and identity mismatches like stepName / hook token / wait resumeAt), escalating via existing runtime recovery.
  • Thread an optimistic-concurrency precondition snapshot through replay-context event writes in the QuickJS entrypoint (leaving run_failed intentionally unfenced for parity).
  • Add unit + integration coverage for divergence arbitration and precondition fencing behavior.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
FileDescription
packages/core/src/runtime/quickjs-runtime.tsRuns fixed-point replay-divergence sweeps during initial replay and each continueWithEvents burst.
packages/core/src/runtime/quickjs-runtime.test.tsAdds VM-level integration tests for replay divergence behavior (orphan, wrong stepName, live continuation, etc.).
packages/core/src/runtime/quickjs-entrypoint.tsImplements incremental precondition snapshot tracking and applies it to replay-context writes (plus inline-step claim fencing behavior).
packages/core/src/runtime/quickjs-entrypoint.fencing.test.tsVerifies fenced vs. unfenced writes and guard kill-switch behavior with a mocked VM.
packages/core/src/runtime/quickjs-divergence.tsIntroduces the pure arbitration logic used by the fixed-point sweep.
packages/core/src/runtime/quickjs-divergence.test.tsUnit-tests arbitration semantics (structural events, orphan/family/identity mismatches).
.changeset/quickjs-divergence-parity.mdChangeset entry documenting the parity improvements in @workflow/core.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadpackages/core/src/runtime/quickjs-divergence.ts
Comment threadpackages/core/src/runtime/quickjs-entrypoint.ts
TooTallNate added a commit that referenced this pull request Aug 11, 2026
Combines the QuickJS-default dry-run with divergence-detection and
write-fencing parity (#3453) so the event-log-race repro measures the
same corruption classes on both engines. Conflict resolutions:
- makeLiveSession takes both the snapshot-state accessor and the
observed-events ledger; the snapshot-restore path runs the divergence
sweep against its delta view (a throw there falls back to a fresh
full replay, whose sweep is authoritative).
- The precondition view tracker seeds its event count from the restored
snapshot's metadata.eventCount (delta ids sort above the snapshot
cursor, so watermark+count still describe the full log); snapshots
without an eventCount fail open, and the restore-failure fallback
rebuilds the view from the refetched full log.
@TooTallNateTooTallNate added the event-log-race-repro Run the event log race reproduction job label Aug 11, 2026
@github-actions

github-actionsBot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Event Log Race Repro

1 of 14 latest repro runs hit event-log regressions.

Run History

Metric2026-08-11 08:38 UTC #1
logs / deploy
2026-08-11 08:45 UTC #1
logs / deploy
Result4/14 regressions1/14 regressions
Total1414
completed1013
CORRUPTED_EVENT_LOG41
USER_ERROR00
RUNTIME_ERROR00
stuck00
other00
infra00
Config14 runs / step-storm 6, hook-storm 6, hook-sleep 2 / c8 / 6x814 runs / step-storm 6, hook-storm 6, hook-sleep 2 / c8 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm66000000
hook-storm65100000
hook-sleep22000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
hook-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KZQZW4H20GMCB9240WQ0ZPA1

@VaguelySeriousVaguelySerious 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.

Precondition guard

I don't think this is necessary anymore - we're removing this feature in favor of the specVersion 6 IDs

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious