Skip to content

world-local: atomically dedupe duplicate step_created/wait_created events - #1877

Merged
TooTallNate merged 1 commit into
mainfrom
world-local-event-uniqueness
May 4, 2026
Merged

world-local: atomically dedupe duplicate step_created/wait_created events#1877
TooTallNate merged 1 commit into
mainfrom
world-local-event-uniqueness

Conversation

@TooTallNate

Copy link
Copy Markdown
Member

Summary

Fixes a race condition in @workflow/world-local where concurrent invocations producing identical correlationIds for step_created or wait_created events would both succeed and persist duplicate events in the log.

Background

step_created previously had no atomicity guard — two concurrent calls with the same correlationId both wrote the entity and the event, leaving the second write to silently overwrite the first.

wait_created used a TOCTOU read-then-check pattern: read the existing wait, throw if found, otherwise write. Under concurrency both readers can pass the existence check before either writes.

The rest of the runtime already expects EntityConflictError to be thrown on duplicate writes (see the EntityConflictError.is(err) catch path in runtime/snapshot-entrypoint.ts), so the missing guard was a real correctness gap.

Fix

Both branches now claim a per-(runId, correlationId) constraint file under .locks/{steps,waits}/ with O_CREAT|O_EXCL semantics (via the existing writeExclusive helper used for hook tokens). The loser surfaces as EntityConflictError.

Includes 3 regression tests covering:

  • Concurrent step_created with same correlationId.
  • Concurrent wait_created with same correlationId (replaces the prior TOCTOU pattern).
  • Sequential duplicate step_created (existing pass-through behavior preserved).

Verification

pnpm -F @workflow/world-local typecheck # clean
pnpm -F @workflow/world-local build # clean
pnpm -F @workflow/world-local test # 269 passed (was 266 before the 3 regression tests)

Extracted from PR #1300 (snapshot-runtime), where this fix originated. The snapshot runtime produces deterministic correlationIds across concurrent VM invocations of the same resumption by design — that path made the dedup gap reliably reproducible — but the fix is also valuable on its own for the replay runtime under any concurrent-create scenario.

CopilotAI review requested due to automatic review settings April 30, 2026 08:11
@changeset-bot

changeset-botBot commented Apr 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7f7a9ff

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

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

@vercel

vercelBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (-1.2%)1.006s (~)0.963s101.00x
💻 LocalExpress0.043s (-3.4%)1.005s (~)0.962s101.00x
💻 LocalNext.js (Turbopack)0.051s1.006s0.954s101.20x
🐘 PostgresNext.js (Turbopack)0.056s1.010s0.954s101.31x
🐘 PostgresExpress0.058s (-0.9%)1.010s (~)0.953s101.35x
🐘 PostgresNitro0.189s (+98.4% 🔺)1.084s (+3.9%)0.895s104.43x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.123s2.006s0.884s101.00x
💻 LocalExpress1.126s (~)2.005s (~)0.880s101.00x
💻 LocalNitro1.128s (~)2.006s (~)0.879s101.00x
🐘 PostgresNext.js (Turbopack)1.141s2.009s0.868s101.02x
🐘 PostgresExpress1.145s (~)2.011s (~)0.866s101.02x
🐘 PostgresNitro1.285s (+12.7% 🔺)2.050s (+2.0%)0.765s101.14x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)10.837s11.017s0.180s31.00x
💻 LocalNext.js (Turbopack)10.845s11.024s0.179s31.00x
🐘 PostgresExpress10.915s (~)11.025s (~)0.110s31.01x
💻 LocalExpress10.948s (~)11.025s (~)0.076s31.01x
💻 LocalNitro10.955s (~)11.022s (~)0.068s31.01x
🐘 PostgresNitro11.967s (+10.1% 🔺)12.354s (+12.0% 🔺)0.387s31.10x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)14.492s15.020s0.528s41.00x
🐘 PostgresExpress14.647s (~)15.026s (~)0.379s41.01x
💻 LocalNext.js (Turbopack)14.816s15.030s0.214s41.02x
💻 LocalNitro15.034s (~)15.530s (-3.1%)0.496s41.04x
💻 LocalExpress15.041s (~)16.033s (+6.7% 🔺)0.992s41.04x
🐘 PostgresNitro15.304s (+4.9%)16.027s (+6.6% 🔺)0.723s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)13.747s14.022s0.274s71.00x
🐘 PostgresExpress14.027s (~)14.594s (~)0.568s71.02x
🐘 PostgresNitro15.835s (+13.4% 🔺)16.192s (+13.2% 🔺)0.357s61.15x
💻 LocalNext.js (Turbopack)16.260s17.033s0.773s61.18x
💻 LocalExpress16.800s (+1.2%)17.031s (~)0.231s61.22x
💻 LocalNitro16.929s (+0.9%)17.366s (+2.0%)0.437s61.23x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.234s2.010s0.777s151.00x
🐘 PostgresExpress1.281s (+1.6%)2.010s (~)0.729s151.04x
🐘 PostgresNitro1.359s (+6.6% 🔺)2.019s (~)0.660s151.10x
💻 LocalExpress1.540s (+3.5%)2.006s (~)0.466s151.25x
💻 LocalNitro1.565s (-4.1%)2.006s (-3.3%)0.441s151.27x
💻 LocalNext.js (Turbopack)1.578s2.006s0.427s151.28x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.344s (-0.7%)3.008s (~)0.664s101.00x
🐘 PostgresNext.js (Turbopack)2.401s3.010s0.608s101.02x
🐘 PostgresNitro2.422s (+3.0%)3.011s (~)0.589s101.03x
💻 LocalNext.js (Turbopack)2.944s3.759s0.815s81.26x
💻 LocalExpress2.965s (~)3.308s (-4.2%)0.343s101.27x
💻 LocalNitro3.093s (-1.6%)3.760s (-3.2%)0.667s81.32x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.463s (-0.7%)4.013s (~)0.549s81.00x
🐘 PostgresNext.js (Turbopack)3.661s4.010s0.348s81.06x
🐘 PostgresNitro3.853s (+10.7% 🔺)4.323s (+7.8% 🔺)0.470s71.11x
💻 LocalExpress7.935s (-4.8%)8.772s (-2.8%)0.837s42.29x
💻 LocalNitro8.345s (~)9.022s (~)0.677s42.41x
💻 LocalNext.js (Turbopack)8.732s9.521s0.789s42.52x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.220s2.010s0.790s151.00x
🐘 PostgresExpress1.278s (+1.7%)2.008s (~)0.729s151.05x
🐘 PostgresNitro1.308s (+4.0%)2.011s (~)0.703s151.07x
💻 LocalExpress1.519s (-19.8% 🟢)2.006s (-15.2% 🟢)0.487s151.24x
💻 LocalNitro1.522s (-18.4% 🟢)2.006s (-14.3% 🟢)0.483s151.25x
💻 LocalNext.js (Turbopack)1.537s2.006s0.470s151.26x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.352s (~)3.012s (~)0.659s101.00x
🐘 PostgresNext.js (Turbopack)2.385s3.008s0.623s101.01x
🐘 PostgresNitro2.574s (+10.0% 🔺)3.011s (~)0.437s101.09x
💻 LocalNext.js (Turbopack)3.049s3.760s0.712s81.30x
💻 LocalNitro3.102s (+1.2%)3.885s (~)0.783s81.32x
💻 LocalExpress3.110s (-0.7%)4.010s (+6.6% 🔺)0.900s81.32x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.489s (~)4.014s (~)0.525s81.00x
🐘 PostgresNext.js (Turbopack)3.650s4.013s0.364s81.05x
🐘 PostgresNitro4.058s (+16.6% 🔺)4.674s (+16.6% 🔺)0.617s71.16x
💻 LocalNext.js (Turbopack)8.353s9.023s0.670s42.39x
💻 LocalExpress8.728s (-0.8%)9.024s (-2.7%)0.296s42.50x
💻 LocalNitro8.898s (-2.7%)9.522s (-5.0% 🟢)0.624s42.55x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.761s1.006s0.245s601.00x
🐘 PostgresExpress0.841s (~)1.023s (~)0.182s591.11x
💻 LocalNext.js (Turbopack)0.904s1.058s0.153s571.19x
💻 LocalExpress0.999s (+1.6%)1.424s (+32.3% 🔺)0.425s431.31x
💻 LocalNitro1.009s (+2.9%)1.611s (+47.2% 🔺)0.601s381.33x
🐘 PostgresNitro1.166s (+42.1% 🔺)1.664s (+65.3% 🔺)0.498s371.53x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.874s2.029s0.155s451.00x
🐘 PostgresExpress1.973s (~)2.284s (+1.2%)0.311s401.05x
🐘 PostgresNitro2.602s (+35.0% 🔺)3.055s (+45.5% 🔺)0.453s301.39x
💻 LocalNext.js (Turbopack)2.833s3.075s0.242s301.51x
💻 LocalNitro3.028s (~)3.730s (-0.8%)0.702s251.62x
💻 LocalExpress3.052s (+1.2%)3.885s (+8.4% 🔺)0.833s241.63x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)3.814s4.043s0.229s301.00x
🐘 PostgresExpress4.045s (+1.4%)4.626s (+5.9% 🔺)0.581s261.06x
🐘 PostgresNitro5.942s (+44.8% 🔺)6.390s (+38.8% 🔺)0.449s191.56x
💻 LocalNext.js (Turbopack)8.899s9.232s0.334s142.33x
💻 LocalNitro9.202s (-1.0%)9.788s (-2.3%)0.585s132.41x
💻 LocalExpress9.280s (+0.8%)9.865s (-1.5%)0.585s132.43x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.257s1.008s0.751s601.00x
🐘 PostgresExpress0.294s (+4.1%)1.007s (~)0.713s601.15x
🐘 PostgresNitro0.346s (+22.1% 🔺)1.034s (+2.6%)0.688s591.35x
💻 LocalNext.js (Turbopack)0.555s1.022s0.466s592.16x
💻 LocalExpress0.633s (+12.9% 🔺)1.022s (+1.7%)0.389s592.47x
💻 LocalNitro0.640s (+5.8% 🔺)1.021s (~)0.381s592.49x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.481s1.006s0.525s901.00x
🐘 PostgresExpress0.507s (-0.5%)1.007s (~)0.500s901.06x
🐘 PostgresNitro0.673s (+35.7% 🔺)1.164s (+15.7% 🔺)0.491s781.40x
💻 LocalExpress2.512s (~)3.009s (~)0.497s305.23x
💻 LocalNitro2.552s (+0.6%)3.009s (~)0.457s305.31x
💻 LocalNext.js (Turbopack)2.606s3.009s0.403s305.42x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.761s1.006s0.245s1201.00x
🐘 PostgresExpress0.819s (~)1.009s (-0.8%)0.190s1191.08x
🐘 PostgresNitro1.197s (+51.4% 🔺)1.627s (+61.5% 🔺)0.431s741.57x
💻 LocalNext.js (Turbopack)10.872s11.483s0.611s1114.28x
💻 LocalExpress10.982s (-1.9%)11.573s (-3.1%)0.591s1114.43x
💻 LocalNitro11.223s (~)12.030s (+3.1%)0.807s1014.75x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.177s1.003s0.012s1.018s0.841s101.00x
🐘 PostgresNext.js (Turbopack)0.186s1.000s0.001s1.009s0.823s101.05x
💻 LocalExpress0.209s (+5.0%)1.004s (~)0.012s (-3.3%)1.018s (~)0.809s101.18x
🐘 PostgresExpress0.214s (+4.4%)0.998s (~)0.002s (-6.3% 🟢)1.011s (~)0.797s101.21x
🐘 PostgresNitro0.215s (+4.8%)0.997s (~)0.028s (+1766.7% 🔺)1.055s (+4.4%)0.840s101.21x
💻 LocalNitro0.218s (+2.2%)1.004s (~)0.012s (-1.6%)1.018s (~)0.800s101.23x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.613s1.009s0.006s1.024s0.411s591.00x
🐘 PostgresExpress0.623s (-1.1%)1.007s (~)0.004s (+5.3% 🔺)1.023s (~)0.400s591.02x
💻 LocalExpress0.751s (-0.9%)1.011s (-1.8%)0.010s (+6.9% 🔺)1.023s (-1.7%)0.272s591.23x
💻 LocalNext.js (Turbopack)0.753s1.012s0.010s1.118s0.365s541.23x
💻 LocalNitro0.758s (-9.6% 🟢)1.012s (~)0.011s (+19.6% 🔺)1.025s (-8.1% 🟢)0.267s591.24x
🐘 PostgresNitro0.820s (+31.3% 🔺)1.274s (+26.6% 🔺)0.029s (+602.2% 🔺)1.325s (+29.6% 🔺)0.506s491.34x
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.935s1.177s0.000s1.184s0.249s511.00x
🐘 PostgresExpress0.981s (+2.1%)1.245s (-2.6%)0.000s (+91.7% 🔺)1.259s (-3.6%)0.278s481.05x
💻 LocalExpress1.222s (~)2.022s (~)0.000s (+10.0% 🔺)2.024s (~)0.802s301.31x
💻 LocalNitro1.237s (+1.1%)2.022s (~)0.000s (+200.0% 🔺)2.024s (~)0.787s301.32x
💻 LocalNext.js (Turbopack)1.242s2.019s0.000s2.022s0.780s301.33x
🐘 PostgresNitro1.370s (+41.4% 🔺)1.881s (+50.8% 🔺)0.000s (-100.0% 🟢)1.916s (+52.3% 🔺)0.546s331.46x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.738s (-1.9%)2.032s (-6.7% 🟢)0.000s (NaN%)2.072s (-5.8% 🟢)0.333s291.00x
🐘 PostgresNext.js (Turbopack)1.821s2.146s0.000s2.154s0.333s281.05x
🐘 PostgresNitro2.869s (+60.1% 🔺)3.355s (+56.7% 🔺)0.000s (-100.0% 🟢)3.444s (+58.4% 🔺)0.576s181.65x
💻 LocalExpress2.888s (-16.7% 🟢)3.086s (-23.5% 🟢)0.000s (-62.5% 🟢)3.088s (-23.5% 🟢)0.200s201.66x
💻 LocalNitro2.918s (-13.9% 🟢)3.192s (-20.8% 🟢)0.001s (-1.3%)3.195s (-20.9% 🟢)0.276s191.68x
💻 LocalNext.js (Turbopack)3.030s3.678s0.001s3.682s0.653s171.74x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)13/21
🐘 PostgresNext.js (Turbopack)16/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@github-actions

