Skip to content

Slot event identity (SDK 4/4): scope listByCorrelationId to a run - #3247

Closed
VaguelySerious wants to merge 1 commit into
peter/slot-ids-8-worldsfrom
peter/slot-ids-9-correlation-scope
Closed

Slot event identity (SDK 4/4): scope listByCorrelationId to a run#3247
VaguelySerious wants to merge 1 commit into
peter/slot-ids-8-worldsfrom
peter/slot-ids-9-correlation-scope

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Jul 31, 2026

Copy link
Copy Markdown
Member

Stacked on #3246. Review the last commit only.

Why

A correlation id identifies a step or wait within its run, not across runs. Nothing enforced that while every id carried a ULID, so the distinction never mattered. It matters as soon as a run numbers its own steps: step_…001 names the first step of every slot-numbered run.

Measured on the Local World before this change — two runs, each with one step, one unscoped lookup:

listByCorrelationId('step_…001') → 2 events, runIds [wrun_01KYTTGYF6…, wrun_01KYTTGYF9…]

Two consequences:

  • Foreign runs in the page. The observability search already post-filters on runId, so nothing wrong is displayed — but it pages against a bounded page cap, so the run it wants can sit past the cap and the search reports nothing found.
  • An ambiguous cursor. The Postgres query orders and resumes by eventId. Two runs can hold the same correlation id at the same slot, so eventId > cursor skips the sibling row at the equal id. Scoped, (run_id, id) is the primary key, so the cursor is a key again.

Nothing in the runtime reads this path — it is an observability read — so this is not a correctness regression in a run. It is a read that gets quietly wrong answers under slot identity.

What

ListEventsByCorrelationIdParams (and the analytics variant) gain an optional runId.

World How it scopes
Local Restricts the scan to the run's own event files
Postgres AND run_id = $n, which also disambiguates the cursor
Vercel — analytics Routes to the run-scoped endpoint that already takes a correlation-id filter
Vercel — runtime read Applies the scope to the returned page; the backend index is keyed by correlation id alone, so the pagination cursor and hasMore stay the backend's

The observability search passes the run it is already looking at, and keeps its post-filter as defence for a world that cannot scope.

Leaving the param optional keeps every existing caller working, and an unscoped lookup still means "every run that numbered one the same" — which is the honest answer to the question asked.

Tests

Both slot-identity suites gain the pair, and Postgres gains the cursor case:

  • unscoped lookup matches every run that numbered a step the same (asserts the collision rather than pretending it away)
  • scoped lookup returns one run
  • Postgres: paging a scoped query whose event ids repeat across runs returns both of the run's events, in order

Local 524 pass, Postgres 184 pass (real container), world 98, core 1687 + 3 expected fail, world-vercel 317, web 94.

Follow-up

The backend index behind the Vercel runtime read is keyed by correlation id alone, so a slot-mode step_…001 is a hot key across all runs and the scope is applied client-side. Taking a run scope there is a backend-side change, tracked separately.


Stack: #3228#3234#3246#3247

Paired backend stack (world-vercel side): 6 PRs, all rebased on main and stacked; WORKFLOW_SERVER_URL_OVERRIDE points at the top of it and must be reverted before merge.

@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 350af92

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

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

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

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

@vercel

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 Aug 2, 2026 4:56pm
example-nextjs-workflow-webpack Ready Ready Preview Aug 2, 2026 4:56pm
example-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-astro-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-express-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-fastify-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-hono-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-nestjs-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-nitro-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-nuxt-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-sveltekit-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-tanstack-start-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workbench-vite-workflow Ready Ready Preview Aug 2, 2026 4:56pm
workflow-docs Ready Ready Preview, v0 Aug 2, 2026 4:56pm
workflow-swc-playground Ready Ready Preview Aug 2, 2026 4:56pm
workflow-tarballs Ready Ready Preview Aug 2, 2026 4:56pm
workflow-web Ready Ready Preview Aug 2, 2026 4:56pm

@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)

fastify (1 failed):

  • AbortController abortListenerWorkflow: signal.addEventListener fires on the deserialized step signal

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 1465 1 239 1705
✅ 💻 Local Development 1497 0 208 1705
✅ 📦 Local Production 1633 0 227 1860
✅ 🐘 Local Postgres 1633 0 227 1860
✅ 📋 Other 1028 0 212 1240
✅ vercel-multi-region 27 0 0 27
Total 7283 1 1113 8397
Details by Category

❌ ▲ Vercel Production

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

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable 129 0 26
✅ express-stable 129 0 26
✅ fastify-stable 129 0 26
✅ hono-stable 129 0 26
✅ nextjs-turbopack-canary 136 0 19
✅ nextjs-turbopack-stable 155 0 0
✅ nextjs-webpack-stable 155 0 0
✅ nitro-stable 129 0 26
✅ nuxt-stable 129 0 26
✅ sveltekit-stable 148 0 7
✅ vite-stable 129 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable 129 0 26
✅ express-stable 129 0 26
✅ fastify-stable 129 0 26
✅ hono-stable 129 0 26
✅ nextjs-turbopack-canary 136 0 19
✅ nextjs-turbopack-stable 155 0 0
✅ nextjs-webpack-canary 136 0 19
✅ nextjs-webpack-stable 155 0 0
✅ nitro-stable 129 0 26
✅ nuxt-stable 129 0 26
✅ sveltekit-stable 148 0 7
✅ vite-stable 129 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable 129 0 26
✅ express-stable 129 0 26
✅ fastify-stable 129 0 26
✅ hono-stable 129 0 26
✅ nextjs-turbopack-canary 136 0 19
✅ nextjs-turbopack-stable 155 0 0
✅ nextjs-webpack-canary 136 0 19
✅ nextjs-webpack-stable 155 0 0
✅ nitro-stable 129 0 26
✅ nuxt-stable 129 0 26
✅ sveltekit-stable 148 0 7
✅ vite-stable 129 0 26

✅ 📋 Other

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

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@VaguelySerious
VaguelySerious force-pushed the peter/slot-ids-9-correlation-scope branch from 4e89132 to 04efaec Compare July 31, 2026 04:50
@VaguelySerious
VaguelySerious force-pushed the peter/slot-ids-9-correlation-scope branch from e20895c to 73199f0 Compare July 31, 2026 10:20
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 350af92 · Sun, 02 Aug 2026 17:16:53 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 229 (-71%) 💚 1336 🔴 (+22%) 🔻 1363 🔴 (+19%) 🔻 1396 🔴 (-8.4%) 30
TTFS stream 241 (+1.3%) 1366 🔴 (+26%) 🔻 1400 🔴 (+26%) 🔻 1674 🔴 (+43%) 🔻 30
TTFS hook + stream 486 (-61%) 💚 1628 🔴 (+19%) 🔻 1643 🔴 (+14%) 1672 🔴 (-9.1%) 30
STSO 1020 steps (inline) 154 (-11%) 538 (+3.9%) 603 (+3.1%) 823 (-6.3%) 1016
STSO 1020 steps (queue-hop) 1430 (-28%) 💚 3359 (+1.6%) 3359 (+1.6%) 3359 (+1.6%) 3
WO 1020 steps 460167 (+2.9%) 460167 (+2.9%) 460167 (+2.9%) 460167 (+2.9%) 1
SL stream latency 92 (±0%) 171 🔴 (-5.0%) 186 🔴 (-23%) 💚 419 🔴 (-3.5%) 30
SO stream overhead (text) 127 (-6.6%) 217 (±0%) 389 (+43%) 🔻 579 (+54%) 🔻 30
SO stream overhead (structured) 114 (-3.4%) 215 (-10%) 251 (-21%) 💚 532 (+19%) 🔻 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 437759ms → this run 451743ms (Δ +13984ms, +3%)

  150-200 ms  ┃█                        main  10  this   2    -8
  200-250 ms  ███████┃█████             main  86  this  49   -37
  250-300 ms  ███████████████┃          main 104  this 100    -4
  300-350 ms  ███████████████░░░░░░┃    main  98  this 139   +41
  350-400 ms  █████████████████████░░┃  main 136  this 153   +17
  400-450 ms  ██████████████████┃██     main 132  this 120   -12
  450-500 ms  ████████████████┃█████    main 140  this 108   -32
  500-550 ms  ██████████████████┃██     main 135  this 118   -17
  550-600 ms  ███████████████░░░┃       main  93  this 123   +30
  600-650 ms  ██████┃                   main  38  this  45    +7
  650-700 ms  ███┃                      main  18  this  25    +7
  700-750 ms  █┃                        main   8  this  11    +3
  750-800 ms  ┃                         main   3  this   7    +4
  800-850 ms  ┃                         main   2  this   6    +4
  850-900 ms  ┃                         main   3  this   4    +1
  900-950 ms  ┃                         main   5  this   3    -2
 950-1000 ms  ┃                         main   0  this   1    +1
1050-1100 ms  ┃                         main   1  this   0    -1
1200-1250 ms  ┃                         main   2  this   0    -2
1250-1300 ms  ┃                         main   1  this   0    -1
1300-1350 ms  ┃                         main   1  this   1    +0
4600-4650 ms  ┃                         main   0  this   1    +1

1020 steps (queue-hop)

Cumulative STSO time: main 8262ms → this run 7134ms (Δ -1128ms, -14%)

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

09f8e32

Sat, 01 Aug 2026 20:52:27 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 328 (-58%) 💚 1305 🔴 (+19%) 🔻 1321 🔴 (+15%) 🔻 1357 🔴 (-11%) 30
TTFS stream 1237 (+420%) 🔻 1277 🔴 (+18%) 🔻 1311 🔴 (+18%) 🔻 1519 🔴 (+30%) 🔻 30
TTFS hook + stream 1570 (+25%) 🔻 1650 🔴 (+20%) 🔻 1661 🔴 (+15%) 🔻 1678 🔴 (-8.8%) 30
STSO 1020 steps (inline) 157 (-9.2%) 496 (-4.2%) 549 (-6.2%) 723 (-18%) 💚 1016
STSO 1020 steps (queue-hop) 1638 (-17%) 💚 3485 (+5.4%) 3485 (+5.4%) 3485 (+5.4%) 3
WO 1020 steps 420928 (-5.8%) 420928 (-5.8%) 420928 (-5.8%) 420928 (-5.8%) 1
SL stream latency 85 (-7.6%) 169 🔴 (-6.1%) 182 🔴 (-25%) 💚 254 🔴 (-41%) 💚 30
SO stream overhead (text) 97 (-29%) 💚 149 (-32%) 💚 165 (-39%) 💚 233 (-38%) 💚 30
SO stream overhead (structured) 101 (-14%) 152 (-37%) 💚 188 (-41%) 💚 247 (-45%) 💚 30

d8f5792

Fri, 31 Jul 2026 21:46:43 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 226 (-78%) 💚 1374 🔴 (+18%) 🔻 1470 🔴 (+18%) 🔻 1505 🔴 (-3.5%) 30
TTFS stream 240 (-38%) 💚 1353 🔴 (+14%) 1398 🔴 (+12%) 1445 🔴 (-62%) 💚 30
TTFS hook + stream 401 (-24%) 💚 1599 🔴 (+7.8%) 1618 🔴 (+3.4%) 1651 🔴 (-10%) 30
STSO 1020 steps (inline) 162 (-21%) 💚 502 (-11%) 582 (-12%) 848 (-17%) 💚 1015
STSO 1020 steps (queue-hop) 2691 (+20%) 🔻 3417 (+9.0%) 254886 (+6039%) 🔻 254886 (+6039%) 🔻 4
WO 1020 steps 701452 (+37%) 🔻 701452 (+37%) 🔻 701452 (+37%) 🔻 701452 (+37%) 🔻 1
SL stream latency 121 (+8.0%) 223 🔴 (+11%) 243 🔴 (-16%) 💚 518 🔴 (-27%) 💚 30
SO stream overhead (text) 188 (+17%) 🔻 818 🔴 (+110%) 🔻 996 🔴 (+60%) 🔻 2088 🔴 (-25%) 💚 30
SO stream overhead (structured) 164 (-6.8%) 360 🔴 (-14%) 424 (-23%) 💚 476 (-99%) 💚 30

f55cdf1

Fri, 31 Jul 2026 21:03:08 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1302 (+86%) 🔻 1422 🔴 (+27%) 🔻 1436 🔴 (+5.7%) 1597 🔴 (-5.2%) 30
TTFS stream 270 (+0.7%) 1403 🔴 (+27%) 🔻 1432 🔴 (+27%) 🔻 1546 🔴 (+31%) 🔻 30
TTFS hook + stream 1303 (-3.6%) 1645 🔴 (+6.1%) 1698 🔴 (+0.9%) 1857 🔴 (-52%) 💚 30
STSO 1020 steps (inline) 178 (-15%) 468 (-16%) 💚 511 (-20%) 💚 682 (-35%) 💚 1016
STSO 1020 steps (queue-hop) 2226 (-10%) 3425 (-2.8%) 3425 (-4.4%) 3425 (-4.4%) 3
WO 1020 steps 398148 (-20%) 💚 398148 (-20%) 💚 398148 (-20%) 💚 398148 (-20%) 💚 1
SL stream latency 99 (-14%) 143 🔴 (-34%) 💚 154 🔴 (-38%) 💚 372 🔴 (-5.6%) 30
SO stream overhead (text) 102 (-40%) 💚 163 (-46%) 💚 180 (-53%) 💚 254 (-100%) 💚 30
SO stream overhead (structured) 97 (-50%) 💚 140 (-70%) 💚 145 (-83%) 💚 230 (-100%) 💚 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.

A correlation id identifies a step or wait within its run, not across
runs. That was invisible while every id carried a ULID, and it stops
being invisible once a run numbers its own steps: `step_…001` names the
first step of every slot-numbered run, so an unscoped lookup answers
with one event per run, and a cursor — an event id — cannot tell two
such rows apart.

`ListEventsByCorrelationIdParams` gains an optional `runId`. The Local
and Postgres Worlds filter on it, which also makes their cursor a key
again; the Vercel World routes analytics to the run-scoped endpoint it
already has and applies the scope to the page for the runtime read,
whose backend index is keyed by correlation id alone. The observability
search passes the run it is already looking at, and keeps its own
post-filter for a world that cannot scope.
@VaguelySerious

Copy link
Copy Markdown
Member Author

Superseded by #3280, which carries this commit on main instead of stacked behind #3246, and makes runId required rather than optional. Closing this one.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Event Log Race Repro

2 of 14 latest repro runs hit event-log regressions.

Run History

Metric 2026-08-02 17:18 UTC #1
logs / deploy
Result 2/14 regressions
Total 14
completed 12
CORRUPTED_EVENT_LOG 2
USER_ERROR 0
RUNTIME_ERROR 0
stuck 0
other 0
infra 0
Config 14 runs / step-storm 6, hook-storm 6, hook-sleep 2 / c8 / 6x8
Timing 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 6 4 2 0 0 0 0 0
hook-storm 6 6 0 0 0 0 0 0
hook-sleep 2 2 0 0 0 0 0 0

Latest Non-Completed Runs

Scenario Attempt Outcome Status Error code Run
step-storm 6 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KZ1QEFKR0GPMFKB12F3NNG7G
step-storm 1 CORRUPTED_EVENT_LOG failed CORRUPTED_EVENT_LOG wrun_41KZ1QEFHA0GWQD6BRSHMCC7GX

The event-log-race-repro-results artifact carries a window of the committed log around the divergent event, for a sample of the corruptions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-repro Run the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant