Skip to content

Fix three crash-class defects behind this week's flaky CI and tighten the e2e harness - #4119

Draft
pranaygp wants to merge 3 commits into
mainfrom
pranaygp/festive-bardeen-hq4tf9
Draft

Fix three crash-class defects behind this week's flaky CI and tighten the e2e harness#4119
pranaygp wants to merge 3 commits into
mainfrom
pranaygp/festive-bardeen-hq4tf9

Conversation

@pranaygp

@pranaygp pranaygp commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Summary & Motivation

A triage of the last two weeks of tests.yml runs on main and the e2e results comments on 18 PRs (185 retried-to-green entries over 79 tests; 22 of 30 main pushes red since Sept 8) found that most of the "flaky" signal was the harness and the deployment pipeline, but three recurring failures were real defects, two of which crash a host process. This PR fixes those three and the harness defects that #4107, #4115 and #4120 do not already cover.

Real bugs

core: a run that fails before its body starts crashed the host process ~100ms later. runWorkflow creates the session in state running with initialInterruption and registers the structural consumer, which starts walking the event log immediately. The workflow function is only resolved afterwards, so WorkflowNotRegisteredError, a bundle that fails to evaluate, or input that fails to hydrate all throw before waitForExecution attaches the only handler to initialInterruption.promise. If the log holds an ordered event nobody claims (any replay), the consumer's deferred check fires onWorkflowError, whose running branch rejects that un-awaited promise. That is an unhandledRejection, and it is what took down the world-postgres conformance server in Unit Tests (ubuntu) on Sept 10. The same path covers a workflow renamed or removed while runs are in flight on a local or Postgres World. Fix: mark the promise handled up front; Promise.race in waitForExecution still observes the rejection. The new test fails without the one-line fix and passes with it.

world-local: two processes could publish two hook_created events for one hook. storage.test.ts › converges same-hook creation across separate storage instances to one event failed on three main pushes on Windows (Sept 10, Sept 9, Sept 8) with 26 events for 25 creations. The claimer records its candidate slot in the token claim and stays unpinned; a second writer that reads the claim before the first publish commits adopts that slot, publishes there first, and the claimer's collision-and-bump publishes a second event. The token claim lock is now held through the create (released in createImpl's finally), so an adopter can only read a claim whose event is already in the log, or one whose writer crashed and left the lock stale. withHookTokenClaimLock is unchanged for its other callers and now sits on a new acquireHookTokenClaimLock handle.

world-postgres: the LISTEN clients and the owned pool had no error handler. A connection the server closes (restart, failover, idle reaper, or a test container stopping, which is how it surfaced as two uncaught 57P01 errors in the same Sept 10 job) was an uncaught exception. listenChannel now registers a handler, ends the client and reports to its owner; the run-status listener forgets the subscription and retries past its existing backoff, the streamer resubscribes after 5s, and createWorld attaches a no-op error listener to a pool it owns so pg.Pool discards the dead client instead of throwing.

Harness

  • WORKFLOW_E2E_CLI_TIMEOUT_MS: 40000 and WORKFLOW_VERCEL_PROJECT_NAME on the Vercel lanes: CLI children were being killed at the 20s budget while still booting (inspect --withData dying at "Inferring vercel project and team"), which is what put getterStepWorkflow, Calculator.calculate and the other CLI-asserting tests in the flaky list.
  • hookCleanupTestWorkflow gets the 90s budget its sibling multi-round-trip hook tests have: on green lanes it ran at 42–59s locally and 50–74s on Vercel against 60s, so its retries were budget exhaustion.
  • Two Unit Tests (windows-latest) wall-clock assertions that tripped on this PR's own runs are restated as the property they approximated. events-consumer.test.ts › steps over a straggler without waiting out the deferred window (102ms measured against a 100ms window) now asserts the walk stands at the end of the log on the very next tick, with every straggler reported through onDuplicateEvent; a deferred straggler would park the walk until a timer fires. run-return-value-real-world.test.ts (828ms measured against an 800ms bound at the 1s default interval) now runs at a 3s poll interval with the wait path bounded at half of it, so a stall of over a second no longer crosses the bound while the interval path still cannot report before ~3s.
  • Fixture hooks get the same 60s budget as test bodies (hookTimeout in the root vitest.config.ts). Unit Tests (windows-latest) on this PR failed fs.test.ts › cursor optimization with Hook timed out in 10000ms at a beforeEach that writes ten small JSON files. Main's green run had the whole 78-test file at 6.7s; in the same job a sibling file was pushing a thousand events through the same disk (that preload test alone: 101s here, 133s on main's green run). The root config already gave test bodies 60s for exactly this reason and left hooks at vitest's default. No test is skipped or weakened.

