Commit Graph

1306 Commits

Author SHA1 Message Date
github-actions[bot] 8aeb0a4c4a Version Packages (beta) (#2540)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@5.0.0-beta.21
2026-06-22 13:35:36 -07:00
Pranay Prakash 66ca0dcc09 perf(core): memoize step return value hydration across inline replays (#2472)
* perf(core): memoize step return value hydration across replays

The inline replay loop re-executes the workflow body and re-consumes the
full event log on every iteration. For each already-completed step, the
step consumer re-decrypted and re-devalue-parsed the serialized result on
every replay — O(N^2) decrypt+parse operations across a single
invocation of a sequential N-step workflow.

Add a per-run memoization cache, owned by the inline loop in runtime.ts
(alongside cachedEvents) so it survives across replay iterations of the
same run but never leaks across runs. It is threaded into runWorkflow and
stored on the orchestrator context, and consulted in the step_completed
path keyed by the persisted event id. This makes a completed step's
hydrated result O(1) on subsequent replays, turning the aggregate cost
into O(N).

Determinism is preserved: the cache lookup happens inside the existing
ctx.promiseQueue slot and still resolves via the same resolve(), so a
cache hit occupies the identical position in the ordered delivery chain a
re-hydrate would have — pendingDeliveries accounting, delivery barriers,
and Promise.race/all replay are untouched.

Identity safety: hydrateStepReturnValue returns a fresh object graph each
call and each replay runs in a fresh VM, so sharing an object reference
across replays could let one replay's mutation leak into the next. Only
primitive results are memoized (immutable, reference-share == re-parse);
non-primitives re-hydrate fresh every replay, exactly as before. Hook,
wait, and abort hydration paths are intentionally left uncached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(core): bound memoized step-hydration cache by primitive size

Address the review note that the per-run step hydration cache was never
size-bounded: cached entries hold the decrypted/parsed plaintext of a
primitive step result for the whole invocation, on top of the serialized
bytes already retained in cachedEvents, so a long run returning large
strings could roughly double peak retained memory for those results.

Document the cache's memory characteristic (per-invocation, freed when the
invocation ends, bounded by primitive-returning step count) and cap the
only primitive types that can carry a large payload: string/bigint results
longer than MAX_MEMOIZED_PRIMITIVE_LENGTH (4 KiB) fall through to the
existing per-replay re-hydrate path instead of being memoized. Large
payloads are cheap to re-hydrate relative to their footprint, so this caps
the worst case at negligible cost. Other primitives are inherently small
and always memoized.

The cap only ever reduces what is cached, so deterministic replay is
unaffected: oversized values take the already-correct re-hydrate path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:30:09 -07:00
Peter Wielander 6de5ea5c2f [core] Fix abort signal not reflected in subsequent step (replay-ordering flake) (#2412) 2026-06-22 20:25:12 +00:00
JJ Kasper 896d45d6cc Amend lazy discovery removal changeset (#2560) 2026-06-22 13:16:23 -07:00
Peter Wielander 9f1a3110f2 [docs] Document minimum SDK version for using hook.getConflict (#2423) 2026-06-22 13:15:30 -07:00
JJ Kasper bc7a06a025 Update default CODEOWNERS (#2556) 2026-06-22 20:00:03 +00:00
JJ Kasper 5291f1549f Optimize and fix the default eager build mode (#2546) 2026-06-22 14:47:39 -05:00
Peter Wielander 6b1842e10d Revert "fix(world-vercel): cancel v4 event frame stream on early exit (#2547)" (#2554)
This reverts commit e3672e84f4.
2026-06-22 12:06:58 -07:00
Peter Wielander 3e82a12712 [core] Turbo mode: fast-path the first invocation (#2526)
On the first delivery of a run's first invocation, background run_started,
skip the initial event-log load, and force optimistic inline start so the run
reaches its first steps with no preceding network round-trips. Safe because the
first delivery has no concurrent handler to race the step create-claim; turbo
exits the moment a suspension creates a hook or wait, and is a no-op for every
other invocation. On by default; disable with WORKFLOW_TURBO=0.
2026-06-22 11:42:46 -07:00
JJ Kasper 57cccaf373 Remove lazy discovery from workflow/next (#2545) 2026-06-22 13:14:35 -05:00
Shohei Maeda e3672e84f4 fix(world-vercel): cancel v4 event frame stream on early exit (#2547) 2026-06-20 12:36:51 -07:00
Ben Sabic 5377e880c7 feat(docs): add eve and AI SDK to product switcher (#2543)
Wire the existing AI SDK logo into the OSS product switcher (above Flags
SDK) and add a new eve entry (text wordmark + Beta badge, linking to
eve.dev/docs) above it at the top of the list.

Co-authored-by: Ben Sabic <bensabic@users.noreply.github.com>
2026-06-20 09:40:26 -07:00
Peter Wielander 047ebd0368 [vitest] Fix local imports failing to load in test step bundles (#2351) 2026-06-19 20:04:41 -07:00
Peter Wielander b713d8417b [builders] Fix unicode-escape crash in workflow graph extraction (#2324)
Signed-off-by: EfeDurmaz16 <efebarandurmaz05@gmail.com>
Co-authored-by: EfeDurmaz16 <efebarandurmaz05@gmail.com>
2026-06-19 20:04:27 -07:00
github-actions[bot] a12b32cd0f Version Packages (beta) (#2495)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@5.0.0-beta.20
2026-06-19 09:17:42 -07:00
Karthik Kalyan 90efb9653c otel(world-vercel): inject trace context on v4 event requests (#2533)
* otel(world-vercel): inject trace context on v4 event requests

The v4 event path (createEvent / getEvent / listEvents) routes through
fetchV4 → global fetch with a custom undici dispatcher, bypassing both the
makeRequest path (where the explicit W3C trace-context injection lives) and
ambient undici auto-instrumentation. As a result, v4 event traffic from the
flow route carried no traceparent, so workflow-server could not parent its
spans to the invocation — its spans never joined the /flow execution trace,
even though v2/v3 reads/writes (via makeRequest) did join.

fetchV4 now calls injectTraceContextIntoHeaders before fetch, the single
choke point for all v4 create/get/list requests, mirroring makeRequest.
No-op when no OpenTelemetry SDK is registered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(agents): require trace-context injection on new world-vercel HTTP paths

Codify the guardrail that the v4 regression revealed: any outgoing
world-vercel request must call injectTraceContextIntoHeaders (auto-
instrumentation can't be relied on with the custom dispatcher / global fetch),
with a test in trace-propagation.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* changeset: make v4 trace-propagation note concise

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 07:02:35 -07:00
Nathan Colosimo b563126aa1 Bump undici to 7.28.0 (#2534) 2026-06-19 07:56:56 +00:00
Pranay Prakash 37312edd0a Default source maps to dev-on / prod-off (#2529)
* Default source maps to dev-on / prod-off

Inline source maps are embedded in the step bundle and the intermediate
workflow VM bundle, which bloats production function bundles (a problem for
the Vercel 250MB limit) even though maps only help when reading a stack trace.

Make the default environment-aware in @workflow/builders: inline in
development (next dev / nitro dev / Vite-based dev servers, detected via
config.watch or NODE_ENV=development) and off in production. The `sourcemap`
config option and `WORKFLOW_SOURCEMAP` env var still override in either
environment. A production build with no override also drops the
source-map-support shim from the Vercel step function.

Keep runtime stack remapping graceful and fast when maps are absent
(@workflow/core): short-circuit when no frame references the workflow file
and memoize the parsed map (or its absence) per bundle, so production failures
don't rescan the bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(e2e): make source-map expectations match dev-on/prod-off default

The e2e error-stack tests gate source-map assertions on hasWorkflowSourceMaps()
and hasStepSourceMaps(). Now that source maps default to off in production
builds, update those helpers:

- hasWorkflowSourceMaps(): false for all production builds (local prod,
  postgres, Vercel — keyed off DEV_TEST_CONFIG), and exclude nest in dev (the
  Nest integration builds with watch:false / no NODE_ENV=development, so its
  bundles have no maps).
- hasStepSourceMaps(): nest now resolves to a production build (maps off) in
  both dev and prod.

Add unit cases for the dev-vs-prod and nest behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 16:28:26 -07:00
Karthik Kalyan de91f20f68 otel: nest linked-mode invocations under the delivery context (route + execution in one trace) (#2527)
* otel: nest linked-mode invocations under the delivery context

Follow-up to the linked-trace mode shipped in #2363. In linked mode the
queue-delivered workflow.execute / step.execute spans were created as new
trace roots (root: true) with span links to both the delivery context and
the run-origin context. That split a single local invocation across two
traces: the framework route/server span and the workflow execution span
ended up in different traces connected only by a link.

This nests each invocation under its local delivery context instead:

- Drop `root: true` on the queue-delivered workflow.execute / step.execute
  spans so they become children of the active context — the framework
  route/server span when one exists, otherwise a clean root. One invocation
  (route handler, replay, inline steps, event writes) is now a single
  bounded trace.
- buildInvocationSpanLinks in linked mode now returns only the run-origin
  link; the delivery context is the parent, so it is no longer also a link.

The run-origin context remains a link (never a parent) and re-enqueues still
forward the original carrier unchanged, so a long-running run is still never
stitched into one giant trace across invocations. continuous mode is
unchanged. Everything remains a no-op when no OpenTelemetry SDK is
registered, and there is no dependency on any particular framework: with no
route/server span active, the invocation span is a clean root rather than an
orphan.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update .changeset/nest-linked-invocations-under-delivery.md

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

---------

Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-06-18 16:04:02 -07:00
Peter Wielander 84ccd40ea3 perf(core): parallel inline steps + optimistic lazy step start (#2516) 2026-06-18 16:03:21 -07:00
Nathan Rajlich 7aee0d4e4a perf(core): decouple workflow VM seed/clock from startedAt (#2525)
Derive the deterministic RNG seed from `runId:workflowName:deploymentId`
and the VM's initial fixed clock from the ULID timestamp embedded in
`runId` (via the new `runIdCreatedAt` helper). All of these inputs are
available the instant a queue message arrives, so the VM seed and clock
no longer depend on `startedAt` (set only after the `run_started`
round-trip). This is the prerequisite for starting VM initialization
earlier on the critical path.

This changes the seed-derived value sequence for a given run, so the
affected deterministic test fixtures are regenerated accordingly.
2026-06-18 22:21:58 +00:00
Peter Wielander 722bb7c6a2 [world-local] [core] Cache local dev server port per process (#2522) 2026-06-18 14:51:44 -07:00
Mitul Shah d575c7e1a9 Show pending runs as gray animated stripes in trace viewer (#2520)
* Show pending runs as gray animated stripes in the new trace viewer.

Pending was incorrectly using the blue running indicator; it now has its own segment status and gray stripe styling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add changeset for pending trace viewer indicator styling.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 14:17:00 -07:00
Peter Wielander fb5abbbaf2 [world-vercel] Route v4 event requests through global fetch (#2514) 2026-06-18 13:27:42 -07:00
Peter Wielander ab2e9b8d07 [core] Send workflowName with step events (#2511) 2026-06-18 12:43:40 -07:00
Karthik Kalyan 1332da3df9 Stamp run IDs on world spans (#2508)
* Stamp run IDs on world spans

* Apply suggestions from code review

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>

---------

Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-06-18 10:41:00 -07:00
Pranay Prakash a92c16debd Reject empty-string hook tokens in createHook() (#2490)
createHook() used `options.token ?? ctx.generateNanoid()`, so a nullish
token fell back to a generated one but an empty string `""` was accepted
verbatim — a meaningless, non-deterministic token that is almost always
an accidental value (e.g. an unset variable). Throw a clear error when an
explicit empty-string token is passed; `undefined`/`null` still
auto-generate, and non-empty strings are unchanged.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 09:41:36 -07:00
Pranay Prakash 939890d4c2 perf(core): cache compiled workflow-bundle vm.Script across replays (#2471)
* perf(core): cache compiled workflow-bundle vm.Script across replays

The inline replay loop calls runWorkflow on every iteration, and each call
re-parsed the entire workflow bundle string via vm.runInContext. For a bundle
containing many workflow definitions (the production shape: one workflow called
per replay), this re-scans every definition on every replay.

Cache the compiled vm.Script per process, keyed by (workflowCode, filename),
and run it against the fresh context instead of recompiling. Compilation is a
pure function of (code, filename), so the result is byte-identical to the
previous re-parse-every-time behaviour — determinism is preserved. filename is
part of the key because it drives source attribution in stack traces (consumed
by remapErrorStack).

Measured per-replay savings scale with bundle size (and multiply by replay
count): ~34% for a 50-workflow app, ~59% for 155 workflows, ~80% for 400.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* perf(core): bound script cache with LRU; soften determinism claim; add tests

Addresses review on #2471:

- Bound `scriptCache` to a small LRU (cap 8 bundle versions). Production
  serves one bundle per process so the bound is never reached; it exists for
  dev/watch mode, where each edit produces a new bundle string that would
  otherwise be pinned forever (~0.8MB/edit, monotonic). Touch-on-access keeps
  the latest bundle hot; evicting a `code` entry drops its per-filename scripts
  together, restoring pre-cache GC behaviour.
- Document precisely why keying includes `filename` (intentional: drives
  stack-trace attribution via `remapErrorStack`; NOT a dedupe key), and that
  the whole bundle is compiled once per distinct filename.
- Soften the "byte-identical including thrown errors" claim to
  same-workflow-function + same-`filename`-attribution, noting the one caveat:
  a lookup-expression error's line number shifts to line 1 of the separate
  lookup Script. Updated in both the code comment and the PR description.
- Add tests: cache-is-bounded regression (eviction past the cap), LRU recency
  (hot bundle survives churn), and a realistic multi-workflow collision test
  (distinct code/filename never returns the wrong Script, results carry their
  own bundle marker).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 06:37:45 +00:00
Pranay Prakash 16b36703e2 perf(core): drain consumable replay events synchronously (#2473)
* docs(core): document scheduleWhenIdle macrotask is load-bearing

Revert the synchronous consume-loop drain optimization: it caused a
replay divergence (ReplayDivergenceError on step_started →
CorruptedEventLogError) in the world-testing inline-batches parallel
workflow on the Windows CI runner. The per-event `process.nextTick` in
the consume loop is load-bearing — it guarantees at most one event is
consumed per macrotask, letting the cross-VM `resolve → workflow VM body
→ subscribe()` chain register the next operation's consumer before the
drain advances. A synchronous drain races ahead of that registration.

What remains is a documentation comment on `scheduleWhenIdle` capturing
why its initial `setTimeout(0)` must not be downgraded to a microtask
(empirically: queueMicrotask breaks hook/sleep Promise.race ordering →
CorruptedEventLogError). No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(core): use empty changeset for comment-only macrotask doc

The scheduleWhenIdle change is a pure code comment with no consumer-facing
effect, so it does not warrant a patch bump / changelog entry. Replace the
patch changeset with an empty one to satisfy the changeset-bot convention
without claiming a release. Per the PR template's `pnpm changeset --empty`
guidance for non-releasing changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>

* perf(core): drain consumable replay events synchronously

The EventsConsumer rescheduled `process.nextTick(this.consume)` after every
consumed event, so replaying N already-consumable events (structural
lifecycle events, step_created/step_started, completed deliveries) cost N
macrotask hops — O(N) per consume wave across a sequential replay.

Drain consecutively consumable events within a single synchronous pass
instead. This is safe because callbacks only ever consume events with a
consumer that is already registered; new consumers are registered by
workflow VM body code that runs asynchronously off ctx.promiseQueue after a
delivery resolve(). When the next event's consumer is not yet registered,
no callback consumes it and we fall through to the existing cross-VM-safe
deferred unconsumed-event check, exactly as before. A null end-of-events
sentinel never continues the drain, so it cannot spin past end-of-log.

scheduleWhenIdle is intentionally left unchanged: its initial setTimeout(0)
is load-bearing for cross-VM propagation (pendingDeliveries is already 0
between a delivery resolve() and the VM body registering its next
subscriber). Replacing it with queueMicrotask empirically breaks hook/sleep
Promise.race ordering (CorruptedEventLogError); a comment now records this.

Re-validated after a premature revert: the windows-unit flake that prompted
the revert reproduces on unmodified main at the same rate (local 8-way
harness: opt 4/80 vs main 7/80; main historical windows-unit ~13%), so it is
a pre-existing flake, not a regression from this change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Signed-off-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 06:26:13 +00:00
Pranay Prakash e7ef9d823b perf(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>
2026-06-18 05:51:35 +00:00
Pranay Prakash 2074f91b86 perf(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>
2026-06-18 01:29:28 +00:00
github-actions[bot] fe333088b7 Version Packages (beta) (#2491)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@5.0.0-beta.19
2026-06-17 17:20:44 -07:00
Peter Wielander 26fd184278 [world-vercel] Honor hasMore flag from v4 list pagination endpoint (#2486) 2026-06-17 17:15:48 -07:00
github-actions[bot] f193d6e8ef Version Packages (beta) (#2451)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@5.0.0-beta.18
2026-06-17 17:06:19 -07:00
Nathan Rajlich 744024458f Fix Next workflow module specifier root (#2455) 2026-06-17 13:47:07 -07:00
Peter Wielander 6aa1ce0054 [world-vercel] Send remoteRefBehavior=lazy on v4 metadata-only event listings (#2415) 2026-06-17 13:44:29 -07:00
Karthik Kalyan da373493d2 [swc-plugin] Fix eager discovery for object property steps (#2484)
* Fix eager discovery for object property steps

* Add changeset for object property step discovery
2026-06-17 12:44:21 -07:00
Mitul Shah 2599da0d8c fix(web-shared): align attributes panel styling (#2483)
* fix(web-shared): align attributes panel styling.

Remove the attribute count from the section title and tighten key-value rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Create little-sites-cover.md

Signed-off-by: Mitul Shah <mitulxshah@gmail.com>

---------

Signed-off-by: Mitul Shah <mitulxshah@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 18:53:04 +00:00
Mitul Shah 8f6973319d [web-shared] Auto-scroll trace viewer on J/K span navigation (#2366)
* [web-shared] Auto-scroll trace viewer on J/K span navigation

Bring the selected span into view when J/K (or the up/down chevrons)
navigate to a span outside the visible area. The event list is windowed
with fixed-height rows, so the target offset is computed from the row
index rather than relying on a DOM node that may not be mounted.

Co-authored-by: Cursor <cursoragent@cursor.com>

* [web-shared] Extract scrollRowIntoView helper from trace viewer

Relocate the J/K auto-scroll geometry math out of the large
NewTraceViewerContent component and into a small pure helper colocated
with the windowing primitives in use-row-window.ts. scrollRowIntoView
reuses the existing getScrollParent walker and takes the row height as a
parameter, so the trace viewer's scrollSpanIntoView is now a thin caller
that finds the span index and delegates.

No behavior change: the off-screen-only condition, one-row margin,
clamp to [0, scrollHeight - clientHeight], and reduced-motion behavior
are all preserved. getScrollParent(#event-list) resolves to the same
SplitPane scroll container the old code measured against directly, so
the net scroll geometry is identical.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Update trace-viewer.tsx

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:17:10 -07:00
Mitul Shah 9eb5b9f54d fix(web): render restarted step segment as solid gray, not running stripes (#2480)
When a step emits `step_started` twice in a row (a re-start with no
retrying/failed/completed in between), the interval between the two starts
was shown with the animated blue "running" stripes, implying active
progress. Treat that segment as gray ('retrying') instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:02:00 -07:00
Mitul Shah 0090788c45 fix(web-shared): use solid gray for queued trace segment (#2474)
* fix(web-shared): use solid gray for queued trace segment

The queued span segment used alpha gray tokens for its hatched fill,
making it look washed out. Switch to the solid gray tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix typo in queued trace segment color message

Signed-off-by: Mitul Shah <mitulxshah@gmail.com>

---------

Signed-off-by: Mitul Shah <mitulxshah@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 11:01:48 -07:00
Mitul Shah 2acf13cc72 Add trace viewer span markers for hooks and attributes (#2452)
* feat(web-shared): add event markers to the trace timeline

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update span-markers.tsx

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 12:34:26 -04:00
Nathan Rajlich b805a8d660 test: support Vercel protection bypass secret in e2e headers (#2458)
Allow local or CI-adjacent e2e runs to bypass deployment protection with a
Protection Bypass for Automation secret via VERCEL_PROTECTION_BYPASS. When
set, getTrustedSourcesHeaders returns x-vercel-protection-bypass; otherwise
existing GitHub Actions / VERCEL_OIDC_TOKEN trusted-sources behavior is
unchanged.
2026-06-16 23:59:20 -07:00
Nathan Rajlich 3c79c56af2 fix(core): bump payload-compression cutoff to 5.0.0-beta.18 (#2470)
The gzip/zstd FORMAT_VERSION_TABLE entries were gated on 5.0.0-beta.16,
but beta.16 and beta.17 were both published (2026-06-15) before the
compression PR (#2394) merged (2026-06-16) — neither contains the
compression read path. The next published version is beta.18 (pending
Version Packages #2451), which is the first that can decode these
payloads.

With the cutoff at beta.16, getRunCapabilities() reported beta.16/.17
targets as compression-capable, so a cross-deployment start()/resumeHook()
(or a resilient-start probe resolving to such a target) would write
zstd/gzip payloads the target cannot decode — silent replay corruption,
exactly the TODO(release) hazard noted on those lines.

Bump both entries (and the doc comments) to beta.18 and extend the
capability test to assert beta.16/beta.17 are treated as incapable.
2026-06-16 20:24:47 -07:00
Pranay Prakash cb181392b9 feat(cli): print run deep links with --url, fix dashboard route (#2467)
Add a `--url` flag to `inspect`/`web` that prints a run's observability
dashboard deep link to stdout and exits — no browser, no local server —
so scripts and agents can share a link instead of opening a UI.

Fix the Vercel dashboard URL to the current
`…/workflows/runs/<id>?environment=<env>` route (drop the legacy
`/observability` segment) and respect `--env`. Apply the same route fix
to the e2e helpers, CI aggregation scripts, and the nextjs-turbopack
workbench. Document deep-linking in the workflow skill and observability
docs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:37:03 -07:00
Pranay Prakash 5f0b845211 RFC: 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>
2026-06-16 23:27:47 +00:00
Pranay Prakash d0472511ca fix(deps): upgrade hono to 4.12.25 to resolve CVE-2026-54290 (#2462)
hono <4.12.25 is vulnerable to CVE-2026-54290 (GHSA-88fw-hqm2-52qc):
the CORS middleware reflects any request Origin with
Access-Control-Allow-Credentials: true when credentials are enabled and
origin is left at the default wildcard, exposing cookie-authenticated
endpoints to arbitrary origins.

- packages/world-testing: hono 4.12.21 -> 4.12.25 (the flagged manifest)
- workbench/hono: ^4.12.8 -> ^4.12.25, clearing the also-vulnerable
  4.12.9 from the lockfile

Neither app uses hono's CORS middleware, so neither was exploitable, but
the bump clears the vulnerable code from the dependency tree. Only the
core Hono class is imported in world-testing; build and typecheck pass.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:18:30 -07:00
Pranay Prakash 53ede3079c fix(swc-plugin): count destructuring-default references in DCE usage analysis (#2398)
The DCE usage collector skipped the entire variable name pattern when
visiting a `VarDeclarator` (to avoid marking the binding name as "used").
But default-value initializers inside destructuring patterns live in that
pattern — e.g. the `TTL` in `const { ttl = TTL } = options;` — so those
references were invisible to the collector. A module-scope `const`
referenced only through such a default was treated as unused and stripped,
while the surviving code kept reading it, producing a runtime
`ReferenceError` when the default fired.

Traverse the default-value initializer expressions (and computed keys)
within destructuring patterns while still not marking the binding names
themselves, so the referenced declaration is preserved. Function-parameter
defaults were already covered (params are visited in full).

Fixes #2396.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:16:13 -07:00
Pranay Prakash 4b7a7203bf fix(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>
2026-06-16 16:09:49 -07:00
Pranay Prakash b92dfbb94d fix(deps): upgrade astro to 6.4.6 to resolve CVE-2026-54299 (#2457)
Astro <6.4.6 is vulnerable to CVE-2026-54299 (GHSA-2pvr-wf23-7pc7, host
header SSRF in prerendered error page fetch). The fix only exists in the
6.x line — there is no 5.x backport — so this bumps:

- workbench/astro: astro ^6.4.6, @astrojs/node 10.1.4, @astrojs/vercel ^10.0.8
- packages/astro: astro devDependency 6.4.6 (typecheck only, not shipped)

Removes both vulnerable astro@5.16.3 and astro@5.18.0 from the lockfile.
Verified the example app builds under both the node and vercel adapters.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:18:36 -07:00