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

@TooTallNateTooTallNate 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 samehook_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

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

vercelBot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot 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
NameType
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/vitestPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

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

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

@github-actions

github-actionsBot commented 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

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.043s (+0.7%)1.008s (~)0.965s101.00x
💻 LocalExpress0.043s (+1.9%)1.005s (~)0.962s101.01x
💻 LocalNext.js (Turbopack)0.058s (-7.6% 🟢)1.006s (~)0.947s101.37x
🐘 PostgresExpress0.066s (+7.0% 🔺)1.014s (~)0.949s101.54x
🐘 PostgresNitro0.066s (~)1.013s (~)0.947s101.54x
🐘 PostgresNext.js (Turbopack)0.069s (-7.8% 🟢)1.012s (~)0.943s101.62x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.285s (+5.4% 🔺)2.536s (-1.7%)2.251s101.00x
▲ VercelNext.js (Turbopack)0.305s (-19.1% 🟢)2.374s (-2.3%)2.069s101.07x
▲ VercelNitro0.358s (-5.1% 🟢)2.742s (+30.6% 🔺)2.384s101.26x

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

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.093s (~)2.006s (~)0.914s101.00x
💻 LocalNitro1.097s (+0.5%)2.007s (~)0.910s101.00x
🐘 PostgresNitro1.109s (~)2.010s (~)0.901s101.01x
🐘 PostgresExpress1.119s (~)2.012s (~)0.893s101.02x
💻 LocalNext.js (Turbopack)1.131s (~)2.006s (~)0.876s101.03x
🐘 PostgresNext.js (Turbopack)1.147s (~)2.010s (~)0.863s101.05x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)1.723s (+2.7%)3.673s (+1.0%)1.950s101.00x
▲ VercelNitro1.871s (+11.6% 🔺)3.881s (+12.5% 🔺)2.011s101.09x
▲ VercelExpress1.910s (+8.9% 🔺)3.713s (~)1.803s101.11x

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

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.517s (~)11.021s (~)0.504s31.00x
💻 LocalExpress10.533s (~)11.022s (~)0.489s31.00x
🐘 PostgresNitro10.572s (~)11.019s (~)0.447s31.01x
🐘 PostgresExpress10.605s (~)11.020s (~)0.415s31.01x
🐘 PostgresNext.js (Turbopack)10.902s (-1.0%)11.347s (-2.9%)0.445s31.04x
💻 LocalNext.js (Turbopack)10.915s (+1.3%)11.356s (+3.0%)0.441s31.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)14.123s (~)16.212s (~)2.089s21.00x
▲ VercelNitro14.392s (+7.9% 🔺)16.613s (+9.8% 🔺)2.221s21.02x
▲ VercelExpress14.559s (+8.5% 🔺)16.528s (+5.0% 🔺)1.969s21.03x

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

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express13.705s (-1.0%)14.028s (~)0.322s51.00x
💻 LocalNitro13.776s (~)14.028s (-1.4%)0.251s51.01x
🐘 PostgresNitro13.849s (~)14.019s (~)0.170s51.01x
🐘 PostgresExpress13.877s (~)14.020s (~)0.143s51.01x
🐘 PostgresNext.js (Turbopack)14.384s (-0.8%)15.010s (~)0.626s41.05x
💻 LocalNext.js (Turbopack)14.626s (+1.0%)15.280s (+1.7%)0.653s41.07x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro21.790s (-2.8%)23.980s (~)2.190s31.00x
▲ VercelNext.js (Turbopack)22.120s (+2.4%)24.160s (+3.5%)2.040s31.02x
▲ VercelExpress22.137s (+3.1%)25.654s (+11.2% 🔺)3.516s31.02x

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

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express12.394s (-0.9%)13.025s (~)0.630s71.00x
💻 LocalNitro12.461s (~)13.024s (~)0.563s71.01x
🐘 PostgresExpress12.697s (~)13.165s (+1.1%)0.467s71.02x
🐘 PostgresNitro12.799s (+1.8%)13.305s (+2.2%)0.506s71.03x
💻 LocalNext.js (Turbopack)13.747s (+0.5%)14.027s (~)0.280s71.11x
🐘 PostgresNext.js (Turbopack)13.815s (-1.1%)14.021s (-2.0%)0.206s71.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.939s (+0.7%)34.401s (+3.3%)2.462s31.00x
▲ VercelNext.js (Turbopack)32.831s (+14.5% 🔺)34.982s (+15.2% 🔺)2.151s31.03x
▲ VercelExpress33.553s (+13.1% 🔺)36.201s (+16.2% 🔺)2.648s31.05x

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

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.181s (-3.4%)2.006s (~)0.825s151.00x
🐘 PostgresNitro1.189s (-1.2%)2.008s (~)0.819s151.01x
🐘 PostgresExpress1.197s (~)2.008s (~)0.811s151.01x
💻 LocalExpress1.253s (+5.3% 🔺)2.007s (~)0.754s151.06x
🐘 PostgresNext.js (Turbopack)1.269s (+0.8%)2.007s (~)0.738s151.07x
💻 LocalNext.js (Turbopack)1.370s (+1.6%)2.006s (~)0.636s151.16x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.730s (+7.0% 🔺)4.642s (+3.1%)1.912s71.00x
▲ VercelNitro3.363s (-9.1% 🟢)4.957s (-4.3%)1.594s71.23x
▲ VercelNext.js (Turbopack)3.592s (+18.0% 🔺)5.522s (+20.2% 🔺)1.930s71.32x

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

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.270s (+0.9%)2.007s (~)0.737s151.00x
🐘 PostgresExpress1.283s (+1.6%)2.007s (~)0.724s151.01x
🐘 PostgresNext.js (Turbopack)1.391s (-0.7%)2.008s (~)0.617s151.10x
💻 LocalNitro1.694s (~)2.006s (~)0.312s151.33x
💻 LocalExpress1.727s (-6.2% 🟢)2.006s (-6.7% 🟢)0.279s151.36x
💻 LocalNext.js (Turbopack)1.813s (-3.5%)2.222s (+3.3%)0.409s141.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.298s (-31.1% 🟢)6.102s (-21.2% 🟢)1.804s51.00x
▲ VercelExpress4.970s (+12.5% 🔺)6.675s (+9.1% 🔺)1.706s51.16x
▲ VercelNext.js (Turbopack)5.522s (-98.2% 🟢)7.164s (-97.7% 🟢)1.642s51.28x

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

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.409s (~)2.007s (~)0.598s151.00x
🐘 PostgresExpress1.415s (+1.0%)2.008s (~)0.592s151.00x
🐘 PostgresNext.js (Turbopack)1.708s (-5.3% 🟢)2.008s (-12.6% 🟢)0.300s151.21x
💻 LocalExpress4.539s (-15.7% 🟢)5.348s (-13.5% 🟢)0.809s63.22x
💻 LocalNext.js (Turbopack)4.539s (-20.4% 🟢)5.013s (-19.3% 🟢)0.474s63.22x
💻 LocalNitro4.554s (-13.2% 🟢)5.015s (-16.6% 🟢)0.460s63.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.863s (-65.5% 🟢)8.038s (-56.0% 🟢)2.175s41.00x
▲ VercelExpress6.717s (-26.4% 🟢)9.422s (-17.0% 🟢)2.705s41.15x
▲ VercelNext.js (Turbopack)7.032s (+16.9% 🔺)8.592s (+1.8%)1.559s41.20x

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

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.192s (~)2.009s (~)0.816s151.00x
🐘 PostgresExpress1.202s (-0.7%)2.009s (~)0.807s151.01x
🐘 PostgresNext.js (Turbopack)1.257s (-0.6%)2.007s (~)0.750s151.05x
💻 LocalNext.js (Turbopack)1.361s (+0.7%)2.006s (~)0.646s151.14x
💻 LocalExpress1.527s (-7.9% 🟢)2.007s (-3.3%)0.480s151.28x
💻 LocalNitro1.558s (+3.3%)2.007s (~)0.448s151.31x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.613s (-3.2%)4.149s (-12.0% 🟢)1.536s81.00x
▲ VercelNitro2.643s (+2.5%)4.289s (+1.3%)1.646s71.01x
▲ VercelNext.js (Turbopack)2.940s (-28.3% 🟢)4.511s (-20.0% 🟢)1.571s71.13x

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

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.264s (-2.5%)2.007s (~)0.744s151.00x
🐘 PostgresNitro1.272s (~)2.008s (~)0.736s151.01x
🐘 PostgresNext.js (Turbopack)1.420s (+1.1%)2.010s (~)0.589s151.12x
💻 LocalNitro1.919s (-11.6% 🟢)2.395s (-7.6% 🟢)0.476s131.52x
💻 LocalNext.js (Turbopack)1.966s (-3.9%)2.470s (-17.9% 🟢)0.504s131.56x
💻 LocalExpress1.967s (-8.9% 🟢)2.471s (-7.6% 🟢)0.504s131.56x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.684s (+7.5% 🔺)5.499s (+7.8% 🔺)1.815s61.00x
▲ VercelNitro4.191s (-13.2% 🟢)6.054s (-5.4% 🟢)1.864s51.14x
▲ VercelNext.js (Turbopack)4.432s (+21.6% 🔺)6.459s (+23.8% 🔺)2.027s51.20x

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

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.422s (+2.5%)2.008s (~)0.585s151.00x
🐘 PostgresExpress1.457s (+3.9%)2.008s (~)0.551s151.02x
🐘 PostgresNext.js (Turbopack)1.685s (-6.8% 🟢)2.316s (+4.1%)0.631s131.18x
💻 LocalNext.js (Turbopack)5.108s (-15.6% 🟢)5.679s (-16.7% 🟢)0.571s63.59x
💻 LocalNitro5.331s (-1.9%)5.848s (-2.7%)0.518s63.75x
💻 LocalExpress5.387s (-15.3% 🟢)6.014s (-14.3% 🟢)0.627s63.79x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.654s (-19.2% 🟢)7.277s (-15.4% 🟢)1.622s51.00x
▲ VercelExpress5.708s (+11.9% 🔺)7.379s (+1.9%)1.671s51.01x
▲ VercelNext.js (Turbopack)6.313s (-21.4% 🟢)8.096s (-17.3% 🟢)1.783s51.12x

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

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.595s (-0.9%)1.005s (~)0.410s601.00x
🐘 PostgresNitro0.610s (+5.3% 🔺)1.007s (~)0.397s601.03x
💻 LocalExpress0.613s (~)1.022s (+1.7%)0.409s591.03x
🐘 PostgresExpress0.626s (+4.3%)1.024s (+1.7%)0.398s591.05x
🐘 PostgresNext.js (Turbopack)0.832s (-2.6%)1.023s (~)0.191s591.40x
💻 LocalNext.js (Turbopack)0.897s (+4.0%)1.039s (+1.7%)0.142s581.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.562s (-35.3% 🟢)8.271s (-29.8% 🟢)1.709s81.00x
▲ VercelNitro6.835s (+37.8% 🔺)8.915s (+36.8% 🔺)2.080s71.04x
▲ VercelNext.js (Turbopack)7.354s (+40.8% 🔺)8.920s (+30.0% 🔺)1.566s71.12x

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

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.457s (+1.6%)2.008s (~)0.551s451.00x
💻 LocalExpress1.507s (-3.0%)2.006s (-1.1%)0.499s451.03x
💻 LocalNitro1.516s (+0.8%)2.006s (~)0.490s451.04x
🐘 PostgresNitro1.521s (+5.9% 🔺)2.102s (+3.5%)0.581s431.04x
🐘 PostgresNext.js (Turbopack)1.972s (-2.8%)2.228s (-16.1% 🟢)0.256s411.35x
💻 LocalNext.js (Turbopack)2.147s (+2.3%)3.008s (~)0.861s301.47x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express14.229s (-1.0%)16.298s (+0.9%)2.068s61.00x
▲ VercelNitro14.700s (+12.2% 🔺)16.963s (+17.3% 🔺)2.263s61.03x
▲ VercelNext.js (Turbopack)15.761s (+18.6% 🔺)17.780s (+17.8% 🔺)2.019s61.11x

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

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro2.805s (~)3.167s (~)0.362s381.00x
🐘 PostgresExpress2.899s (+3.2%)3.220s (+2.6%)0.321s381.03x
💻 LocalNitro3.309s (~)4.009s (~)0.700s301.18x
💻 LocalExpress3.364s (+2.4%)4.010s (~)0.645s301.20x
🐘 PostgresNext.js (Turbopack)3.854s (-3.3%)4.075s (-5.9% 🟢)0.221s301.37x
💻 LocalNext.js (Turbopack)4.335s (~)5.010s (~)0.676s241.55x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro34.281s (+17.7% 🔺)36.928s (+19.9% 🔺)2.648s41.00x
▲ VercelExpress35.352s (+34.7% 🔺)38.329s (+35.8% 🔺)2.976s41.03x
▲ VercelNext.js (Turbopack)37.624s (+5.3% 🔺)40.077s (+6.9% 🔺)2.453s31.10x

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

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.227s (+3.6%)1.006s (~)0.779s601.00x
🐘 PostgresExpress0.244s (+5.2% 🔺)1.006s (~)0.762s601.08x
🐘 PostgresNext.js (Turbopack)0.278s (~)1.006s (~)0.728s601.22x
💻 LocalExpress0.427s (+3.4%)1.005s (~)0.578s601.88x
💻 LocalNitro0.440s (+2.4%)1.005s (-1.7%)0.564s601.94x
💻 LocalNext.js (Turbopack)0.605s (+10.7% 🔺)1.040s (+3.5%)0.435s582.66x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.547s (-0.8%)4.231s (+3.0%)1.683s151.00x
▲ VercelNitro2.753s (-2.5%)4.727s (+9.5% 🔺)1.974s131.08x
▲ VercelNext.js (Turbopack)3.245s (-27.2% 🟢)5.075s (-18.5% 🟢)1.830s121.27x

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

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.361s (+0.7%)1.006s (~)0.645s901.00x
🐘 PostgresExpress0.371s (-0.8%)1.007s (~)0.636s901.03x
🐘 PostgresNext.js (Turbopack)0.474s (-5.1% 🟢)1.006s (~)0.533s901.31x
💻 LocalNitro2.202s (+1.0%)2.767s (+1.9%)0.565s336.09x
💻 LocalExpress2.268s (+11.3% 🔺)2.853s (+12.5% 🔺)0.585s326.28x
💻 LocalNext.js (Turbopack)2.507s (+2.3%)3.184s (+1.1%)0.676s296.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.998s (-35.2% 🟢)7.670s (-29.4% 🟢)1.672s121.00x
▲ VercelExpress6.064s (+12.0% 🔺)7.819s (+10.5% 🔺)1.754s121.01x
▲ VercelNext.js (Turbopack)6.587s (-14.5% 🟢)8.239s (-12.2% 🟢)1.652s121.10x

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

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.694s (~)1.006s (~)0.312s1201.00x
🐘 PostgresNitro0.705s (+3.5%)1.006s (~)0.301s1201.02x
🐘 PostgresNext.js (Turbopack)0.988s (-1.7%)1.567s (-15.5% 🟢)0.579s771.42x
💻 LocalNitro9.484s (+2.2%)10.109s (+3.2%)0.625s1213.66x
💻 LocalExpress9.722s (+4.4%)10.195s (+3.3%)0.473s1214.01x
💻 LocalNext.js (Turbopack)10.945s (~)11.847s (+3.2%)0.902s1115.77x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)16.330s (-26.2% 🟢)18.492s (-24.2% 🟢)2.162s71.00x
▲ VercelNitro16.648s (-13.6% 🟢)18.682s (-15.6% 🟢)2.033s71.02x
▲ VercelExpress18.002s (-4.8%)20.383s (-0.7%)2.381s61.10x

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

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express1.161s (-0.7%)2.005s (~)0.011s (-8.5% 🟢)2.018s (~)0.858s101.00x
💻 LocalNitro1.167s (~)2.004s (~)0.011s (+1.9%)2.018s (~)0.851s101.01x
🐘 PostgresNitro1.170s (~)1.999s (~)0.001s (+7.7% 🔺)2.011s (~)0.841s101.01x
🐘 PostgresExpress1.176s (~)1.999s (~)0.001s (+8.3% 🔺)2.011s (~)0.835s101.01x
💻 LocalNext.js (Turbopack)1.216s (~)2.004s (~)0.011s (-13.7% 🟢)2.018s (~)0.802s101.05x
🐘 PostgresNext.js (Turbopack)1.256s (+0.9%)2.002s (~)0.001s (-15.4% 🟢)2.012s (~)0.756s101.08x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.373s (+9.5% 🔺)3.463s (+8.1% 🔺)1.350s (+48.6% 🔺)5.282s (+15.3% 🔺)2.908s101.00x
▲ VercelNext.js (Turbopack)2.450s (+9.1% 🔺)3.729s (+15.4% 🔺)1.192s (+4.4%)5.403s (+10.7% 🔺)2.954s101.03x
▲ VercelNitro2.576s (+5.3% 🔺)3.724s (+10.7% 🔺)1.040s (+9.6% 🔺)5.263s (+10.4% 🔺)2.687s101.09x

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

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.586s (-0.5%)2.013s (~)0.012s (-7.2% 🟢)2.027s (~)0.441s301.00x
💻 LocalExpress1.589s (+1.1%)2.010s (~)0.013s (+9.4% 🔺)2.025s (~)0.436s301.00x
🐘 PostgresNitro1.601s (-0.7%)2.007s (~)0.005s (~)2.028s (~)0.426s301.01x
🐘 PostgresExpress1.615s (+0.9%)2.008s (~)0.005s (+0.6%)2.026s (~)0.411s301.02x
💻 LocalNext.js (Turbopack)1.717s (-1.3%)2.008s (~)0.013s (+5.7% 🔺)2.024s (~)0.307s301.08x
🐘 PostgresNext.js (Turbopack)1.739s (-2.6%)2.011s (~)0.005s (~)2.027s (~)0.288s301.10x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.482s (+9.5% 🔺)7.912s (+12.5% 🔺)0.502s (+7.0% 🔺)8.916s (+11.6% 🔺)2.433s71.00x
▲ VercelNext.js (Turbopack)6.581s (+2.1%)7.849s (+1.9%)0.166s (-70.4% 🟢)8.672s (-2.3%)2.091s71.02x
▲ VercelExpress6.688s (+10.1% 🔺)8.275s (+13.4% 🔺)0.241s (-8.3% 🟢)9.015s (+12.7% 🔺)2.327s71.03x

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

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.766s (+8.0% 🔺)1.031s (-1.2%)0.000s (-34.5% 🟢)1.042s (-2.0%)0.277s581.00x
🐘 PostgresExpress0.776s (+9.7% 🔺)1.087s (+7.2% 🔺)0.000s (+216.1% 🔺)1.105s (+7.5% 🔺)0.329s561.01x
🐘 PostgresNext.js (Turbopack)0.827s (-4.1%)1.052s (-3.6%)0.000s (-51.8% 🟢)1.060s (-3.6%)0.233s571.08x
💻 LocalExpress1.442s (+6.8% 🔺)2.014s (~)0.000s (+100.0% 🔺)2.017s (~)0.575s301.88x
💻 LocalNitro1.446s (~)2.014s (~)0.000s (+160.0% 🔺)2.017s (~)0.571s301.89x
💻 LocalNext.js (Turbopack)1.530s (+4.3%)2.014s (~)0.000s (-26.7% 🟢)2.018s (~)0.488s302.00x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.369s (~)4.811s (+5.2% 🔺)0.000s (-100.0% 🟢)5.323s (+5.6% 🔺)1.954s121.00x
▲ VercelNitro3.400s (-10.6% 🟢)4.979s (+3.1%)0.000s (NaN%)5.471s (+4.8%)2.071s121.01x
▲ VercelNext.js (Turbopack)3.720s (+0.6%)5.175s (+4.0%)0.000s (-100.0% 🟢)5.643s (+3.1%)1.922s111.10x

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

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.478s (+10.9% 🔺)2.140s (+5.6% 🔺)0.000s (NaN%)2.154s (+4.4%)0.676s281.00x
🐘 PostgresExpress1.517s (-0.6%)2.138s (-3.6%)0.000s (-100.0% 🟢)2.175s (-3.3%)0.658s281.03x
🐘 PostgresNext.js (Turbopack)1.696s (-2.0%)2.262s (~)0.000s (NaN%)2.269s (~)0.573s271.15x
💻 LocalNext.js (Turbopack)3.080s (+8.0% 🔺)3.545s (+3.8%)0.000s (+23.5% 🔺)3.561s (+4.2%)0.481s172.08x
💻 LocalExpress3.227s (+4.2%)3.966s (+8.1% 🔺)0.001s (+6.2% 🔺)3.969s (+7.9% 🔺)0.742s162.18x
💻 LocalNitro3.233s (+2.1%)4.028s (+5.0% 🔺)0.001s (+184.4% 🔺)4.031s (+4.9%)0.798s152.19x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Next.js (Turbopack)5.445s (-38.7% 🟢)6.743s (-43.4% 🟢)0.000s (-100.0% 🟢)7.225s (-42.2% 🟢)1.781s91.00x
▲ VercelNitro5.779s (-21.6% 🟢)7.212s (-9.1% 🟢)0.001s (+Infinity% 🔺)7.791s (-13.7% 🟢)2.012s81.06x
▲ VercelExpress125.180s (+2252.0% 🔺)126.485s (+1822.5% 🔺)0.000s (NaN%)126.947s (+1665.1% 🔺)1.767s122.99x

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

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro10/21
🐘 PostgresNitro16/21
▲ VercelExpress9/21
Fastest World by Framework

