Skip to content

[core] QuickJS engine reports its log position and consumes returned event pages; document who names a position - #4106

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/quickjs-slot-report-parity
Sep 11, 2026
Merged

VaguelySerious merged 3 commits into
mainfrom
peter/quickjs-slot-report-parity

Conversation

@VaguelySerious

@VaguelySerious VaguelySerious commented Sep 11, 2026 •

Copy link
Copy Markdown
Member

What

Two things that share one rule.

1. QuickJS engine parity on eventCount / sinceCursor. The engine's header carried a KNOWN GAP note: no write it made named a log position, so a World never reported skipped events back to it, and every iteration of its inline loop re-listed the run's log from the top. Now:

  • Every write made from the invocation's view of the log (step_created, wait_created, hook_created, hook_disposed, attr_set, the abort hook_received, wait_completed, and the step_created + step_failed pair for an unserializable input) goes through one createEvent seam that attaches eventCount.
  • A single inline step's terminal write asks for the inline delta (inlineDeltaSinceCursor on executeStep) under the node engine's requestInlineDelta gate translated to this loop: the step is the only step outstanding and no wait is pending. A lone hook_created asks for the delta too, under the node suspension handler's single-hook gate.
  • The page a World returns on either is queued by a new QuickJSLogView and delivered to the live VM in position order, ahead of the next events.list. The list now reads forward from a cursor (seeded by a new preloadedCursor param the replay loop passes) instead of from the start of the log, and only runs when the queue cannot account for the next position.
  • Only pages are delivered, never the created event a write returns on its own: a create response may carry it with payload refs unresolved and telemetry fields stripped (world-vercel does, for every type whose entity the runtime does not read off the response), which is not what a listing would deliver. The created event only advances the position the next write names. wait_completed, which carries nothing a VM reads, is the one opt-in. (The first push fed created events and failed the e2e serialization-failure tests on every world-vercel QuickJS lane with a FatalError where a SerializationError was expected; that is the failure this rule prevents.)
  • Run-terminal writes and the terminal drain send nothing: nothing replays the log afterwards.
  • A World whose event ids are not slots turns tracking off for the invocation and the engine behaves as before.

The node engine merges a returned page into the array it replays from. The QuickJS engine holds a live VM that consumes events exactly once, so "merge" there means "queue and deliver in order", and a queued event above a position not yet in hand waits for a listing to fill the gap. QuickJSLogView owns that, and its cursor only advances past a delta when the queue is dense below the delta's end, so a listing can never skip an event the VM still needs.

2. Documentation of who names a position. A new Events returned on a write section in the event-sourcing docs states the rule (a write carries eventCount / sinceCursor when the process writing it holds a loaded log it keeps deciding from) and tabulates every event type by writer, what it sends, and why. The same table sits above slotSnapshotParams in helpers.ts for the code reader, and building-a-world.mdx tells World authors they may decline the report read for types whose writer never holds a log.

Docs Preview

Page v5
Event sourcing, new section /docs/how-it-works/event-sourcing#events-returned-on-a-write
Building a World, eventCount paragraph /worlds/building-a-world#event-id-allocation

Depends on

The step-executor rows describe the state after #4096 (executor writes send no eventCount). Until that merges, the node engine's executor still sends one; the table is written for where the code is going.

Validation

  • New quickjs-log-view.test.ts (11 tests): position naming, in-order delivery of a write with its skipped span, hold-back above a hole, truncated report dropped, delta cursor advance and its two refusals, tracking turned off on a non-slot id.
  • New quickjs-slot-report.test.ts (4 tests, real world-local, VM mocked): a suspension write names the loaded position and the listing that follows reads forward from the preload's cursor; a concurrent writer's event comes back on the report and is fed ahead of any listing, in position order; a single inline step's terminal write carries sinceCursor and no eventCount, and the step's events reach the VM from the delta with no listing after it; a lone hook_created carries both and the hook event reaches the VM from the delta with no listing.
  • Existing QuickJS suites (quickjs-runtime, preload, partial-preload, hook-dispose-token) pass unchanged. Full packages/core src/ suite green; the only failures are e2e/ files that need a deployment.
  • tsc --noEmit clean; biome clean on the changed files apart from the file's pre-existing complexity warnings.

🤖 Generated with Claude Code

…ames one

Every write the QuickJS engine makes from its view of the log now carries
`eventCount`, a single inline step's terminal write asks for the inline
delta (`sinceCursor`), and the page a World hands back on either is queued
for the live VM in position order ahead of the next `events.list`, which
now reads forward from a cursor instead of the top of the log. Run-terminal
writes and the terminal drain send nothing, as nothing replays the log
after them. A World whose ids are not slots turns all of this off and gets
the engine's previous behavior.

Documents the rule for which writes name a position (the writer holds a
loaded log it keeps deciding from) as a per-event table in the
event-sourcing docs and above `slotSnapshotParams`, with a World-author
pointer in building-a-world.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 11, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ebb5337

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

@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 11, 2026 8:35pm UTC
example-nextjs-workflow-webpack Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
example-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-astro-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-express-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-fastify-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-hono-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-nestjs-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-nitro-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-nuxt-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-python-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-sveltekit-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-tanstack-start-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workbench-vite-workflow Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workflow-docs Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workflow-swc-playground Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workflow-tarballs Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC
workflow-web Ready Ready Preview, v0 Sep 11, 2026 8:35pm UTC

@github-actions

github-actions Bot commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit ebb5337 · Fri, 11 Sep 2026 20:51:25 GMT · run logs

Backend: vercel · app: nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 1254 (+443%) 🔻 1437 🔴 (+6.1%) 1452 🔴 (+5.0%) 1517 🔴 (+4.9%) 30
TTFS stream 1250 (+658%) 🔻 1402 🔴 (+2.9%) 1473 🔴 (+4.0%) 3296 🔴 (+100%) 🔻 30
TTFS hook + stream 443 (-12%) 1696 🔴 (-8.3%) 1786 🔴 (-10%) 2289 🔴 (-0.8%) 30
Fan-out TTFS Promise.all(100 steps) 556 (-17%) 💚 700 (-13%) 772 (-18%) 💚 2772 (+38%) 🔻 10
Fan-out TTLS Promise.all(100 steps) 1871 (-4.9%) 6488 (+153%) 🔻 8602 (+91%) 🔻 9087 (+28%) 🔻 10
STSO 1020 steps (inline) 98 (-20%) 💚 124 (-22%) 💚 140 (-22%) 💚 181 (-46%) 💚 1019
WO 1020 steps 123866 (-22%) 💚 123866 (-22%) 💚 123866 (-22%) 💚 123866 (-22%) 💚 1
CRTT first chunk (pooled) 52 (-17%) 💚 82 (-34%) 💚 93 (-44%) 💚 98 (-79%) 💚 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 70.5 (-24%) 115 (-70%) 153 (-67%) 401 (-60%) 99 (-77%) 10
size sweep (100/s, 160B-12KB) 77.5 (-17%) 115 (-73%) 180 (-80%) 589 (-60%) 146 (-59%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 55 (-57%) 108 (-43%) 132 (-49%) 270 (-57%) 191 (-64%) 3
replay eve-gpt-5.6-sol-2000t (1x) 75.5 (-29%) 304 (+76%) 1157 (+401%) 4478 (+758%) 2022 (+384%) 2
replay eve-gpt-5.6-sol-2000t (2x) 91 (-7%) 162 (-48%) 222 (-55%) 438 (-61%) 192 (-52%) 3
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 159219ms → this run 123712ms (Δ -35507ms, -22%)

 50-100 ms  ┃                         main   0  this   1    +1
100-150 ms  ███████████████░░░░░░░░┃  main 595  this 968  +373
150-200 ms  ┃████████                 main 372  this  45  -327
200-250 ms  ┃                         main  31  this   2   -29
250-300 ms  ┃                         main   6  this   2    -4
300-350 ms  ┃                         main   8  this   1    -7
350-400 ms  ┃                         main   2  this   0    -2
400-450 ms  ┃                         main   1  this   0    -1
450-500 ms  ┃                         main   1  this   0    -1
550-600 ms  ┃                         main   2  this   0    -2
600-650 ms  ┃                         main   1  this   0    -1
📈 CRTT drill-down vs main (RTT distributions & profiles)
variant  RTT 1ms→5s+             avg         p50           p90           p99     n
control  ······█▅▁····   99.6 (-56%)   93 (-37%)    153 (-67%)    401 (-60%)  3000
sweep    ·····▁█▅▁▁···  103.3 (-60%)   93 (-49%)    180 (-80%)    589 (-60%)  3000
gw 1x    ·····▁█▄▁····   96.3 (-38%)   91 (-35%)    132 (-49%)    270 (-57%)  5295
eve 1x   ·····▁██▂▁▁▁·    271 (+85%)   90 (-31%)  1157 (+401%)  4478 (+758%)  5186
eve 2x   ·····▁▃█▂▁···  141.2 (-40%)  128 (-30%)    222 (-55%)    438 (-61%)  7779

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

control  ▃▂██▄▂▅▂▂▁  89–116ms
sweep    ▂▃▁▁▁▃█▄▂▁  88–150ms
gw 1x    █▄▆▂▃▇▂▂▃▁  92–103ms
eve 1x   ██▃▂▁▁▁▁▁▁  98–786ms
eve 2x   ▂▁▃▃▁▅▆██▁  122–173ms

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

sweep  ▅███▄▁▁  100–106ms

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

control  ▅▂██▇▄▄▃▄▁  25–33ms
sweep    ▆▄▁▂▃██▅▄▁  32–48ms
gw 1x    █▇▃▁▅▅▃▁▄▄  24–30ms
eve 1x   █▆▅▄▃▁▃▂▂▃  20–50ms
eve 2x   █▅▁▃▃▂▂▁▃▆  19–26ms
📜 Previous results (1)

7cd9c3e

Fri, 11 Sep 2026 16:47:45 GMT · run logs

vercel / nextjs-turbopack

Metric Scenario Best (ms) P75 (ms) P90 (ms) P99 (ms) Samples
TTFS step 277 (+56%) 🔻 1272 🔴 (-5.9%) 1322 🔴 (-4.1%) 1369 🔴 (-11%) 30
TTFS stream 170 (-1.2%) 1287 🔴 (-4.9%) 1302 🔴 (-6.4%) 1410 🔴 (-21%) 💚 30
TTFS hook + stream 503 (-9.0%) 1578 🔴 (-7.2%) 1719 🔴 (±0%) 1918 🔴 (+2.3%) 30
Fan-out TTFS Promise.all(100 steps) 590 (-12%) 877 (-3.3%) 1916 (+103%) 🔻 2114 (-4.9%) 10
Fan-out TTLS Promise.all(100 steps) 1475 (-22%) 💚 8040 (+99%) 🔻 8274 (+65%) 🔻 8350 (-8.8%) 10
STSO 1020 steps (inline) 84 (-33%) 💚 120 (-27%) 💚 139 (-25%) 💚 711 (+172%) 🔻 1019
WO 1020 steps 134152 (-17%) 💚 134152 (-17%) 💚 134152 (-17%) 💚 134152 (-17%) 💚 1
CRTT first chunk (pooled) 59 (-13%) 80 (-32%) 💚 98 (-33%) 💚 113 (-80%) 💚 28

Streams

Scenario CRTT 1st p75 p90 p99 CDV max iters
paced control (100/s, 60B) 71.5 (-14%) 163 (-12%) 403 (+26%) 805 (-7%) 137 (-21%) 10
size sweep (100/s, 160B-12KB) 71.5 (-28%) 583 (+184%) 775 (+171%) 1357 (+75%) 438 (+139%) 10
replay gateway-gpt-5.4-nano-2000t (1x) 98 (-14%) 194 (+11%) 417 (+42%) 611 (-13%) 365 (-18%) 3
replay eve-gpt-5.6-sol-2000t (1x) 76.5 (-29%) 134 (-17%) 218 (-3%) 752 (+37%) 654 (+71%) 2
replay eve-gpt-5.6-sol-2000t (2x) 96 (-18%) 169 (-51%) 354 (-36%) 782 (-51%) 369 (-22%) 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

🧪 E2E Test Results

✅ All tests passed

⚠️ Flaky E2E Tests (passed on retry)

These tests failed at least once and passed on a retry. A recurring entry here is a real race worth investigating.

  • addTenWorkflow (hono)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (fastify)
  • deploymentId: 'latest' is a no-op in non-Vercel worlds (nitro)
  • getTailIndex returns correct index after stream completes (nextjs-webpack)
  • promiseAllWorkflow (express)
  • promiseRaceWorkflow (express)
  • uncaught step-argument serialization failure fails the run as USER_ERROR without redelivery retries (nextjs-webpack)

🛠 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 20:37:01Z · abandoned wrun_01M292ZBY7XYNRKRXPKHN79NM3

E2E Test Summary

Summary
Passed Failed Skipped Total
✅ ▲ Vercel Production 3662 0 685 4347
✅ 💻 Local Development 3922 0 586 4508
✅ 📦 Local Production 3922 0 586 4508
✅ 🐘 Local Postgres 3922 0 586 4508
✅ 🪟 Windows 320 0 2 322
✅ 🌐 Cross-language Conformance 68 0 74 142
✅ vercel-http-transport 690 0 276 966
✅ vercel-multi-region 27 0 0 27
✅ vercel-ws-transport 557 0 87 644
Total 17090 0 2882 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 141 0 20
✅ nextjs-turbopack-canary-quickjs 141 0 20
✅ nextjs-turbopack-stable-node 160 0 1
✅ nextjs-turbopack-stable-quickjs 160 0 1
✅ nextjs-webpack-canary-node 141 0 20
✅ nextjs-webpack-canary-quickjs 141 0 20
✅ 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 141 0 20
✅ nextjs-turbopack-canary-quickjs 141 0 20
✅ nextjs-turbopack-stable-node 160 0 1
✅ nextjs-turbopack-stable-quickjs 160 0 1
✅ nextjs-webpack-canary-node 141 0 20
✅ nextjs-webpack-canary-quickjs 141 0 20
✅ 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 141 0 20
✅ nextjs-turbopack-canary-quickjs 141 0 20
✅ nextjs-turbopack-stable-node 160 0 1
✅ nextjs-turbopack-stable-quickjs 160 0 1
✅ nextjs-webpack-canary-node 141 0 20
✅ nextjs-webpack-canary-quickjs 141 0 20
✅ 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 0 0 161
✅ 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

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 201.6 KiB (-34.4 KiB) 42.3 KiB (±0) 1.80 MiB (-11.5 KiB)
nextjs-turbopack 207.8 KiB (-34.4 KiB) 439 B (±0) 780.2 KiB (-57.5 KiB)
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.

ebb5337 · run

…off a write

A create response may carry the created event with its payload still a ref
descriptor and telemetry fields stripped (world-vercel does, for every type
whose entity the runtime does not read off the response). Feeding that copy
of a step_failed gave the VM a step that failed with no error in it; the
e2e serialization-failure tests saw a FatalError where a SerializationError
was expected. The created event now only advances the position the next
write names, and is delivered by the page that covers it or by the next
listing. wait_completed, which carries nothing a VM reads, opts in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@pranaygp pranaygp 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.

Counter-review with the context from #4096 and vercel/workflow-server#944.

What it does, in one line: the QuickJS engine now sends eventCount on the writes it makes from its loaded view (and sinceCursor on a lone inline step's terminal write), and instead of merging returned pages into an array like the node engine, it queues them and feeds the live VM in position order ahead of the next list.

Verdict: good to merge, with the notes below, none blocking. The design is sound: QuickJSLogView only delivers contiguous runs above what the VM already has, drops truncated reports whole, only advances the cursor past a delta when the queue is dense below its end, and turns itself off on a non-slot id. Executor writes and run-terminal writes stay silent, which matches the rule #4096 and #944 landed on. Not delivering the bare created event (world-vercel returns it with lazy refs and stripped telemetry) is the right call and the e2e failure that motivated it is a good reason to keep that rule.

Inline:

  1. The inline-delta gate is looser than the node engine's on open waits and overflow siblings. I think it is safe here, but the reasoning is not recorded; mirror the gate or write it down.
  2. A wording nit on what the queued feed saves.
  3. A JSDoc table row in helpers.ts lost its * gutter.
  4. The hook_created row does not hold for QuickJS yet (no sinceCursor there).

One process note: the branch is based before #4096 merged, so CI validated it against the old executor. There is no overlap in the touched lines and the table already describes the post-#4096 state, so a merge is fine as-is.

// node engine: several steps would each get a delta from the same
// cursor, and only one could be taken. Not requested when the log has
// no cursor to name (tracking off, or nothing read yet).
const inlineDeltaSinceCursor =

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.

This gate is looser than the node engine's requestInlineDelta (runtime.ts ~L4230). The node gate also requires err.waitCount === 0, !suspensionResult.waitTimeout, !openHookWaitState.openWait, pendingSteps.length === 1 and ownedRecoverySteps.length === 0, and its comment gives the reason for the wait half: a wait_completed "is a resolution the replay is waiting on rather than an event it can observe one iteration late, so consuming a delta that predates it would settle the sleep from a view that does not contain its completion."

Here the delta is requested with an open wait and with overflow (queued) siblings when MAX_INLINE_STEPS is 1. I traced the open-wait case and I believe it is safe for this engine: the delta is absorbed, the cursor advances past it, the VM still sees the wait pending, and on the next iteration the timer path either writes wait_completed (or hits EntityConflictError if the continuation delivery beat it) and the listing from the advanced cursor picks the completion up. So it is a one-iteration-late observation, not a divergence. But that argument is not written down anywhere, and the node engine chose to fetch instead. Either mirror the node gate (cheap, and keeps the two engines on one rule) or add the reasoning here so the next reader does not have to re-derive it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(AI) Mirrored the node gate in ebb5337: the delta is requested only when this step is the only step outstanding (stepOps.length === 1, so no overflow, unserializable, or earlier-invocation sibling) and no wait is pending in the suspension's ops, plus the existing cursor condition. The wait reasoning is written into the comment. I went with mirroring rather than documenting the looser gate, since one rule across both engines is easier to keep true than two arguments.

// 2. Cheap progress first: feed newly recorded events into the live
// VM before blocking on step bodies.
// VM before blocking on step bodies. What the writes above handed back
// is delivered first; a listing runs only when that does not reach the

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.

Nit on the wording: "a listing runs only when that does not reach the next position" overstates it slightly. When takeQueuedEvents returns a page, this branch continues, and the next iteration's feed finds the queue empty and lists anyway (from a cursor a report does not advance, so it re-reads the reported span and dedupes on seenEventIds). So a report changes what is fed first, but the list before step execution still happens one iteration later. That is the same shape the node engine has, so no change needed, just worth saying so the comment does not promise a saved round-trip that only the delta path actually delivers.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(AI) Reworded in ebb5337. The comment now says a report changes what is fed first, not whether the pre-step listing happens (it re-runs one iteration later from a cursor the report does not advance, deduped on seenEventIds), and that only the delta path saves a listing outright.

Comment thread packages/core/src/runtime/helpers.ts Outdated
* | `step_completed`, `step_failed` | step executor | `sinceCursor` only, outside turbo, single inline step | this is where the inline execution loop gets its log back; in turbo or from a queued delivery a fresh replay loads it anyway |
* | `step_failed` | suspension handler, unserializable input | `eventCount` | the handler holds a log; rare, and follows a `step_created` that drew the page |
* | `attr_set` | `setAttributes()` API (raw) | nothing | out-of-band |
| `run_completed`, `run_failed`, `run_cancelled` | replay loop, replay budget, `runs.cancel` | `eventCount` where the node loop's seam stamps it (the QuickJS engine sends nothing); never `sinceCursor` (`deltaRequestCursor` excludes terminal types) | terminal: nothing replays the log afterwards, so a World reads no page for them |

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.

Formatting: this table row is missing the leading *, so it sits outside the JSDoc gutter (still inside the comment block, so it compiles, but it breaks the table for anything that renders the doc).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(AI) Fixed in ebb5337.

| `attr_set` | `setAttributes()` outside a workflow | Nothing | Out-of-band. |
| `run_completed`, `run_failed`, `run_cancelled` | Replay, or `cancel()` | Never `sinceCursor` | Terminal. Nothing replays the log afterwards, so a page would be read by no one. |

Batched writes (`events.createBatch()`) carry no position and get no page. Both runtime engines follow this table: the `node:vm` engine merges a returned page into the log it replays from, and the QuickJS engine queues it for delivery to its live VM, in position order, ahead of its next `events.list()`.

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.

"Both runtime engines follow this table" is not quite true for the hook_created row: the node suspension handler sends eventCount and sinceCursor on one hook write per suspension (that delta is how a hook whose payload already arrived resolves in-process), while this PR's QuickJS dispatchPendingOps sends only eventCount on hook_created. Either note the QuickJS exception in the row, or add the sinceCursor ask on the QuickJS side in a follow-up. The same applies to the helpers.ts copy of the table.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

(AI) Added the sinceCursor ask on the QuickJS side in ebb5337 rather than noting an exception. dispatchPendingOps takes a deltaCursor from the loop and passes it as sinceCursor on the hook_created create when exactly one hook needs creating (the node handler's hookDeltaCursor gate); the createEvent seam takes a delta response through absorbDelta so the cursor moves with it. Pinned by a new test in quickjs-slot-report.test.ts (hook_created reaches the VM off the delta, no listing). Both table copies updated to say the ask is gated on exactly one hook, since that is the part that was imprecise for the node engine too.

@pranaygp pranaygp 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.

Stamp

…d delta, doc fixes

- The QuickJS inline-delta gate now matches the node engine's
  requestInlineDelta: sole outstanding step (no overflow, unserializable, or
  earlier-invocation sibling) and no pending wait, with the reasoning for the
  wait half written down.
- A lone hook_created asks for the inline delta (sinceCursor) with the same
  single-hook gate as the node suspension handler, and the createEvent seam
  takes a delta response through the delta path. The hook_created row now
  holds for both engines.
- Feed comment no longer claims a report saves the pre-step listing; only
  the delta does, since a report does not advance the cursor.
- Restore the JSDoc gutter on the run-terminal table row in helpers.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious merged commit 357aa7c into main Sep 11, 2026
181 of 182 checks passed
@VaguelySerious
VaguelySerious deleted the peter/quickjs-slot-report-parity branch September 11, 2026 20:37
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for 357aa7c (AI decision).

This is feature/parity work on the experimental QuickJS engine: it closes a documented KNOWN GAP by adding eventCount/sinceCursor reporting and a new QuickJSLogView that consumes event pages returned on writes, saving round trips rather than fixing a user-visible defect. It also does not apply to stable: packages/core/src/runtime/quickjs-entrypoint.ts and both touched docs pages (docs/content/docs/v5/..., docs/content/worlds/v5/...) are absent from origin/stable, and the runtime.ts/helpers.ts edits only serve the QuickJS path. Nothing here is a stability fix for the maintenance line.

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

357aa7c38aaa038122ecd3c1c7cb220603b42ea8

This branch was successfully deployed

18 active deployments
Preview – workflow-swc-playground — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workflow-docs — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – example-nextjs-workflow-webpack — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-nuxt-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-astro-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – example-nextjs-workflow-turbopack — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-vite-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – example-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-fastify-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-nitro-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workflow-tarballs — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-express-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-sveltekit-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-tanstack-start-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-hono-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-nestjs-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workflow-web — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
Preview – workbench-python-workflow — ebb53375 Deployed Sep 11, 2026 by vercel[bot]
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