Pranay Prakash
90d · built 2026-08-09
90-day totals
- Commits
- 74
- Grow
- 9.9
- Maintenance
- 14.6
- Fixes
- 10.4
- Total ETV
- 34.9
Where this dev ranks
Percentile against the global top-100 leaderboard (all-time totals).
- By commits
- Top 86 %
- By Growth share
- Top 49 %
30-day trajectory
Last 30 days vs. the 30 days before. Up arrows on Growth and ETV mean improvement; up arrow on Fixes share means more time on fixes (worse).
↓-63.6 %
vs 33 prior
↓-15.4 pp
recent vs prior
↑+37.5 pp
recent vs prior
Daily performance
Daily ETV, stacked by Growth, Maintenance and Fixes.
Work-mix over time
Share of Growth / Maintenance / Fixes over a rolling 7-day window. Reads as 'where is effort flowing right now'.
Repository spread
Where this developer's commits land. Concentrated work (top1 > 80%) vs polymath spread (top1 < 30%).
Most impactful commits
Top 20 by ETV in the 90-day window.
- 2.2ETVfix(core): order step-result deliveries against wait/hook deliveries by event-log position (#3139) * fix(core): order step-result deliveries against wait/hook deliveries by event-log position Two production runs on `@workflow/core@5.0.0-beta.36` burned all three divergence-recovery replays at the same event and terminated with CORRUPTED_EVENT_LOG: wrun_41KYJENABV0GSF5YTE9EETV5DD (step vs wait) wrun_41KYJEE01S0GPC9RWT5MEKVCX8 (step vs hook) Replay divergence: step event step_created for step_X belongs to "A", but the current step consumer is "B" `useStep` proxies draw deterministic ULIDs in invocation order, so the ULID -> stepName allocation is a function of the order in which promise resolutions are delivered to workflow code. The delivery-barrier registry pinned that order to event-log position for hook payloads and wait completions, but step results were delivered straight off the serial `promiseQueue` — and their latency varies between replays of the SAME invocation, because the first replay pays full hydration while later replays memo-hit primitive results in the shared `ReplayPayloadCache`. A step completion adjacent in the log to a `wait_completed` was therefore delivered wait-first on a cold replay and step-first on a warm one; whichever order the invocation that wrote the follow-up `step_created` events happened to see became law, and every replay computing the other order diverged permanently. Step results and step failures now register a 'step' delivery barrier at their event-log index and resolve from a detached continuation after every relevant earlier-in-log delivery, mirroring the hook payload path: hydration stays inside the serial queue slot (which also releases `pendingDeliveries`), while the barrier wait and the resolve run off the queue so a queue slot never blocks on a resolution the queue itself drives. Waits and hook payloads likewise defer behind earlier step results. Two details are what actually make the ordering hold, and both were found by testing rather than by reading the code: The deferral set is captured while CONSUMING the event, not at the start of the hydration slot. Captured at slot start it is not merely less deterministic, it is usually empty: an earlier delivery whose own slot runs first on the serial queue has typically already resolved and deregistered its barrier before the later slot begins, so the later delivery does not defer at all. Every event in one drain window is consumed before any slot runs, so consumption time sees all of them. A delivery that had to wait then yields a macrotask before resolving. An earlier delivery being "delivered" only means its `resolve()` ran; the branch it woke may need arbitrarily many further microtask hops before it reaches its next `useStep` call (a `for await` over a hook resumes the generator, settles the promise from `next()`, and only then runs the loop body). Ordering the `resolve()` calls alone therefore buys a fixed hop or two of margin and leaves a hop-count race that holds only for the shortest consumers; yielding a macrotask lets the earlier branch drain completely, whatever its shape. One asymmetry is load-bearing: a step result skips any earlier delivery that will not resolve on its own, i.e. one blocked directly or transitively on a buffered hook payload no consumer has claimed. Such a payload is delivered only when the workflow next reads the hook, and reaching that read commonly requires the step result itself, so gating the step on it stalls the run until the barrier's idle safety net fires — which then releases every delivery queued behind that payload at once and loses the very race the ordering exists to protect. Waits and hooks keep gating on unclaimed payloads, where waiting for the claim IS the guarantee. Tests come in two files. `step-delivery-ordering.test.ts` is byte-identical to the file in the repro-only companion PR vercel/workflow#3137 apart from two `it.fails` markers there (which let a repro-only branch have green CI); `sed 's/it\.fails(/it(/g' | cmp` verifies it. Each of its five cases replays one committed log twice through a shared `ReplayPayloadCache`, and the two warm-replay cases fail on main with the production error text. `step-delivery-hop-count.test.ts` exists because those five cases cannot tell "delivered in log order" apart from "resolves a hop or two later than before". It replays logs a live run legitimately produced — the live invocation received the two events in separate deliveries, so the first branch finished long before the second event existed — while the replay receives both in one drain window, and pads the consumer with a varying number of extra awaits so hop count is the only variable. It covers step results against both wait completions and hook payloads, plus step FAILURES against wait completions, since a rejection decides whether a `catch` continuation runs and so which ULID the `useStep` there draws. All 18 cases fail on main; of the 12 that predate the macrotask, 9 still fail with the resolve-ordering-only version of this fix; all 18 pass here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * fix(core): close remaining delivery-barrier ordering gaps Follow-up on the step-delivery barrier work, addressing three cases the registry did not yet cover. Each has a regression test in the new `delivery-barrier-coverage.test.ts` that reproduces the production `ReplayDivergenceError` when its fix is reverted. - Step results now defer behind earlier STEP results. The old exclusion assumed the serial `promiseQueue` fixes step-vs-step order, which stopped holding once a step began resolving from a detached continuation instead of its queue slot: two steps consumed in different drain windows can disagree on their deferral set, and the earlier one — parked on the macrotask yield — gets overtaken. - `sleep.ts` and `hook.ts` (waiting-consumer path) now capture their deferral at event-consumption time, as `step.ts` already does. Reading the registry after their queue work misses an earlier step or hook that delivered and retired its barrier in the meantime, skipping both the gate and the macrotask yield. The buffered hook payload path deliberately keeps evaluating at claim time; a consumption-time snapshot there stalls the e2e `hookWithSleepWorkflow`. - Abort deliveries participate in the registry. `_setAborted` fires the signal's listeners, which may invoke a step and draw a ULID, so an abort is as branch-deciding as any other delivery. Also memoizes `resolvesOnItsOwn`. The walk is exponential in the number of live hook/wait barriers, and the registry is not bounded — a fan-out of `Promise.race([hook, sleep])` branches accumulates one barrier per branch per kind (49 measured for 24 branches). At 40 barriers a single scan took 92s before, and is instant after. --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Nathan Rajlich <n@n8.io> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · 2941b1c3 · 2026-07-28
- 2.0ETVRFC: compress serialized payload refs — zstd (gzip fallback), specVersion 5 (#2394) * feat(core,world): gzip-compress serialized payloads behind specVersion 5 Add a composable 'gzip' format prefix layer to the serialization pipeline (compress before encrypt: encr(gzip(devl))), cutting stored payload bytes by ~70-87% on real-world-style workloads. Compression is gated on run specVersion 5 (new SPEC_VERSION_SUPPORTS_COMPRESSION) and on target-deployment capabilities for cross-deployment writes; payloads under 1KB or that don't compress meaningfully are stored unchanged. Reads dispatch on the format prefix so both compressed and uncompressed data are always readable. WORKFLOW_DISABLE_COMPRESSION=1 disables writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(core): add CPU/perf compression benchmark + shared workloads Split the compression benchmark into reproducible size and CPU scripts sharing deterministic workloads (lib/workloads.mjs). The CPU benchmark measures serialize/deserialize overhead per payload, total CPU across thousands of events, and compares gzip levels/brotli/deflate. Documents how to run the size, CPU, and end-to-end (bench.bench.ts) benchmarks against local and Vercel in scripts/README.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(world-vercel): advertise specVersion 5 to enable compression on Vercel Now that workflow-server declares spec-5 support (vercel/workflow-server#520), bump the Vercel world's advertised specVersion from 4 to 5 so new Vercel runs are stamped spec 5 and become eligible for gzip payload compression. Payloads stay opaque to the server (compression is client-side); spec 5 is a superset of spec 4, so initial run attributes still work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(core): emit OTel span attributes for compression impact Track gzip payload compression on both the serialize (write) and deserialize (read) paths via span attributes: workflow.serialization.{operation,compressed,uncompressed_bytes, stored_bytes,compression_ratio}. Sizes are measured at the compression boundary (pre-encryption), so they reflect compression's effect rather than the at-rest size. The compression codec stays pure — compress/decompress optionally populate a CompressionStats sink, threaded through CodecOptions to the mode serializers and read by the dehydrate/hydrate wrappers, which set attributes on the active span. Telemetry failures are swallowed so they can never break the serialize/deserialize data path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(core,web-shared): prefer zstd compression codec (gzip fallback) Switch the payload compression codec to zstd, which benchmarks 3–7× faster than gzip at an equal-or-better ratio on representative workloads (compression runs at every step boundary, so the write CPU is a per-step tax). zstd uses node:zlib (>= 22.15); gzip via the portable CompressionStream remains the fallback when zstd is unavailable, and WORKFLOW_COMPRESSION_CODEC=gzip forces it. Reads dispatch on the format prefix, so 'zstd' and 'gzip' payloads are both always decodable. zstd is Node-only (Web CompressionStream has no zstd), so the browser o11y read path registers a WASM-backed decoder (@tootallnate/zstd-wasm) via a new registerZstdDecoder hook; node:zlib handles Node-side reads (runtime replay, CLI, server o11y). A new workflow.serialization.codec span attribute reports which codec applied. gzip and zstd read support co-ship, so the existing specVersion-5 capability gate is unchanged. Verified end-to-end: spec-5 runs store zstd-prefixed payloads on disk and replay/complete correctly; the WASM decoder round-trips node:zlib zstd output. Benchmarks updated to compare zstd vs gzip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 5f0b8452 · 2026-06-16
- 2.0ETVdocs: move World SDK and getWorld under workflow/runtime, split out workflow/observability (#2375)github.com-vercel-workflow · 055b6664 · 2026-06-12
- 1.6ETVAdd native v4 workflow attribute events (#2226) * Add native workflow attribute events * Fix abbreviated attributes docs sample * Document attribute replay ordering for step races * Address native attribute review feedback * Validate before claiming attr_set dedup lock; clearer start() attribute errors - world-local: claim the attr_set correlation lock only after validation, so a validation failure does not permanently mark the correlationId as written and wedge the run in a re-invoke loop on retry - world-postgres: distinguish a concurrently-deleted run from a cap violation when the guarded attributes update matches no rows - core: reject non-string initial attribute values in start() with a clear error instead of a downstream schema failure Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add attribute edge-case tests across all layers - core: normalizeAttributeChanges unit tests (non-object inputs, FatalError wrapping, key/value/batch limits, boundary lengths, UTF-8 byte counting) - core: start() rejects reserved keys, oversized keys/values, and over-cap initial attribute batches before any write - world-local + world-postgres: per-run cap enforced against existing attributes (upsert-at-cap allowed, removal frees room), oversized values rejected on attr_set, invalid initial attributes rejected on run_created - e2e: validation DX workflow asserting every invalid write throws a catchable FatalError naming the violated rule and limit, with the run staying healthy; start() rejects invalid initial attributes client-side Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove accidentally committed local e2e diagnostics artifact Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bump world-vercel to spec version 4 for native attributes The deployed workflow-server (vercel/workflow-server#469) materializes native attr_set events and accepts initial run attributes, but world-vercel still advertised spec v3 — so start(..., { attributes }) rejected itself client-side ('requires spec version 4') on every Vercel deployment, failing the new e2e seeding test across the prod matrix. New runs are now stamped v4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reject duplicate correlated attr_set before materializing in Postgres A redelivered duplicate — including one carrying different changes for the same correlationId — previously re-applied the run attributes update and only then failed the event insert, leaving the snapshot out of sync with the event log. Pre-check the event log for the correlationId before mutating; the unique index still guards the truly-concurrent race, which is idempotent (deterministic replay carries identical changes). Also apply the suggested docs wording for initial attributes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Apply suggestion from @VaguelySerious Signed-off-by: Peter Wielander <mittgfu@gmail.com> * Fail the run on World-rejected attribute writes; un-nest runtime test Two fixes from review: - runtime.test.ts: the pre-existing test "propagates transient step_created failures..." was accidentally nested inside the new attribute-race test, failing the new test ("Calling the test function inside another test function is not allowed") and preventing the old test from running. Restored it verbatim at describe level. - A workflow-body attr_set the World rejects as invalid (e.g. the cumulative per-run attribute cap, which only the World can check) is deterministic: redelivering the orchestrator message replays the same write into the same rejection, wedging the run in redelivery with no terminal event. handleSuspension now wraps such rejections in FatalError, and workflowEntrypoint fails the run with the validation error instead of rejecting the delivery. Transient storage errors still propagate and retry via redelivery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com>github.com-vercel-workflow · ae8d6fee · 2026-06-11
- 1.6ETVfeat(core): stamp creator environment into runInput and reject cross-environment queue deliveries client-side (#3244) * feat(core): stamp creator environment into runInput and reject cross-environment queue deliveries client-side `start()` makes two writes that have to land in the same tenant: the `run_created` event, attributed to whatever environment the caller authenticates as, and the queue message, pinned to a deployment. A misconfigured caller can split them — writing the run to one environment while addressing the message to a deployment in another. The consumer finds no run under its own tenant, the backend's resilient start (`run_started` creates the run when `run_created` was never seen) mints a second copy of the same run id in the consumer's environment, and both copies are real: the creator's sits pending forever while the other executes. The deployment id is not the discriminator — it matched end to end in the incident that motivated this. The environment is. So carry it: add an optional `World.getEnvironment()`, implement it in world-vercel from the same resolution that produces the `x-vercel-environment` header, and stamp it into the queue message's `runInput`. The consuming deployment already knows its own environment, so it can refuse the delivery itself with no server coordination — and refuse before `run_started`, the write that would create the fork. The refusal acks the message instead of throwing: the mismatch is baked into the message, so every redelivery would reach the same verdict and throwing would hot-loop until MAX_QUEUE_DELIVERIES. Both sides must be known for the check to run, so worlds with a single tenant (local, Postgres) and runs started by an older SDK behave exactly as before. A companion diagnostic logs a deployment-id mismatch without refusing, since deployment ids differ for benign reasons too. Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * fix(world-vercel): resolve the runtime environment from VERCEL_TARGET_ENV For a deployment in a Vercel custom environment, the OIDC token's environment claim is the custom environment's slug (the platform mints `customEnvironment?.slug ?? envTarget`) while VERCEL_ENV reports 'preview' — so keying the cross-environment guard on VERCEL_ENV could false-refuse a legitimate delivery, e.g. a CLI client attributed to 'staging' starting a run on the staging deployment. VERCEL_TARGET_ENV is populated from exactly the same slug-or-target pair as the claim, so prefer it, keeping VERCEL_ENV as the fallback for contexts that don't inject it. Also sorts runtime.ts imports per the Biome rule that landed on main in #3241. Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>github.com-vercel-workflow · ee944d24 · 2026-07-31
- 1.3ETVfix(docs): repair broken links, fix the link linter, and version-correct v5 Card + edit links (#2391) * fix(docs): repair broken links and make the docs link linter actually validate The docs link linter (docs/scripts/lint.ts) had been silently passing everything since the app moved under app/[lang]/ (#552): the next-validate-link populate key 'docs/[[...slug]]' no longer matched the real route, and the unpopulated [lang] homepage route produced a fallback regex (^\/(.+)$) that matched every href. It also only scanned v4 content. - Rewrite lint.ts to build explicit v4/v5 URL spaces from both fumadocs sources (including cookbook URL variants, app routes, worlds pages, public/ assets, and next.config.ts redirects) and validate each version's content against version-correct render semantics. Also validate frontmatter related/prerequisites references (version-relative) and heading fragments. - Rewrite Card hrefs on v5 pages: the v5 routes rewrote inline markdown links from /docs/... to /v5/docs/... but Card renders its own Link, so Card hrefs escaped to the v4 routes and 404'd for v5-only pages (e.g. /v5/docs/observability linking to /docs/observability/attributes). - Fix all dead content links surfaced by the working linter (56 across v4+v5): nonexistent use-workflow/use-step/start API pages now point at foundations/workflows-and-steps and workflow-api/start, getStepMetadata path corrected, /docs/worlds/local → /worlds/local, dead changelog/ internal references removed or unlinked, retired common-patterns links point at the cookbook, and a dead #returnvalue anchor now targets #returns. - Add an index page for api-reference/workflow-errors (both versions), which was linked from the API reference landing page but had no page. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(docs): add version prefix to 'Edit this page on GitHub' links All "Edit this page on GitHub" links 404'd since the v4/v5 content split (#1948): page.path is relative to the per-version content dir, but EditSource built URLs against docs/content/docs/ without the v4/ or v5/ segment. Add a required version prop, passed from each page route. Incorporates #2120 by Luke Howard (@gldkhoward), rebased onto the v5 route changes from this branch. Fixes #2119. Co-authored-by: Luke Howard <dev@lukehoward.com.au> 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 3229d206 · 2026-06-12
- 1.1ETVValidate unique workflow step IDs at build time (#2018) * Validate unique step ids at build time * Fall back to file-path IDs for non-exported package files Instead of synthesizing a 'name/dist/<path>@version' specifier (which hardcoded the dist/ output convention), non-exported workspace/node_modules files now return moduleSpecifier: undefined and let the SWC plugin's './{filepath}' fallback produce per-file IDs. This is the same path local app files have always taken and avoids the dist/ assumption flagged in review. The build-time duplicate-ID check stays as the safety net. * Dedupe virtual-entry imports by canonical module identity When both the source and the compiled-dist copies of the same workspace package export end up in discoveredSteps/discoveredWorkflows (e.g. the 'workflow' package's internal/builtins in monorepo dev), they resolve to the same module via esbuild's package resolution. The virtual entry was emitting BOTH 'import "workflow/internal/builtins";' (the built-in preamble) and 'import "../../packages/workflow/src/internal/builtins.ts";' (via the isWorkspaceSourceBackedPackageFile carve-out in createImport), which made the swc plugin transform both copies and generate duplicate step IDs. Track a per-bundle set of emitted module identities (package specifier when reachable, otherwise the file path) and skip files whose identity has already been imported. The steps bundle pre-seeds the set with the built-in steps specifier so workspace step files at that path don't emit a competing relative-path import. * Stop rewriting workspace package /dist/ -> /src/ during Next.js discovery The Next.js deferred builder's `resolveSourceBackedPackagePath` rewrote any discovered `/dist/` path to its `/src/` sibling for workspace packages and for `workflow`/`@workflow/*` tarballs. That made the discovered step file list point at source files while base-builder's esbuild bundle (which builds the workflow VM and step registrations) resolved the same package imports through `pkg.exports` to `/dist/`. The workflow proxy ID — generated from the dist path — didn't match the step bundle's registration ID — generated from the src path — producing "Step function not registered" failures at runtime, most visibly with @workflow/ai's doStreamStep on Vercel and Windows Next.js deployments. App code that imports a package by name should resolve naturally through pkg.exports; the loader has no business reaching into the package's source tree. Drop the rewrite (and the now-unused `resolveCopiedStepImportTargetPath` helper that supported it). Workspace packages are still discovered — that's a separate predicate (`shouldPreferSourceBackedPackagePath`) which only gates inclusion, not path translation. Verified locally with the nextjs-turbopack workbench: agent e2e suite (19 tests, including the failing `agentBasicE2e`) and the addTenWorkflow duplicate-name suite all pass. * Address review nits: extract stripPackageVersion, expand duplicate-ID hint, note new build-time check in changeset --------- Co-authored-by: Nathan Rajlich <n@n8.io> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · f5f6d0ed · 2026-06-10
- 1.1ETVRedrive on transient workflow-server transport failures instead of failing the run (#2445) * Redrive on transient workflow-server transport failures instead of failing the run A firewall in front of workflow-server shedding load with sustained 429/503 makes undici's shared RetryAgent exhaust its retries and throw UND_ERR_REQ_RETRY. That raw error was rethrown unwrapped, so it was classified as USER_ERROR and the replay terminal branch wrote run_failed — permanently failing a run on a transient blip (or, in an outage, falling back to the ~5min queue visibility-timeout redrive). - world-vercel: map exhausted-retry / socket / connect / DNS / timeout failures to a typed WorkflowWorldError (code TRANSPORT/TIMEOUT) by walking the fetch() cause chain. - core: add isRetryableWorldError (429 / 5xx / TRANSPORT / TIMEOUT) and rethrow such errors from the replay terminal branch so the queue redrives quickly (1s->60s backoff) instead of failing the run. Reuse it in start() and step_started handling. - world-vercel: surface the Vercel firewall x-vercel-mitigated (challenge/deny) header alongside x-vercel-id in error diagnostics and logs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review: back off + cap on every retry path; fix mock; refine scope Revises the transport-error handling per PR review (VaguelySerious, karthikscale3). Blocking fix — step_started no longer self-enqueues a throttled defer for transient world errors. Returning `{ type: 'throttled', timeoutSeconds: 1 }` acked the delivery and enqueued a fresh message, resetting the delivery count so the path never backed off and never reached MAX_QUEUE_DELIVERIES — an unbounded flat-1s loop if step_started kept failing. It now throws, so the error flows through the replay loop's retryable-world-error rethrow and earns both the delivery-count backoff and the max-delivery cap. Throwing is safe on step_started (the body hasn't run; a write that landed dedupes to skipped). Also in this revision: - Backoff that lasts: raise the queue handler-error retry ceiling 60s -> 900s. VQS clamps each redelivery to its 900s SQS limit and adds its own post-32 exponential, so ramping our base toward 900s stretches survival from ~3.7h to most of the 24h message-visibility window. Corrected the stale MAX_QUEUE_DELIVERIES comment to match the real VQS schedule. - Stop amplifying firewall challenges: the undici RetryAgent no longer retries 429 in-process (a challenge is a 429 the client can't solve). 429s surface immediately as ThrottleError carrying x-vercel-mitigated / x-vercel-id, so the diagnostic header now reaches us for the challenge case too. - Track world faults as WORLD_CONTRACT_ERROR (not USER_ERROR) in classifyRunError so an outage isn't attributed to user code. - Fix queue.test.ts mock that `biome check --write` had rewritten from a newable `function` into an arrow (broke `new QueueClient`); pin with a biome-ignore. All Vercel-specific logic stays in @workflow/world-vercel; @workflow/core operates only on the generic WorkflowWorldError abstraction. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Update .changeset/transport-error-redrive.md Signed-off-by: Peter Wielander <mittgfu@gmail.com> * Trim changeset to a single sentence per review Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Route firewall-challenge 429s to the retryable transport path, not ThrottleError A 429 carrying `x-vercel-mitigated: challenge` is a firewall challenge our server-to-server client cannot solve, so it recurs for the life of the incident. Mapping it to `ThrottleError` meant the `step_started` write deferred it as `{ type: 'throttled' }`, which self-enqueues a FRESH queue message and resets the delivery count — so it never backed off past `retryAfter` and never reached `MAX_QUEUE_DELIVERIES`, hot-looping against an already-overloaded firewall (the exact amplification this PR set out to remove, and contrary to the "step_started can't loop unbounded" invariant, which only held for 5xx). Map a challenge to a retryable transport `WorkflowWorldError` (`code: 'TRANSPORT'`) in both the v3 `makeRequest` and v4 `throwForErrorResponse` (the hot event-write path) error mappings, via a shared `isFirewallChallenge429` helper. It then propagates through the V1/V2 step paths and the replay loop's retryable-world-error rethrow, earning the delivery-count backoff AND the delivery cap. A genuine application-level 429 (no `challenge` mitigation) stays a `ThrottleError` and keeps its `Retry-After`-paced defer. Also correct the survival-window comments: with the 900s ceiling, MAX_QUEUE_DELIVERIES=48 spans ~9-10h (~35,000s), not "the better part of 24h"; reaching 24h would need a higher delivery cap, not a higher per-hop ceiling. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com>github.com-vercel-workflow · 603ad976 · 2026-06-29
- 0.9ETV[codex] Guard event consumers during replay (#2030) * Guard event consumers during replay * Address event guard review feedback * Update event guard test fixturesgithub.com-vercel-workflow · b124365e · 2026-05-20
- 0.9ETV[RFC] feat(nitro): embed observability dashboard in-process at /_workflow (#2548) * feat(nitro): embed observability dashboard in-process at /_workflow Serve the @workflow/web observability UI inside the Nitro process at a configurable route (default /_workflow) instead of spawning a separate web server and 302-redirecting to it. Enabled in dev, omitted from production builds by default (so prod bundles carry no @workflow/web import). Never mounted on Vercel deploys (use the hosted dashboard). - @workflow/web: add a framework-neutral `@workflow/web/handler` (createWorkflowWebHandler) that serves SSR + static client assets + RPC as one Web Request->Response handler under a runtime basename (asset manifest URLs + publicPath are reprefixed so the dashboard is self-contained under its mount). Add `@workflow/web/registry` for embedded-dashboard discovery; make the RPC/stream client basename-aware. - @workflow/nitro: mount the handler in-process (Nitro v2 h3 + v3 native paths), gated by a new `dashboard` option (default = dev). - @workflow/cli: `workflow web` / `inspect --web` defer to a running embedded dashboard instead of starting a redundant server; pass `--standalone` to force the standalone UI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(nitro): normalize dashboard path once, use isNitroV2() helper Address review feedback on the embedded dashboard: - Normalize the dashboard mount path in one place before it feeds both the Nitro route registration (`[path, path + '/**']`) and the handler `basename`. Force a single leading slash, strip trailing slashes, and reject the root mount, so a custom `path` can't make the route and the handler's internal `normalizeBasename` disagree. - Replace the handler-level `!nitro.routing` v2 checks with the existing `isNitroV2()` helper for consistent v2/v3 detection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com>github.com-vercel-workflow · 25715d45 · 2026-07-29
- 0.9ETVdocs: make /worlds the canonical home for World docs (#2934) * docs: make /worlds the canonical home for World docs The world pages (Local/Postgres/Vercel) and Building a World were duplicated inside the v4 and v5 docs trees while /worlds/[id] rendered the v4 copy — hiding v5-only content like multi-region and leaving two diverging sources of truth. - Move world docs to an unversioned docs/content/worlds/ collection (based on the v5 copies, with inline 4.x callouts for factory naming and 5.x-only env vars), rendered at /worlds/* - Add /worlds/building-a-world; flatten the docs Deploying section to a single intro page and drop its Rocket icon - Point every link, frontmatter ref, and worlds-manifest docs field at /worlds/*; add redirects for the removed v5 and building-a-world URLs - Keep world docs on agent-facing surfaces: search, llms.txt, sitemap.md/.xml, and .md exports now serve the worlds collection - Extend the docs link linter to validate worlds pages (with heading anchors) and their outgoing links Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs: version the world docs like the docs trees (v4/v5 switcher) Instead of a single unversioned copy, world docs now follow the same versioning strategy as the docs pages: content/worlds/v4 is served at /worlds/* (current) and content/worlds/v5 at /v5/worlds/*, restoring the original per-version content. Each world detail page (and Building a World) renders the docs version switcher — the worlds listing page has no natural home for it, so it lives on the world pages themselves. - Render-time href rewriting on v5 pages now covers /worlds/... links (shared rewriteHrefForVersion helper, also used by the v5 docs and cookbook routes), and the markdown-export rewrite does the same - v5 world pages are noindexed with a canonical to /worlds/<id>; community worlds stay unversioned (/v5/worlds/<id> redirects) - /v5/docs/deploying/world/* redirects now land on /v5/worlds/*; /v5/worlds and /v5/worlds/compare redirect to the unversioned pages - Link linter models the versioned worlds URL spaces (v5 pages resolve /worlds hrefs against the v5 collection); sitemap.md and the .md export routes cover /v5/worlds/* Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs: fix v4 multi-region anchor and tighten version-prefix matching Address PR review: - The v4 Deploying page linked /worlds/vercel#multi-region, but the Multi-region section only exists on the v5 world page; use the explicit cross-version /v5/worlds/vercel#multi-region link (this was the Docs Links CI failure) - rewriteHrefForVersion now uses the boundary-checked hasPathPrefix (shared leaf module lib/geistdocs/path-prefix.ts, also used by source.ts) instead of bare startsWith - buildVersionUrl's shared-route fast path is segment-based rather than substring includes() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 8a872529 · 2026-07-16
- 0.9ETVdocs: document run idempotency (#2011) * docs: document run idempotency * docs: address idempotency review feedback * docs: make hook tokens the idempotency pattern * docs: address toolbar idempotency feedback * docs: clarify idempotency page description * docs: scope idempotency descriptions * docs: move step idempotency example under section * docs: simplify idempotency guidance * docs: simplify idempotency cookbook * docs: add empty changeset Signed-off-by: Nathan Rajlich <n@n8.io> * docs: address idempotency review feedback * feat: add hook ready promise * docs: mention conflicting hook run id * test: cover hook ready continuation scheduling * feat: replace hook.ready with hook.hasConflict (Promise<boolean>) - hook.hasConflict resolves true when the token is owned by another active hook, false once registration is committed — no throw, so workflows can branch on conflicts early. Awaiting it suspends the workflow to commit the hook registration (createHook alone does not). - Chain the already-created fast-path through promiseQueue so resolution order matches event-log order (review feedback). - Skip inline step execution when a suspension has an awaited hook creation so the hasConflict continuation can advance independently of step execution (review feedback). - Update unit tests, e2e tests, workbench workflows, and v4/v5 docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix inconsistent hasConflict bullet in create-webhook reference State both resolution values explicitly (true = token already owned, false = registered) instead of a parenthetical that only described the false case. * docs: require docs preview links in PR descriptions for docs changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore SWC Plugin heading in AGENTS.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: adopt hook.hasConflict in run idempotency docs - Primary claim pattern is now `if (await hook.hasConflict)` instead of try/catch on HookConflictError; payload awaits still reject with HookConflictError (with conflictingRunId) when the owner's run ID is needed. - Route example returns the active owner via resumeHook()'s runId instead of threading conflictingRunId through the workflow result. - Update claim-pattern prose across start(), getHookByToken(), world storage, scheduling, workflow composition, and cookbook idempotency pages (v4 + v5). - Add @skip-typecheck marker to the cross-block route sample, fixing a pre-existing docs typecheck failure. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: move resume-or-start guidance into a dedicated resumeHook example The early callout was too vague and out of place at the top of the API reference. Replace it with a 'Resume or Start' example section that explains the flow, shows the resume-first/start-then-retry route, and links to the run idempotency pattern. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue The 'Resume or Start' example returned the just-started run's runId with reused: false even when a concurrent request's run won the token race — the payload had reached the actual owner, so the response pointed callers at a run that exits as a duplicate. The foundations route handled the race correctly but by awaiting run.returnValue, blocking the HTTP response on full workflow completion. resumeHook() always resolves against the actual active owner, so comparing the resumed hook's runId with the started run's runId detects the race in both examples — race-correct and non-blocking. * feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>) hasConflict's boolean didn't expose WHICH run owns the token, so the duplicate run couldn't act on the conflict. getConflict resolves with null once registration commits, or with a Run handle for the conflicting run — letting the workflow return/log the owner's runId, inspect its status, await its result, or cancel it and continue, all in code. The workflow-mode create-hook module exposes the bundle's compiled Run class (durable step-proxy methods) on a well-known symbol so the host- side hook consumer can construct the conflicting run inside the VM. Contexts without the class (plain unit tests) fall back to a { runId } object, which is also the documented v4 shape (no native Run serialization in v4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: adopt hook.getConflict and add conflict-handling strategy guide Run idempotency docs now use getConflict (resolves with the conflicting Run in v5, { runId } in v4) and document code-driven conflict strategies in place of static ID-reuse policies: reject the duplicate, adopt the owner's result, inspect before deciding, signal the owner via resumeHook, and supersede via cancel-and-reclaim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: never resolve getConflict with a non-Run fallback shape getConflict's contract is Promise<Run | null>. In the degenerate cases where a real Run cannot be constructed — a hook_conflict event persisted by an old world without conflictingRunId, or a context that never loaded the workflow-mode create-hook module — reject with HookConflictError instead of resolving with a { runId }-shaped impostor. Test harnesses now register the Run class on the (VM) globalThis like real bundles do. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor: make getConflict a method — hook.getConflict() A property getter that triggers registration/suspension reads as passive state; a method makes the side effect explicit. Update implementation, types, tests, e2e workflows, docs, and changeset. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: getConflict is a method — hook.getConflict() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: typecheck every sample — drop skip-typecheck escape hatches Route examples typecheck as-is since the runId-comparison rewrite; strategy fragments are now complete self-contained workflows; the publishing-libraries cross-block dependency uses the declare @setup convention. 934 samples typechecked, none skipped by this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * review: guard Run class registration, fix anchors, clarify changeset - Only register WORKFLOW_RUN_CLASS when the workflow runtime is present (WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the workflow-mode module neither mutate the host global nor expose the non-step-proxy host Run. - Drop #run-idempotency link fragments — that section lands in the stacked docs PR (#2011), which restores the anchored links. - Note in docs that getConflict() rejects with HookConflictError for legacy hook_conflict events lacking the owner's run ID. - Changeset now calls out the hasConflict -> getConflict() replacement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore run-idempotency anchors now that the section exists here Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: describe fixed conflict policies generically, without naming other systems Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Nathan Rajlich <n@n8.io> Co-authored-by: Nathan Rajlich <n@n8.io> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>github.com-vercel-workflow · 5dbeecbb · 2026-06-14
- 0.9ETVperf(core): lazy inline step start (save one world round-trip per step) (#2478) * perf(core): lazy inline step start to save a world round-trip per step The owned-inline runtime path used to write step_created (suspension handler) and then step_started (executeStep) as two separate world round-trips for a step it already owns and is about to run inline. This defers the step_created write: executeStep sends a single step_started carrying the step input, and the world creates the step on the fly (materializing the step entity plus a synthetic step_created event so replay still observes it). Mirrors the existing resilient run_started -> run_created pattern. Exactly-one ownership is preserved by the world's atomic create-claim: the loser of a concurrent lazy step_started gets EntityConflictError, which executeStep maps to `skipped`, so it never runs the body. A lazy step_started is only ever sent for a brand-new step (the suspension handler defers only steps with no prior step_created), so crash recovery still re-runs a `running` step via the normal non-lazy step_started. Worlds updated: world-local, world-postgres (implicit create + synthetic step_created event), world-vercel (routes the input as the v4 frame payload and threads the server's stepCreated flag). @workflow/world adds optional `input` to step_started and a `stepCreated` EventResult signal. Rollout: server-first. The matching workflow-server change must deploy before this ships; the Vercel world targets a single Vercel-operated backend (server always >= SDK). For local/postgres the world ships in the same package as the runtime, so there is no version skew. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(core): materialize deferred step before failing unregistered step on lazy inline path The lazy inline step-start optimization defers a step's step_created write, expecting executeStep to materialize the step via a lazy step_started carrying its input. For an UNREGISTERED step, executeStep bails out before sending that step_started and writes step_failed directly — but the step entity was never created, so the world's "step must exist" ordering guard rejects the step_failed and the run wedges (times out). This regressed the StepNotRegisteredError e2e tests uniformly across every framework/world (the ghost step never reached `failed`). Fix: on the lazy path, send the lazy step_started first to materialize the step (entity + synthetic step_created, keeping replay correct), then write step_failed. The lazy step_started's atomic create-claim preserves exactly-one-owner: a concurrent winner makes ours reject with EntityConflictError → skipped, so the failure is never written twice. Adds world-level regression tests (world-local, world-postgres) asserting a lazy step_started followed by step_failed marks the step failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · e7ef9d82 · 2026-06-18
- 0.8ETVfix(core): resolve forwarded stream keys across deployments (#2191)github.com-vercel-workflow · 8f68d352 · 2026-06-01
- 0.7ETVdocs: replace migration guides with a Comparisons section (#2676) * docs: replace migration guides with a Comparisons section Add a Comparisons section (v4 + v5) with an index/snapshot across all frameworks and deep-dive pages for Temporal, Cloudflare Workflows, AWS Step Functions, AWS Bedrock AgentCore, Inngest, and trigger.dev. Remove the old migration-guides section, folding its concept-mapping and migration content into the relevant comparison pages, and repoint top-level nav in both versions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: refresh comparison pages with current facts (July 2026) Re-verified each comparison against the vendor's current public docs and updated what changed since the June 2026 snapshot: - Temporal: Worker Versioning is now GA; Serverless Workers (AWS Lambda, pre-release) scale to zero, so soften the blanket "no scale-to-zero"; drop the unsubstantiated "Uber" customer claim (Uber is Cadence's origin, not a Temporal customer). - Cloudflare Workflows: note the new per-step billing dimension (500K/mo included, then $0.80/100K) landing no earlier than Aug 10, 2026; note the 50K concurrency ceiling was raised from 4,500 at GA. - AWS Bedrock AgentCore: add newer GA modules (Harness, Policy, Evaluations); correct compliance (SOC/PCI/ISO under internal assessment, audits pending; FedRAMP not yet authorized; drop GovCloud claim); refresh languages (@aws/agentcore CLI scaffolds TS or Python); "some modules preview" is stale. - Inngest: Pro pricing $75 -> $99/mo; encryption middleware now TS + Python; AgentKit/Realtime are Developer Preview and Connect is public beta; self-host is community/best-effort (not "unsupported"); Free-tier run duration 30 days vs 366 on Pro; soften funding to ~$30M+. - AWS Step Functions & trigger.dev: facts re-confirmed; date stamp only. Bumped every "as of June 2026" stamp to July 2026. v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: present comparisons in the present tense; v5-only; Pro usage-based pricing Follow-up pass on the comparison pages: - Remove all date references (past and future). Anything that lands on a date is stated as already in effect: Cloudflare's per-step billing, Temporal Serverless Workers and GA Worker Versioning, AgentCore's Harness/Policy/ Evaluations modules. Dropped "as of July 2026" stamps, founding/GA years, funding round dates, and roadmap/"being added" phrasing. - Workflow SDK: reference v5 only and treat it as GA (was "v4 GA / v5 beta"). - Pricing and limits: quote the Pro/paid tier only and usage-based rates only; drop plan-included quotas and free-tier allowances (Step Functions 4K/mo free, Cloudflare 500K steps/mo included, Inngest 50K free execs, Inngest/Free 30-day run cap, Temporal $100/mo plan minimum). v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: tighten comparison maturity/status wording - Drop dateless "upcoming change" phrasing: AgentCore compliance now states current facts only (no "self-assessed"/audit-pending implication); remove Inngest's "SSPL → Apache after 3 yrs" license-conversion note. - Don't label the Workflow SDK "GA" — non-beta is the default; also drop bare "GA" where it only meant "not beta" (Temporal "7 SDKs", Inngest "TypeScript", competitor maturity cells). - Maturity cells no longer cite version numbers; they describe backing/track record instead (e.g. "Built and maintained by Vercel", "Backed by AWS"). v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: make "features without a 1:1 equivalent" sections directional Rename each heading to name the competitor that has the feature (e.g. "Temporal features without a direct Workflow SDK equivalent") and add a lead-in clarifying these are the competitor's capabilities the Workflow SDK doesn't replicate one-to-one, with how to cover each on the Workflow SDK side. v4 and v5 kept identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: point world links at the /worlds routes The comparison pages linked to /docs/deploying/world/* and /docs/deploying/building-a-world, which no longer exist in the docs trees (the Docs Links check rejects them on v5 pages, where /docs hrefs are render-rewritten and skip the legacy redirects). Link the canonical /worlds/* routes directly, in both body links and frontmatter refs. v4 and v5 kept identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: address toolbar review feedback on the comparison pages - Drop the Maturity row from every at-a-glance table - Add a "what the limits mean in practice" paragraph to each comparison, spelling out what the competitor's caps mean for long-running AI workloads, and link Vercel World limits to the pricing doc - Security cells: lead with zero-config per-run E2E encryption and note platform security is per-World, instead of the VM-sandbox framing - Temporal: drop the throughput sentence and the still-in-preview Serverless Workers mention from the performance cell - Cloudflare: end the recommendation on "already all-in on Cloudflare" - Convert the "features without a direct equivalent" bullet lists into two-column tables so it's unambiguous which product owns each feature - Fix the Inngest page's "no step cap" cell (Vercel World caps runs at 10K steps per the pricing doc) v4 and v5 kept identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com>github.com-vercel-workflow · 918a2c55 · 2026-07-21
- 0.7ETVAdd `hook.hasConflict` for early hook conflict detection (#2015) * feat: add hook ready promise * test: cover hook ready continuation scheduling * feat: replace hook.ready with hook.hasConflict (Promise<boolean>) - hook.hasConflict resolves true when the token is owned by another active hook, false once registration is committed — no throw, so workflows can branch on conflicts early. Awaiting it suspends the workflow to commit the hook registration (createHook alone does not). - Chain the already-created fast-path through promiseQueue so resolution order matches event-log order (review feedback). - Skip inline step execution when a suspension has an awaited hook creation so the hasConflict continuation can advance independently of step execution (review feedback). - Update unit tests, e2e tests, workbench workflows, and v4/v5 docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix inconsistent hasConflict bullet in create-webhook reference State both resolution values explicitly (true = token already owned, false = registered) instead of a parenthetical that only described the false case. * docs: require docs preview links in PR descriptions for docs changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore SWC Plugin heading in AGENTS.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Nathan Rajlich <n@n8.io>github.com-vercel-workflow · e1634225 · 2026-06-11
- 0.6ETVfix(core): make deploymentId 'latest' a no-op in non-Vercel worlds (#2397) * fix(core): make deploymentId 'latest' a no-op in non-Vercel worlds Previously, start({ deploymentId: 'latest' }) threw a WorkflowRuntimeError in any World that doesn't implement resolveLatestDeploymentId() (local dev, Postgres). That meant a workflow which opts into 'latest' on Vercel would fail outright in local development. Resolving 'latest' only means something in worlds with atomic, immutable deployments. In other worlds there is nothing to resolve between, so instead of throwing we now log a warning and fall back to the current deployment, making 'latest' an effective no-op there. - start.ts: warn + fall back to currentDeploymentId instead of throwing - start.test.ts: replace the "should throw" test with a warn + fallback test - e2e.test.ts: assert 'latest' completes (no-op) on non-Vercel worlds - docs: note the no-op behavior in v4 + v5 start.mdx Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(core): warn once for deploymentId 'latest' no-op; harden test cleanup Address PR review: - Gate the 'latest'-has-no-effect warning behind a once-per-process guard (mirrors the warnOnce pattern in constants.ts) so a workflow that hardcodes 'latest' for Vercel doesn't flood local/Postgres dev logs on every run. Exposes _resetLatestNoOpWarnForTests() (@internal) for unit tests. - start.test.ts: reset the guard in beforeEach and restore spies in afterEach via vi.restoreAllMocks() so a throwing assertion can't leak the runtimeLogger.warn spy into later tests; drop the manual mockRestore(). - Add a test asserting the warning fires exactly once across repeated 'latest' starts while every run still falls back to the current deployment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · 4b7a7203 · 2026-06-16
- 0.6ETV[world-local] Reduce sequential replay I/O (#2152) * [world-local] Reduce sequential replay I/O * Fix relative local event cache lookups * Keep event cache eviction test lightweight --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · fc5bdcb0 · 2026-06-30
- 0.6ETVperf(core): skip per-step events.list via inline event-log delta (#2475) * perf(core): skip per-step events.list via inline event-log delta In the inline sequential loop, the runtime re-read its own just-written step events with an incremental events.list every iteration — pure latency on the Vercel world. Add an opt-in CreateEventParams.sinceCursor so a step-terminal write can return the event-log delta since that cursor (EventResult.events/cursor/hasMore), and have the inline loop consume it in place of the fetch. The delta is computed identically to events.list against the same log, so the consumed prefix is byte-for-byte what a fetch would return. The fast path is gated conservatively to the single-step sequential case with no open hooks/waits (so no out-of-band hook_received/wait_completed can land in the snapshot→replay window), and falls back to the normal fetch on any World that does not return a delta. world-local implements the delta; world-vercel/world-postgres are unchanged and fall back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * perf(world-vercel): forward sinceCursor over the v4 wire for inline delta Adds `sinceCursor` to the v4 POST frame meta so a step-terminal write can ask the server for the authoritative event-log delta on the response (events/cursor/hasMore), letting the inline loop skip a follow-up events.list. The server-side computation ships in vercel/workflow-server#538; older servers ignore the field and the runtime falls back to events.list (no behavior change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(inline-delta): cover truncated multi-page delta -> hasMore fallback The inline-delta query in world-local intentionally omits a `limit`, so a delta larger than one page is truncated and reports `hasMore: true`. The runtime consume gate only stashes a delta when `!hasMore` and otherwise falls back to the exhaustive `events.list` loop, so a partial page can never be consumed as the complete delta. Make that contract explicit with a comment at the query site, and add tests pinning it: a world-local test proving the delta truncates and surfaces `hasMore: true` byte-identically to `events.list(sinceCursor)`, and an executeStep test proving `hasMore: true` is threaded verbatim. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(world): clarify sinceCursor returns the first delta page, not the full set The CreateEventParams.sinceCursor docstring said the result is "exactly the delta an events.list(...) call would return," which read as the full set. It is the first page of that delta; hasMore signals more. Spell out the single-page-or-fallback contract so other World adapters implement sinceCursor consistently, and note that an in-band burst larger than one page bypasses the fast path (correct, but forgoes the saved round-trip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>github.com-vercel-workflow · 2074f91b · 2026-06-18
- 0.6ETVRetry replay divergence before failing event logs (#2212) (cherry picked from commit 813cd9a9de0592f9660c0384c1d49e54be7b1dcb)github.com-vercel-workflow · 2a3b11bc · 2026-06-02