Skip to content

fix(world-local,world-postgres): make duplicate hook_created idempotent#2295

Merged
TooTallNate merged 18 commits into
mainfrom
fix/world-local-hook-self-conflict
Jun 11, 2026
Merged

fix(world-local,world-postgres): make duplicate hook_created idempotent#2295
TooTallNate merged 18 commits into
mainfrom
fix/world-local-hook-self-conflict

Conversation

@TooTallNate

@TooTallNate TooTallNate commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #2283. Both @workflow/world-local and @workflow/world-postgres were turning duplicate processing of the same hook_created (same runId, hookId, and token — e.g. cross-process replay, queue redelivery, or the snapshot-runtime same-tick replay reported in #1665) into a hook_conflict event, which then replayed as a self-conflict HookConflictError.

The fix in both worlds mirrors the existing step_created duplicate-correlation path: when the duplicate-detection branch fires and the existing claim/hook is for the same (runId, hookId), throw EntityConflictError instead of writing hook_conflict. The runtime's existing concurrent-replay catch path (suspension-handler.ts:142) then swallows it as a benign duplicate.

The issue reproduces in both worlds for the same reason — neither path distinguished "same hook being re-processed" from "different hook claiming the same token". world-postgres' unique partial index on workflow_events(runId, correlationId, eventType) for entity-creation events does not catch this case either, because the duplicate branch inserts a hook_conflict event (different eventType), not a second hook_created.

Subsequent review rounds hardened the world-local path into a full crash-recovery/convergence protocol: orphaned token claims, orphaned hook entities, event-first orphans, cross-process convergence on a canonical eventId, legacy-claim upgrades via a recovery-marker sidecar, and per-lifetime marker identity. See the review threads for the full history.

Changes

packages/world-local/src/storage/events-storage.ts

  • HookTokenClaimSchema preserves hookId (the persisted claim always carried it — only the read schema was dropping it, which is the root cause of world-local can turn duplicate same-hook creation into self-conflict #2283) and now also persists the canonical eventId so cross-process retries converge on a single event path.
  • In the hook_created branch, when writeExclusive on the token claim fails and the existing claim matches the incoming (runId, hookId), the retry adopts the canonical eventId (from the claim, or — for legacy claims without one — via an exclusive recovery-marker sidecar keyed by (token, runId, hookId)), and the outer writeExclusive(eventPath) atomically arbitrates publication: the loser throws EntityConflictError (swallowed by the runtime), the winner repairs any partial write. Cross-hook / cross-run token conflicts still emit hook_conflict.
  • The hook entity write is deferred until after the event publish commits, so a colliding retry cannot mutate already-committed entity state with its own payload.
  • The inverse crash window (event published, deferred entity write lost) is repaired on the next retry: the entity is reconstructed from the persisted event's payload (never the retry's), via a race-safe writeExclusive.
  • Recovery markers are cleaned up on every token-release path (hook_disposed, terminal-run cleanup, tagged clear()).

packages/world-postgres/src/storage.ts

  • In the hook_created branch, before writing a hook_conflict event, compare the existing hook's (runId, hookId) (already returned by the getHookByToken prepared statement) to the incoming event. On a match, verify the hook_created event actually exists for (runId, correlationId): if yes, throw EntityConflictError; if no (crash-orphaned hook row), skip the hook insert and complete the partial write by emitting hook_created. Otherwise fall through to the existing hook_conflict write.

Unit tests

Regression tests in the concurrent entity-creation races describe blocks of packages/world-local/src/storage.test.ts and packages/world-postgres/test/storage.test.ts, covering: same-hook duplicate idempotency, cross-hook/cross-run conflicts still emitting hook_conflict, orphaned-claim recovery, orphaned-entity recovery, event-first-orphan repair (entity rebuilt from the persisted event payload, not the retry's), no entity mutation on already-committed duplicates, cross-process convergence to one event (real child_process.fork subprocess workers), legacy-claim upgrades, and token reuse across run lifetimes.

E2E test

parallelStepsThenWebhookWorkflow in workbench/example/workflows/99_e2e.ts reproduces the user-facing scenario from #1665: N sequential iterations of await Promise.all([stepA, stepB]) followed by createWebhook() + await. When the two step resolutions align in the same tick, the workflow body is re-walked and each pass submits hook_created with the same deterministic (correlationId, token).

Note: this test is skipped on world-postgres (WORKFLOW_TARGET_WORLD includes postgres). The same-tick replay pattern it stresses also surfaces a separate, pre-existing world-postgres bug — a late concurrent step_started can land after step_completed in the event log because the step entity UPDATE and event INSERT are not atomic — which corrupts the run independently of the hook fix (the failing CI job's logs show the duplicate hook_created inserts were correctly rejected). Tracked in #2331; re-enable on postgres when that lands.

Verification

  • pnpm --filter @workflow/world-local test — 381 tests pass
  • pnpm --filter @workflow/world-postgres test — passes (testcontainers Postgres)
  • pnpm vitest run packages/core/e2e/e2e.test.ts -t "parallelStepsThenWebhookWorkflow" — passes against fixed world-local, fails against unfixed
  • pnpm build, pnpm typecheck, full pnpm test — green

Notes

Fixes #2283

Copilot AI review requested due to automatic review settings June 8, 2026 19:13
@TooTallNate TooTallNate requested a review from a team as a code owner June 8, 2026 19:13
@vercel

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

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 Jun 11, 2026 8:28pm
example-nextjs-workflow-webpack Ready Ready Preview, Comment Jun 11, 2026 8:28pm
example-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-astro-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-express-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-fastify-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-hono-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-nitro-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-nuxt-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-sveltekit-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-tanstack-start-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workbench-vite-workflow Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workflow-docs Ready Ready Preview, Comment, Open in v0 Jun 11, 2026 8:28pm
workflow-swc-playground Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workflow-tarballs Ready Ready Preview, Comment Jun 11, 2026 8:28pm
workflow-web Ready Ready Preview, Comment Jun 11, 2026 8:28pm

@changeset-bot

changeset-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8b80f55

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

This PR includes changesets to release 18 packages
Name Type
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/cli Patch
@workflow/core Patch
@workflow/vitest Patch
workflow Patch
@workflow/world-testing Patch
@workflow/builders Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/web-shared Patch
@workflow/web 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

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

📊 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 🥇 Nitro 0.043s (+0.7%) 1.008s (~) 0.965s 10 1.00x
💻 Local Express 0.043s (+1.9%) 1.005s (~) 0.962s 10 1.01x
💻 Local Next.js (Turbopack) 0.058s (-7.6% 🟢) 1.006s (~) 0.947s 10 1.37x
🐘 Postgres Express 0.066s (+7.0% 🔺) 1.014s (~) 0.949s 10 1.54x
🐘 Postgres Nitro 0.066s (~) 1.013s (~) 0.947s 10 1.54x
🐘 Postgres Next.js (Turbopack) 0.069s (-7.8% 🟢) 1.012s (~) 0.943s 10 1.62x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 0.285s (+5.4% 🔺) 2.536s (-1.7%) 2.251s 10 1.00x
▲ Vercel Next.js (Turbopack) 0.305s (-19.1% 🟢) 2.374s (-2.3%) 2.069s 10 1.07x
▲ Vercel Nitro 0.358s (-5.1% 🟢) 2.742s (+30.6% 🔺) 2.384s 10 1.26x

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

workflow with 1 step

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.093s (~) 2.006s (~) 0.914s 10 1.00x
💻 Local Nitro 1.097s (+0.5%) 2.007s (~) 0.910s 10 1.00x
🐘 Postgres Nitro 1.109s (~) 2.010s (~) 0.901s 10 1.01x
🐘 Postgres Express 1.119s (~) 2.012s (~) 0.893s 10 1.02x
💻 Local Next.js (Turbopack) 1.131s (~) 2.006s (~) 0.876s 10 1.03x
🐘 Postgres Next.js (Turbopack) 1.147s (~) 2.010s (~) 0.863s 10 1.05x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 1.723s (+2.7%) 3.673s (+1.0%) 1.950s 10 1.00x
▲ Vercel Nitro 1.871s (+11.6% 🔺) 3.881s (+12.5% 🔺) 2.011s 10 1.09x
▲ Vercel Express 1.910s (+8.9% 🔺) 3.713s (~) 1.803s 10 1.11x

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

workflow with 10 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 10.517s (~) 11.021s (~) 0.504s 3 1.00x
💻 Local Express 10.533s (~) 11.022s (~) 0.489s 3 1.00x
🐘 Postgres Nitro 10.572s (~) 11.019s (~) 0.447s 3 1.01x
🐘 Postgres Express 10.605s (~) 11.020s (~) 0.415s 3 1.01x
🐘 Postgres Next.js (Turbopack) 10.902s (-1.0%) 11.347s (-2.9%) 0.445s 3 1.04x
💻 Local Next.js (Turbopack) 10.915s (+1.3%) 11.356s (+3.0%) 0.441s 3 1.04x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 14.123s (~) 16.212s (~) 2.089s 2 1.00x
▲ Vercel Nitro 14.392s (+7.9% 🔺) 16.613s (+9.8% 🔺) 2.221s 2 1.02x
▲ Vercel Express 14.559s (+8.5% 🔺) 16.528s (+5.0% 🔺) 1.969s 2 1.03x

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

workflow with 25 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 13.705s (-1.0%) 14.028s (~) 0.322s 5 1.00x
💻 Local Nitro 13.776s (~) 14.028s (-1.4%) 0.251s 5 1.01x
🐘 Postgres Nitro 13.849s (~) 14.019s (~) 0.170s 5 1.01x
🐘 Postgres Express 13.877s (~) 14.020s (~) 0.143s 5 1.01x
🐘 Postgres Next.js (Turbopack) 14.384s (-0.8%) 15.010s (~) 0.626s 4 1.05x
💻 Local Next.js (Turbopack) 14.626s (+1.0%) 15.280s (+1.7%) 0.653s 4 1.07x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 21.790s (-2.8%) 23.980s (~) 2.190s 3 1.00x
▲ Vercel Next.js (Turbopack) 22.120s (+2.4%) 24.160s (+3.5%) 2.040s 3 1.02x
▲ Vercel Express 22.137s (+3.1%) 25.654s (+11.2% 🔺) 3.516s 3 1.02x

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

workflow with 50 sequential steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 12.394s (-0.9%) 13.025s (~) 0.630s 7 1.00x
💻 Local Nitro 12.461s (~) 13.024s (~) 0.563s 7 1.01x
🐘 Postgres Express 12.697s (~) 13.165s (+1.1%) 0.467s 7 1.02x
🐘 Postgres Nitro 12.799s (+1.8%) 13.305s (+2.2%) 0.506s 7 1.03x
💻 Local Next.js (Turbopack) 13.747s (+0.5%) 14.027s (~) 0.280s 7 1.11x
🐘 Postgres Next.js (Turbopack) 13.815s (-1.1%) 14.021s (-2.0%) 0.206s 7 1.11x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 31.939s (+0.7%) 34.401s (+3.3%) 2.462s 3 1.00x
▲ Vercel Next.js (Turbopack) 32.831s (+14.5% 🔺) 34.982s (+15.2% 🔺) 2.151s 3 1.03x
▲ Vercel Express 33.553s (+13.1% 🔺) 36.201s (+16.2% 🔺) 2.648s 3 1.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.181s (-3.4%) 2.006s (~) 0.825s 15 1.00x
🐘 Postgres Nitro 1.189s (-1.2%) 2.008s (~) 0.819s 15 1.01x
🐘 Postgres Express 1.197s (~) 2.008s (~) 0.811s 15 1.01x
💻 Local Express 1.253s (+5.3% 🔺) 2.007s (~) 0.754s 15 1.06x
🐘 Postgres Next.js (Turbopack) 1.269s (+0.8%) 2.007s (~) 0.738s 15 1.07x
💻 Local Next.js (Turbopack) 1.370s (+1.6%) 2.006s (~) 0.636s 15 1.16x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.730s (+7.0% 🔺) 4.642s (+3.1%) 1.912s 7 1.00x
▲ Vercel Nitro 3.363s (-9.1% 🟢) 4.957s (-4.3%) 1.594s 7 1.23x
▲ Vercel Next.js (Turbopack) 3.592s (+18.0% 🔺) 5.522s (+20.2% 🔺) 1.930s 7 1.32x

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

Promise.all with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.270s (+0.9%) 2.007s (~) 0.737s 15 1.00x
🐘 Postgres Express 1.283s (+1.6%) 2.007s (~) 0.724s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.391s (-0.7%) 2.008s (~) 0.617s 15 1.10x
💻 Local Nitro 1.694s (~) 2.006s (~) 0.312s 15 1.33x
💻 Local Express 1.727s (-6.2% 🟢) 2.006s (-6.7% 🟢) 0.279s 15 1.36x
💻 Local Next.js (Turbopack) 1.813s (-3.5%) 2.222s (+3.3%) 0.409s 14 1.43x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 4.298s (-31.1% 🟢) 6.102s (-21.2% 🟢) 1.804s 5 1.00x
▲ Vercel Express 4.970s (+12.5% 🔺) 6.675s (+9.1% 🔺) 1.706s 5 1.16x
▲ Vercel Next.js (Turbopack) 5.522s (-98.2% 🟢) 7.164s (-97.7% 🟢) 1.642s 5 1.28x

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

Promise.all with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.409s (~) 2.007s (~) 0.598s 15 1.00x
🐘 Postgres Express 1.415s (+1.0%) 2.008s (~) 0.592s 15 1.00x
🐘 Postgres Next.js (Turbopack) 1.708s (-5.3% 🟢) 2.008s (-12.6% 🟢) 0.300s 15 1.21x
💻 Local Express 4.539s (-15.7% 🟢) 5.348s (-13.5% 🟢) 0.809s 6 3.22x
💻 Local Next.js (Turbopack) 4.539s (-20.4% 🟢) 5.013s (-19.3% 🟢) 0.474s 6 3.22x
💻 Local Nitro 4.554s (-13.2% 🟢) 5.015s (-16.6% 🟢) 0.460s 6 3.23x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.863s (-65.5% 🟢) 8.038s (-56.0% 🟢) 2.175s 4 1.00x
▲ Vercel Express 6.717s (-26.4% 🟢) 9.422s (-17.0% 🟢) 2.705s 4 1.15x
▲ Vercel Next.js (Turbopack) 7.032s (+16.9% 🔺) 8.592s (+1.8%) 1.559s 4 1.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.192s (~) 2.009s (~) 0.816s 15 1.00x
🐘 Postgres Express 1.202s (-0.7%) 2.009s (~) 0.807s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.257s (-0.6%) 2.007s (~) 0.750s 15 1.05x
💻 Local Next.js (Turbopack) 1.361s (+0.7%) 2.006s (~) 0.646s 15 1.14x
💻 Local Express 1.527s (-7.9% 🟢) 2.007s (-3.3%) 0.480s 15 1.28x
💻 Local Nitro 1.558s (+3.3%) 2.007s (~) 0.448s 15 1.31x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.613s (-3.2%) 4.149s (-12.0% 🟢) 1.536s 8 1.00x
▲ Vercel Nitro 2.643s (+2.5%) 4.289s (+1.3%) 1.646s 7 1.01x
▲ Vercel Next.js (Turbopack) 2.940s (-28.3% 🟢) 4.511s (-20.0% 🟢) 1.571s 7 1.13x

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

Promise.race with 25 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.264s (-2.5%) 2.007s (~) 0.744s 15 1.00x
🐘 Postgres Nitro 1.272s (~) 2.008s (~) 0.736s 15 1.01x
🐘 Postgres Next.js (Turbopack) 1.420s (+1.1%) 2.010s (~) 0.589s 15 1.12x
💻 Local Nitro 1.919s (-11.6% 🟢) 2.395s (-7.6% 🟢) 0.476s 13 1.52x
💻 Local Next.js (Turbopack) 1.966s (-3.9%) 2.470s (-17.9% 🟢) 0.504s 13 1.56x
💻 Local Express 1.967s (-8.9% 🟢) 2.471s (-7.6% 🟢) 0.504s 13 1.56x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.684s (+7.5% 🔺) 5.499s (+7.8% 🔺) 1.815s 6 1.00x
▲ Vercel Nitro 4.191s (-13.2% 🟢) 6.054s (-5.4% 🟢) 1.864s 5 1.14x
▲ Vercel Next.js (Turbopack) 4.432s (+21.6% 🔺) 6.459s (+23.8% 🔺) 2.027s 5 1.20x

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

Promise.race with 50 concurrent steps

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.422s (+2.5%) 2.008s (~) 0.585s 15 1.00x
🐘 Postgres Express 1.457s (+3.9%) 2.008s (~) 0.551s 15 1.02x
🐘 Postgres Next.js (Turbopack) 1.685s (-6.8% 🟢) 2.316s (+4.1%) 0.631s 13 1.18x
💻 Local Next.js (Turbopack) 5.108s (-15.6% 🟢) 5.679s (-16.7% 🟢) 0.571s 6 3.59x
💻 Local Nitro 5.331s (-1.9%) 5.848s (-2.7%) 0.518s 6 3.75x
💻 Local Express 5.387s (-15.3% 🟢) 6.014s (-14.3% 🟢) 0.627s 6 3.79x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.654s (-19.2% 🟢) 7.277s (-15.4% 🟢) 1.622s 5 1.00x
▲ Vercel Express 5.708s (+11.9% 🔺) 7.379s (+1.9%) 1.671s 5 1.01x
▲ Vercel Next.js (Turbopack) 6.313s (-21.4% 🟢) 8.096s (-17.3% 🟢) 1.783s 5 1.12x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 0.595s (-0.9%) 1.005s (~) 0.410s 60 1.00x
🐘 Postgres Nitro 0.610s (+5.3% 🔺) 1.007s (~) 0.397s 60 1.03x
💻 Local Express 0.613s (~) 1.022s (+1.7%) 0.409s 59 1.03x
🐘 Postgres Express 0.626s (+4.3%) 1.024s (+1.7%) 0.398s 59 1.05x
🐘 Postgres Next.js (Turbopack) 0.832s (-2.6%) 1.023s (~) 0.191s 59 1.40x
💻 Local Next.js (Turbopack) 0.897s (+4.0%) 1.039s (+1.7%) 0.142s 58 1.51x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 6.562s (-35.3% 🟢) 8.271s (-29.8% 🟢) 1.709s 8 1.00x
▲ Vercel Nitro 6.835s (+37.8% 🔺) 8.915s (+36.8% 🔺) 2.080s 7 1.04x
▲ Vercel Next.js (Turbopack) 7.354s (+40.8% 🔺) 8.920s (+30.0% 🔺) 1.566s 7 1.12x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 1.457s (+1.6%) 2.008s (~) 0.551s 45 1.00x
💻 Local Express 1.507s (-3.0%) 2.006s (-1.1%) 0.499s 45 1.03x
💻 Local Nitro 1.516s (+0.8%) 2.006s (~) 0.490s 45 1.04x
🐘 Postgres Nitro 1.521s (+5.9% 🔺) 2.102s (+3.5%) 0.581s 43 1.04x
🐘 Postgres Next.js (Turbopack) 1.972s (-2.8%) 2.228s (-16.1% 🟢) 0.256s 41 1.35x
💻 Local Next.js (Turbopack) 2.147s (+2.3%) 3.008s (~) 0.861s 30 1.47x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 14.229s (-1.0%) 16.298s (+0.9%) 2.068s 6 1.00x
▲ Vercel Nitro 14.700s (+12.2% 🔺) 16.963s (+17.3% 🔺) 2.263s 6 1.03x
▲ Vercel Next.js (Turbopack) 15.761s (+18.6% 🔺) 17.780s (+17.8% 🔺) 2.019s 6 1.11x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 2.805s (~) 3.167s (~) 0.362s 38 1.00x
🐘 Postgres Express 2.899s (+3.2%) 3.220s (+2.6%) 0.321s 38 1.03x
💻 Local Nitro 3.309s (~) 4.009s (~) 0.700s 30 1.18x
💻 Local Express 3.364s (+2.4%) 4.010s (~) 0.645s 30 1.20x
🐘 Postgres Next.js (Turbopack) 3.854s (-3.3%) 4.075s (-5.9% 🟢) 0.221s 30 1.37x
💻 Local Next.js (Turbopack) 4.335s (~) 5.010s (~) 0.676s 24 1.55x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 34.281s (+17.7% 🔺) 36.928s (+19.9% 🔺) 2.648s 4 1.00x
▲ Vercel Express 35.352s (+34.7% 🔺) 38.329s (+35.8% 🔺) 2.976s 4 1.03x
▲ Vercel Next.js (Turbopack) 37.624s (+5.3% 🔺) 40.077s (+6.9% 🔺) 2.453s 3 1.10x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.227s (+3.6%) 1.006s (~) 0.779s 60 1.00x
🐘 Postgres Express 0.244s (+5.2% 🔺) 1.006s (~) 0.762s 60 1.08x
🐘 Postgres Next.js (Turbopack) 0.278s (~) 1.006s (~) 0.728s 60 1.22x
💻 Local Express 0.427s (+3.4%) 1.005s (~) 0.578s 60 1.88x
💻 Local Nitro 0.440s (+2.4%) 1.005s (-1.7%) 0.564s 60 1.94x
💻 Local Next.js (Turbopack) 0.605s (+10.7% 🔺) 1.040s (+3.5%) 0.435s 58 2.66x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.547s (-0.8%) 4.231s (+3.0%) 1.683s 15 1.00x
▲ Vercel Nitro 2.753s (-2.5%) 4.727s (+9.5% 🔺) 1.974s 13 1.08x
▲ Vercel Next.js (Turbopack) 3.245s (-27.2% 🟢) 5.075s (-18.5% 🟢) 1.830s 12 1.27x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.361s (+0.7%) 1.006s (~) 0.645s 90 1.00x
🐘 Postgres Express 0.371s (-0.8%) 1.007s (~) 0.636s 90 1.03x
🐘 Postgres Next.js (Turbopack) 0.474s (-5.1% 🟢) 1.006s (~) 0.533s 90 1.31x
💻 Local Nitro 2.202s (+1.0%) 2.767s (+1.9%) 0.565s 33 6.09x
💻 Local Express 2.268s (+11.3% 🔺) 2.853s (+12.5% 🔺) 0.585s 32 6.28x
💻 Local Next.js (Turbopack) 2.507s (+2.3%) 3.184s (+1.1%) 0.676s 29 6.94x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 5.998s (-35.2% 🟢) 7.670s (-29.4% 🟢) 1.672s 12 1.00x
▲ Vercel Express 6.064s (+12.0% 🔺) 7.819s (+10.5% 🔺) 1.754s 12 1.01x
▲ Vercel Next.js (Turbopack) 6.587s (-14.5% 🟢) 8.239s (-12.2% 🟢) 1.652s 12 1.10x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Express 0.694s (~) 1.006s (~) 0.312s 120 1.00x
🐘 Postgres Nitro 0.705s (+3.5%) 1.006s (~) 0.301s 120 1.02x
🐘 Postgres Next.js (Turbopack) 0.988s (-1.7%) 1.567s (-15.5% 🟢) 0.579s 77 1.42x
💻 Local Nitro 9.484s (+2.2%) 10.109s (+3.2%) 0.625s 12 13.66x
💻 Local Express 9.722s (+4.4%) 10.195s (+3.3%) 0.473s 12 14.01x
💻 Local Next.js (Turbopack) 10.945s (~) 11.847s (+3.2%) 0.902s 11 15.77x

▲ Production (Vercel)

World Framework Workflow Time Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 16.330s (-26.2% 🟢) 18.492s (-24.2% 🟢) 2.162s 7 1.00x
▲ Vercel Nitro 16.648s (-13.6% 🟢) 18.682s (-15.6% 🟢) 2.033s 7 1.02x
▲ Vercel Express 18.002s (-4.8%) 20.383s (-0.7%) 2.381s 6 1.10x

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

Stream Benchmarks (includes TTFB metrics)
workflow with stream

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Express 1.161s (-0.7%) 2.005s (~) 0.011s (-8.5% 🟢) 2.018s (~) 0.858s 10 1.00x
💻 Local Nitro 1.167s (~) 2.004s (~) 0.011s (+1.9%) 2.018s (~) 0.851s 10 1.01x
🐘 Postgres Nitro 1.170s (~) 1.999s (~) 0.001s (+7.7% 🔺) 2.011s (~) 0.841s 10 1.01x
🐘 Postgres Express 1.176s (~) 1.999s (~) 0.001s (+8.3% 🔺) 2.011s (~) 0.835s 10 1.01x
💻 Local Next.js (Turbopack) 1.216s (~) 2.004s (~) 0.011s (-13.7% 🟢) 2.018s (~) 0.802s 10 1.05x
🐘 Postgres Next.js (Turbopack) 1.256s (+0.9%) 2.002s (~) 0.001s (-15.4% 🟢) 2.012s (~) 0.756s 10 1.08x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 2.373s (+9.5% 🔺) 3.463s (+8.1% 🔺) 1.350s (+48.6% 🔺) 5.282s (+15.3% 🔺) 2.908s 10 1.00x
▲ Vercel Next.js (Turbopack) 2.450s (+9.1% 🔺) 3.729s (+15.4% 🔺) 1.192s (+4.4%) 5.403s (+10.7% 🔺) 2.954s 10 1.03x
▲ Vercel Nitro 2.576s (+5.3% 🔺) 3.724s (+10.7% 🔺) 1.040s (+9.6% 🔺) 5.263s (+10.4% 🔺) 2.687s 10 1.09x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
💻 Local 🥇 Nitro 1.586s (-0.5%) 2.013s (~) 0.012s (-7.2% 🟢) 2.027s (~) 0.441s 30 1.00x
💻 Local Express 1.589s (+1.1%) 2.010s (~) 0.013s (+9.4% 🔺) 2.025s (~) 0.436s 30 1.00x
🐘 Postgres Nitro 1.601s (-0.7%) 2.007s (~) 0.005s (~) 2.028s (~) 0.426s 30 1.01x
🐘 Postgres Express 1.615s (+0.9%) 2.008s (~) 0.005s (+0.6%) 2.026s (~) 0.411s 30 1.02x
💻 Local Next.js (Turbopack) 1.717s (-1.3%) 2.008s (~) 0.013s (+5.7% 🔺) 2.024s (~) 0.307s 30 1.08x
🐘 Postgres Next.js (Turbopack) 1.739s (-2.6%) 2.011s (~) 0.005s (~) 2.027s (~) 0.288s 30 1.10x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Nitro 6.482s (+9.5% 🔺) 7.912s (+12.5% 🔺) 0.502s (+7.0% 🔺) 8.916s (+11.6% 🔺) 2.433s 7 1.00x
▲ Vercel Next.js (Turbopack) 6.581s (+2.1%) 7.849s (+1.9%) 0.166s (-70.4% 🟢) 8.672s (-2.3%) 2.091s 7 1.02x
▲ Vercel Express 6.688s (+10.1% 🔺) 8.275s (+13.4% 🔺) 0.241s (-8.3% 🟢) 9.015s (+12.7% 🔺) 2.327s 7 1.03x

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

10 parallel streams (1MB each)

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 0.766s (+8.0% 🔺) 1.031s (-1.2%) 0.000s (-34.5% 🟢) 1.042s (-2.0%) 0.277s 58 1.00x
🐘 Postgres Express 0.776s (+9.7% 🔺) 1.087s (+7.2% 🔺) 0.000s (+216.1% 🔺) 1.105s (+7.5% 🔺) 0.329s 56 1.01x
🐘 Postgres Next.js (Turbopack) 0.827s (-4.1%) 1.052s (-3.6%) 0.000s (-51.8% 🟢) 1.060s (-3.6%) 0.233s 57 1.08x
💻 Local Express 1.442s (+6.8% 🔺) 2.014s (~) 0.000s (+100.0% 🔺) 2.017s (~) 0.575s 30 1.88x
💻 Local Nitro 1.446s (~) 2.014s (~) 0.000s (+160.0% 🔺) 2.017s (~) 0.571s 30 1.89x
💻 Local Next.js (Turbopack) 1.530s (+4.3%) 2.014s (~) 0.000s (-26.7% 🟢) 2.018s (~) 0.488s 30 2.00x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Express 3.369s (~) 4.811s (+5.2% 🔺) 0.000s (-100.0% 🟢) 5.323s (+5.6% 🔺) 1.954s 12 1.00x
▲ Vercel Nitro 3.400s (-10.6% 🟢) 4.979s (+3.1%) 0.000s (NaN%) 5.471s (+4.8%) 2.071s 12 1.01x
▲ Vercel Next.js (Turbopack) 3.720s (+0.6%) 5.175s (+4.0%) 0.000s (-100.0% 🟢) 5.643s (+3.1%) 1.922s 11 1.10x

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

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

💻 Local Development

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
🐘 Postgres 🥇 Nitro 1.478s (+10.9% 🔺) 2.140s (+5.6% 🔺) 0.000s (NaN%) 2.154s (+4.4%) 0.676s 28 1.00x
🐘 Postgres Express 1.517s (-0.6%) 2.138s (-3.6%) 0.000s (-100.0% 🟢) 2.175s (-3.3%) 0.658s 28 1.03x
🐘 Postgres Next.js (Turbopack) 1.696s (-2.0%) 2.262s (~) 0.000s (NaN%) 2.269s (~) 0.573s 27 1.15x
💻 Local Next.js (Turbopack) 3.080s (+8.0% 🔺) 3.545s (+3.8%) 0.000s (+23.5% 🔺) 3.561s (+4.2%) 0.481s 17 2.08x
💻 Local Express 3.227s (+4.2%) 3.966s (+8.1% 🔺) 0.001s (+6.2% 🔺) 3.969s (+7.9% 🔺) 0.742s 16 2.18x
💻 Local Nitro 3.233s (+2.1%) 4.028s (+5.0% 🔺) 0.001s (+184.4% 🔺) 4.031s (+4.9%) 0.798s 15 2.19x

▲ Production (Vercel)

World Framework Workflow Time TTFB Slurp Wall Time Overhead Samples vs Fastest
▲ Vercel 🥇 Next.js (Turbopack) 5.445s (-38.7% 🟢) 6.743s (-43.4% 🟢) 0.000s (-100.0% 🟢) 7.225s (-42.2% 🟢) 1.781s 9 1.00x
▲ Vercel Nitro 5.779s (-21.6% 🟢) 7.212s (-9.1% 🟢) 0.001s (+Infinity% 🔺) 7.791s (-13.7% 🟢) 2.012s 8 1.06x
▲ Vercel Express 125.180s (+2252.0% 🔺) 126.485s (+1822.5% 🔺) 0.000s (NaN%) 126.947s (+1665.1% 🔺) 1.767s 1 22.99x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World 🥇 Fastest Framework Wins
💻 Local Nitro 10/21
🐘 Postgres Nitro 16/21
▲ Vercel Express 9/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

Passed Failed Skipped Total
✅ ▲ Vercel Production 1321 0 219 1540
✅ 💻 Local Development 1741 0 219 1960
✅ 📦 Local Production 1741 0 219 1960
✅ 🐘 Local Postgres 1727 0 233 1960
✅ 🪟 Windows 140 0 0 140
✅ 📋 Other 802 0 178 980
Total 7472 0 1068 8540

Details by Category

✅ ▲ Vercel Production
App Passed Failed Skipped
✅ astro 114 0 26
✅ example 114 0 26
✅ express 114 0 26
✅ fastify 114 0 26
✅ hono 114 0 26
✅ nextjs-turbopack 138 0 2
✅ nextjs-webpack 138 0 2
✅ nitro 114 0 26
✅ nuxt 114 0 26
✅ sveltekit 133 0 7
✅ vite 114 0 26
✅ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 115 0 25
✅ express-stable 115 0 25
✅ fastify-stable 115 0 25
✅ hono-stable 115 0 25
✅ nextjs-turbopack-canary 121 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 140 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 140 0 0
✅ nextjs-webpack-canary 121 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 140 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 140 0 0
✅ nitro-stable 115 0 25
✅ nuxt-stable 115 0 25
✅ sveltekit-stable 134 0 6
✅ vite-stable 115 0 25
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 115 0 25
✅ express-stable 115 0 25
✅ fastify-stable 115 0 25
✅ hono-stable 115 0 25
✅ nextjs-turbopack-canary 121 0 19
✅ nextjs-turbopack-stable-lazy-discovery-disabled 140 0 0
✅ nextjs-turbopack-stable-lazy-discovery-enabled 140 0 0
✅ nextjs-webpack-canary 121 0 19
✅ nextjs-webpack-stable-lazy-discovery-disabled 140 0 0
✅ nextjs-webpack-stable-lazy-discovery-enabled 140 0 0
✅ nitro-stable 115 0 25
✅ nuxt-stable 115 0 25
✅ sveltekit-stable 134 0 6
✅ vite-stable 115 0 25
✅ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 114 0 26
✅ express-stable 114 0 26
✅ fastify-stable 114 0 26
✅ hono-stable 114 0 26
✅ nextjs-turbopack-canary 120 0 20
✅ nextjs-turbopack-stable-lazy-discovery-disabled 139 0 1
✅ nextjs-turbopack-stable-lazy-discovery-enabled 139 0 1
✅ nextjs-webpack-canary 120 0 20
✅ nextjs-webpack-stable-lazy-discovery-disabled 139 0 1
✅ nextjs-webpack-stable-lazy-discovery-enabled 139 0 1
✅ nitro-stable 114 0 26
✅ nuxt-stable 114 0 26
✅ sveltekit-stable 133 0 7
✅ vite-stable 114 0 26
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 140 0 0
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 115 0 25
✅ e2e-local-dev-tanstack-start- 115 0 25
✅ e2e-local-postgres-nest-stable 114 0 26
✅ e2e-local-postgres-tanstack-start- 114 0 26
✅ e2e-local-prod-nest-stable 115 0 25
✅ e2e-local-prod-tanstack-start- 115 0 25
✅ e2e-vercel-prod-tanstack-start 114 0 26

📋 View full workflow run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an idempotency bug in @workflow/world-local where duplicate processing of the same hook_created could incorrectly produce a hook_conflict event and later replay as a self-conflict HookConflictError. It aligns hook_created behavior with the existing step_created duplicate-correlation path by treating same-entity duplicates as benign via EntityConflictError.

Changes:

  • Preserve hookId when reading existing hook token-claim files and use (runId, hookId) to detect duplicate same-hook creation.
  • On duplicate same-hook claim, throw EntityConflictError instead of writing a hook_conflict event; keep hook_conflict for genuine token conflicts.
  • Add regression tests covering same-hook duplicates vs. real conflicts (same token with different hook/run) and include a patch changeset.

Reviewed changes

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

File Description
packages/world-local/src/storage/events-storage.ts Updates hook token-claim parsing and makes duplicate same-hook hook_created idempotent by throwing EntityConflictError.
packages/world-local/src/storage.test.ts Adds regression tests ensuring same-hook duplicates don’t append hook_conflict, while real conflicts still do.
.changeset/fix-world-local-hook-self-conflict.md Adds a patch changeset documenting the behavior fix for @workflow/world-local.

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

Comment thread packages/world-local/src/storage/events-storage.ts Outdated
… duplicate hook_created

Addresses follow-up review on PR #2295.

The previous dedup branch checked whether the durable hook entity
existed on disk. But the hook entity is written before the
`hook_created` event, and the two writes are not atomic, so a crash
between them leaves both the claim file and the hook entity on disk
with no event in the log. The dedup branch then matched on
`(runId, hookId)`, found the hook entity, threw EntityConflictError,
and the suspension handler swallowed the retry — permanently losing
`hook_created` from the event log.

The fix mirrors what the world-postgres branch already does: probe
the run's event log for an existing `hook_created` event for the
same `(runId, correlationId)`. The event is the durable record of a
successful hook creation; the claim file and hook entity are partial-
write artifacts that may exist without the event.

- exists  → real duplicate: throw EntityConflictError so the
  runtime's concurrent-replay catch path swallows it.
- missing → orphaned partial write (crash at any point before the
  event landed): re-write the hook entity (with overwrite: true, in
  case a stale partial copy exists) and let the outer code path emit
  the hook_created event.

Added a new helper findHookCreatedEvent that runs a filtered
paginatedFileSystemQuery with limit:1 over the run's events.

Regression test "should recover an orphaned hook entity with no
matching hook_created event" added — pre-creates a hook, deletes
just the hook_created event from disk to simulate a crash between
the entity write and the event write, asserts the retry emits a
fresh hook_created event (no hook_conflict, no swallowed
EntityConflictError). I verified this test fails on the prior fix
(throws `EntityConflictError: Hook "hook_orphan_entity_1" already
created`, exactly as pranaygp reported) and passes on this commit.

The previous test ("should recover an orphaned hook token claim
with no matching hook entity") continues to pass — the event-log
probe is a strict superset of the entity probe, since a missing
entity always also implies a missing event.
…nical eventId

Addresses follow-up review on PR #2295.

The previous fix made the dedup branch probe the event log to decide
real-duplicate vs orphan-recovery, but the probe and the recovery
write are not a single atomic operation. Two workers sharing a data
directory (or two retries that lose `writeExclusive(constraintPath)`
back to back) could both pass the probe (each observing no
hook_created event yet), both fall through to the recovery write,
and both append a hook_created event with a different eventId —
producing two events in the log for the same (runId, hookId). The
in-process `withHookLock` mutex does not help here because it is
process-local and tag-specific.

The fix persists `eventId` in the durable token claim file (written
by the original `writeExclusive(constraintPath)`). On a same-(runId,
hookId) dedup match, retries adopt that canonical eventId and
rebuild the event with a deterministic createdAt derived from the
eventId (a ULID). The outer event write switches from `writeJSON`
(check-then-write, TOCTOU) to `writeExclusive` (O_CREAT|O_EXCL via
temp-file + hard-link, atomic across processes). Either worker may
win the publish; the other throws EntityConflictError which the
runtime's existing concurrent-replay catch path swallows. Net
result: exactly one hook_created event per logical creation.

Backward compatibility: a claim file written before this commit lacks
`eventId`. Retries that read such a claim fall back to the
event-log probe + fresh-eventId recovery — the legacy behavior that
does not converge across workers but cannot regress for freshly-
written claims after upgrade.

world-postgres already converges across workers via the partial
unique index on workflow_events_entity_creation_unique
(runId+correlationId+eventType for hook/step/wait_created): the
loser's INSERT raises 23505 which is already translated to
EntityConflictError.

Regression tests:

- world-local: `converges same-hook creation across workers to one
  event` uses two tagged storage instances sharing one data
  directory and fires 25 paired Promise.allSettled hook_created
  calls. Expected 25 hook_created events total; before this fix
  yielded 50.

- world-postgres: `converges same-hook creation across concurrent
  calls to one event` exercises the same shape against the real
  Postgres unique index. Already converges; the test is a guard
  against future regressions to the catch path.

Verified the world-local test fails on c7b23e1 with exactly the
shape pranaygp reported (50 events for 25 logical creations) and
passes on this commit. The earlier orphaned-claim and orphaned-
entity recovery tests also continue to pass.
…ecar; replace tag-proxy test with real subprocess workers

Addresses follow-up review on PR #2295.

Two distinct issues, both flagged by pranaygp as P1:

1. The fallback path for token claims written by versions before
   eventId was persisted inline (legacy claims after upgrade) still
   permitted the same cross-process corruption the inline fast path
   was fixed to prevent. Two processes both reading a legacy claim
   each generated their own eventId, landed their
   writeExclusive(eventPath) calls at different paths, and appended
   two hook_created events for the same (runId, hookId). Existing
   persisted claims after a real upgrade are exactly the state the
   crash-recovery branch needs to repair, so leaving the legacy path
   non-convergent is silent corruption, not backward compatibility.

2. The committed cross-worker convergence test used two tagged
   storage instances sharing one directory as a proxy for separate
   processes. But tags change the destination filename
   (events/wrun_X-evnt_Y.worker-a.json vs ...worker-b.json), so two
   tagged workers can each writeExclusive their own event at
   different paths and both fulfill. The Map-by-eventId
   deduplication in the assertion then masked the duplicate
   publication, so the test passed for the wrong reason.

Implementation:

- New HookRecoveryMarkerSchema (`{ eventId, hookId, runId }`) and
  HookRecoveryMarkerPath helper. The marker is a sidecar at
  hooks/tokens/<hash>.recovery.json, written via writeExclusive so
  the first cross-process retry pins its candidate eventId as
  canonical; subsequent retries read the marker and adopt that
  eventId. Together with the existing writeExclusive(eventPath) in
  the outer publish, this gives the legacy-fallback path the same
  single-event convergence guarantee as the inline-eventId fast
  path.

- pinCanonicalEventIdForLegacyClaim() encapsulates the marker
  write-or-read. A stale marker for a different (runId, hookId)
  (token-reuse with leaked state) is overwritten best-effort — the
  common cross-worker race for the same hook still converges; only
  the narrow stale-token-reuse case loses convergence.

- hook_disposed now also deletes the recovery marker when it
  deletes the token constraint file, preventing a future legacy
  recovery for a recycled token from latching onto a stale eventId.

- The dedup branch unified: existingClaim.eventId for new claims,
  pinCanonicalEventIdForLegacyClaim() for legacy ones. Removed the
  now-redundant findHookCreatedEvent helper — the
  writeExclusive(eventPath) in the outer publish is the
  authoritative duplicate-vs-orphan detector.

Tests:

- New test fixture test-fixtures/hook-race-worker.ts (TypeScript,
  run via child_process.fork with tsx as execPath — tsx is a
  transitive dev dep via vitest). Each subprocess gets its own
  createStorage(testDir) so the in-process hookLocks Map cannot
  serialize across workers.

- Replaced the tag-proxy test with
  "converges same-hook creation across separate OS processes to one
  event". Spawns workerCount subprocesses, releases them from a
  barrier into the same hook_created, asserts exactly one fulfilled
  + (N-1) rejected with EntityConflictError, and asserts directly
  on the raw events.list() result (no Map dedup) that the number of
  hook_created entries equals the number of logical creations.

- Added "converges same-hook creation across processes when only a
  legacy token claim exists". Same shape, but pre-seeds the legacy
  claim format (`{ token, hookId, runId }` with no eventId) before
  each race. Verified to FAIL on 7ce6655 (both subprocesses
  fulfill, no convergence) and pass on this commit.

- Also verified the new-eventId subprocess test FAILS when the
  event write is reverted to writeJSON (TOCTOU), confirming it
  exercises the writeExclusive-based cross-process arbitration.
  Both prior orphaned-claim / orphaned-entity recovery tests also
  continue to pass.
…obe, fix CI tsx resolution

Addresses three P1 review comments on PR #2295.

1. Stale recovery marker leaking across token-reuse lifetimes
   (pranaygp):

   The previous marker path used `hashToken(token)` so a stale
   marker for run A could leak into run B's recovery when the same
   token was reused after run A terminated through normal lifecycle.
   `deleteAllHooksForRun()` and tagged `world.clear()` deleted the
   token constraint and hook entity but NOT the marker sidecar, so
   the next legacy claim on the same token entered the stale-marker
   overwrite branch and the workers overwrote it non-atomically,
   yielding divergent publication.

   Fix:
   - Marker path now hashes `(token, runId, hookId)` together
     (`hookRecoveryMarkerPath` in storage/helpers.ts). Different
     lifetimes can never share a marker, so the stale-marker
     overwrite branch is removed entirely.
   - `hookRecoveryMarkerPath` is moved to helpers.ts and shared
     across events-storage.ts, hooks-storage.ts, and index.ts.
   - `deleteAllHooksForRun()` and tagged `world.clear()` now also
     delete the recovery marker for each hook (disk hygiene; per-
     lifetime identity makes leaks no longer corrupting).
   - `hook_disposed` now uses the new per-lifetime marker path too.

2. Duplicate `hook_created` event when a legacy claim's event was
   already published (VADE bot, also implied by pranaygp's analysis):

   Removing the event-log probe from the legacy fallback let a post-
   upgrade retry pin a new canonical eventId via the marker and
   publish a duplicate event at that path, even when the original
   pre-upgrade writer had already successfully published the event
   with its own (different) eventId.

   Fix:
   - Restore `findExistingHookCreatedEventId()` (renamed and made
     to return the eventId for clearer semantics).
   - Legacy fallback now probes the event log BEFORE pinning the
     marker; if a matching `hook_created` event already exists,
     throw `EntityConflictError` so the runtime's concurrent-replay
     catch path swallows the retry.
   - Inline-`eventId` fast path does NOT need the probe — the claim
     itself is the durable convergence key.

3. CI failure: tsx not resolvable under pnpm isolated linking
   (pranaygp; confirmed by ubuntu/windows unit test 60s timeouts):

   The previous test hard-coded `node_modules/.bin/tsx` assuming
   tsx would be hoisted there. But tsx was only a transitive peer
   dep via vitest, and pnpm's isolated linking does NOT link
   transitive peer deps into the workspace bin after a fresh
   install — so neither root nor package-local `.bin/tsx` existed
   in CI, the subprocess fork never started, and the barrier hung
   until vitest killed the test.

   Fix:
   - Add `tsx` as a direct `devDependency` of `@workflow/world-
     local` (pinned to 4.20.6 to match the existing transitive
     resolution).
   - Resolve via `import.meta.resolve('tsx/package.json')` and read
     the `bin` field dynamically, so we adapt to wherever pnpm
     links tsx for this package — not a hard-coded layout.
   - Lazy-init the resolver (no module-load IIFE) so an absent tsx
     fails only the convergence tests, not all 376 tests in the
     file.
   - Surface a clear error message if resolution fails, calling
     out the cause (transitive vs direct deps) for future readers.

   Also: harden the barrier helper so `error` events and pre-ready
   exits resolve BOTH `readyPromises` and `donePromises`, then
   `SIGKILL` siblings. Previously a broken child only resolved
   `donePromises`, leaving `Promise.all(readyPromises)` pending
   until the per-test timeout (60s in CI).

Regression tests added:

- `legacy claim whose hook_created event was already published does
  not append a duplicate event` — pre-seeds a legacy claim AND a
  pre-existing `hook_created` event with a different eventId,
  asserts the retry throws EntityConflictError and the log still
  has exactly the original event.

- `converges legacy claim recovery across run lifetimes after token
  reuse` — runs pranaygp's full lifecycle path: race subprocess
  workers on run A's legacy claim, terminate run A via
  `run_completed` (triggers `deleteAllHooksForRun`), reuse the
  token in a legacy claim for run B, race subprocess workers again,
  asserts exactly one fulfillment + one `EntityConflictError` per
  race and exactly one `hook_created` event per run.

Both new tests verified to fail on 2c673e4 (after rebuilding):
the published-event test throws via duplicate publish instead of
EntityConflictError, the token-reuse test sees both run B workers
fulfill (2 events instead of 1).

The existing orphaned-claim and orphaned-entity recovery tests also
continue to pass.

CI loop confirmed to be repaired locally by spawning subprocesses
via the new resolver and intentionally breaking the worker fixture
to verify the helper fails fast (~500ms) instead of hanging at the
barrier.
Addresses karthikscale3's P1 review comment on PR #2295.

The dedup-recovery path used to write the hook entity BEFORE the
outer event publish proved whether the attempt was repairing a
missing event or just colliding with an already-published
`hook_created`. For already-committed duplicates, the event write
then throws `EntityConflictError`, but the hook entity had
already been overwritten with the retry's payload — leaving the
durable hook entity and the event log inconsistent (e.g. the
entity reflects the retry's metadata while the event still
carries the original).

karthikscale3 reproduced this on the prior head by creating
`hook_created` with metadata `{ v: "a" }`, then retrying the
same `(runId, hookId, token)` with metadata `{ v: "b" }` and
`isWebhook: false`: the retry threw `EntityConflictError` but
`hooks.get()` returned the retry's payload.

Fix: defer the hook entity write until AFTER the outer
`writeExclusive(eventPath)` commits. The branch now only
captures the entity-to-write and its overwrite options; the
actual write happens immediately after the event publish in the
shared trailing block. A retry that ends in
`EntityConflictError` (the event was already published) now
leaves the entity untouched.

The first-writer happy path and all recovery paths (orphaned-
claim, orphaned-entity, cross-worker convergence, legacy claim,
token-reuse across lifetimes) are unaffected — they all reach
the event publish successfully, then the entity write runs as
before.

Regression test `does not mutate an already-committed hook
entity when a duplicate hook_created retry collides` added to
world-local: runs karthikscale3's exact scenario and asserts the
persisted entity still carries the original metadata and
isWebhook. Verified to fail on the prior commit (persisted
metadata = 0xbb instead of 0xaa) and pass on this commit after
rebuilding.

Parallel guard test `does not mutate an already-committed hook
entity when a duplicate hook_created retry collides` added to
world-postgres. Postgres already protected this via
`onConflictDoNothing()` on the hook INSERT, but the test guards
against a future regression that adds an UPDATE/UPSERT to the
dedup path.
@socket-security

socket-security Bot commented Jun 9, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
High CVE: npm @isaacs/brace-expansion has Uncontrolled Resource Consumption

CVE: GHSA-7h2j-956f-4vf2 @isaacs/brace-expansion has Uncontrolled Resource Consumption (HIGH)

Affected versions: < 5.0.1

Patched version: 5.0.1

From: pnpm-lock.yamlnpm/glob@11.1.0npm/@isaacs/brace-expansion@5.0.0

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@isaacs/brace-expansion@5.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern

CVE: GHSA-3ppc-4f35-3m26 minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern (HIGH)

Affected versions: >= 10.0.0 < 10.2.1; >= 9.0.0 < 9.0.6; >= 8.0.0 < 8.0.5; >= 7.0.0 < 7.4.7; >= 6.0.0 < 6.2.1; >= 5.0.0 < 5.1.7; >= 4.0.0 < 4.2.4; < 3.1.3

Patched version: 10.2.1

From: pnpm-lock.yamlnpm/glob@11.1.0npm/minimatch@10.1.1

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@10.1.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions

CVE: GHSA-23c5-xmqv-rm74 minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions (HIGH)

Affected versions: >= 10.0.0 < 10.2.3; >= 9.0.0 < 9.0.7; >= 8.0.0 < 8.0.6; >= 7.0.0 < 7.4.8; >= 6.0.0 < 6.2.2; >= 5.0.0 < 5.1.8; >= 4.0.0 < 4.2.5; < 3.1.4

Patched version: 10.2.3

From: pnpm-lock.yamlnpm/glob@11.1.0npm/minimatch@10.1.1

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@10.1.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments

CVE: GHSA-7r86-cg39-jmmj minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments (HIGH)

Affected versions: >= 10.0.0 < 10.2.3; >= 9.0.0 < 9.0.7; >= 8.0.0 < 8.0.6; >= 7.0.0 < 7.4.8; >= 6.0.0 < 6.2.2; >= 5.0.0 < 5.1.8; >= 4.0.0 < 4.2.5; < 3.1.3

Patched version: 10.2.3

From: pnpm-lock.yamlnpm/glob@11.1.0npm/minimatch@10.1.1

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@10.1.1. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions

CVE: GHSA-23c5-xmqv-rm74 minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions (HIGH)

Affected versions: >= 10.0.0 < 10.2.3; >= 9.0.0 < 9.0.7; >= 8.0.0 < 8.0.6; >= 7.0.0 < 7.4.8; >= 6.0.0 < 6.2.2; >= 5.0.0 < 5.1.8; >= 4.0.0 < 4.2.5; < 3.1.4

Patched version: 5.1.8

From: pnpm-lock.yamlnpm/@testcontainers/postgresql@11.12.0npm/@vercel/analytics@2.0.1npm/nitropack@2.13.2npm/@oclif/core@4.11.4npm/nuxt@4.4.7npm/genversion@3.2.0npm/minimatch@5.1.6

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@5.1.6. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern

CVE: GHSA-3ppc-4f35-3m26 minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern (HIGH)

Affected versions: >= 10.0.0 < 10.2.1; >= 9.0.0 < 9.0.6; >= 8.0.0 < 8.0.5; >= 7.0.0 < 7.4.7; >= 6.0.0 < 6.2.1; >= 5.0.0 < 5.1.7; >= 4.0.0 < 4.2.4; < 3.1.3

Patched version: 5.1.7

From: pnpm-lock.yamlnpm/@testcontainers/postgresql@11.12.0npm/@vercel/analytics@2.0.1npm/nitropack@2.13.2npm/@oclif/core@4.11.4npm/nuxt@4.4.7npm/genversion@3.2.0npm/minimatch@5.1.6

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@5.1.6. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments

CVE: GHSA-7r86-cg39-jmmj minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments (HIGH)

Affected versions: >= 10.0.0 < 10.2.3; >= 9.0.0 < 9.0.7; >= 8.0.0 < 8.0.6; >= 7.0.0 < 7.4.8; >= 6.0.0 < 6.2.2; >= 5.0.0 < 5.1.8; >= 4.0.0 < 4.2.5; < 3.1.3

Patched version: 5.1.8

From: pnpm-lock.yamlnpm/@testcontainers/postgresql@11.12.0npm/@vercel/analytics@2.0.1npm/nitropack@2.13.2npm/@oclif/core@4.11.4npm/nuxt@4.4.7npm/genversion@3.2.0npm/minimatch@5.1.6

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@5.1.6. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments

CVE: GHSA-7r86-cg39-jmmj minimatch has ReDoS: matchOne() combinatorial backtracking via multiple non-adjacent GLOBSTAR segments (HIGH)

Affected versions: >= 10.0.0 < 10.2.3; >= 9.0.0 < 9.0.7; >= 8.0.0 < 8.0.6; >= 7.0.0 < 7.4.8; >= 6.0.0 < 6.2.2; >= 5.0.0 < 5.1.8; >= 4.0.0 < 4.2.5; < 3.1.3

Patched version: 9.0.7

From: pnpm-lock.yamlnpm/@testcontainers/postgresql@11.12.0npm/@vercel/analytics@2.0.1npm/nitropack@2.13.2npm/@swc/cli@0.8.1npm/nuxt@4.4.7npm/@swc/cli@0.6.0npm/@astrojs/vercel@9.0.2npm/minimatch@9.0.5

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@9.0.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions

CVE: GHSA-23c5-xmqv-rm74 minimatch ReDoS: nested *() extglobs generate catastrophically backtracking regular expressions (HIGH)

Affected versions: >= 10.0.0 < 10.2.3; >= 9.0.0 < 9.0.7; >= 8.0.0 < 8.0.6; >= 7.0.0 < 7.4.8; >= 6.0.0 < 6.2.2; >= 5.0.0 < 5.1.8; >= 4.0.0 < 4.2.5; < 3.1.4

Patched version: 9.0.7

From: pnpm-lock.yamlnpm/@testcontainers/postgresql@11.12.0npm/@vercel/analytics@2.0.1npm/nitropack@2.13.2npm/@swc/cli@0.8.1npm/nuxt@4.4.7npm/@swc/cli@0.6.0npm/@astrojs/vercel@9.0.2npm/minimatch@9.0.5

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@9.0.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
High CVE: npm minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern

CVE: GHSA-3ppc-4f35-3m26 minimatch has a ReDoS via repeated wildcards with non-matching literal in pattern (HIGH)

Affected versions: >= 10.0.0 < 10.2.1; >= 9.0.0 < 9.0.6; >= 8.0.0 < 8.0.5; >= 7.0.0 < 7.4.7; >= 6.0.0 < 6.2.1; >= 5.0.0 < 5.1.7; >= 4.0.0 < 4.2.4; < 3.1.3

Patched version: 9.0.6

From: pnpm-lock.yamlnpm/@testcontainers/postgresql@11.12.0npm/@vercel/analytics@2.0.1npm/nitropack@2.13.2npm/@swc/cli@0.8.1npm/nuxt@4.4.7npm/@swc/cli@0.6.0npm/@astrojs/vercel@9.0.2npm/minimatch@9.0.5

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/minimatch@9.0.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Comment thread packages/world-local/src/storage/events-storage.ts
…cess test plumbing

You were right that the tsx subprocess machinery was overkill for a
storage-level convergence test. Replaced with a simple two-instance
in-process test that exercises the same cross-process semantics
without spawning anything.

The trick: `stepLocks` and `hookLocks` were module-level Maps shared
by all `createEventsStorage` calls in the same process. Move them
inside the function so each `createStorage(dir)` call gets its own
lock map. Two storage instances sharing one data directory then
behave exactly like two separate OS processes:

  - independent in-process `hookLocks` Maps (no in-process
    serialization between them), and
  - a shared filesystem (so the on-disk `writeExclusive` claim /
    marker / event publish primitives are the only thing arbitrating
    convergence).

This is also a real architectural improvement — the global lock map
was always a leaky abstraction that made unit-test simulation of
the cross-process path awkward.

Changes:

- `stepLocks` and `hookLocks` moved from module scope into
  `createEventsStorage`. `withStepLock` and `withHookLock` wrappers
  collapsed into direct `withInProcessLock(map, key, fn)` calls at
  the two call sites that need them.

- The three convergence regression tests in `storage.test.ts` now
  use `const workerA = createStorage(testDir); const workerB =
  createStorage(testDir);` and race `Promise.allSettled` of
  `events.create` from both — no subprocess, no IPC, no barrier
  helper, no `raceHookCreatedAcrossProcesses`. Same assertions
  (exactly one fulfillment + N-1 `EntityConflictError` per race,
  raw `events.list()` shows exactly one `hook_created` per
  logical creation — no Map dedup) so the regression catches are
  identical.

- Removed: `tsx` devDep, `test-fixtures/hook-race-worker.ts`,
  `HOOK_RACE_WORKER` / `resolveTsxLoaderUrl` / `TSX_BIN` /
  `raceHookCreatedAcrossProcesses` and the
  `fork`/`fileURLToPath` imports they pulled in.

Verified (after rebuilding world-local):

- All 379 tests pass on macOS in ~1s (was ~6.7s with subprocesses).
- Convergence tests confirmed to still catch the bugs: temporarily
  reverted the `eventId = canonicalEventId` adoption → both workers
  fulfilled (2 events instead of 1). Temporarily reverted the
  legacy-claim marker pin → same: both workers fulfilled.
- No subprocess machinery means no Windows-specific quirks
  (cli.mjs shebang, .cmd wrappers, .bin hoisting under pnpm
  isolated linking, etc.) that produced the Windows CI 60s
  timeouts.
- World-postgres still has its own parallel guard test for the
  karthikscale3 "no-mutate-on-duplicate" regression; that one
  exercises real DB concurrency and is unaffected by this change.

Full repo `pnpm test` (43 packages) and the
`parallelStepsThenWebhookWorkflow` e2e test against world-local
both green.
@TooTallNate

Copy link
Copy Markdown
Member Author

Follow-up on the Windows unit-test failures and the tsx complexity from the previous rounds.

@nrajlich called out (correctly) that introducing tsx as a devDep + a subprocess fixture + a barrier helper to run a TypeScript worker file felt like a lot of machinery for what is fundamentally a storage-level convergence test. The Windows CI failure was the symptom — child_process.fork with execPath pointing at tsx's dist/cli.mjs worked on Linux/macOS via the #!/usr/bin/env node shebang but Windows can't exec a .mjs directly, and pnpm's .bin wrappers don't compose with fork.

Rather than keep wrestling with platform-specific subprocess plumbing, fdf3be9 takes a different approach: move stepLocks and hookLocks from module scope into createEventsStorage so each createStorage(dir) call gets its own in-process lock map. Two storage instances sharing one data directory now behave exactly like two separate OS processes for the test's purposes — independent in-process locks, shared filesystem.

The three convergence tests now just do:

const workerA = createStorage(testDir);
const workerB = createStorage(testDir);
// ... seed legacy claim if needed ...
const results = await Promise.allSettled([
  workerA.events.create(runId, { eventType: 'hook_created', correlationId, eventData: { token } }),
  workerB.events.create(runId, { eventType: 'hook_created', correlationId, eventData: { token } }),
]);
// Assert exactly one fulfillment + one EntityConflictError, exactly one
// hook_created event in the raw event log.

Same assertions (no Map dedup; raw events.list() count must equal attempts) so the regression catches are identical. I verified by temporarily reverting the convergence machinery in events-storage.ts that both 'same-instance' and 'legacy-claim' tests still fail with two fulfillments instead of one.

Removed:

  • tsx devDep
  • test-fixtures/hook-race-worker.ts
  • fork/fileURLToPath imports
  • HOOK_RACE_WORKER / resolveTsxLoaderUrl / TSX_BIN constants
  • raceHookCreatedAcrossProcesses barrier helper

The module-level hookLocks was always a leaky abstraction that made cross-process simulation in tests awkward; this change is a real architectural simplification independent of the test concern.

Net: −184 lines, storage test file runs in ~1s (was ~6.7s with subprocesses), no Windows-specific quirks possible.

Full pnpm test (43 packages) green; e2e parallelStepsThenWebhookWorkflow green.

@socket-security

socket-security Bot commented Jun 9, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​swc/​cli@​0.6.0991007583100

View full report

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

Re-reviewed the latest head. The lock scoping, canonical event ID convergence, and duplicate-entity mutation fixes are good improvements, but two blocking correctness gaps remain; details are inline.

Comment thread packages/core/e2e/e2e.test.ts
…event; skip #1665 e2e on world-postgres

- A crash between the hook_created event publish and the deferred hook
  entity write left the event committed with the entity missing and
  unrepairable (retries threw EntityConflictError without materializing
  the entity). Retries now rebuild the entity from the PERSISTED event's
  payload — never the retry's eventData — via a race-safe writeExclusive,
  on both the canonical-eventId collision path and the legacy-claim
  probe path.
- Skip parallelStepsThenWebhookWorkflow e2e on world-postgres: the
  same-tick replay pattern surfaces a separate pre-existing step_started
  ordering bug there (#2331).

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

Requesting changes. The core idempotency fix is sound and each prior round of findings was genuinely addressed, but three things block approval on the current head:

  1. Event-first orphan is confirmed and unaddressed — see the open thread at events-storage.ts:1533 (#2295 (comment)). I re-verified on 99fd2e0: when writeExclusive(eventPath) returns false, the code throws EntityConflictError before the deferred hook-entity write ever runs, so a crash after event publish but before the entity write leaves the hook permanently unresolvable (HookNotFoundError on every hooks.get). The repair must reconstruct the hook from the persisted event payload, not the retry's.
  2. The PR's own #1665 regression fails on world-postgres — see the open thread on packages/core/e2e/e2e.test.ts (#2295 (comment)). Either fix the duplicate step_started path or scope this PR to #2283 and drop the "Closes #1665" claim.
  3. E2E Required Check is red on the head — including the sveltekit postgres failure above, plus nextjs-webpack (lazyDiscovery disabled) Local Prod/Postgres and sveltekit Vercel Prod. Even if some are flakes, the required check needs to be green.

Two inline notes below; one is a blocker (same bug as #1, different branch), one is non-blocking.

Comment thread packages/world-local/src/storage/events-storage.ts
Comment thread packages/world-local/src/storage/events-storage.ts

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

Approving — all three blockers from my previous review are resolved at 8b80f55d8:

  1. Event-first orphan fixed (fec2229): repairHookEntityFromPersistedEvent() rebuilds the missing hook entity from the persisted event's payload (never the retry's) via a race-safe no-overwrite writeExclusive, on both the canonical-eventId collision path and the legacy-claim probe path. Both regression tests use deliberately different retry metadata, so they also guard against reintroducing the earlier mutation bug.
  2. #1665 scope corrected: the e2e test is skipped on world-postgres (the skip's WORKFLOW_TARGET_WORLD detection matches what tests.yml sets), the separate step_started ordering bug is tracked in #2331, and the PR body no longer claims to close #1665.
  3. CI is green: E2E Required Check and unit tests on both platforms pass on the head.

The architectural follow-up (event log as single source of truth) is tracked in #2339. Nice work converging this.

@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically (backport job run).

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

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

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2374. Merge conflicts were resolved by AI — please review carefully. (backport job run)

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.

world-local can turn duplicate same-hook creation into self-conflict

5 participants