github-actionsBot commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10110671078
✅ 💻 Local Development10900861176
✅ 📦 Local Production10900861176
✅ 🐘 Local Postgres10900861176
✅ 🪟 Windows980098
✅ 📋 Other276018294
Total465503434998

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9107
✅ example9107
✅ express9107
✅ fastify9107
✅ hono9107
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro9107
✅ nuxt9107
✅ sveltekit9107
✅ vite9107
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9206
✅ express-stable9206
✅ fastify-stable9206
✅ hono-stable9206
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable9206
✅ nuxt-stable9206
✅ sveltekit-stable9206
✅ vite-stable9206
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9206
✅ express-stable9206
✅ fastify-stable9206
✅ hono-stable9206
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable9206
✅ nuxt-stable9206
✅ sveltekit-stable9206
✅ vite-stable9206
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9206
✅ express-stable9206
✅ fastify-stable9206
✅ hono-stable9206
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable9206
✅ nuxt-stable9206
✅ sveltekit-stable9206
✅ vite-stable9206
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9206
✅ e2e-local-postgres-nest-stable9206
✅ e2e-local-prod-nest-stable9206

📋 View full workflow run

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 race in @workflow/world-local where concurrent writers could create duplicate step_created / wait_created events (and overwrite entities) when the same correlationId is produced concurrently (notably by the snapshot runtime’s deterministic correlation IDs).

Changes:

  • Add an atomic per-(runId, correlationId) constraint-file claim (via writeExclusive / O_CREAT|O_EXCL) for step_created.
  • Replace wait_created’s TOCTOU read-then-check with the same atomic constraint-file claim.
  • Add regression tests covering concurrent duplicates for steps/waits and sequential duplicate steps, plus a changeset for a patch release.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

FileDescription
packages/world-local/src/storage/events-storage.tsAdds atomic .locks/{steps,waits} constraint-file claims to dedupe concurrent step_created/wait_created.
packages/world-local/src/storage.test.tsAdds regression coverage for concurrent duplicate creation races and sequential duplicate step_created.
.changeset/fix-world-local-step-created-race.mdPublishes a patch changeset describing the concurrency fix and behavior change.

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

Comment threadpackages/world-local/src/storage/events-storage.ts Outdated
…-local
Concurrent invocations producing identical correlationIds (as the snapshot
runtime does by design across replays) previously both succeeded and
persisted duplicate events. step_created had no guard at all; wait_created
used a TOCTOU read-then-check that allowed both writers through under
concurrency. Both now claim a per-(runId, correlationId) constraint file
with O_CREAT|O_EXCL before writing, so the loser surfaces as
EntityConflictError — which the runtime's dedup catch path already
handles.
@TooTallNate
TooTallNateforce-pushed the world-local-event-uniqueness branch from 2a4c395 to 7f7a9ffCompareMay 3, 2026 17:30
@TooTallNate
TooTallNate merged commit 92dc826 into mainMay 4, 2026
268 of 289 checks passed
@TooTallNate
TooTallNate deleted the world-local-event-uniqueness branch May 4, 2026 00:21
pranaygp added a commit that referenced this pull request May 4, 2026
…ignal
* origin/main:
[workbench] Add TanStack Start workbench and tests (#1875)
Atomically dedupe duplicate step_created/wait_created events in world-local (#1877)
Split tarball hosting out of docs into its own project (#1893)
Replace fixed-sleep hook waits with event-driven waitForHook helper (#1879)
pranaygp added a commit that referenced this pull request May 4, 2026
…lier-errors-followups
* origin-https/main:
[workbench] Add TanStack Start workbench and tests (#1875)
Atomically dedupe duplicate step_created/wait_created events in world-local (#1877)
Split tarball hosting out of docs into its own project (#1893)
Replace fixed-sleep hook waits with event-driven waitForHook helper (#1879)
# Conflicts:
#	pnpm-lock.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@TooTallNate@VaguelySerious