Skip to content

Bound decoded sparse-array lengths - #3462

Merged
NathanColosimo merged 2 commits into
mainfrom
codex/bound-decoded-collections
Aug 11, 2026
Merged

Bound decoded sparse-array lengths#3462
NathanColosimo merged 2 commits into
mainfrom
codex/bound-decoded-collections

Conversation

@NathanColosimo

@NathanColosimo NathanColosimo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • reject compact sparse arrays above the supported logical length at the main devalue hydration boundary
  • delegate accepted sparse-array construction to devalue
  • cover both current binary payloads and legacy flattened payloads

Why

Compact sparse-array encodings can represent a logical length that is disproportionate to the stored payload. Applying one codec-level bound keeps hydration predictable before downstream consumers process the decoded value.

Impact

Compact sparse arrays with logical lengths above 100,000 now fail hydration with a RangeError. Other payloads are unchanged.

Verification

  • pnpm --filter @workflow/core test — 2,023 passed, 3 expected failures
  • pnpm --filter @workflow/core typecheck
  • pnpm --filter @workflow/core build
  • focused serialization suite — 154 passed
  • direct root-argument, bound-step, and aggregate-error payload checks
  • reuse, quality, and efficiency review

@vercel

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

@changeset-bot

changeset-bot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cc179ac

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

This PR includes changesets to release 16 packages
Name Type
@workflow/core Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web 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 Aug 11, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit cc179ac · Tue, 11 Aug 2026 17:25:37 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 379 (-57%) 💚 1447 🔴 (+18%) 🔻 1476 🔴 (+13%) 1540 🔴 (-15%) 30
TTFS stream 1307 (+25%) 🔻 1378 🔴 (+22%) 🔻 1394 🔴 (+19%) 🔻 1456 🔴 (+17%) 🔻 30
TTFS hook + stream 1537 (+9.1%) 1640 🔴 (+5.4%) 1664 🔴 (+5.2%) 1872 🔴 (+17%) 🔻 30
STSO 1020 steps (inline) 89 (-43%) 💚 122 (-51%) 💚 144 (-54%) 💚 301 (-57%) 💚 1019
WO 1020 steps 124394 (-49%) 💚 124394 (-49%) 💚 124394 (-49%) 💚 124394 (-49%) 💚 1
SL stream latency 84 (-33%) 💚 103 🔴 (-44%) 💚 167 🔴 (-29%) 💚 285 🔴 (-9.5%) 30
SO stream overhead (text) 109 (-26%) 💚 150 (-40%) 💚 172 (-50%) 💚 529 (-11%) 30
SO stream overhead (structured) 107 (-34%) 💚 165 (-45%) 💚 183 (-48%) 💚 212 (-80%) 💚 30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 242330ms → this run 123042ms (Δ -119288ms, -49%)

   50-100 ms  ░┃                        main   0  this  75   +75
  100-150 ms  ░░░░░░░░░░░░░░░░░░░░░░░┃  main   0  this 852  +852
  150-200 ms  █┃████████                main 345  this  61  -284
  200-250 ms  ┃███████████              main 431  this  12  -419
  250-300 ms  ┃██                       main 123  this   8  -115
  300-350 ms  ┃                         main  49  this   8   -41
  350-400 ms  ┃                         main  28  this   0   -28
  400-450 ms  ┃                         main  17  this   1   -16
  450-500 ms  ┃                         main   6  this   0    -6
  500-550 ms  ┃                         main   2  this   0    -2
  550-600 ms  ┃                         main   1  this   1    +0
  600-650 ms  ┃                         main   5  this   1    -4
  650-700 ms  ┃                         main   1  this   0    -1
  700-750 ms  ┃                         main   5  this   0    -5
  750-800 ms  ┃                         main   1  this   0    -1
  800-850 ms  ┃                         main   1  this   0    -1
  900-950 ms  ┃                         main   2  this   0    -2
1050-1100 ms  ┃                         main   1  this   0    -1
1300-1350 ms  ┃                         main   1  this   0    -1
ℹ️ Metric definitions & methodology

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

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

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

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

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

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

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 3466 0 590 4056
✅ 💻 Local Development 3810 0 558 4368
✅ 📦 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 15235 0 2264 17499
Details by Category

✅ ▲ Vercel Production

App Passed Failed Skipped
✅ astro-node 128 0 28
✅ astro-quickjs 128 0 28
✅ example-node 128 0 28
✅ example-quickjs 128 0 28
✅ express-node 128 0 28
✅ express-quickjs 128 0 28
✅ fastify-node 128 0 28
✅ fastify-quickjs 128 0 28
✅ hono-node 128 0 28
✅ hono-quickjs 128 0 28
✅ nest-node 128 0 28
✅ nest-quickjs 128 0 28
✅ nextjs-turbopack-node 153 0 3
✅ nextjs-turbopack-quickjs 153 0 3
✅ nextjs-webpack-node 153 0 3
✅ nextjs-webpack-quickjs 153 0 3
✅ nitro-node 128 0 28
✅ nitro-quickjs 128 0 28
✅ nuxt-node 128 0 28
✅ nuxt-quickjs 128 0 28
✅ sveltekit-node 147 0 9
✅ sveltekit-quickjs 147 0 9
✅ tanstack-start-node 128 0 28
✅ tanstack-start-quickjs 128 0 28
✅ vite-node 128 0 28
✅ vite-quickjs 128 0 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-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 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

🟠 The mint run produced no summary — see the job log.

🟠 The append-only run produced no summary — see the job log.

@NathanColosimo
NathanColosimo merged commit 9add9d7 into main Aug 11, 2026
293 of 297 checks passed
@NathanColosimo
NathanColosimo deleted the codex/bound-decoded-collections branch August 11, 2026 20:27
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 9add9d7 (AI decision).

This is defensive hardening rather than a fix for an observed defect on stable, and it changes existing behavior by making previously-accepted payloads (sparse arrays with logical length > 100,000) fail hydration with a RangeError. It also requires a main-only API: defaultParseOperations/ParseOptions only exist in devalue 5.9.0, while stable pins devalue 5.8.1 (verified via git show origin/stable:packages/core/package.json and the 5.8.1 type definitions), and stable has no packages/core/src/serialization/codec-devalue.ts at all — its devalue parse/unflatten calls are spread across packages/core/src/serialization.ts. If the unbounded-allocation risk is judged real for stable, it needs a separately authored patch plus a dependency bump, not this cherry-pick.

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

9add9d782d0cbf41ce447175c7b9fe47846d794d

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