Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Enable additional perf optimizations when correctness guarantees are met by VaguelySerious · Pull Request #2970 · vercel/workflow · GitHub
Skip to content

Enable additional perf optimizations when correctness guarantees are met - #2970

Merged
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks
Jul 17, 2026
Merged

Enable additional perf optimizations when correctness guarantees are met #2970
VaguelySerious merged 3 commits into
mainfrom
peter/inline-delta-turbo-unlocks

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

Two relaxations of conservative gates in the inline replay loop, each tied to the mechanism that makes it safe. Behavior is byte-identical unless the corresponding flag is enabled.

1. Inline-delta fast path stays active with open hooks (requires WORKFLOW_PRECONDITION_GUARD=1 on a World that declares capabilities.preconditionGuard)

The per-step event-log delta optimization (#2475) — consuming the delta returned by the step-terminal write instead of issuing one events.list per sequential step — previously turned off for the rest of the run once any hook was open, because a hook_received landing between the terminal write and the next replay would be absent from the delta.

With the precondition guard enabled, that window is fenced rather than open-ended:

  • The staleness is qualitatively the same read-to-write race the fetch path already tolerates today: an out-of-band event can land right after events.list returns and before the suspension's writes, and is observed one iteration late. The delta path widens that window; it does not create a new class of it. Delta windows are also contiguous (each delta covers everything since the pre-write cursor), so an event missed by one window is always delivered by the next one.
  • hook_received bumps the run's outside-event marker, so a replay acting on the stale view has its guarded suspension creates rejected with 412 and retried over the reloaded log — or, if reloads cannot surface the event, exhausted into a queue re-invocation whose fresh full replay observes it.

Open waits keep the conservative gate: wait_completed does not bump the outside-event marker, so nothing fences a replay from a delta that missed one. The gate now distinguishes the two kinds (openHookAndWaitState). Hooks created by the same suspension (err.hookCount) are also allowed under the guard — their hook_created lands before the step-terminal write and is therefore inside the delta.

For hook-heavy sequential workflows this removes one world round-trip per step for the entire post-hook stretch of the run.

2. Turbo keeps forced optimistic inline start under WORKFLOW_SEQUENTIAL_REPLAYS=1 (retracted in review)

Turbo's forced optimistic inline start (run the step body immediately instead of awaiting the step_started create-claim) previously latched off the moment the run created a hook or wait: those introduce resume invocations, ending the single-handler guarantee that makes running a body before the claim confirms safe.

With sequential replays enabled, the resume invocations hooks and waits introduce are run-topic messages on a per-run maxConcurrency: 1 topic — the queue does not deliver them until the current delivery acks, so no concurrent orchestrator replay can race the optimistic create-claim, and the latch is waived.

What stays concurrent is unchanged from clean turbo today:

  • Per-step-topic background executions. The last-parallel-step-done handler's fall-through replay can still race a claim; the atomic step_started create-claim still guarantees at most one winner writes events. This window exists in clean (hook-free) turbo on main today.
  • Webhook receivers only append hook_received; they never execute steps.
  • Queue lease semantics.maxConcurrency: 1 is a lease guarantee; an invocation past its visibility timeout can overlap its redelivery. Same at-least-once envelope as crash-redelivery.

Note the pre-existing configuration caveat also applies here: WORKFLOW_SEQUENTIAL_REPLAYS needs the matching build-time flow-trigger config. Setting the env var at runtime only, on a setup whose flow trigger lacks maxConcurrency: 1, does not serialize anything — the new turbo behavior assumes the documented full configuration.

The attr-events check stays in both arms: attribute suspensions resolve through an in-process replay pass that must decide races before any step body runs, independent of queue serialization.

Changes

  • packages/core/src/runtime.ts — split hasOpenHookOrWait into openHookAndWaitState (per-kind); rework the requestInlineDelta and forceOptimisticStart gates as above, with the safety analysis in comments.
  • packages/core/src/runtime/constants.ts — add isSequentialReplaysEnabled() (mirrors the @workflow/builders / @workflow/world-vercel copies; core must not depend on either).
  • Docs (v5): configuration/runtime-tuning.mdx (guard + turbo entries), worlds/vercel.mdx (sequential-replays section). v4 docs untouched — this ships on main (5.0 beta) only unless backported.

Tests

  • runtime.test.ts — new: with WORKFLOW_SEQUENTIAL_REPLAYS=1, a wait-creating suspension keeps optimistic start (body observed while the gated step_started create is still in flight — impossible on the await-then-run path); the existing turbo-exit test covers the flag-off behavior. New describe for the delta gate: with an open hook, the step-terminal write carries sinceCursor only when WORKFLOW_PRECONDITION_GUARD=1and the World declares capabilities.preconditionGuard (negative tests cover flag-without-capability for both arms).
  • constants.test.tsisSequentialReplaysEnabled strict-'1' semantics.
  • cd packages/core && pnpm test: 69 files, 1489 passed (3 expected fail, pre-existing). Typecheck and Biome clean (all warnings pre-existing on main).

Review follow-up (a7f082a)

Addressed the review with three changes:

  1. World capabilities instead of trusting env flags. New optional capabilities?: WorldCapabilities on the World interface (@workflow/world), with preconditionGuard and maxConcurrency members. Both relaxations now require the matching capability in addition to the env flag and fail closed on Worlds that don't declare it. @workflow/world-vercel declares both; world-local/world-postgres (which ignore stateUpdatedAt and have no queue-concurrency concept) declare nothing.
  2. The lazy inline step_started claim is now guard-fenced. It is a hot-path step's first durable write, so it now carries the stateUpdatedAt snapshot (both optimistic and await-then-run paths). A stale (412) rejection is not retried in place: the batch is abandoned — any optimistic body result is discarded, nothing durable is written — and the run re-invokes for a fresh replay. Covered by an interleaving test (open hook + two steps, second claim rejected as stale).
  3. Residual documented: the maxConcurrency capability confines the sequential-replays waiver to queues that support serialized consumption; it cannot verify the build-time trigger config from inside a function invocation — the build-AND-runtime configuration contract stays documented on the Vercel World page.

Review follow-up, round 2 (5e28160)

  • Optimistic bodies are now fenced on guarded stale-sensitive batches. When the guard is enforced and a hook is open (or created by the same suspension), inline steps take await-then-run even under WORKFLOW_OPTIMISTIC_INLINE_START=1 / turbo's force — the claim (carrying the snapshot) is awaited before user code runs, so a 412-fenced step never executes its body. Combined open-hook + optimistic-start + stale-claim test added.
  • The sequential-replays waiver (change 2) is retracted. The runtime env var cannot prove the built flow trigger carries maxConcurrency: 1 (build-time env + per-integration trigger config; @workflow/nitro currently never emits it at all), so the conservative hook/wait latch stays. capabilities.maxConcurrency remains declared as the queue-support half for a follow-up that pairs it with a build-verified serialization signal. This PR is now scoped to the guard-fenced inline-delta relaxation.

Docs Preview

PagePreview (v5)
Runtime Tuning — WORKFLOW_PRECONDITION_GUARDworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_precondition_guard
Runtime Tuning — WORKFLOW_TURBOworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/docs/configuration/runtime-tuning#workflow_turbo
Vercel World — WORKFLOW_SEQUENTIAL_REPLAYSworkflow-docs-git-peter-inline-delta-turbo-unlocks.vercel.sh/v5/worlds/vercel#workflow_sequential_replays

(Links require Vercel team access — the preview deployment is behind deployment protection.)

🤖 Generated with Claude Code

…imistic start under sequential replays
Two relaxations of conservative gates in the inline replay loop, each tied
to the mechanism that makes it safe:
1. The inline-delta fast path (skip one events.list per sequential step) no
longer turns off for runs with an open hook when the precondition guard
(WORKFLOW_PRECONDITION_GUARD=1) is enabled. A hook_received landing in
the delta window is the same read-to-write race the fetch path already
has, and with the guard on it is fenced: the marker bump 412s the stale
replay's guarded creates, which retry over the reloaded log or exhaust
into a fresh-replay re-invocation. Open waits keep the conservative gate
(wait_completed does not bump the outside-event marker).
2. Turbo keeps forcing optimistic inline start after the run creates a hook
or wait when WORKFLOW_SEQUENTIAL_REPLAYS=1: per-run maxConcurrency: 1
flow topics serialize the resume invocations hooks/waits introduce, so
no concurrent orchestrator replay can race the optimistic create-claim,
restoring the single-handler guarantee turbo relies on.
Adds isSequentialReplaysEnabled() to the core runtime (mirroring the
@workflow/builders / @workflow/world-vercel copies) and splits
hasOpenHookOrWait into per-kind state so the two gates can differ.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 23:41
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5e28160

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

This PR includes changesets to release 20 packages
NameType
@workflow/corePatch
@workflow/worldMinor
@workflow/world-vercelPatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/world-localPatch
@workflow/world-postgresPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/nuxtPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch

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-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 5e28160 · Fri, 17 Jul 2026 20:29:11 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1456 (+15%)1677 🔴1832 🔴1856 🔴30
TTFShook + stream1619 (+22%)1952 🔴2066 🔴2335 🔴30
STSO1020 steps (1-20)270 (-9.5%)305 🔴397 🔴401 🔴19
STSO1020 steps (101-120)325 (+5.2%)369 🔴472 🔴544 🔴19
STSO1020 steps (1001-1020)736 (+18%)785 🔴922 🔴944 🔴19
WOstream1456 (+15%)16771832185630
WOhook + stream1619 (+22%)19522066233530
SLstream4039 (+304%)5891 🔴5932 🔴6815 🔴30
SLhook + stream4156 (+113%)5785 🔴5849 🔴6026 🔴30
📜 Previous results (2)

a7f082a

Fri, 17 Jul 2026 17:50:37 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1643 (+30%)1924 🔴1945 🔴3648 🔴30
TTFShook + stream1593 (+20%)2069 🔴2299 🔴2684 🔴30
STSO1020 steps (1-20)299 (±0%)320 🔴396 🔴803 🔴19
STSO1020 steps (101-120)294 (-4.7%)323 🔴365 🔴487 🔴19
STSO1020 steps (1001-1020)743 (+19%)793 🔴830 🔴882 🔴19
WOstream1643 (+30%)19241945364830
WOhook + stream1593 (+20%)20692299268430
SLstream4038 (+304%)5846 🔴5916 🔴6010 🔴30
SLhook + stream3814 (+95%)5647 🔴5746 🔴5942 🔴30

9b1bd46

Fri, 17 Jul 2026 01:06:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream1238 (+14%)1687 🔴1710 🔴2060 🔴30
TTFShook + stream1580 (+11%)1902 🔴1970 🔴2163 🔴30
STSO1020 steps (1-20)317 (+5.0%)335 🔴478 🔴801 🔴19
STSO1020 steps (101-120)435 (+0.5%)446 🔴532 🔴694 🔴19
STSO1020 steps (1001-1020)910 (+6.2%)979 🔴1101 🔴1172 🔴19
WOstream1238 (+14%)16871710206030
WOhook + stream1580 (+11%)19021970216330
SLstream4436 (-5.2%)5670 🔴5759 🔴5944 🔴30
SLhook + stream4492 (-7.4%)4898 🔴5669 🔴5973 🔴30

Avg deltas compare against the most recent benchmark run on main at the time of this run.

Metrics — TTFS: time to first step body execution · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (time outside step bodies, client start → last step body exit) · SL: stream latency (first chunk write → visible to the reader)

Scenarios — stream: one step that streams chunks back to the client; no hooks, so the run stays in turbo mode · hook + stream: registers a hook before the same streaming step, which exits turbo mode · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

TTFS/WO compare client vs deployment clocks and SL compares the step runner’s clock vs the client’s (NTP-synced in CI). WO ends at the last step body exit, the closest observable proxy for the final step-completion request.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Guard the lazy step claim before enabling deltas with open hooks

A hook_received can land after the terminal write that produced the delta, so the next loop replays a stale view. If that replay schedules a lazy inline step, handleSuspension defers step_created and both lazy step_started paths in executeStep omit stateUpdatedAt. workflow-server therefore skips the precondition check, the claim succeeds, and the step body can execute/commit before a fresh replay observes the hook—the loser-step/replay-divergence case this gate is meant to prevent. Please keep the open-hook gate, or pass the snapshot into lazy step_started and ensure the body waits for the guarded claim (including optimistic-start cases), with an interleaving test rather than only asserting sinceCursor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Good catch — the fence I described didn't actually cover the hot path: the lazy claim is a hot-path step's first durable write (its step_created is deferred), and it carried no snapshot. Fixed in a7f082a by taking your second option:

  • The runtime now computes the guard snapshot from the loaded log and threads it into the lazy step_started claim on both paths (optimistic and await-then-run) via a new stateUpdatedAt executor param.
  • A stale (412) rejection is intentionally not translated by the claim-error mapper — re-claiming in place would still commit the stale schedule — so it propagates: the batch is abandoned, any optimistic body result is discarded by the existing reconciliation (no events are ever written by the loser), and the run is re-invoked for a fresh replay that observes the new event.
  • Added the interleaving test you asked for: open hook + two sequential steps, the second step's claim rejected as stale by the backend → the fenced step's body never runs, no events are written for it, no run_failed, and the message redelivers (plus an assertion that the claim actually carried the snapshot).

Two notes on scope: (1) under optimistic start the body may still start before the claim settles — a fenced claim discards the result and writes nothing durable, so the side-effect exposure is exactly the documented optimistic-start idempotency contract, unchanged by this PR; (2) the very first batch of a run loads an empty log and has no snapshot to send — that's pre-existing guard semantics shared with every suspension create (best-effort by design, see latestEventStateUpdatedAt).

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.waitTimeout &&
!hasOpenHookOrWait(cachedEvents ?? []);
!openHookWaitState.openWait &&
(isPreconditionGuardEnabled() ||

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.

[P1] Verify guard support rather than trusting the env flag

This branch treats WORKFLOW_PRECONDITION_GUARD=1 as proof that the World enforces stateUpdatedAt, but the docs say unsupported backends may ignore that field. On such a World, merely setting the env var opens the hook-delta path with no 412 fence at all. Please gate this on an explicit World/backend capability (or otherwise fail closed) instead of a process-local flag alone.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Agreed — the env flag only makes the runtime send snapshots; it can't prove the backend enforces them. Fixed in a7f082a: the World interface now has an optional capabilities?: WorldCapabilities field, and the open-hook delta relaxation requires WORKFLOW_PRECONDITION_GUARD=1andworld.capabilities.preconditionGuard === true, failing closed to the conservative gate otherwise. @workflow/world-vercel declares the capability (workflow-server enforces the marker); world-local and world-postgres ignore stateUpdatedAt entirely and correctly declare nothing. Added a negative test: flag set + no capability → the delta is not requested.

Comment threadpackages/core/src/runtime.ts Outdated
!suspensionResult.hasAttributeEvents &&
!suspensionResult.hasAwaitedHookCreation &&
!hasOpenHookOrWait(cachedEvents ?? []);
(isSequentialReplaysEnabled() ||

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.

[P1] Do not waive the hook/wait latch from a runtime-only flag

isSequentialReplaysEnabled() only checks the runtime env var; it cannot establish that the built flow trigger actually has maxConcurrency: 1. If runtime is set without matching build config, hook/wait resume messages may overlap, yet this branch forces optimistic start. Two handlers can then run the same step body before either atomic claim settles; the loser drops its result but external side effects already happened. Please require a verified serialization capability/config signal before taking this arm, rather than relying on the raw env flag plus documentation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Fixed in a7f082a: the sequential-replays waiver now also requires world.capabilities.maxConcurrency === true (declared by @workflow/world-vercel, whose queue implements maxConcurrency-limited consumers), failing closed to the conservative hook/wait latch otherwise — with a negative test (env set, no capability → await-then-run ordering preserved).

To be explicit about the residual: the capability confines the waiver to Worlds whose queue actually supports serialized consumption; it cannot by itself verify the build-time half of the contract (the flow trigger's maxConcurrency: 1 config), which the runtime has no way to introspect from inside a function invocation. That remains the documented set-it-at-build-AND-runtime requirement (called out in the Vercel World docs and the gate's code comment). If we later surface the built trigger config to the runtime (e.g. via the manifest), the gate can tighten further — happy to file a follow-up.

…ies; fence the lazy inline claim
Address review on #2970:
- Add `capabilities?: WorldCapabilities` to the World interface
(`preconditionGuard`, `maxConcurrency`); the Vercel World declares both.
The env flags alone cannot prove backend enforcement, so the inline-delta
open-hook relaxation and turbo's sequential-replays waiver now also require
the matching capability and fail closed on Worlds that don't declare it.
- Thread the precondition-guard `stateUpdatedAt` snapshot into the lazy
inline `step_started` claim (both the optimistic and await-then-run
paths). The claim is a hot-path step's first durable write, so without it
a stale replay could claim — and commit — a step scheduled off a view that
misses an out-of-band event. A 412-rejected claim is not translated:
the batch is abandoned (any optimistic body result is discarded) and the
run is re-invoked for a fresh replay.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooks; keep turbo optimistic start under sequential replaysfeat(core): guard-fenced inline delta with open hooksJul 17, 2026

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

Re-reviewed at 5e28160. The outstanding concurrency concerns are addressed: guard-enforced hook-sensitive batches now await the fenced step_started claim before running user code, and the sequential-replays optimistic-start waiver has been removed until deployed maxConcurrency configuration can be verified. No blocking findings.

@VaguelySeriousVaguelySerious changed the title feat(core): guard-fenced inline delta with open hooksEnable additional perf optimizations when correctness guarantees are met Jul 17, 2026
@VaguelySerious
VaguelySerious merged commit bb773e9 into mainJul 17, 2026
172 of 174 checks passed
@VaguelySerious
VaguelySerious deleted the peter/inline-delta-turbo-unlocks branch July 17, 2026 21:02
@github-actionsgithub-actionsBot mentioned this pull request Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for bb773e9 (AI decision).

This commit relaxes gates in main-only machinery — the inline-delta fast path, precondition guard (WORKFLOW_PRECONDITION_GUARD, isPreconditionGuardEnabled, stateUpdatedAtForCreate), turbo optimistic inline start, lazy inline steps, hasOpenHookOrWait, and the step-executor.ts module — none of which exist on stable (verified via git show/git grep on origin/stable). It explicitly builds on APIs introduced only on main (5.0 beta) and its docs are v5-only, so there is no corresponding stable behavior to fix.

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

bb773e950786b15100a8058407cbfcba23a44ebc

pranaygp added a commit that referenced this pull request Jul 21, 2026
* origin/main: (21 commits)
docs(agents): note lint/format/typecheck are advisory, not blocking (#2886)
Retry transient connection timeouts (#3013)
fix(world-vercel): append caller User-Agent products instead of discarding them (#2998)
[ci] Enable NestJS e2e-vercel-prod and add to docs as "experimental" (#3011)
[ci] Benchmark comment: Best column + best/p75/p99 deltas (drop Avg/P10) (#3005)
docs: fall back to first child page for sidebar folders without an index (#3009)
[nest] Fix NestJS Vercel build output (#2988)
Avoid resolving run data for background steps (#2993)
chore(docs): update @vercel/geistdocs to 1.14.0 (#3002)
fix(docs): add version-switcher fallback redirects for pages missing in one version (#3003)
ci: update opencode to 1.18.4 and switch backport AI model to claude-fable-5 (#3006)
fix(core): batch stream writes via writeMulti (#2995)
perf(core): cache port discovery in step invocations for self-hosted worlds (#2996)
feat(web-shared): Alt+hover span measurement in the new trace viewer (#2985)
fix(world-postgres): throw EntityConflictError on duplicate run_created (#2983)
[ci] Run benchmarks in-deployment to avoid proxy overhead (#2967)
Enable additional perf optimizations when correctness guarantees are met (#2970)
perf(core): prepare replay payloads concurrently (#2980)
Fix dotted tsconfig alias workflow discovery (#2963)
Adjust helper position on trace viewer (#2968)
...
@github-actionsgithub-actionsBot mentioned this pull request Jul 21, 2026
Sign up for freeto 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

@VaguelySerious@karthikscale3