Skip to content

fix: resume open streams from tail cursors - #3430

Open
NathanColosimo wants to merge 13 commits into
codex/stream-tail-worldsfrom
codex/stream-tail-cursor
Open

fix: resume open streams from tail cursors#3430
NathanColosimo wants to merge 13 commits into
codex/stream-tail-worldsfrom
codex/stream-tail-cursor

Conversation

@NathanColosimo

@NathanColosimo NathanColosimo commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Description

Update core and dashboard stream consumers to page while hasMore is true, then retain the final open-tail cursor for the next poll.

The consumers no longer request an overlapping page or skip duplicate frames. Each returned chunk is consumed exactly once, and a later poll resumes after the last delivered chunk.

This PR consumes the versioned World contract and matching package constraints from #3475.

Stacked on #3475.

How did you test your changes?

  • Full affected dependency typecheck passed.
  • Web tests cover multi-page reads and open-tail polling.
  • Core pagination coverage terminates on hasMore rather than treating every resume cursor as another available page.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
@NathanColosimo
NathanColosimo requested a review from a team as a code owner August 10, 2026 21:11
@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc1cb8e

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

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

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

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

@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview Aug 13, 2026 11:24pm
example-nextjs-workflow-webpack Ready Ready Preview Aug 13, 2026 11:24pm
example-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-astro-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-express-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-fastify-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-hono-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-nestjs-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-nitro-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-nuxt-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-python-workflow Error Error Aug 13, 2026 11:24pm
workbench-sveltekit-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-tanstack-start-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workbench-vite-workflow Ready Ready Preview Aug 13, 2026 11:24pm
workflow-docs Ready Ready Preview, v0 Aug 13, 2026 11:24pm
workflow-swc-playground Ready Ready Preview Aug 13, 2026 11:24pm
workflow-tarballs Ready Ready Preview Aug 13, 2026 11:24pm
workflow-web Ready Ready Preview Aug 13, 2026 11:24pm

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit cc1cb8e · Thu, 13 Aug 2026 23:43:13 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 241 (-37%) 💚 1391 🔴 (+25%) 🔻 1408 🔴 (+24%) 🔻 1465 🔴 (-4.4%) 30
TTFS stream 250 (-75%) 💚 1397 🔴 (+32%) 🔻 1436 🔴 (+34%) 🔻 1476 🔴 (+33%) 🔻 30
TTFS hook + stream 416 (-67%) 💚 1664 🔴 (+20%) 🔻 1733 🔴 (+21%) 🔻 1836 🔴 (+13%) 30
Fan-out TTFS Promise.all(100 steps) 6842 (-23%) 💚 10524 (+5.7%) 10557 (+4.7%) 10564 (-22%) 💚 10
Fan-out TTLS Promise.all(100 steps) 16863 (-4.6%) 19283 (+2.2%) 19370 (+1.9%) 19586 (-16%) 💚 10
STSO 1020 steps (inline) 134 (+8.9%) 190 (-0.5%) 215 (-6.1%) 328 (-43%) 💚 1019
WO 1020 steps 184315 (-5.7%) 184315 (-5.7%) 184315 (-5.7%) 184315 (-5.7%) 1
SL stream latency 88 (+11%) 144 🔴 (+31%) 🔻 188 🔴 (+46%) 🔻 267 🔴 (-22%) 💚 30
SO stream overhead (text) 127 (+14%) 297 🔴 (+65%) 🔻 470 (+128%) 🔻 990 (+64%) 🔻 30
SO stream overhead (structured) 134 (+40%) 🔻 251 🔴 (+61%) 🔻 344 (+106%) 🔻 900 (+395%) 🔻 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 194368ms → this run 184115ms (Δ -10253ms, -5%)

  100-150 ms  ┃████                     main 180  this  47  -133
  150-200 ms  ███████████████████░░░░┃  main 627  this 805  +178
  200-250 ms  ███┃                      main 134  this 126    -8
  250-300 ms  ┃                         main  29  this  27    -2
  300-350 ms  ┃                         main  15  this   7    -8
  350-400 ms  ┃                         main  11  this   3    -8
  400-450 ms  ┃                         main   4  this   1    -3
  450-500 ms  ┃                         main   5  this   3    -2
  500-550 ms  ┃                         main   3  this   0    -3
  550-600 ms  ┃                         main   1  this   0    -1
  600-650 ms  ┃                         main   5  this   0    -5
  650-700 ms  ┃                         main   1  this   0    -1
  750-800 ms  ┃                         main   1  this   0    -1
  800-850 ms  ┃                         main   1  this   0    -1
1100-1150 ms  ┃                         main   1  this   0    -1
4450-4500 ms  ┃                         main   1  this   0    -1
📜 Previous results (5)

32fdf03

Thu, 13 Aug 2026 00:15:56 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 231 (-4.1%) 1363 🔴 (+19%) 🔻 1406 🔴 (+20%) 🔻 1510 🔴 (+19%) 🔻 30
TTFS stream 1301 (+375%) 🔻 1357 🔴 (+16%) 🔻 1386 🔴 (+14%) 1475 🔴 (-5.9%) 30
TTFS hook + stream 1527 (+282%) 🔻 1640 🔴 (+18%) 🔻 1684 🔴 (+19%) 🔻 1788 🔴 (+20%) 🔻 30
STSO 1020 steps (inline) 101 (-11%) 162 (-23%) 💚 182 (-28%) 💚 325 (-22%) 💚 1019
WO 1020 steps 161013 (-23%) 💚 161013 (-23%) 💚 161013 (-23%) 💚 161013 (-23%) 💚 1
SL stream latency 80 (-18%) 💚 109 🔴 (-32%) 💚 158 🔴 (-27%) 💚 236 🔴 (-37%) 💚 30
SO stream overhead (text) 103 (-31%) 💚 157 (-45%) 💚 193 (-68%) 💚 389 (-50%) 💚 30
SO stream overhead (structured) 101 (-23%) 💚 178 (-33%) 💚 199 (-45%) 💚 288 (-99%) 💚 30

9704155

Wed, 12 Aug 2026 20:58:21 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 275 (-72%) 💚 1340 🔴 (+20%) 🔻 1374 🔴 (+14%) 1519 🔴 (-5.9%) 30
TTFS stream 1292 (+22%) 🔻 1350 🔴 (+20%) 🔻 1370 🔴 (+19%) 🔻 1503 🔴 (+25%) 🔻 30
TTFS hook + stream 1555 (+226%) 🔻 1649 🔴 (+14%) 1698 🔴 (+14%) 1748 🔴 (+5.9%) 30
STSO 1020 steps (inline) 118 (+12%) 166 (-1.8%) 194 (-3.0%) 354 (-2.7%) 1019
WO 1020 steps 166903 (±0%) 166903 (±0%) 166903 (±0%) 166903 (±0%) 1
SL stream latency 85 (-3.4%) 112 🔴 (-14%) 142 🔴 (-29%) 💚 543 🔴 (+75%) 🔻 30
SO stream overhead (text) 110 (-11%) 150 (-29%) 💚 247 (-23%) 💚 503 (-62%) 💚 30
SO stream overhead (structured) 107 (+10%) 171 (-21%) 💚 198 (-21%) 💚 282 (-98%) 💚 30

62fa152

Wed, 12 Aug 2026 17:34:02 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1375 (+35%) 🔻 1610 🔴 (+49%) 🔻 1660 🔴 (+46%) 🔻 1692 🔴 (+3.7%) 30
TTFS stream 254 (-2.3%) 1472 🔴 (+38%) 🔻 1517 🔴 (+39%) 🔻 1855 🔴 (+51%) 🔻 30
TTFS hook + stream 1595 (+262%) 🔻 1760 🔴 (+27%) 🔻 1800 🔴 (+26%) 🔻 1944 🔴 (+21%) 🔻 30
STSO 1020 steps (inline) 130 (+19%) 🔻 186 (+0.5%) 217 (+0.9%) 359 (-8.2%) 1019
WO 1020 steps 184896 (+2.4%) 184896 (+2.4%) 184896 (+2.4%) 184896 (+2.4%) 1
SL stream latency 104 (+12%) 139 🔴 (-5.4%) 175 🔴 (+14%) 415 🔴 (+90%) 🔻 30
SO stream overhead (text) 131 (+21%) 🔻 247 (+7.9%) 353 (+23%) 🔻 486 (-49%) 💚 30
SO stream overhead (structured) 136 (+7.9%) 207 (-20%) 💚 229 (-24%) 💚 308 (-65%) 💚 30

