Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Fix concurrent wait_completed race condition in world-local - #1388

Closed
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed
Closed

Fix concurrent wait_completed race condition in world-local#1388
TooTallNate wants to merge 3 commits into
mainfrom
fix/world-local-duplicate-wait-completed

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

  • Fix a TOCTOU race condition in world-local where two concurrent runtime invocations could both create wait_completed events for the same wait, causing duplicate events in the event log
  • The duplicate wait_completed caused WorkflowRuntimeError: Unconsumed event during replay, since the sleep callback consumed the first event and was removed, leaving the second with no consumer
  • Use writeExclusive (O_CREAT|O_EXCL) to atomically claim a .completed lock file before transitioning a wait to completed — if a concurrent invocation already claimed it, the second gets a 409 which the runtime's existing conflict handler gracefully skips
  • Add createWait and completeWait test helpers
  • Add tests for wait lifecycle including a concurrent race regression test (Promise.allSettled with two simultaneous completions — exactly one succeeds, the other gets 409)

Use writeExclusive (O_CREAT|O_EXCL) to atomically prevent concurrent
invocations from both completing the same wait. Previously, two
concurrent runtime invocations could both read the wait as 'waiting'
and both create wait_completed events, causing duplicate events in the
event log. On replay, the sleep callback consumed the first and the
second was reported as an unconsumed event.
Add wait test helpers (createWait, completeWait) and tests for:
- Basic wait creation and completion
- Duplicate wait_created rejection
- Sequential duplicate wait_completed rejection (409)
- Concurrent wait_completed race (Promise.allSettled, exactly one wins)
@vercel

vercelBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

@TooTallNate
TooTallNate requested a review from a team as a code ownerMarch 14, 2026 09:32
CopilotAI review requested due to automatic review settings March 14, 2026 09:32
@changeset-bot

changeset-botBot commented Mar 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c3cac83

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

This PR includes changesets to release 18 packages
NameType
@workflow/world-localPatch
@workflow/corePatch
workflowPatch
@workflow/cliPatch
@workflow/vitestPatch
@workflow/world-postgresPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/world-testingPatch
@workflow/aiPatch
@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

@github-actions

