* [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>
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>
* 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>
* 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>
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.
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.
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>
* 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>
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>
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>
* 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>
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>
@workflow/ai declares `workflow` as a peerDependency. By default,
changesets force-bumps a package a full major whenever a peer
dependency takes a minor/major bump, regardless of whether the new
version is still within the declared range
(`onlyUpdatePeerDependentsWhenOutOfRange` defaults to false).
On the `stable` branch (regular changeset mode) this caused three
ordinary `workflow` minors (4.3.0/4.4.0/4.5.0) to drag @workflow/ai
to 5.0.0/6.0.0/7.0.0 with no real changes. Setting the option to
true makes the major bump fire only when a new `workflow` version
actually leaves @workflow/ai's `^4` peer range.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* otel: explicit traceparent injection + linked-trace mode for bounded per-invocation traces
- Add WORKFLOW_TRACE_MODE ('linked' default, 'continuous' legacy) to the
workflow and step queue handlers. In linked mode, WORKFLOW_V2/STEP spans
start a new trace root with span links to the incoming delivery context
and the run-origin context, and re-enqueued messages forward the
ORIGINAL run-origin trace carrier unchanged.
- world-vercel now explicitly injects W3C traceparent/tracestate/baggage
headers on outgoing workflow-server HTTP requests from inside the
client span (no-op without an OTEL SDK registered).
- New workflow.trace.mode span attribute; unit tests for both modes and
for header injection.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* changeset: call out behavioral telemetry changes of the linked default
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: add v5 observability tracing page
Documents OTEL spans/attributes, linked trace mode and WORKFLOW_TRACE_MODE,
span links, context propagation, and the v4 behavior-change callout.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* otel: human-friendly span names for workflow and step spans
WORKFLOW_V2/STEP prefixes with full machine names (workflow//./src/...//fn)
become workflow.execute / step.execute / workflow.start with the short
function name. New workflowDisplayName/stepDisplayName helpers in
@workflow/utils handle both raw and queue-sanitized name forms; full names
remain in the workflow.name/step.name attributes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* changeset: merge span-name and linked-trace notes into one changeset
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: update trace-shape prose to renamed span names
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: replace ascii trace diagram with mermaid
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* address review: empty carriers, shared trace helpers, mode warning, name edge cases, consumer span kind
- Treat an empty ({}) trace carrier as absent everywhere the trace-mode
logic branches, so linked mode falls back to a fresh origin instead of
forwarding a useless {} forever; workflow.trace.propagated now reports
whether a usable carrier arrived.
- Extract the duplicated linked-mode logic into shared telemetry helpers
getNextTraceCarrier() and buildInvocationSpanLinks(), used by both the
workflow and step queue handlers; resume-hook now uses
linkToTraceCarrier (gaining the isSpanContextValid guard).
- Warn once per distinct unrecognized WORKFLOW_TRACE_MODE value instead
of silently selecting linked.
- shortNameFromSanitized: map default/__default to the module short name
(mirroring parseName) and document the `$`-sanitization limitation.
- Queue-delivered workflow.execute spans now use the CONSUMER span kind,
matching queue-delivered step.execute spans; docs span table and
changeset updated accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs: document run idempotency
* docs: address idempotency review feedback
* docs: make hook tokens the idempotency pattern
* docs: address toolbar idempotency feedback
* docs: clarify idempotency page description
* docs: scope idempotency descriptions
* docs: move step idempotency example under section
* docs: simplify idempotency guidance
* docs: simplify idempotency cookbook
* docs: add empty changeset
Signed-off-by: Nathan Rajlich <n@n8.io>
* docs: address idempotency review feedback
* feat: add hook ready promise
* docs: mention conflicting hook run id
* test: cover hook ready continuation scheduling
* feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: fix inconsistent hasConflict bullet in create-webhook reference
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
* docs: require docs preview links in PR descriptions for docs changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: restore SWC Plugin heading in AGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: adopt hook.hasConflict in run idempotency docs
- Primary claim pattern is now `if (await hook.hasConflict)` instead of
try/catch on HookConflictError; payload awaits still reject with
HookConflictError (with conflictingRunId) when the owner's run ID is
needed.
- Route example returns the active owner via resumeHook()'s runId
instead of threading conflictingRunId through the workflow result.
- Update claim-pattern prose across start(), getHookByToken(), world
storage, scheduling, workflow composition, and cookbook idempotency
pages (v4 + v5).
- Add @skip-typecheck marker to the cross-block route sample, fixing a
pre-existing docs typecheck failure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: move resume-or-start guidance into a dedicated resumeHook example
The early callout was too vague and out of place at the top of the API
reference. Replace it with a 'Resume or Start' example section that
explains the flow, shows the resume-first/start-then-retry route, and
links to the run idempotency pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: detect the concurrent-start race via runId comparison instead of awaiting returnValue
The 'Resume or Start' example returned the just-started run's runId with
reused: false even when a concurrent request's run won the token race —
the payload had reached the actual owner, so the response pointed callers
at a run that exits as a duplicate. The foundations route handled the
race correctly but by awaiting run.returnValue, blocking the HTTP
response on full workflow completion.
resumeHook() always resolves against the actual active owner, so
comparing the resumed hook's runId with the started run's runId detects
the race in both examples — race-correct and non-blocking.
* feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: adopt hook.getConflict and add conflict-handling strategy guide
Run idempotency docs now use getConflict (resolves with the conflicting
Run in v5, { runId } in v4) and document code-driven conflict strategies
in place of static ID-reuse policies: reject the duplicate, adopt the
owner's result, inspect before deciding, signal the owner via
resumeHook, and supersede via cancel-and-reclaim.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: never resolve getConflict with a non-Run fallback shape
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: make getConflict a method — hook.getConflict()
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: getConflict is a method — hook.getConflict()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: typecheck every sample — drop skip-typecheck escape hatches
Route examples typecheck as-is since the runId-comparison rewrite;
strategy fragments are now complete self-contained workflows; the
publishing-libraries cross-block dependency uses the declare @setup
convention. 934 samples typechecked, none skipped by this PR.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: guard Run class registration, fix anchors, clarify changeset
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: restore run-idempotency anchors now that the section exists here
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: describe fixed conflict policies generically, without naming other systems
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- Teal diamond markers for attr_set events on the trace timeline with
time tooltips (new trace viewer)
- attr_set payloads render changed/removed keys and the writer
(workflow vs step + attempt) in the run sidebar and Events tab
- Run root span selection now shows run-level events (run lifecycle +
attr_set) in the sidebar
- Attributes card on run details renders key-value rows with reserved
$-prefixed keys badged and sorted after user keys
- attr_set added to MARKER_EVENT_TYPES, BOUNDARY_LABELS, event colors
(teal), and the flat events list run-level grouping
- Docs: screenshots on the attributes page, served from docs/public
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(deps): upgrade esbuild to 0.28.1 (GHSA-gv7w-rqvm-qjhr)
Bump the esbuild catalog from ^0.27.3 (resolving 0.27.7) to ^0.28.1 to
resolve the High-severity advisory GHSA-gv7w-rqvm-qjhr (missing binary
integrity verification before executing downloaded binaries). All
workspace consumers reference esbuild via `catalog:` (@workflow/builders,
@workflow/cli, workbench/example, and the root devDependency), so the
single catalog bump propagates everywhere. Adds a patch changeset for the
two publishable consumers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): exclude esbuild from minimumReleaseAge gate
The canary E2E jobs run `pnpm install --no-frozen-lockfile` (they mutate
the next dependency), which re-resolves the catalog and hits the 48h
`minimumReleaseAge` gate on the freshly-published esbuild@0.28.1, failing
setup with ERR_PNPM_NO_MATCHING_VERSION. Add esbuild and @esbuild/* to
minimumReleaseAgeExclude (pnpm's recommended fix, consistent with the
existing @vercel/*, @workflow/*, turbo exclusions) so the intended,
catalog-pinned security upgrade resolves under non-frozen installs.
Re-resolving also drops the redundant esbuild@0.28.0 (nitropack@2.13.4
consolidates onto 0.28.1 within its ^0.28.0 range).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test: e2e coverage for run-idempotency conflict-handling strategies
Covers the patterns documented in foundations/idempotency:
- claim-only hook mutex: token claimed and held with no payload data,
duplicate identifies the owner, token released after completion
- adopt the owner's result via conflict.returnValue
- signal the owner: duplicate forwards its payload via resumeHook
- supersede: duplicate cancels the owner and reclaims the token
- route-side resume-or-start retry pattern reaching the started run
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: fix adopt-owner-result race — gate owner completion on observed conflict
On slow runtimes the duplicate's first invocation could land after the
owner completed and released the token, making the duplicate a fresh
owner that waits forever for a payload (90s timeout across CI matrices).
Poll the duplicate's event log for hook_conflict before resuming the
owner, and widen the test timeout for the added gate budget.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: assert superseded owner's returnValue rejection; empty changeset
- Await run1.returnValue and assert WorkflowRunCancelledError so the
cancellation is verified end-to-end and no rejection leaks from the
supersede test.
- Test-only PR: use an empty changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: retrigger preview deployments (turbopack deployment for 2e9d000 wedged in esbuild hang)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: bust poisoned turbo cache entry for nextjs-turbopack build
The 2e9d000 deployment's next build crashed in an esbuild hang but its
task (70724907c9dd3a29) was recorded into the turbo remote cache anyway,
so every subsequent build with the same input hash replays the broken
artifact (missing routes-manifest). Change a build input to force a
fresh execution.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(docs): repair broken links and make the docs link linter actually validate
The docs link linter (docs/scripts/lint.ts) had been silently passing
everything since the app moved under app/[lang]/ (#552): the
next-validate-link populate key 'docs/[[...slug]]' no longer matched the
real route, and the unpopulated [lang] homepage route produced a fallback
regex (^\/(.+)$) that matched every href. It also only scanned v4 content.
- Rewrite lint.ts to build explicit v4/v5 URL spaces from both fumadocs
sources (including cookbook URL variants, app routes, worlds pages,
public/ assets, and next.config.ts redirects) and validate each version's
content against version-correct render semantics. Also validate
frontmatter related/prerequisites references (version-relative) and
heading fragments.
- Rewrite Card hrefs on v5 pages: the v5 routes rewrote inline markdown
links from /docs/... to /v5/docs/... but Card renders its own Link, so
Card hrefs escaped to the v4 routes and 404'd for v5-only pages (e.g.
/v5/docs/observability linking to /docs/observability/attributes).
- Fix all dead content links surfaced by the working linter (56 across
v4+v5): nonexistent use-workflow/use-step/start API pages now point at
foundations/workflows-and-steps and workflow-api/start, getStepMetadata
path corrected, /docs/worlds/local → /worlds/local, dead changelog/
internal references removed or unlinked, retired common-patterns links
point at the cookbook, and a dead #returnvalue anchor now targets
#returns.
- Add an index page for api-reference/workflow-errors (both versions),
which was linked from the API reference landing page but had no page.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(docs): add version prefix to 'Edit this page on GitHub' links
All "Edit this page on GitHub" links 404'd since the v4/v5 content split
(#1948): page.path is relative to the per-version content dir, but
EditSource built URLs against docs/content/docs/ without the v4/ or v5/
segment. Add a required version prop, passed from each page route.
Incorporates #2120 by Luke Howard (@gldkhoward), rebased onto the v5
route changes from this branch. Fixes#2119.
Co-authored-by: Luke Howard <dev@lukehoward.com.au>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(web-shared): RelativeTimeCard with shared ContextCard provider
Add a ContextCard provider/trigger and rebuild the timestamp tooltip as a
RelativeTimeCard, giving animated, collision-aware morphing hover cards.
Mount the shared provider in EventListView and AttributePanel.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Match vercel/front timestamp format for run/activity fields
Render absolute Created/Started/Completed (and sibling) timestamps using
date-fns in vercel/front's request/activity format (e.g.
"JUN 10 10:16:02.69 GMT-4") via the shared formatLocalMillisecondTime helper.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shared): register dark-theme/light-theme Tailwind variants
The context-card arrow tip stroke uses `dark-theme:[--context-card-tip-stroke:#252525]`,
but Tailwind v4 has no built-in `dark-theme` variant, so the utility was silently
dropped and the stroke fell back to its light `#DBDBDB` value — rendering as a white
caret in dark mode. Register the `dark-theme`/`light-theme` custom variants in
styles.css (mirroring vercel/front's geistcn tailwind.css, extended to match the
`.dark`/`[data-theme="dark"]` selectors this package and next-themes use).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shared): match context card shadow to vercel/front
The --ds-shadow-tooltip token was guessed when added standalone, producing
an oversized/heavy drop shadow. Reproduce front's exact resolved value for
both light and dark themes (including the background-border layer).
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shared): bridge context card hover gap to stop flicker
The card is positioned `sideOffset` away from the trigger, leaving a
transparent un-hoverable gap that caused the hover card to flicker
(open → close → open) when moving the cursor onto it. Add a transparent
hover bridge inside the floating wrapper that extends the hover surface
by `sideOffset` to meet the trigger edge, keeping the visual spacing
while making the hover surface continuous.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Revert "fix(web-shared): bridge context card hover gap to stop flicker"
This reverts commit 2b961b1610.
* docs: simplify changeset description
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: remove vercel/front references from comments
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web-shared): only clear active context card when the active trigger unmounts
The unmount cleanup had an inverted guard: an unmounting inactive trigger
would clear the shared active card, hiding another trigger's card (and an
unmounting active trigger left a stale card). Guard on === id instead.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs: remove theme-variant comment
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add allowReservedAttributes option to start()
experimental_setAttributes already exposes allowReservedAttributes for
framework-level callers that own a $-prefixed sub-namespace, and the
run_created / run_started event schemas plus the local and Postgres
worlds already accept and validate the flag. start() was the one gap:
it always validated initial attributes with the reserved prefix
disallowed and had no way to opt out, so framework code could not seed
reserved attributes at run creation.
Thread the option through start():
- StartOptions.allowReservedAttributes, passed to client-side
validation and forwarded on the run_created eventData
- carried in the queue runInput (new RunInputSchema field) and
forwarded to run_started so the resilient/lazy run creation path
validates identically
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add e2e coverage for reserved initial attributes via allowReservedAttributes
Verified locally against the nextjs-turbopack dev server: the reserved
key passes client and server validation, lands on the run at creation,
and survives the workflow's own attr_set writes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Animate in-progress segments in the timeline
Add an animated diagonal "barber-pole" stripe overlay to in-progress
(running/received) segments in the new trace viewer timeline, so it's
obvious at a glance which work is still live.
The animation lives in a colocated CSS module (timeline.module.css),
imported by the component — web-shared is in consumers' transpilePackages,
so the keyframes ship with the component rather than relying on the global
styles.css (which Geist-using hosts don't import).
Also fixes a latent status bug this surfaced: the run-segment builders
collapsed every non-failed run to "running", so completed runs rendered as
"running" (and, with the new animation, kept animating). They now map to a
new terminal `completed` status (blue, static) via a fail-closed
runSegmentStatus helper — only genuinely in-progress runs animate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Create chatty-walls-appear.md
Signed-off-by: Mitul Shah <mitulxshah@gmail.com>
---------
Signed-off-by: Mitul Shah <mitulxshah@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the filled gray queued box with a tick and horizontal line into the active bar so wait time reads as "waited, then ran."
Co-authored-by: Cursor <cursoragent@cursor.com>
SidebarFolderTrigger renders a <button>, which shrink-to-fits its
content, so the ms-auto chevron sat directly next to the folder name
for folders without an index link (e.g. How it works, AI Agents,
Testing). SidebarFolderLink renders an <a> that spans the full sidebar
width, so its chevron was pushed to the right edge. Add w-full to both
so every folder caret is right-aligned.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: replace hook.hasConflict with hook.getConflict (Promise<Run | null>)
hasConflict's boolean didn't expose WHICH run owns the token, so the
duplicate run couldn't act on the conflict. getConflict resolves with
null once registration commits, or with a Run handle for the conflicting
run — letting the workflow return/log the owner's runId, inspect its
status, await its result, or cancel it and continue, all in code.
The workflow-mode create-hook module exposes the bundle's compiled Run
class (durable step-proxy methods) on a well-known symbol so the host-
side hook consumer can construct the conflicting run inside the VM.
Contexts without the class (plain unit tests) fall back to a { runId }
object, which is also the documented v4 shape (no native Run
serialization in v4).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix: never resolve getConflict with a non-Run fallback shape
getConflict's contract is Promise<Run | null>. In the degenerate cases
where a real Run cannot be constructed — a hook_conflict event persisted
by an old world without conflictingRunId, or a context that never loaded
the workflow-mode create-hook module — reject with HookConflictError
instead of resolving with a { runId }-shaped impostor.
Test harnesses now register the Run class on the (VM) globalThis like
real bundles do.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: make getConflict a method — hook.getConflict()
A property getter that triggers registration/suspension reads as passive
state; a method makes the side effect explicit. Update implementation,
types, tests, e2e workflows, docs, and changeset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* review: guard Run class registration, fix anchors, clarify changeset
- Only register WORKFLOW_RUN_CLASS when the workflow runtime is present
(WORKFLOW_CREATE_HOOK installed on globalThis), so host imports of the
workflow-mode module neither mutate the host global nor expose the
non-step-proxy host Run.
- Drop #run-idempotency link fragments — that section lands in the
stacked docs PR (#2011), which restores the anchored links.
- Note in docs that getConflict() rejects with HookConflictError for
legacy hook_conflict events lacking the owner's run ID.
- Changeset now calls out the hasConflict -> getConflict() replacement.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor: resolve the conflicting Run through the serialization class registry
Replace the bespoke WORKFLOW_RUN_CLASS global with the registry the
serialization pipeline already uses to revive Run instances:
- The SWC plugin already auto-registers the workflow bundle's compiled
Run in globalThis[workflow-class-registry], but under a path-derived
classId the host cannot know statically. The workflow-mode create-hook
module now aliases it under a stable id (class//workflow//Run) via a
new aliasSerializationClass() helper (a plain registry entry —
registerSerializationClass cannot be reused since the plugin's IIFE
already defined the non-configurable classId property).
- createConflictingRun() looks the class up with
getSerializationClass(RUN_CLASS_ID, ctx.globalThis) and constructs
through its WORKFLOW_DESERIALIZE hook, exactly as the Instance reviver
would for a serialized Run crossing from a step into the workflow.
- Because the registry is keyed per-global, no environment guard is
needed: a stray host-side import registers the host Run on the host
registry, which is the correct class for that context. The
WORKFLOW_CREATE_HOOK guard, the ??=, and the WORKFLOW_RUN_CLASS symbol
are all deleted.
Verified: 1156 core unit tests; compiled workbench bundle contains the
stable alias alongside the plugin's path-derived registration with zero
WORKFLOW_RUN_CLASS references; all 5 hookGetConflict e2e tests pass
against a local nextjs-turbopack dev server, including conflict
resolution reading conflict.status through a durable step.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
* feat: add hook ready promise
* test: cover hook ready continuation scheduling
* feat: replace hook.ready with hook.hasConflict (Promise<boolean>)
- hook.hasConflict resolves true when the token is owned by another
active hook, false once registration is committed — no throw, so
workflows can branch on conflicts early. Awaiting it suspends the
workflow to commit the hook registration (createHook alone does not).
- Chain the already-created fast-path through promiseQueue so
resolution order matches event-log order (review feedback).
- Skip inline step execution when a suspension has an awaited hook
creation so the hasConflict continuation can advance independently
of step execution (review feedback).
- Update unit tests, e2e tests, workbench workflows, and v4/v5 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: fix inconsistent hasConflict bullet in create-webhook reference
State both resolution values explicitly (true = token already owned,
false = registered) instead of a parenthetical that only described the
false case.
* docs: require docs preview links in PR descriptions for docs changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: restore SWC Plugin heading in AGENTS.md
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
* [core] V2: pre-schedule the wait timer before inline-executing a step
Fix `Promise.race(step, sleep)` semantics in V2 mixed suspensions
without losing inline step execution.
Inline `await executeStep(...)` blocks the V2 handler for the full
step duration, but `wait_completed` events are only created on the
*next* loop iteration's "complete elapsed waits" pass. So if the
sleep is shorter than the step, replay always picked the step
because the wait_completed event hadn't been written yet —
`sleepWinsRaceWorkflow` returned `'step'` instead of `'sleep'`.
Fix: when a suspension contains both an owned inline step and at
least one pending wait, queue a delayed self-message with
`delaySeconds = suspensionResult.timeoutSeconds` *before* starting
inline execution. The queued continuation fires in a separate
function invocation while the step is still running. That parallel
invocation's "complete elapsed waits" pass writes wait_completed,
replay observes the elapsed wait, and `Promise.race` resolves with
the sleep correctly. The original (still-running) inline invocation
finishes its step, sees `run_completed` on the next loop iteration,
and exits.
This preserves inline-step execution speed for the step-wins case:
the step finishes inline and the workflow returns directly. The
eagerly-queued wait continuation fires after the step has won and
just no-ops on the terminal run.
Test plan:
- New e2e tests `sleepWinsRaceWorkflow` and `stepWinsRaceWorkflow`
exercising `Promise.race` between a step function and `sleep()`,
in both directions.
- Verified locally against `nextjs-turbopack` workbench: both pass.
Event log confirms `wait_completed` is created at t≈1s after
`wait_created` (1s sleep) instead of at t≈11s after the inline
step finishes.
Eager-processing changelog updated with a "Mixed Suspensions"
section describing the pre-scheduled wait approach and its
rationale.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* [world-local] Honor delaySeconds before message delivery
The local queue's `queue()` enqueue path ignored the `delaySeconds`
option entirely — every message was delivered immediately, regardless
of the requested delay. VQS-side queues (used by world-vercel and
world-postgres) honor delaySeconds at the broker, so this brings
world-local in line with production semantics.
The runtime needs this to land before the wait-as-continuation
unification in the next commit: that change starts queueing wait
timers as fresh delayed continuations instead of returning
`{ timeoutSeconds }`. Without delaySeconds support, those wait
continuations would fire instantly in dev and trigger spurious
replays.
Sleep happens outside the queue's worker semaphore so a delayed
message doesn't tie up a worker slot during its delay window — other
immediate messages are free to dispatch in parallel.
New tests in queue.test.ts cover:
- delaySeconds > 0 → setTimeout called with the right ms value
- delaySeconds === 0 → no setTimeout (immediate dispatch)
- delaySeconds omitted → no setTimeout (immediate dispatch)
* [core] V2: unify wait+step queue dispatch in suspension processing
Replace the asymmetric "steps go to the queue, waits become a
{ timeoutSeconds } return value" pattern with a single Promise.all
batch that queues every pending operation we are not running inline.
Before this change, suspension processing had three branches that
all needed to keep the wait/step asymmetry consistent:
- pendingSteps.length === 0 returned { timeoutSeconds }
- inlineStep + waits eagerly queued a delayed self-message AND set
inlineStep to undefined (Option A) AND returned { timeoutSeconds }
- inlineStep retry path returned { timeoutSeconds } if there were waits
After this change, every suspension goes through one path:
for non-inline pendingSteps: queue stepId message
if timeoutSeconds defined: queue delayed continuation
await Promise.all(dispatches)
if !inlineStep: return
await executeStep(inlineStep)
Behaviorally, this restores inline step execution even when the
suspension also has a wait (Option A's carve-out is no longer
necessary): the wait timer fires in a separate function invocation
on the queue, in parallel with the inline step. If the sleep wins
the race, that parallel invocation observes wait_completed via the
"complete elapsed waits" pass and finishes the run; if the step
wins, the wait continuation fires later and no-ops on the terminal
run via the existing terminal-event check.
Other cleanups:
- The inline-step retry path no longer needs to forward
suspensionResult.timeoutSeconds — the wait timer was already
enqueued as part of the unified dispatch above.
- A dead post-step `if (timeoutSeconds && pendingSteps.length === 1)`
block (just a comment, no body) is removed; the loop's
"complete elapsed waits" pass handles the same case correctly.
- Step queueing now uses a shared `traceCarrier` rather than
re-serializing per step.
Retry/throttle and hook-conflict paths still return { timeoutSeconds }
since their semantics are "redeliver THIS message after a delay"
rather than "schedule a fresh wait timer." Those can be unified in
a follow-up.
Test plan:
- New e2e tests `sleepWinsRaceWorkflow` and `stepWinsRaceWorkflow`
pass against the `nextjs-turbopack` workbench.
- Event log inspection confirms wait_completed fires at t≈1s (after
wait_created at t≈0s) for the sleep-wins case, and that the inline
step runs only once (no duplicate step_started events that the
earlier eager-queue approach produced in dev).
- All 842 @workflow/core unit tests pass.
- All 346 @workflow/world-local unit tests pass (with the
delaySeconds support added in the previous commit).
Requires the world-local delaySeconds fix in the prior commit;
without it, wait continuations would fire instantly in dev and the
parallel replay would re-enter handleSuspension before the wait
elapsed (recoverable via existing redelivery, but inefficient).
* [docs] V2 unified suspension dispatch + changeset
Update the "Mixed Suspensions" section in eager-processing.mdx to
describe the unified parallel-dispatch model:
- All non-inline pendingSteps are queued with stepId
- The wait timer (if any) is queued as a delayed continuation
- All dispatched in one Promise.all batch
- One owned step is then inline-executed (if any)
The doc previously described Option A (the carve-out where waits
forced all steps to be queued); the unified model removes that
carve-out and explains why the wait continuation works in parallel
with the inline step.
Also notes the dependency on world-local's new delaySeconds support
(landed earlier in the same PR series).
Changeset bumps both @workflow/core and @workflow/world-local since
both packages have user-observable behavior changes.
* [core] Dedupe wait continuations on the wait's correlationId
While a wait is pending, every replay pass over the run re-observes it
(once per step completion in Promise.all([steps..., sleep()]), etc.) and
would enqueue another delayed continuation — each a spurious replay when
the wait elapses, and each a fresh message that resets the
delivery-attempt runaway guard. Key the continuation on the wait's
correlationId so the worlds' idempotency dedupe collapses them.
Near-elapsed waits (<= 2s) are enqueued without the key: a continuation
delivered marginally early (clock skew; the ceil() on the delay can
leave a ~0 margin) re-observes its wait as pending and must be able to
enqueue a fresh short-delay retry. VQS idempotency records persist until
message-retention TTL — reusing the key there would drop the retry and
stall the run permanently.
Also adapts wait-completion-replay tests (from #2038) to the unified
dispatch model: the hook-branch step now executes inline (registered in
the test world, which now returns a step entity from step_started), so
each scenario performs one extra loop-iteration event fetch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* [core] Always key wait continuations; bucket the key for near-elapsed waits
CI caught sleepWinsRaceWorkflow failing across the world-postgres lanes:
world-postgres serializes KEY-LESS workflow messages per run
(inflightWorkflowRuns), so a key-less wait continuation parks behind the
flow message that is inline-executing the racing step — wait_completed
lands after step_completed and the race resolves to the step. Keyed
messages take the concurrent dedupe path, so the continuation must
always carry an idempotency key.
The near-elapsed exception (<= 2s) now uses a second-bucketed suffix
instead of omitting the key: an early-delivered continuation re-observes
its wait as pending and re-enqueues with >= 1s delay, which guarantees a
later bucket — a fresh key that dedupe windows cannot drop — while
same-instant duplicates still collapse.
Verified against a local world-postgres setup (express workbench,
Graphile worker): sleepWins/stepWins pass 3/3 with wait_completed at
t+1s; the event log confirms the continuation fires in parallel with
the in-flight inline step.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* [core] Clamp wait-continuation delays; chain long waits with hop-keyed dedupe
Addresses PR review: the unified dispatch passed delaySeconds to the
queue unclamped while keying the continuation on the bare wait
correlationId. On world-vercel (23h max delay, 24h VQS message
retention) a sleep() longer than the max either failed the dispatch or
was delivered early with its re-enqueue silently dropped by the
still-live idempotency record - stalling the run permanently.
- New runtime/wait-continuation.ts owns delay + idempotency-key
selection: delays clamp to 23h and longer waits chain across hops,
with the hop index suffixed to the key so re-observations within a
hop window dedupe while each hop delivery gets a fresh key. Near-
elapsed threshold and max delay are named constants; full rationale
moved out of the runtime.ts comment block. Unit tests pin the key
selection including chain advancement.
- SuspensionHandlerResult: timeoutSeconds/timeoutWaitCorrelationId
collapsed into waitTimeout?: { seconds, correlationId } so the
pairing can't drift (review nit).
- runtime.test.ts ack-ordering harness adapted to the unified model:
step_created now answers EntityConflictError so the handler observes
the step without owning it and must queue it (the carve-out the tests
relied on - "pending wait disables inline execution" - is exactly
what this branch removes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* [world-local] Abort pending queue sleeps on close()
Addresses PR review: a pending delayed message kept the dev process's
event loop alive for its full delay, and close() only closed the HTTP
agent - a sleep that fired afterwards attempted delivery against the
closed agent and logged a spurious "[local world] Queue operation
failed" error during test/CLI shutdown.
One AbortController owned by the queue now cancels the delaySeconds
sleep, the timeoutSeconds re-delivery sleep, and the retry backoff on
close(); the resulting AbortError is already swallowed by the existing
isAbortError check. close() is idempotent since shutdown paths may
invoke it twice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* [docs] Wait-continuation clamping + hop chaining; changeset
eager-processing.mdx pseudocode now shows the continuation's
idempotency key and clamped delay (PR review nit); the dedupe prose
covers the two key variations (hop suffix for chained long waits,
second bucket for near-elapsed waits). Changeset mentions long-sleep
chaining and world-local's abort-on-close.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>