Skip to content

[core] Slot event identity + main (race-repro run) - #3249

Closed
VaguelySerious wants to merge 41 commits into
mainfrom
peter/slot-ids-race-repro
Closed

[core] Slot event identity + main (race-repro run)#3249
VaguelySerious wants to merge 41 commits into
mainfrom
peter/slot-ids-race-repro

Conversation

@VaguelySerious

Copy link
Copy Markdown
Member

Draft for CI only — combines the slot-identity stack (#3246 and its parents) with current main, which now includes #3241. Not for merge; #3246 stays the review PR.

Carries the event-log-race-repro label so the event-log race reproduction job runs against the combined tree.

Merge resolutions

Three conflicts, all in @workflow/core:

  • runtime/step-executor.tsmain's computeInstanceId stamp and the stack's eventCreateFence are both CreateEventParams fields, so startEventParams now carries the stamp plus a spread of the fence. Keeps both mechanisms; neither clobbers the other.
  • runtime/helpers.test.ts, workflow.ts — import unions.
  • runtime/step-executor.test.tsmain's compute-instance test passed stateUpdatedAt directly; the stack moved that under eventCreateFence, so the test now passes the fence.

@workflow/core typechecks clean and its suite is green (1688 passed, 3 expected fail).

Note: the stack still carries its temporary backend URL override commit; that is deliberate for this run.

🤖 Generated with Claude Code

VaguelySeriousand others added 6 commits July 30, 2026 14:15
… keys
Adds the client half of slot-based event identity, dormant until a run is
stamped at spec version 6.
- `@workflow/world` gains `slot-identity.ts` (26-char zero-padded decimal
bodies, so a slot is a valid ULID body everywhere a ULID is accepted),
`SPEC_VERSION_SLOT_IDENTITY` and `usesSlotIdentity()`.
- `nextCorrelationId(kind)` on the orchestrator context is now the only place
step and wait correlation ids are minted. In slot mode each kind counts
independently from 1; otherwise it delegates to the same seeded ULID
generator as before, so existing runs are byte-identical.
- Every queue idempotency key is scoped to the run. Queues dedupe per queue,
which is per workflow, so slot-numbered correlation ids would otherwise
collide across concurrent runs of one workflow — silently, because a
deduped send is answered normally and never dispatched. A source-scan test
asserts no site builds a key any other way.
- `packages/world` had test files but no `test` script, so 10 files never ran
in CI. Added one.
On a run that numbers its events by slot, the runtime names each event's
own id, which is its claim on that position in the log. The backend
inserts the id conditionally, so a 409 proves another writer got there
first and that this replay ran against a log missing at least one event.
Claims are reserved contiguously off the loaded log rather than all at
maxSlot + 1: a suspension flushes its operations concurrently, so without
reservation every operation in a flush would propose the same slot and
all but one would conflict, on every flush. Operations are built in
deterministic replay order, so each one's slot is replay-stable.
`withEventCreateFence` picks the run's fence: the event slot for a
slot-numbered run, the `stateUpdatedAt` watermark otherwise. The two
retry loops stay separate — they differ in what a rejection proves and in
what the client does about it, and both are live at once while runs on the
older numbering drain, which is what keeps 409s and 412s separately
countable during a rollout.
Creates that must not retry in place (`run_completed`, the inline
`step_started` claims) take a bare fence from `eventCreateFenceFor`, so a
rejection escapes to a fresh replay: merged events can change what the
workflow body decides, and only a replay from the top can act on them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Slot identity is only useful if a World can actually keep it, so both
first-party Worlds now allocate, honour, and defend dense per-run
positions:
- `SPEC_VERSION_MAX_SUPPORTED` separates the newest version a World can
read from the version it stamps, so turning the flag on somewhere does
not make the runs it creates unreadable elsewhere.
- `mintedSpecVersion()` gives both Worlds one place to opt new runs in.
- The Local World allocates under its storage lock and re-probes on a
lost exclusive write; the Postgres World makes the events primary key
run-scoped and treats a unique violation as "try the next free
position", re-probing every round so contention always makes progress.
- A caller-claimed position that is already taken is a 409 carrying the
events the caller was missing, and nothing is materialized for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…epro
# Conflicts:
#	packages/core/src/runtime/helpers.test.ts
#	packages/core/src/runtime/step-executor.ts
#	packages/core/src/workflow.ts
@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 61e7dc8

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

This PR includes changesets to release 21 packages
NameType
@workflow/coreMinor
workflowMinor
@workflow/world-localMinor
@workflow/worldMinor
@workflow/world-vercelMinor
@workflow/errorsMinor
@workflow/world-postgresMinor
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/world-testingPatch
@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

@VaguelySeriousVaguelySerious added the event-log-race-repro Run the event log race reproduction job label Jul 31, 2026
@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreviewJul 31, 2026 8:44pm
example-nextjs-workflow-webpackReadyReadyPreviewJul 31, 2026 8:44pm
example-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-astro-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-express-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-fastify-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-hono-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-nestjs-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-nitro-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-nuxt-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-sveltekit-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-tanstack-start-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workbench-vite-workflowReadyReadyPreviewJul 31, 2026 8:44pm
workflow-docsReadyReadyPreview, v0Jul 31, 2026 8:44pm
workflow-swc-playgroundReadyReadyPreviewJul 31, 2026 8:44pm
workflow-tarballsReadyReadyPreviewJul 31, 2026 8:44pm
workflow-webReadyReadyPreviewJul 31, 2026 8:44pm

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (1 failed)

express (1 failed):

  • fibonacciWorkflow - recursive workflow composition via start() | wrun_41KYWZ3C8B0GVKKC0S3JQEAAJ0 | 🔍 observability

E2E Test Summary

Summary
PassedFailedSkippedTotal
❌ ▲ Vercel Production145412391694
✅ 💻 Local Development162102271848
✅ 📦 Local Production162102271848
✅ 🐘 Local Postgres162102271848
✅ 🪟 Windows15400154
✅ 📋 Other102002121232
✅ vercel-multi-region270027
Total7518111328651
Details by Category

❌ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro126028
✅ example126028
❌ express125128
✅ fastify126028
✅ hono126028
✅ nextjs-turbopack15103
✅ nextjs-webpack15103
✅ nitro126028
✅ nuxt126028
✅ sveltekit14509
✅ vite126028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable128026
✅ express-stable128026
✅ fastify-stable128026
✅ hono-stable128026
✅ nextjs-turbopack-canary135019
✅ nextjs-turbopack-stable15400
✅ nextjs-webpack-canary135019
✅ nextjs-webpack-stable15400
✅ nitro-stable128026
✅ nuxt-stable128026
✅ sveltekit-stable14707
✅ vite-stable128026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128026
✅ e2e-local-dev-tanstack-start-128026
✅ e2e-local-postgres-nest-stable128026
✅ e2e-local-postgres-tanstack-start-128026
✅ e2e-local-prod-nest-stable128026
✅ e2e-local-prod-tanstack-start-128026
✅ e2e-vercel-prod-nest126028
✅ e2e-vercel-prod-tanstack-start126028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 61e7dc8 · Fri, 31 Jul 2026 21:07:03 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep247 (-65%) 💚643 🔴 (-42%) 💚838 🔴 (-38%) 💚2077 🔴 (+23%) 🔻30
TTFSstream247 (-7.8%)569 🔴 (-48%) 💚1025 🔴 (-9.1%)1448 🔴 (+23%) 🔻30
TTFShook + stream374 (-72%) 💚1044 🔴 (-33%) 💚1321 🔴 (-22%) 💚1853 🔴 (-52%) 💚30
STSO1020 steps (inline)188 (-10%)541 (-2.9%)612 (-3.8%)943 (-11%)1016
STSO1020 steps (queue-hop)1569 (-37%) 💚3995 (+13%)3995 (+12%)3995 (+12%)3
WO1020 steps465720 (-7.0%)465720 (-7.0%)465720 (-7.0%)465720 (-7.0%)1
SLstream latency167 (+45%) 🔻455 🔴 (+110%) 🔻685 🔴 (+174%) 🔻772 🔴 (+96%) 🔻30
SOstream overhead (text)260 (+54%) 🔻1517 🔴 (+399%) 🔻2331 🔴 (+510%) 🔻3423 🔴 (-96%) 💚30
SOstream overhead (structured)255 (+31%) 🔻1429 🔴 (+203%) 🔻2503 🔴 (+201%) 🔻3721 🔴 (-96%) 💚30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 486797ms → this run 457266ms (Δ -29531ms, -6%)

 150-200 ms ┃ main 0 this 5 +5
200-250 ms ████████░┃ main 45 this 60 +15
250-300 ms █████████████░░░░┃ main 80 this 106 +26
300-350 ms ████████████████░┃ main 93 this 107 +14
350-400 ms ████████████████████┃ main 117 this 128 +11
400-450 ms ████████████████████┃ main 124 this 124 +0
450-500 ms ████████████████████┃███ main 142 this 123 -19
500-550 ms █████████████████████┃██ main 144 this 130 -14
550-600 ms ██████████████████┃█ main 118 this 113 -5
600-650 ms ███████┃███ main 64 this 46 -18
650-700 ms ███┃███ main 43 this 24 -19
700-750 ms █░░┃ main 8 this 24 +16
750-800 ms ┃█ main 10 this 4 -6
800-850 ms ┃ main 2 this 6 +4
850-900 ms ┃ main 5 this 3 -2
900-950 ms ┃ main 5 this 3 -2
950-1000 ms ┃ main 2 this 3 +1
1000-1050 ms ┃ main 1 this 2 +1
1050-1100 ms ┃ main 3 this 0 -3
1150-1200 ms ┃ main 2 this 1 -1
1200-1250 ms ┃ main 1 this 0 -1
1250-1300 ms ┃ main 2 this 2 +0
1300-1350 ms ┃ main 0 this 1 +1
1350-1400 ms ┃ main 0 this 1 +1
2050-2100 ms ┃ main 1 this 0 -1
2350-2400 ms ┃ main 1 this 0 -1
3250-3300 ms ┃ main 1 this 0 -1
3450-3500 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 12497ms → this run 8151ms (Δ -4346ms, -35%)

1500-2000 ms ░░░░░░░░░░░┃ main 0 this 1 +1
2000-2500 ms ┃███████████ main 1 this 0 -1
2500-3000 ms ███████████┃ main 1 this 1 +0
3500-4000 ms ███████████┃████████████ main 2 this 1 -1
📜 Previous results (4)

bf289bb

Fri, 31 Jul 2026 20:23:03 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep995 (+42%) 🔻1276 🔴 (+14%)1316 🔴 (-3.1%)1379 🔴 (-18%) 💚30
TTFSstream1227 (+358%) 🔻1275 🔴 (+16%) 🔻1282 🔴 (+14%)1326 🔴 (+13%)30
TTFShook + stream506 (-63%) 💚1574 🔴 (+1.5%)1656 🔴 (-1.6%)1704 🔴 (-56%) 💚30
STSO1020 steps (inline)185 (-11%)462 (-17%) 💚530 (-17%) 💚678 (-36%) 💚1016
STSO1020 steps (queue-hop)2250 (-9.5%)3364 (-4.5%)3364 (-6.1%)3364 (-6.1%)3
WO1020 steps406411 (-19%) 💚406411 (-19%) 💚406411 (-19%) 💚406411 (-19%) 💚1
SLstream latency97 (-16%) 💚140 🔴 (-35%) 💚155 🔴 (-38%) 💚251 🔴 (-36%) 💚30
SOstream overhead (text)99 (-41%) 💚158 (-48%) 💚186 (-51%) 💚3313 🔴 (-96%) 💚30
SOstream overhead (structured)103 (-47%) 💚155 (-67%) 💚170 (-80%) 💚497 (-99%) 💚30

b67828c

Fri, 31 Jul 2026 18:43:28 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1298 (+394%) 🔻1411 🔴 (-1.7%)1514 🔴 (-5.8%)1877 🔴 (-6.5%)30
TTFSstream1300 (+426%) 🔻1432 🔴 (+2.1%)1463 🔴 (-7.0%)1622 🔴 (-21%) 💚30
TTFShook + stream1245 (+210%) 🔻1644 🔴 (+27%) 🔻1696 🔴 (+25%) 🔻1782 🔴 (+22%) 🔻30
STSO1020 steps (inline)157 (-11%)490 (-4.7%)547 (-5.4%)830 (+6.3%)1016
STSO1020 steps (queue-hop)2406 (+62%) 🔻3291 (+18%) 🔻3291 (+18%) 🔻3291 (+18%) 🔻3
WO1020 steps426331 (-1.3%)426331 (-1.3%)426331 (-1.3%)426331 (-1.3%)1
SLstream latency99 (+13%)142 🔴 (-10%)163 🔴 (-5.2%)246 🔴 (+25%) 🔻30
SOstream overhead (text)107 (-24%) 💚168 (-21%) 💚192 (-15%) 💚3402 🔴 (+587%) 🔻30
SOstream overhead (structured)113 (-16%) 💚161 (-41%) 💚172 (-51%) 💚200 (-58%) 💚30

6b2ccdb

Fri, 31 Jul 2026 14:25:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep229 (-77%) 💚920 🔴 (-16%) 💚1318 🔴 (+20%) 🔻1627 🔴 (+1.4%)30
TTFSstream216 (-41%) 💚577 🔴 (-45%) 💚627 🔴 (-42%) 💚1479 🔴 (+33%) 🔻30
TTFShook + stream360 (-28%) 💚1070 🔴 (-24%) 💚1568 🔴 (+6.8%)1641 🔴 (+9.6%)30
STSO1020 steps (inline)166 (-9.8%)521 (+6.1%)580 (+3.9%)761 (+0.7%)1016
STSO1020 steps (queue-hop)1512 (-26%) 💚2436 (-13%)2436 (-13%)2436 (-13%)3
WO1020 steps440529 (+1.9%)440529 (+1.9%)440529 (+1.9%)440529 (+1.9%)1
SLstream latency119 (+38%) 🔻399 🔴 (+173%) 🔻546 🔴 (+197%) 🔻803 🔴 (+215%) 🔻30
SOstream overhead (text)229 (+68%) 🔻663 🔴 (+31%) 🔻1186 🔴 (+26%) 🔻3671 🔴 (+205%) 🔻30
SOstream overhead (structured)189 (+69%) 🔻769 🔴 (+277%) 🔻1527 🔴 (+483%) 🔻2689 🔴 (+507%) 🔻30

48084a7

Fri, 31 Jul 2026 13:27:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1348 (+34%) 🔻1417 🔴 (+30%) 🔻1448 🔴 (+32%) 🔻1556 🔴 (-3.1%)30
TTFSstream1316 (+257%) 🔻1410 🔴 (+33%) 🔻1433 🔴 (+32%) 🔻1465 🔴 (+32%) 🔻30
TTFShook + stream697 (+39%) 🔻1663 🔴 (+19%) 🔻1701 🔴 (+16%) 🔻2041 🔴 (+36%) 🔻30
STSO1020 steps (inline)168 (-8.7%)483 (-1.6%)541 (-3.0%)734 (-2.9%)1016
STSO1020 steps (queue-hop)2496 (+21%) 🔻3474 (+25%) 🔻3474 (+25%) 🔻3474 (+25%) 🔻3
WO1020 steps414514 (-4.1%)414514 (-4.1%)414514 (-4.1%)414514 (-4.1%)1
SLstream latency89 (+3.5%)179 🔴 (+23%) 🔻199 🔴 (+8.2%)354 🔴 (+39%) 🔻30
SOstream overhead (text)89 (-35%) 💚138 (-73%) 💚159 (-83%) 💚262 (-78%) 💚30
SOstream overhead (structured)91 (-19%) 💚160 (-22%) 💚225 (-14%)305 (-31%) 💚30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the Best column shows the fastest (warm-start) sample for comparison.

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Event Log Race Repro

168 of 1400 latest repro runs hit event-log regressions.

Run History

Metric2026-07-31 03:32 UTC #1
logs / deploy
2026-07-31 06:08 UTC #1
logs / deploy
2026-07-31 08:35 UTC #1
logs / deploy
2026-07-31 10:37 UTC #1
logs / deploy
2026-07-31 11:09 UTC #1
logs / deploy
2026-07-31 12:40 UTC #1
logs / deploy
2026-07-31 13:37 UTC #1
logs / deploy
2026-07-31 13:59 UTC #1
logs / deploy
2026-07-31 14:00 UTC #1
logs
2026-07-31 14:34 UTC #1
logs / deploy
2026-07-31 19:12 UTC #1
logs / deploy
2026-07-31 20:36 UTC #1
logs / deploy
2026-07-31 20:40 UTC #1
logs / deploy
2026-07-31 21:39 UTC #1
logs / deploy
Result1152/1400 regressions1088/1088 regressions — partial (1088 of 1400 planned)1178/1400 regressions703/722 regressions — partial (722 of 1400 planned)1165/1400 regressions1122/1400 regressions (+54 infra)1158/1400 regressions522/690 regressions — partial (690 of 1400 planned)missing result file1040/1400 regressions157/1400 regressions174/843 regressions (+11 infra) — partial (843 of 1400 planned)2/2 regressions — partial (2 of 1400 planned)168/1400 regressions
Total14001088140072214001400140069001400140084321400
completed2480222192352242421680360124365801232
CORRUPTED_EVENT_LOG11494761178703116311211158522010301571742148
USER_ERROR00000000000000
RUNTIME_ERROR00000000000000
stuck361200210001000020
other00000000000000
infra0000054000001100
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81088 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8722 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8690 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8843 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x82 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x81400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000mswatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm60045114700200
hook-storm6005811001800
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6A90GG6X9BJPN6KF1CB
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6CR0GM02NE7BVVGQ5Y7
step-storm39CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6Q70GM2W1HFK5JHXMP1
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6C90GYWR9EJA77P3C89
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM65E0GP1QKSFFBYEEX26
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6580GNHB59XAF5AKECF
step-storm36CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6MX0GT3B1AFCM5RNK06
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6F40GX4HE4PMJ8HFV3H
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM66E0GWTGQTERPPJZ7KP
step-storm33CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6MY0GYJ8RCYQD800NCF
step-storm32CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYM6MT0GR159RNC8H16F5M
step-storm49CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYNXMZ0GJ3F0697D1W52CT
step-storm46CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYNV3C0GG7P005HH5MHG37
step-storm43CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYN0Y50GHAET1J5PAXHSY0
step-storm60CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYPMKJ0GZZ84AM2JJYZ16F
step-storm62CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYPMVV0GK8RC06EV9ZNVSR
step-storm48CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYNWZW0GSGXE8JBSBZG1F0
step-storm68CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYPPMW0GQYCVVBEDPNSXBE
step-storm67CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYPP7J0GT372S5N12KHY7V
step-storm78CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYWYPV940GP5WW1GRYQRMQMG

Showing 20 of 168 non-completed runs.

…s its start
The synthetic `step_created` a lazy step start materializes was written with a
plain create-if-absent write, so losing its position surfaced as a duplicate-
entity error - which the runtime reads as "another handler owns this step" and
skips, leaving the step claimed but never run. It now takes the position the
same way every other event does, and a loss is reported as a slot conflict the
caller can merge and re-propose.
…s its start
The synthetic `step_created` a lazy step start materializes was written with a
plain create-if-absent write, so losing its position surfaced as a duplicate-
entity error - which the runtime reads as "another handler owns this step" and
skips, leaving the step claimed but never run. It now takes the position the
same way every other event does, and a loss is reported as a slot conflict the
caller can merge and re-propose.
VaguelySeriousand others added 2 commits July 31, 2026 05:10
An optimistic start runs against a locally synthesized run row, so the
workflow start time this invocation reports comes from the client clock
while every later replay reads the persisted run. Sending that instant as
the run_started event's occurredAt lets a backend record it as the run's
startedAt, so the two agree instead of differing by the round-trip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An optimistic start runs against a locally synthesized run row, so the
workflow start time this invocation reports comes from the client clock
while every later replay reads the persisted run. Sending that instant as
the run_started event's occurredAt lets a backend record it as the run's
startedAt, so the two agree instead of differing by the round-trip.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment threadpackages/world-local/src/storage/events-storage.ts
VaguelySeriousand others added 17 commits July 31, 2026 06:02
…e waited for
Three diagnostic gaps that together made replay divergence unreadable:
- `composeLogLine` dropped `errorMessage` whenever the message did not already
contain it, so a warn carrying an error alongside its own summary line logged
the symptom and none of the diagnosis.
- An unconsumable event named only itself. It is almost always an event whose
entity this replay never issued, so the pending invocation queue is what
distinguishes "never issued" from "issued under another id".
- An inline step batch abandoned on a fenced claim logged neither which member
was fenced nor how the others settled. The fence is per-write, so a batch can
split: the rejected claim writes nothing while a sibling on a different slot
commits.
Also read a failed run's error through `returnValue()` in the race-repro
harness — `runs.get` returns the raw serialized payload, so every corruption in
the report carried a code and no message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…atch
An inline step's step_started claim is fenced per-write under slot identity, so
a 409 only proves another writer took that write's number — routinely true,
since the backend allocates outside events from the same next-free pointer the
client reserves from. Abandoning the whole batch on it left the loser's events
landing seconds later, after a whole later phase, in an order no single replay
could consume.
stepClaimFence keeps a watermark-guarded run on its single shared fence (a 412
does mean the view is stale, and the batch is meant to fail as a unit) and
gives a slot-numbered run an in-place reclaim: merge the delta, reserve past
it, re-claim.
The reservation pointer is now absolute and only moves forward, so a merge
cannot hand the retrying writer a slot a sibling is still in flight on.
…rt time
`createdAt` is stamped when a write begins, before its final slot is known, so
it disagrees with slot order in two ways: a writer that loses a slot re-proposes
above the winner while keeping its earlier stamp, and a caller that reserves a
range of slots for one flush commits them in whatever order the network returns.
Replay consumes the log in list order and never sorts, so listing by `createdAt`
hands it an order no execution produced.
Slot events now report one shared order time and let the existing event-id
tie-break do the ordering, matching the Postgres World's `orderBy(eventId)` and
the Vercel World's sort key. ULID runs keep their wall-clock order, which their
ids agree with anyway.
…rt time
`createdAt` is stamped when a write begins, before its final slot is known, so
it disagrees with slot order in two ways: a writer that loses a slot re-proposes
above the winner while keeping its earlier stamp, and a caller that reserves a
range of slots for one flush commits them in whatever order the network returns.
Replay consumes the log in list order and never sorts, so listing by `createdAt`
hands it an order no execution produced.
Slot events now report one shared order time and let the existing event-id
tie-break do the ordering, matching the Postgres World's `orderBy(eventId)` and
the Vercel World's sort key. ULID runs keep their wall-clock order, which their
ids agree with anyway.
Two independent sources of `CorruptedEventLogError` on well-formed event
logs, both found by dumping the logs of runs that failed that way.
A hook delivery is ordered by when its event row commits, not by when the
payload arrived, so a delivery that races a disposal — arriving first,
committing second — lands after its own `hook_disposed` in the log. The
hook's consumer retired on the disposal, so nothing consumed that event on
any replay: a divergence that recurs identically every attempt and escalates
to a terminal error. The consumer now stays registered as a tombstone and
discards the late delivery, which is what `disposeHook` already assumed when
it settled every awaiter.
Separately, the unconsumed-event check gave the VM a flat 100ms of wall clock
to register the next event's consumer. Real logs routinely need more: a hook
payload fanning out into steps measures 254-717ms between the delivery and
the first `step_created` it causes. The check now re-arms while a delivery is
still in flight — the condition `scheduleWhenIdle` already polls — bounded by
`WORKFLOW_DEFERRED_CHECK_MAX_GRACE_MS` so an abandoned delivery cannot park
it forever.
Two independent sources of `CorruptedEventLogError` on well-formed event
logs, both found by dumping the logs of runs that failed that way.
A hook delivery is ordered by when its event row commits, not by when the
payload arrived, so a delivery that races a disposal — arriving first,
committing second — lands after its own `hook_disposed` in the log. The
hook's consumer retired on the disposal, so nothing consumed that event on
any replay: a divergence that recurs identically every attempt and escalates
to a terminal error. The consumer now stays registered as a tombstone and
discards the late delivery, which is what `disposeHook` already assumed when
it settled every awaiter.
Separately, the unconsumed-event check gave the VM a flat 100ms of wall clock
to register the next event's consumer. Real logs routinely need more: a hook
payload fanning out into steps measures 254-717ms between the delivery and
the first `step_created` it causes. The check now re-arms while a delivery is
still in flight — the condition `scheduleWhenIdle` already polls — bounded by
`WORKFLOW_DEFERRED_CHECK_MAX_GRACE_MS` so an abandoned delivery cannot park
it forever.
Contiguous allocation is not the same thing as a gap-free published log: a
slot claimed by an operation that then fails for a reason of its own is never
filled, and once a later slot is published that gap is permanent. What the
scheme actually buys is explicit contention and a log that reads in write
order. Nothing consumed the proof, so this is a comment and docs correction.
…e waited for
Three diagnostic gaps that together made replay divergence unreadable:
- `composeLogLine` dropped `errorMessage` whenever the message did not already
contain it, so a warn carrying an error alongside its own summary line logged
the symptom and none of the diagnosis.
- An unconsumable event named only itself. It is almost always an event whose
entity this replay never issued, so the pending invocation queue is what
distinguishes "never issued" from "issued under another id".
- An inline step batch abandoned on a fenced claim logged neither which member
was fenced nor how the others settled. The fence is per-write, so a batch can
split: the rejected claim writes nothing while a sibling on a different slot
commits.
Also read a failed run's error through `returnValue()` in the race-repro
harness — `runs.get` returns the raw serialized payload, so every corruption in
the report carried a code and no message.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…atch
An inline step's step_started claim is fenced per-write under slot identity, so
a 409 only proves another writer took that write's number — routinely true,
since the backend allocates outside events from the same next-free pointer the
client reserves from. Abandoning the whole batch on it left the loser's events
landing seconds later, after a whole later phase, in an order no single replay
could consume.
stepClaimFence keeps a watermark-guarded run on its single shared fence (a 412
does mean the view is stale, and the batch is meant to fail as a unit) and
gives a slot-numbered run an in-place reclaim: merge the delta, reserve past
it, re-claim.
The reservation pointer is now absolute and only moves forward, so a merge
cannot hand the retrying writer a slot a sibling is still in flight on.
A slot names a position in the replay order, so allocation has to hand out a
position no published event sits above. Handing out the lowest free position
instead let a late `step_completed` drop into a hole beneath its own
`step_created`/`step_started`, and every replay of that run then met a
completion for a step it had not started.
The book now keeps a monotonic ceiling: a reservation goes above every position
the book has ever seen, and releasing one does not lower it. `run_created` takes
the first slot outright rather than allocating it, since a `run_started` racing
it can already have moved the book past that position.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	docs/content/docs/v5/configuration/runtime-tuning.mdx
#	packages/world-local/src/storage/events-storage.ts
#	packages/world-local/src/storage/slot-identity.test.ts
#	packages/world-local/src/storage/slots.test.ts
#	packages/world-local/src/storage/slots.ts
#	packages/world-postgres/src/slots.ts
#	packages/world-postgres/test/slot-identity.test.ts
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

(AI) Superseded by #3305, which carries the whole slot-identity stack merged with current main. Closing this CI-only draft.

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.

1 participant

@VaguelySerious