Skip to content

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

Open
TooTallNate wants to merge 1 commit intomainfrom
world-local-event-uniqueness
Open

world-local: atomically dedupe duplicate step_created/wait_created events#1877
TooTallNate wants to merge 1 commit intomainfrom
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.

Copilot AI review requested due to automatic review settings April 30, 2026 08:11
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 30, 2026

🦋 Changeset detected

Latest commit: 632010f

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

This PR includes changesets to release 19 packages
Name Type
@workflow/world-local Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/vitest Patch
@workflow/world-postgres Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/ai Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 30, 2026

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Apr 30, 2026 8:34am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 30, 2026 8:34am
example-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-astro-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-express-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-fastify-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-hono-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-nitro-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workbench-vite-workflow Ready Ready Preview, Comment Apr 30, 2026 8:34am
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 30, 2026 8:34am
workflow-swc-playground Ready Ready Preview, Comment Apr 30, 2026 8:34am
workflow-web Ready Ready Preview, Comment Apr 30, 2026 8:34am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

📊 Benchmark Results

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

workflow with no steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 0.041s (-7.7% 🟢) 1.005s (~) 0.964s 10 1.00x
💻 Local Nitro 0.043s (+0.9%) 1.005s (~) 0.961s 10 1.06x
🐘 Postgres Nitro 0.062s (-35.2% 🟢) 1.011s (-3.1%) 0.949s 10 1.51x
🐘 Postgres Express 0.062s (+6.9% 🔺) 1.011s (~) 0.949s 10 1.52x
workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.129s (~) 2.006s (~) 0.877s 10 1.00x
💻 Local Nitro 1.135s (~) 2.005s (~) 0.870s 10 1.01x
🐘 Postgres Nitro 1.153s (+1.2%) 2.011s (~) 0.858s 10 1.02x
🐘 Postgres Express 1.155s (+0.7%) 2.010s (~) 0.855s 10 1.02x
workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 10.927s (+0.5%) 11.026s (~) 0.099s 3 1.00x
💻 Local Express 10.944s (~) 11.023s (~) 0.079s 3 1.00x
💻 Local Nitro 10.948s (~) 11.022s (~) 0.074s 3 1.00x
🐘 Postgres Express 10.972s (~) 11.021s (~) 0.049s 3 1.00x
workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 14.684s (+0.6%) 15.026s (~) 0.342s 4 1.00x
🐘 Postgres Express 14.685s (+0.7%) 15.025s (~) 0.340s 4 1.00x
💻 Local Nitro 14.956s (-0.7%) 15.029s (-6.2% 🟢) 0.073s 4 1.02x
💻 Local Express 14.961s (~) 15.029s (~) 0.068s 4 1.02x
workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 14.155s (+1.3%) 15.025s (+5.0% 🔺) 0.870s 6 1.00x
🐘 Postgres Express 14.323s (+2.3%) 15.029s (+3.0%) 0.706s 6 1.01x
💻 Local Express 16.361s (-1.5%) 17.032s (~) 0.671s 6 1.16x
💻 Local Nitro 16.473s (-1.8%) 17.031s (~) 0.559s 6 1.16x
Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.273s (~) 2.010s (~) 0.737s 15 1.00x
🐘 Postgres Express 1.281s (+1.6%) 2.009s (~) 0.728s 15 1.01x
💻 Local Nitro 1.518s (-7.0% 🟢) 2.005s (-3.3%) 0.487s 15 1.19x
💻 Local Express 1.545s (+3.8%) 2.005s (~) 0.460s 15 1.21x
Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.346s (~) 3.011s (~) 0.665s 10 1.00x
🐘 Postgres Express 2.375s (+0.6%) 3.010s (~) 0.635s 10 1.01x
💻 Local Express 2.737s (-7.3% 🟢) 3.008s (-12.9% 🟢) 0.271s 10 1.17x
💻 Local Nitro 2.790s (-11.2% 🟢) 3.108s (-20.0% 🟢) 0.318s 10 1.19x
Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.460s (-0.6%) 4.014s (~) 0.554s 8 1.00x
🐘 Postgres Express 3.467s (-0.6%) 4.011s (~) 0.544s 8 1.00x
💻 Local Express 7.524s (-9.8% 🟢) 8.023s (-11.1% 🟢) 0.499s 4 2.17x
💻 Local Nitro 7.725s (-7.5% 🟢) 8.270s (-8.3% 🟢) 0.545s 4 2.23x
Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.258s (~) 2.009s (~) 0.751s 15 1.00x
🐘 Postgres Express 1.276s (+1.5%) 2.009s (~) 0.733s 15 1.01x
💻 Local Nitro 1.531s (-17.9% 🟢) 2.006s (-14.3% 🟢) 0.475s 15 1.22x
💻 Local Express 1.574s (-16.9% 🟢) 2.073s (-12.3% 🟢) 0.499s 15 1.25x
Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 2.334s (~) 3.009s (~) 0.675s 10 1.00x
🐘 Postgres Nitro 2.335s (~) 3.011s (~) 0.676s 10 1.00x
💻 Local Nitro 2.852s (-7.0% 🟢) 3.107s (-20.0% 🟢) 0.256s 10 1.22x
💻 Local Express 2.954s (-5.7% 🟢) 3.343s (-11.1% 🟢) 0.389s 9 1.27x
Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 3.452s (-0.8%) 4.009s (~) 0.557s 8 1.00x
🐘 Postgres Express 3.467s (-0.9%) 4.010s (~) 0.543s 8 1.00x
💻 Local Nitro 7.976s (-12.8% 🟢) 8.518s (-15.0% 🟢) 0.543s 4 2.31x
💻 Local Express 8.089s (-8.1% 🟢) 8.775s (-5.4% 🟢) 0.687s 4 2.34x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.824s (-1.8%) 1.006s (-1.7%) 0.182s 60 1.00x
🐘 Postgres Nitro 0.867s (+5.7% 🔺) 1.023s (+1.7%) 0.156s 59 1.05x
💻 Local Express 1.007s (+2.3%) 1.543s (+43.4% 🔺) 0.537s 39 1.22x
💻 Local Nitro 1.023s (+4.3%) 1.672s (+52.9% 🔺) 0.649s 36 1.24x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.993s (+0.8%) 2.467s (+9.3% 🔺) 0.474s 37 1.00x
🐘 Postgres Nitro 2.108s (+9.4% 🔺) 2.853s (+35.8% 🔺) 0.745s 32 1.06x
💻 Local Nitro 3.005s (-1.0%) 3.649s (-2.9%) 0.643s 25 1.51x
💻 Local Express 3.046s (+1.0%) 3.966s (+10.6% 🔺) 0.920s 23 1.53x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 4.049s (+1.5%) 4.665s (+6.8% 🔺) 0.617s 26 1.00x
🐘 Postgres Nitro 4.318s (+5.2% 🔺) 5.016s (+8.9% 🔺) 0.698s 24 1.07x
💻 Local Express 8.847s (-3.9%) 9.232s (-7.8% 🟢) 0.385s 14 2.19x
💻 Local Nitro 8.888s (-4.4%) 9.231s (-7.9% 🟢) 0.343s 14 2.20x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.280s (-0.8%) 1.007s (~) 0.727s 60 1.00x
🐘 Postgres Nitro 0.296s (+4.4%) 1.007s (~) 0.712s 60 1.06x
💻 Local Express 0.571s (+2.0%) 1.021s (+1.7%) 0.450s 59 2.04x
💻 Local Nitro 0.650s (+7.5% 🔺) 1.095s (+7.2% 🔺) 0.445s 55 2.32x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.485s (-4.8%) 1.006s (~) 0.521s 90 1.00x
🐘 Postgres Nitro 0.495s (~) 1.006s (~) 0.512s 90 1.02x
💻 Local Nitro 2.364s (-6.9% 🟢) 3.009s (~) 0.645s 30 4.87x
💻 Local Express 2.390s (-4.9%) 3.008s (~) 0.619s 30 4.93x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.784s (-0.7%) 1.008s (~) 0.223s 120 1.00x
🐘 Postgres Express 0.823s (+0.5%) 1.009s (-0.8%) 0.186s 119 1.05x
💻 Local Nitro 10.378s (-7.3% 🟢) 11.027s (-5.5% 🟢) 0.649s 11 13.23x
💻 Local Express 10.516s (-6.0% 🟢) 11.026s (-7.7% 🟢) 0.510s 11 13.41x
Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.206s (+0.7%) 0.998s (~) 0.001s (-20.0% 🟢) 1.010s (~) 0.803s 10 1.00x
🐘 Postgres Express 0.209s (+2.1%) 0.999s (~) 0.001s (-31.3% 🟢) 1.010s (~) 0.801s 10 1.02x
💻 Local Express 0.215s (+8.2% 🔺) 1.004s (~) 0.012s (-1.7%) 1.018s (~) 0.803s 10 1.04x
💻 Local Nitro 0.220s (+2.9%) 1.004s (~) 0.011s (-15.2% 🟢) 1.016s (~) 0.796s 10 1.07x
stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.629s (+0.8%) 1.006s (~) 0.004s (+4.5%) 1.023s (~) 0.394s 59 1.00x
🐘 Postgres Express 0.639s (+1.4%) 1.006s (~) 0.004s (+5.3% 🔺) 1.025s (~) 0.386s 59 1.02x
💻 Local Nitro 0.766s (-8.6% 🟢) 1.012s (~) 0.012s (+25.9% 🔺) 1.026s (-8.1% 🟢) 0.259s 59 1.22x
💻 Local Express 0.860s (+13.6% 🔺) 1.012s (-1.7%) 0.009s (-8.7% 🟢) 1.115s (+7.2% 🔺) 0.254s 54 1.37x
10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.957s (-1.2%) 1.246s (~) 0.000s (~) 1.258s (~) 0.301s 48 1.00x
🐘 Postgres Express 0.977s (+1.7%) 1.247s (-2.4%) 0.000s (-52.1% 🟢) 1.266s (-3.1%) 0.289s 48 1.02x
💻 Local Nitro 1.202s (-1.7%) 2.020s (~) 0.000s (+133.3% 🔺) 2.022s (~) 0.820s 30 1.26x
💻 Local Express 1.218s (-0.6%) 2.020s (~) 0.000s (+20.0% 🔺) 2.022s (~) 0.805s 30 1.27x
fan-out fan-in 10 streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.758s (-0.8%) 2.102s (-3.5%) 0.000s (NaN%) 2.115s (-3.8%) 0.357s 29 1.00x
🐘 Postgres Nitro 1.780s (-0.6%) 2.069s (-3.4%) 0.000s (+93.1% 🔺) 2.079s (-4.4%) 0.299s 29 1.01x
💻 Local Nitro 2.864s (-15.5% 🟢) 3.193s (-20.8% 🟢) 0.001s (+8.6% 🔺) 3.195s (-20.8% 🟢) 0.331s 19 1.63x
💻 Local Express 2.901s (-16.3% 🟢) 3.245s (-19.6% 🟢) 0.001s (+18.4% 🔺) 3.248s (-19.5% 🟢) 0.347s 19 1.65x

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 11/21
🐘 Postgres Nitro 14/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework 🥇 Fastest World Wins
Express 🐘 Postgres 18/21
Nitro 🐘 Postgres 19/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
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 30, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ ▲ Vercel Production 987 2 67 1056
❌ 💻 Local Development 1065 1 86 1152
✅ 📦 Local Production 1066 0 86 1152
❌ 🐘 Local Postgres 1064 2 86 1152
✅ 📋 Other 270 0 18 288
Total 4452 5 343 4800

❌ Failed Tests

▲ Vercel Production (2 failed)

nextjs-turbopack (1 failed):

  • DurableAgent e2e experimental_onToolCallStart (GAP) completes but callbacks are not called (GAP)

vite (1 failed):

  • getterStepWorkflow - getter functions with "use step" directive | wrun_01KQERV2N9YBF4GXKW4KCCTQE5 | 🔍 observability
💻 Local Development (1 failed)

hono-stable (1 failed):

  • distributedAbortController - TTL expiration triggers signal | wrun_01KQERX4DFR9N3N6X6AVVWEG57
🐘 Local Postgres (2 failed)

nitro-stable (2 failed):

  • fibonacciWorkflow - recursive workflow composition via start() | wrun_01KQERP4FC4EJM2HD6G4VR6AXY
  • health check (queue-based) - workflow and step endpoints respond to health check messages

Details by Category

❌ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 89 0 7
✅ example 89 0 7
✅ express 89 0 7
✅ fastify 89 0 7
✅ hono 89 0 7
❌ nextjs-turbopack 93 1 2
✅ nextjs-webpack 94 0 2
✅ nitro 89 0 7
✅ nuxt 89 0 7
✅ sveltekit 89 0 7
❌ vite 88 1 7
❌ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 90 0 6
✅ express-stable 90 0 6
✅ fastify-stable 90 0 6
❌ hono-stable 89 1 6
✅ nextjs-turbopack-canary 77 0 19
✅ nextjs-turbopack-stable 96 0 0
✅ nextjs-webpack-canary 77 0 19
✅ nextjs-webpack-stable 96 0 0
✅ nitro-stable 90 0 6
✅ nuxt-stable 90 0 6
✅ sveltekit-stable 90 0 6
✅ vite-stable 90 0 6
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 90 0 6
✅ express-stable 90 0 6
✅ fastify-stable 90 0 6
✅ hono-stable 90 0 6
✅ nextjs-turbopack-canary 77 0 19
✅ nextjs-turbopack-stable 96 0 0
✅ nextjs-webpack-canary 77 0 19
✅ nextjs-webpack-stable 96 0 0
✅ nitro-stable 90 0 6
✅ nuxt-stable 90 0 6
✅ sveltekit-stable 90 0 6
✅ vite-stable 90 0 6
❌ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 90 0 6
✅ express-stable 90 0 6
✅ fastify-stable 90 0 6
✅ hono-stable 90 0 6
✅ nextjs-turbopack-canary 77 0 19
✅ nextjs-turbopack-stable 96 0 0
✅ nextjs-webpack-canary 77 0 19
✅ nextjs-webpack-stable 96 0 0
❌ nitro-stable 88 2 6
✅ nuxt-stable 90 0 6
✅ sveltekit-stable 90 0 6
✅ vite-stable 90 0 6
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 90 0 6
✅ e2e-local-postgres-nest-stable 90 0 6
✅ e2e-local-prod-nest-stable 90 0 6

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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.

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

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

…-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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants