* 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>
* Prevent local tests from hanging
This commit allows manually running pnpm test:
- use vitest run to stop world-vercel and world-testing when complete
- disable Nuxt telemetry to prevent interactive telemetry prompt
Signed-off-by: Justin Xu <xu.justin.j@gmail.com>
* Disable Nuxt telemetry via workspace .nuxtrc instead of inline env vars
The inline NUXT_TELEMETRY_DISABLED=1 in package.json scripts doesn't work
on Windows (pnpm runs scripts through cmd.exe). A workspace-root .nuxtrc
is cross-platform, and @nuxt/kit resolves it from the pnpm workspace root,
so it also covers workbench/nuxt's build/dev, which hit the same
first-run consent prompt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Signed-off-by: Justin Xu <xu.justin.j@gmail.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* [web-shared] Show precise durations in the new trace viewer
The events list and timeline bar labels now render two-decimal seconds for
durations over 1s instead of rounding to whole seconds.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [web-shared] Fix duration rounding at unit boundaries
formatDurationPrecise bucketed durations on the raw ms value but only
rounded at display time, so inputs just below a unit boundary carried
into the next unit without re-bucketing (e.g. 59999ms -> "60.00s").
Round to centisecond precision FIRST, then decompose in integer
centisecond space so the seconds component stays in [0.00, 59.99] and
carries re-bucket into the next unit (59999ms -> "1m 0.00s").
Co-authored-by: Cursor <cursoragent@cursor.com>
* [web-shared] Simplify precise duration formatting
Reuse the existing MS_IN_* constants instead of the centisecond decomposition; behavior is unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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>
* Move run attributes into their own detail card
Render the run's `attributes` field as a dedicated collapsible
DetailCard in the detail panel instead of as a cramped JSON value
inside the top metadata key/value list.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Create empty-worlds-throw.md
Signed-off-by: Mitul Shah <mitulxshah@gmail.com>
* Render attributes card after Input/Output
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Signed-off-by: Mitul Shah <mitulxshah@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* [world-vercel] Validate ref resolve responses before use
When workflow-server returns a ref body to the SDK, the bytes are
fed into the workflow runtime's event log and deserialized via
`decodeFormatPrefix`. The SDK always writes ref payloads with at
least a 4-byte format prefix (see `encodeWithFormatPrefix` in
`@workflow/core`), so a zero-byte response — or one whose length
disagrees with `Content-Length` — is never a valid stored value.
Before this change, `resolveRefDescriptor` had no validation: a
200 with an empty body would be passed downstream as a zero-length
Uint8Array, which then failed deep inside replay with:
Data too short to contain format prefix: expected at least 4 bytes, got 0
By that point the workflow's in-memory event snapshot is already
poisoned with the empty payload, so every subsequent replay
deterministically reproduces the same failure, downstream
`resumeHook()` calls surface as `Hook not found`, and the run
only unsticks when stale-run cleanup terminates the sandbox.
This catches the failure at the transport boundary instead, where
it can be retried as a `WorkflowWorldError`. Both an empty body
and a length mismatch (truncated streaming response) are rejected.
This is the SDK-side companion to vercel/workflow-server#432, which
adds the same validation on the server side.
* Address review: reject <4-byte bodies, handle malformed Content-Length
Three review changes:
1. Reject any body shorter than the 4-byte format-prefix length, not
just zero-byte bodies. The SDK guarantees every stored ref payload
starts with a 4-byte format prefix (FORMAT_PREFIX_LENGTH in
@workflow/core), so a 1-3 byte body would also fail downstream
replay with the same 'Data too short to contain format prefix'
error this PR exists to prevent.
2. Parse Content-Length safely with parseInt + Number.isFinite +
non-negative checks instead of bare Number(). A non-numeric value
like 'abc' would otherwise produce NaN and silently surface as a
'truncated' error, masking the real cause. Malformed values are
treated as absent; the minimum-length check still defends against
actual truncation in that case.
3. Add tests for the truncated-body-without-Content-Length case
(chunked transfer where Content-Length validation can't see the
truncation), and for a malformed Content-Length header that should
be ignored rather than misreported as truncation.
The validation logic also moves into a small assertValidRefBody
helper to keep the inner trace function under the noExcessiveCognitiveComplexity limit.
* Address review: scope 4-byte minimum to binary refs, strict Content-Length parsing
- Only apply the 4-byte format-prefix minimum to application/octet-stream
payloads; CBOR refs can legitimately be 1-byte primitives (true/0/null).
- Require Content-Length to be a plain run of digits before comparing;
parseInt would otherwise accept numeric-prefixed garbage ('12junk' -> 12).
- Make the changeset succinct.
* Address review: skip Content-Length check for compressed responses
fetch/undici transparently decompresses gzip/br bodies but leaves
Content-Length describing the encoded (compressed) size, so comparing it
against the decompressed byteLength would reject valid compressed refs as
a phantom 'ref-body-length-mismatch'. Skip the comparison when a
non-identity Content-Encoding is present; an absent or 'identity' encoding
is still validated. Adds regression tests for both cases.
The nitro-native-build changelog sample calls useStorage() (a Nitro
server-side auto-import) from a step, which the docs type-checker
couldn't resolve and failed with TS2552. Add liberal global
declarations for useStorage/useDatabase/useRuntimeConfig so Nitro
auto-imports type-check in docs samples.
* fix(next): always apply turbopack content condition regardless of builder mode
When lazy discovery is enabled (deferred builder), shouldApplyTurboCondition
was false, so turbopack.rules were added with no content filter — causing the
workflow loader to run on every JS/TS file. Apply the content condition
unconditionally so the loader only fires on files with workflow directives.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* add changeset
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: JJ Kasper <jj@jjsweb.site>
* feat(world-vercel): allow a custom dispatcher
Add a `dispatcher` option to `APIConfig`/`createVercelWorld` so callers can
supply a custom undici dispatcher. It is threaded through every request path
(HTTP and the queue) and defaults to the shared undici RetryAgent.
* docs(world-vercel): explain why dispatcher is typed unknown
* Reduce unnecessary CI runtime
* Fix shared E2E artifact extraction path
* Stabilize getWorkflowPort timeout test on Windows
* Preserve UI unit coverage on CI fast path
* fix(world-vercel): retry transient response-body parse failures in the HTTP client
A sporadic failure reading/decoding a 2xx response body (truncated or
terminated stream, connection reset mid-body, or a gateway returning a
non-CBOR/JSON body) was surfaced immediately as a PARSE_ERROR. The
shared RetryAgent only retries connection/5xx failures — body
consumption happens after it returns the response, so these escape its
retry logic.
Retry such failures inside `makeRequest` with bounded exponential
backoff, scoped to idempotent methods (GET/HEAD) so writes are never
replayed. This fixes the reported `events.list` parse failure at the
adapter layer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(core): propagate exhausted transient world errors to the queue
Pairs with the world-vercel in-adapter retry: when a response-body parse
failure survives the adapter's retries (or comes from a non-idempotent
write that is never retried in-process), it must not fail the run.
Re-throw such transient world errors from the replay loop so they
propagate to the queue handler, which replays the whole run — safe
because replay is idempotent. Schema-validation contract errors stay
fatal.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Revert "fix(core): propagate exhausted transient world errors to the queue"
This reverts commit 7bb62e9f81.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>