mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
perf/script-precompile-module-init
176 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cb181392b9 |
feat(cli): print run deep links with --url, fix dashboard route (#2467)
Add a `--url` flag to `inspect`/`web` that prints a run's observability dashboard deep link to stdout and exits — no browser, no local server — so scripts and agents can share a link instead of opening a UI. Fix the Vercel dashboard URL to the current `…/workflows/runs/<id>?environment=<env>` route (drop the legacy `/observability` segment) and respect `--env`. Apply the same route fix to the e2e helpers, CI aggregation scripts, and the nextjs-turbopack workbench. Document deep-linking in the workflow skill and observability docs. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d0472511ca |
fix(deps): upgrade hono to 4.12.25 to resolve CVE-2026-54290 (#2462)
hono <4.12.25 is vulnerable to CVE-2026-54290 (GHSA-88fw-hqm2-52qc): the CORS middleware reflects any request Origin with Access-Control-Allow-Credentials: true when credentials are enabled and origin is left at the default wildcard, exposing cookie-authenticated endpoints to arbitrary origins. - packages/world-testing: hono 4.12.21 -> 4.12.25 (the flagged manifest) - workbench/hono: ^4.12.8 -> ^4.12.25, clearing the also-vulnerable 4.12.9 from the lockfile Neither app uses hono's CORS middleware, so neither was exploitable, but the bump clears the vulnerable code from the dependency tree. Only the core Hono class is imported in world-testing; build and typecheck pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b92dfbb94d |
fix(deps): upgrade astro to 6.4.6 to resolve CVE-2026-54299 (#2457)
Astro <6.4.6 is vulnerable to CVE-2026-54299 (GHSA-2pvr-wf23-7pc7, host header SSRF in prerendered error page fetch). The fix only exists in the 6.x line — there is no 5.x backport — so this bumps: - workbench/astro: astro ^6.4.6, @astrojs/node 10.1.4, @astrojs/vercel ^10.0.8 - packages/astro: astro devDependency 6.4.6 (typecheck only, not shipped) Removes both vulnerable astro@5.16.3 and astro@5.18.0 from the lockfile. Verified the example app builds under both the node and vercel adapters. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
4763a760bf |
test: e2e coverage for run-idempotency conflict-handling strategies (#2387)
* 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 |
||
|
|
01c8c0878a |
Replace hook.hasConflict with hook.getConflict() returning the conflicting Run (#2373)
* 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>
|
||
|
|
e163422551 |
Add hook.hasConflict for early hook conflict detection (#2015)
* feat: add hook ready promise * test: cover hook ready continuation scheduling * feat: replace hook.ready with hook.hasConflict (Promise<boolean>) - hook.hasConflict resolves true when the token is owned by another active hook, false once registration is committed — no throw, so workflows can branch on conflicts early. Awaiting it suspends the workflow to commit the hook registration (createHook alone does not). - Chain the already-created fast-path through promiseQueue so resolution order matches event-log order (review feedback). - Skip inline step execution when a suspension has an awaited hook creation so the hasConflict continuation can advance independently of step execution (review feedback). - Update unit tests, e2e tests, workbench workflows, and v4/v5 docs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: fix inconsistent hasConflict bullet in create-webhook reference State both resolution values explicitly (true = token already owned, false = registered) instead of a parenthetical that only described the false case. * docs: require docs preview links in PR descriptions for docs changes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs: restore SWC Plugin heading in AGENTS.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Nathan Rajlich <n@n8.io> |
||
|
|
f2a7bdeb0a |
fix(world-local,world-postgres): make duplicate hook_created idempotent (#2295)
* fix(world-local): make duplicate hook_created idempotent Duplicate processing of the same hook_created — same runId, hookId, and token, e.g. cross-process replay or queue redelivery — was being recorded as a hook_conflict in the event log, which then replayed as a self- conflict HookConflictError. The fix mirrors the existing step_created duplicate-correlation path: when the exclusive token claim fails and the existing claim has the same (runId, hookId), throw EntityConflictError so the runtime's existing concurrent-replay catch path swallows it. Different runId or hookId reusing the same token still produces a real hook_conflict. The persisted token claim already carried hookId; only the read schema was dropping it. The schema now preserves hookId (marked optional for backward compatibility with older claim files). Fixes #2283 * fix(world-postgres): make duplicate hook_created idempotent world-postgres has the same gap as world-local was just fixed for: the duplicate-token check in events.create unconditionally writes a hook_conflict event when an existing hook with the same token is found, even when the existing hook has the same (runId, hookId) as the incoming event. The unique partial index on workflow_events does not catch this because the duplicate path inserts hook_conflict, not hook_created. Mirror the world-local fix: when the existing hook's (runId, hookId) matches the incoming event, throw EntityConflictError so the runtime's existing concurrent-replay catch path swallows it. Different runId or hookId reusing the same token still produces a real hook_conflict. Refs #2283 * test(e2e): add regression test for hook_conflict from same-tick replay race Regression test for #1665 / #2283. A parent workflow awaits 6 child workflows with Promise.all; each child does a tiny step and creates one webhook. Awaited children flatten into the parent run, so all webhook creations land on the same workflow body. When their step resolutions align in the same tick the workflow body is re-walked and each pass submits hook_created with the same deterministic (correlationId, token). Before the world-side idempotency fix, the world wrote hook_conflict events for the duplicates and the workflow failed with HookConflictError. With the fix, duplicates throw EntityConflictError (swallowed by the suspension handler), no hook_conflict events appear in the log, and the webhooks resolve normally. Verified locally against world-local: the test fails reliably (3/3) on the unfixed code and passes reliably (5/5) on the fixed code. * test(e2e): rewrite parallelStepsThenWebhookWorkflow to match the actual #1665 repro The earlier version invoked another 'use workflow' function directly from inside the parent workflow, which is not a valid child-workflow invocation (child workflows must be spawned via start()) and didn't mirror the bug shape on #1665 anyway. Rewrite the workflow as a single 'use workflow' function that exactly mirrors Paolo's minimal repro: await Promise.all([stepA(), stepB()]); using webhook = createWebhook(); await webhook; The for-loop runs N independent iterations of that sequence in series, each disposing its webhook via 'using' before the next, to give the timing-sensitive race multiple chances to fire. The race is hard to force deterministically on fast local dev — but the same (runId, hookId) idempotency invariant is covered deterministically by the new unit tests in world-local and world-postgres. This e2e test serves as a higher-level regression net: its assertions (no hook_conflict event in the log, no HookConflictError-failed run) are correct whether the race fires or not, and will catch any future regression on a run that does hit it. * fix(world-local,world-postgres): recover crash-orphaned hook claims/rows instead of suppressing the retry Addresses review feedback on PR #2295. The original idempotency fix made duplicate same-(runId, hookId) hook_created submissions throw EntityConflictError so the suspension handler's concurrent-replay catch path swallows them. But the claim file (world-local) and hook row (world-postgres) are written before the durable hook_created event, and the writes are not atomic. A process / DB interruption between the claim/hook write and the event write leaves an orphaned claim/hook row; the retry then matched the same (runId, hookId), threw EntityConflictError, got swallowed, and the run was permanently left with no hook_created event in the log. world-local: - Add a per-(runId, hookId) in-process mutex (withHookLock) mirroring the existing withStepLock, so two same-tick concurrent calls serialize on the entity write and the dedup branch never observes an in-flight winner mid-write. - In the dedup branch, when the existing claim is for the same (runId, hookId) we are trying to create, check whether the durable hook entity actually exists on disk: - exists → real duplicate: throw EntityConflictError as before. - missing → orphaned claim from a prior crash: fall through and complete the partial write (write the hook entity with overwrite, then emit hook_created via the outer code path). world-postgres: - In the dedup branch, when the existing hook row matches the incoming (runId, hookId), check whether a hook_created event for this (runId, correlationId) already exists in the event log: - exists → real duplicate: throw EntityConflictError as before. - missing → orphaned hook row from a prior crash between hook INSERT and events INSERT: skip the hook insert (the row is already there) and let the outer code path emit hook_created, completing the partial write. Tests: - world-local: pre-seed an orphaned token claim with no matching hook entity, retry hook_created, assert hook entity and hook_created event both land (no hook_conflict, no EntityConflictError). - world-postgres: pre-seed an orphaned hook row with no matching hook_created event, retry, assert hook_created event lands (no hook_conflict, no EntityConflictError). Both tests fail on the prior implementation (EntityConflictError thrown on retry, exact symptom from the review). * fix(world-local): probe the event log (not the hook entity) to detect duplicate hook_created Addresses follow-up review on PR #2295. The previous dedup branch checked whether the durable hook entity existed on disk. But the hook entity is written before the `hook_created` event, and the two writes are not atomic, so a crash between them leaves both the claim file and the hook entity on disk with no event in the log. The dedup branch then matched on `(runId, hookId)`, found the hook entity, threw EntityConflictError, and the suspension handler swallowed the retry — permanently losing `hook_created` from the event log. The fix mirrors what the world-postgres branch already does: probe the run's event log for an existing `hook_created` event for the same `(runId, correlationId)`. The event is the durable record of a successful hook creation; the claim file and hook entity are partial- write artifacts that may exist without the event. - exists → real duplicate: throw EntityConflictError so the runtime's concurrent-replay catch path swallows it. - missing → orphaned partial write (crash at any point before the event landed): re-write the hook entity (with overwrite: true, in case a stale partial copy exists) and let the outer code path emit the hook_created event. Added a new helper findHookCreatedEvent that runs a filtered paginatedFileSystemQuery with limit:1 over the run's events. Regression test "should recover an orphaned hook entity with no matching hook_created event" added — pre-creates a hook, deletes just the hook_created event from disk to simulate a crash between the entity write and the event write, asserts the retry emits a fresh hook_created event (no hook_conflict, no swallowed EntityConflictError). I verified this test fails on the prior fix (throws `EntityConflictError: Hook "hook_orphan_entity_1" already created`, exactly as pranaygp reported) and passes on this commit. The previous test ("should recover an orphaned hook token claim with no matching hook entity") continues to pass — the event-log probe is a strict superset of the entity probe, since a missing entity always also implies a missing event. * fix(world-local): converge same-hook creation across workers via canonical eventId Addresses follow-up review on PR #2295. The previous fix made the dedup branch probe the event log to decide real-duplicate vs orphan-recovery, but the probe and the recovery write are not a single atomic operation. Two workers sharing a data directory (or two retries that lose `writeExclusive(constraintPath)` back to back) could both pass the probe (each observing no hook_created event yet), both fall through to the recovery write, and both append a hook_created event with a different eventId — producing two events in the log for the same (runId, hookId). The in-process `withHookLock` mutex does not help here because it is process-local and tag-specific. The fix persists `eventId` in the durable token claim file (written by the original `writeExclusive(constraintPath)`). On a same-(runId, hookId) dedup match, retries adopt that canonical eventId and rebuild the event with a deterministic createdAt derived from the eventId (a ULID). The outer event write switches from `writeJSON` (check-then-write, TOCTOU) to `writeExclusive` (O_CREAT|O_EXCL via temp-file + hard-link, atomic across processes). Either worker may win the publish; the other throws EntityConflictError which the runtime's existing concurrent-replay catch path swallows. Net result: exactly one hook_created event per logical creation. Backward compatibility: a claim file written before this commit lacks `eventId`. Retries that read such a claim fall back to the event-log probe + fresh-eventId recovery — the legacy behavior that does not converge across workers but cannot regress for freshly- written claims after upgrade. world-postgres already converges across workers via the partial unique index on workflow_events_entity_creation_unique (runId+correlationId+eventType for hook/step/wait_created): the loser's INSERT raises 23505 which is already translated to EntityConflictError. Regression tests: - world-local: `converges same-hook creation across workers to one event` uses two tagged storage instances sharing one data directory and fires 25 paired Promise.allSettled hook_created calls. Expected 25 hook_created events total; before this fix yielded 50. - world-postgres: `converges same-hook creation across concurrent calls to one event` exercises the same shape against the real Postgres unique index. Already converges; the test is a guard against future regressions to the catch path. Verified the world-local test fails on c7b23e1b5 with exactly the shape pranaygp reported (50 events for 25 logical creations) and passes on this commit. The earlier orphaned-claim and orphaned- entity recovery tests also continue to pass. * fix(world-local): converge legacy hook claims via recovery-marker sidecar; replace tag-proxy test with real subprocess workers Addresses follow-up review on PR #2295. Two distinct issues, both flagged by pranaygp as P1: 1. The fallback path for token claims written by versions before eventId was persisted inline (legacy claims after upgrade) still permitted the same cross-process corruption the inline fast path was fixed to prevent. Two processes both reading a legacy claim each generated their own eventId, landed their writeExclusive(eventPath) calls at different paths, and appended two hook_created events for the same (runId, hookId). Existing persisted claims after a real upgrade are exactly the state the crash-recovery branch needs to repair, so leaving the legacy path non-convergent is silent corruption, not backward compatibility. 2. The committed cross-worker convergence test used two tagged storage instances sharing one directory as a proxy for separate processes. But tags change the destination filename (events/wrun_X-evnt_Y.worker-a.json vs ...worker-b.json), so two tagged workers can each writeExclusive their own event at different paths and both fulfill. The Map-by-eventId deduplication in the assertion then masked the duplicate publication, so the test passed for the wrong reason. Implementation: - New HookRecoveryMarkerSchema (`{ eventId, hookId, runId }`) and HookRecoveryMarkerPath helper. The marker is a sidecar at hooks/tokens/<hash>.recovery.json, written via writeExclusive so the first cross-process retry pins its candidate eventId as canonical; subsequent retries read the marker and adopt that eventId. Together with the existing writeExclusive(eventPath) in the outer publish, this gives the legacy-fallback path the same single-event convergence guarantee as the inline-eventId fast path. - pinCanonicalEventIdForLegacyClaim() encapsulates the marker write-or-read. A stale marker for a different (runId, hookId) (token-reuse with leaked state) is overwritten best-effort — the common cross-worker race for the same hook still converges; only the narrow stale-token-reuse case loses convergence. - hook_disposed now also deletes the recovery marker when it deletes the token constraint file, preventing a future legacy recovery for a recycled token from latching onto a stale eventId. - The dedup branch unified: existingClaim.eventId for new claims, pinCanonicalEventIdForLegacyClaim() for legacy ones. Removed the now-redundant findHookCreatedEvent helper — the writeExclusive(eventPath) in the outer publish is the authoritative duplicate-vs-orphan detector. Tests: - New test fixture test-fixtures/hook-race-worker.ts (TypeScript, run via child_process.fork with tsx as execPath — tsx is a transitive dev dep via vitest). Each subprocess gets its own createStorage(testDir) so the in-process hookLocks Map cannot serialize across workers. - Replaced the tag-proxy test with "converges same-hook creation across separate OS processes to one event". Spawns workerCount subprocesses, releases them from a barrier into the same hook_created, asserts exactly one fulfilled + (N-1) rejected with EntityConflictError, and asserts directly on the raw events.list() result (no Map dedup) that the number of hook_created entries equals the number of logical creations. - Added "converges same-hook creation across processes when only a legacy token claim exists". Same shape, but pre-seeds the legacy claim format (`{ token, hookId, runId }` with no eventId) before each race. Verified to FAIL on 7ce66551b (both subprocesses fulfill, no convergence) and pass on this commit. - Also verified the new-eventId subprocess test FAILS when the event write is reverted to writeJSON (TOCTOU), confirming it exercises the writeExclusive-based cross-process arbitration. Both prior orphaned-claim / orphaned-entity recovery tests also continue to pass. * fix(world-local): per-lifetime recovery markers, restore event-log probe, fix CI tsx resolution Addresses three P1 review comments on PR #2295. 1. Stale recovery marker leaking across token-reuse lifetimes (pranaygp): The previous marker path used `hashToken(token)` so a stale marker for run A could leak into run B's recovery when the same token was reused after run A terminated through normal lifecycle. `deleteAllHooksForRun()` and tagged `world.clear()` deleted the token constraint and hook entity but NOT the marker sidecar, so the next legacy claim on the same token entered the stale-marker overwrite branch and the workers overwrote it non-atomically, yielding divergent publication. Fix: - Marker path now hashes `(token, runId, hookId)` together (`hookRecoveryMarkerPath` in storage/helpers.ts). Different lifetimes can never share a marker, so the stale-marker overwrite branch is removed entirely. - `hookRecoveryMarkerPath` is moved to helpers.ts and shared across events-storage.ts, hooks-storage.ts, and index.ts. - `deleteAllHooksForRun()` and tagged `world.clear()` now also delete the recovery marker for each hook (disk hygiene; per- lifetime identity makes leaks no longer corrupting). - `hook_disposed` now uses the new per-lifetime marker path too. 2. Duplicate `hook_created` event when a legacy claim's event was already published (VADE bot, also implied by pranaygp's analysis): Removing the event-log probe from the legacy fallback let a post- upgrade retry pin a new canonical eventId via the marker and publish a duplicate event at that path, even when the original pre-upgrade writer had already successfully published the event with its own (different) eventId. Fix: - Restore `findExistingHookCreatedEventId()` (renamed and made to return the eventId for clearer semantics). - Legacy fallback now probes the event log BEFORE pinning the marker; if a matching `hook_created` event already exists, throw `EntityConflictError` so the runtime's concurrent-replay catch path swallows the retry. - Inline-`eventId` fast path does NOT need the probe — the claim itself is the durable convergence key. 3. CI failure: tsx not resolvable under pnpm isolated linking (pranaygp; confirmed by ubuntu/windows unit test 60s timeouts): The previous test hard-coded `node_modules/.bin/tsx` assuming tsx would be hoisted there. But tsx was only a transitive peer dep via vitest, and pnpm's isolated linking does NOT link transitive peer deps into the workspace bin after a fresh install — so neither root nor package-local `.bin/tsx` existed in CI, the subprocess fork never started, and the barrier hung until vitest killed the test. Fix: - Add `tsx` as a direct `devDependency` of `@workflow/world- local` (pinned to 4.20.6 to match the existing transitive resolution). - Resolve via `import.meta.resolve('tsx/package.json')` and read the `bin` field dynamically, so we adapt to wherever pnpm links tsx for this package — not a hard-coded layout. - Lazy-init the resolver (no module-load IIFE) so an absent tsx fails only the convergence tests, not all 376 tests in the file. - Surface a clear error message if resolution fails, calling out the cause (transitive vs direct deps) for future readers. Also: harden the barrier helper so `error` events and pre-ready exits resolve BOTH `readyPromises` and `donePromises`, then `SIGKILL` siblings. Previously a broken child only resolved `donePromises`, leaving `Promise.all(readyPromises)` pending until the per-test timeout (60s in CI). Regression tests added: - `legacy claim whose hook_created event was already published does not append a duplicate event` — pre-seeds a legacy claim AND a pre-existing `hook_created` event with a different eventId, asserts the retry throws EntityConflictError and the log still has exactly the original event. - `converges legacy claim recovery across run lifetimes after token reuse` — runs pranaygp's full lifecycle path: race subprocess workers on run A's legacy claim, terminate run A via `run_completed` (triggers `deleteAllHooksForRun`), reuse the token in a legacy claim for run B, race subprocess workers again, asserts exactly one fulfillment + one `EntityConflictError` per race and exactly one `hook_created` event per run. Both new tests verified to fail on 2c673e436 (after rebuilding): the published-event test throws via duplicate publish instead of EntityConflictError, the token-reuse test sees both run B workers fulfill (2 events instead of 1). The existing orphaned-claim and orphaned-entity recovery tests also continue to pass. CI loop confirmed to be repaired locally by spawning subprocesses via the new resolver and intentionally breaking the worker fixture to verify the helper fails fast (~500ms) instead of hanging at the barrier. * fix(world-local): defer hook entity write until event publish commits Addresses karthikscale3's P1 review comment on PR #2295. The dedup-recovery path used to write the hook entity BEFORE the outer event publish proved whether the attempt was repairing a missing event or just colliding with an already-published `hook_created`. For already-committed duplicates, the event write then throws `EntityConflictError`, but the hook entity had already been overwritten with the retry's payload — leaving the durable hook entity and the event log inconsistent (e.g. the entity reflects the retry's metadata while the event still carries the original). karthikscale3 reproduced this on the prior head by creating `hook_created` with metadata `{ v: "a" }`, then retrying the same `(runId, hookId, token)` with metadata `{ v: "b" }` and `isWebhook: false`: the retry threw `EntityConflictError` but `hooks.get()` returned the retry's payload. Fix: defer the hook entity write until AFTER the outer `writeExclusive(eventPath)` commits. The branch now only captures the entity-to-write and its overwrite options; the actual write happens immediately after the event publish in the shared trailing block. A retry that ends in `EntityConflictError` (the event was already published) now leaves the entity untouched. The first-writer happy path and all recovery paths (orphaned- claim, orphaned-entity, cross-worker convergence, legacy claim, token-reuse across lifetimes) are unaffected — they all reach the event publish successfully, then the entity write runs as before. Regression test `does not mutate an already-committed hook entity when a duplicate hook_created retry collides` added to world-local: runs karthikscale3's exact scenario and asserts the persisted entity still carries the original metadata and isWebhook. Verified to fail on the prior commit (persisted metadata = 0xbb instead of 0xaa) and pass on this commit after rebuilding. Parallel guard test `does not mutate an already-committed hook entity when a duplicate hook_created retry collides` added to world-postgres. Postgres already protected this via `onConflictDoNothing()` on the hook INSERT, but the test guards against a future regression that adds an UPDATE/UPSERT to the dedup path. * refactor(world-local): per-instance in-process locks; drop tsx subprocess test plumbing You were right that the tsx subprocess machinery was overkill for a storage-level convergence test. Replaced with a simple two-instance in-process test that exercises the same cross-process semantics without spawning anything. The trick: `stepLocks` and `hookLocks` were module-level Maps shared by all `createEventsStorage` calls in the same process. Move them inside the function so each `createStorage(dir)` call gets its own lock map. Two storage instances sharing one data directory then behave exactly like two separate OS processes: - independent in-process `hookLocks` Maps (no in-process serialization between them), and - a shared filesystem (so the on-disk `writeExclusive` claim / marker / event publish primitives are the only thing arbitrating convergence). This is also a real architectural improvement — the global lock map was always a leaky abstraction that made unit-test simulation of the cross-process path awkward. Changes: - `stepLocks` and `hookLocks` moved from module scope into `createEventsStorage`. `withStepLock` and `withHookLock` wrappers collapsed into direct `withInProcessLock(map, key, fn)` calls at the two call sites that need them. - The three convergence regression tests in `storage.test.ts` now use `const workerA = createStorage(testDir); const workerB = createStorage(testDir);` and race `Promise.allSettled` of `events.create` from both — no subprocess, no IPC, no barrier helper, no `raceHookCreatedAcrossProcesses`. Same assertions (exactly one fulfillment + N-1 `EntityConflictError` per race, raw `events.list()` shows exactly one `hook_created` per logical creation — no Map dedup) so the regression catches are identical. - Removed: `tsx` devDep, `test-fixtures/hook-race-worker.ts`, `HOOK_RACE_WORKER` / `resolveTsxLoaderUrl` / `TSX_BIN` / `raceHookCreatedAcrossProcesses` and the `fork`/`fileURLToPath` imports they pulled in. Verified (after rebuilding world-local): - All 379 tests pass on macOS in ~1s (was ~6.7s with subprocesses). - Convergence tests confirmed to still catch the bugs: temporarily reverted the `eventId = canonicalEventId` adoption → both workers fulfilled (2 events instead of 1). Temporarily reverted the legacy-claim marker pin → same: both workers fulfilled. - No subprocess machinery means no Windows-specific quirks (cli.mjs shebang, .cmd wrappers, .bin hoisting under pnpm isolated linking, etc.) that produced the Windows CI 60s timeouts. - World-postgres still has its own parallel guard test for the karthikscale3 "no-mutate-on-duplicate" regression; that one exercises real DB concurrency and is unaffected by this change. Full repo `pnpm test` (43 packages) and the `parallelStepsThenWebhookWorkflow` e2e test against world-local both green. * fix(world-local): repair event-first hook orphans from the persisted event; skip #1665 e2e on world-postgres - A crash between the hook_created event publish and the deferred hook entity write left the event committed with the entity missing and unrepairable (retries threw EntityConflictError without materializing the entity). Retries now rebuild the entity from the PERSISTED event's payload — never the retry's eventData — via a race-safe writeExclusive, on both the canonical-eventId collision path and the legacy-claim probe path. - Skip parallelStepsThenWebhookWorkflow e2e on world-postgres: the same-tick replay pattern surfaces a separate pre-existing step_started ordering bug there (#2331). --------- Co-authored-by: Peter Wielander <peter.wielander@vercel.com> |
||
|
|
564a47c504 |
fix: settle aborted parallel steps before completing abortParallelWorkflow (#2244)
* fix: wait for aborted parallel steps to settle * test: assert aborted results for parallel abort workflow |
||
|
|
ae8d6feeda |
Add native v4 workflow attribute events (#2226)
* Add native workflow attribute events * Fix abbreviated attributes docs sample * Document attribute replay ordering for step races * Address native attribute review feedback * Validate before claiming attr_set dedup lock; clearer start() attribute errors - world-local: claim the attr_set correlation lock only after validation, so a validation failure does not permanently mark the correlationId as written and wedge the run in a re-invoke loop on retry - world-postgres: distinguish a concurrently-deleted run from a cap violation when the guarded attributes update matches no rows - core: reject non-string initial attribute values in start() with a clear error instead of a downstream schema failure Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add attribute edge-case tests across all layers - core: normalizeAttributeChanges unit tests (non-object inputs, FatalError wrapping, key/value/batch limits, boundary lengths, UTF-8 byte counting) - core: start() rejects reserved keys, oversized keys/values, and over-cap initial attribute batches before any write - world-local + world-postgres: per-run cap enforced against existing attributes (upsert-at-cap allowed, removal frees room), oversized values rejected on attr_set, invalid initial attributes rejected on run_created - e2e: validation DX workflow asserting every invalid write throws a catchable FatalError naming the violated rule and limit, with the run staying healthy; start() rejects invalid initial attributes client-side Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Remove accidentally committed local e2e diagnostics artifact Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bump world-vercel to spec version 4 for native attributes The deployed workflow-server (vercel/workflow-server#469) materializes native attr_set events and accepts initial run attributes, but world-vercel still advertised spec v3 — so start(..., { attributes }) rejected itself client-side ('requires spec version 4') on every Vercel deployment, failing the new e2e seeding test across the prod matrix. New runs are now stamped v4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Reject duplicate correlated attr_set before materializing in Postgres A redelivered duplicate — including one carrying different changes for the same correlationId — previously re-applied the run attributes update and only then failed the event insert, leaving the snapshot out of sync with the event log. Pre-check the event log for the correlationId before mutating; the unique index still guards the truly-concurrent race, which is idempotent (deterministic replay carries identical changes). Also apply the suggested docs wording for initial attributes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Apply suggestion from @VaguelySerious Signed-off-by: Peter Wielander <mittgfu@gmail.com> * Fail the run on World-rejected attribute writes; un-nest runtime test Two fixes from review: - runtime.test.ts: the pre-existing test "propagates transient step_created failures..." was accidentally nested inside the new attribute-race test, failing the new test ("Calling the test function inside another test function is not allowed") and preventing the old test from running. Restored it verbatim at describe level. - A workflow-body attr_set the World rejects as invalid (e.g. the cumulative per-run attribute cap, which only the World can check) is deterministic: redelivering the orchestrator message replays the same write into the same rejection, wedging the run in redelivery with no terminal event. handleSuspension now wraps such rejections in FatalError, and workflowEntrypoint fails the run with the validation error instead of rejecting the delivery. Transient storage errors still propagate and retry via redelivery. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com> |
||
|
|
bb6ff9ac99 |
Patch vulnerable package dependencies (#2301)
* chore: patch package dependency vulnerabilities Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Prefer direct dependency upgrades for security fixes --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> |
||
|
|
625fab46c8 |
[e2e] Add event-log-race-repro label for triggering CI stress-test (#2159)
|
||
|
|
409b1033d9 | Allow setting workflow attributes from steps (#2157) | ||
|
|
4b5f017635 |
fix: stabilize abort signal E2E cancellation paths (#2150)
* fix: stabilize abort signal e2e cancellation paths * fix(core): tolerate duplicate durable abort receipts |
||
|
|
1e6b1fdea2 |
Attributes MVP (experimental and write-only) and CI hardening (#2134)
* fix(core): scan inline sourcemaps during error remapping * Attributes MVP (experimental and write-only) (#2088) |
||
|
|
c58cae6612 |
[Docs] Cookbook update for child workflows pattern (#2100)
* docs(cookbook): replace child workflow polling with hook resume pattern Recommend startAndWait() with withChildCompletionHook() for v4 and v5 child workflow orchestration instead of getRun().status polling loops. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix child-workflows cookbook review feedback Tighten resumeParentCompletion to a discriminated union so hook.resume typechecks, add zod to the vitest workbench, remove unused resumeHook import, and add an empty changeset per AGENTS.md. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(cookbook): trim child-workflows hook resume guide Remove redundant polling comparison copy, the getRun() alternative section, and v5-only start() tips to keep the cookbook focused on the hook pattern. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c597ad999d |
fix(workbench): cache .vercel/output for Next workbench builds (#2095)
Turborepo replays nextjs-turbopack:build from cache without restoring the Vercel diagnostics manifest (.vercel/output/diagnostics/workflows-manifest.json), which causes the Vercel deployment to fail post-build. Add .vercel/output/** to the workbench's Turbo outputs so it is persisted and replayed. Applies to both nextjs-turbopack and nextjs-webpack (whose turbo.json is a symlink). |
||
|
|
070bd0cea9 |
[next] make lazyDiscovery the default in withWorkflow (#1805)
* [next] make lazyDiscovery the default in withWorkflow
Flips the default for `workflows.lazyDiscovery` from `false` to `true`
so new projects get deferred workflow discovery automatically on Next.js
versions that support deferred entries (>= 16.2.0-canary.48). Older
versions continue to fall back to eager discovery.
Users can still opt back into eager discovery explicitly by passing
`workflows: { lazyDiscovery: false }`.
Also:
- Remove the now-redundant `lazyDiscovery: true` from the Next.js
workbench apps.
- Reword the fallback warning for clarity when lazy is the default.
- Update the local-build e2e assertion to match the new warning text.
- Update the withWorkflow docs with the new default.
* [workbench] remove commented 'export default nextConfig' lines
|
||
|
|
0d0bb013d7 |
Generate local gitignore when using public workflow manifests (#1683)
Co-authored-by: Peter Wielander <peter.wielander@vercel.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
76d786efa1 | [tests] Fix abort-fetch e2e flake (#2081) | ||
|
|
1d7bc24b3d | Fix Next workbench cached outputs (#2071) | ||
|
|
49da6c50b3 |
feat(core): support passing parent WritableStream to child workflow via start() (#2059)
* test(e2e): cover WritableStream passed as start() argument Adds an e2e workflow + test where a parent workflow gets a WritableStream via getWritable(), forwards it through start() to a child workflow, and the child step writes raw bytes to it. Asserts the external reader on the parent's stream observes the exact bytes the child wrote. * fix(core): avoid double-framing when WritableStream is forwarded via start() When a workflow's getWritable() handle is passed across start() to a child workflow, the parent step's reviver wraps it in a serialize transform that pipes into a workflow server stream. Until now, getExternalReducers.WritableStream then installed a second serialize transform on top of that — so every chunk the child step wrote got devalue-framed twice but only deframed once on the reader side, and external consumers saw the inner frame instead of the original bytes. Fix: tag every user-visible writable that's already backed by a workflow server stream with its (runId, name). When the external reducer recognizes those tags during dehydration, it bridges bytes straight from the new child-side server stream to the original server stream instead of piping through the user's writable. That leaves the producer-side serialize transform (installed once by the child's step reviver) as the only framing layer in the chain. * fix(core): forward (runId, name) when a tagged WritableStream crosses start() Replaces the previous in-process bridge with first-class writable forwarding at the descriptor level. When a parent workflow's getWritable() handle is passed as an argument to a child workflow, the dehydrated descriptor now carries the original (runId, name). The child run's step-side reviver opens the writable against the parent's server stream directly and resolves the parent run's encryption key (encrypt-only) via getEncryptionKeyForRun. This removes the architectural limitation that the bridge could only stay alive for the duration of the parent step process — on Vercel that capped forwarding at ~15 minutes regardless of the child run's lifetime, dropping any writes the child made after the parent step process exited. importKey() now accepts a usages parameter, defaulting to ['encrypt', 'decrypt']. The cross-run forwarding path imports with ['encrypt'] only so a compromised child run cannot decrypt any existing data on the parent's stream — only contribute new writes. * test: rename writable-forwarded workflows and cover step-context getWritable() Addresses PR review: - Rename writableForwardedToChildChildWorkflow → writableForwardedChildWorkflow (drops the duplicated 'Child' segment). - Split writableForwardedToChildWorkflow into two variants covered by a test.each: writableForwardedFromWorkflowWorkflow (workflow-context getWritable, the original test) and writableForwardedFromStepWorkflow (step-context getWritable passed directly into start() from the same step that called getWritable()). - Terser changeset description. |
||
|
|
c1242e8dc5 |
[nitro] Use nitro v3 functionRules for workflow routes (#1575)
Signed-off-by: Peter Wielander <peter.wielander@vercel.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Peter Wielander <peter.wielander@vercel.com> |
||
|
|
09a0c1d6d1 |
Remove instrumentation from workbench (#1959)
* Remove instrumentation from workbench * bump |
||
|
|
d0e3f2722b |
[swc-plugin] Capture lexical this for nested arrow step functions (#1935)
* [swc-plugin] Capture lexical `this` for nested arrow step functions When a nested arrow `"use step"` references the enclosing function/method's `this`, plumb that `this` through the workflow runtime so the step body sees the correct receiver. - Workflow mode wraps the step proxy with `.bind(this)`, so invoking the proxy captures the caller's `this` as `thisVal` on the queue item. - Step mode hoists the body as a regular `function` (not an arrow) so the runtime's `stepFn.apply(thisVal, args)` rebinds `this` inside the hoisted body. Detection only fires for arrows, since arrows inherit `this` lexically. Nested non-arrow functions/methods/getters/setters introduce their own `this`, so the detector stops at those boundaries. The runtime already supported `thisVal` for instance-method steps; this PR is purely a compiler change to feed the existing pipeline. Caveat: capture works at runtime only when the captured value is serializable across the workflow->step boundary (i.e. the enclosing class implements `WORKFLOW_SERIALIZE`/`WORKFLOW_DESERIALIZE`). Refs vercel/workflow#1865 * Address PR review: preserve step proxy metadata + tighter `this` detection - core: Override `.bind` on step proxies so the bound function retains `stepId` and `__closureVarsFn`. Without this, a bound proxy that flows through workflow serialization (e.g. as a step argument) would be treated as a non-serializable plain function by `getStepFunctionReducer`. - swc-plugin: Detector now also walks `arrow.params` so `this` references in default values / destructuring initializers (e.g. `(x = this.foo) => ...`) trigger the `.bind(this)` path. - swc-plugin: Class bodies inside the arrow body are now treated as `this`-binding boundaries — `this` inside class field initializers, methods, etc. is bound to the class instance, not the outer arrow. The detector still walks `extends` clauses and computed property keys because those are evaluated in the surrounding scope. - spec.md: Sharpen the note about `this` in step bodies — it's syntactically allowed but only meaningful for instance-method steps and lexical-`this` arrow steps; other shapes compile but `this` will be whatever the caller of the step proxy passes. - Add `lexical-this-detector-edge-cases` fixture covering both the default-param positive case and the inner-class false-positive guard. - Strengthen the runtime test to assert `stepId` / `__closureVarsFn` survive `.bind(...)`. * [swc-plugin] Fix `arguments` closure-var capture; drop dead `this`/`arguments` checks - Add `arguments` to `is_global_identifier` so it's not captured as a closure variable. Previously a nested `function`-form step like function step() { 'use step'; return arguments[0]; } was hoisted with `const { arguments } = ...` (a strict-mode syntax error) and the body's `arguments[0]` resolved against the destructured binding instead of the function's intrinsic `arguments` object. - Remove dead `ForbiddenExpression` checks for `this` and `arguments` in `visit_mut_this_expr` / `visit_mut_ident`. The `'use step'` / `'use workflow'` directives are stripped during the module-level traversal before children are visited, so `in_step_function` / `in_workflow_function` are never observed as true here in practice. The existing `step-with-this-arguments-super` fixture explicitly documents that all three identifiers are allowed in step bodies. - Tighten the spec note about `arguments` accordingly: it works in `function`-form steps (reflecting positional args) but is not captured for arrow-form steps; use `...args` for that case. - Add `nested-step-arguments` fixture pinning down the new behavior. |
||
|
|
aee56993c7 |
feat: serializable AbortController/AbortSignal (#1301)
* feat: add docs and test stubs for serializable AbortController/AbortSignal Adds documentation and test infrastructure for making AbortController and AbortSignal serializable across workflow and step boundaries. The feature uses a dual hook+stream backing: hooks for deterministic replay in the workflow context, streams for real-time propagation to running steps. Docs: - Cancellation guide (foundations) covering AbortSignal and run cancellation - How Cancellation Works (how-it-works) explaining hook+stream internals - AbortSignal.timeout() error page for the workflow VM restriction - Updated serialization docs with AbortController/AbortSignal section Tests (all .todo stubs for TDD): - VM behavior: AbortController API, static methods, hook integration - Step-side: stream reader setup, abort propagation, ops queue - Serialization round-trips: all boundaries, encryption, nested structures - Consistency: race conditions, partial failure, eventual convergence - E2E workflows: timeout, parallel, step-initiated, hook-triggered, replay Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: use correct frontmatter type for error page Change type from "error" to "troubleshooting" to match the valid frontmatter schema used by all other error pages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: address review feedback on cancellation docs - abort() in workflow does not synchronously update signal.aborted; instead it queues hook resumption and the replay handles state update - stream name and hook token are generated at serialization time (not deterministically in the workflow) and stored in the event log - use throwIfAborted() instead of manual signal.aborted checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: document runtime change for processing abort queue items on completion The current runtime only processes invocation queue items on suspension. When abort() is called after the last suspension point and the workflow completes, the queue items are dropped with a warning. Document that the runtime needs to flush abort-related items on completion/failure too. Add test stubs for this behavior. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: generalize queue processing on completion to all item types Processing pending invocations queue items on workflow completion/failure should apply to all queue item types (steps, hooks, waits, abort signals), not just abort-related ones. Update docs and tests accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: abort errors in steps are automatically wrapped in FatalError When a step throws due to an abort (AbortError from fetch, throwIfAborted, etc.), the error is wrapped in FatalError so the step skips retries. An abort is intentional cancellation, not a transient failure. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: remove contrived "aborting from within a step" example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add meaningful step-initiated abort example (quota monitor) Replace the contrived example with a watchdog pattern where a monitoring step polls an external condition and aborts parallel work when triggered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: remove unnecessary "as const" from hook cancellation example Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: implement serializable AbortController/AbortSignal Core serialization layer: - Add AbortController/AbortSignal to SerializableSpecial interface - Add reducers for all 4 contexts (external, workflow, step, common) - Add revivers for all 4 contexts with stream-backed propagation - Add reviveAbortController helper for step/external contexts - Guard instanceof checks for VMs without AbortController global Workflow VM: - New workflow/abort-controller.ts with createCreateAbortController factory - WorkflowAbortSignal class with hook-backed state - AbortSignal static methods (abort, any, timeout blocked) - Hook integration via invocations queue and events consumer Supporting changes: - Add ABORT_STREAM_NAME, ABORT_HOOK_TOKEN symbols - Add getAbortStreamId() for system stream namespace - Add isSystem, abortRequested, abortReason to HookInvocationQueueItem - Add isSystem to world Hook entity and events - Wrap AbortError in FatalError in step handler (skip retries) - Add AbortController/AbortSignal to Serializable type - Add observability revivers for abort types - Add isSystem to postgres schema and web-shared attribute panel All 454 existing tests pass with no regressions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: wire up AbortController in workflow VM and process queue on completion - Wire up AbortController/AbortSignal in workflow VM (workflow.ts) - Add abort processing to suspension handler (hook resume + stream write) - Process pending queue items on workflow completion (throw WorkflowSuspension instead of warning for actionable items) - Fix instanceof guards for non-function AbortSignal in VM - Update test to expect WorkflowSuspension for unawaited steps All 454 existing tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: implement tests and Request.signal serialization Tests (516 passing, 18 todo for integration tests): - 18 VM behavior tests (abort-controller.test.ts) - 18 step-side behavior tests (abort-controller-step.test.ts) - 4 consistency tests + 14 integration todos (abort-consistency.test.ts) - 14 serialization round-trip tests (serialization.test.ts) - 7 hook integration + 4 integration todos (step.test.ts) Request.signal serialization: - Add signal field to SerializableSpecial Request type - Include signal in Request reducer when present - Pass signal through in external and step Request revivers Fix workflow reviver for AbortController/AbortSignal: - Use plain objects instead of prototype-based stubs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: implement all remaining .todo test stubs Convert all 27 remaining .todo stubs to real implementations: - 14 consistency tests (race conditions, partial failures, queue processing) - 4 hook integration tests (suspension handler, hydration, eventual consistency) - 9 e2e tests (timeout, parallel, step-abort, hook-cancel, replay, external signal) All 558 tests pass, 0 todos remaining. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review comments + add changelog PR review fixes: - Move cancellation after streaming in foundations nav - Fix AbortSignal reducer to detect WorkflowAbortSignal via symbol - Guard AbortController reducer from matching AbortSignal objects - Add e2e tests: throwIfAborted, reason types, uncaught fetch AbortError Changelog: - Add hidden changelog section (not in sidebar, accessible via URL) - Add draft changelog entry for serializable AbortController/AbortSignal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: show changelog in nav for preview deployments only - Add `preview` flag to nav items in geistdocs.tsx - Filter preview items in Navbar (server component) based on VERCEL_ENV - Show "Preview" badge on preview nav items in DesktopMenu - Changelog link visible in preview deployments and local dev only Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: move preview badge from home page to navbar Move the PreviewBadge (with package tarball install modal) from the fixed bottom-right position on the home page to the navbar, so it appears on every page during preview deployments. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: consolidate preview tools into single Internal page Replace separate Changelog nav item and PreviewBadge with a single "Internal" page that only appears in preview deployments: - Rename docs/changelog/ to docs/internal/ - Internal page includes preview package install commands and draft changelogs in one place - Nav shows "Internal" with Preview badge in preview/dev only - Remove PreviewBadge from navbar (now on the Internal page) - Add callout that page is preview-only Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: use real deployment URLs on internal page + exclude from indexing - Add PreviewInstall component with copy-to-clipboard buttons using the actual VERCEL_URL (not placeholders) - Register PreviewInstallServer as MDX component for docs pages - Exclude /internal/ pages from sitemap.xml, sitemap.md, and llms.mdx - Add robots.txt Disallow for /internal/ paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing type declarations for docs code sample typechecking Add declare statements and @setup/@skip-typecheck annotations for undeclared functions in code samples (stepA, stepB, fetchData, cancellableStep, splitIntoChunks, processChunk). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add missing type declarations for all docs code samples Fix docs typecheck CI by adding declare statements and @skip-typecheck annotations for all undeclared function references across cancellation docs, error page, how-it-works page, and internal changelog. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: only suspend on completion for abort items, not all pending items The previous logic threw WorkflowSuspension for any pending queue item on completion (steps, waits, hooks). This broke fire-and-forget patterns like `void sleep('1d').then(...)` which intentionally leave a wait in the queue without awaiting it. Now only abort-related items (hooks with abortRequested) trigger suspension on completion. Other pending items get the original warning behavior — they may be intentional fire-and-forget operations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: all pending queue items are fire-and-forget on completion Remove special-case suspension for abort items on workflow completion. ALL pending queue items (steps, hooks, waits, abort signals) are now fire-and-forget when the workflow completes — they get warned about but don't block completion. This matches the existing behavior for fire-and-forget patterns like `void sleep('1d').then(...)`. Abort signals propagate through the normal suspension flow during the workflow (not at completion time). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: resolve docs typecheck errors in code samples Move declare statements before imports to avoid TypeScript overload signature conflicts with auto-inferred imports. Add @skip-typecheck for conceptual snippets. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: abort() in workflow updates signal.aborted synchronously abort() must update signal.aborted immediately so that: 1. Subsequent reads in the workflow see the correct state 2. Serialization captures aborted=true when passing signal to steps 3. Event listeners fire synchronously The hook resumption still happens via the suspension handler for durable event log recording. Both local state and durable state are now updated. Fixes e2e failures where steps received aborted=false for signals that were aborted before being passed to the step. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: update how-it-works to reflect synchronous signal.aborted update abort() now updates signal.aborted synchronously in the workflow. Update lifecycle diagram and remove outdated paragraph about signal not being updated synchronously. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: ensure abort listeners fire at deterministic point across replays On replay, hook_received is processed during event consumer subscription (at AbortController construction time), which is BEFORE the abort() call in the workflow code. If listeners fired during event processing, they'd fire at a different point than on first-run — breaking determinism. Solution: split abort into two phases: 1. _markAbortedFromReplay(): Sets signal.aborted=true (for reads/serialization) but does NOT fire listeners. Called by event consumer during replay. 2. abort(): Detects the replay flag and fires listeners at the call site. On first-run, fires listeners immediately as before. This ensures listeners fire at the abort() call site on BOTH first-run and replay, maintaining consistent ordering of side effects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add replay ordering tests for interleaved hook scenarios Add 3 tests validating that abort listeners fire at the abort() call site on both first-run and replay, even when other hook events are interleaved in the event log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: signal.aborted stays false until abort() is called for deterministic replay _markAbortedFromReplay no longer sets signal.aborted = true. Both aborted state and listener firing are fully deferred to abort(). This prevents if-checks on signal.aborted from taking different branches on first-run vs replay. Add deterministic branching test (unit + e2e): const controller = new AbortController(); if (controller.signal.aborted) { return 'was aborted'; // never taken } else { controller.abort(); return 'just aborted'; // always taken, both runs } Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: add abort+hook ordering matrix e2e tests (4 combinations) Test all combinations of listener registration order and event trigger order to validate deterministic ordering across first-run and replay: 1. addEventListener first, abort() first 2. addEventListener first, resumeHook first 3. hook.then first, abort() first 4. hook.then first, resumeHook first Each test verifies that abort-listener fires synchronously at the abort() call site (immediately before 'after-abort' in the log), regardless of when the hook is resumed or when listeners are registered. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: simplify abort — event consumer calls _setAborted directly Remove the deferred _markAbortedFromReplay approach. The event consumer now calls _setAborted directly when hook_received is processed, which sets signal.aborted = true AND fires listeners at that point. This is correct because: - Cross-execution aborts (step/external): signal.aborted SHOULD be true on replay since the abort is a fact from a previous run. Listeners must fire so the workflow can react to the abort. - Same-execution aborts: abort() fires _setAborted synchronously. On replay, the event consumer fires it first, and abort() is a no-op. - The promiseQueue ensures listeners fire at the deterministic point matching the hook_received event's position in the event log. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * test: skip abort+hook ordering e2e tests pending full integration The 4 ordering matrix tests require the abort controller's internal system hook to be fully wired through the suspension handler. The hook creation timing interacts with the user hook lookup in getHookByToken. Skip until the full integration is complete. All 13 other abort e2e tests pass on CI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * handle dangling streams * fix postgres world * fix abort serialization bug * refactors * add drizzle migration file * fix tests * fix tests * replace setTimeout probe and any casts with typed abort internals Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * cover post-serialization abort and nested-in-Request reader cleanup Two leak paths the prior fix left uncovered: - External signal aborted after serialization: verifies the listener attached by reduceAbortWithListener actually fires and writes the abort packet once the caller aborts later. - Signal nested inside a Request: exposed a real leak. The Request constructor copies the signal to an internal AbortSignal, so the ABORT_READER_CANCEL symbol set by reviveAbortSignal never reached request.signal, and cancelAbortReaders' walker had no Request case so Object.values(request) returned []. Fixed both sides: - Request reviver copies abort-internal symbols via copyAbortInternals - Walker descends into Request.signal explicitly Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * add v4/v5 docs switcher and pre-release gating - Mark new abort-controller/cancellation pages with preRelease: true (cancellation, how-it-works/cancellation, abort-signal-timeout-in-workflow, serializable-abort-controller). preRelease is a new optional frontmatter field declared in source.config.ts. - lib/geistdocs/versions.ts: declarative version list (v4 Latest, v5 Pre-release) plus getVersionFromPathname and buildVersionUrl helpers used by the switcher. - lib/geistdocs/version-source.ts: filter preRelease pages out of the v4 sidebar tree; rewrite sidebar URLs to /v5/docs/* on v5 so links stay in the pre-release view. - components/geistdocs/version-switcher.tsx: dropdown at the top of the sidebar, styled after the ai-sdk.dev pattern (label + subtitle). - components/geistdocs/pre-release-banner.tsx: banner rendered above the docs layout on all /v5/docs/* routes, linking back to /docs/* (Latest). - app/[lang]/v5/docs: parallel route (layout + page) that reuses the existing docs rendering but keeps preRelease pages visible. - app/[lang]/docs/[[...slug]]: 404 direct access to preRelease pages on v4 so unreleased content is never reachable without the /v5 prefix. - next.config.ts: /v5/docs -> /v5/docs/getting-started mirror of the existing /docs -> /docs/getting-started redirect. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix version switcher URL when default locale is hidden buildVersionUrl assumed segment 0 was the locale, but next.js i18n middleware hides the default locale from the URL so usePathname() returns '/docs/...' rather than '/en/docs/...'. The old logic treated 'docs' as the locale and produced '/docs/v5/getting-started' (404) instead of '/v5/docs/getting-started'. Detect the locale by checking whether segment 0 is a known structural token ('docs' or 'v5') rather than by position, so the function works for both '/docs/...' and '/<locale>/docs/...' inputs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * match ai-sdk pre-release banner styling Filled sparkles glyph, blue tint on the message text, and a plain underlined "Go to ..." link in the foreground color instead of a bordered pill. Matches the ai-sdk.dev v7 banner reference. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * match ai-sdk switcher icons and banner link color - Switcher: colored rounded icon tile next to each version (orange tint for pre-release, blue for latest), matching the ai-sdk.dev dropdown. Uses a workflow glyph inside a tinted ring. - Banner link: blue text with a softer underline by default, deeper blue on hover. Replaces the foreground-colored link that didn't match ai-sdk's styling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * use exact ai-sdk icons and darker banner link - Switcher tile: use the T-mark SVG and the bg-orange-100/border-orange-300 (pre-release) / bg-blue-100/border-blue-300 (latest) palette extracted from the ai-sdk.dev live markup, with matching dark-mode variants. - Pre-release banner sparkle: replaced the placeholder with the exact three-path geist sparkle used by ai-sdk. - Banner "Go to Latest" link: foreground color with a muted underline by default (same weight as ai-sdk's near-black link), underline intensifies on hover. The previous blue-600 was too light. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(docs): correct dark-mode colors for pre-release banner and version switcher The geistcn design-system palette inverts brightness semantics in dark mode (low indices = dim, high indices = bright) and remaps `blue-*` but not `orange-*`, so the previous token choices rendered as dim gray-blue text and a mid-bright blue icon inconsistent with the dropdown list. - Banner: use `dark:text-blue-900` for icon + label and switch the "Go to" link from `text-foreground` to the same blue (with a blue underline) so it reads as a single colored banner. - VersionSwitcher: move the text color onto the SVG itself so the `DropdownMenuItem` SVG-color override no longer hijacks the T color, and invert the dark blue palette (dark bg, light border, bright T) so the selected/trigger icon matches the list icon. - Active-row check icon: use green instead of `fd-primary` (which resolves to near-white in dark mode). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add signal field to Request serializable type The merge from main moved the Request type into serialization/types.ts without carrying over the signal?: AbortSignal field, causing the abort-related reducers/revivers in serialization.ts to fail typecheck. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: address review feedback on abort serialization - Dedupe abort listener attach in serialization reducers via marker symbol (prevents N-listener leak when one controller is serialized to N steps, which would double-close the backing stream on abort). - Replace token.replace('abrt_', '') string-surgery in suspension-handler by storing streamName directly on HookInvocationQueueItem at the point where it's already known (workflow/abort-controller.ts construction). - Document the deliberate sync-vs-microtask listener divergence in the workflow VM (replay determinism > spec parity inside the VM). - Add changeset noting the AbortError -> FatalError behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: correct cancellation docs against implementation - Remove the contradictory paragraph claiming signal.aborted is not set synchronously when abort() is called in the workflow. The implementation sets it sync via _setAborted; replay re-applies via the events consumer. - Reword the "Stream Succeeds, Hook Fails" recovery — there's no in-process retry loop on the step-side resumeHook call; convergence comes from the next replay re-reading the stream. - Tighten Request.signal handling: plain non-aborted native signals are intentionally dropped to avoid minting stream infra for auto-generated Request signals; only already-aborted or workflow-tagged signals are forwarded. - Replace the wrong "Pending queue items processed on completion" bullet with an accurate fire-and-forget note matching the warn-only behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: DOMException serialization (replace broken isNativeError guard) DOMException is `instanceof Error` in Node but does NOT pass `types.isNativeError()` — the existing reducer's first guard was `isNativeError(value)`, so DOMException never matched. Devalue then fell through to its arbitrary-POJO failure path. This surfaced as a real bug for AbortController/AbortSignal: when abort() is called with no argument, native AbortController synthesizes a default DOMException as signal.reason. Returning that signal's reason from a step (e.g. `{aborted, reason: signal.reason}`) crashed step return-value serialization. Replace the guard with a constructor-name check (cross-VM safe; same pattern used elsewhere for matching Error subclasses across realms). Also fixes 7 pre-existing DOMException tests in serialization.test.ts that were previously failing on main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: drain pending queue items on workflow completion End-of-run now goes through the same suspension handler that processes a real suspension. Previously, items left in the invocations queue when the workflow function returned (or threw) were dropped with an "uncommitted operation" warning — `controller.abort()` called as the last statement of a workflow never actually propagated. Concretely fixes: - Abort hooks now write hook_received + stream packet so in-flight steps on other compute instances see signal.aborted=true and bail out. - Unawaited hooks are created (so external callers can resume them). - Unawaited steps and sleeps are queued (will execute / fire later). Strengthens abortTimeoutWorkflow's test to inspect the event log for the hook_received event — the original assertion only verified the workflow VM's local signal.aborted, which was set synchronously by the abort() call regardless of whether propagation actually happened. The strengthened test fails on main and passes after this commit. Drops the warnPendingQueueItems warning entirely. Drain failures are swallowed so the workflow's own outcome (return value or thrown error) remains the source of truth for the run's terminal state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: cover the deserialized AbortSignal listener path with an in-flight fetch The existing abort tests exercised either the polled `signal.aborted` read path (longStep busy-wait) or the already-aborted-before-fetch path. Nothing exercised the live listener path: signal starts non-aborted, step kicks off a fetch against a slow endpoint, abort fires while fetch is awaiting the response, and fetch's internal `signal.addEventListener('abort', …)` listener cancels the in-flight HTTP request. The pre-existing `fetchWithSignal` helper step was orphaned — defined but not referenced by any workflow. Wires it into a new `abortFetchInFlightWorkflow` that races a 30s fetch against a 2s sleep, aborts when the sleep wins, and returns the step's catch-path result. The test asserts both `winner=timeout` and `fetchResult.aborted=true`, which together prove fetch saw the cancellation mid-flight (the natural-completion path would set ok=true,aborted=false). Adds a local /api/delay endpoint to the nextjs-turbopack workbench so the test doesn't depend on an external service. Honors the request's own AbortSignal so cancelled connections close immediately. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: extend abortFromStepWorkflow to verify in-flight sibling cancellation The original test only asserted that the workflow VM's signal saw aborted=true after a step called controller.abort(). It didn't actually verify that another in-flight step received the cancellation through the backing stream — those two paths are different (workflow VM signal updates via the hook event; sibling-step propagation runs through the live stream packet). Restructure the workflow to run longStep (a 30s polling loop on signal.aborted) in parallel with abortFromStep (now sleeps 1s, then aborts). The new assertion expects longStep.result === 'aborted' — proving it exited via the abort branch within ~1.5s, NOT ran to its 30s natural completion. Returning 'completed' would mean realtime cross-step cancellation is broken. abortFromStep gained an optional delayMs parameter so it can be sequenced against a sibling without an out-of-band sleep. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: dehydrate abort stream packets via the same machinery as hook events The abort stream packet was being encoded with bare `JSON.stringify({reason})` on the writer and decoded with `JSON.parse(text).reason` on the reader. That codec drops `undefined` (so a reason-less abort wrote literally `{}` and the observability UI showed an empty stream), and doesn't handle DOMException or any other type the rest of the codebase serializes via devalue+reducers. Switch all three sites — suspension-handler workflow-side write, patched abort step-side write, and `setupAbortStreamReader` — to use `dehydrateStepArguments`/`hydrateStepArguments`. Now the `reason` round-trips with full type fidelity (DOMException, custom errors, encrypted payloads), matching what the hook event payload already does. The suspension handler literally reuses the same dehydrated bytes for the event and the stream so they're guaranteed identical. Encryption key threading: - Suspension handler: `encryptionKey` was already in scope. - Patched abort: read from `contextStorage.getStore()?.encryptionKey` (set by the step handler before invoking the deserialize chain). - Reader (`setupAbortStreamReader`): read from `contextStorage.getStore()?.encryptionKey` for the same reason; falls back to `undefined` when called outside step context (the hydrate path is key-tolerant). On-disk verification: - Before: chunk for `controller.abort()` (no reason) was `00 7b 7d` — 3 bytes, the literal JSON `{}`, no reason carried at all. - After: chunk is `00 64 65 76 6c [{"aborted":1,"reason":2},true,"test"]` — 43 bytes, devalue-flat-encoded with the reason intact. Updated the existing stream-reader unit test to encode its mock payload through the same dehydrate path so the reader can decode it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: cover addEventListener, mid-flight throwIfAborted, and step-initiated determinism The polled-`signal.aborted` path was the only abort consumption pattern exercised end-to-end. Three new e2e tests fill the gaps: - **abortListenerWorkflow** — `signal.addEventListener('abort', cb)` firing on the deserialized step-side signal. Distinct from abortFetchInFlightWorkflow which only proves it indirectly through fetch's internal listener; this one verifies user-attached listeners directly. Step resolves with via:'listener' if propagation worked, via:'timeout' on a 30s safety timeout if it didn't. - **abortThrowIfAbortedMidFlightWorkflow** — throwIfAborted() in a polling loop, not just at step entry. The existing abortThrowIfAbortedWorkflow only covers the synchronous-throw case on a pre-aborted signal. This one starts the signal non-aborted, polls throwIfAborted every 500ms, and aborts from a sibling step after 1s. Verifies the DOMException propagates as FatalError (no retries) when fired mid-flight. - **abortDeterministicBranchFromStepWorkflow** — counterpart to abortDeterministicBranchWorkflow, but with the abort source being a step (via the patched abort() path / hook event) instead of the workflow body. Both branch-reads MUST take the same path on every replay. Uncovered a real semantic: signal.aborted reflects step-initiated aborts only after the next promise-queue checkpoint (sleep, step await, etc.) since _setAborted is chained on promiseQueue. The test inserts the required sleep('1s') checkpoint and asserts both pre and post values. Helper steps factored: stepWaitingOnAbortListener and stepPollingThrowIfAborted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: drop signal.aborted shortcut in stepWaitingOnAbortListener The shortcut would have masked a regression in the addEventListener-on-an- already-aborted-signal contract. Per the AbortSignal spec, calling addEventListener('abort', cb) on an aborted signal fires the callback (on a microtask), so user code that subscribes via the listener path alone — the common pattern — depends on it. Test the contract directly: rely solely on the listener resolving the promise. If addEventListener-on-aborted ever silently breaks, this test now reports via:'timeout' instead of paving over it with a fast-path that reads signal.aborted directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: add DOMException reviver to observabilityRevivers so the o11y UI hydrates abort reasons The observability UI (and CLI) hydrates step IO via `observabilityRevivers`, which had no `DOMException` entry. When a step returned a value containing a DOMException (typically `{aborted, reason: <DOMException>}` — synthesized by native AbortController when abort() is called with no reason), devalue's `parse` would throw on the `["DOMException", ...]` tag, `hydrateStepIO`'s try/catch would swallow it, and the raw devalue-flat string survived to the UI. The user-visible result was step Output showing literal text like: devl[{"aborted":1,"reason":2},true,["DOMException",3]...] instead of a JSON viewer with a proper DOMException card. Add the reviver. Reconstruct as a real DOMException when the global is available (modern browsers + Node 18+, where the o11y consumers run), falling back to a name-tagged Error otherwise. Preserves message/name/ stack/cause for display. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: cover the external-signal-aborted-in-flight propagation path The existing abortExternalSignalWorkflow only validates a static read of an already-aborted signal — it tells us nothing about whether an abort that fires AFTER serialization actually propagates from the caller process, through the listener attached at workflow-start, into the backing stream, and out into the deserialized signals on the in-flight step compute. Add abortExternalSignalInFlightWorkflow that takes a non-aborted signal and runs two parallel consumption patterns against it: longStep (polling signal.aborted) and stepWaitingOnAbortListener (addEventListener path). The test creates a fresh AbortController, calls start() with its non-aborted signal, and aborts the source controller 1.5s later via setTimeout — well after both steps are mid-flight on their compute instances. Both consumers must see the cancellation: - pollResult === 'aborted' (NOT 'completed' — that would mean longStep ran the full 30s without ever seeing signal.aborted=true) - listenerResult.via === 'listener' (NOT 'timeout' — that would mean the addEventListener callback never fired) This exercises the longest end-to-end abort path in the codebase: caller-process AbortController → serialization-time listener → backing stream → step compute → deserialized signal → (poll OR addEventListener) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): use httpbin.org/delay for abortFetchInFlightWorkflow The previous setup added a /api/delay route to workbench/nextjs-turbopack to give the test a slow endpoint to fetch against. That made the workflow fail in CI on every other workbench (nextjs-webpack, astro, sveltekit, …) since the route only existed on one of them — fetch returned 404 and the test failed within 1s instead of taking the expected ~3s. Switch to httpbin.org/delay/30, the same external-service pattern used by other e2e workflows in this file (jsonplaceholder, example.com). Removes the per-workbench dependency. Drops the now-unused deploymentUrl argument from the workflow signature and test call site. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: fix serialization page — drop duplicate header, move AbortController section Two issues on the serialization foundations page: 1. `## Pass-by-Value Semantics` appeared twice. The second occurrence had no body, which rendered as an orphaned heading just above the AbortController section in the docs preview. 2. `## AbortController & AbortSignal` was at the bottom of the page, after `## Custom Class Serialization`. It belongs above the custom-class section so the standard serializable types are grouped together before the advanced topic. Removes the empty duplicate; relocates the AbortController section to sit between Request & Response and Custom Class Serialization. No content changes inside the section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: note that run.cancel() is the same as the observability Cancel button The Run Cancellation section showed the programmatic path but didn't tie it back to the UI. Add a callout: calling run.cancel() is the same action as clicking the Cancel button on a run in the observability UI — both produce identical run_cancelled events. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test: cover AbortSignal.any in both workflow VM and step contexts Two distinct paths: the workflow VM ships its own AbortSignal.any impl in workflow/abort-controller.ts (composes WorkflowAbortSignals via listeners, no stream/hook backing on the composite), while steps use the native Node implementation over deserialized signals. Neither was tested. abortAnyInWorkflowWorkflow exercises the VM impl directly: creates two controllers, composes their signals via AbortSignal.any, aborts one, and asserts the composite reflects the abort synchronously without any stream round-trip. Also asserts the other source signal is unaffected so a mass-abort regression would surface here. abortAnyInStepWorkflow exercises the longest end-to-end path that uses AbortSignal.any: source controller is aborted by a sibling step, abort flows through the workflow's VM, then the backing stream, into the step's deserialized signal, into the AbortSignal.any composite, into the user's listener. Returning via:'timeout' instead of via:'listener' would mean a break anywhere on that chain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Update .changeset/fix-dom-exception-serialization.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Update .changeset/serializable-abort-controller.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Update .changeset/drain-pending-queue-on-completion.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * docs(errors): match slug-as-title convention + simplify the timeout example Two toolbar-comment fixes on the abort-signal-timeout-in-workflow error page: 1. The page title was Title Case ("AbortSignal.timeout() in Workflow") while every other page in docs/content/docs/errors/ uses the kebab-case slug as the title (e.g. timeout-in-workflow, fetch-in-workflow, workflow-not-registered). Match the convention. 2. The recommended replacement for AbortSignal.timeout() was a Promise.race that wrapped the abort + null sentinel + custom Error throw. Boil it down to the much simpler: const controller = new AbortController(); void sleep("10s").then(() => controller.abort()); return await fetchData(controller.signal); If fetchData finishes within 10s you get the response; if not, the timer fires controller.abort(), fetch rejects with AbortError, and the step's failure propagates to the workflow as a FatalError (no retries). Same observable behavior, no Promise.race scaffolding. Adds abortVoidSleepTimeoutWorkflow + matching e2e test that exercises this exact pattern end-to-end so the doc example is verified runnable (not just pseudocode). Asserts the fetch is cancelled mid-flight by the timer, returning aborted=true,ok=false from the step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
ab639a1cd6 |
Add dev-tmux skill for portless+tmux local Workflow SDK dev (#1916)
* [e2e] Add step-vs-sleep race tests + dev-tmux skill Adds two race workflows (sleepWinsRaceWorkflow, stepWinsRaceWorkflow) that exercise Promise.race between a step function and a sleep call. The current `sleepWinsRaceWorkflow` test fails — surfacing how the replay engine resolves a previously-completed step instantly while sleep still has to elapse. Also adds a `dev-tmux` skill that documents the 3-pane tmux + portless setup for testing workflows interactively in a worktree alongside the observability UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [workbench/nextjs-turbopack] Allow *.turbopack.localhost in dev Adds allowedDevOrigins entries so portless-style worktree-prefixed .localhost URLs (e.g. https://<branch>.turbopack.localhost) can hit HMR and dev-only endpoints without Next's cross-origin protection flooding the logs with warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop duplicate race workflows after merging main PR #1924 added the same sleep/step race workflows directly to main while this branch was open. The textual concat from `git merge` left both copies in 99_e2e.ts; this drops the duplicate set so the file matches origin/main verbatim and the e2e tests pick up the upstream definitions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Narrow activation, robust pane IDs, statusline helper - Tighten activation phrases so the skill only fires for the specific portless+tmux setup it documents, not the generic "start the dev server" task. Addresses #1916 review. - Capture pane IDs at split time (-P -F '#{pane_id}') so the snippet works under both pane-base-index 0 and 1. Addresses Copilot review. - Add `statusline.sh` that filters `portless list` to the current worktree's routes and emits a one-line summary, plus instructions for wiring it into Claude Code's `statusLine.command`. - Bump version to 1.1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Recommend primary-checkout path for statusline Worktrees get deleted, so wiring the statusline to a worktree path breaks the moment the worktree is removed. Update the skill and the script header to recommend pointing `statusLine.command` at the primary checkout (`$HOME/github/vercel/workflow/...`). The script itself is already worktree-aware via Claude's `workspace.current_dir` stdin JSON, so the same invocation surfaces routes for whichever worktree the session is in. Bump version to 1.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] OSC 8 link statusline + worktree-named tmux session - Statusline overlay now renders `[dev] · [obs] · tmux:<prefix>`, with the bracketed labels emitted as OSC 8 hyperlinks (clickable in any modern terminal) styled cyan + underline so they stand out. Replaces the old long-URL form that was hard to scan and click. - Add a tmux-session indicator: shown when a session named exactly the worktree prefix exists (uses `tmux has-session -t =<prefix>` for exact matching). - Change the skill's tmux session naming convention from the fixed `workflow-dev` to `<worktree-prefix>` (basename of the branch — same string portless uses as the subdomain prefix). This lets the statusline locate the session deterministically and lets multiple worktrees run dev sessions concurrently without manual disambiguation. - Bump skill to v1.3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Statusline: print full `tmux attach -t <name>` command Replaces the abbreviated `tmux:<prefix>` indicator with the full copy-paste-ready `tmux attach -t <prefix>` invocation. Saves a step when grabbing the session from another shell. Bump skill to v1.4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Brighter statusline + Nerd Font icons - Drop the dim styling that made the overlay hard to read; use bold bright cyan + underline for links and bold bright green for the tmux command. - Add Nerd Font glyphs: for dev, for obs, for the tmux copy-paste hint. Falls back to box-drawing if the font lacks Nerd Font ranges; layout is unaffected. - Visual differentiation: cyan + underline = clickable hyperlink; green = copy this command. Bump skill to v1.5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/dev-tmux] Restore Nerd Font icons via Unicode escapes The copy glyph in `emit_tmux` was a literal Nerd Font byte embedded in the printf string and got stripped during a prior rewrite. Promote all three icons (rocket / graph / copy) to top-level shell variables that use \uHHHH-equivalent UTF-8 escapes, so the source survives editor round-trips that don't preserve Private Use Area code points. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [skills/internal-dev-workbench] Rename from dev-tmux, set author + reset version - Rename `skills/dev-tmux/` → `skills/internal-dev-workbench/` to make the name self-explanatory about the skill's scope (an internal contributor's local dev workbench, not a generic tmux helper). - Author: Pranay Prakash. Version: 0.1 (first release of the skill). - Update internal references in SKILL.md and statusline.sh accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3535caf449 | [core] Skip inline step execution when suspension also has a wait (#1924) | ||
|
|
5f22832675 |
Serialize run_failed/step_failed errors through serialization pipeline (#1851)
* Serialize run_failed/step_failed errors through serialization pipeline
Switch run_failed, step_failed, and step_retrying events to persist
the full thrown value via the workflow serialization pipeline (as
SerializedData / Uint8Array) instead of a lossy { message, stack, code }
StructuredError shape. Consumers hydrate via hydrateRunError /
hydrateStepError to reconstruct the original thrown value, preserving
Error subclass identity, cause chains, and custom properties.
- WorkflowRun.error and Step.error are now SerializedData
- WorkflowRun gains a top-level errorCode plaintext field
- WorkflowRunFailedError.cause is now the hydrated thrown value
- Adds world-postgres migration 0010_add_error_code.sql
- Legacy pre-pipeline errorJson records surface as undefined on read
* Update Next.js workbenches for new WorkflowRunFailedError.cause type
cause is now `unknown` (the hydrated thrown value) rather than
`Error & { code }`. Defensively extract Error-shaped fields when the
hydrated value is an Error, otherwise round-trip the raw value, and
expose the new `errorCode` classification field.
* Update docs for WorkflowRunFailedError.cause: unknown
The hydrated `cause` is now `unknown` (the original thrown value
through the serialization pipeline) and the error classification has
moved to the top-level `errorCode` property. Update the two affected
docs pages and the `TSDoc` interface to reflect the new shape, and
narrow `cause` with `instanceof Error` before accessing fields.
* Expand test coverage for the run/step error serialization pipeline
Unit tests:
- 19 new dehydrate/hydrate{Step,Run}Error round-trip tests covering
FatalError, plain Error, built-in Error subclasses, non-Error thrown
values (string, plain object), cause chains, encryption round-trip,
the binary format prefix contract, and the unserializable / unknown-
format error paths.
- 5 new tests for Run.returnValue when the run is failed: hydrated
FatalError + cause as cause, plain Error preservation, non-Error
thrown values surfaced verbatim, cross-class cause chains, and the
hydration-failure fallback that still surfaces errorCode.
E2E tests (new, in 99_e2e.ts + e2e.test.ts):
- Step throw → workflow catch round-trips a FatalError with a TypeError
cause chain, asserting class identity, fatal marker, and cause name +
message all survive the step_failed event pipeline.
- Workflow throw → run_failed reaches status with the new
top-level errorCode metadata exposed (cause-shape coverage lives at
the unit level, since the SWC plugin's class registration is not
invoked in the plain-Node e2e runner).
- Workflow throw of a non-Error value round-trips that value verbatim
as WorkflowRunFailedError.cause.
Adjustments to existing assertions:
- error.cause is now ; tests narrow with
and use the new top-level field instead of .
- step.error / run.error from CLI --withData are now hydrated payloads:
unregistered class instances surface as Instance refs whose
carries the original message + stack.
Observability hydration:
- hydrateStepIO / hydrateWorkflowIO in serialization-format.ts now
hydrate the field via hydrateData, so the CLI and web UI
continue to surface readable run/step error messages and stacks.
* Tighten error serialization changeset description
* Trim error serialization changeset to a single sentence
* Resolve FatalError/RetryableError revivers via cross-realm registry
When a workflow runs in a Node `vm` context, its bundled
`@workflow/errors` is a different module instance than the host's
import (separate prototype chains, separate class identity). Calling
`new FatalError(...)` from the host-side reviver produces a
host-realm instance that fails `err instanceof FatalError` checks
in the workflow code — even when the serialized payload was correctly
tagged via the dedicated `FatalError` reducer.
Surfaced by the local-prod e2e "step throw round-trips FatalError"
test on Next.js Turbopack: each route gets its own bundled chunk, so
the flow handler's `@workflow/errors` and the workflow VM bundle's
`@workflow/errors` are two distinct copies of the same module.
Fix:
- Each bundled copy of `@workflow/errors` self-registers its
`FatalError` and `RetryableError` classes on `globalThis` via
`Symbol.for("@workflow/errors//FatalError")` /
`Symbol.for("@workflow/errors//RetryableError")`. First load wins
per realm; the descriptor is non-writable / non-configurable to make
accidental clobbering loud.
- The revivers in `@workflow/core`'s common reducers module read the
consumer's `globalThis` (passed in as `global`) to pick up the
realm-local class, falling back to the host-imported class when no
registration is present (e.g. in the CLI / test runner).
* Use `types.isNativeError` to remap workflow stacks across VM realms
The runtime's run-failure path computes a source-map-remapped stack
and then assigns it back onto the thrown value via `if (err
instanceof Error) err.stack = errorStack`. Workflows run inside a
Node `vm` context, so a workflow-thrown error is an instance of the
VM realm's `Error` — `instanceof` against the host realm's
`Error` returns `false`, the assignment is skipped, and the
serialized `run_failed` event carries the un-remapped (bundled-line-
number) stack instead of the source-mapped one.
Switch the gate to `types.isNativeError`, which uses V8's internal
type tag and works across realms — same approach already in place
for the serialization reducers.
Caught by the local-prod e2e "nested function calls preserve message
and stack trace" and "cross-file imports preserve message and stack
trace" tests, which assert that the persisted run-error stack
contains `99_e2e.ts` / `helpers.ts`.
* Sync CLI revivers with core + add toJSON shim for Error subclasses
Two issues with the CLI's hand-rolled reviver list:
1. It hadn't been updated for the new first-class Error subclass
reducers (`TypeError`, `RangeError`, `FatalError`, `RetryableError`,
etc.). devalue throws "Unknown type X" when it encounters a
reduced value with no matching reviver, and `hydrateResourceIO`
swallows that error and surfaces the raw `Uint8Array` payload —
so `step.error` / `run.error` showed up as raw byte dumps in
`workflow inspect` output.
2. Even with all the right revivers, `Error.prototype`'s `message`
/ `stack` / `cause` are non-enumerable, so `JSON.stringify`
(used by `workflow inspect --json`) drops them — leaving the
subclass-specific enumerable fields (e.g. `FatalError.fatal`)
visible but the actual error data missing.
Fix:
- Build the CLI reviver set on top of `getCommonRevivers()` from
`@workflow/core` so the CLI stays in sync with the runtime's
reducer set automatically. New core reducers/revivers will Just
Work without any CLI-side change.
- Wrap each Error reviver from the common set with a thin shim that
attaches a non-enumerable `toJSON` method to the produced
`Error` instance. `JSON.stringify` calls `toJSON` and gets a
full object (`name` + `message` + `stack` + `cause` + any
enumerable subclass fields like `fatal` / `retryAfter` /
`errors`); `util.inspect` ignores `toJSON` and renders the
canonical `Error: msg\\n at ...` format. Best of both worlds for
CLI output without compromising the runtime hydration path.
Caught by the local-prod e2e "basic step error preserves" and
"cross-file step error preserves" tests, which read
`failedStep.error.message` / `.stack` from the CLI's JSON output.
* Clarify parseErrorJson JSDoc to match its always-null return
The previous JSDoc described preserving legacy values "for best-effort
hydration" which contradicted the implementation, where legacy errors
are intentionally surfaced as absent (the pre-pipeline shapes can't be
hydrated by the new error revivers). Rewrite the comment so the contract
matches behavior. Also rename the now-unused parameter to `_errorJson`
to reflect that the function ignores it.
Caught by a code review on #1851.
* Refine error-handler ergonomics on the step / run hot paths
Three review-driven adjustments that all touch the queue handlers and
their interaction with the error serialization pipeline:
1. Memoize the per-run encryption key fetch. The step handler used to
eagerly fetch + import the key at the top of every step delivery so
the value would be in scope for every potential dehydrateStepError
path. That pessimized step-started early-return cases (the fetch
happens unconditionally even when the step never reaches user code)
and required duplicating the same boilerplate at four call sites in
runtime.ts. Introduce `memoizeEncryptionKey(world, run)` in
runtime/helpers.ts that returns a lazy, single-fetch accessor;
step-handler / runtime call sites use `await getEncryptionKey()`
instead. The first caller pays the fetch cost, subsequent callers
await the cached promise, and steps that fail before any
encryption-aware work happens skip the fetch entirely.
2. Preserve the prior attempt's serialized error as the cause on the
defensive max-retries-exceeded `step_failed` re-invocation guard.
The existing comment explicitly opted out of cause attachment, but
the symmetric post-failure path below already does this and the
reviewer is right that consumers shouldn't have to walk the
step_retrying event history to recover the underlying error. Best-
effort: if hydration of the prior `step.error` throws, fall back
to a FatalError without cause rather than letting the event write
itself fail.
3. Document the intentional `unflatten` throw in
`hydrateStepError` / `hydrateRunError` for non-Uint8Array input.
SDK version is pinned per workflow run via skew protection so the
non-binary branch is dead in production; if a misshapen value
reaches it, surfacing the throw via the surrounding o11y try/catch
is more debuggable than masking it. Add a comment so future
reviewers don't reach for a defensive fallback.
A standalone `falls back to plaintext` suggestion on the run_failed
key fetch was rejected: when encryption is configured we should fail
loudly rather than silently emit plaintext error data. The queue's
redelivery semantics will retry the key fetch; persistent KMS outages
get logged with the existing "persistent error preventing the run from
being terminated" message rather than a security regression.
* Hydrate `event.eventData.error` in event listings
`hydrateEventData` enumerated the per-event fields that need
hydration (`result`, `input`, `output`, `metadata`, `payload`)
but omitted the new `error` field on `step_failed`,
`step_retrying`, and `run_failed` events. Without this branch,
o11y tools that list events (e.g. `workflow inspect events`) surface
the raw `Uint8Array` payload instead of a hydrated
`{ name, message, stack, … }` object even though the entity-level
`Run.error` / `Step.error` paths already hydrate.
Mirrors the existing per-field branches; the `try/catch` leaves the
field un-hydrated on parse failure rather than failing the whole
event view. Adds a unit test.
* Use `.is()` static checks in `classifyRunError` for cross-realm safety
Workflows execute inside a separate `vm` realm: the
`WorkflowRuntimeError` class bundled into the workflow code and the
host-imported one are distinct constructors, so an
`err instanceof WorkflowRuntimeError` check on a VM-thrown error
returns `false` and we'd misclassify genuine runtime errors (corrupted
event log, missing timestamps, workflow/step not registered) as user
errors.
Switch to each subclass's `.is()` static (a name-based duck check that
works across realms). Since `WorkflowRuntimeError.is` only matches its
own concrete name, enumerate every concrete subclass we want to
recognize (`StepNotRegisteredError`, `WorkflowNotRegisteredError`)
in a `RUNTIME_ERROR_CHECKS` table; keep that table in sync with the
class hierarchy in `@workflow/errors`.
Existing `classify-error.test.ts` already covers `WorkflowRuntimeError`
and `WorkflowNotRegisteredError` cases — both still pass.
* Add e2e coverage for step throws of non-Error values
We had `errorWorkflowThrowNonErrorValue` (workflow body throws a plain
object — round-trips verbatim as `WorkflowRunFailedError.cause`) but
no symmetric coverage for the step-throw side. Step-throw goes through
a different code path: non-Error values aren't recognized as
`FatalError` (no `name === 'FatalError'`) nor `RetryableError`,
so they take the transient retry path. After max retries the runtime
wraps the original thrown value as `cause` on a fresh `FatalError`
which the workflow's catch block then sees.
Add a workflow that throws a recognizable plain object from a step
with `maxRetries = 0` (so we exhaust on first attempt and avoid a
long test wait) and a workflow that asserts the wrapped FatalError
shape: `isFatal`, `instanceof FatalError`, message includes the
original object's serialized form, `cause` is the original non-Error
object verbatim with structure preserved.
Documents the current retry-then-wrap behavior so any future change
to "non-Error throws skip retries" semantics has to update the test.
* Note legacy postgres error-data loss in the run/step error changeset
Pre-upgrade failed runs that wrote into world-postgres's deprecated
`error` text column can't be hydrated through the new pipeline (the
shape is incompatible with the new revivers). The new runtime
intentionally surfaces them as `error: undefined` on read; the
original payload is still readable directly from the `errorJson`
column for manual inspection. Add a one-sentence note to the
changeset's migration text so consumers upgrading don't get blindsided
by suddenly-empty error fields on historical runs.
|
||
|
|
00a011dee4 |
Add stable Next.js eager and lazy test coverage (#1747)
* Add stable Next.js eager and lazy test coverage * Address PR review feedback * Fix eager Next step route builds * Fix eager Next manifest refreshes * Fix eager Next e2e stack assertions * Externalize native step bundle bindings * Lazy load Vercel world runtime * Fix Next dev step sourcemap assertions * Consolidate eager build changesets * Fix Vercel world tracing in Next deployments * Externalize Vercel world in Next builds * Fix webpack tracing for Vercel world deps * Fix eager workflow route bundling * Rely on Next server externals |
||
|
|
8ea1532e48 | [core] Combine flow+step bundle and process steps eagerly (#1338) | ||
|
|
1203dae70c |
Friendlier workflow errors (consolidated) (#1849)
* Introduce structured context-violation errors + Ansi renderer Phase 1: Add Ansi rendering helpers (frame, hint, note, help, code, inline) to @workflow/errors, and a chalk mock for readable snapshot tests. Phase 2: Add four context-violation error classes to @workflow/core (NotInWorkflowContextError, NotInStepContextError, NotInWorkflowOrStepContextError, UnavailableInWorkflowContextError) and apply them to all twelve user-facing throw sites so errors now include docs links and a structured "what/why/fix" frame. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review: tighten changeset, implement ansifyName, harden Ansi - Tighten phase 1 changeset to a single sentence (per pranaygp review) and switch to double-quoted frontmatter (per Copilot + repo convention). - Implement `ansifyName` to actually apply dim styling to workflow/ / step/ prefixes; add an `Ansi.dim` helper to `@workflow/errors` so callers don't need to import chalk directly. - Remove the `void getWorkflowMetadata;` workaround in context-errors.ts by dropping the unused value import (we only needed the type and symbol). - Render the plain-Error throw in `workflow/get-workflow-metadata.ts` with `Ansi.frame` + docs link so the VM path matches the structured-class styling from the sibling step path (still uses a plain Error to avoid the module-init cycle). - Guard `buildUnderline` against zero-length markers so a stray empty token can't produce a negative `String.repeat` count. * Structured runtime logger metadata + fold in replay-timeout logging Adds a `.child()` and `.forRun(runId, workflowName)` child-logger API to the structured logger so runtime/step code doesn't have to repeat `workflowRunId`/`workflowName`/`stepId` on every call. Normalizes error metadata to structured `errorName` / `errorMessage` / `errorStack` fields instead of ad-hoc `error: err.message` strings, and adds comments to silent catches that swallow expected idempotency conflicts. Also folds in the pending changes from #1812 so that PR can be closed: - Standardize the console prefix to `[workflow-sdk]`. - Split the replay-timeout log into a warn-while-retrying vs. error-when-giving-up, and surface the underlying error when we can't mark a timed-out run as failed. - Include the error stack in the "Fatal runtime error during workflow setup" log and in the top-level user-code workflow error log so the stack surfaces in flattened log drains. - Drop the `[Workflows] "<runId>" - ` prefix from `buildWorkflowSuspensionMessage` — the structured logger now attaches run context. Supersedes #1812. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use double-quoted changeset frontmatter per repo convention * Add SerializationError + apply to user-facing serialization sites Phase 4 of friendlier errors: introduce a `SerializationError` class with an optional `hint` and a docs link (workflow-sdk.dev/err/serialization-failed), and adopt it at every user-facing serialization boundary in @workflow/core: - Locked ReadableStream at a workflow boundary - Unregistered class / missing `classId` / missing `WORKFLOW_DESERIALIZE` - Attempting to return step functions to clients or call workflow functions directly - Webhook `respondWith()` called outside a step - `dehydrate*` / `getSerializeStream` failures (workflow args/return, step args/return, stream chunks) Internal invariants (format prefix length checks, unknown format bytes, missing `STREAM_NAME_SYMBOL`, encryption key/size guards, etc.) now throw `WorkflowRuntimeError` instead of plain `Error` so the classifier and logger treat them consistently. `formatSerializationError` now returns `{ message, hint }` so the hint fragment can be rendered with the standard SerializationError framing instead of being baked into the message string. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use double-quoted changeset frontmatter per repo convention * Presentation-only user vs SDK error attribution Add describeError() that derives attribution and class-aware hints from existing error classes + RUN_ERROR_CODES — no event data changes. Wire into step failures, max-delivery exhaustion, run failures, and fatal setup errors so terminal logs include errorAttribution and a hint for known error types. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review: describeError accepts precomputed errorCode + instanceof - `describeError(err, errorCode?)` now accepts an optional precomputed `RunErrorCode`. `classifyRunError(err)` only narrows to USER_ERROR / RUNTIME_ERROR, so the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches were previously unreachable from the step / run failure log sites. Callers that know the failure category (runtime.ts for replay timeout and max-deliveries exhaustion) now pass the code in. - Context-violation checks use `instanceof` against the actual classes from context-errors.ts instead of a name-string set. Type-safe + survives class renames. - Wire the new hints through to the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED log sites so those branches actually render a hint now. - 3 new tests cover the reachable code paths + precomputed-code override. - Changeset frontmatter switched to double quotes per repo convention. * Cosmetic consistency pass on remaining bare throws Internal invariants now use WorkflowRuntimeError so describeError attributes them to the SDK: missing startedAt, VM generateKey, closure-vars outside step context, ENOTSUP. defineHook().resume() formats schema validation failures as a readable list instead of a JSON blob. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use double-quoted changeset frontmatter per repo convention * Data-driven describeRunError + expose via @workflow/core/describe-error Observability renderers read persisted run_failed / step_failed event data, not live Error instances. describeRunError takes { errorCode, errorName } and returns the same { attribution, hint } shape as describeError, so the CLI and web UI can derive user-vs-SDK framing from the event log directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Friendlier build-time errors: WorkflowBuildError class + applications Add `WorkflowBuildError` class in `@workflow/errors` with optional `hint` for an actionable next step, and apply it in `@workflow/builders` at user-facing sites: failed esbuild phases, unresolved built-in steps, and empty esbuild output now throw `WorkflowBuildError` with a hint pointing at the likely fix. Runtime invariants remain plain `Error`. * Polish friendlier-errors rendering: drop functionName leak, simplify docs link, redirect stack - Drop the readonly `functionName` param-property on context-error classes so util.inspect no longer prints a trailing `{ functionName: 'foo()' }` block. - Replace the `DocLink` ("label: https://…") shape with a plain `DocsUrl` template-literal type. Error output now renders a single clean line: `docs: https://…` (new `Ansi.docs` helper) instead of the noisier "note: Read more about foo(): https://…". - Add throw helpers (`throwNotInWorkflowContext`, etc.) that call `Error.captureStackTrace(err, stackStartFn)` on V8 engines so the top frame of the thrown error points at the user's call site instead of at the gate function inside the framework. Callers pass themselves as the boundary. - Refactor `defineHook()` (both root and `/workflow`) to use named function closures rather than `this.create`/`this.resume`, since the stack redirect relies on a stable function identity that survives destructuring. - Update context-errors.test.ts to snapshot the new `docs:` framing and to add a regression test asserting the top stack frame is the user call site. * Consolidate friendlier-errors stack: fix ANSI leak + non-retry semantics Addresses PR review feedback across the 8-phase friendlier-errors stack and fixes issues surfaced by manual testing (createHook() inside a step): - ANSI no longer leaks into .message / .stack. Context-violation errors now store plain text on .message and render the colored framed form lazily via [util.inspect.custom] / toString(). Structured logs, log drains, CBOR-serialized events, and JSON payloads no longer contain raw \x1B[...m bytes. - Context violations are now fatal. ContextViolationError sets fatal = true; FatalError.is(err) recognizes any error with a fatal: true own property. Calling createHook() from a step no longer burns three retry attempts on a guaranteed-to-fail context violation. - Ansi helpers moved to @workflow/errors/ansi subpath so imports from @workflow/errors no longer pull chalk into consumers that only want error classes (addresses reviewer VaguelySerious). - Shared redirectStackToCaller helper in packages/core/src/capture-stack.ts, used by both context-errors.ts and workflow/get-workflow-metadata.ts (addresses Copilot review on #1849). - Structured framed content: ContextViolationError now takes a structured FramedContent (title segments + detail branches) and renders plain/pretty from the same source of truth. Tightens the eight existing phase changesets to 1-2 sentences each and adds four new scoped changesets (errors-ansi-subpath, context-errors-plain-message, context-errors-fatal, capture-stack-shared) for the followup fixes, so the final changelog history stays readable. * test: update step-handler mocks for scoped forRun() logger The runtime logger now uses .forRun(runId, name, {stepId, stepName}) to attach scope context, so 409-handling log calls no longer repeat {workflowRunId, stepId} in every metadata bag — those live on the scoped logger instance. Update the mock to return itself from forRun() and tighten assertions to check both the log args (errorName/errorMessage) and the forRun() scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Mark SerializationError fatal + route dehydration through step-failure path SerializationError now carries readonly fatal = true. Step-return dehydration is wrapped inside the user-code try/catch so that the resulting error flows through userCodeFailed → step_failed → FatalError.is() short-circuit instead of bubbling up as HTTP 500 and triggering a queue retry loop. Retrying a step that returned a non-POJO is guaranteed to fail the same way, so this saves ~20s and 3 near- identical error blocks per serialization failure. * Add logging snapshot tests + manual-test artifacts Snapshot tests lock in the exact shape of: - describeError() payloads (attribution, errorCode, hint) for every classification — plain Error, SerializationError, context-violation, WorkflowRuntimeError, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED. - The scoped-logger call signature for the two canonical runtime failure paths (fatal-bubble and hit-max-retries), so refactors of forRun() / child() metadata merging can't silently change what users see in their log drains. SerializationError now also has a direct test for readonly fatal=true + FatalError.is() recognition. pr-artifacts/ contains real log-output snapshots from running the nextjs-turbopack workbench against five error scenarios. These are reference material for reviewers and are flagged to be removed before merge. * Readable step-fatal logs: inline stack + friendly step/workflow names The step-level fatal-error log used to embed the full stack trace inside an `errorStack` string field in the metadata object, so util.inspect rendered it as a quote-escaped, line-continuation blob when the log hit the terminal — unreadable in practice. Move framing + stack into the log *message* (matching the workflow-level log in runtime.ts) and keep the metadata object compact with only the indexable structured fields (`errorAttribution`, `errorName`, `errorMessage`, `hint`, IDs). Log drains still get the same keys; humans now see a readable stack trace. Also introduce `formatStepName` / `formatWorkflowName` in `@workflow/utils` that render machine names (`step//./workflows/1_simple//add`) as `add (./workflows/1_simple)` in log framings, using the existing `parseStepName` / `parseWorkflowName` parsers. Applied to step-fatal, hit-max-retries, exceeded-max-retries, and workflow-threw log sites. Artifacts in pr-artifacts/ updated to show the new output shape, and renamed .log → .md since they're Markdown and IDE previews are nicer that way. * Opinionated pretty formatter for runtime structured-log metadata Replace util.inspect's default object dump (which quote-escapes multi-line stacks and paragraph hints into a single-line JSON-y blob) with a workflow-aware formatter that composes the entire log line into a single string passed to console.error / console.warn. Highlights of the new output: - Per-run / per-step IDs render with their parsed friendly names so users see `wrun_… · simple (./workflows/1_simple)` instead of just the raw `workflowName: 'workflow//./workflows/1_simple//simple'`. - Color-coded attribution badge (user error red / sdk error magenta) paired with the error class in bold. - Hints render as a paragraph under `hint:` rather than a backslash- `\n`-escaped string. - Drops redundant fields (errorStack always; errorMessage when it's already in the parent message) to avoid double-printing. - Unknown fields fall through as a sorted `key value` tail so we never silently drop log information. @workflow/errors/ansi gains bold/red/magenta helpers used by the formatter. The web / web-shared packages don't consume stderr — they read structured event payloads from the World event log — so this is presentation-only at the runtime layer. * ci(benchmarks): disable pnpm cache for getCommunityWorldsMatrix The job never runs `pnpm install` (it just calls `node` against a checked-in script), so the pnpm store path never exists. The post-job `actions/setup-node@v4` cache-save then fails with `Path Validation Error: Path(s) specified in the action for caching do(es) not exist` and red-X's the entire job even though the matrix step succeeded. The setup-workflow-dev composite already has a `cache-pnpm` opt-out input for this exact case — wire it through here. * Address PR review comments: inspect dedup, cause leak, retry-loop tests - ContextViolationError: util.inspect(err) duplicated every framed detail line because the stack-tail strip only sliced the first message line. V8's Error.stack reads `Name: messageLine1\n messageLine2\n at ...`, so for our multi-line `title\n╰▶ docs: …` messages every detail line was getting prepended twice (once in the pretty form, once via the unsliced message tail). Count the actual message lines and slice past all of them. Repro test asserts `╰▶ docs:` appears exactly once. - WorkflowError: stop assigning `cause: undefined` as an enumerable own property when no cause is provided. Subclasses (every error in this PR) inherit the parent constructor; the unconditional assignment polluted `util.inspect(err)` output with `{ cause: undefined, … }` on every no-cause instance. The `super(...)` call already conditionally sets `.cause` non-enumerably when `options.cause` is provided. - step-handler.test.ts: add a regression-gate suite that exercises the fatal-vs-retryable retry-loop wiring directly. Asserts that an error with `fatal: true` produces exactly one `step_failed` event with no `step_retrying`, and that a non-fatal `Error` retries via `step_retrying` on early attempts and emits `step_failed` once the retry budget is exhausted. Catches the silent-regression case where `fatal = true` is removed from a context-violation error class but the `FatalError.is()` unit tests stay green. * Consolidate changesets + remove pr-artifacts Address review feedback to drastically shorten the changesets — fold the 15 file-by-file entries into a single user-facing changeset for @workflow/core / errors / builders / utils. Also drop the pr-artifacts/ folder (reviewer-only log captures, no longer needed). * Polish runtime error logging: layout, stack trim, hint consolidation Five user-driven fixes from manual smoke-testing of #1849: 1. Logger layout. composeLogLine() now puts the structured-fields block (attribution badge, run/step IDs, error code) **between** the framing line and the stack body, instead of after it where 30+ lines of stack buried the most useful information. The framing stays at the top, stack at the bottom, structured info readable at a glance. 2. Stack trim. Drops framework-internal frames (`node_modules/.pnpm/`, `node:internal/`, Turbopack-bundled `node_modules__pnpm_*` chunks, `_next_dist_*` chunks) and caps the surviving frame count at 6 so the stack stays compact even on heavy async wrappers. Suppressed runs emit one summary line so users know the trim happened. 3. Wrapper-route noise. The nextjs-turbopack workbench's start route was catching `WorkflowRunFailedError` rejection on `Promise.race([readLoop(), run.returnValue])` and re-logging it via `console.error('Error in workflow stream:', error)` plus `controller.error(error)` — which then triggered Next.js's `⨯ failed to pipe response` overlay. The SDK already logs the failure cleanly upstream and the runId is on the response header, so the wrapper now closes the SSE stream cleanly on WorkflowRunFailedError. 4. Consistent framed `╰▶ hint:` / `╰▶ docs:` layout for all errors that carry a hint or docs slug. WorkflowError, SerializationError, and WorkflowBuildError now share one `appendFramedDetails` helper matching the box-drawing structure that ContextViolationError already used. Was: blank-line-separated `Learn more: <url>`. Now: one tree, indistinguishable from context-violation rendering. 5. Drop the duplicate logger-side `hint` field. Hints now live on the error message only — actionable hints get serialized into the event log, rehydrated on the workflow side, and shown in observability automatically. The previous logger-only hint duplicated stderr but never made it past the step boundary. Updated SerializationError hint to point at the foundations doc ("Ensure you're returning workflow serializable types. Check the serialization docs to see what's serializable: https://workflow-sdk.dev/docs/foundations/serialization") instead of the hardcoded `(plain objects, arrays, primitives, …)` list, which drifted out of sync as the supported types grew. Same hint reuses for step args, workflow args/return, stream messages, and any other site that goes through `formatSerializationError`. Also retitled the retry summary `3 retries` → `3 max retries` since "3 retries" next to "4 attempts" was ambiguous (already-happened vs. budget). * Trim error-card title + drop machine step name from persisted error - ErrorStackBlock (web observability): show just the first non-empty trimmed line of the error message in the card title with single-line truncation. Multi-line messages (`Failed to serialize step return value\n╰▶ hint: …`) were rendering the entire framed body in the title, pushing the copy button off-screen and burying the scannability of the headline. Full message stays in the body via the stack (V8 prepends `Name: message` to `Error.stack`), so no information is lost; hover-tooltip exposes the full title text. - Persisted error message: drop the `Step "step//./.../foo"` machine name from `Step failed after N retries: …` and `Step exceeded max retries (…)` strings. Observability already attributes the event to a specific step via the UI tree, and the CLI logger emits the friendly `Step foo (./...) hit max retries` framing on its own line. Embedding the raw `step//./...` machine name in the persisted message text was duplicate noise. * Update .changeset/friendlier-errors.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Update .changeset/pretty-log-format.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Update SerializationError snapshot tests for slug-less message The class no longer attaches a slug-based `╰▶ docs:` line — the foundations URL is embedded directly in the hint via the `formatSerializationError` helper in @workflow/core. Update the test expectations accordingly: - bare-title case is now a single line (no docs link) - hint case renders one `╰▶ hint: …` branch (no second branch) * Update serialization.test.ts hint assertions for foundations URL Four `should throw error for an unsupported type` cases were still asserting on the old hardcoded type list. Update to the new hint phrasing that points at the foundations doc, matching the change in `formatSerializationError` (`packages/core/src/serialization/errors.ts`). --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
a7071bf3d4 | Fix pnpm type issue after tanstack PR (#1907) | ||
|
|
8202663857 | [workbench] Add TanStack Start workbench and tests (#1875) | ||
|
|
072e86bbef |
Add additional tests for event consumer fixes for hook/sleep/step race conditions (#1528)
Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
e7ea0684f4 |
Add first-class serialization for built-in Error subclasses (#1511)
* Add first-class serialization for built-in Error subclasses Replace the generic Error reducer/reviver with specific handlers for TypeError, RangeError, SyntaxError, URIError, ReferenceError, EvalError, and AggregateError. Preserve the cause property on all Error types, only serializing it when present to maintain absence vs presence semantics. * Refactor Error subclass reducers/revivers with helper functions Extract repetitive reducer/reviver logic into makeErrorSubclassReducer and makeErrorSubclassReviver helpers. Reduces ~120 lines of duplicated code to ~50 while preserving identical behavior. AggregateError remains as a wrapping extension since it needs to preserve the errors array. * Bump error-subclass-serialization changeset to minor Adding new serialization support for built-in Error subclasses is a feature, not a bug fix. |
||
|
|
9ea125427f |
Decode UTF-8 stream chunks (#1852)
* Decode typed array stream chunks * Render decoded stream bytes with raw view * Render decoded bytes in data inspector * Use generic byte inspector for streams * review feedback: narrow stream-display exports, fix tab a11y, add collapseRefs tests - Remove unused formatStreamChunkForDisplay/sanitizeStreamChunkForDisplay exports; keep only the formatArrayBufferViewForDisplay path actually used by DataInspector. - Replace broken role=tablist/role=tab on the Decoded/Bytes switcher with aria-pressed toggle-button semantics. - Export collapseRefs/isBytesDisplay and add regression tests covering typed-array detection (top-level, nested in object/array/Map/Set, DataView exclusion). * Replace eval with JSON.parse in serialization revive helper (#1848) * Replace eval with JSON.parse in serialization revive helper devalue.stringify() always produces valid JSON — special values (undefined, NaN, Infinity, -0) are encoded as negative integer sentinels. JSON.parse yields the same flattened array form that unflatten() expects, without the eval anti-pattern (VULN-918). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Drop redundant workflow package from changeset Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Add e2e test for UTF-8 parseable stream chunks Emits Uint8Array chunks containing multi-byte UTF-8 (Latin Extended, CJK, emoji, RTL Arabic) plus a UTF-8 encoded JSON document, and asserts each chunk round-trips through TextDecoder({ fatal: true }). Exercises the same decode path the web inspector relies on for typed-array stream values. Made-with: Cursor --------- Co-authored-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
56ba32feb8 | Bump vite (#1827) | ||
|
|
b163860fe4 |
Guard fibonacciWorkflow against non-finite n (#1814)
Throw a FatalError at the top of fibonacciWorkflow if n is not a finite number. Prevents a runaway recursion if the workflow is ever invoked without its numeric argument — NaN - 1 === NaN, NaN <= 1 === false, so the base case would never fire and every descendant would spawn two more children. |
||
|
|
b27921985c |
Add distributed abort controller guide and implementation (#1811)
* feat: add distributed abort controller guide and implementation Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * feat: implement abort signal step function and refactor workflows to use it Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * feat: enhance distributed abort controller with user-provided ID Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * feat: enhance distributed abort controller with TTL and reconnection logic Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * feat: add grace period to abort controller workflow Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * feat: add distributed abort controller workflow and tests Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> * add the pattern to the sidebar * fix: correct start() call signature and run.id → run.runId start() takes args as a positional second argument, not an options object property. The Run object exposes runId, not id. Made-with: Cursor * fix: address PR review comments on distributed abort controller - Make abort() idempotent by catching hook-not-found/expired errors - Add error handling to signal getter's stream reader IIFE - Fix tests: use instance methods instead of non-existent static methods, pass required ttlMs/graceMs args, include expired field in assertions Made-with: Cursor * fix: add missing import to Custom TTL docs code sample The docs typecheck runs each code block in isolation. The Custom TTL example was missing the DistributedAbortController import. Made-with: Cursor * fix: only sleep grace period on TTL expiration, not manual abort Manual aborts now complete immediately instead of sleeping through the full TTL + grace period. This fixes vitest timeouts where all 3 distributed-abort-controller tests exceeded the 30s limit. Made-with: Cursor * fix: wait for hook registration before aborting in test The DistributedAbortController instance test was timing out because abort() was called before the workflow had registered the hook. The idempotent catch silently swallowed the "not found" error, leaving the workflow running forever. - Make runId readonly (was private) so tests can access it - Add waitForHook(getRun(controller.runId)) before controller.abort() Made-with: Cursor * fix: apply conditional grace period to E2E workflow copy The distributedAbortControllerWorkflow in 99_e2e.ts had the same unconditional grace sleep bug, causing the reconnect test to timeout at 60s while waiting ~66s for the grace period after manual abort. Made-with: Cursor --------- Co-authored-by: v0 <v0[bot]@users.noreply.github.com> Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com> Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com> |
||
|
|
e295bae417 |
feat: allow start() to be called directly inside workflow functions (#1491)
* feat: allow start() to be called directly inside workflow functions
Add 'use step' to start() so it can be called directly from workflow
code. The SWC compiler strips the function body in workflow mode and
replaces it with a step proxy. When called from a workflow:
1. The workflow function reference is serialized via WorkflowFunction
reducer (serializes { workflowId })
2. start() executes in the step context with full Node.js access
3. The returned Run is serialized via WORKFLOW_SERIALIZE and deserialized
back in the workflow VM
4. Run getters (.status, .returnValue, etc.) are 'use step' getters
that each execute as separate steps
Also re-exports start from @workflow/core/runtime/start in api-workflow.ts
instead of using a throwing stub, adds e2e tests for startFromWorkflow
(with hook communication) and fibonacciWorkflow (recursive composition).
* fix(next): don't copy package step files in deferred builder to avoid duplicate classes
Files belonging to packages (detected by walking up to find a
package.json with a name field) are imported via relative path
instead of being copied to __workflow_step_files__/. Copying creates
a second module instance which breaks JS native private field (#)
brand checks when the runtime creates instances from one copy and
the step handler accesses fields from the other.
* fix(next): only skip copying package files that are serde classes, not all package step files
Regular package step files (like fetch) must still be copied to ensure
the SWC loader registers them. Only serde class files from packages are
excluded from copying since those define classes with JS native private
fields (#) that break when duplicated.
* fix(next): generate thin wrappers for package serde step files instead of full copies
For package files that define serde classes (like Run), generate a thin
wrapper that imports the original class and registers steps/classes from
the manifest. This avoids duplicating the class definition (which breaks
JS native private field brand checks) while still registering all step
functions and the class in the serialization registry.
Regular package step files (like fetch) are still copied as before.
* fix(next): use forceStepModeFiles to transform package serde files in step mode
Instead of copying package serde+step files (which creates duplicate
classes with #private brand check issues) or generating fragile wrappers,
add the original file paths to a shared forceStepModeFiles set. The
loader checks this set and transforms those files in step mode directly,
so the SWC plugin generates proper step registrations on the original
class — no duplication, no reimplemented registration logic.
* fix(next): use step mode for all files with step/serde patterns, not just copies
The loader now selects step mode for any file that has 'use step'
directives or serde patterns, regardless of whether it's a deferred
step copy. Step mode is a superset of client mode — the only addition
is step registry IIFEs, which are harmless for non-step consumers.
This means package serde+step files (like Run) no longer need to be
copied to get step registrations. They're imported directly in the
step route and the loader transforms the original file in step mode.
One class instance, no duplication, no wrapper generation.
---------
Co-authored-by: Nathan Rajlich <n@n8.io>
|
||
|
|
417c4930be |
refactor(swc-plugin): remove client transform mode, merge into step mode (#1686)
* refactor(swc-plugin): remove client transform mode, merge into step mode Remove the `client` transform mode from the SWC compiler plugin. The `client` and `step` modes were nearly identical — both preserved step function bodies, replaced workflow bodies with throw stubs, and emitted the same JSON manifest. Step mode now absorbs all client-mode behaviors: - Dead code elimination (previously only workflow + client) - Hoisted variable references for object property steps - All integrations use mode: 'step' instead of 'client' BREAKING CHANGE: The `client` value for the SWC plugin `mode` option is no longer accepted. Use `step` instead. * fix(nitro): force-inline workflow packages in dev mode for serde classId registration In dev mode, Nitro's Rollup externalizes npm packages like @workflow/core, so the SWC transform plugin never processes files like run.js. This means serde classes (e.g. Run) never get the classId registration IIFE, causing serialization failures when step functions return Run instances. Uses a Rollup resolveId hook to force workflow SDK packages to be bundled (non-external) while leaving all other dependencies external. This is more targeted than noExternals=true which bundles everything and causes TDZ errors from circular imports in packages like vue-bundle-renderer/h3. The Nitro module now also ignores .nitro/workflow/** in watchOptions so writing generated workflow bundles does not retrigger Nitro's own dev bundle rebuild loop. Also wraps dev:reload workflow rebuilds and makes LocalBuilder.build() atomic (writes to temp files, renames on success) to avoid partial output state during HMR. For Nuxt, also configures Vite's ssr.noExternal to bundle workflow packages in the SSR context. * fix(nitro,nuxt): address review feedback on dev-mode classId fix - nitro builders: use crypto.randomUUID() for temp file suffix instead of Date.now() to avoid collisions under rapid/concurrent build() calls, and serialize concurrent build() calls through an internal queue so two overlapping dev rebuilds cannot clobber each other's temp outputs. - nitro index: use fileURLToPath() to convert file:// URLs to filesystem paths, which correctly handles Windows paths (file:///C:/... -> C:\...) and percent-decoding, instead of relying on new URL(...).pathname. - nuxt module: normalize vite.ssr.noExternal to an array (preserving any existing string/RegExp/array entry) before appending workflow package matchers, so the force-bundle behavior is not a no-op when noExternal is already set to a non-array value. |
||
|
|
173756dc4d |
[docs] Rename workflowdevkit to workflowsdk and useworkflow.dev to workflow-sdk.dev (#1759)
* [docs] Rename workflowdevkit references to workflowsdk * [docs] Rename useworkflow.dev to workflow-sdk.dev * [chore] Add changeset for domain rename * [docs] Revert sitemap rewrite to useworkflow.dev (crawled-sitemap not yet available for new domain) |
||
|
|
1fa6397241 |
Move swc-playground-wasm into workbench/swc-playground (#1704)
* move swc-playground-wasm into workbench/swc-playground Move the Rust/WASM source from packages/swc-playground-wasm into workbench/swc-playground/wasm/ so it is no longer built as part of the packages/* turbo filter in CI. The WASM build now runs as part of the playground's own prebuild/dev scripts, avoiding Rust toolchain failures in unrelated CI jobs. * fix: ensure default rustup toolchain in wasm build The Vercel build environment has rustup installed but no default toolchain configured. Add a check for this and install stable if needed. Also check for VERCEL env var in addition to CI. * fix: ensure cargo bin dir is in PATH on Vercel After cargo install wasm-pack, the binary lives in $CARGO_HOME/bin which may not be in PATH on the Vercel build environment. Always add the cargo bin directory to PATH after ensuring the toolchain. |
||
|
|
851a530164 |
Add cookbook with 50 workflow pattern recipes (#1564)
* Add cookbook section with 50 workflow pattern recipes
Migrate the "Workflow API Explorer" decision tree concept from
workflow-campaign-demos into useworkflow.dev docs as a Cookbook.
Infrastructure:
- docs/lib/cookbook-tree.ts: decision tree data, 50 recipe metadata entries, slug-to-category mapping
- docs/components/geistdocs/cookbook-explorer.tsx: interactive "I want to..." decision tree UI with breadcrumb navigation
- docs/content/docs/cookbook/index.mdx: landing page rendering CookbookExplorer component
- docs/content/docs/cookbook/meta.json + 8 category meta.json files for sidebar nav
- docs/content/docs/meta.json: added cookbook to docs nav between foundations and how-it-works
- docs/app/[lang]/docs/[[...slug]]/page.tsx: registered CookbookExplorer component
50 recipe MDX files across 8 categories (payments, approvals, resilience,
notifications, webhooks, data-processing, routing, observability), each with:
- Frontmatter (title, description, type: guide, summary with use-case scenario)
- Simplified code snippet (core pattern only, stripped of demo UI concerns)
- Full implementation code snippet (exact source from campaign demos)
- Key APIs section with links to API reference docs
* ploop: iteration 1 checkpoint
Automated checkpoint commit.
Ploop-Iter: 1
* fix: polish cookbook public routing
Keep the cookbook surface canonical at /cookbooks so docs navigation, sitemap output, and AI/chat entry points stop leaking the legacy /docs/cookbook paths.
Correct the approval-chain example so the docs teach the intended sequential approval semantics instead of implying the workflow approves after the first successful level. This keeps the cookbook aligned with the docs quality bar and avoids misleading readers with inconsistent behavior.
Ploop-Iter: 2
* docs: canonicalize cookbook doc routes
Align cookbook-facing docs outputs with the new public route so
redirects, sitemap entries, and LLM-facing exports stay consistent.
This keeps the polished cookbook section discoverable at its canonical
location while trimming the last demo-heavy recipe examples toward the
same concise style as the rest of the docs.
Ploop-Iter: 3
* ploop: iteration 4 checkpoint
Automated checkpoint commit.
Ploop-Iter: 4
* fix(docs): finalize cookbook route split
Keep cookbook content discoverable after moving it to a first-class /cookbooks surface so navigation, canonical metadata, and markdown consumers resolve the new public URLs consistently.
Avoid serving the legacy /docs/cookbook tree as if it were still part of the docs section, which reduces duplicate navigation paths and prevents stale static output from competing with the new route structure.
Ploop-Iter: 5
* docs: improve cookbook discovery
The cookbook landing page needs to work for both exploratory users and users who already know the pattern they want. This keeps the guided decision tree while adding shared category metadata and a searchable browse mode so recipe discovery feels faster and more consistent with the rest of the docs experience.
Ploop-Iter: 6
* docs: refine cookbook pattern examples
Tighten the simplified cookbook recipes so the examples teach the intended workflow semantics clearly and consistently. The changes keep the documentation focused on the core control-flow patterns reviewers called out, while removing ambiguity around partial arrivals, deadlines, and first-success behavior.
Ploop-Iter: 7
* docs: decouple cookbook sidebar tree
Separate cookbook navigation from the docs page tree so the standalone /cookbooks experience stays stable after the route move and the main docs sidebar no longer leaks cookbook entries.\n\nThis keeps cookbook navigation driven by explicit recipe metadata, which avoids duplicated section titles and makes the docs and cookbook surfaces easier to evolve independently.\n\nPloop-Iter: 8
* fix(docs): align cookbook public nav
Keep cookbook pages on their public /cookbooks surface so metadata and copied markdown do not leak legacy /docs/cookbook paths.\n\nSimplify sidebar rendering to trust the injected page tree, which avoids route-specific filtering and keeps cookbook navigation consistent with the active layout tree.\n\nPloop-Iter: 9
* refactor(docs): decouple cookbook routing
Move cookbook rendering off the shared docs route so cookbook pages can behave like a first-class docs surface without leaking cookbook-specific UI into the main docs experience.
Centralizing cookbook tree filtering keeps sidebar behavior consistent in one place and avoids duplicate cookbook navigation state across layouts.
Ploop-Iter: 10
* docs: improve cookbook explorer accessibility
Improve the cookbooks entrypoint so loading and keyboard navigation
are usable without visual cues, and keep guided and browse modes
resilient while the route hydrates.
Ploop-Iter: 11
* ploop: iteration 12 checkpoint
Automated checkpoint commit.
Ploop-Iter: 12
* docs: rename cookbooks route to cookbook (singular)
* docs: add 5 cookbook overview design variations
Add getRecipeHref, getRecipesByCategory, and collectSlugs helpers to
cookbook-tree.ts, then create 5 distinct overview page variations at
/cookbook/v1 through /cookbook/v5 for side-by-side comparison:
- v1: Category Grid (zero-JS scannable overview)
- v2: Search-First (real-time filtering with category pills)
- v3: Accordion Catalog (expandable category sections)
- v4: Decision Wizard (step-by-step guided questions)
- v5: Problem-Solution Table (master-detail by scenario)
Also adds .impeccable.md with project design context.
* docs: remove unrelated package.json bumps and generated artifacts
* docs: restructure cookbook from 50 recipes into 27 consolidated pages
Restructure the cookbook based on team meeting feedback, toolbar comments,
mux-ai pattern analysis, and Vercel org code search. Consolidates duplicates,
adds missing patterns, ensures all examples have proper directives and
type-check against the real workflow SDK.
New structure:
- common-patterns/ (9): saga, batching, rate-limiting, fan-out,
scheduling, idempotency, webhooks, content-router, child-workflows
- agent-patterns/ (5): durable-agent, tool-streaming,
human-in-the-loop, tool-orchestration, stop-workflow
- integrations/ (3): ai-sdk, sandbox, chat-sdk
- advanced/ (6): serializable-steps, durable-objects,
isomorphic-packages, secure-credentials, custom-serialization,
publishing-libraries
All 92 code snippets pass docs-typecheck against real workflow SDK types.
Deleted 8 old category folders. Updated cookbook-tree.ts, explorer, nav.
* docs: add workflow migration guides
Help teams evaluating the GA launch translate existing durable workflow systems into Vercel Workflow without reverse-engineering concept parity from product docs alone.
The guides focus on real migration decisions and concrete TypeScript examples so adoption can be driven by implementation clarity rather than platform marketing.
Ploop-Iter: 1
* ploop: iteration 2 checkpoint
Automated checkpoint commit.
Ploop-Iter: 2
* docs: refine workflow migration guides
Clarify the migration narrative for GA so teams evaluating a move from Temporal, Inngest, or Step Functions get examples that are realistic enough to trust and pricing guidance that matches the current platform model.
The Inngest guide needed a complete order-saga flow so the durable orchestration, compensation, and streaming patterns read as a credible migration target instead of a partial sketch. The pricing language across all three guides also needed to be aligned with current Workflow and Vercel compute billing semantics to avoid creating the wrong cost expectations during evaluation.
Ploop-Iter: 3
* ploop: iteration 1 checkpoint
Automated checkpoint commit.
Ploop-Iter: 1
* docs(skill): refine workflow migration guidance
Clarify migration rules so agents choose the correct resume primitive, keep streaming guidance aligned with runtime behavior, and avoid implying Vercel-managed execution for self-hosted targets.
This reduces avoidable migration mistakes in generated guidance and keeps the skill consistent with the acceptance criteria used to evaluate it.
Ploop-Iter: 2
* ploop: iteration 3 checkpoint
Automated checkpoint commit.
Ploop-Iter: 3
* docs(skills): refine workflow migration guidance
Reduce the migration skill entry point to the decision surface agents need\nso they can select the correct resume pattern without carrying duplicate\nexamples in the initial context.\n\nClarify framework precedence so prompts that explicitly ask for\nframework-agnostic boundaries do not get Hono- or Next-specific route\nshapes, while preserving framework-specific examples when requested.\n\nCentralize canonical resume examples in the shared patterns reference to\nkeep the guidance consistent across migration paths and reduce drift.\n\nPloop-Iter: 4
* docs: add workflow deep-dive references
Add a cohesive set of deep-dive reference articles so the GA launch
has architecture-level documentation grounded in the current SDK
implementation. This gives readers verified explanations of runtime,
replay, streaming, compiler, and cost-model behavior while linking the
series together for easier navigation.
Ploop-Iter: 1
* ploop: iteration 5 checkpoint
Automated checkpoint commit.
Ploop-Iter: 5
* docs: normalize deep-dive related links
Keep the new deep-dive reference pages cross-linked so readers can move between adjacent runtime concepts without depending on older how-it-works pages alone.
This preserves navigational consistency across the GA launch docs set and reduces the chance that architectural explanations drift into isolated pages that are harder to discover and maintain.
Ploop-Iter: 2
* docs(skill): refine workflow migration routing
Tighten the migration guidance so agents choose the correct resume surface and runtime boundary earlier, reducing incorrect mixed patterns in generated migrations.
Add explicit fast paths for self-hosted targets and Step Functions task-token callbacks so the skill stays consistent on callback URL vs deterministic resume decisions.
Ploop-Iter: 6
* docs: refine workflow migration skill guidance
Clarify route selection so the migration skill composes resume, runtime, and app-boundary concerns deterministically. Add a canonical Step Functions self-hosted Hono callback recipe so migrations produce the correct callback-url pattern without mixing incompatible hook surfaces.\n\nPloop-Iter: 7
* docs: checkpoint deep-dive drafts
Preserve the verified GA launch deep-dive drafts in git so the campaign work can continue from a stable checkpoint.
Capture the reviewed documentation progress now to reduce risk of drift between source-backed research and the publishable drafts.
Ploop-Iter: 3
* docs(skills): tighten workflow migration routing
Clarify the migration skill's route-selection rules so generated guidance stays consistent across resume surfaces, runtime targets, and named framework boundaries.
This reduces ambiguous outputs where agents might mix framework syntaxes or invent callback routes for webhook-based flows, which leads to migration guidance that does not match the user's runtime model.
Ploop-Iter: 8
* docs: finalize workflow deep dives
Clarify the runtime mechanics behind the GA deep-dive series so launch content stays aligned with the implementation and existing docs. Tightening these explanations reduces the risk of readers internalizing inaccurate mental models about replay, compilation, and workflow execution.\n\nPloop-Iter: 4
* docs(skills): refine workflow migration routing
Clarify route selection so migrations choose the correct resume surface and app boundary patterns for the target runtime and framework. Strengthen verification guidance to reject invented callback routes in URL-based flows and keep examples aligned with the documented migration rules.
Ploop-Iter: 9
* docs: sync compiler deep-dive runtime details
Align the compiler deep-dive trio with the actual Workflow runtime so launch materials describe the same execution model users rely on. This keeps the GA narrative accurate around deterministic replay, step queue triggers, and runtime bundle responsibilities, reducing the risk of docs teaching an architecture the SDK does not implement.
Ploop-Iter: 5
* docs(skill): tighten workflow migration guidance
Reduce hot-path skill context so migration routing stays easier to select and
verify during activation.
Trimmed examples and converted long invalid samples into concise failure rules
so the skill points agents to on-demand references instead of loading bulky
worked code by default.
Ploop-Iter: 10
* docs: tighten workflow migration guidance
Clarify route-key planning and resume-surface defaults so migration outputs
stay deterministic when prompts underspecify callback behavior.
Strengthen the deep-dive docs to trace runtime handoffs more directly,
which reduces ambiguity about how the compiler split maps to durable
execution behavior.
Ploop-Iter: 11
* docs: refine streaming and cost deep dives
Clarify the operational model behind durable streaming and zero-cost suspension so launch materials stay source-accurate for readers comparing workflow runtimes.
The updates make the workflow-step boundary, persistence path, and queue-driven cost story more explicit, reducing ambiguity around where stream I/O is allowed and why long waits do not consume compute.
Ploop-Iter: 6
* ploop: iteration 12 checkpoint
Automated checkpoint commit.
Ploop-Iter: 12
* docs: tighten deep-dive streaming accuracy
Align the launch deep dives with the current runtime so campaign content does not misstate suspension behavior or streaming backend capabilities.
These edits clarify the distinct resume paths for step suspension versus timed waits and document the backend-specific streaming guarantees now available across local, Vercel, and Postgres worlds, reducing the risk that readers build incorrect mental models from launch materials.
Ploop-Iter: 7
* docs(skills): refine callback resume taxonomy
Clarify when migrations should use deterministic internal resume versus generated callback URLs so skill outputs stay consistent across frameworks and hosting targets. Distinguish default webhook responses from manual-response flows to prevent ambiguous guidance and keep the shared callback references directly inspectable.\n\nPloop-Iter: 13
* docs: refine deep-dive runtime wording
Clarify the runtime semantics behind suspension and durable streaming so the GA launch materials stay aligned with the source of truth.
These edits tighten descriptions around wake-up paths, backend behavior, and stream lifecycle details to reduce ambiguity for readers comparing the docs to the implementation.
Ploop-Iter: 8
* docs: clarify webhook response mode defaults
Clarify when migrations should use the default webhook behavior versus
manual responses so agents make the same callback choice across the
skill entrypoint, shared patterns, and API reference.
This reduces avoidable ambiguity for callback-url prompts and makes the
default 202 behavior explicit unless a prompt requires custom response
semantics.
Ploop-Iter: 14
* docs: align cost model wake-up semantics
Prevent the GA launch materials from teaching an incorrect mental model about how suspended runs wake back up. The updated wording keeps the blog, social, and reference variants anchored to the real runtime paths so readers understand which transitions are queue-delayed, which are step-driven re-enqueues, and why that distinction matters for the cost story.
Ploop-Iter: 9
* ploop: iteration 10 checkpoint
Automated checkpoint commit.
Ploop-Iter: 10
* docs: clarify webhook resume choices
Explain the resume-surface decision points so migration and API guidance steer authors toward the correct webhook or hook pattern for the prompt.
Reduce common callback-routing mistakes early in the docs and skill so agents make fewer wrong assumptions during workflow migrations.
Ploop-Iter: 15
* docs: tighten cost model deep dives
Clarify the cost-model narrative so launch materials make source-verifiable
claims about suspension, wake-up paths, and polling behavior.
This keeps the GA messaging aligned with the runtime's actual control
flow and avoids overclaiming where the implementation has narrower
semantics than the original copy suggested.
Ploop-Iter: 11
* docs(skills): tighten resume routing guidance
Keep the migration skill entrypoint small so agents load the routing contract only when the source actually pauses for external resume. Clarify the public webhook docs around the default callback flow to reduce accidental use of lower-level runtime APIs.\n\nPloop-Iter: 16
* docs: tighten deep-dive runtime claims
Align the launch materials with the current runtime semantics so the
cost-model and execution-model narrative stays defensible against the
actual implementation.
This keeps the GA campaign focused on claims we can support directly from
source, especially around suspension, re-enqueue behavior, and the
difference between orchestration compute and client-side polling helpers.
Ploop-Iter: 12
* docs: correct cost model deep dive claims
Align the cost-model launch content with the runtime's actual suspension and re-entry mechanics so GA messaging does not overstate identical-cost waits or imply residency that the queue-based engine does not have.
This keeps the public explanation consistent with source-backed behavior around timed wake-ups, explicit workflow re-queue after step completion, and the distinction between idle worker residency and boundary I/O.
Ploop-Iter: 13
* chore: remove non-cookbook files from cookbook branch
Remove deep-dive articles, migration guides/skill, vercel-toolbar skill,
workflow-skills test fixtures, and misc artifacts that belong in separate
branches (deep-dives, migration-guides). Revert create-webhook.mdx,
getting-started/meta.json, and code-transform.mdx to main versions.
* docs: address toolbar feedback on cookbook pages
Address Vercel toolbar comments from Nathan Rajlich, Peter Wielander, Pranay Prakash, and Karthik Kalyanaraman on the cookbook branch.
In saga.mdx, remove the unnecessary `if (!(error instanceof FatalError)) throw error;` guard in the catch block — compensations should always unwind regardless of error type. Replace the `while/pop()!()` loop with a cleaner `for...of reverse()` to avoid the non-null assertion.
In ai-sdk.mdx, split the "Using Different Providers" section into two subsections: "Vercel Gateway (string model IDs)" clarifying that all string model IDs route through Gateway, and "Direct Provider Access" showing how to import from provider packages like `@workflow/ai/openai` to bypass Gateway. Change the "Tool Functions as Steps" section to "Tool Functions with Steps" and reword to explain that tool execute functions can optionally include steps via "use step" but don't have to — when they aren't steps, they run in workflow context and can modify workflow state directly.
In sandbox.mdx, rewrite the page to reflect that `@vercel/sandbox` now has first-class Workflow SDK support. Replace all `declare function` stubs and `// TODO` placeholders with real `import { Sandbox } from "@vercel/sandbox"`. Remove the four separate "use step" wrapper functions (provisionSandbox, runCommand, teardownSandbox, saveSandboxSnapshot) and show direct `Sandbox.create()`, `sandbox.runCommand()`, and `sandbox.destroy()` calls in the workflow function since these implicitly run as steps. Simplify the agent tool example to use inline execute functions that call Sandbox methods directly with an `activeSandbox` variable for workflow state.
Across all cookbook files, replace "Workflow DevKit" with "Workflow SDK" (8 instances in 5 files: publishing-libraries.mdx, secure-credentials.mdx, ai-sdk.mdx, chat-sdk.mdx, sandbox.mdx).
* docs: simplify cookbook index to plain MDX listing
Replace the interactive CookbookExplorer (726-line decision tree + browse
component) with a simple MDX page that lists recipes grouped by category
with linked titles and descriptions. Remove v1-v5 design variations and
trim cookbook-tree.ts to sidebar-only metadata.
* fix type checks
---------
Co-authored-by: Karthik Kalyanaraman <karthik.kalyanaraman@vercel.com>
|
||
|
|
71d39d2f8d |
fix: serialize Run across runtime and workflow VM (#1616)
* feat: serialize Run via custom class serialization with "use step" Single Run class implementation: - Add WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE to core Run class - Mark all runtime-dependent methods/getters with "use step" so SWC strips their bodies from the workflow bundle - Remove duplicate Run stub from api-workflow.ts; re-export core Run - SWC auto-registers the class (no manual registerSerializationClass) - Add e2e test for Run serialization across workflow/step boundaries - Add unit tests for serde roundtrip * chore: bump changeset to minor (new feature) * fix: include resilientStart in Run serde payload and add readable getter comment * . * fix: remove eager getWorld() from constructor, rely on lazy getter |
||
|
|
873b4e2bb4 | [core] Refactor getWorld interface to be asynchronous (#942) | ||
|
|
4d31619eb7 |
fix(ai): preserve provider tool identity across step boundaries (#1663)
* fix(ai): preserve provider tool identity across step boundaries
Port of vercel/ai#14229. Provider tools (e.g. anthropic.tools.webSearch)
were converted to plain function tools in toolsToModelTools, stripping
type, id, and args fields. This caused providers like Anthropic Gateway
to not recognize them as provider-executed tools.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* DCO Remediation Commit for Gregor Martynus <39992+gr2m@users.noreply.github.com>
I, Gregor Martynus <39992+gr2m@users.noreply.github.com>, hereby add my Signed-off-by to this commit:
|
||
|
|
ec517fa225 |
Add features.encryption to WorkflowMetadata (#1652)
* Add features.encryption to WorkflowMetadata Expose a `features` object on `getWorkflowMetadata()` so library authors can detect at runtime whether encryption is enabled for the current workflow run, allowing them to conditionally handle sensitive data serialization. * Improve docs example: show workflow-level encryption check Step inputs are serialized before the step body runs, so the encryption check should happen in the workflow function before passing data to steps. * Fix docs example: show encryption check on step return value Step return values are serialized to the event log after the step body runs, so this is where the check is actually useful - controlling what data gets persisted as output. * Fix docs typecheck: declare external function in code sample * minor |
||
|
|
bab8cddf98 |
Support getter functions with "use step" directive (#1630)
* Support getter functions with "use step" directive Add SWC compiler plugin support for JavaScript getters marked with "use step", enabling patterns like `await obj.prop` where the getter triggers a step function invocation. - Handle Prop::Getter in object literals and MethodKind::Getter in classes - Emit Object.getOwnPropertyDescriptor registration in step mode - Emit hoisted proxy + Object.defineProperty in workflow mode - Emit error for getters with "use workflow" - Fix @vercel/workflow -> @workflow/serde imports in existing fixtures - Update spec.md with getter transformation documentation * Add changeset for getter step support * Add e2e test for getter step functions * Add static getter support, sanitize hoisted var identifiers Address PR review feedback: - Support static getters with "use step" using ClassName (not .prototype) - Add sanitize_ident_part() to produce valid JS identifiers from getter names that may contain special characters (e.g. string literal keys) - Add static-getter-step test fixture - Update spec.md with static getter transformation documentation * Remove duplicate getter workflow error in visit_mut_prop_or_spread |
||
|
|
f5d2aef58f |
Add serde compliance tooling and improve custom class serialization DX (#1552)
* Add serde compliance tooling and update skill documentation - Add serde compliance checker library to @workflow/builders - Add build-time warnings for serde classes with Node.js imports in workflow bundle - Add 'workflow transform' CLI command for inspecting SWC output - Implement 'workflow validate' CLI command with serde compliance checks - Add serde analysis panel to SWC playground - Update workflow skill with custom class serialization documentation * Fix --json + --strict: use process.exitCode so JSON output is returned before exit * Address code review feedback - Fix --strict exit code: honor process.exitCode in BaseCommand.finally() - Remove unused --module-specifier flag from transform command - Add missing Node.js builtins (e.g. test) to playground detection list - De-dupe build-time serde warnings by grouping identical issues across classes - Make Serde Analysis panel collapsible like the output panels * Use .gitignore for validate file discovery; fix changeset wording |