1816 Commits

Author SHA1 Message Date
Pranay Prakash f14c378846 fix(ci): read manifests from the commit in check-published (#4147) 2026-09-13 16:17:20 -07:00
Pranay Prakash 37f10111a8 fix(core): render the error stack in the run-failure log (#4145)
`runtime.ts` computes a source-map-remapped `errorStack` when a run fails and
passes it to a log whose framing line — "Error while running workflow" — has
no body of its own. `composeLogLine` drops `errorStack` unconditionally, on
the assumption (true for the step executor and the combined runtime, which
render `${framing}\n${stack}`, false here) that the message already carries
it. The stack never reaches the console; #4021 noted this at the call site
and worked around it by adding `errorMessage`, but the stack itself is still
discarded.

- `composeLogLine` promotes the `errorStack` field into the stack body when
  the message carries no body of its own, so it goes through the same frame
  trimming and is still never duplicated when the caller did embed one. This
  also lets #4021's existing `body.includes(errorMessage)` check suppress the
  now-redundant `error` row at this call site.
- The header row is skipped when it would be a lone class name that the
  promoted stack header already states. A badge still always renders —
  attribution is the one thing the stack cannot express — so the step
  executor and combined-runtime sites are untouched, as is a stack naming a
  different class than `errorName`.

Adds a runtime-level regression test that drives a throwing workflow through
`workflowEntrypoint` and asserts the emitted `console.error` line carries the
message and a stack frame, plus formatter unit tests for the promote,
don't-duplicate, badge and different-class cases. No existing snapshot
changes: the composition is identical everywhere except the run-failure log.

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
2026-09-13 00:15:37 +00:00
Michael Chen e1f7d2f694 docs: add Eveland to community Worlds (#4069)
Signed-off-by: Michael Chen <mechiland@gmail.com>
2026-09-12 08:48:29 -07:00
Pranay Prakash b360cb4d8d chore(changeset): trim the route-groups changeset to one line (#4143)
The changeset body is copied verbatim into the changelog, where the
rationale paragraphs from #4070 are noise. Keep the line that says what
changed and where it matters; the reasoning is already in the commit,
the PR and issue #3991.

This was reviewed on #4070 and fixed on #4140, a CI mirror of that PR.
#4070 is the one that merged and #4140 was closed, so the fix never
landed. The changeset is still unconsumed on `main`, so correcting the
file is enough — the open release PR regenerates from it and no
CHANGELOG.md has been written yet.

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
2026-09-12 05:09:22 +00:00
Matias Torsello d427c47e89 fix(core): allow Next.js route groups and dynamic segments in workflow names (#4070)
A workflow name is derived from the module path it is defined in, so Next.js
App Router conventions end up in the name verbatim. `SAFE_WORKFLOW_NAME_PATTERN`
permitted alphanumerics, `_`, `-`, `.`, `/` and `@`, but not parentheses or
square brackets. Any workflow inside a route group (`app/(dashboard)/...`) or a
dynamic segment (`app/[teamId]/...`, `app/[...slug]/...`) threw

    Invalid workflow name "workflow//./app/(group)/workflows/": must only
    contain alphanumeric characters, ...

before it could be enqueued, and the generated name cannot be overridden.

The pattern exists to keep unsafe characters out of the queue name it is
interpolated into. These four are inert there: `ValidQueueName` accepts any
suffix after its prefix, and the only other consumers of the name are OpenTelemetry
span names. It is never placed in a URL or a SQL identifier.

Fixes #3991

Signed-off-by: Matias Torsello <23641125+torsello@users.noreply.github.com>
Co-authored-by: Matias Torsello <23641125+torsello@users.noreply.github.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
2026-09-11 21:54:49 -07:00
github-actions[bot] fe8b27d183 Version Packages (beta) (#4071)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
@workflow/world-local@5.0.0-beta.44 @workflow/world-postgres@5.0.0-beta.42 @workflow/world-vercel@5.0.0-beta.46 @workflow/astro@5.0.0-beta.51 @workflow/nuxt@5.0.0-beta.51 @workflow/cli@5.0.0-beta.51 @workflow/core@5.0.0-beta.51 @workflow/world-testing@5.0.0-beta.51 @workflow/nest@5.0.0-beta.51 @workflow/ai@5.0.0-beta.16 @workflow/nitro@5.0.0-beta.51 @workflow/vite@5.0.0-beta.51 @workflow/world@5.0.0-beta.35 @workflow/rollup@5.0.0-beta.51 @workflow/builders@5.0.0-beta.51 @workflow/next@5.0.0-beta.51 @workflow/web@5.0.0-beta.51 @workflow/sveltekit@5.0.0-beta.51 @workflow/web-shared@5.0.0-beta.51 @workflow/swc-plugin@5.0.0-beta.7 workflow@5.0.0-beta.51 @workflow/vitest@5.0.0-beta.51
2026-09-11 17:25:33 -07:00
Peter Wielander 7e8e5dda2f fix(world-postgres): stream reader lifecycle cleanup and offset cursor (#4125) 2026-09-11 16:46:21 -07:00
Nathan Rajlich 03455a2979 Carry run identity on step-dispatch messages; drop the blocking runs.get from the queued-step prologue (#3457)
* Carry immutable run identity on step-dispatch messages; drop the blocking runs.get from the consumer prologue

Closes #3456. Every queued step execution paid a runs.get round trip
before its step_started claim — one RTT per branch on the TTLS-critical
path, and under a 256-branch fan-out burst the read amplification drove
that read to p90 ~5.1s (durabench parallel sweeps), smearing branch
starts.

The dispatch sites (node dispatch loop, delayed retries, the suspension
handler's resilient publish, and the quickjs engine's queueStepMessage)
now stamp WorkflowInvokePayload.runContext with the fields the consumer
actually needs — deploymentId, specVersion, startedAt, rootRunId — all
immutable for the life of a run and known from the run row the producer
already holds. A consumer that receives it skips the run fetch: the
run-status early exit is enforced by the step_started claim itself
(RunExpired → gone, terminal step → skipped), guardDeployment takes the
carried identity, and only the fan-out's LAST completer fetches the full
run row, lazily, for its inline replay — once per fan-out instead of
once per branch. The deployment-mismatch re-route now also preserves
stepInput/runContext on the re-enqueued payload.

Messages without runContext (older producers) keep the legacy prologue;
messages are deployment-pinned, so mixed handling within one run cannot
occur.

* Address review: terminal-only lazy status gate, terminal-run start fence in local worlds, prologue telemetry, last-completer coverage

- The last completer's lazy runs.get result is now gated on
  isTerminalWorkflowRunStatus (with a debug log): a stale 'pending' read
  — a run with completed steps has necessarily started — no longer
  silently abandons the fan-out's continuation; it falls through to the
  inline replay, whose next entity write is fenced server-side if the
  run truly ended meanwhile.
- world-local / world-postgres now reject step_started on terminal runs
  even when the step row still reads 'running' (a redelivered start a
  previous delivery claimed): starting work on a finished run is never
  valid, and previously the body re-ran with its outcome unconsumable.
  In-flight steps still write their terminal events unchanged. This
  closes the adapter gap behind the fetch-free prologue's reliance on
  the step_started claim as the run-liveness check, and the prologue
  comment now states the contract precisely.
- workflow.step.dispatch_prologue span attribute ('run_context' |
  'runs_get') makes fetch-free adoption and the saved round trip
  observable during version-skew windows.
- Restated why the eager redelivery re-ensure survives on the
  fetch-free path (no run fetch to overlap; still cheaper than the
  in-band recovery's failed-start round trip).
- New two-phase fan-out coverage: a real replay emits the queued step
  message (asserting the stamped runContext), then its redelivery runs
  as the LAST completer — zero reads before the step, exactly one lazy
  runs.get, run completed; plus the stale-'pending' fall-through and
  the genuinely-terminal skip.
2026-09-11 21:31:15 +00:00
Dmitry Petrov 2eb2fe6f4d fix(world-postgres): create runs and initial events atomically (#4111)
Signed-off-by: Dmitry Petrov <Komly@yandex.ru>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
2026-09-11 14:09:25 -07:00
Dmitry Petrov 3dad0a9d67 fix(world-postgres): advance stream cursors for skipped chunks (#4113)
Signed-off-by: Dmitry Petrov <Komly@yandex.ru>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
2026-09-11 14:08:49 -07:00
Pranay Prakash fb9e27589d perf(core): commit pre-claimed inline pairs in their own batch chunk (#4098)
* perf(core): commit pre-claimed inline pairs in their own batch chunk

The batched fan-out fold sorted the pre-claimed inline
[step_created, step_started] pairs to the front and then filled the same
chunk with plain step/wait creates up to MAX_BATCH_FANOUT_EVENTS. Only
that chunk gates the inline bodies, so the bodies waited on a 32-row
commit (a 61-item DynamoDB transaction on the Vercel backend) when all
they needed was the pairs' own rows.

Chunk the pair rows and the plain creates separately: the pairs get their
own leading chunk(s), still adjacent and never split across a boundary,
and the plain creates fill the subsequent chunks of 32, committing
concurrently beside the pair chunk and gating only their own queue
publishes. Eligibility, the batch-of-one rule, pairCommits gating,
per-chunk publishes and the foreign-interleaving diagnostic are unchanged.

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

* perf(core): fold pairs only with two inline steps; guard a lone plain create

With the pairs in a chunk of their own, plain creates alongside them share
no round trip with the pairs, so "company" no longer makes a lone pair
worth folding. `inlinePairFoldEligible` now requires two or more inline
steps: a lone inline step keeps the lazy `step_started` (one row,
optimistic-start capable, bump-and-report) while the eager creates beside
it still batch.

A plain partition of exactly one entry is the batch-of-one case again, so
it takes the guarded single create (slot-snapshot params, bump-and-report,
same conflict tolerance and `createdStepCorrelationIds` bookkeeping)
instead of a one-row createBatch, and its queue publish still waits for
that create.

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 20:47:50 +00:00
Alex Langenfeld 080c592567 test: persist E2E flake history (#4120)
## Summary & Motivation

Job conclusions hide tests that pass on retry, and the evidence only lived in overwritten PR comments and 7-day artifacts, so recurring flakes couldn't be ranked against how often they ran.

- retry sidecars are now named after the Vitest report they came from, so lanes, VMs, and worlds no longer overwrite each other when artifacts merge — and the aggregate comment can attribute a flake to an exact lane
- `generate-e2e-flake-history.js` publishes a bounded 30-run history to gh-pages: one series per (lane, app, world, vm, platform) × test, carrying both an executed count and a passed-on-retry count so a rate has a denominator
- the reporter always writes the sidecar, which is what lets a clean run be distinguished from a lane that reported no retry telemetry at all

## Test Plan

Unit tests added for dimension parsing, denominators, schema validation, and the 30-run window; verified every current report filename maps to explicit dimensions, and a one-run history built from a full artifact set came out around 100 KiB.
2026-09-11 15:41:26 -05:00
Nathan Colosimo c29200fac5 docs(ai): clean up WorkflowAgent docs and examples (#3891)
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
2026-09-11 13:39:43 -07:00
Peter Wielander 357aa7c38a [core] QuickJS engine reports its log position and consumes returned event pages; document who names a position (#4106) 2026-09-11 13:37:28 -07:00
Alex Langenfeld bbacc7ffc0 test: relax Windows CLI cancellation timeout (#4115) 2026-09-11 13:11:55 -07:00
Pranay Prakash 788d4fbc26 perf(core): don't re-publish step messages this invocation already published (#4099)
On a fan-out that runs some steps inline, the orchestrator invocation
publishes the queued siblings' step-execution messages, runs its inline
steps, falls back into the replay loop, reloads the log, and its
pending-step dispatch pass publishes every one of those messages again
(measured 19-28 re-sends per pass on a 32-branch fan-out, ~400 ms after
the originals). The queue dedupes them by idempotency key, but the sends
still cost round-trips on the shared connection pool and hold the
invocation open past its useful work.

Track, per delivery, the correlation ids this invocation has already
published a step message for (the suspension handler's resilient
publishes plus the dispatch pass's own immediate enqueues) and skip the
immediate re-enqueue for those on later passes, unless a step_retrying
has been observed since (a new schedule). The set is invocation-scoped
and never derived from the log: a step_created does not prove the message
was ever sent, so a different delivery still re-enqueues unconditionally.

Reported as a debug log and the `workflow.dispatch.republish_skipped`
span attribute. The QuickJS engine already keeps the equivalent
invocation-scoped `queuedStepIds` set, so it is unchanged.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 13:07:19 -07:00
Pranay Prakash 6cc851c342 [core] Stop sending a slot snapshot on step executor writes (#4096)
The only thing a World does with `eventCount` is bump-and-report: when the
write lands above the position named, it reads the events in between and
returns them so the writer can merge them without a second round-trip. The
replay loop and the suspension handler merge that page into their loaded
log. The step executor has no log to merge into, so it took the page's
highest position and discarded the rest.

In production that discarded read fell on a third of all `step_started`
writes (10.8M of 13.4M skipped-slot report reads per day were on executor
event types), each a strongly consistent DynamoDB query on the run
partition with resolved refs, on the response path. This removes the
executor's `knownSlot` / `observeSlot` machinery, the `slotSnapshot`
executor param, and the `batchCommittedSlotCeiling` the suspension handler
computed only to seed it. The loop's and the suspension handler's own
snapshots are unchanged; they consume their reports.

The World contract already describes omitting the count for a caller with
no loaded log to be stale against; the executor now matches it.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-11 12:57:25 -07:00
Pranay Prakash e00b1a57ee perf(world-vercel): batch a fan-out's step-execution queue publishes (#3838)
* perf(world-vercel): batch a fan-out's step-execution queue publishes

A `Promise.all` fan-out dispatched one queue message per branch. Those
publishes ride the shared default undici agent (8 connections, HTTP/1.1,
`pipelining: 1` — see `getQueueDispatcher`), and `handleSuspension` is
awaited in full before the first inline step body runs, so an N-branch
fan-out paid ~N/8 serialized round trips straight onto time-to-first-step.
The `step_created` writes were already batched and HTTP/2-multiplexed; the
publishes were the remaining per-branch round trip.

Adds an optional `Queue.queueBatch`, implemented on `@vercel/queue`'s
`experimental_sendBatch` (0.5.1), and uses it for the batched fan-out fold's
publishes. Each commit chunk now publishes in one request instead of up to
32.

`queueBatch` reports per-entry outcomes rather than throwing, because a
batch can partially fail. `queueMessages` in core keeps the previous
all-or-nothing behavior for this call site: it rejects if any entry failed,
so the delivery is redelivered and republishes the set, deduped by the
per-step `idempotencyKey` the caller already passed. Worlds without
`queueBatch` fall back to concurrent single sends.

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

* fix(core): reject a short queueBatch result set instead of reading it as success

`queueMessages` only inspected `error`, so a World whose `queueBatch`
returned fewer results than it was given messages reported success for the
whole batch. The omitted entries were never published and nothing raised:
`handleSuspension` resolved, the delivery was acked, and those steps were
never dispatched, so the run stalls with no error recorded anywhere.
Reproduced at 64 branches against a World returning half its results: 32 of
63 steps silently lost.

world-vercel guards this internally and `@vercel/queue` length-checks its
own response, so it was not reachable through the world added here. It is
reachable through the interface `building-a-world` opens to third-party
worlds, which is where the check belongs. Documented on the interface and
in the guide alongside it.

Also notes that the batch grouping degenerates to one request per message
under WORKFLOW_SEQUENTIAL_REPLAYS=1 (per-step physical topics are one of
the routing dimensions groups split on), and corrects the comment claiming
the error's `retryable` flag is consumed downstream: nothing reads it yet.

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

* fix(world-vercel): carry trace context on each batched queue message

`experimental_sendBatch` injects the active trace context into the multipart
REQUEST headers, and the per-part headers it builds never see it. VQS stores
headers per message and re-emits a stored `traceparent` at delivery as
`x-vercel-queue-traceparent`, which is what lets a consumer attach a span link
back to its producer, so a batched message arrived with no producer context
and its `vqs.process` span got no link. `send()` is unaffected: for a single
message the request headers ARE that message's headers.

At 64 branches that was 63 of 64 step dispatches losing the transport-level
producer link. The run's own step tracing was never affected: that carrier
travels in the message payload (`WorkflowInvokePayload.traceCarrier`), which
is what the consumer builds its trace context from, not a header.

Injects the active context into each entry's headers in `queueBatch` — last,
so it wins over caller-supplied `opts.headers` exactly as the SDK's own
injection does — and honors VERCEL_QUEUE_TRACE_PROPAGATION so that kill
switch still covers both paths. `getTraceContextHeaders()` is factored out of
`injectTraceContextIntoHeaders` so the two share one source.

Verified on the wire against a stub VQS speaking the real batch endpoint:
`traceparent` carrying the producer's traceId/spanId lands on all 64
multipart parts through the real SDK, with the per-message idempotency keys
still alongside it.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>
2026-09-11 11:23:33 -07:00
Michael J. Sullivan 0392d69fcf workflow docs: add a bunch of missing material (#4068)
* cancellable steps and use of asyncio.timeout
* typed streams
* hook return values
* share_sandboxes
* deterministic helpers
2026-09-11 10:48:31 -07:00
Alex Langenfeld d864efb07b test: tighten CI health signals (#4107)
## Summary & Motivation

- Manifest coverage now declares an entry for every matrix app, uses real Vitest skips instead of silent early returns, and fails when a targeted app's manifest is missing, unknown, or unparseable.
- Retries are scoped to deployment e2e runs (`DEPLOYMENT_URL` set), so a flaky unit or integration test can no longer be hidden by a second attempt.
- The stop-workflow cookbook parks on a sleep between iterations, giving the hook an observable barrier to race instead of a fixed delay, and the AbortController hook test waits on queue state rather than a 10ms timer.
- The world-postgres direct-storage fixture drives its run to a terminal state so the conformance worker doesn't recover and replay an unregistered workflow.
- Generated e2e result sidecars are ignored and the committed copies removed; they're CI artifacts, not fixtures.

## Test Plan

Existing coverage runs in CI. With retries disabled: the cookbook agent suite passed 8/8, the two stop-workflow tests passed 10/10, and the AbortController hook replay test passed 25/25 under `CI=1`. The manifest suite skips 52 apps explicitly when nothing is built, and fails on unknown or missing targeted apps. The Docker-backed Postgres spec could not run locally (Testcontainers found no container runtime); `@workflow/world-postgres` typechecks.
2026-09-11 12:39:17 -05:00
Alex Langenfeld 5fc8fb7a98 perf(streams): connect WebSocket after first write (#4104)
## Summary & Motivation

Keeps WebSocket setup off the first stream group, then starts the background upgrade when the second HTTP request is dispatched. Later groups continue over HTTP without waiting until the socket is OPEN, when the serialized writer switches transports at a confirmed request boundary. One-group streams never create a socket.

An ambiguous HTTP outcome poisons the writer and retires any provisional socket before it can carry a frame. Initial upgrades have a dedicated 10-second background timeout; the existing 250ms bound remains scoped to reconnects after an established socket closes. WS write spans include chunk sequence and count for direct takeover analysis.

## Test Plan

Tests cover dispatch overlap, continued HTTP writes while connecting, one-group streams, close during background connection, ambiguous HTTP outcomes, timeout and late OPEN behavior, and trace propagation. The world-vercel suite and typecheck pass locally.
2026-09-11 17:03:54 +00:00
Alex Langenfeld 01fa7a4158 perf(streams): avoid blocking first write on WebSocket (#4076)
## Summary & Motivation

While the first socket is still connecting, complete groups go over HTTP instead of parking on the handshake; the socket takes over once it opens. An HTTP-first write that fails poisons the writer rather than falling back, since its outcome may be unknown and replaying it over WS could duplicate a group.

## Test Plan

Tests added for transport switching, ordering against close, and the poisoned-writer path; the world-vercel suite passes locally.
2026-09-11 11:43:31 -05:00
Karthik Kalyan 86eb8229f8 Fix lazy v4 event metadata decoding (#4095)
* Fix lazy v4 event metadata decoding

Signed-off-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>

* Update .changeset/lazy-v4-event-metadata.md

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

---------

Signed-off-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>
Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-09-11 09:33:01 -07:00
Nathan Colosimo 7a46a81a53 Upgrade to Zod 4.5 and compile schemas (#3902)
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
2026-09-11 09:00:20 -07:00
Alex Langenfeld c09c1bb6ea fix(core): drain step stream writes before completion (#3941)
## Summary & Motivation

### Situation

- Workflow stream writers are expected to call `releaseLock()` when a step finishes writing so another step can acquire the stream.
- The runtime observes that release and drains the server sink, but `step_completed` currently races the overall stream operation against 500ms.
- A slow PUT can therefore continue under `waitUntil` while the next step starts and reads a stale tail.
- Release can also happen while native `writer.write()` promises remain unsettled, leaving frames upstream of the server sink when a naive drain runs.
- Writers intentionally kept locked must remain non-blocking so producer and consumer steps can overlap.

### Fix

- Treat a writer released before step return as an implicit durable handoff boundary.
- At step end, acquire the unlocked stream with a temporary writer and enqueue an internal checkpoint behind all writes queued by the released writer.
- Once the checkpoint crosses serialization, wait for those frames to reach the server sink and drain the group-commit PUT before `step_completed`.
- If the writer remains locked, do not wait for durability; preserve the existing 500ms inline-loop heuristic and background `waitUntil` lifecycle.
- Drain failures or the 30-second safety timeout fail/retry the step. Client disconnect errors remain non-fatal.

## Test Plan

- Covers released and held locks, release with unsettled writes, delayed first writer acquisition, forwarded writable arguments, drain timeout/failure, and multiple streams.
- `pnpm --filter @workflow/core build`
- `pnpm --filter @workflow/core typecheck`
- `pnpm --filter @workflow/core test` — 2,405 passed, 3 expected failures, 1 skipped
2026-09-11 09:21:38 -05:00
Peter Wielander 17bd649839 [e2e] Capture the divergence signature in the event-log-race-repro harness (#4093) 2026-09-10 19:21:24 -07:00
Rich Harris 938c7ffb07 Bump devalue dependency (#3843) 2026-09-11 00:56:16 +00:00
Peter Wielander ec57aff3be [core] Log pending consumers in divergence diagnostics (#4021) 2026-09-10 15:16:03 -07:00
Mitul Shah a5694d34ba fix(web-shared): stop event payload stub flash in events view (#3951) 2026-09-10 15:06:34 -07:00
nityam 74058c141c fix: order the 429 check before the 4xx check in the workflow skill (#3915) 2026-09-10 14:38:17 -07:00
Nathan Rajlich acb6b1370a test(swc-plugin): verify class-name preservation at runtime (#4015) 2026-09-10 14:34:59 -07:00
Pranay Prakash 0b1216ebe2 (chore) Update Next.js to 16.3.4 in the workbench apps and @workflow/next (#4026) 2026-09-10 14:06:20 -07:00
Alex Langenfeld 3aa4c161af Add step claim attribution to client spans (#4066)
## Summary & Motivation

Adds three bounded trace attributes so a sampled trace says how a `step_started` claim was made: `workflow.step_start.strategy` on the step span (`awaited` / `optimistic` / `batch_preclaimed`, set before the write so a losing claim keeps it after its 409 reconciles to `skipped`), and `workflow.step_start.mode` plus `workflow.step_start.owner_stamped` on the world-vercel write spans across the http, batch, and ws paths. Purely additive telemetry — no change to execution behavior.

## Test Plan

Test added covering a stamped lazy claim's attributes on the per-write span; existing coverage runs in CI. Local run of 81 focused core and world-vercel tests passed; full core typecheck is blocked by unrelated workspace resolution issues.
2026-09-10 15:32:01 -05:00
Alex Langenfeld 7740388d7f feat(streams): trace first WebSocket writes (#4074)
## Summary & Motivation

Tags the first write of a session, and the first write on each reconnect, with phase timings on the existing `workflow.stream.write` span — token/config resolution, connect, wait-for-open, send, and ack round trip — so cold WebSocket write latency can be attributed to a client-side phase before changing first-chunk transport behavior. Later writes keep the two attributes they had, since the per-phase clocks only mean anything while a connection is being established.

## Test Plan

Unit tests added; existing world-vercel suite and typecheck pass.
2026-09-10 13:29:01 -05:00
Peter Wielander 45a3072948 [core] Fix the python e2e conformance suite after the retention merge (#4022) 2026-09-10 08:10:57 -07:00
Alex Langenfeld d4817ce548 feat(streams): add WebSocket writer lifecycle (#3833)
## Summary & Motivation

Implements the client half of `workflow-stream-ws/v1` behind the existing default-off `WORKFLOW_STREAMS_TRANSPORT=ws` gate, populating the `createWriteSession` seam only when opted in.

- Writes and closes are serialized over one socket per writer lifetime; groups above the v1 per-request chunk cap are split without resetting writer-local sequence.
- Any failure before the upgrade is accepted (declined upgrade, proxy, load error, a dispatch that beats the handshake) falls back to HTTP for the rest of the writer's life.
- Once a write is on the socket, a missing or uncorrelatable reply poisons the session rather than replaying over HTTP, since a duplicate append cannot be ruled out.
- Idle clean closes reconnect with the same writer id, capped at three attempts so a draining server can't hot-loop.
- The handshake gets a `workflow.stream.ws.connect` span and each frame a synthesized `http POST` span, so per-event tracing survives the non-`fetch` transport.

## Test Plan

New unit tests cover the lifecycle, fallback, and poisoning paths; 598 `@workflow/world-vercel` tests plus package build/typecheck and workspace lint/format pass. Root build/typecheck couldn't run locally (missing Rust toolchain for the unrelated `@workflow/swc-plugin`).
2026-09-10 09:21:51 -05:00
Pranay Prakash f5aeaa869c Move to changesets v3 and changesets/action v2 (#3974)
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
@workflow/tsconfig@5.0.0-beta.0
2026-09-09 14:44:37 -07:00
Pranay Prakash c477cfa3d3 Keep one failed publish from taking down the release, and verify what actually reached npm (#3967) 2026-09-09 12:51:48 -07:00
github-actions[bot] 32a74e3941 Version Packages (beta) (#4062) workflow@5.0.0-beta.50 2026-09-09 12:40:45 -07:00
Pranay Prakash efbdc213a0 [core] Make hook.metadata a lazy Promise getter (#3988)
* [core] Make `hook.metadata` a lazy Promise getter

Hydrating a hook's metadata is a decrypting READ: it needs the owning
run's payload keys, and resolving those costs a run fetch plus a
`run-key` API round trip (~350ms). `getHookByToken()` did that work
eagerly on every lookup that found a metadata-bearing hook, so callers
that only wanted `runId`/`token` — and hook resumption, which never
reads metadata at all — paid for it anyway.

`metadata` is now a getter returning a memoized Promise, the same shape
as `run.returnValue`. The lookup is one read again; hydration and the
key resolution behind it happen on first access, or never.

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

Co-Authored-By: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* docs: surface the lazy hook.metadata change in What's new, the migration skill, and the resumeHook reference

Adds the breaking-change row to the v5 What's new page and puts that page
in the sidebar as the first visible entry (the /v5/docs redirect to
getting-started is unchanged). Teaches the migrating-workflow-v4-to-v5
skill the `await hook.metadata` rewrite and bumps its version. Points the
resumeHook reference at HookWithLazyMetadata, and notes on the World
storage page that world.hooks.getByToken() returns raw serialized
metadata.

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

* [core] Export the lazy-metadata hook type as `Hook` from `workflow/api`

`getHookByToken()` and `resumeHook()` return `Hook`, not a separate
`HookWithLazyMetadata`: one public hook type whose `metadata` is a lazy
Promise, mirroring `Run` for runs. The World-level record from
`@workflow/world` is unchanged and is referenced as `WorldHook` inside the
runtime.

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

* [core] Define the lazy `metadata` getter in place; tighten changeset and docs wording

Review feedback: the hook record a World returns is a fresh object per
lookup and the eager path mutated it anyway, so define the getter on it
directly instead of copying it with Object.create(). The changeset is one
sentence, and the docs describe hydration as extra network round trips
rather than decryption, since not every World encrypts.

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

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-09 12:26:28 -07:00
Peter Wielander 22b5f48e37 [ci] Print opencode's server log in the backport job (#4056) 2026-09-09 12:18:43 -07:00
github-actions[bot] 855b4e92e6 Version Packages (beta) (#4011) workflow@5.0.0-beta.49 2026-09-09 12:12:11 -07:00
Peter Wielander 2354301f39 [world-vercel] Bound the queue client's requests (#4049) 2026-09-09 18:50:37 +00:00
Alex Langenfeld 4547e1a7a9 feat(streams): add writer session seam (#3832)
## Summary & Motivation

Gives one in-memory stream writer a stable identity and its own sequence space, so a transport can preserve chunk ordering across a mid-stream HTTP/WebSocket transition. `Streamer.streams.createWriteSession` is optional — Worlds that don't implement it keep using `write`/`writeMulti`/`close` unchanged.

Abort disposes the session rather than closing it, since a producer failure is transport cleanup, not stream completion.

## Test Plan

Tests added, plus the full `@workflow/world-vercel` suite and package builds/typecheck pass. Root build/typecheck is blocked locally by a missing Rust toolchain for the unrelated `@workflow/swc-plugin`.
2026-09-09 12:20:08 -05:00
Peter Wielander 51a181af91 docs: document WORKFLOW_NODE_HTTP in the v4 World docs (#4050) 2026-09-09 10:17:33 -07:00
Peter Wielander f83e8367f4 [world-vercel] Honor WORKFLOW_NODE_HTTP on the queue transport (#4044)
* fix(world-vercel): honor WORKFLOW_NODE_HTTP on the queue transport

getQueueDispatcher was the one dispatcher getter that ignored the flag. The
reasoning was that `undefined` cannot move the queue client onto node:http
(QueueClient takes a dispatcher and no fetch override), so returning it would
only drop this package's pool tuning and fall back to undici's global agent.

That misses what the flag is actually for. The deployments that need it are the
ones where the undici copy *this package bundles* is unusable, and `undefined`
does move the request off that copy: global fetch dispatches on the runtime's
own undici instead. On such a deployment every other request survives while the
queue client keeps dispatching through the broken copy, and an
acknowledgeMessage that never resolves means the message is redelivered for as
long as the platform keeps killing the invocation holding it.

Losing pool tuning is the correct trade under a flag whose premise is that the
bundled undici is not usable here. An explicit config.dispatcher still wins.

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

* docs: correct the queue-send note for WORKFLOW_NODE_HTTP

The queue client is a partial exception to the flag, not a full one: it cannot
move to node:http, but it does honor the flag by dispatching through the
runtime's own copy of the HTTP client library instead of the copy the World
bundles. That distinction is the whole point when the bundled copy is what does
not work.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 10:17:08 -07:00
Alex Langenfeld fdeb642270 feat(streams): add WebSocket capability gate (#3764)
## Summary & Motivation

`WORKFLOW_STREAMS_TRANSPORT=ws` advertises client support for `workflow-stream-ws/v1` on stream writes; anything else keeps HTTP. It's a capability signal only — the server decides each upgrade, so there's no version or tenant-policy heuristic on the client side.

## Test Plan

Unit tests for the gate's accepted values; typecheck, build, and lint pass locally.
2026-09-09 11:48:08 -05:00
Chad Hietala 9a5660fbd6 fix(world-local): retry JSON reads on Windows (#4051) 2026-09-09 15:01:25 +00:00
Peter Wielander 8a91d18d0d [core] Add the wake-loop scenario to the event log race repro (#4017) 2026-09-08 15:19:32 -07:00
Peter Wielander 9a9af618f7 [ci] Cap concurrent Vercel E2E action repo-wide (10 by default) (#4039) 2026-09-08 14:47:37 -07:00