Repository navigation
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 4e2cf45 The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
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 |
🧪 E2E Test Results✅ All tests passed
|
| Passed | Failed | Skipped | Total | |
|---|---|---|---|---|
| ✅ ▲ Vercel Production | 4649 | 0 | 940 | 5589 |
| ✅ 💻 Local Development | 5272 | 0 | 731 | 6003 |
| ✅ 📦 Local Production | 5272 | 0 | 731 | 6003 |
| ✅ 🐘 Local Postgres | 5417 | 0 | 586 | 6003 |
| ✅ 🪟 Windows | 401 | 0 | 13 | 414 |
| ✅ dynamic-runs | 0 | 0 | 0 | 0 |
| ✅ local-postgres-spec9 | 0 | 0 | 364 | 364 |
| ✅ local-spec9 | 409 | 0 | 5 | 414 |
| ✅ vercel-http-transport | 1054 | 0 | 188 | 1242 |
| ✅ vercel-multi-region | 27 | 0 | 0 | 27 |
| ✅ vercel-queue-hold | 4 | 0 | 0 | 4 |
| ✅ vercel-spec9 | 395 | 0 | 19 | 414 |
| ✅ vercel-ws-transport | 712 | 0 | 116 | 828 |
| Total | 23612 | 0 | 3693 | 27305 |
Details by Category
✅ ▲ Vercel Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-node | 171 | 0 | 36 |
| ✅ astro-quickjs | 170 | 0 | 37 |
| ✅ example-node | 171 | 0 | 36 |
| ✅ example-quickjs | 170 | 0 | 37 |
| ✅ express-node | 171 | 0 | 36 |
| ✅ express-quickjs | 170 | 0 | 37 |
| ✅ fastify-node | 171 | 0 | 36 |
| ✅ fastify-quickjs | 170 | 0 | 37 |
| ✅ hono-node | 171 | 0 | 36 |
| ✅ hono-quickjs | 170 | 0 | 37 |
| ✅ nest-node | 171 | 0 | 36 |
| ✅ nest-quickjs | 170 | 0 | 37 |
| ✅ nextjs-turbopack-node | 199 | 0 | 8 |
| ✅ nextjs-turbopack-quickjs | 198 | 0 | 9 |
| ✅ nextjs-webpack-node | 199 | 0 | 8 |
| ✅ nextjs-webpack-quickjs | 198 | 0 | 9 |
| ✅ nitro-node | 171 | 0 | 36 |
| ✅ nitro-quickjs | 170 | 0 | 37 |
| ✅ nuxt-node | 171 | 0 | 36 |
| ✅ nuxt-quickjs | 170 | 0 | 37 |
| ✅ python-node | 66 | 0 | 141 |
| ✅ sveltekit-node | 190 | 0 | 17 |
| ✅ sveltekit-quickjs | 189 | 0 | 18 |
| ✅ tanstack-start-node | 171 | 0 | 36 |
| ✅ tanstack-start-quickjs | 170 | 0 | 37 |
| ✅ vite-node | 171 | 0 | 36 |
| ✅ vite-quickjs | 170 | 0 | 37 |
✅ 💻 Local Development
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 172 | 0 | 35 |
| ✅ astro-stable-quickjs | 171 | 0 | 36 |
| ✅ express-stable-node | 172 | 0 | 35 |
| ✅ express-stable-quickjs | 171 | 0 | 36 |
| ✅ fastify-stable-node | 172 | 0 | 35 |
| ✅ fastify-stable-quickjs | 171 | 0 | 36 |
| ✅ hono-stable-node | 172 | 0 | 35 |
| ✅ hono-stable-quickjs | 171 | 0 | 36 |
| ✅ nest-stable-node | 172 | 0 | 35 |
| ✅ nest-stable-quickjs | 171 | 0 | 36 |
| ✅ nextjs-turbopack-canary-node | 201 | 0 | 6 |
| ✅ nextjs-turbopack-canary-quickjs | 200 | 0 | 7 |
| ✅ nextjs-turbopack-quickjs-snapshot | 200 | 0 | 7 |
| ✅ nextjs-turbopack-stable-node | 201 | 0 | 6 |
| ✅ nextjs-turbopack-stable-quickjs | 200 | 0 | 7 |
| ✅ nextjs-webpack-canary-node | 201 | 0 | 6 |
| ✅ nextjs-webpack-canary-quickjs | 200 | 0 | 7 |
| ✅ nextjs-webpack-stable-node | 201 | 0 | 6 |
| ✅ nextjs-webpack-stable-quickjs | 200 | 0 | 7 |
| ✅ nitro-stable-node | 172 | 0 | 35 |
| ✅ nitro-stable-quickjs | 171 | 0 | 36 |
| ✅ nuxt-stable-node | 172 | 0 | 35 |
| ✅ nuxt-stable-quickjs | 171 | 0 | 36 |
| ✅ sveltekit-stable-node | 191 | 0 | 16 |
| ✅ sveltekit-stable-quickjs | 190 | 0 | 17 |
| ✅ tanstack-start-node | 172 | 0 | 35 |
| ✅ tanstack-start-quickjs | 171 | 0 | 36 |
| ✅ vite-stable-node | 172 | 0 | 35 |
| ✅ vite-stable-quickjs | 171 | 0 | 36 |
✅ 📦 Local Production
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 172 | 0 | 35 |
| ✅ astro-stable-quickjs | 171 | 0 | 36 |
| ✅ express-stable-node | 172 | 0 | 35 |
| ✅ express-stable-quickjs | 171 | 0 | 36 |
| ✅ fastify-stable-node | 172 | 0 | 35 |
| ✅ fastify-stable-quickjs | 171 | 0 | 36 |
| ✅ hono-stable-node | 172 | 0 | 35 |
| ✅ hono-stable-quickjs | 171 | 0 | 36 |
| ✅ nest-stable-node | 172 | 0 | 35 |
| ✅ nest-stable-quickjs | 171 | 0 | 36 |
| ✅ nextjs-turbopack-canary-node | 201 | 0 | 6 |
| ✅ nextjs-turbopack-canary-quickjs | 200 | 0 | 7 |
| ✅ nextjs-turbopack-quickjs-snapshot | 200 | 0 | 7 |
| ✅ nextjs-turbopack-stable-node | 201 | 0 | 6 |
| ✅ nextjs-turbopack-stable-quickjs | 200 | 0 | 7 |
| ✅ nextjs-webpack-canary-node | 201 | 0 | 6 |
| ✅ nextjs-webpack-canary-quickjs | 200 | 0 | 7 |
| ✅ nextjs-webpack-stable-node | 201 | 0 | 6 |
| ✅ nextjs-webpack-stable-quickjs | 200 | 0 | 7 |
| ✅ nitro-stable-node | 172 | 0 | 35 |
| ✅ nitro-stable-quickjs | 171 | 0 | 36 |
| ✅ nuxt-stable-node | 172 | 0 | 35 |
| ✅ nuxt-stable-quickjs | 171 | 0 | 36 |
| ✅ sveltekit-stable-node | 191 | 0 | 16 |
| ✅ sveltekit-stable-quickjs | 190 | 0 | 17 |
| ✅ tanstack-start-node | 172 | 0 | 35 |
| ✅ tanstack-start-quickjs | 171 | 0 | 36 |
| ✅ vite-stable-node | 172 | 0 | 35 |
| ✅ vite-stable-quickjs | 171 | 0 | 36 |
✅ 🐘 Local Postgres
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-stable-node | 177 | 0 | 30 |
| ✅ astro-stable-quickjs | 176 | 0 | 31 |
| ✅ express-stable-node | 177 | 0 | 30 |
| ✅ express-stable-quickjs | 176 | 0 | 31 |
| ✅ fastify-stable-node | 177 | 0 | 30 |
| ✅ fastify-stable-quickjs | 176 | 0 | 31 |
| ✅ hono-stable-node | 177 | 0 | 30 |
| ✅ hono-stable-quickjs | 176 | 0 | 31 |
| ✅ nest-stable-node | 177 | 0 | 30 |
| ✅ nest-stable-quickjs | 176 | 0 | 31 |
| ✅ nextjs-turbopack-canary-node | 206 | 0 | 1 |
| ✅ nextjs-turbopack-canary-quickjs | 205 | 0 | 2 |
| ✅ nextjs-turbopack-quickjs-snapshot | 205 | 0 | 2 |
| ✅ nextjs-turbopack-stable-node | 206 | 0 | 1 |
| ✅ nextjs-turbopack-stable-quickjs | 205 | 0 | 2 |
| ✅ nextjs-webpack-canary-node | 206 | 0 | 1 |
| ✅ nextjs-webpack-canary-quickjs | 205 | 0 | 2 |
| ✅ nextjs-webpack-stable-node | 206 | 0 | 1 |
| ✅ nextjs-webpack-stable-quickjs | 205 | 0 | 2 |
| ✅ nitro-stable-node | 177 | 0 | 30 |
| ✅ nitro-stable-quickjs | 176 | 0 | 31 |
| ✅ nuxt-stable-node | 177 | 0 | 30 |
| ✅ nuxt-stable-quickjs | 176 | 0 | 31 |
| ✅ sveltekit-stable-node | 196 | 0 | 11 |
| ✅ sveltekit-stable-quickjs | 195 | 0 | 12 |
| ✅ tanstack-start-node | 177 | 0 | 30 |
| ✅ tanstack-start-quickjs | 176 | 0 | 31 |
| ✅ vite-stable-node | 177 | 0 | 30 |
| ✅ vite-stable-quickjs | 176 | 0 | 31 |
✅ 🪟 Windows
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 201 | 0 | 6 |
| ✅ nextjs-turbopack-quickjs | 200 | 0 | 7 |
✅ dynamic-runs
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ astro-vercel | 0 | 0 | 0 |
| ✅ example-vercel | 0 | 0 | 0 |
| ✅ express-vercel | 0 | 0 | 0 |
| ✅ fastify-vercel | 0 | 0 | 0 |
| ✅ hono-vercel | 0 | 0 | 0 |
| ✅ nest-vercel | 0 | 0 | 0 |
| ✅ nextjs-turbopack-vercel | 0 | 0 | 0 |
| ✅ nextjs-webpack-vercel | 0 | 0 | 0 |
| ✅ nitro-vercel | 0 | 0 | 0 |
| ✅ nuxt-vercel | 0 | 0 | 0 |
| ✅ sveltekit-vercel | 0 | 0 | 0 |
| ✅ tanstack-start-vercel | 0 | 0 | 0 |
| ✅ vite-vercel | 0 | 0 | 0 |
✅ local-postgres-spec9
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-stable-node | 0 | 0 | 182 |
| ✅ nextjs-turbopack-stable-quickjs | 0 | 0 | 182 |
✅ local-spec9
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 205 | 0 | 2 |
| ✅ nextjs-turbopack-quickjs | 204 | 0 | 3 |
✅ vercel-http-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 171 | 0 | 36 |
| ✅ express | 171 | 0 | 36 |
| ✅ hono | 171 | 0 | 36 |
| ✅ nextjs-turbopack | 199 | 0 | 8 |
| ✅ nitro | 171 | 0 | 36 |
| ✅ vite | 171 | 0 | 36 |
✅ vercel-multi-region
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack | 27 | 0 | 0 |
✅ vercel-queue-hold
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack | 4 | 0 | 0 |
✅ vercel-spec9
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ nextjs-turbopack-node | 198 | 0 | 9 |
| ✅ nextjs-turbopack-quickjs | 197 | 0 | 10 |
✅ vercel-ws-transport
| App | Passed | Failed | Skipped |
|---|---|---|---|
| ✅ example | 171 | 0 | 36 |
| ✅ express | 171 | 0 | 36 |
| ✅ nextjs-turbopack | 199 | 0 | 8 |
| ✅ vite | 171 | 0 | 36 |
📊 Workflow Benchmarkscommit Backend:
📈 STSO distribution vs main (inline / queue-hop histograms)1020 steps (inline) Cumulative STSO time: main 113704ms → this run 101475ms (Δ -12229ms, -11%) 📜 Previous results (4)2d04a32Fri, 13 Nov 2026 19:42:18 GMT · run logs
a86b822Fri, 06 Nov 2026 01:22:05 GMT · run logs
e3278e0Wed, 23 Sep 2026 20:28:19 GMT · run logs
e3278e0Wed, 23 Sep 2026 19:57:16 GMT · run logs
ℹ️ Metric definitions & methodologyThe 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). Best/P75/P90/P99 deltas compare against the most recent benchmark run on 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) 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 🔴 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 = Cold starts stay in the numbers (real bursty-workload latency, inflates P75+); Best is the warm floor. |
Sim WorldSimulated world deterministic testing for races. Traces 🟢 world-sim scenario book — 0 fail of 45 total
Full trace: |
About these numbersSizes are gzip; parentheses show the change against
|
…tail Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
|
yo pranay, long time! funny running into your work here. i've been deep in durable/replay/agent infra lately and ended up reading through #4306 / #4334. reading through the patch, the bounded reader-side containment makes sense. one edge case from the production evidence: probably fine for this bounded patch, but multi-hole recovery feels worth a follow-up, even if it's just making the error/consumer path cheap to iterate across consecutive gaps. happy to put together a focused consecutive-hole regression test if useful |
…urable-run-streams-lose-ack Co-Authored-By: Peter Wielander <29887157+VaguelySerious@users.noreply.github.com>
…ole stall A session at the resume index that is cut partway through a large chunk received bytes but completed no frame, so it was counted as a stall below the tail. Three such cuts in a row failed a healthy read with "the chunk is missing from storage". Bytes arriving show the server is serving the chunk, so treat them as progress. Also cover the consecutive-stall reset. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Peter Wielander <29887157+VaguelySerious@users.noreply.github.com>
|
I pushed two commits:
Left as is: a session that fails right after its headers, with zero bytes, while the reader is behind the tail still counts as a stall. So does a session that ends with a clean EOF the same way. Three of these in quick succession, for example during a brief server blip, would produce the hole error instead of retrying. A minimum session duration would tell these apart from the ~2 min hole sessions, but it adds a tunable. I didn't add it here. |
Co-Authored-By: Peter Wielander <29887157+VaguelySerious@users.noreply.github.com>
…ent holes A zero-byte session below the tail now counts toward the hole stall cap only if it stayed open for at least 10 seconds. A session at a hole is held open until the server's max response duration (about two minutes), so a session that ends sooner is a brief blip: it is retried against the general reconnect budget as before, instead of three of them failing the read within a second. Drop the WORKFLOW_FRAMED_STREAM_MAX_STALLS_BELOW_TAIL override and its docs entry; the stall cap is a constant. Add a regression test for two adjacent holes: resuming at index + 1 lands on the second hole, fails the same bounded way naming that index, and resuming past it reads the rest of the stream. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Peter Wielander <29887157+VaguelySerious@users.noreply.github.com>
|
I pushed two commits:
Quick zero-byte sessions (the leftover risk from my last comment). A zero-byte session below the tail now counts as a stall only if it stayed open for at least 10 seconds ( Adjacent holes. A resume at Configuration. I removed Smaller changes: the
CI on |
Addresses the reader-side part of #4306. It does not close the issue, because the chunk loss itself is not fixed here.
Problem
When a chunk below a stream's known tail is missing from storage, every live-read session opened at that index ends without delivering a frame.
createReconnectingFramedStreamreconnects from the same index until its consecutive-reconnect budget (50) runs out. Each such session lasts the server's max response duration (~2 min), so a reader sits on the hole for well over an hour before it errors, with a generic "exceeded maximum reconnection attempts" message.Fix
When a session stays open for at least 10 seconds and ends (clean EOF or errored body) without receiving any bytes, the reader checks
streams.getInfo. If the tail is at or past the resume index, the chunk is known to exist but was not served, so this isn't an idle wait for new data. After 3 consecutive stalls like this, the read fails with aStreamErrorthat names the missing index and the tail, and says to resume fromstartIndex= index + 1.startpart) can corrupt consumer state. Failing with the index lets the caller decide.This does not restore lost chunks. The storage-side loss reported in #4306 is outside this repo.
Tests
packages/core/src/reconnecting-framed-stream.test.ts: new cases for a hole ending in a clean EOF and in an errored body, which fail fast with the index; for two adjacent holes, where resuming at index + 1 fails the same bounded way at the second hole and resuming past it reads the rest; for quick zero-byte sessions, partial-frame cuts and non-consecutive stalls, which don't fail the read; and for an idle reader at the tail, which keeps reconnecting. The existing budget-exhaustion test now uses an empty stream (tail -1), since its old fixture (tail 4 at index 0) is a hole.🤖 Generated with Claude Code