103e15e

Tue, 11 Aug 2026 21:58:08 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 223 (-31%) 💚 1533 🔴 (+45%) 🔻 1582 🔴 (+41%) 🔻 1812 🔴 (+30%) 🔻 30
TTFS stream 232 (+46%) 🔻 1460 🔴 (+36%) 🔻 1530 🔴 (+40%) 🔻 1636 🔴 (+32%) 🔻 30
TTFS hook + stream 377 (-72%) 💚 1736 🔴 (+19%) 🔻 1814 🔴 (+21%) 🔻 2016 🔴 (+28%) 🔻 30
STSO 1020 steps (inline) 107 (-16%) 💚 212 (+12%) 251 (+16%) 🔻 433 (+2.4%) 1019
WO 1020 steps 210749 (+14%) 210749 (+14%) 210749 (+14%) 210749 (+14%) 1
SL stream latency 101 (+16%) 🔻 177 🔴 (+39%) 🔻 277 🔴 (+102%) 🔻 332 🔴 (-31%) 💚 30
SO stream overhead (text) 141 (+31%) 🔻 278 🔴 (+38%) 🔻 338 (+8.0%) 974 (+105%) 🔻 30
SO stream overhead (structured) 136 (+27%) 🔻 382 🔴 (+116%) 🔻 448 (+87%) 🔻 1006 🔴 (+128%) 🔻 30

1b6891b

Tue, 11 Aug 2026 20:59:14 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 303 (-6.2%) 1436 🔴 (+36%) 🔻 1484 🔴 (+33%) 🔻 1819 🔴 (+30%) 🔻 30
TTFS stream 1396 (+778%) 🔻 1463 🔴 (+36%) 🔻 1514 🔴 (+39%) 🔻 1564 🔴 (+26%) 🔻 30
TTFS hook + stream 1576 (+18%) 🔻 1710 🔴 (+17%) 🔻 1753 🔴 (+17%) 🔻 1975 🔴 (+26%) 🔻 30
STSO 1020 steps (inline) 126 (-0.8%) 160 (-15%) 💚 181 (-16%) 💚 273 (-35%) 💚 1019
WO 1020 steps 160808 (-13%) 160808 (-13%) 160808 (-13%) 160808 (-13%) 1
SL stream latency 87 (±0%) 122 🔴 (-3.9%) 156 🔴 (+14%) 225 🔴 (-54%) 💚 30
SO stream overhead (text) 103 (-4.6%) 180 (-10%) 419 (+34%) 🔻 615 (+29%) 🔻 30
SO stream overhead (structured) 112 (+4.7%) 190 (+7.3%) 233 (-2.9%) 3999 🔴 (+807%) 🔻 30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · 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) · 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 · 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 · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. Fan-out TTFS/TTLS are the first and last step completions of a single Promise.all over trivial steps, from the same anchor, so the gap between the two rows is the spread the runtime adds across the fan-out. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

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

@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

❌ Failed E2E Tests

▲ Vercel Production (27 failed)

astro-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

astro-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

example-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

example-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

express-node (2 failed):

  • outputStreamWorkflow positive startIndex (skips first chunk)
  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

express-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

fastify-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

fastify-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

hono-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

hono-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nest-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nest-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nextjs-turbopack-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nextjs-turbopack-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nextjs-webpack-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nextjs-webpack-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nitro-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nitro-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nuxt-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

nuxt-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

sveltekit-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

sveltekit-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

tanstack-start-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

tanstack-start-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

vite-node (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

vite-quickjs (1 failed):

  • outputStreamWorkflow - getTailIndex and getChunks getChunks returns same content as reading the stream

E2E Test Summary

Summary
Passed Failed Skipped Total
❌ ▲ Vercel Production 3439 27 590 4056
✅ 💻 Local Development 3673 0 539 4212
✅ 📦 Local Production 3810 0 558 4368
✅ 🐘 Local Postgres 3810 0 558 4368
✅ 🪟 Windows 312 0 0 312
✅ vercel-multi-region 27 0 0 27
Total 15071 27 2245 17343
Details by Category

❌ ▲ Vercel Production

App Passed Failed Skipped
❌ astro-node 127 1 28
❌ astro-quickjs 127 1 28
❌ example-node 127 1 28
❌ example-quickjs 127 1 28
❌ express-node 126 2 28
❌ express-quickjs 127 1 28
❌ fastify-node 127 1 28
❌ fastify-quickjs 127 1 28
❌ hono-node 127 1 28
❌ hono-quickjs 127 1 28
❌ nest-node 127 1 28
❌ nest-quickjs 127 1 28
❌ nextjs-turbopack-node 152 1 3
❌ nextjs-turbopack-quickjs 152 1 3
❌ nextjs-webpack-node 152 1 3
❌ nextjs-webpack-quickjs 152 1 3
❌ nitro-node 127 1 28
❌ nitro-quickjs 127 1 28
❌ nuxt-node 127 1 28
❌ nuxt-quickjs 127 1 28
❌ sveltekit-node 146 1 9
❌ sveltekit-quickjs 146 1 9
❌ tanstack-start-node 127 1 28
❌ tanstack-start-quickjs 127 1 28
❌ vite-node 127 1 28
❌ vite-quickjs 127 1 28

✅ 💻 Local Development

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 📦 Local Production

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 🐘 Local Postgres

App Passed Failed Skipped
✅ astro-stable-node 130 0 26
✅ astro-stable-quickjs 130 0 26
✅ express-stable-node 130 0 26
✅ express-stable-quickjs 130 0 26
✅ fastify-stable-node 130 0 26
✅ fastify-stable-quickjs 130 0 26
✅ hono-stable-node 130 0 26
✅ hono-stable-quickjs 130 0 26
✅ nest-stable-node 130 0 26
✅ nest-stable-quickjs 130 0 26
✅ nextjs-turbopack-canary-node 137 0 19
✅ nextjs-turbopack-canary-quickjs 137 0 19
✅ nextjs-turbopack-stable-node 156 0 0
✅ nextjs-turbopack-stable-quickjs 156 0 0
✅ nextjs-webpack-canary-node 137 0 19
✅ nextjs-webpack-canary-quickjs 137 0 19
✅ nextjs-webpack-stable-node 156 0 0
✅ nextjs-webpack-stable-quickjs 156 0 0
✅ nitro-stable-node 130 0 26
✅ nitro-stable-quickjs 130 0 26
✅ nuxt-stable-node 130 0 26
✅ nuxt-stable-quickjs 130 0 26
✅ sveltekit-stable-node 149 0 7
✅ sveltekit-stable-quickjs 149 0 7
✅ tanstack-start-node 130 0 26
✅ tanstack-start-quickjs 130 0 26
✅ vite-stable-node 130 0 26
✅ vite-stable-quickjs 130 0 26

✅ 🪟 Windows

App Passed Failed Skipped
✅ nextjs-turbopack-node 156 0 0
✅ nextjs-turbopack-quickjs 156 0 0

✅ vercel-multi-region

App Passed Failed Skipped
✅ nextjs-turbopack 27 0 0

📋 View full workflow run

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Sim World

Simulated world deterministic testing for races. Traces

🟠 Mint-ordered log — 3 fail of 41 total

log=mint-ordered · 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 failed 9 1.0m MISMATCH 1
in-flight-before-decision-counted failed 9 1.0m MISMATCH 1
in-flight-after-decision failed 9 1.0m MISMATCH 1
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-mint.txt

🟢 Append-only log — 0 fail of 41 total

log=append-only · 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-append-only.txt

@VaguelySerious VaguelySerious left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: blocking issues found

if (newChunks.length > 0 && mounted) {
setChunks((prev) => [...prev, ...newChunks]);
serverCursorRef.current =
pollResult.cursor ?? serverCursorRef.current;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Blocking

Dropping skipFrames is the correct pairing for the World contract in #3475, and the initial-load and poll paths here are internally consistent. The problem is that it is correct only against that contract, and @workflow/web and the World packages are separately versioned, so users can land on a mismatched pair.

Against a World still on the old contract, the open tail returns cursor: null. readStreamChunksServerAction then reports cursor: null, this hook leaves serverCursorRef.current at null, and the next poll re-reads from index 0 with nothing skipping the frames already rendered, so the whole stream is appended again every POLL_INTERVAL_MS. I ran the new pair against an old-contract World: 3 chunks render, one poll leaves 7 rendered entries, a second leaves 12. It grows quadratically in poll count for the life of the run, and the failure mode is duplicated output rather than an error, so nothing surfaces it.

"Old contract" is not hypothetical here. It is any pinned @workflow/world-local or @workflow/world-postgres older than #3475, plus every community World implementing what docs/content/worlds/v4/building-a-world.mdx and the v5 copy currently document.

Two ways out. Make the cursor change additive in #3475, adding a new resumeCursor field and leaving cursor alone, so both pairings work and this hook can prefer the new field when present. Or declare the coupling: a peer or version floor on the World packages, and a minor rather than a patch. As written the changeset is a patch on @workflow/web alone, which is the one thing that cannot express the dependency.

Worth noting the reverse skew is already handled well: readStreamChunksServerAction only throws when hasMore && !result.cursor, which an old-contract World never produces. So the tolerance is there, it just is not enough on its own.

cursor = page.cursor;
if (!page.hasMore) {
hasMore = page.hasMore;
if (!hasMore) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Note

if (!hasMore) expect(page.done).toBe(true) asserts an implication the contract explicitly disclaims. The getChunks docs already say new chunks may appear in future requests even after hasMore is false, and this PR's premise is that hasMore: false on an open stream is a normal terminal state for a page loop. The assertion only passes here because the stream is already closed before the loop runs.

That makes it a trap for whoever copies this loop against a live stream, and it does not add coverage beyond the expect(paginatedChunks).toHaveLength(streamChunks.length) below it. I would drop it, or hoist it to a single assertion after the loop with a comment that the stream was closed first.

Since this is the loop shape users will copy, it would also be worth asserting the open-stream case somewhere: read a stream that is still open, confirm hasMore goes false with a non-null cursor, write another chunk, and confirm the cursor resumes after the delivered chunks. That is the behavior the whole stack turns on and the e2e suite does not cover it yet.

for (const chunk of allChunks) {
totalSize += chunk.length;
}
cursor = result.cursor ?? cursor;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Review: Note

cursor = result.cursor ?? cursor is the load-bearing line of the new contract. It is what stops an idle poll, which returns an empty page with cursor: null, from resetting the reader to index 0 and replaying the stream. Same for cursor: streamDone ? null : (cursor ?? null) below.

There is no test for either. The web suite covers readStream transport and hook status, not this retention. A test that polls twice with no new chunks in between and asserts the returned cursor is unchanged would pin down the behavior everything else depends on. I wrote that case against world-local while reviewing and it passes, so this is about locking it in rather than a defect: it is the most likely thing to regress silently later, because the symptom is duplicated output rather than a failure.

if (newChunks.length > 0 && mounted) {
setChunks((prev) => [...prev, ...newChunks]);
serverCursorRef.current =
pollResult.cursor ?? serverCursorRef.current;

@vercel vercel Bot Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A @workflow/web reader relying solely on the new tail-cursor contract silently and quadratically duplicates open-stream output when paired with an older dynamically-resolved World package that returns cursor: null at the open-stream tail.

Fix on Vercel

@vercel vercel Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

A bidirectional workflow@workflow/world-* peer-dependency scheme introduces a cyclic workspace dependency graph that turbo run build hard-rejects, failing every workbench deployment.

Fix on Vercel

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