Winner determined by most benchmark wins

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

Worlds:

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

📋 View full workflow run

@github-actions

github-actionsBot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production132102191540
✅ 💻 Local Development174102191960
✅ 📦 Local Production174102191960
✅ 🐘 Local Postgres172702331960
✅ 🪟 Windows14000140
✅ 📋 Other8020178980
Total7472010688540

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro114026
✅ example114026
✅ express114026
✅ fastify114026
✅ hono114026
✅ nextjs-turbopack13802
✅ nextjs-webpack13802
✅ nitro114026
✅ nuxt114026
✅ sveltekit13307
✅ vite114026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable115025
✅ express-stable115025
✅ fastify-stable115025
✅ hono-stable115025
✅ nextjs-turbopack-canary121019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14000
✅ nextjs-turbopack-stable-lazy-discovery-enabled14000
✅ nextjs-webpack-canary121019
✅ nextjs-webpack-stable-lazy-discovery-disabled14000
✅ nextjs-webpack-stable-lazy-discovery-enabled14000
✅ nitro-stable115025
✅ nuxt-stable115025
✅ sveltekit-stable13406
✅ vite-stable115025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable115025
✅ express-stable115025
✅ fastify-stable115025
✅ hono-stable115025
✅ nextjs-turbopack-canary121019
✅ nextjs-turbopack-stable-lazy-discovery-disabled14000
✅ nextjs-turbopack-stable-lazy-discovery-enabled14000
✅ nextjs-webpack-canary121019
✅ nextjs-webpack-stable-lazy-discovery-disabled14000
✅ nextjs-webpack-stable-lazy-discovery-enabled14000
✅ nitro-stable115025
✅ nuxt-stable115025
✅ sveltekit-stable13406
✅ vite-stable115025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable114026
✅ express-stable114026
✅ fastify-stable114026
✅ hono-stable114026
✅ nextjs-turbopack-canary120020
✅ nextjs-turbopack-stable-lazy-discovery-disabled13901
✅ nextjs-turbopack-stable-lazy-discovery-enabled13901
✅ nextjs-webpack-canary120020
✅ nextjs-webpack-stable-lazy-discovery-disabled13901
✅ nextjs-webpack-stable-lazy-discovery-enabled13901
✅ nitro-stable114026
✅ nuxt-stable114026
✅ sveltekit-stable13307
✅ vite-stable114026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack14000
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable115025
✅ e2e-local-dev-tanstack-start-115025
✅ e2e-local-postgres-nest-stable114026
✅ e2e-local-postgres-tanstack-start-114026
✅ e2e-local-prod-nest-stable115025
✅ e2e-local-prod-tanstack-start-115025
✅ e2e-vercel-prod-tanstack-start114026

📋 View full workflow run

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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.

FileDescription
packages/world-local/src/storage/events-storage.tsUpdates hook token-claim parsing and makes duplicate same-hook hook_created idempotent by throwing EntityConflictError.
packages/world-local/src/storage.test.tsAdds regression tests ensuring same-hook duplicates don’t append hook_conflict, while real conflicts still do.
.changeset/fix-world-local-hook-self-conflict.mdAdds 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 threadpackages/world-local/src/storage/events-storage.ts Outdated
…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-securityBot 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.

ActionSeverityAlert (click "▶" to expand/collapse)
WarnHigh
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.

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

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

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

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

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

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

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

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

WarnHigh
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 threadpackages/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
MemberAuthor

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:

constworkerA=createStorage(testDir);constworkerB=createStorage(testDir);// ... seed legacy claim if needed ...constresults=awaitPromise.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-securityBot commented Jun 9, 2026

Copy link
Copy Markdown

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

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​swc/​cli@​0.6.0991007583100

View full report

@pranaygppranaygp 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 threadpackages/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).

@pranaygppranaygp 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 threadpackages/world-local/src/storage/events-storage.ts
Comment threadpackages/world-local/src/storage/events-storage.ts

@pranaygppranaygp 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-actionsgithub-actionsBot mentioned this pull request Jun 11, 2026
@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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

5 participants

@TooTallNate@pranaygp@karthikscale3@VaguelySerious