github-actionsBot commented Mar 14, 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🥇 Express0.040s (-11.4% 🟢)1.005s (-2.0%)0.965s101.00x
💻 LocalNitro0.043s (~)1.005s (~)0.963s101.07x
💻 LocalNext.js (Turbopack)0.050s1.005s0.955s101.26x
🌐 RedisNext.js (Turbopack)0.056s1.006s0.950s101.41x
🐘 PostgresNitro0.060s (-3.2%)1.011s (~)0.951s101.52x
🐘 PostgresExpress0.069s (+9.2% 🔺)1.012s (~)0.943s101.74x
🌐 MongoDBNext.js (Turbopack)0.115s1.008s0.893s102.89x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.462s (-1.2%)2.766s (+1.4%)2.304s101.00x
▲ VercelNitro0.495s (-26.7% 🟢)2.649s (+6.8% 🔺)2.154s101.07x
▲ VercelNext.js (Turbopack)0.538s (-43.5% 🟢)2.606s (-12.2% 🟢)2.068s101.16x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.111s2.006s0.895s101.00x
💻 LocalExpress1.121s (~)2.005s (~)0.884s101.01x
💻 LocalNitro1.123s (~)2.005s (~)0.882s101.01x
🌐 RedisNext.js (Turbopack)1.128s2.007s0.879s101.02x
🐘 PostgresNitro1.139s (-0.9%)2.012s (~)0.873s101.03x
🐘 PostgresExpress1.158s (+1.3%)2.012s (~)0.854s101.04x
🌐 MongoDBNext.js (Turbopack)1.315s2.009s0.694s101.18x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.027s (-22.3% 🟢)3.892s (-17.2% 🟢)1.864s101.00x
▲ VercelNext.js (Turbopack)2.165s (+0.8%)3.597s (-5.7% 🟢)1.432s101.07x
▲ VercelExpress2.545s (+22.6% 🔺)4.189s (+15.0% 🔺)1.645s101.26x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)10.812s11.024s0.212s31.00x
💻 LocalNext.js (Turbopack)10.838s11.024s0.185s31.00x
💻 LocalNitro10.898s (~)11.023s (~)0.125s31.01x
💻 LocalExpress10.899s (~)11.023s (~)0.124s31.01x
🐘 PostgresNitro10.904s (~)11.039s (~)0.135s31.01x
🐘 PostgresExpress10.923s (~)11.043s (~)0.120s31.01x
🌐 MongoDBNext.js (Turbopack)12.294s13.024s0.730s31.14x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express16.570s (-2.2%)18.028s (-1.3%)1.458s21.00x
▲ VercelNext.js (Turbopack)16.899s (-3.6%)18.788s (-2.2%)1.889s21.02x
▲ VercelNitro17.663s (+1.0%)19.093s (-6.0% 🟢)1.430s21.07x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)26.940s27.388s0.448s31.00x
🐘 PostgresNitro27.073s (~)27.725s (-1.2%)0.652s31.00x
💻 LocalNext.js (Turbopack)27.102s28.053s0.951s31.01x
🐘 PostgresExpress27.208s (~)28.065s (~)0.857s31.01x
💻 LocalNitro27.446s (~)28.053s (~)0.607s31.02x
💻 LocalExpress27.516s (~)28.054s (~)0.538s31.02x
🌐 MongoDBNext.js (Turbopack)30.549s31.046s0.497s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)43.830s (~)45.404s (-2.3%)1.574s21.00x
▲ VercelNitro44.103s (+1.3%)45.430s (~)1.327s21.01x
▲ VercelExpress44.114s (+0.6%)45.883s (~)1.769s21.01x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)53.541s54.102s0.561s21.00x
🐘 PostgresNitro54.041s (~)54.099s (-1.8%)0.058s21.01x
🐘 PostgresExpress54.228s (~)55.102s (~)0.874s21.01x
💻 LocalNext.js (Turbopack)55.932s56.101s0.168s21.04x
💻 LocalNitro56.614s (~)57.102s (~)0.488s21.06x
💻 LocalExpress56.742s (~)57.103s (~)0.362s21.06x
🌐 MongoDBNext.js (Turbopack)60.706s61.069s0.363s21.13x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro91.182s (-4.8%)92.485s (-5.5% 🟢)1.303s11.00x
▲ VercelExpress92.565s (~)94.790s (+0.6%)2.225s11.02x
▲ VercelNext.js (Turbopack)96.993s (+0.6%)99.223s (+0.7%)2.230s11.06x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.299s2.007s0.708s151.00x
🐘 PostgresExpress1.415s (-4.6%)2.011s (~)0.596s151.09x
🐘 PostgresNitro1.416s (-1.8%)2.011s (~)0.594s151.09x
💻 LocalNitro1.500s (+0.6%)2.005s (~)0.505s151.16x
💻 LocalNext.js (Turbopack)1.523s2.005s0.482s151.17x
💻 LocalExpress1.558s (+5.3% 🔺)2.006s (~)0.447s151.20x
🌐 MongoDBNext.js (Turbopack)2.129s3.008s0.879s101.64x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.388s (-11.8% 🟢)4.451s (+1.5%)2.063s71.00x
▲ VercelExpress2.433s (+6.3% 🔺)3.855s (-1.6%)1.421s81.02x
▲ VercelNext.js (Turbopack)2.588s (+2.9%)3.990s (-2.6%)1.402s81.08x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.534s (-2.2%)3.013s (~)0.478s101.00x
🐘 PostgresExpress2.575s (-1.0%)3.013s (~)0.438s101.02x
🌐 RedisNext.js (Turbopack)2.585s3.009s0.424s101.02x
💻 LocalNext.js (Turbopack)2.667s3.008s0.341s101.05x
💻 LocalNitro2.919s (+2.1%)3.208s (-4.0%)0.289s101.15x
💻 LocalExpress2.976s (+0.7%)3.453s (+4.4%)0.477s91.17x
🌐 MongoDBNext.js (Turbopack)4.739s5.178s0.439s61.87x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.678s (~)4.209s (-9.2% 🟢)1.531s81.00x
▲ VercelExpress2.707s (+7.4% 🔺)3.971s (-9.5% 🟢)1.264s81.01x
▲ VercelNitro2.719s (+1.3%)4.404s (+5.7% 🔺)1.685s71.02x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.989s (~)4.589s (-3.0%)0.600s71.00x
🐘 PostgresExpress4.161s (+4.2%)4.446s (~)0.286s71.04x
🌐 RedisNext.js (Turbopack)4.200s5.011s0.812s61.05x
💻 LocalExpress7.942s (-2.6%)8.519s (-2.9%)0.577s41.99x
💻 LocalNext.js (Turbopack)7.982s8.516s0.533s42.00x
💻 LocalNitro8.643s (+5.9% 🔺)9.024s (~)0.381s42.17x
🌐 MongoDBNext.js (Turbopack)9.999s10.685s0.686s32.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.931s (-14.7% 🟢)4.124s (-19.8% 🟢)1.193s81.00x
▲ VercelNext.js (Turbopack)3.257s (-14.3% 🟢)5.458s (-1.2%)2.201s61.11x
▲ VercelNitro3.512s (+14.8% 🔺)5.562s (+21.6% 🔺)2.050s61.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🌐 Redis🥇 Next.js (Turbopack)1.275s2.007s0.732s151.00x
🐘 PostgresNitro1.389s (-2.3%)2.011s (~)0.622s151.09x
🐘 PostgresExpress1.401s (+1.3%)2.011s (~)0.610s151.10x
💻 LocalNext.js (Turbopack)1.506s2.005s0.499s151.18x
💻 LocalNitro1.527s (~)2.006s (~)0.479s151.20x
💻 LocalExpress1.536s (~)2.006s (~)0.470s151.21x
🌐 MongoDBNext.js (Turbopack)2.212s3.009s0.797s101.73x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)2.095s (-6.1% 🟢)3.884s (+3.0%)1.788s81.00x
▲ VercelNitro2.103s (-10.8% 🟢)3.797s (-16.5% 🟢)1.694s81.00x
▲ VercelExpress2.152s (-17.5% 🟢)3.960s (-11.1% 🟢)1.807s81.03x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.597s (+3.2%)3.012s (~)0.415s101.00x
🐘 PostgresNitro2.626s (-1.4%)3.011s (-3.3%)0.385s101.01x
🌐 RedisNext.js (Turbopack)2.630s3.008s0.379s101.01x
💻 LocalNext.js (Turbopack)2.781s3.107s0.327s101.07x
💻 LocalExpress2.895s (-3.2%)3.342s (-6.3% 🟢)0.446s91.12x
💻 LocalNitro2.993s (-1.9%)3.455s (-11.1% 🟢)0.462s91.15x
🌐 MongoDBNext.js (Turbopack)4.747s5.178s0.432s61.83x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.469s (+2.9%)3.867s (-4.6%)1.398s81.00x
▲ VercelNext.js (Turbopack)2.703s (+8.2% 🔺)4.823s (+21.2% 🔺)2.120s71.10x
▲ VercelExpress2.863s (+3.6%)4.140s (+1.0%)1.277s81.16x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro3.940s (-3.6%)4.446s (~)0.506s71.00x
🐘 PostgresExpress3.975s (+0.6%)4.445s (-3.1%)0.470s71.01x
🌐 RedisNext.js (Turbopack)4.204s5.012s0.808s61.07x
💻 LocalNitro8.447s (-9.1% 🟢)9.023s (-9.9% 🟢)0.576s42.14x
💻 LocalExpress8.534s (-2.2%)9.021s (~)0.487s42.17x
💻 LocalNext.js (Turbopack)8.570s9.017s0.447s42.18x
🌐 MongoDBNext.js (Turbopack)10.147s10.685s0.538s32.58x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.683s (+1.1%)4.105s (+5.3% 🔺)1.422s81.00x
▲ VercelExpress3.021s (+9.4% 🔺)4.308s (+6.5% 🔺)1.288s71.13x
▲ VercelNext.js (Turbopack)3.105s (-12.7% 🟢)5.640s (+3.3%)2.535s61.16x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.167s1.002s0.012s1.018s0.850s101.00x
🌐 RedisNext.js (Turbopack)0.184s1.000s0.002s1.008s0.824s101.10x
💻 LocalExpress0.198s (+1.7%)1.003s (~)0.013s (+9.6% 🔺)1.018s (~)0.820s101.18x
💻 LocalNitro0.199s (~)1.003s (~)0.011s (-2.6%)1.017s (~)0.819s101.19x
🐘 PostgresNitro0.214s (-3.1%)0.994s (~)0.002s (+23.1% 🔺)1.013s (~)0.799s101.28x
🐘 PostgresExpress0.225s (+1.6%)0.995s (~)0.002s (+33.3% 🔺)1.013s (~)0.788s101.35x
🌐 MongoDBNext.js (Turbopack)0.500s0.950s0.002s1.009s0.509s102.99x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.580s (-16.5% 🟢)2.616s (+14.8% 🔺)0.006s (-6.3% 🟢)3.429s (+7.9% 🔺)1.848s101.00x
▲ VercelNext.js (Turbopack)1.667s (-2.9%)2.616s (-3.6%)0.177s (+3589.6% 🔺)3.362s (-4.3%)1.695s101.05x
▲ VercelNitro1.817s (+13.9% 🔺)3.156s (+17.2% 🔺)0.006s (-95.7% 🟢)3.710s (+8.5% 🔺)1.893s101.15x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)8/12
🐘 PostgresNitro10/12
▲ VercelNitro5/12
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres6/12
Next.js (Turbopack)🌐 Redis7/12
Nitro🐘 Postgres5/12
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)

📋 View full workflow run

@github-actions

github-actionsBot commented Mar 14, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production714067781
✅ 💻 Local Development7400112852
✅ 📦 Local Production7400112852
❌ 🐘 Local Postgres7391112852
✅ 🪟 Windows680371
❌ 🌍 Community Worlds1165515186
✅ 📋 Other186027213
Total3303564483807

❌ Failed Tests

🐘 Local Postgres (1 failed)

vite-stable (1 failed):

  • webhookWorkflow
🌍 Community Worlds (55 failed)

mongodb (3 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

redis (2 failed):

  • hookWorkflow is not resumable via public webhook endpoint
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously

turso (50 failed):

  • addTenWorkflow
  • addTenWorkflow
  • wellKnownAgentWorkflow (.well-known/agent)
  • should work with react rendering in step
  • promiseAllWorkflow
  • promiseRaceWorkflow
  • promiseAnyWorkflow
  • importedStepOnlyWorkflow
  • hookWorkflow
  • hookWorkflow is not resumable via public webhook endpoint
  • webhookWorkflow
  • sleepingWorkflow
  • parallelSleepWorkflow
  • nullByteWorkflow
  • workflowAndStepMetadataWorkflow
  • fetchWorkflow
  • promiseRaceStressTestWorkflow
  • error handling error propagation workflow errors nested function calls preserve message and stack trace
  • error handling error propagation workflow errors cross-file imports preserve message and stack trace
  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
  • error handling retry behavior regular Error retries until success
  • error handling retry behavior FatalError fails immediately without retries
  • error handling retry behavior RetryableError respects custom retryAfter delay
  • error handling retry behavior maxRetries=0 disables retries
  • error handling catchability FatalError can be caught and detected with FatalError.is()
  • hookCleanupTestWorkflow - hook token reuse after workflow completion
  • concurrent hook token conflict - two workflows cannot use the same hook token simultaneously
  • hookDisposeTestWorkflow - hook token reuse after explicit disposal while workflow still running
  • stepFunctionPassingWorkflow - step function references can be passed as arguments (without closure vars)
  • stepFunctionWithClosureWorkflow - step function with closure variables passed as argument
  • closureVariableWorkflow - nested step functions with closure variables
  • spawnWorkflowFromStepWorkflow - spawning a child workflow using start() inside a step
  • health check (queue-based) - workflow and step endpoints respond to health check messages
  • pathsAliasWorkflow - TypeScript path aliases resolve correctly
  • Calculator.calculate - static workflow method using static step methods from another class
  • AllInOneService.processNumber - static workflow method using sibling static step methods
  • ChainableService.processWithThis - static step methods using this to reference the class
  • thisSerializationWorkflow - step function invoked with .call() and .apply()
  • customSerializationWorkflow - custom class serialization with WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE
  • instanceMethodStepWorkflow - instance methods with "use step" directive
  • crossContextSerdeWorkflow - classes defined in step code are deserializable in workflow context
  • stepFunctionAsStartArgWorkflow - step function reference passed as start() argument
  • cancelRun - cancelling a running workflow
  • cancelRun via CLI - cancelling a running workflow
  • pages router addTenWorkflow via pages router
  • pages router promiseAllWorkflow via pages router
  • pages router sleepingWorkflow via pages router
  • hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep
  • sleepWithSequentialStepsWorkflow - sequential steps work with concurrent sleep (control)

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro6407
✅ example6407
✅ express6407
✅ fastify6407
✅ hono6407
✅ nextjs-turbopack6902
✅ nextjs-webpack6902
✅ nitro6407
✅ nuxt6407
✅ sveltekit6407
✅ vite6407
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
✅ vite-stable6209
❌ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable6209
✅ express-stable6209
✅ fastify-stable6209
✅ hono-stable6209
✅ nextjs-turbopack-canary54017
✅ nextjs-turbopack-stable6803
✅ nextjs-webpack-canary54017
✅ nextjs-webpack-stable6803
✅ nitro-stable6209
✅ nuxt-stable6209
✅ sveltekit-stable6209
❌ vite-stable6119
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack6803
❌ 🌍 Community Worlds
AppPassedFailedSkipped
✅ mongodb-dev302
❌ mongodb5133
✅ redis-dev302
❌ redis5223
✅ turso-dev302
❌ turso4503
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable6209
✅ e2e-local-postgres-nest-stable6209
✅ e2e-local-prod-nest-stable6209

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: success
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: failure
  • Windows: success

Check the workflow run for details.

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

Fixes a concurrency bug in @workflow/world-local event storage where concurrent wait_completed writes could create duplicate events and break replay, by adding an atomic completion-claim mechanism and regression tests.

Changes:

  • Add createWait / completeWait helpers for storage lifecycle tests.
  • Update wait_completed handling to atomically claim a completion lock file via writeExclusive.
  • Add wait lifecycle + concurrent completion regression tests and a patch changeset.

Reviewed changes

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

FileDescription
packages/world-local/src/test-helpers.tsAdds wait-focused helper functions used by storage tests.
packages/world-local/src/storage/events-storage.tsIntroduces exclusive-create lock file to prevent concurrent double-completion.
packages/world-local/src/storage.test.tsAdds wait lifecycle tests, including a concurrent completion regression test.
.changeset/fix-concurrent-wait-completed.mdDeclares a patch release note for the race-condition fix.

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

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 731 to 749
@@ -732,12 +747,6 @@ export function createEventsStorage(
status: 404,
});
}
Comment on lines +725 to +729
const lockPath = taggedPath(
basedir,
'waits',
`${waitCompositeKey}.completed`,
tag
When a concurrent invocation already created a wait_completed event,
the local events array was missing it (the 409 handler just continued).
This could cause the workflow to not see the event during replay.
Re-fetch the full event log from the source of truth when any 409 is
encountered during wait completion, ensuring the events array has the
correct ordering with all events from concurrent invocations.
events.push(result.event!);
} catch (err) {
if (WorkflowAPIError.is(err) && err.status === 409) {
runtimeLogger.info('Wait already completed, skipping', {

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.

If we see a 409 here, wouldn't it mean we can exit this flow early since there's a different flow that created the wait_completed event? 🤔 Unsure. Otherwise LGTM

@TooTallNateTooTallNateMar 14, 2026

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya I had that same optimization in mind. I kinda worry about if that earlier replay might not have the full event log though (i.e. if two steps completed ~simultaneously after the wait had elapsed, first replay only has the first step_completed event but the second one has both?)

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.

Mmh fair. I think this is likely fine to merge and maybe only leads to addition unnecessary replays. Can we add a TODO in here to maybe log or monitor how often this happen? Might also need to re-examine with all the changes in #1338

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

LGTM let's add a comment or TODO though about re-examining this since it feels like a hack and it also applies to vercel/postgres world despite being a local world fix

@pranaygp

Copy link
Copy Markdown
Contributor

I have a new PR here: #1434 where I'm checking for invalid state transitions across local and postgres world and ensuring all of them are tight. That should supersede this PR I think

pranaygp added a commit that referenced this pull request Mar 18, 2026
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pranaygp added a commit that referenced this pull request Mar 18, 2026
…tions (#1434)
* [world-postgres] Fix TOCTOU race in step_started that corrupts event log
The step_started UPDATE had no conditional guard on step status, allowing
a concurrent execution to revert a completed step back to 'running'. This
caused duplicate step_completed events, triggering CORRUPTED_EVENT_LOG.
Add notInArray guard to match the existing pattern on step_completed and
the DynamoDB conditional expression used in the Vercel world.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-postgres] Add atomic terminal-state guards to all entity UPDATEs
Add conditional WHERE clauses to match the Vercel world's DynamoDB
conditional expressions, preventing TOCTOU races where concurrent
requests could bypass pre-validation and write invalid state transitions.
Changes:
- step_started: add NOT IN (completed, failed, cancelled) guard
- step_retrying: add terminal-state guard (was unguarded)
- step_completed/step_failed: add cancelled to guard
- run_completed/run_failed/run_cancelled: add terminal-state guards
- isStepTerminal: include cancelled status
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* [world-local] Add atomic terminal-state guards and concurrent race tests
Local world fixes:
- step_completed/step_failed: use writeExclusive lock to prevent
concurrent duplicate terminal transitions
- step_started: check for terminal lock file before allowing start
- wait_completed: use writeExclusive lock (port from PR #1388)
- isStepTerminal: include cancelled status
Tests:
- Concurrent step_completed race (exactly one succeeds, one gets 409)
- Concurrent step_failed race
- step_started rejection after concurrent step_completed
- Concurrent wait_completed race
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address PR review feedback
- Fix lock file extension: use .lock instead of .json via taggedPath to
avoid polluting entity directories with empty JSON files that cause
SyntaxError during listing/parsing
- Make startedAt update atomic using COALESCE in SQL instead of
deriving isFirstStart from the TOCTOU validation read
- Split changeset into separate entries per package
- Remove unnecessary context from changeset text
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Address review feedback from @TooTallNate
- step_started: replace fs.access() with a fresh re-read of the step
entity — honest about being best-effort rather than claiming atomicity
(local world is dev-only; postgres world has SQL-level atomic guards)
- wait_completed: clean up lock file on 404 to avoid leaked lock files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move lock files to .locks/ subdirectory off basedir
Lock files in entity directories (steps/, waits/) broke tests that
expect only tagged .json files. Move all locks to basedir/.locks/
and clean them up in clear().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: pass ISO string to COALESCE instead of Date object
The postgres driver can't serialize a Date object inside a raw sql
template literal. Convert to ISO string for proper parameterization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: embed tag in lock file names for test isolation
Parallel vitest workers with different tags would collide on untagged
lock files, causing spurious 409s. Include the tag in the lock filename
(e.g. stepId.terminal.vitest-0) matching the pattern from PR #1388.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.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.

4 participants

@TooTallNate@pranaygp@VaguelySerious