Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
[e2e] Rebuild the event-log corruption repro around step-count divergence by VaguelySerious · Pull Request #3147 · vercel/workflow · GitHub
Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [e2e] Rebuild the event-log corruption repro around step-count divergence by VaguelySerious · Pull Request #3147 · vercel/workflow · GitHub
Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [e2e] Rebuild the event-log corruption repro around step-count divergence by VaguelySerious · Pull Request #3147 · vercel/workflow · GitHub
Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' [e2e] Rebuild the event-log corruption repro around step-count divergence by VaguelySerious · Pull Request #3147 · vercel/workflow · GitHub
Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' [e2e] Rebuild the event-log corruption repro around step-count divergence by VaguelySerious · Pull Request #3147 · vercel/workflow · GitHub
Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [e2e] Rebuild the event-log corruption repro around step-count divergence by VaguelySerious · Pull Request #3147 · vercel/workflow · GitHub
Skip to content

[e2e] Rebuild the event-log corruption repro around step-count divergence - #3147

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro
Jul 28, 2026
Merged

[e2e] Rebuild the event-log corruption repro around step-count divergence#3147
VaguelySerious merged 1 commit into
mainfrom
peter/event-log-corruption-repro

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 28, 2026

Copy link
Copy Markdown
Member

Why

The event-log-race-repro job exists to give us a measurable CORRUPTED_EVENT_LOG rate to test event-guard changes against. Its last full run produced 2 corruptions / 2000 runs (0.1%), all in the hook-sleep scenario (2/1498); step-fanout was 0/250 and both step-sleep-race-* were 0/125. That rate is too low to tell a fix from noise, while real user workloads hit it repeatedly.

A corrupted event log needs three things to line up. The old scenarios supplied at most two:

  1. Concurrent writers on one run. The old scenarios had roughly one wake source per run. Parallel sleep()s do not help — the suspension handler schedules only the soonest pending wait as a single delayed re-invocation, so N parallel sleeps produce one wake, not N.
  2. A write derived from an incomplete event load. Needs (1) to be true first.
  3. Control flow that depends on the missing event by step count. This was entirely absent. Every old branch emitted the same number of steps whichever way it resolved, so a replay working from a stale log still minted the same correlation IDs — a benign retry, not a divergence.

Item 3 is the amplifier. Correlation IDs are positional ordinals of one seeded sequence, so a replay that emits a different number of steps renames every entity after that point, turning one missing event into an unrecoverable divergence. The corrupted production runs we traced diverged at ordinals 1337 and 901, deep into long logs.

What

Two new storm workflows (workbench/nextjs-turbopack/workflows/103_event_log_corruption_repro.ts) supply all three by construction. Each round fans out width branches; every branch races a settle path that emits one step against a recovery path that emits two, and then the round fans out a reconcile batch whose width is derived from how many branches took the recovery path.

  • step-storm races an in-run step completion against a per-branch watchdog timer. The step body also issues setAttributes, which is a genuinely out-of-band write, and the driver resumes a hook the workflow creates but never reads on a continuous cadence, so every replay is racing a stream of hook_received events written with no snapshot behind them.
  • hook-storm races an out-of-band hook delivery against the watchdog, with the round's deliveries staggered so they straddle the deadline. This is the shape seen corrupting production runs: a task waiting on a callback with a timeout.
  • hook-sleep is retained at low attempt counts (200) as a calibration control, since its historical ~0.1% rate is our only baseline.

Defaults: 600 step-storm + 600 hook-storm + 200 control at concurrency 40, 6 rounds x 8 branches, watchdog 2500 ms against a 2200 ms +/- 250 ms step delay so the jitter decides each winner. Every knob is a workflow_dispatch input.

Two other changes matter for reading the results:

  • The classifier now reads the top-level errorCode.error is rehydrated into an Error carrying only name/message, so error.code was always undefined and every real failure was bucketed as other. The previous artifact's other: 2 and this branch's CORRUPTED_EVENT_LOG: 2 would be the same outcome reported differently — worth remembering when diffing against the old sticky comment.
  • A ledger consistency check (validateStormReturn) catches silent corruption: a run that diverged but still reached completed will normally disagree with itself about its own straggler count or reconcile width. That reports as BAD_STORM_LEDGER.

Result

First full run on this branch (plain main SDK, no guard), run 30316814473:

scenariorunsCORRUPTED_EVENT_LOGratestuck
step-storm60054490.7%0
hook-storm60036260.3%22
hook-sleep (control)20000%0
total140090664.7%22

0 USER_ERROR (the poke-hook risk below did not materialise), 0 RUNTIME_ERROR, 0 BAD_STORM_LEDGER, 0 infra. Against the previous harness's 2/2000 (0.1%), that is a ~650x increase, and the retained hook-sleep control stayed at 0/200 in the same run — so the gain comes from the step-count-divergent shape, not from load.

The Event Log Race Repro check failing is the corruption gate firing as designed; every other check on this PR passes, including E2E Required Check.

The 22 stuck runs are all hook-storm, all still running at the 240 s timeout, spread evenly across attempts and not correlated with failed hook resumes (some have 8/8 resumes delivered). That is a separate signal from divergence and worth a look on its own.

Acceptance bar

Label a PR event-log-race-repro and the job runs. Met: 906 corruptions in one run, with step-storm alone at 90.7%. The knobs are still there if a future backend change suppresses the rate and it needs re-tuning: width, watchdog_ms vs step_delay_ms separation, rounds, then attempt counts.

Known risk

A step-storm run accumulates roughly 40 unread hook_received events on the poke hook. If the backend capped pending hook events this would surface as USER_ERROR rather than a corruption. The first full run reported 0 USER_ERROR, so it does not.

…ence
The existing `event-log-race-repro` job produced ~0.1% `CORRUPTED_EVENT_LOG`
(2 / 2000 runs), all from the `hook-sleep` scenario, because its scenarios
supplied only part of the recipe: one wake source per run, short event logs,
and — critically — branches that emit the same number of steps whichever way
they resolve. A stale write in that shape is a benign retry, not a divergence.
The two new storm workflows race a settle path that emits one step against a
recovery path that emits two, then fan out a reconcile batch whose width is
derived from how many branches took the recovery path. Because correlation IDs
are positional ordinals of one seeded sequence, a replay that disagrees about
the winner renames every entity after it.
- `step-storm` races an in-run step completion against a per-branch watchdog,
under continuous out-of-band pressure from a never-read poke hook.
- `hook-storm` races an out-of-band hook delivery against the watchdog — the
shape seen corrupting production runs.
- `hook-sleep` is retained at low attempt counts as a calibration control.
The harness also now classifies failures from the top-level `errorCode` rather
than `error.code`, which is rehydrated into an `Error` carrying only
`name`/`message`; every real corruption was previously reported as `other`.
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c0163e9

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

This PR includes changesets to release 0 packages

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

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

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

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

vercelBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit c0163e9 · Tue, 28 Jul 2026 00:39:27 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1277 (+73%) 🔻1370 🔴 (+21%) 🔻1381 🔴 (+14%)1460 🔴 (+6.8%)30
TTFSstream1271 (+305%) 🔻1347 🔴 (+23%) 🔻1366 🔴 (+19%) 🔻1420 🔴 (+21%) 🔻30
TTFShook + stream1462 (+15%) 🔻1637 🔴 (+17%) 🔻1669 🔴 (+14%)1698 🔴 (+3.9%)30
STSO1020 steps (1-20)177 (+4.1%)270 🔴 (-11%)316 🔴 (-10%)322 🔴 (-15%) 💚19
STSO1020 steps (101-120)193 (-2.0%)285 🔴 (-16%) 💚360 🔴 (-12%)386 🔴 (-23%) 💚19
STSO1020 steps (1001-1020)492 (+1.9%)606 🔴 (+5.6%)684 🔴 (+5.9%)716 🔴 (+9.0%)19
WO1020 steps401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)401175 (-8.6%)1
SLstream latency100 (+7.5%)187 🔴 (+11%)361 🔴 (+54%) 🔻449 🔴 (+77%) 🔻30
SOstream overhead (text)105 (-20%) 💚164 (-43%) 💚314 (-12%)522 (+24%) 🔻30
SOstream overhead (structured)108 (-20%) 💚173 (-33%) 💚195 (-30%) 💚263 (-36%) 💚30
ℹ️ Metric definitions & methodology

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 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

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

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

@github-actions

github-actionsBot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

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

✅ ▲ Vercel Production

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

✅ 💻 Local Development

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

✅ 📦 Local Production

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

✅ 🐘 Local Postgres

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

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15400

✅ 📋 Other

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

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

Copy link
Copy Markdown
Contributor

Event Log Race Repro

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

Run History

Metric2026-07-28 00:54 UTC #1
logs / deploy
Result928/1400 regressions
Total1400
completed472
CORRUPTED_EVENT_LOG906
USER_ERROR0
RUNTIME_ERROR0
stuck22
other0
infra0
Config1400 runs / step-storm 600, hook-storm 600, hook-sleep 200 / c40 / 6x8
Timingwatchdog 2500ms / step 2200±250ms / stagger 400ms / poke 750ms / timeout 240000ms

Latest Scenario Breakdown

ScenarioTotalcompletedCORRUPTED_EVENT_LOGUSER_ERRORRUNTIME_ERRORstuckotherinfra
step-storm6005654400000
hook-storm600216362002200
hook-sleep200200000000

Latest Non-Completed Runs

ScenarioAttemptOutcomeStatusError codeRun
step-storm2CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ1B0GV5KBGJ8N8NHXHT
step-storm3CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR5
step-storm4CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ0N0GJYS7RAMHBNPSR4
step-storm5CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ140GYRGA147NWHY1TE
step-storm6CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ120GNHJY7SJ3VGTE0R
step-storm7CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3A0GXA2BAVSN4CBSDW
step-storm8CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ310GNCGZ2FGGJFBTMD
step-storm9CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3S0GT7Q8ZASHEANBV7
step-storm10CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ3K0GMCMBBW5SQXE6VE
step-storm11CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5F0GV3F1SMEC6269KT
step-storm12CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4B0GQ7PSR74ADPVP4M
step-storm13CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5M0GKFHWZEG0FR3JQA
step-storm14CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ4V0GQMAKTMXKNX9V75
step-storm16CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ5X0GSJZ6DX4P1ERENG
step-storm17CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ620GRPCVH6QV18CNXQ
step-storm18CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ6R0GH4620J7NNZFYWR
step-storm19CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7C0GHQV4XPPJYTMFFT
step-storm20CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJ7F0GVKAMGV8CHM3XQ8
step-storm21CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJAV0GK187EBGPR4TB0B
step-storm22CORRUPTED_EVENT_LOGfailedCORRUPTED_EVENT_LOGwrun_41KYK1CJD40GKN797VCNC2W4XH

Showing 20 of 928 non-completed runs.

@VaguelySerious
VaguelySerious marked this pull request as ready for review July 28, 2026 01:10
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 28, 2026 01:10
@VaguelySerious
VaguelySerious merged commit 04e5ec9 into mainJul 28, 2026
106 of 107 checks passed
@VaguelySerious
VaguelySerious deleted the peter/event-log-corruption-repro branch July 28, 2026 01:12
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 04e5ec9 (AI decision).

This rewrites the opt-in, label-triggered event-log-race-repro diagnostic job around two brand-new storm workflows and a new set of knobs/inputs — that is new investigative tooling built to measure event-guard work on main, not a fix keeping stable buildable, testable, or releasable (the job is not part of the normal CI gate, and its old form still runs fine on stable). The one genuine fix in here is the classifier reading the top-level errorCode instead of error.code, which previously misbucketed every real failure as other; a human could split that out and force it through via workflow_dispatch if the repro job is actually being run against stable.

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

04e5ec987310f9d14997c358b919d0e478a8dc27

pranaygp added a commit that referenced this pull request Jul 28, 2026
…ry-2
* origin/main: (292 commits)
feat(core): seal forwarded stream writes to the owner's public key (#3098)
feat(core): seal hook payloads to the target run's public key (#3096)
[e2e] Rebuild the event-log corruption repro around step-count divergence (#3147)
feat: decrypt sealed payloads in the dashboard and CLI (#3146)
Prewarm only appended replay payloads (#3131)
feat: publish each run's X25519 public key on the run entity (#3095)
feat(core): route sealed envelopes through the serialization layer (#3094)
docs: redirect retired migration-guides URLs to comparisons (#3127)
feat(core): add `encp` sealed-box encryption primitive (#3093)
chore(core): clarify runtime comments (#3111)
Remove obsolete world factory aliases (#3112)
feat(core): deterministic sandbox hardening (#3045)
Remove retired v1 step route plumbing (#3061)
[core] Don't count racing invocations' duplicate step_started events toward the maxRetries ceiling (#3069)
[world-testing] Isolate each spawned test server's data directory (#3055)
fix: upgrade postcss to >=8.5.18 to address GHSA-r28c-9q8g-f849 (#3102)
[next] Respect .gitignore in dev watcher to avoid EMFILE on large monorepos (#3085)
[ci] Backport only stability fixes to `stable`, default to claude-opus-5 (#3092)
perf(core): immediate leading-edge dispatch for idle streams (flush window default 0) (#3088)
Optimize `processImportSpecifier` by computing `shouldFollowImportsFromFile` once per file (#3052)
...
# Conflicts:
#	docs/components/geistdocs/desktop-menu.tsx
#	docs/components/geistdocs/mobile-menu.tsx
#	docs/content/docs/v5/cookbook/advanced/child-workflows.mdx
#	docs/content/docs/v5/cookbook/advanced/upgrading-workflows.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/agent-cancellation.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/durable-agent.mdx
#	docs/content/docs/v5/cookbook/agent-patterns/human-in-the-loop.mdx
#	docs/content/docs/v5/cookbook/common-patterns/batching.mdx
#	docs/content/docs/v5/cookbook/common-patterns/idempotency.mdx
#	docs/content/docs/v5/cookbook/common-patterns/rate-limiting.mdx
#	docs/content/docs/v5/cookbook/common-patterns/saga.mdx
#	docs/content/docs/v5/cookbook/common-patterns/scheduling.mdx
#	docs/content/docs/v5/cookbook/common-patterns/sequential-and-parallel.mdx
#	docs/content/docs/v5/cookbook/common-patterns/timeouts.mdx
#	docs/content/docs/v5/cookbook/common-patterns/webhooks.mdx
#	docs/content/docs/v5/cookbook/common-patterns/workflow-composition.mdx
#	docs/content/docs/v5/cookbook/index.mdx
#	docs/content/docs/v5/cookbook/integrations/ai-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/chat-sdk.mdx
#	docs/content/docs/v5/cookbook/integrations/sandbox.mdx
#	docs/next.config.ts
#	docs/proxy.ts
#	docs/scripts/lint.ts
#	pnpm-lock.yaml
#	pnpm-workspace.yaml
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

event-log-race-reproRun the event log race reproduction job

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@VaguelySerious