Not in this PR: H5 (the spec test's leaked run) landed in #4107; the cliCancel 10s override was removed by #4115, which is now on main (this branch's port of it was dropped on rebase); and #4120 labels every retried test with its lane, world and VM, which supersedes the backend label this PR originally added to the results comment.

Test Plan

  • Rebased onto main at fe8b27d. Two conflicts along the way, each resolved once: .github/scripts/aggregate-e2e-results.js took main's version from test: persist E2E flake history #4120, and packages/world-postgres/src/streamer.ts merges this PR's LISTEN reconnect with [world-postgres] Fix stream reader listener leaks and skipped-chunk offset cursor #4125's reader lifecycle cleanup (one shared closed flag; close() clears the reconnect timer, aborts active readers, then closes the subscription).
  • packages/core: vitest run src/workflow.test.ts src/events-consumer.test.ts src/runtime/run-return-value-real-world.test.ts (148 passed, 1 expected fail from main's suite); tsc --noEmit clean. The new test does not leave an unhandled rejection when the workflow is not registered and the log holds an unconsumed event fails with the guard commented out and passes with it.
  • packages/world-local: full pnpm test (586 passed, 5 skipped), including the cross-instance convergence and lock-compromise tests.
  • packages/world-postgres: tsc --noEmit clean; the docker-free unit tests pass, including [world-postgres] Fix stream reader listener leaks and skipped-chunk offset cursor #4125's streamer.test.ts against the merged file (the pg mock in reenqueue.test.ts gained on). The container-backed suites run in CI.
  • Root vitest.config.ts: hookTimeout resolves to 60000 from packages/world-local (confirmed via createVitest); fs.test.ts 78 passed.
  • Changesets validated with scripts/check-changesets.mjs.

🤖 Generated with Claude Code

https://claude.ai/code/session_012Rw5CkA3EvADg9hpVjS6RP

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f707253

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

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

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

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

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

🛠 Infra Events (absorbed by the harness)

Platform anomalies the e2e harness detected and worked around (e.g. a run the queue never picked up, replaced by a fresh run). Clustered timestamps indicate a backend blip; a steady drip indicates a platform issue worth escalating.

  • cold-start-warmup · suite warmup (tanstack-start) · at 00:41:33Z · abandoned wrun_01M29GZ1V9PCWYQYB0EYZF2VA6
  • run-pickup-stall · Calculator.calculate - static workflow method using static step methods from another class (vite) · at 00:43:06Z · abandoned wrun_01M29H2AQFH8SGGG51C081RNBQ
  • run-pickup-stall · hookWithSleepWorkflow - hook payloads delivered correctly with concurrent sleep (nextjs-webpack) · at 00:46:45Z · abandoned wrun_01M29H91E9EF1AB99RHCPCHBK5

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 3662 0 685 4347
✅ 💻 Local Development 3998 0 510 4508
✅ 📦 Local Production 3998 0 510 4508
✅ 🐘 Local Postgres 3998 0 510 4508
✅ 🪟 Windows 320 0 2 322
✅ 🌐 Cross-language Conformance 68 0 74 142
✅ vercel-http-transport 823 0 143 966
✅ vercel-multi-region 27 0 0 27
✅ vercel-ws-transport 557 0 87 644
Total 17451 0 2521 19972
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 133 0 28
✅ astro-quickjs 133 0 28
✅ example-node 133 0 28
✅ example-quickjs 133 0 28
✅ express-node 133 0 28
✅ express-quickjs 133 0 28
✅ fastify-node 133 0 28
✅ fastify-quickjs 133 0 28
✅ hono-node 133 0 28
✅ hono-quickjs 133 0 28
✅ nest-node 133 0 28
✅ nest-quickjs 133 0 28
✅ nextjs-turbopack-node 158 0 3
✅ nextjs-turbopack-quickjs 158 0 3
✅ nextjs-webpack-node 158 0 3
✅ nextjs-webpack-quickjs 158 0 3
✅ nitro-node 133 0 28
✅ nitro-quickjs 133 0 28
✅ nuxt-node 133 0 28
✅ nuxt-quickjs 133 0 28
✅ python-node 66 0 95
✅ sveltekit-node 152 0 9
✅ sveltekit-quickjs 152 0 9
✅ tanstack-start-node 133 0 28
✅ tanstack-start-quickjs 133 0 28
✅ vite-node 133 0 28
✅ vite-quickjs 133 0 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 134 0 27
✅ astro-stable-quickjs 134 0 27
✅ express-stable-node 134 0 27
✅ express-stable-quickjs 134 0 27
✅ fastify-stable-node 134 0 27
✅ fastify-stable-quickjs 134 0 27
✅ hono-stable-node 134 0 27
✅ hono-stable-quickjs 134 0 27
✅ nest-stable-node 134 0 27
✅ nest-stable-quickjs 134 0 27
✅ nextjs-turbopack-canary-node 160 0 1
✅ nextjs-turbopack-canary-quickjs 160 0 1
✅ nextjs-turbopack-stable-node 160 0 1
✅ nextjs-turbopack-stable-quickjs 160 0 1
✅ nextjs-webpack-canary-node 160 0 1
✅ nextjs-webpack-canary-quickjs 160 0 1
✅ nextjs-webpack-stable-node 160 0 1
✅ nextjs-webpack-stable-quickjs 160 0 1
✅ nitro-stable-node 134 0 27
✅ nitro-stable-quickjs 134 0 27
✅ nuxt-stable-node 134 0 27
✅ nuxt-stable-quickjs 134 0 27
✅ sveltekit-stable-node 153 0 8
✅ sveltekit-stable-quickjs 153 0 8
✅ tanstack-start-node 134 0 27
✅ tanstack-start-quickjs 134 0 27
✅ vite-stable-node 134 0 27
✅ vite-stable-quickjs 134 0 27

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 134 0 27
✅ astro-stable-quickjs 134 0 27
✅ express-stable-node 134 0 27
✅ express-stable-quickjs 134 0 27
✅ fastify-stable-node 134 0 27
✅ fastify-stable-quickjs 134 0 27
✅ hono-stable-node 134 0 27
✅ hono-stable-quickjs 134 0 27
✅ nest-stable-node 134 0 27
✅ nest-stable-quickjs 134 0 27
✅ nextjs-turbopack-canary-node 160 0 1
✅ nextjs-turbopack-canary-quickjs 160 0 1
✅ nextjs-turbopack-stable-node 160 0 1
✅ nextjs-turbopack-stable-quickjs 160 0 1
✅ nextjs-webpack-canary-node 160 0 1
✅ nextjs-webpack-canary-quickjs 160 0 1
✅ nextjs-webpack-stable-node 160 0 1
✅ nextjs-webpack-stable-quickjs 160 0 1
✅ nitro-stable-node 134 0 27
✅ nitro-stable-quickjs 134 0 27
✅ nuxt-stable-node 134 0 27
✅ nuxt-stable-quickjs 134 0 27
✅ sveltekit-stable-node 153 0 8
✅ sveltekit-stable-quickjs 153 0 8
✅ tanstack-start-node 134 0 27
✅ tanstack-start-quickjs 134 0 27
✅ vite-stable-node 134 0 27
✅ vite-stable-quickjs 134 0 27

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 134 0 27
✅ astro-stable-quickjs 134 0 27
✅ express-stable-node 134 0 27
✅ express-stable-quickjs 134 0 27
✅ fastify-stable-node 134 0 27
✅ fastify-stable-quickjs 134 0 27
✅ hono-stable-node 134 0 27
✅ hono-stable-quickjs 134 0 27
✅ nest-stable-node 134 0 27
✅ nest-stable-quickjs 134 0 27
✅ nextjs-turbopack-canary-node 160 0 1
✅ nextjs-turbopack-canary-quickjs 160 0 1
✅ nextjs-turbopack-stable-node 160 0 1
✅ nextjs-turbopack-stable-quickjs 160 0 1
✅ nextjs-webpack-canary-node 160 0 1
✅ nextjs-webpack-canary-quickjs 160 0 1
✅ nextjs-webpack-stable-node 160 0 1
✅ nextjs-webpack-stable-quickjs 160 0 1
✅ nitro-stable-node 134 0 27
✅ nitro-stable-quickjs 134 0 27
✅ nuxt-stable-node 134 0 27
✅ nuxt-stable-quickjs 134 0 27
✅ sveltekit-stable-node 153 0 8
✅ sveltekit-stable-quickjs 153 0 8
✅ tanstack-start-node 134 0 27
✅ tanstack-start-quickjs 134 0 27
✅ vite-stable-node 134 0 27
✅ vite-stable-quickjs 134 0 27

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 160 0 1
✅ nextjs-turbopack-quickjs 160 0 1

✅ 🌐 Cross-language Conformance

App Passed Failed Skipped
✅ python 68 0 74

✅ vercel-http-transport

App Passed Failed Skipped
✅ example 133 0 28
✅ express 133 0 28
✅ hono 133 0 28
✅ nextjs-turbopack 158 0 3
✅ nitro 133 0 28
✅ vite 133 0 28

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

✅ vercel-ws-transport

App Passed Failed Skipped
✅ example 133 0 28
✅ express 133 0 28
✅ nextjs-turbopack 158 0 3
✅ vite 133 0 28

📋 View full workflow run

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit f707253 · Sat, 12 Sep 2026 00:54:34 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1571 (+12%) 1695 🔴 (+6.0%) 1754 🔴 (+7.8%) 1994 🔴 (+15%) 🔻 30
TTFS stream 159 (-15%) 1642 🔴 (+8.5%) 1658 🔴 (+6.9%) 1679 🔴 (+5.9%) 30
TTFS hook + stream 639 (-51%) 💚 2045 🔴 (+14%) 2116 🔴 (+13%) 2203 🔴 (+5.5%) 30
Fan-out TTFS Promise.all(100 steps) 432 (-0.7%) 2060 (+215%) 🔻 2144 (+176%) 🔻 2153 (+16%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 2019 (+3.0%) 3038 (+29%) 🔻 3397 (+44%) 🔻 9106 (+15%) 🔻 10
STSO 1020 steps (inline) 117 (-2.5%) 151 (+6.3%) 170 (+6.3%) 235 (-11%) 1019
WO 1020 steps 154461 (+6.6%) 154461 (+6.6%) 154461 (+6.6%) 154461 (+6.6%) 1
CRTT first chunk (pooled) 61 (-19%) 💚 94 (-32%) 💚 131 (-22%) 💚 516 (+71%) 🔻 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 77 (-40%) 148 (-39%) 223 (-45%) 372 (-58%) 98.5 (-62%) 10
size sweep (100/s, 160B-12KB) 81 (-13%) 144 (-48%) 226 (-34%) 440 (-9%) 146 (-39%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 83 (-34%) 142 (-45%) 187 (-89%) 548 (-83%) 375 (-12%) 3
replay eve-gpt-5.6-sol-2000t (1x) 84.5 (-30%) 139 (-36%) 182 (-47%) 395 (-38%) 418 (-22%) 2
replay eve-gpt-5.6-sol-2000t (2x) 67 (-38%) 220 (-67%) 297 (-75%) 535 (-67%) 256 (-57%) 3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 144758ms → this run 152813ms (Δ +8055ms, +6%)

100-150 ms  ████████████████████┃███  main 856  this 742  -114
150-200 ms  ████░░┃                   main 144  this 253  +109
200-250 ms  ┃                         main   8  this  15    +7
250-300 ms  ┃                         main   3  this   6    +3
300-350 ms  ┃                         main   3  this   0    -3
350-400 ms  ┃                         main   2  this   0    -2
400-450 ms  ┃                         main   2  this   0    -2
450-500 ms  ┃                         main   1  this   2    +1
850-900 ms  ┃                         main   0  this   1    +1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant  RTT 1ms→5s+             avg         p50         p90         p99     n
control  ······▄█▁····    121 (-39%)  112 (-34%)  223 (-45%)  372 (-58%)  3000
sweep    ······▅█▂····    124 (-38%)  115 (-39%)  226 (-34%)   440 (-9%)  3000
gw 1x    ·····▁▅█▁▁···  125.4 (-55%)  113 (-32%)  187 (-89%)  548 (-83%)  5295
eve 1x   ·····▁▆█▂▁···  121.3 (-36%)  104 (-35%)  182 (-47%)  395 (-38%)  5186
eve 2x   ·····▁▂█▃▁···  170.2 (-55%)  153 (-47%)  297 (-75%)  535 (-67%)  7779

RTT over stream progress (avg per tenth of stream, bars scaled min→max):

control  █▅▆▅▄▃▄▁▁▃  105–143ms
sweep    █▃▁▅▇▃▄▅▁▁  109–146ms
gw 1x    ▇▃█▃▁▂▂▁▄▅  106–161ms
eve 1x   ▂▁▅▂▃▃▅▅▄█  104–146ms
eve 2x   ▃▂▂▂▁▁▆█▃▂  146–239ms

RTT by chunk size (avg per log size bin, ~160B → ~12KB serialized, bars scaled min→max):

sweep  ▃▁██▅▅▅  122–125ms

Delivery jitter over stream progress (avg positive CDV per tenth of stream, bars scaled min→max):

control  ▅▃█▅▄▂▄▂▁▄  33–46ms
sweep    ▄▃▆█▅▄█▁▂▁  44–59ms
gw 1x    ▇▃█▁▄▄▄▂▅▅  30–41ms
eve 1x   ▃▂▆▁▇▁▅▃▄█  20–27ms
eve 2x   █▆▂▃▆▂▅▁▃▆  24–30ms
📜 Previous results (2)

f6dbd43

Fri, 11 Sep 2026 23:31:45 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1588 (+41%) 🔻 1679 🔴 (+17%) 🔻 1732 🔴 (+17%) 🔻 1798 🔴 (+6.8%) 30
TTFS stream 165 (-13%) 1663 🔴 (+17%) 🔻 1702 🔴 (+18%) 🔻 1779 🔴 (+18%) 🔻 30
TTFS hook + stream 692 (+19%) 🔻 2085 🔴 (+17%) 🔻 2145 🔴 (+13%) 5905 🔴 (+138%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 488 (+21%) 🔻 776 (-0.5%) 2054 (+16%) 🔻 2105 (+7.9%) 10
Fan-out TTLS Promise.all(100 steps) 2310 (+48%) 🔻 5848 (+53%) 🔻 6330 (+38%) 🔻 11784 (+50%) 🔻 10
STSO 1020 steps (inline) 121 (-2.4%) 145 (-8.2%) 157 (-11%) 230 (+2.7%) 1019
WO 1020 steps 145958 (-7.2%) 145958 (-7.2%) 145958 (-7.2%) 145958 (-7.2%) 1
CRTT first chunk (pooled) 56 (-33%) 💚 112 (-22%) 💚 288 (+34%) 🔻 3439 (+1024%) 🔻 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 75.5 (-43%) 141 (-67%) 189 (-69%) 288 (-72%) 104 (-69%) 10
size sweep (100/s, 160B-12KB) 91.5 (-16%) 154 (-44%) 394 (-7%) 985 (+28%) 141 (-50%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 92 (±0%) 164 (-42%) 286 (-41%) 988 (+6%) 282 (-32%) 3
replay eve-gpt-5.6-sol-2000t (1x) 203 (+44%) 141 (-39%) 174 (-48%) 522 (-36%) 546 (-1%) 2
replay eve-gpt-5.6-sol-2000t (2x) 197 (+84%) 189 (-56%) 295 (-64%) 1014 (-39%) 397 (-44%) 3

3d9ca61

Fri, 11 Sep 2026 20:11:16 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1585 (+51%) 🔻 1678 🔴 (+7.1%) 1692 🔴 (+2.1%) 1773 🔴 (+3.1%) 30
TTFS stream 284 (+57%) 🔻 1679 🔴 (+10%) 1756 🔴 (+13%) 2309 🔴 (+32%) 🔻 30
TTFS hook + stream 924 (-39%) 💚 2201 🔴 (+15%) 2235 🔴 (+12%) 2620 🔴 (+22%) 🔻 30
Fan-out TTFS Promise.all(100 steps) 696 (+27%) 🔻 972 (-0.6%) 1053 (-44%) 💚 2448 (-38%) 💚 10
Fan-out TTLS Promise.all(100 steps) 2158 (-1.1%) 2925 (-48%) 💚 3264 (-59%) 💚 10085 (+25%) 🔻 10
STSO 1020 steps (inline) 117 (-13%) 170 (+6.9%) 191 (+9.8%) 293 (+22%) 🔻 1019
WO 1020 steps 166366 (+4.9%) 166366 (+4.9%) 166366 (+4.9%) 166366 (+4.9%) 1
CRTT first chunk (pooled) 64 (-1.5%) 116 (-5.7%) 229 (-4.2%) 453 (+3.0%) 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 152 (+70%) 178 (-21%) 347 (+4%) 588 (-10%) 168 (-29%) 10
size sweep (100/s, 160B-12KB) 87 (-19%) 176 (-49%) 330 (-39%) 748 (-5%) 198 (-21%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 106 (+20%) 148 (-29%) 233 (-14%) 523 (+3%) 323 (+13%) 3
replay eve-gpt-5.6-sol-2000t (1x) 85 (-55%) 130 (-33%) 166 (-46%) 447 (-39%) 461 (-2%) 2
replay eve-gpt-5.6-sol-2000t (2x) 91 (-25%) 179 (-59%) 234 (-62%) 331 (-64%) 198 (-48%) 3
ℹ️ Metric definitions & methodology

Streams: first-chunk RTT (the stream-open path, before any buffering/backpressure), CRTT percentiles, and worst delivery stall (CDV max). Cells are medians across iterations; per-run values in the artifacts. No 🔴/🟢 marks until targets attach.

The collapsed STSO distribution section above buckets every step gap, split inline (same warm process — pure framework overhead) vs queue-hop (fresh process — dispatch, reinit, replay). = main, = this run, = fill.

The collapsed CRTT drill-down: per-variant RTT histograms (fixed log bins, · = empty) and mean RTT/positive-CDV profile lines over stream progress and chunk size. Histograms, avgs, and profiles merge exactly across runs; p50–p99 are percentile-of-percentiles. Per-index rows live in the artifacts.

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) · Fan-out TTFS: fan-out time to first step (in-deployment start() → first of the parallel step bodies to complete) · Fan-out TTLS: fan-out time to last step (in-deployment start() → last of the parallel step bodies to complete, i.e. when the Promise.all resolves) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · CRTT: chunk round-trip time (per-chunk write → read latency, one clock domain: deployment → stream backend → same deployment) · CDV: chunk delay variation / delivery jitter (inter-arrival gap minus inter-write gap per seq-adjacent pair; skew-free; the row is each run's MAX positive value, so one stall moves it)

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 · Promise.all(100 steps): 100 trivial no-op steps started together in a single Promise.all; Fan-out TTFS is the first of them to complete and Fan-out TTLS the last, both from the in-deployment clientStart, so their gap is the spread the runtime adds across the fan-out · paced control (100/s, 60B): the control: 300 tiny (~60B) deltas metronome-paced at 100/s — zero workload structure, so it reads the transport floor and flush cadence, and disambiguates transport-wide vs workload-specific when a replay row moves · size sweep (100/s, 160B-12KB): same pacing as the control with deltas padded in rotation across seven log-spaced sizes (~160B–12KB) — rotation decouples size from stream position, so it isolates whether chunk size causes latency · replay gateway-gpt-5.4-nano-2000t (1x): raw provider SSE cadence captured at the AI gateway boundary (gpt-5.4-nano, the most popular gateway model; per-token deltas p50 208B = the modal production chunk size), replayed exactly as measured — the typical customer's workload; its CDV is the typical customer's real delivery jitter · replay eve-gpt-5.6-sol-2000t (1x): a captured eve turn (gpt-5.6-sol, the most-used demanding eve model; ~2000 output tokens = production p50 turn length) replayed exactly as measured — eve's envelope protocol re-ships the cumulative message so sizes ramp 142B→13KB; the demanding outlier tenant's reality · replay eve-gpt-5.6-sol-2000t (2x): the same eve capture at 2x — the headroom/stress row; real fast-tier models emit the same chunk sizes at proportionally higher rate, so time compression is a faithful speed model · first chunk (pooled): every run's seq-0 RTT pooled across all stream scenarios — the first chunk precedes any workload differentiation, so pooling samples one shared stream-open path with exact percentiles

Replay cadences (semantic sha256) — eve-gpt-5.6-sol-2000t eaf22f5946e7c61f3c65c7006d550df180cfabd4e706254a09f22aec0cfb420d · gateway-gpt-5.4-nano-2000t 6f24ac518b6b83ff1d0e85a5fe78230db192716d66a7fc6b2fe022752001d041

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600

All timestamps are deployment-side; runs are triggered in-deployment, so the CI runner and api.vercel.com sit outside every measured window. TTFS = start() → first step body (includes dispatch + any cold start); Fan-out TTFS/TTLS = first/last step completion of one Promise.all from the same anchor (the gap is the runtime’s fan-out spread); STSO/WO between step bodies; CRTT inside the workflow (excludes the api.vercel.com read path).

Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 world-sim scenario book — 1 fail of 41 total

fence=per-spec

scenario outcome events virt replay violations
smoke-no-steps completed 3 0ms ok 0
smoke-one-step completed 6 0ms ok 0
hook-at-step-started completed 12 0ms ok 0
hook-at-step-completed completed 12 0ms ok 0
hook-at-hook-created completed 12 0ms ok 0
deadline-hook-wins completed 7 1.0h ok 0
deadline-expires completed 7 1.0h ok 0
long-sleep completed 11 30.0d ok 0
hook-never-arrives stalled 3 0ms skipped 0
step-retries-twice completed 10 2.0s ok 0
parallel-steps completed 9 0ms ok 0
hook-on-execution-state completed 12 0ms ok 0
peek-hook-before-branch completed 12 0ms ok 0
peek-hook-after-branch completed 12 0ms ok 0
peek-hook-at-registration completed 12 0ms ok 0
race-hook-before-probe completed 12 0ms ok 0
race-hook-after-probe completed 12 0ms ok 0
race-duplicate-delivery completed 13 0ms ok 0
attr-hook-before-step completed 11 0ms ok 0
attr-hook-after-step completed 11 0ms ok 0
attr-from-step-body completed 13 0ms ok 0
fork-hook-after-timeout completed 14 1.0m ok 0
fork-hook-before-timeout completed 14 1.0m ok 0
count-hook-after-timeout completed 17 1.0m ok 0
count-hook-before-timeout completed 20 1.0m ok 0
stale-read-step-count-fork completed 20 1.0m ok 0
stale-read-equal-step-counts completed 14 1.0m ok 0
step-vs-step-fork completed 12 0ms ok 0
step-vs-step-fork-fenced completed 12 0ms ok 0
fence-catches-benign-direction completed 12 5ms ok 0
in-flight-before-decision completed 17 1.0m ok 0
in-flight-before-decision-counted completed 17 1.0m ok 0
in-flight-after-decision completed 19 2.0m ok 0
stale-read-step-count-fork-fenced completed 20 1.0m ok 0
fork-hook-wins completed 13 1.0m ok 0
fork-timeout-wins completed 13 1.0m ok 0
unclaimed-payload-under-fork completed 17 1.0m ok 0
claimed-payload-under-fork completed 17 1.0m ok 0
writers-independent-step-bodies completed 12 0ms ok 0
writers-scripted-tempo completed 12 0ms ok 0
cancel-mid-step cancelled 7 0ms skipped 0

Full trace: world-sim.txt

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor
Framework Flow route Step reg. Framework output
hono 250.7 KiB (±0) 93.0 KiB (±0) 1.89 MiB (+537 B)
nextjs-turbopack 257.2 KiB (±0) 426 B (±0) 897.8 KiB (+123 B)
About these numbers

Sizes are gzip; parentheses show the change against main.
Flow route and Step reg. gate this job, on raw bytes rather than the gzip shown, at max(2%, 50.0 KiB). Framework output is informational.

f707253 · run

@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated
example-nextjs-workflow-turbopack Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
example-nextjs-workflow-webpack Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
example-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-astro-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-express-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-fastify-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-hono-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-nestjs-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-nitro-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-nuxt-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-python-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-sveltekit-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-tanstack-start-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workbench-vite-workflow Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workflow-docs Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workflow-swc-playground Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workflow-tarballs Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC
workflow-web Ready Ready Preview, v0 Sep 12, 2026 12:39am UTC

Copy link
Copy Markdown
Contributor Author

CI status on the first run (d7b9005), and what changed in 85a9045:

  • E2E Local Postgres Tests (nest - stable quickjs) failed cancelRun via CLI - cancelling a running workflow on both attempts, each time CLI command failed (killed by signal SIGTERM) with the child still at Initializing world. That is the 10s cliCancel override this PR's report calls H1, which fails the same way on main and on unrelated PRs, and which test: relax Windows CLI cancellation timeout #4115 removes. I ported test: relax Windows CLI cancellation timeout #4115's change into this branch so the lane can go green here; it becomes a no-op once main carries it.
  • Unit Tests (windows-latest) failed run-return-value-real-world.test.ts › resolves as soon as the run finishes, not at the next poll tick with expected 828 to be less than 800. The test completes a run after 300ms and asserts returnValue resolves inside 800ms; nothing in this diff is on that path (the world-local change is confined to the hook_created branch, and the core change only marks a promise handled). It is a wall-clock assertion on a Windows runner and the push above re-runs it; if it fails again on this head I will treat it as real.
  • The Sim World comment's one red scenario, in-flight-before-decision-counted, shows the same result on test: relax Windows CLI cancellation timeout #4115 (a one-line e2e change), so it is the current baseline rather than this PR.

Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

Second run (85a9045) and what 9f2b02f changes:

  • E2E Vercel Prod Tests (nuxt - node) died in pnpm/setup with ECONNRESET from registry.npmjs.org during dependency install, so no test body ran. Not this PR's. A re-run of the failed job from this session was refused (403); the push below starts a fresh run instead.
  • Unit Tests (windows-latest) failed events-consumer.test.ts › steps over a straggler without waiting out the deferred window with expected 102 to be less than 100: a wall-clock bound (finish inside one 100ms deferred window) on a runner that spent 130s importing the suite. The previous head tripped the same lane on a different wall-clock bound (run-return-value-real-world, 828 vs 800). For comparison, main's most recent red on this lane (Sept 10) was storage.test.ts › converges same-hook creation across separate storage instances to one event with 26 events for 25 creations, which is the world-local defect this PR fixes.
  • 9f2b02f restates both bounds as the property they were approximating. The straggler test asserts the walk stands at the end of the log on the very next tick, with every straggler reported through onDuplicateEvent and none through onUnconsumedEvent; a deferred straggler would have parked the walk until a timer fired. The return-value tests run at a 3s poll interval with the wait path bounded at half of it, so a stall of over a second no longer crosses the bound while the interval path still cannot report before ~3s. Both files pass locally; no runtime code changed.

The Sim World row in-flight-before-decision-counted is unchanged from the baseline noted above.


Generated by Claude Code

Copy link
Copy Markdown
Contributor Author

3d9ca61 is green. Tests run 34640542518 passed end to end, including Unit Tests (windows-latest) (the fs.test.ts hook timeout this commit addressed) and E2E Required Check; the e2e results comment shows 17223 passed / 0 failed. World Sim, Biome, Changesets, docs, tarballs and bundle size are green too.

Two corrections to my earlier comments on 9f2b02f:

  • Its e2e results were 17063 passed / 0 failed, so E2E Required Check went red there solely because E2E Local Prod Tests (nextjs-turbopack - stable node) died in pnpm/setup (ECONNRESET from registry.npmjs.org), not because of any test.
  • My comment above says the 3d9ca61 push cancelled that run. It did not: the run had already completed at 19:44:49Z, before the push.

The one non-green check is DCO (action_required on all four commits). The commits are authored as Claude <noreply@anthropic.com>, which is not an org member, so the members: false exemption in .github/dco.yml does not apply. Fixing it means either re-authoring the four commits as the PR author (how #3988 and #3838 passed) or adding a sign-off, and in both cases a force-push of this branch. That changes commit attribution, so I am leaving the choice to the author rather than doing it unilaterally.

One thing the run also shows: with the backend now kept in the flaky list, this run's four retried-to-green entries were addTenWorkflow (nest · local, nitro · local), promiseAllWorkflow (nextjs-webpack · local) and the experimental_retention: 0 test (vite · vercel). The addTenWorkflow retries are on local dev servers, not world-vercel's queue.


Generated by Claude Code

@pranaygp
pranaygp force-pushed the pranaygp/festive-bardeen-hq4tf9 branch from 3d9ca61 to f6dbd43 Compare September 11, 2026 23:12
… the e2e harness

core: a run that fails before its body starts (an unregistered workflow
name, a bundle that fails to evaluate, input that fails to hydrate) left
the events consumer's deferred divergence check armed. When it fired it
rejected an interruption promise nothing had awaited yet, which surfaced
as an unhandledRejection and took the host process down about 100ms
after the flow route had already reported the run as failed. Mark the
promise handled up front and add the regression test.

world-local: hold the Hook token claim lock through the hook_created
publish. The claim records the writer's candidate slot; a second writer
that read it before the first publish committed adopted that slot,
published there first, and the claimer's collision-and-bump then
published a second hook_created for the same hook. This is the Windows
unit failure seen on three main pushes this week.

world-postgres: the dedicated LISTEN clients and the World's own pool had
no error handler, so a connection the server closed was an uncaught
exception. Subscriptions now resubscribe after a backoff and the pool
discards the dead client.

e2e harness: a 40s CLI budget and WORKFLOW_VERCEL_PROJECT_NAME on the
Vercel lanes, and a 90s budget for the two-round-trip hook cleanup test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rw5CkA3EvADg9hpVjS6RP
…ut wall-clock bounds

`Unit Tests (windows-latest)` tripped two elapsed-time assertions on this
PR's own runs: the straggler test measured 102ms against a 100ms deferred
window, and the return-value wait measured 828ms against an 800ms bound.
Both bounds were proxies for a property the runner's load has no business
deciding.

- events-consumer: `subscribe()` queues one synchronous `consume` pass on
  `process.nextTick`, and that pass steps over a straggler in place, while
  deferring one parks the walk until a timer fires. So the walk standing at
  the end of the log on the very next tick, with every straggler reported
  through `onDuplicateEvent` and none through `onUnconsumedEvent`, is the
  property itself rather than a clock reading of it.
- run-return-value-real-world: raise the poll interval to 3s for the file
  and bound the wait path at half of it. A stall of over a second no longer
  pushes the wait path past its bound, and the interval path still cannot
  report before ~3s, so the kill switch stays observable.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rw5CkA3EvADg9hpVjS6RP
`Unit Tests (windows-latest)` on this PR failed
`fs.test.ts > paginatedFileSystemQuery > cursor optimization` with
`Hook timed out in 10000ms` at a `beforeEach` that writes ten small JSON
files. Nothing in that hook is slow: main's green run had the whole
78-test file at 6.7s. The Windows runner runs world-local's
filesystem-heavy files in parallel against one slow temp volume, and in
the same job a sibling file was pushing a thousand events through that
disk (the `run_started` preload test alone took 101s here and 133s on
main's green run).

The root config already gives test bodies 60s for exactly this reason
and left hooks at vitest's 10s default; the e2e utils work around the
gap by passing 30s explicitly. Set `hookTimeout` to match
`testTimeout`, so a fixture doing the same writes as the test it sets
up has the same budget. No test is skipped or weakened by this.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Rw5CkA3EvADg9hpVjS6RP
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants