Skip to content

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

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

[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

VaguelySerious and 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-bot Bot 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
Name Type
@workflow/core Minor
workflow Minor
@workflow/world-local Minor
@workflow/world Minor
@workflow/world-vercel Minor
@workflow/errors Minor
@workflow/world-postgres Minor
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
@workflow/world-testing Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch

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

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

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

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Jul 31, 2026 8:44pm
example-nextjs-workflow-webpack Ready Ready Preview Jul 31, 2026 8:44pm
example-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-astro-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-express-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-fastify-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-hono-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-nestjs-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-nitro-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-nuxt-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-sveltekit-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-tanstack-start-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workbench-vite-workflow Ready Ready Preview Jul 31, 2026 8:44pm
workflow-docs Ready Ready Preview, v0 Jul 31, 2026 8:44pm
workflow-swc-playground Ready Ready Preview Jul 31, 2026 8:44pm
workflow-tarballs Ready Ready Preview Jul 31, 2026 8:44pm
workflow-web Ready Ready Preview Jul 31, 2026 8:44pm

@github-actions

github-actions Bot 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
Passed Failed Skipped Total
❌ ▲ Vercel Production 1454 1 239 1694
✅ 💻 Local Development 1621 0 227 1848
✅ 📦 Local Production 1621 0 227 1848
✅ 🐘 Local Postgres 1621 0 227 1848
✅ 🪟 Windows 154 0 0 154
✅ 📋 Other 1020 0 212 1232
✅ vercel-multi-region 27 0 0 27
Total 7518 1 1132 8651
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
✅ astro 126 0 28
✅ example 126 0 28
❌ express 125 1 28
✅ fastify 126 0 28
✅ hono 126 0 28
✅ nextjs-turbopack 151 0 3
✅ nextjs-webpack 151 0 3
✅ nitro 126 0 28
✅ nuxt 126 0 28
✅ sveltekit 145 0 9
✅ vite 126 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable 128 0 26
✅ express-stable 128 0 26
✅ fastify-stable 128 0 26
✅ hono-stable 128 0 26
✅ nextjs-turbopack-canary 135 0 19
✅ nextjs-turbopack-stable 154 0 0
✅ nextjs-webpack-canary 135 0 19
✅ nextjs-webpack-stable 154 0 0
✅ nitro-stable 128 0 26
✅ nuxt-stable 128 0 26
✅ sveltekit-stable 147 0 7
✅ vite-stable 128 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack 154 0 0

✅ 📋 Other

App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 128 0 26
✅ e2e-local-dev-tanstack-start- 128 0 26
✅ e2e-local-postgres-nest-stable 128 0 26
✅ e2e-local-postgres-tanstack-start- 128 0 26
✅ e2e-local-prod-nest-stable 128 0 26
✅ e2e-local-prod-tanstack-start- 128 0 26
✅ e2e-vercel-prod-nest 126 0 28
✅ e2e-vercel-prod-tanstack-start 126 0 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@github-actions

github-actions Bot 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

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 247 (-65%) 💚 643 🔴 (-42%) 💚 838 🔴 (-38%) 💚 2077 🔴 (+23%) 🔻 30
TTFS stream 247 (-7.8%) 569 🔴 (-48%) 💚 1025 🔴 (-9.1%) 1448 🔴 (+23%) 🔻 30
TTFS hook + stream 374 (-72%) 💚 1044 🔴 (-33%) 💚 1321 🔴 (-22%) 💚 1853 🔴 (-52%) 💚 30
STSO 1020 steps (inline) 188 (-10%) 541 (-2.9%) 612 (-3.8%) 943 (-11%) 1016
STSO 1020 steps (queue-hop) 1569 (-37%) 💚 3995 (+13%) 3995 (+12%) 3995 (+12%) 3
WO 1020 steps 465720 (-7.0%) 465720 (-7.0%) 465720 (-7.0%) 465720 (-7.0%) 1
SL stream latency 167 (+45%) 🔻 455 🔴 (+110%) 🔻 685 🔴 (+174%) 🔻 772 🔴 (+96%) 🔻 30
SO stream overhead (text) 260 (+54%) 🔻 1517 🔴 (+399%) 🔻 2331 🔴 (+510%) 🔻 3423 🔴 (-96%) 💚 30
SO stream 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

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 995 (+42%) 🔻 1276 🔴 (+14%) 1316 🔴 (-3.1%) 1379 🔴 (-18%) 💚 30
TTFS stream 1227 (+358%) 🔻 1275 🔴 (+16%) 🔻 1282 🔴 (+14%) 1326 🔴 (+13%) 30
TTFS hook + stream 506 (-63%) 💚 1574 🔴 (+1.5%) 1656 🔴 (-1.6%) 1704 🔴 (-56%) 💚 30
STSO 1020 steps (inline) 185 (-11%) 462 (-17%) 💚 530 (-17%) 💚 678 (-36%) 💚 1016
STSO 1020 steps (queue-hop) 2250 (-9.5%) 3364 (-4.5%) 3364 (-6.1%) 3364 (-6.1%) 3
WO 1020 steps 406411 (-19%) 💚 406411 (-19%) 💚 406411 (-19%) 💚 406411 (-19%) 💚 1
SL stream latency 97 (-16%) 💚 140 🔴 (-35%) 💚 155 🔴 (-38%) 💚 251 🔴 (-36%) 💚 30
SO stream overhead (text) 99 (-41%) 💚 158 (-48%) 💚 186 (-51%) 💚 3313 🔴 (-96%) 💚 30
SO stream 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

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1298 (+394%) 🔻 1411 🔴 (-1.7%) 1514 🔴 (-5.8%) 1877 🔴 (-6.5%) 30
TTFS stream 1300 (+426%) 🔻 1432 🔴 (+2.1%) 1463 🔴 (-7.0%) 1622 🔴 (-21%) 💚 30
TTFS hook + stream 1245 (+210%) 🔻 1644 🔴 (+27%) 🔻 1696 🔴 (+25%) 🔻 1782 🔴 (+22%) 🔻 30
STSO 1020 steps (inline) 157 (-11%) 490 (-4.7%) 547 (-5.4%) 830 (+6.3%) 1016
STSO 1020 steps (queue-hop) 2406 (+62%) 🔻 3291 (+18%) 🔻 3291 (+18%) 🔻 3291 (+18%) 🔻 3
WO 1020 steps 426331 (-1.3%) 426331 (-1.3%) 426331 (-1.3%) 426331 (-1.3%) 1
SL stream latency 99 (+13%) 142 🔴 (-10%) 163 🔴 (-5.2%) 246 🔴 (+25%) 🔻 30
SO stream overhead (text) 107 (-24%) 💚 168 (-21%) 💚 192 (-15%) 💚 3402 🔴 (+587%) 🔻 30
SO stream 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

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 229 (-77%) 💚 920 🔴 (-16%) 💚 1318 🔴 (+20%) 🔻 1627 🔴 (+1.4%) 30
TTFS stream 216 (-41%) 💚 577 🔴 (-45%) 💚 627 🔴 (-42%) 💚 1479 🔴 (+33%) 🔻 30
TTFS hook + stream 360 (-28%) 💚 1070 🔴 (-24%) 💚 1568 🔴 (+6.8%) 1641 🔴 (+9.6%) 30
STSO 1020 steps (inline) 166 (-9.8%) 521 (+6.1%) 580 (+3.9%) 761 (+0.7%) 1016
STSO 1020 steps (queue-hop) 1512 (-26%) 💚 2436 (-13%) 2436 (-13%) 2436 (-13%) 3
WO 1020 steps 440529 (+1.9%) 440529 (+1.9%) 440529 (+1.9%) 440529 (+1.9%) 1
SL stream latency 119 (+38%) 🔻 399 🔴 (+173%) 🔻 546 🔴 (+197%) 🔻 803 🔴 (+215%) 🔻 30
SO stream overhead (text) 229 (+68%) 🔻 663 🔴 (+31%) 🔻 1186 🔴 (+26%) 🔻 3671 🔴 (+205%) 🔻 30
SO stream 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

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1348 (+34%) 🔻 1417 🔴 (+30%) 🔻 1448 🔴 (+32%) 🔻 1556 🔴 (-3.1%) 30
TTFS stream 1316 (+257%) 🔻 1410 🔴 (+33%) 🔻 1433 🔴 (+32%) 🔻 1465 🔴 (+32%) 🔻 30
TTFS hook + stream 697 (+39%) 🔻 1663 🔴 (+19%) 🔻 1701 🔴 (+16%) 🔻 2041 🔴 (+36%) 🔻 30
STSO 1020 steps (inline) 168 (-8.7%) 483 (-1.6%) 541 (-3.0%) 734 (-2.9%) 1016
STSO 1020 steps (queue-hop) 2496 (+21%) 🔻 3474 (+25%) 🔻 3474 (+25%) 🔻 3474 (+25%) 🔻 3
WO 1020 steps 414514 (-4.1%) 414514 (-4.1%) 414514 (-4.1%) 414514 (-4.1%) 1
SL stream latency 89 (+3.5%) 179 🔴 (+23%) 🔻 199 🔴 (+8.2%) 354 🔴 (+39%) 🔻 30
SO stream overhead (text) 89 (-35%) 💚 138 (-73%) 💚 159 (-83%) 💚 262 (-78%) 💚 30
SO stream 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-actions Bot 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

Metric 2026-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
Result 1152/1400 regressions 1088/1088 regressions — partial (1088 of 1400 planned) 1178/1400 regressions 703/722 regressions — partial (722 of 1400 planned) 1165/1400 regressions 1122/1400 regressions (+54 infra) 1158/1400 regressions 522/690 regressions — partial (690 of 1400 planned) missing result file 1040/1400 regressions 157/1400 regressions 174/843 regressions (+11 infra) — partial (843 of 1400 planned) 2/2 regressions — partial (2 of 1400 planned) 168/1400 regressions
Total 1400 1088 1400 722 1400 1400 1400 690 0 1400 1400 843 2 1400
completed 248 0 222 19 235 224 242 168 0 360 1243 658 0 1232
CORRUPTED_EVENT_LOG 1149 476 1178 703 1163 1121 1158 522 0 1030 157 174 2 148
USER_ERROR 0 0 0 0 0 0 0 0 0 0 0 0 0 0
RUNTIME_ERROR 0 0 0 0 0 0 0 0 0 0 0 0 0 0
stuck 3 612 0 0 2 1 0 0 0 10 0 0 0 20
other 0 0 0 0 0 0 0 0 0 0 0 0 0 0
infra 0 0 0 0 0 54 0 0 0 0 0 11 0 0
Config 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1088 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 722 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 690 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 843 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 2 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8 1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timing watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms watchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

Scenario Total completed CORRUPTED_EVENT_LOG USER_ERROR RUNTIME_ERROR stuck other infra
step-storm 600 451 147 0 0 2 0 0
hook-storm 600 581 1 0 0 18 0 0
hook-sleep 200 200 0 0 0 0 0 0

Latest Non-Completed Runs

Scenario Attempt Outcome Status Error code Run
step-storm 7 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6A90GG6X9BJPN6KF1CB
step-storm 11 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6CR0GM02NE7BVVGQ5Y7
step-storm 39 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6Q70GM2W1HFK5JHXMP1
step-storm 12 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6C90GYWR9EJA77P3C89
step-storm 8 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM65E0GP1QKSFFBYEEX26
step-storm 2 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6580GNHB59XAF5AKECF
step-storm 36 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6MX0GT3B1AFCM5RNK06
step-storm 19 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6F40GX4HE4PMJ8HFV3H
step-storm 10 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM66E0GWTGQTERPPJZ7KP
step-storm 33 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6MY0GYJ8RCYQD800NCF
step-storm 32 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYM6MT0GR159RNC8H16F5M
step-storm 49 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYNXMZ0GJ3F0697D1W52CT
step-storm 46 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYNV3C0GG7P005HH5MHG37
step-storm 43 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYN0Y50GHAET1J5PAXHSY0
step-storm 60 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYPMKJ0GZZ84AM2JJYZ16F
step-storm 62 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYPMVV0GK8RC06EV9ZNVSR
step-storm 48 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYNWZW0GSGXE8JBSBZG1F0
step-storm 68 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYPPMW0GQYCVVBEDPNSXBE
step-storm 67 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KYWYPP7J0GT372S5N12KHY7V
step-storm 78 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_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.
VaguelySerious and 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 thread packages/world-local/src/storage/events-storage.ts
VaguelySerious and 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant