Skip to content

[core] Derive correlation ids from per-kind sequences (opt-in) - #3301

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/per-kind-correlation-ids
Aug 3, 2026
Merged

[core] Derive correlation ids from per-kind sequences (opt-in)#3301
VaguelySerious merged 6 commits into
mainfrom
peter/per-kind-correlation-ids

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Aug 3, 2026

Copy link
Copy Markdown
Member

Correlation ids are minted by the workflow VM and are the backend's identity gate: a conditional create on the id is what makes a duplicate write from a second live replay idempotent instead of additive. Today every id is the Nth draw of one monotonic ULID sequence per run, shared by steps, waits, hooks, attribute writes, abort controllers and stream ids alike. Every id is therefore an ordinal over the whole run, and one extra draw of any kind renumbers every entity of every kind after it.

That is the coupling this PR removes. Each kind draws from its own independent sequence, so two replays that agree about every step but disagree about one sleep() still mint the same id for the Nth step. Under the shared sequence they mint different ids for every step after the sleep, so their writes append side by side instead of colliding, and the settled log ends up holding two names for one logical step. Only one of them can be consumed on the next replay; the other is fatal (onUnconsumedEventCORRUPTED_EVENT_LOG).

Off by default, opt in with WORKFLOW_PER_KIND_CORRELATION_IDS=1. Default-off is deliberate: a run must replay under the scheme that minted its ids, and on a platform that does not pin a run to the deployment that started it (world-postgres, world-local, any self-hosted process) a default flip would make the SDK upgrade itself the scheme change, failing every in-flight run's next replay. Turning the flag on has that same hazard, so the docs say to do it during a quiet window and to roll the value out fleet-wide at once rather than through a rolling deploy that leaves both values live. On Vercel skew protection removes the hazard, and workbench/nextjs-turbopack sets the flag so e2e and the race-repro harness run the new scheme.

What this is not

This is deliberately much less than #3179 (call-site-addressed ids). It does not make an id independent of ordinal position within its own kind: two replays that disagree about how many steps ran still mint different ids for the next step. It narrows the blast radius of a disagreement to the one kind that actually disagrees, and it preserves properties #3179 gives up:

  • ids stay monotonic within a kind, so hooks.list keeps creation order among the hooks a workflow creates. [measurement] Call-site-addressed correlation ids #3179's hash-per-call-site reorders it, which broke the webhookWorkflow e2e. The caveat: hook and abortHook are separate kinds and no world filters system hooks out of a listing, so a run that constructs an abort controller and creates its own hooks lists that one system hook at a position set by its kind's hash. The only consumer in this repo is wf inspect's display ordering.
  • no argument fingerprinting, so no dependence on how a step's arguments serialize.

Ids remain syntactically valid ULIDs: 10 Crockford characters of fixedTimestamp plus a 16-character body derived per kind from a 128-bit hash of the run seed, then advanced with incrementBase32.

Also here: a replay-determinism fix, unconditional

serialization.ts mints stream ids (and an abort holder's stream name and abrt_ hook token) through the STABLE_ULID global and calls it with no seed time. ulid's monotonicFactory returns encodeTime(lastTime) on its increment branch, so a single such call latches the host wall clock into lastTime, and every id the run mints afterwards carries that timestamp instead of fixedTimestamp. The timestamp differs on every replay, so any workflow that serialized a stream before creating further entities minted different correlation ids on each replay. STABLE_ULID now goes through the same generator, which always passes fixedTimestamp. This applies in both modes, and has its own changeset.

Testing

  • packages/core/src/correlation-id.test.ts: determinism across replays, per-run distinctness, monotonicity within a kind, that extra hook/wait/stream/attr/abort draws do not renumber steps, that an abort controller does not renumber user hooks, that every id decodes to fixedTimestamp in both modes, and the flag's default.
  • packages/core/src/correlation-id-replay.test.ts: the same properties seen through the primitives that actually mint ids, including consuming an event log authored with the derived id.
  • packages/core/src/workflow.test.ts: a runWorkflow-level pin on the STABLE_ULID binding site. Reverting that line to the raw monotonic factory fails it with ReplayDivergenceError, which is the bug the fix removes.
  • Core unit suite green in both modes. Tests that replay against event logs with hardcoded correlation ids pin themselves to the shared scheme, because those fixtures record ids the shared sequence minted; the rest of the suite runs whichever scheme the flag selects.

Docs Preview

Page Link
Runtime tuning → WORKFLOW_PER_KIND_CORRELATION_IDS https://workflow-docs-git-peter-per-kind-correlation-ids.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_per_kind_correlation_ids

Correlation ids were the Nth draw of one monotonic ULID sequence per run,
shared by steps, waits, hooks, attribute writes, abort controllers and
stream ids. Every id was an ordinal over the whole run, so one extra draw
of any kind renumbered every entity of every kind after it, and two
replays that disagreed about a single sleep minted different ids for
every step that followed.

Each kind now draws from its own independent sequence, behind
WORKFLOW_PER_KIND_CORRELATION_IDS=1.

Also routes STABLE_ULID through the same generator. Serialization minted
stream ids with no seed time, and monotonicFactory's increment branch
returns encodeTime(lastTime), so one such call latched the host wall
clock and every later id in the run carried a per-replay timestamp
instead of fixedTimestamp. That fix applies in both modes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7da5002

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core Patch
workflow Patch
@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

@vercel

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

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

The benchmark run for 7da5002 failed. See the run logs for details.

Partial results from the failed run:

commit 7da5002 · Mon, 03 Aug 2026 23:50:29 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 210 (-12%) 840 🔴 (-22%) 💚 1348 🔴 (+24%) 🔻 1616 🔴 (+32%) 🔻 30
TTFS hook + stream 345 (-11%) 1577 🔴 (+20%) 🔻 1626 🔴 (+21%) 🔻 2073 🔴 (+45%) 🔻 30
STSO 1020 steps (inline) 95 (+13%) 145 (-5.2%) 167 (-9.7%) 259 (-24%) 💚 1018
STSO 1020 steps (queue-hop) 3567 (-0.7%) 3567 (-0.7%) 3567 (-0.7%) 3567 (-0.7%) 1
WO 1020 steps 142817 (-7.0%) 142817 (-7.0%) 142817 (-7.0%) 142817 (-7.0%) 1
SL stream latency 94 (+1.1%) 149 🔴 (+11%) 163 🔴 (+6.5%) 196 🔴 (+7.1%) 30
SO stream overhead (text) 127 (+4.1%) 227 (-21%) 💚 270 (-34%) 💚 387 (-37%) 💚 30
SO stream overhead (structured) 132 (-2.9%) 213 (-13%) 415 (+52%) 🔻 599 (+42%) 🔻 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 148883ms → this run 137813ms (Δ -11070ms, -7%)

 50-100 ms  ┃                         main   3  this   4    +1
100-150 ms  ██████████████████████░┃  main 721  this 803   +82
150-200 ms  ████┃██                   main 226  this 166   -60
200-250 ms  ┃                         main  39  this  32    -7
250-300 ms  ┃                         main  10  this  10    +0
300-350 ms  ┃                         main  11  this   3    -8
350-400 ms  ┃                         main   4  this   0    -4
400-450 ms  ┃                         main   3  this   0    -3
500-550 ms  ┃                         main   1  this   0    -1

1020 steps (queue-hop)

Cumulative STSO time: main 3591ms → this run 3567ms (Δ -24ms, -1%)

3500-4000 ms  ███████████████████████┃  main 1  this 1  +0
📜 Previous results (2)

e49c053

Mon, 03 Aug 2026 22:27:27 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1310 (+23%) 🔻 1417 🔴 (+26%) 🔻 1444 🔴 (+26%) 🔻 1517 🔴 (+23%) 🔻 30
TTFS stream 329 (+28%) 🔻 1438 🔴 (+30%) 🔻 1478 🔴 (+31%) 🔻 1639 🔴 (+42%) 🔻 30
TTFS hook + stream 1535 (+265%) 🔻 1697 🔴 (+29%) 🔻 1768 🔴 (+28%) 🔻 1886 🔴 (+25%) 🔻 30
STSO 1020 steps (inline) 176 (+1.1%) 487 (-3.2%) 548 (-1.1%) 705 (-5.7%) 1016
STSO 1020 steps (queue-hop) 2297 (+4.6%) 3421 (-6.8%) 3421 (-6.8%) 3421 (-6.8%) 3
WO 1020 steps 415709 (-3.8%) 415709 (-3.8%) 415709 (-3.8%) 415709 (-3.8%) 1
SL stream latency 106 (+14%) 158 🔴 (+11%) 164 🔴 (-2.4%) 232 🔴 (-51%) 💚 30
SO stream overhead (text) 110 (-19%) 💚 154 (-36%) 💚 194 (-40%) 💚 233 (-99%) 💚 30
SO stream overhead (structured) 114 (-26%) 💚 193 (-29%) 💚 274 (-11%) 504 (-36%) 💚 30

b82b39d

Mon, 03 Aug 2026 19:45:36 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1252 (+190%) 🔻 1309 🔴 (+27%) 🔻 1331 🔴 (+27%) 🔻 1584 🔴 (+2.7%) 30
TTFS stream 1275 (+34%) 🔻 1327 🔴 (+33%) 🔻 1337 🔴 (+33%) 🔻 1465 🔴 (+39%) 🔻 30
TTFS hook + stream 494 (-58%) 💚 1586 🔴 (+24%) 🔻 1601 🔴 (+23%) 🔻 1889 🔴 (+38%) 🔻 30
STSO 1020 steps (inline) 161 (-1.2%) 477 (+2.1%) 532 (±0%) 766 (+8.0%) 1016
STSO 1020 steps (queue-hop) 2006 (-4.8%) 6762 (+116%) 🔻 6762 (+116%) 🔻 6762 (+116%) 🔻 3
WO 1020 steps 407782 (+1.5%) 407782 (+1.5%) 407782 (+1.5%) 407782 (+1.5%) 1
SL stream latency 98 (+21%) 🔻 139 🔴 (+15%) 156 🔴 (+13%) 172 🔴 (-7.0%) 30
SO stream overhead (text) 101 (-3.8%) 152 (-17%) 💚 159 (-24%) 💚 267 (-64%) 💚 30
SO stream overhead (structured) 105 (+1.0%) 148 (-10%) 190 (-7.8%) 338 (+33%) 🔻 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 Aug 3, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (122 failed)

astro (17 failed):

  • hookWorkflow | wrun_41KZ4ZTPHS0GKX5AD44988GGQM | 🔍 observability
  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8 | 🔍 observability
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S | 🔍 observability
  • sleepingWorkflow | wrun_41KZ4ZTWHX0GH6GXJ2R4KDJZPD | 🔍 observability
  • sleepWinsRaceWorkflow | wrun_41KZ4ZVCDG0GZ5CS7873JAQ3SH | 🔍 observability
  • nullByteWorkflow | wrun_41KZ4ZVM3D0GSPG1TKQKWP5HG3 | 🔍 observability
  • workflowAndStepMetadataWorkflow | wrun_41KZ4ZWJV00GWKY7Q5B9X4GKMM | 🔍 observability
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions | wrun_41KZ4ZW2GB0GQ21DEMEZ60QGBF | 🔍 observability
  • writableForwardedFromWorkflowWorkflow | wrun_41KZ4ZWP1D0GWYMF492VKS8NF3 | 🔍 observability
  • fetchWorkflow | wrun_41KZ4ZX4KC0GWMN8KG420AXGA1 | 🔍 observability
  • promiseRaceStressTestWorkflow | wrun_41KZ4ZX6TM0GN11X32KKN4D8SW | 🔍 observability
  • error handling error propagation workflow errors nested function calls preserve message and stack trace

example (10 failed):

express (17 failed):

  • hookWorkflow | wrun_41KZ4ZTPHS0GKX5AD44988GGQM | 🔍 observability
  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8 | 🔍 observability
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY | 🔍 observability
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S | 🔍 observability
  • sleepingWorkflow | wrun_41KZ4ZTWHX0GH6GXJ2R4KDJZPD | 🔍 observability
  • parallelSleepWorkflow | wrun_41KZ4ZV8MB0GMSVK5VFB996NWT | 🔍 observability
  • sleepWinsRaceWorkflow | wrun_41KZ4ZVCDG0GZ5CS7873JAQ3SH | 🔍 observability
  • stepWinsRaceWorkflow | wrun_41KZ4ZVG6H0GYFMTXEJMFB56DC | 🔍 observability
  • nullByteWorkflow | wrun_41KZ4ZVM3D0GSPG1TKQKWP5HG3 | 🔍 observability
  • workflowAndStepMetadataWorkflow | wrun_41KZ4ZWJV00GWKY7Q5B9X4GKMM | 🔍 observability
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns -1 before any chunks are written
  • writableForwardedFromWorkflowWorkflow | wrun_41KZ4ZWP1D0GWYMF492VKS8NF3 | 🔍 observability
  • writableForwardedFromStepWorkflow | wrun_41KZ4ZWXGG0GXDYC4VG1MX9C9P | 🔍 observability

fastify (6 failed):

hono (16 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8 | 🔍 observability
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY | 🔍 observability
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S | 🔍 observability
  • sleepingWorkflow | wrun_41KZ4ZTWHX0GH6GXJ2R4KDJZPD | 🔍 observability
  • stepWinsRaceWorkflow | wrun_41KZ4ZVG6H0GYFMTXEJMFB56DC | 🔍 observability
  • nullByteWorkflow | wrun_41KZ4ZVM3D0GSPG1TKQKWP5HG3 | 🔍 observability
  • workflowAndStepMetadataWorkflow | wrun_41KZ4ZWJV00GWKY7Q5B9X4GKMM | 🔍 observability
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns -1 before any chunks are written
  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions | wrun_41KZ4ZW2GB0GQ21DEMEZ60QGBF | 🔍 observability
  • utf8StreamWorkflow | wrun_41KZ4ZWEJ30GN4Z38FGT7SYGQN | 🔍 observability
  • writableForwardedFromWorkflowWorkflow | wrun_41KZ4ZWP1D0GWYMF492VKS8NF3 | 🔍 observability

nextjs-turbopack (9 failed):

  • DurableAgent e2e prepareCall (GAP) completes but prepareCall is not applied (GAP)
  • DurableAgent e2e prepareStep on constructor agent-level prepareStep is called for each LLM step
  • DurableAgent e2e prepareStep on constructor stream-level prepareStep overrides constructor-level
  • DurableAgent e2e multimodal tool results passes through LanguageModelV3ToolResultOutput from tools
  • DurableAgent e2e tool approval (GAP) completes but needsApproval is not checked (GAP)
  • retainedInterleavingWorkflow | wrun_41KZ4ZT1Z80GY6VVGJJ13765J3 | 🔍 observability
  • readableStreamWorkflow | wrun_41KZ4ZT80T0GKWEBAMSP22HTR6 | 🔍 observability
  • hookWorkflow | wrun_41KZ4ZTPHS0GKX5AD44988GGQM | 🔍 observability
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY | 🔍 observability

nextjs-webpack (17 failed):

  • DurableAgent e2e core multiple sequential tool calls
  • DurableAgent e2e provider tools provider tool identity preserved across step boundaries
  • DurableAgent e2e provider tools mixed provider and function tools
  • DurableAgent e2e instructions string instructions are passed to the model
  • DurableAgent e2e timeout completes within timeout
  • DurableAgent e2e experimental_onStart (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e experimental_onStepStart (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e experimental_onToolCallStart (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e experimental_onToolCallFinish (GAP) completes but callbacks are not called (GAP)
  • DurableAgent e2e prepareCall (GAP) completes but prepareCall is not applied (GAP)
  • DurableAgent e2e prepareStep on constructor agent-level prepareStep is called for each LLM step
  • DurableAgent e2e prepareStep on constructor stream-level prepareStep overrides constructor-level
  • DurableAgent e2e multimodal tool results passes through LanguageModelV3ToolResultOutput from tools
  • DurableAgent e2e tool approval (GAP) completes but needsApproval is not checked (GAP)
  • promiseAllWorkflow | wrun_41KZ4ZRZE20GT8KXYAK4F60BAD | 🔍 observability
  • promiseRaceWorkflow | wrun_41KZ4ZS41M0GZXDS8BQ86NP955 | 🔍 observability
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY | 🔍 observability

nitro (5 failed):

nuxt (5 failed):

sveltekit (8 failed):

  • DurableAgent e2e prepareStep on constructor agent-level prepareStep is called for each LLM step
  • DurableAgent e2e prepareStep on constructor stream-level prepareStep overrides constructor-level
  • DurableAgent e2e tool approval (GAP) completes but needsApproval is not checked (GAP)
  • promiseAnyWorkflow | wrun_41KZ4ZSSRV0GS90AAGGR53ND35 | 🔍 observability
  • retainedInterleavingWorkflow | wrun_41KZ4ZT1Z80GY6VVGJJ13765J3 | 🔍 observability
  • readableStreamWorkflow | wrun_41KZ4ZT80T0GKWEBAMSP22HTR6 | 🔍 observability
  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8 | 🔍 observability
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S | 🔍 observability

vite (12 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8 | 🔍 observability
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY | 🔍 observability
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S | 🔍 observability
  • parallelSleepWorkflow | wrun_41KZ4ZV8MB0GMSVK5VFB996NWT | 🔍 observability
  • sleepWinsRaceWorkflow | wrun_41KZ4ZVCDG0GZ5CS7873JAQ3SH | 🔍 observability
  • stepWinsRaceWorkflow | wrun_41KZ4ZVG6H0GYFMTXEJMFB56DC | 🔍 observability
  • workflowAndStepMetadataWorkflow | wrun_41KZ4ZWJV00GWKY7Q5B9X4GKMM | 🔍 observability
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns correct index after stream completes
  • outputStreamWorkflow - getTailIndex and getChunks getTailIndex returns -1 before any chunks are written
  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

💻 Local Development (1 failed)

astro-stable (1 failed):

  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY

📦 Local Production (1 failed)

vite-stable (1 failed):

  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY
📋 Other (24 failed)

e2e-vercel-prod-nest (10 failed):

  • hookWorkflow | wrun_41KZ4ZTPHS0GKX5AD44988GGQM
  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S
  • sleepingWorkflow | wrun_41KZ4ZTWHX0GH6GXJ2R4KDJZPD
  • parallelSleepWorkflow | wrun_41KZ4ZV8MB0GMSVK5VFB996NWT
  • workflowAndStepMetadataWorkflow | wrun_41KZ4ZWJV00GWKY7Q5B9X4GKMM
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)

e2e-vercel-prod-tanstack-start (14 failed):

  • hookWorkflow is not resumable via public webhook endpoint | wrun_41KZ4ZTC2K0GZZE4ZB826E51E8
  • webhookWorkflow | wrun_41KZ4ZV2TK0GH4S5G3BS3WVQQY
  • parallelStepsThenWebhookWorkflow - no hook_conflict from same-tick replay race | wrun_41KZ4ZV6QV0GS8DD6PWTC4099S
  • sleepingWorkflow | wrun_41KZ4ZTWHX0GH6GXJ2R4KDJZPD
  • parallelSleepWorkflow | wrun_41KZ4ZV8MB0GMSVK5VFB996NWT
  • sleepWinsRaceWorkflow | wrun_41KZ4ZVCDG0GZ5CS7873JAQ3SH
  • nullByteWorkflow | wrun_41KZ4ZVM3D0GSPG1TKQKWP5HG3
  • workflowAndStepMetadataWorkflow | wrun_41KZ4ZWJV00GWKY7Q5B9X4GKMM
  • outputStreamWorkflow no startIndex (reads all chunks)
  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow negative startIndex (reads from end)
  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream
  • outputStreamInsideStepWorkflow - getWritable() called inside step functions | wrun_41KZ4ZW2GB0GQ21DEMEZ60QGBF
  • writableForwardedFromWorkflowWorkflow | wrun_41KZ4ZWP1D0GWYMF492VKS8NF3

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 1344 122 239 1705
❌ 💻 Local Development 1632 1 227 1860
❌ 📦 Local Production 1632 1 227 1860
✅ 🐘 Local Postgres 1633 0 227 1860
✅ 🪟 Windows 155 0 0 155
❌ 📋 Other 1004 24 212 1240
✅ vercel-multi-region 27 0 0 27
Total 7427 148 1132 8707
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
❌ astro 110 17 28
❌ example 117 10 28
❌ express 110 17 28
❌ fastify 121 6 28
❌ hono 111 16 28
❌ nextjs-turbopack 143 9 3
❌ nextjs-webpack 135 17 3
❌ nitro 122 5 28
❌ nuxt 122 5 28
❌ sveltekit 138 8 9
❌ vite 115 12 28

❌ 💻 Local Development

App Passed Failed Skipped
❌ astro-stable 128 1 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 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 128 1 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

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack 155 0 0

❌ 📋 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 117 10 28
❌ e2e-vercel-prod-tanstack-start 113 14 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@pranaygp
pranaygp marked this pull request as ready for review August 3, 2026 19:13
@pranaygp
pranaygp requested a review from ijjk as a code owner August 3, 2026 19:13
Copilot AI review requested due to automatic review settings August 3, 2026 19:13
@VaguelySerious
VaguelySerious requested a review from ijjk as a code owner August 3, 2026 19:13
@pranaygp
pranaygp requested a review from a team as a code owner August 3, 2026 19:13
@VaguelySerious
VaguelySerious requested a review from a team as a code owner August 3, 2026 19:13
@pranaygp
pranaygp requested a review from a team as a code owner August 3, 2026 19:13
@VaguelySerious
VaguelySerious requested a review from a team as a code owner August 3, 2026 19:13
@VaguelySerious

Copy link
Copy Markdown
Member Author

(AI) Thanks for running both schemes and the baseline comparison — that changed the shape of this PR.

On efficacy, you are right and I am not going to argue it. step-storm not moving is by construction (same-kind disagreement, explicitly out of scope), but hook-storm is where cross-kind absorption should have shown up and it did not move outside noise: 4/6 and 3/6 here against 4/6 on the flag-off baseline in #3273. I do not have production evidence that isolates the cross-kind class either. So the honest position is that this is a mechanism whose failure class is real in the code but not demonstrated by the harness built to measure it.

That is the reason for the change in this push rather than a re-run: the default is off again. Shipping an upgrade-boundary scheme change on the strength of an argument, with a flat harness, was the wrong trade. Opt-in, it costs nothing to anyone who does not set the variable, and workbench/nextjs-turbopack still sets it so e2e and the race-repro label exercise the new scheme on every run. The corruption that harness measures is being taken to zero by the slot-id stack, not by this.

Everything else in this push is your review:

  • STABLE_ULID binding pinned at the runWorkflow level; reverting the line now fails with ReplayDivergenceError.
  • hooks.list caveat documented in code and docs (two kinds mint hook_ ids, no world filters system hooks); checked the one consumer in this repo, wf inspect, which only displays.
  • stream kind doc notes abort-holder identities mint through it too.
  • Second changeset for the unconditional stream-id determinism fix; patch stays right now that no default moves.
  • Biome organizeImports fixed (biome ci clean), PR description rewritten for opt-in, Docs Preview link added.

Core unit suite green in both schemes; pnpm typecheck clean.

@pranaygp

pranaygp commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Re-reviewed at e49c053 — this addresses everything substantive from my review (opt-in default, both changesets, the runWorkflow-level STABLE_ULID regression test, the doc caveats, import sorting), and I verified locally that the new tests pass and the Biome errors are gone. The regression test is well-constructed: deriving the expected ids with the same seeded factory means reverting the binding site fails both the decodeTime assertion and the recorded-event consumption.

Three follow-ups on docs and test wiring, none blocking:

1. docs/content/docs/v5/how-it-works/event-sourcing.mdx — "Entity IDs" section (~line 278)

ULIDs enable chronological ordering: … making them lexicographically sortable by creation time. This property is essential for the event log—events are always stored and retrieved in the correct chronological order simply by sorting their IDs.

This claim is fully true only for server-minted ids (evnt_, wrun_). VM-minted correlation ids (step_/hook_/wait_) already carried the run's start timestamp rather than each entity's creation time — they sort by mint order, not by clock — and with WORKFLOW_PER_KIND_CORRELATION_IDS=1 cross-kind sorting stops reflecting creation order entirely (each kind lives in its own hash-derived range, including the two hook_ families). Worth a sentence in that section distinguishing event ids from correlation ids so the ordering claim stays scoped to the event log.

2. docs/content/docs/v5/errors/corrupted-event-log.mdx — "Common scenarios" list (and/or replay-divergence.mdx)

The page currently says "This error indicates a bug in the Workflow SDK or Workflow server — not in your workflow code." Once this flag exists, there's an operational cause too: flipping WORKFLOW_PER_KIND_CORRELATION_IDS while runs are in flight on a platform without deployment pinning, or a rolling deploy that leaves both values live across a fleet. That's exactly the error a world-postgres operator will see, and today the docs would send them straight to filing an SDK bug. A fourth bullet in "Common scenarios" (cross-linking the runtime-tuning.mdx entry, which already explains the invariant well) would let them self-diagnose.

3. The local race-repro rig still runs the old scheme

The commit message says the workbench flag makes "e2e and the race-repro harness exercise the new scheme," but that's true only for the Vercel-deployed lanes: vercel.json env doesn't reach next build/next start, and scripts/event-log-race-repro-local.sh exports WORKFLOW_TARGET_WORLD / WORKFLOW_PUBLIC_MANIFEST / WORKFLOW_POSTGRES_* but not this var — so local soaks silently measure the shared sequence. Either exporting it from the script (or a --per-kind-ids toggle, since comparing both schemes is exactly what that rig is for) or noting it in the script's env docs would close the gap.

Comment thread .changeset/per-kind-correlation-ids.md Outdated
'workflow': patch
---

Add experimental `WORKFLOW_PER_KIND_CORRELATION_IDS=1`, which gives each kind of entity a workflow creates its own sequence of correlation IDs so an extra hook or sleep no longer renumbers every step after it. Off by default; a run must replay under the scheme that minted its IDs, so only turn it on while no runs are in flight unless your platform pins a run to the deployment it started on

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.

too verbose

Comment thread .changeset/per-kind-correlation-ids.md Outdated
VaguelySerious and others added 2 commits August 3, 2026 16:30
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…ation-ids

# Conflicts:
#	packages/core/src/workflow.ts
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

No backport to stable for cb77725 (AI decision).

The bulk of this commit is feature work: a new opt-in WORKFLOW_PER_KIND_CORRELATION_IDS env flag with a new correlation-id.ts module, a new context API (generateCorrelationId replacing generateUlid), docs for the new setting, and workbench config enabling it — all additive capability rather than a defect fix. It does bundle one genuine unconditional fix (binding STABLE_ULID through the seeded generator so a stream-id draw no longer latches the host wall clock into the run's id sequence, which broke replay determinism), and per the mixed-commit rule that should be split out and force-backported on its own if wanted on stable. Flipping id-derivation plumbing on a maintenance line also carries its own in-flight-run hazard, which argues further against taking it wholesale.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

cb77725960a8c280bd96fda0068fca0ba83d52f2

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.

3 participants