') + ')', '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); } })(); })(); [codex] Guard event consumers during replay by pranaygp · Pull Request #2030 · vercel/workflow · GitHub
Skip to content

[codex] Guard event consumers during replay - #2030

Merged
pranaygp merged 3 commits into
mainfrom
pranaygp/codex/guard-event-consumers
May 20, 2026
Merged

[codex] Guard event consumers during replay#2030
pranaygp merged 3 commits into
mainfrom
pranaygp/codex/guard-event-consumers

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Adds replay-time ownership guards for step, wait, and hook lifecycle events so consumers do not resolve solely by correlation ID when guard metadata is present.

  • Persists optional guard metadata on new events: step names, wait resumeAt, and hook tokens.
  • Validates guard metadata in step, sleep, user hook, and abort-hook consumers during replay.
  • Keeps storage permissive for wait/hook guard mismatches so suspicious events remain committed and replay deterministically records run_failed with RUNTIME_ERROR.
  • Adds focused unit tests plus runtime-entrypoint coverage for committed bad wait/hook events becoming run_failed instead of retrying forever.

Validation

  • pnpm --filter @workflow/core exec vitest run src/workflow/sleep.test.ts src/workflow/hook.test.ts src/abort-controller.test.ts src/runtime.test.ts
  • pnpm --filter @workflow/core exec vitest run src/workflow.test.ts -t "resumeAt mismatches|hook_received token mismatches"
  • pnpm --filter @workflow/core typecheck
  • git diff --check

Note: pnpm exec biome check ... still reports pre-existing complexity/legacy lint diagnostics in touched runtime files; safe formatter/import fixes were applied.

@vercel

vercelBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 20, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d680a29

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

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

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-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.033s (-23.0% 🟢)1.005s (~)0.972s101.00x
💻 LocalExpress0.040s (-10.6% 🟢)1.013s (+0.8%)0.974s101.19x
🐘 PostgresExpress0.044s (-24.5% 🟢)1.011s (~)0.968s101.32x
💻 LocalNext.js (Turbopack)0.049s1.006s0.957s101.47x
🐘 PostgresNitro0.051s (-46.8% 🟢)1.013s (-2.9%)0.962s101.52x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.79x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.278s (+18.1% 🔺)2.082s (-2.5%)1.804s101.00x
▲ VercelNitro0.315s (-23.1% 🟢)2.012s (-19.8% 🟢)1.698s101.13x
▲ VercelNext.js (Turbopack)0.316s (+25.8% 🔺)2.265s (-2.9%)1.948s101.14x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.068s (-5.1% 🟢)2.006s (~)0.938s101.00x
💻 LocalNitro1.070s (-5.4% 🟢)2.006s (~)0.935s101.00x
🐘 PostgresExpress1.084s (-5.5% 🟢)2.010s (~)0.926s101.01x
🐘 PostgresNitro1.085s (-4.8%)2.009s (~)0.924s101.02x
💻 LocalNext.js (Turbopack)1.112s2.005s0.893s101.04x
🐘 PostgresNext.js (Turbopack)1.123s2.009s0.885s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.579s (-59.4% 🟢)3.741s (-36.7% 🟢)2.162s101.00x
▲ VercelExpress1.590s (-15.2% 🟢)3.646s (-4.2%)2.056s101.01x
▲ VercelNext.js (Turbopack)2.313s (+13.7% 🔺)4.760s (+24.3% 🔺)2.447s101.47x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.395s (-5.0% 🟢)11.022s (~)0.626s31.00x
🐘 PostgresNitro10.409s (-4.3%)11.016s (~)0.607s31.00x
💻 LocalExpress10.421s (-4.6%)11.023s (~)0.602s31.00x
🐘 PostgresExpress10.436s (-4.8%)11.019s (~)0.583s31.00x
💻 LocalNext.js (Turbopack)10.654s11.022s0.368s31.02x
🐘 PostgresNext.js (Turbopack)10.717s11.020s0.303s31.03x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express13.226s (-22.1% 🟢)16.251s (-18.8% 🟢)3.026s21.00x
▲ VercelNitro13.294s (-44.0% 🟢)15.795s (-37.1% 🟢)2.501s21.01x
▲ VercelNext.js (Turbopack)13.306s (-23.2% 🟢)16.079s (-17.1% 🟢)2.772s21.01x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.440s (-10.8% 🟢)14.027s (-12.5% 🟢)0.587s51.00x
💻 LocalExpress13.440s (-10.2% 🟢)14.027s (-6.7% 🟢)0.587s51.00x
🐘 PostgresExpress13.478s (-7.6% 🟢)14.020s (-6.7% 🟢)0.542s51.00x
🐘 PostgresNitro13.488s (-7.6% 🟢)14.023s (-6.7% 🟢)0.535s51.00x
💻 LocalNext.js (Turbopack)14.049s15.029s0.980s41.05x
🐘 PostgresNext.js (Turbopack)14.104s15.018s0.914s41.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)21.243s (-59.6% 🟢)24.611s (-54.9% 🟢)3.368s31.00x
▲ VercelNitro21.266s (-67.0% 🟢)24.217s (-63.7% 🟢)2.951s31.00x
▲ VercelExpress21.383s (-57.5% 🟢)23.454s (-55.4% 🟢)2.071s31.01x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.855s (-29.4% 🟢)12.022s (-29.4% 🟢)0.167s81.00x
💻 LocalExpress11.890s (-28.4% 🟢)12.022s (-29.4% 🟢)0.132s81.00x
🐘 PostgresExpress12.023s (-14.2% 🟢)12.643s (-13.4% 🟢)0.620s81.01x
🐘 PostgresNitro12.031s (-13.9% 🟢)12.394s (-13.4% 🟢)0.363s81.01x
💻 LocalNext.js (Turbopack)12.976s13.023s0.047s71.09x
🐘 PostgresNext.js (Turbopack)13.137s14.016s0.879s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro28.841s (-93.2% 🟢)31.157s (-92.7% 🟢)2.316s31.00x
▲ VercelNext.js (Turbopack)30.320s (-92.3% 🟢)32.949s (-91.7% 🟢)2.629s31.05x
▲ VercelExpress35.646s (-70.6% 🟢)38.701s (-68.7% 🟢)3.054s31.24x

🔍 Observability: Nitro | Next.js (Turbopack) | Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.7% 🟢)2.008s (~)0.870s151.00x
🐘 PostgresNitro1.149s (-9.8% 🟢)2.008s (~)0.859s151.01x
💻 LocalExpress1.179s (-20.8% 🟢)2.006s (~)0.827s151.04x
💻 LocalNitro1.196s (-26.7% 🟢)2.006s (-3.3%)0.810s151.05x
🐘 PostgresNext.js (Turbopack)1.222s2.007s0.785s151.07x
💻 LocalNext.js (Turbopack)1.300s2.005s0.705s151.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.642s (-7.6% 🟢)4.880s (+5.6% 🔺)2.239s71.00x
▲ VercelNitro2.803s (-0.5%)5.033s (+16.4% 🔺)2.230s61.06x
▲ VercelNext.js (Turbopack)3.151s (-7.2% 🟢)5.097s (+3.3%)1.946s71.19x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.182s (-49.9% 🟢)2.007s (-33.3% 🟢)0.825s151.00x
🐘 PostgresNitro1.212s (-48.4% 🟢)2.007s (-33.3% 🟢)0.795s151.03x
🐘 PostgresNext.js (Turbopack)1.345s2.007s0.662s151.14x
💻 LocalExpress1.706s (-42.2% 🟢)2.005s (-41.9% 🟢)0.299s151.44x
💻 LocalNitro1.707s (-45.7% 🟢)2.006s (-48.4% 🟢)0.299s151.44x
💻 LocalNext.js (Turbopack)1.809s2.149s0.340s141.53x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.433s (-5.2% 🟢)5.898s (+15.4% 🔺)2.466s61.00x
▲ VercelNitro3.577s (-11.7% 🟢)5.445s (-8.0% 🟢)1.868s61.04x
▲ VercelNext.js (Turbopack)4.236s (-40.3% 🟢)6.856s (-23.0% 🟢)2.620s51.23x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.286s (-63.1% 🟢)2.008s (-49.9% 🟢)0.722s151.00x
🐘 PostgresNitro1.323s (-62.0% 🟢)2.008s (-49.9% 🟢)0.684s151.03x
🐘 PostgresNext.js (Turbopack)1.598s2.008s0.409s151.24x
💻 LocalNext.js (Turbopack)4.624s5.177s0.554s63.60x
💻 LocalExpress4.638s (-44.4% 🟢)5.011s (-44.5% 🟢)0.373s63.61x
💻 LocalNitro4.704s (-43.7% 🟢)5.177s (-42.6% 🟢)0.473s63.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.982s (+17.5% 🔺)7.634s (+24.6% 🔺)2.651s41.00x
▲ VercelNext.js (Turbopack)4.989s (-44.0% 🟢)6.815s (-37.8% 🟢)1.827s51.00x
▲ VercelNitro5.035s (+42.8% 🔺)7.624s (+37.8% 🔺)2.589s41.01x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.134s (-9.8% 🟢)2.008s (~)0.873s151.00x
🐘 PostgresNitro1.170s (-7.0% 🟢)2.010s (~)0.840s151.03x
🐘 PostgresNext.js (Turbopack)1.202s2.008s0.807s151.06x
💻 LocalNext.js (Turbopack)1.334s2.006s0.672s151.18x
💻 LocalExpress1.373s (-27.5% 🟢)2.006s (-15.1% 🟢)0.634s151.21x
💻 LocalNitro1.434s (-23.2% 🟢)2.073s (-11.4% 🟢)0.639s151.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.422s (-1.5%)4.355s (+4.4%)1.932s81.00x
▲ VercelExpress2.575s (~)4.872s (+12.0% 🔺)2.297s71.06x
▲ VercelNext.js (Turbopack)2.681s (-8.5% 🟢)4.926s (+6.1% 🔺)2.245s71.11x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.185s (-49.4% 🟢)2.007s (-33.3% 🟢)0.823s151.00x
🐘 PostgresNitro1.212s (-48.2% 🟢)2.009s (-33.3% 🟢)0.796s151.02x
🐘 PostgresNext.js (Turbopack)1.332s2.007s0.676s151.12x
💻 LocalExpress1.853s (-40.8% 🟢)2.293s (-39.0% 🟢)0.440s141.56x
💻 LocalNitro2.017s (-34.2% 🟢)2.469s (-36.5% 🟢)0.452s131.70x
💻 LocalNext.js (Turbopack)2.034s2.591s0.557s121.72x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.301s (+2.1%)5.603s (+10.4% 🔺)2.301s61.00x
▲ VercelExpress3.479s (+9.0% 🔺)6.151s (+28.4% 🔺)2.672s51.05x
▲ VercelNext.js (Turbopack)3.666s (+16.7% 🔺)5.651s (+25.0% 🔺)1.985s61.11x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.275s (-63.6% 🟢)2.008s (-49.9% 🟢)0.733s151.00x
🐘 PostgresNitro1.342s (-61.4% 🟢)2.008s (-49.9% 🟢)0.666s151.05x
🐘 PostgresNext.js (Turbopack)1.637s2.009s0.372s151.28x
💻 LocalExpress5.004s (-43.1% 🟢)5.514s (-40.5% 🟢)0.510s63.92x
💻 LocalNext.js (Turbopack)5.079s5.514s0.435s63.98x
💻 LocalNitro5.515s (-39.7% 🟢)6.016s (-40.0% 🟢)0.501s54.32x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.007s (-25.9% 🟢)7.825s (-8.4% 🟢)2.817s41.00x
▲ VercelNitro5.032s (-1.2%)7.031s (+3.1%)1.999s51.00x
▲ VercelExpress6.301s (-1.8%)8.642s (+5.7% 🔺)2.340s41.26x

🔍 Observability: Next.js (Turbopack) | Nitro | Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.436s (-48.0% 🟢)1.006s (-1.7%)0.570s601.00x
🐘 PostgresNitro0.458s (-44.2% 🟢)1.007s (~)0.549s601.05x
💻 LocalExpress0.459s (-53.3% 🟢)1.004s (-6.7% 🟢)0.544s601.05x
💻 LocalNitro0.462s (-52.9% 🟢)1.004s (-8.2% 🟢)0.542s601.06x
🐘 PostgresNext.js (Turbopack)0.672s1.006s0.334s601.54x
💻 LocalNext.js (Turbopack)0.734s1.005s0.271s601.68x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)4.886s (-66.3% 🟢)7.577s (-52.9% 🟢)2.691s81.00x
▲ VercelExpress4.942s (-74.0% 🟢)7.079s (-66.8% 🟢)2.137s91.01x
▲ VercelNitro5.017s (-77.3% 🟢)7.173s (-70.1% 🟢)2.156s91.03x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.046s (-47.1% 🟢)1.771s (-21.6% 🟢)0.725s511.00x
🐘 PostgresNitro1.092s (-43.3% 🟢)1.865s (-11.2% 🟢)0.772s491.04x
💻 LocalExpress1.160s (-61.5% 🟢)2.005s (-44.1% 🟢)0.845s451.11x
💻 LocalNitro1.183s (-61.0% 🟢)2.006s (-46.6% 🟢)0.823s451.13x
🐘 PostgresNext.js (Turbopack)1.583s2.007s0.424s451.51x
💻 LocalNext.js (Turbopack)1.811s2.005s0.194s451.73x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express12.886s (-62.7% 🟢)15.534s (-57.8% 🟢)2.647s61.00x
▲ VercelNitro13.883s (-64.8% 🟢)16.243s (-60.7% 🟢)2.360s61.08x
▲ VercelNext.js (Turbopack)13.989s (-71.9% 🟢)16.288s (-68.5% 🟢)2.300s61.09x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.116s (-47.0% 🟢)2.674s (-38.8% 🟢)0.558s451.00x
🐘 PostgresNitro2.173s (-47.0% 🟢)2.867s (-37.7% 🟢)0.694s421.03x
💻 LocalNitro2.630s (-71.7% 🟢)3.007s (-70.0% 🟢)0.378s401.24x
💻 LocalExpress2.676s (-70.9% 🟢)3.008s (-70.0% 🟢)0.332s401.26x
🐘 PostgresNext.js (Turbopack)3.154s4.009s0.855s301.49x
💻 LocalNext.js (Turbopack)3.853s4.146s0.293s291.82x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro37.781s (-61.0% 🟢)40.348s (-59.0% 🟢)2.567s31.00x
▲ VercelExpress39.734s (-69.4% 🟢)43.113s (-67.4% 🟢)3.378s31.05x
▲ VercelNext.js (Turbopack)317.476s (+196.3% 🔺)319.889s (+193.7% 🔺)2.413s18.40x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.202s (-28.4% 🟢)1.006s (~)0.804s601.00x
🐘 PostgresNitro0.214s (-24.4% 🟢)1.023s (+1.6%)0.809s591.06x
🐘 PostgresNext.js (Turbopack)0.224s1.006s0.781s601.11x
💻 LocalExpress0.448s (-20.2% 🟢)1.004s (~)0.557s602.21x
💻 LocalNitro0.456s (-24.5% 🟢)1.004s (-1.7%)0.548s602.26x
💻 LocalNext.js (Turbopack)0.539s1.004s0.465s602.67x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.208s (+13.0% 🔺)4.558s (+25.3% 🔺)2.350s141.00x
▲ VercelNext.js (Turbopack)2.317s (+14.6% 🔺)4.120s (+8.6% 🔺)1.804s151.05x
▲ VercelNitro2.561s (+54.2% 🔺)4.731s (+41.2% 🔺)2.170s131.16x

🔍 Observability: Express | Next.js (Turbopack) | Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.279s (-45.2% 🟢)1.006s (~)0.726s901.00x
🐘 PostgresNitro0.317s (-36.2% 🟢)1.007s (~)0.690s901.13x
🐘 PostgresNext.js (Turbopack)0.429s1.006s0.577s901.54x
💻 LocalNitro2.119s (-16.5% 🟢)2.852s (-5.2% 🟢)0.733s327.59x
💻 LocalExpress2.212s (-12.0% 🟢)2.884s (-4.2%)0.672s327.92x
💻 LocalNext.js (Turbopack)2.358s3.044s0.686s308.44x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.941s (+115.2% 🔺)9.000s (+86.7% 🔺)2.059s101.00x
▲ VercelExpress7.453s (+144.6% 🔺)9.736s (+102.5% 🔺)2.283s101.07x
▲ VercelNext.js (Turbopack)7.788s (+120.3% 🔺)9.687s (+86.5% 🔺)1.899s101.12x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.570s (-30.3% 🟢)1.006s (-1.2%)0.435s1201.00x
🐘 PostgresNitro0.634s (-19.8% 🟢)1.006s (~)0.373s1201.11x
🐘 PostgresNext.js (Turbopack)0.873s1.032s0.159s1171.53x
💻 LocalNitro9.769s (-12.7% 🟢)10.442s (-10.5% 🟢)0.674s1217.13x
💻 LocalExpress9.801s (-12.4% 🟢)10.362s (-13.2% 🟢)0.561s1217.19x
💻 LocalNext.js (Turbopack)10.697s11.394s0.697s1118.76x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro18.496s (+139.5% 🔺)20.816s (+121.4% 🔺)2.320s61.00x
▲ VercelExpress29.528s (+297.9% 🔺)32.079s (+247.0% 🔺)2.551s41.60x
▲ VercelNext.js (Turbopack)29.847s (+189.0% 🔺)32.150s (+161.7% 🔺)2.302s41.61x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.128s (+466.8% 🔺)2.005s (+99.6% 🔺)0.011s (-9.1% 🟢)2.018s (+98.2% 🔺)0.890s101.00x
🐘 PostgresExpress1.130s (+451.0% 🔺)2.002s (+100.5% 🔺)0.001s (-31.3% 🟢)2.009s (+98.7% 🔺)0.879s101.00x
💻 LocalNitro1.130s (+428.8% 🔺)2.005s (+99.6% 🔺)0.010s (-16.8% 🟢)2.017s (+98.0% 🔺)0.887s101.00x
🐘 PostgresNitro1.142s (+457.0% 🔺)2.001s (+100.2% 🔺)0.002s (+6.7% 🔺)2.010s (+98.8% 🔺)0.868s101.01x
💻 LocalNext.js (Turbopack)1.175s2.004s0.010s2.018s0.842s101.04x
🐘 PostgresNext.js (Turbopack)1.208s2.002s0.001s2.011s0.803s101.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.151s (-68.6% 🟢)3.626s (-58.1% 🟢)1.338s (+111.8% 🔺)5.500s (-43.8% 🟢)3.349s101.00x
▲ VercelExpress2.258s (-9.9% 🟢)3.878s (-5.2% 🟢)1.377s (+43.4% 🔺)5.819s (+4.1%)3.561s101.05x
▲ VercelNitro2.264s (-40.9% 🟢)3.992s (-24.3% 🟢)0.843s (+13.6% 🔺)5.394s (-16.8% 🟢)3.130s101.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.509s (+79.9% 🔺)2.010s (+98.7% 🔺)0.010s (+8.5% 🔺)2.022s (+81.2% 🔺)0.513s301.00x
🐘 PostgresNitro1.529s (+145.0% 🔺)2.004s (+99.1% 🔺)0.004s (-5.7% 🟢)2.027s (+98.2% 🔺)0.497s301.01x
🐘 PostgresExpress1.543s (+144.9% 🔺)2.007s (+99.4% 🔺)0.003s (-12.1% 🟢)2.024s (+97.8% 🔺)0.481s301.02x
🐘 PostgresNext.js (Turbopack)1.661s2.011s0.004s2.024s0.363s301.10x
💻 LocalExpress1.697s (+124.1% 🔺)2.010s (+95.4% 🔺)0.009s (-2.5%)2.201s (+111.6% 🔺)0.504s281.12x
💻 LocalNext.js (Turbopack)1.847s2.010s0.010s2.202s0.356s281.22x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.684s (-66.4% 🟢)7.490s (-58.9% 🟢)0.570s (+169.8% 🔺)8.572s (-54.7% 🟢)2.888s71.00x
▲ VercelExpress5.690s (-12.5% 🟢)7.834s (-2.2%)0.258s (-36.9% 🟢)8.628s (-2.3%)2.938s71.00x
▲ VercelNitro6.244s (-78.8% 🟢)7.882s (-74.4% 🟢)0.263s (+134.7% 🔺)8.925s (-71.9% 🟢)2.681s71.10x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.640s (-33.4% 🟢)1.017s (-20.4% 🟢)0.000s (+19.0% 🔺)1.039s (-20.5% 🟢)0.399s581.00x
🐘 PostgresNitro0.677s (-30.1% 🟢)1.016s (-18.6% 🟢)0.000s (+65.5% 🔺)1.035s (-17.7% 🟢)0.358s581.06x
🐘 PostgresNext.js (Turbopack)0.747s1.018s0.000s1.039s0.291s581.17x
💻 LocalExpress1.329s (+8.5% 🔺)2.016s (~)0.001s (+80.0% 🔺)2.018s (~)0.689s302.08x
💻 LocalNitro1.334s (+9.1% 🔺)2.015s (~)0.000s (+266.7% 🔺)2.017s (~)0.683s302.08x
💻 LocalNext.js (Turbopack)1.448s2.014s0.000s2.017s0.568s302.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)3.411s (-66.5% 🟢)4.912s (-57.4% 🟢)0.000s (+Infinity% 🔺)5.439s (-54.9% 🟢)2.028s121.00x
▲ VercelExpress3.460s (-7.5% 🟢)5.213s (+2.2%)0.000s (~)5.787s (+4.6%)2.327s111.01x
▲ VercelNitro3.586s (+17.6% 🔺)5.080s (+15.6% 🔺)0.000s (+18.2% 🔺)5.659s (+17.7% 🔺)2.073s111.05x

🔍 Observability: Next.js (Turbopack) | Express | Nitro

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.258s (-29.8% 🟢)1.964s (-8.3% 🟢)0.000s (-9.7% 🟢)1.999s (-8.1% 🟢)0.741s311.00x
🐘 PostgresExpress1.275s (-28.1% 🟢)1.968s (-9.6% 🟢)0.000s (NaN%)1.994s (-9.3% 🟢)0.719s311.01x
🐘 PostgresNext.js (Turbopack)1.503s2.106s0.000s2.113s0.610s291.19x
💻 LocalNext.js (Turbopack)2.852s3.361s0.001s3.367s0.515s182.27x
💻 LocalNitro3.106s (-8.3% 🟢)3.904s (-3.2%)0.000s (-41.4% 🟢)3.907s (-3.2%)0.800s162.47x
💻 LocalExpress3.142s (-9.4% 🟢)3.966s (-1.7%)0.001s (-29.7% 🟢)3.968s (-1.7%)0.826s162.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.967s (+21.3% 🔺)6.130s (+14.1% 🔺)0.002s (+560.0% 🔺)6.668s (+15.1% 🔺)1.702s101.00x
▲ VercelExpress5.304s (+15.6% 🔺)7.051s (+17.1% 🔺)0.000s (+Infinity% 🔺)7.594s (+17.6% 🔺)2.290s81.07x
▲ VercelNext.js (Turbopack)5.531s (-1.5%)6.926s (-0.8%)0.000s (-11.1% 🟢)7.469s (-0.9%)1.938s91.11x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalExpress10/21
🐘 PostgresExpress18/21
▲ VercelNitro8/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres15/21
Next.js (Turbopack)🐘 Postgres15/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented May 20, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production120002191419
✅ 💻 Local Development158702191806
✅ 📦 Local Production158702191806
✅ 🐘 Local Postgres158702191806
✅ 🪟 Windows12900129
✅ 📋 Other7270176903
Total6817010527869

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro103026
✅ example103026
✅ express103026
✅ fastify103026
✅ hono103026
✅ nextjs-turbopack12702
✅ nextjs-webpack12702
✅ nitro103026
✅ nuxt103026
✅ sveltekit12207
✅ vite103026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable104025
✅ express-stable104025
✅ fastify-stable104025
✅ hono-stable104025
✅ nextjs-turbopack-canary110019
✅ nextjs-turbopack-stable-lazy-discovery-disabled12900
✅ nextjs-turbopack-stable-lazy-discovery-enabled12900
✅ nextjs-webpack-canary110019
✅ nextjs-webpack-stable-lazy-discovery-disabled12900
✅ nextjs-webpack-stable-lazy-discovery-enabled12900
✅ nitro-stable104025
✅ nuxt-stable104025
✅ sveltekit-stable12306
✅ vite-stable104025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack12900
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable104025
✅ e2e-local-dev-tanstack-start-104025
✅ e2e-local-postgres-nest-stable104025
✅ e2e-local-postgres-tanstack-start-104025
✅ e2e-local-prod-nest-stable104025
✅ e2e-local-prod-tanstack-start-104025
✅ e2e-vercel-prod-tanstack-start103026

📋 View full workflow run

Comment threadpackages/core/src/runtime/resume-hook.ts
Comment threadpackages/core/src/runtime/step-handler.ts
Comment threadpackages/core/src/runtime/step-handler.ts
@pranaygp
pranaygp marked this pull request as ready for review May 20, 2026 01:33
@pranaygp
pranaygp requested a review from a team as a code ownerMay 20, 2026 01:33
CopilotAI review requested due to automatic review settings May 20, 2026 01:33

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

Adds “ownership” guard metadata to step/wait/hook lifecycle events and validates that metadata during replay so consumers don’t accidentally resolve the wrong entity purely by correlationId.

Changes:

  • Extend World event schemas to optionally persist guard metadata (stepName, wait resumeAt, hook token).
  • Add replay-time guard checks in step, sleep, hook, and abort-hook consumers and persist guard metadata from runtime emitters.
  • Add focused unit + runtime-entrypoint tests to ensure bad committed events deterministically become run_failed (RUNTIME_ERROR) instead of looping.

Reviewed changes

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

Show a summary per file
FileDescription
packages/world/src/events.tsExtends event schemas to carry optional replay guard metadata.
packages/core/src/workflow/sleep.tsValidates wait_completed.resumeAt during replay to guard wait consumers.
packages/core/src/workflow/sleep.test.tsAdds coverage for legacy wait_completed and resumeAt mismatch behavior.
packages/core/src/workflow/hook.tsValidates hook token metadata on hook lifecycle events during replay.
packages/core/src/workflow/hook.test.tsAdds mismatch tests for hook lifecycle events (created/received/disposed/conflict).
packages/core/src/workflow/abort-controller.tsValidates abort-hook token metadata during replay; minor refactor.
packages/core/src/workflow.test.tsAdds integration tests for hook/wait guard failures via runWorkflow.
packages/core/src/step.tsValidates stepName metadata on step lifecycle events during replay.
packages/core/src/step.test.tsAdds mismatch tests for step lifecycle events (created/completed/failed).
packages/core/src/runtime/suspension-handler.tsPersists hook token when emitting hook_disposed / abort hook_received.
packages/core/src/runtime/step-handler.tsEmits stepName metadata on step lifecycle events.
packages/core/src/runtime/step-executor.tsEmits stepName metadata from direct step execution path.
packages/core/src/runtime/runs.tsIncludes wait resumeAt when emitting wait_completed during wakeups.
packages/core/src/runtime/resume-hook.tsIncludes hook token on hook_received in non-legacy mode.
packages/core/src/runtime.tsIncludes wait resumeAt when auto-completing waits during entrypoint handling.
packages/core/src/runtime.test.tsAdds runtime-entrypoint regression coverage for committed bad events -> run_failed.
packages/core/src/abort-controller.test.tsAdds test for abort hook token mismatch surfacing as WorkflowRuntimeError.
.changeset/guard-step-consumer-events.mdChangeset for patch bumps to core + world.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadpackages/core/src/workflow/sleep.ts Outdated

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

AI review: blocking issues found

Comment threadpackages/core/src/workflow/hook.ts
Comment threadpackages/core/src/workflow/sleep.ts Outdated

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

Copilot reviewed 23 out of 23 changed files in this pull request and generated 2 comments.

Comment on lines +69 to +82
const eventToken =
'eventData' in event && event.eventData && 'token' in event.eventData
? event.eventData.token
: undefined;

if (typeof eventToken === 'string' && eventToken !== token) {
ctx.promiseQueue = ctx.promiseQueue.then(() => {
ctx.onWorkflowError(
new WorkflowRuntimeError(
`Corrupted event log: hook event ${event.eventType} for ${correlationId} belongs to token "${eventToken}", but the current hook consumer expects "${token}"`
)
);
});
return EventConsumerResult.Finished;
Comment on lines +139 to +156
const eventToken =
'eventData' in event && event.eventData && 'token' in event.eventData
? event.eventData.token
: undefined;

if (
typeof eventToken === 'string' &&
eventToken !== this[ABORT_HOOK_TOKEN]
) {
ctx.promiseQueue = ctx.promiseQueue.then(() => {
ctx.onWorkflowError(
new WorkflowRuntimeError(
`Corrupted event log: abort hook event ${event.eventType} for ${correlationId} belongs to token "${eventToken}", but the current abort hook expects "${this[ABORT_HOOK_TOKEN]}"`
)
);
});
return EventConsumerResult.Finished;
}

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

AI review: no blocking issues

@pranaygp
pranaygp merged commit b124365 into mainMay 20, 2026
205 of 216 checks passed
@pranaygp
pranaygp deleted the pranaygp/codex/guard-event-consumers branch May 20, 2026 18:40
@github-actionsgithub-actionsBot mentioned this pull request May 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically.

To resolve manually, push a backport branch and open a PR against stable (the workflow never pushes directly to stable). Note: this repository requires verified signatures on every branch, so your local commits must be signed (git config commit.gpgsign true with a configured GPG/SSH signing key, or git cherry-pick -S).

git fetch origin stable
git checkout -b backport/pr-2030-to-stable origin/stable
git cherry-pick -S b124365e14b0c47a5c830c7009dd5bf0149d5a59 # -S signs the commit# Fix conflicts, then:
git add -A
git cherry-pick --continue
git push -u origin backport/pr-2030-to-stable
gh pr create --base stable --head backport/pr-2030-to-stable \
--title "Backport #2030: <original PR title>" \
--body "Manual backport of #2030 (cherry-pick b124365e14b0) to \`stable\`."

pranaygp added a commit that referenced this pull request May 20, 2026
* Guard event consumers during replay
* Address event guard review feedback
* Update event guard test fixtures
pranaygp added a commit that referenced this pull request May 20, 2026
* Guard event consumers during replay
* Address event guard review feedback
* Update event guard test fixtures
@github-actionsgithub-actionsBot mentioned this pull request May 22, 2026
VaguelySerious added a commit that referenced this pull request Jun 10, 2026
… export ResumedHook; docs
- Only take the resilient path when the target run's recorded
@workflow/core version understands hookInput on the queue payload.
Runs keep executing on the deployment they were created on (skew
protection), and older runtimes parse the queue message with a schema
that silently strips unknown fields - the resume payload would be
lost while resumeHook() reported success. Fail fast (propagate the
original event-write error) for such runs instead, preserving the
caller's ability to retry.
- Carry the hook token on hookInput and write it into the materialized
hook_received event so it gets the same replay-divergence guard as a
directly written event (#2030 parity).
- Export ResumedHook from @workflow/core/runtime and workflow/api.
- Add changelog page and update resumeHook() API reference docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TooTallNate added a commit that referenced this pull request Jul 31, 2026
…ilures (#1834)
* Make resumeHook() resilient to transient hook_received event write failures
When events.create('hook_received') fails with a retryable error (429/5xx),
resumeHook() now dispatches the queue message with a `hookInput` payload
carrying the dehydrated hook payload. The workflow runtime materializes the
missing hook_received event from that payload on its next delivery, mirroring
the existing resilient-start behavior of start() / run_created / run_started.
Returned Hook carries a new `resilientResume: true` flag when the fallback
path was taken. Both write paths share a client-minted `resumeId` as an
idempotency key so the runtime can dedup if the direct write actually
committed but the client saw a transient error.
Uses a sequential write-then-queue flow (not parallel) to avoid a dedup race
on the happy path: hook_received events have no entity-level conflict guard
(unlike run_created), so a duplicate written before the direct write commits
would double-deliver the payload to the workflow.
* Fix resilient resume: use local payload in materialized hook_received event
The server returns a 'lazy' response for hook_received event creation,
where eventData.payload may be a RefDescriptor (when the payload
exceeded the inline size and was offloaded to blob storage) rather
than the raw bytes. Pushing this directly to the in-memory events
array caused the workflow VM to fail with 'Invalid input' when trying
to deserialize the RefDescriptor as a Uint8Array.
Substitute the eventData we already have locally so the in-memory
event matches what getWorkflowRunEvents would return after
client-side ref hydration.
* Gate resilient resume on target runtime capability; carry hook token; export ResumedHook; docs
- Only take the resilient path when the target run's recorded
@workflow/core version understands hookInput on the queue payload.
Runs keep executing on the deployment they were created on (skew
protection), and older runtimes parse the queue message with a schema
that silently strips unknown fields - the resume payload would be
lost while resumeHook() reported success. Fail fast (propagate the
original event-write error) for such runs instead, preserving the
caller's ability to retry.
- Carry the hook token on hookInput and write it into the materialized
hook_received event so it gets the same replay-divergence guard as a
directly written event (#2030 parity).
- Export ResumedHook from @workflow/core/runtime and workflow/api.
- Add changelog page and update resumeHook() API reference docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Address review: correct capability cutoff, drop own-version escape hatch, replay-side resumeId dedup
Review fixes for the resilient-resume capability gate and dedup:
- Bump the supportsQueueHookInput cutoff to 5.0.0-beta.39: 5.0.0-beta.38 is
published WITHOUT this feature (its queue-payload schema strips hookInput),
so classifying it as capable would silently lose resume payloads. The
cutoff is now a single exported constant (QUEUE_HOOK_INPUT_MIN_VERSION)
with a TODO(release) requiring re-verification at merge time.
- Remove the own-version exact-match escape hatch entirely: version strings
do not identify builds (a published beta.38 and a main-built tarball can
share a version string while differing in content), so the check could
declare a featureless published deployment capable. Pre-release builds now
fall back to fail-fast until the version is bumped past the cutoff — the
safe direction. Tests simulate a capable target explicitly.
- Make duplicate suppression authoritative at the replay boundary: replay
now dedups hook_received events sharing a resumeId (same resume attempt),
so even when concurrent redelivery of the same queue message
double-materializes the event (no World enforces uniqueness on
hook_received), the payload reaches workflow code exactly once. This is a
pure function of the persisted log, keeping replay deterministic. The
runtime's snapshot check remains as best-effort write suppression, with
its comment corrected to say so; the EntityConflictError catch is kept as
the forward-compatible signal for planned server-side (runId, resumeId)
uniqueness, with its comment corrected to say it is defensive today.
- Stamp materialized hook_received events with occurredAt decoded from the
resumeId ULID so resiliently-resumed hooks are timestamped at resume time
rather than after the queue round-trip.
- Pin the cross-version compat contract in a test: the direct write is
resumeId-only (no digest or negotiation fields), which later server-side
idempotency work must keep accepting.
- Exercise the published boundary (5.0.0-beta.38) in fail-fast tests, and
make the capability tests self-check against the exported cutoff constant
instead of restating literals.
- Docs: changelog date June -> July 2026, dash consistency, and document the
replay-side dedup guarantee.
* Encode release-gate and successor-rebase contracts into code comments
Comment-only changes capturing the review agreements so they survive the
parallel-resume successor rebase (no behavior change):
- capabilities.ts: the QUEUE_HOOK_INPUT_MIN_VERSION re-verification point
is the actual combined SDK release (after the successor lands and its
server-side dedup is deployed), not source-merge time — this PR merges
source-only and no SDK is published from it alone. Every Version
Packages merge in between moves the earliest possible carrier.
- workflow/hook.ts + runtime.ts: scope the replay-side resumeId dedup
honestly as defense-in-depth over the persisted log, not a
cross-invocation exactly-once guarantee — concurrent invocations
replaying pre-duplicate snapshots each see only their own row; the
storage-level (runId, resumeId) constraint in the successor work is the
correctness boundary. The set stays useful post-constraint for logs
written before it deployed.
- runtime.ts: document the EntityConflictError swallow's known gap while
the branch is defensive (this invocation's local log lacks the payload;
progress relies on the other writer's delivery or redelivery) and pin
the rebase contract for when the constraint makes it live: a matching
claim must append the canonical event locally and succeed; a real
conflict must rethrow for redelivery.
- resume-hook-resilient.test.ts: reframe the wire-shape pin as a tripwire
rather than a permanent contract — the successor deliberately widens it
(ID/digest pair + attestation) before any SDK release, so the
resumeId-only shape never ships as a published server contract.
---------
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto 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.

3 participants

@pranaygp@VaguelySerious