mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
workflow@5.0.0-beta.6
1107 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
245d999031 |
Version Packages (beta) (#1997)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>workflow@5.0.0-beta.6 |
||
|
|
ccb3bb487b |
Ignore the "tarballs" app in changesets config (#2024)
No need to version this app, it doesn't get published to npm. |
||
|
|
4cde3b962b |
fix bad socket file location (#2021)
Co-authored-by: JJ Kasper <jj@jjsweb.site> |
||
|
|
9d2a9261fd |
Expose conflicting run id on hook conflicts (#2012)
* Expose conflicting run id on hook conflicts * Mark hook conflict run id as future required * Address hook conflict docs review * Address hook conflict review comments * Fix hook conflict docs typecheck |
||
|
|
fc6a265997 |
Add workflow versioning docs (#2010)
* Add workflow versioning docs * Link cookbook patterns to versioning docs * Align v5 start docs with native workflow support * Address versioning docs review feedback * Address versioning preview comment * Address versioning toolbar feedback * Cross-link versioning docs * Address latest versioning toolbar feedback * Rename versioning self-upgrade section * Address versioning PR review comments |
||
|
|
b9b121e636 | Hide flaky worlds indicators (#2000) | ||
|
|
23943f11c3 |
fix(web-shared): type trace viewer duration utility (#2006)
Use Tailwind's typed arbitrary duration syntax for trace viewer zoom controls so Tailwind v3 consumers do not emit ambiguous utility warnings when scanning the package. |
||
|
|
c43e721efc |
Retry failed VQS handlers immediately (#1999)
* Retry failed VQS handlers immediately * Address VQS retry review feedback * Tune VQS handler retry backoff * Add jitter to VQS handler retry backoff |
||
|
|
4708a77a35 |
CI: drop setup-command input from reusable community-world workflows (#1828)
* drop setup-command input from reusable community-world workflows The community-world matrix is produced by running scripts/create-community-worlds-matrix.mjs in the fork PR's checkout, so any field on it is attacker-controlled. Forwarding matrix.world.setup-command into the reusable workflow and eval-ing it let a malicious fork PR execute arbitrary shell on the runner. Replace the pass-through with a hardcoded per-world-id case in the reusable workflows (only turso currently needs a setup step) and drop the setup field from the matrix generator. * rename step to "Per-world setup" Addresses Copilot review feedback: the step no longer executes an arbitrary command, so the old name was misleading. |
||
|
|
e213447a64 |
Add Fantastic Four community worlds (#1964)
Register the Fantastic Four community world packages in the worlds manifest and show the Redis variants in the Embedded docs section. Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Pranay Prakash <pranay.gp@gmail.com> |
||
|
|
4753abb970 | Forward-port SvelteKit queue trigger fix (#1995) | ||
|
|
c145bf56d9 |
[codex] Fix detached ArrayBuffer proxy DX (#1985)
* fix(world-local): explain detached ArrayBuffer proxy failures * fix(docs): make proxy handler anchor navigable * fix(docs): open accordions for hash links |
||
|
|
22b5a1240f | Update to latest queue client version (#1987) | ||
|
|
d2121a54ed |
Validate homepage links in docs link lint (#1989)
* Validate app links in docs link lint * Validate app links in docs link lint |
||
|
|
ef872b79e4 | docs(python): Add short URL forwarding for python (#1991) | ||
|
|
af45f79421 |
docs(ai): update durable agents guide to use ToolLoopAgent (#1975)
The AI SDK renamed `Experimental_Agent` to `ToolLoopAgent`. Update the "Building Durable AI Agents" page's API route snippet (v4 and v5) so it matches the current AI SDK API. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
bebc964846 | Fix docs homepage examples link (#1988) | ||
|
|
2a010755f5 |
Remove pull_request_target trigger from backport workflow (#1972)
Drops the label-based backport override in favor of workflow_dispatch. The pull_request_target trigger has security concerns (it runs with write permissions on PR-controlled events), and we already have a manual dispatch path that covers the same use case. |
||
|
|
b885f1f2d1 |
Version Packages (beta) (#1888)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>workflow@5.0.0-beta.5 |
||
|
|
96e92c6370 |
Fix Nitro dev loading of local step dependencies (#1965)
* Fix local step dependency bundling * Limit local dependency bundling to Nitro dev |
||
|
|
09a0c1d6d1 |
Remove instrumentation from workbench (#1959)
* Remove instrumentation from workbench * bump |
||
|
|
72911f7356 |
[core] [nextjs] Fix world.ts being tree-shaken out of the bundle and unavailable at runtime (#1951)
|
||
|
|
f20c70672e |
Show hook name on trace viewer + no toast on decrypt (#1955)
* remove toast on succesful decrypt * show hook name * fix hook on detail view |
||
|
|
b940748743 |
Fix old trace viewer layout (#1952)
* Update workflow-trace-view.tsx * Update trace viewer layout to be in a row Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
270e3f1f62 |
Pipe opencode prompts via stdin instead of argv (#1950)
Large backport prompts (commit message + diff capped at 200KB) can exceed Linux's `ARG_MAX` and cause `opencode run` to fail with "Argument list too long" (exit 126). Redirect the prompt files into `opencode run`'s stdin instead of passing them on the command line. |
||
|
|
ea16d04599 |
docs: split v4/v5 content trees and fix version switcher end-to-end (#1948)
* docs: split v4/v5 content, fix version switcher end-to-end ## Content restructuring - Split `docs/content/docs/` into `docs/content/docs/v4/` and `docs/content/docs/v5/` so each version is a fully independent content tree with no shared-file coupling - v4 excludes the four pages that are v5-only (AbortController cancellation docs and the serializable-abort-controller internal page) - v5 retains all pages; `preRelease` frontmatter field removed (no longer needed now that each version is its own folder) - Removed `AbortController` / `AbortSignal` from v4 serialization page (section moved to v5 only) ## Fumadocs source - Added `v4docs` and `v5docs` as separate `defineDocs()` collections in `source.config.ts`; shared `docsSchema` (no more `preRelease` field) - `source.ts` exports both `source` (v4, `baseUrl: /docs`) and `v5Source` (v5, same base URL) ## Version routing - `version-source.ts` simplified: `filterPreReleaseFromNodes` and `isPreReleaseUrl` logic removed; v4 tree uses `source`, v5 tree uses `v5Source` + `rewriteNodeUrls` - v4 `page.tsx`: removed `preRelease` guard (v4Source has no such pages) - v5 `page.tsx`: uses `v5Source` for `getPage` / `generateStaticParams` / `generateMetadata`; `v5Link` wrapper rewrites `/docs/…` hrefs to `/v5/docs/…` so inline MDX links stay in the v5 context ## Versioned cookbook - Added `app/[lang]/v5/cookbook/` layout + page (mirrors v4 but uses `v5Source`, `rewriteCookbookUrlForVersion`, and `V5CookbookLink`) - `getCookbookTree` accepts a `versionPrefix` parameter; sidebar URLs are prefixed accordingly (`/v5/cookbook/…`) - `cookbook-tree.ts`: added `skipVersions?: string[]` per-recipe field for version-specific exclusions; `distributed-abort-controller` is marked `skipVersions: ['v5']` ## Version switcher — state & navigation - New `VersionProvider` context (`hooks/geistdocs/use-version.tsx`) backed by `localStorage`: URL is source of truth on versioned pages, `localStorage` carries the preference across non-versioned pages (cookbook overview, worlds, etc.) - `VersionSwitcher` uses `useVersion()` context instead of URL-only detection; now visible on all pages including cookbook - `DesktopMenu` and `MobileMenu` use `activeVersion` from context so the "Docs" and "Cookbook" navbar links resolve to the correct version prefix on every page - `buildVersionUrl` expanded to handle `/cookbook/…` paths alongside `/docs/…`; non-versioned routes (worlds, api) return unchanged - `switchVersion` does a `HEAD` probe before navigating; falls back to the versioned cookbook or docs home if the target page doesn't exist in that version (handles v4-only → v5 and v5-only → v4 cases) ## Cookbook content (v5) - Rewrote `agent-cancellation` recipe using a single `AbortController` pattern; removed Hard Cancellation vs Stop Signal two-approach comparison - Deleted `distributed-abort-controller` recipe from v5 (native `AbortController` serialization makes it unnecessary) - Removed references to distributed-abort-controller from `cookbook/index.mdx` and `common-patterns/timeouts.mdx` Co-authored-by: Cursor <cursoragent@cursor.com> * fix(docs): use abortSignal (not signal) in DurableAgent.stream() options Co-authored-by: Cursor <cursoragent@cursor.com> * fix(docs): update prepack scripts to use versioned content paths Content moved from docs/content/docs/ to docs/content/docs/v5/ on main (pre-release channel). Stable branch will use v4/ after backport. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
c80b747af8 |
fix(web-shared): hydrate FatalError/RetryableError and Error subclasses in o11y (#1942)
* fix(web-shared): hydrate FatalError/RetryableError and Error subclasses in o11y The web o11y reviver set was missing entries for the recently-added serialization types (FatalError, RetryableError, the built-in Error subclasses, AggregateError, DOMException), causing devalue.unflatten to throw "Unknown type X" and the UI to surface "Failed to load resource details" whenever a step or run failed with one of these error types. Adds the missing revivers to getWebRevivers() and a regression test that round-trips real values through the runtime's dehydrateStepError back through the web reviver set. * fix(web-shared): address review feedback on error revivers - Pass `cause` through ErrorOptions to the subclass constructor instead of assigning afterwards, matching `getCommonRevivers` in core. This gives the resulting `cause` property the same engine-set, non-enumerable semantics as a freshly thrown Error in the consumer realm. - Guard `RetryableError.retryAfter` against missing/undefined values from older runtime payloads — without it, `new Date(undefined)` produces an Invalid Date rather than the property being absent. Add a defensive test that drives the reviver directly with a payload missing the field. |
||
|
|
1d4f83a29a |
[swc-plugin] Preserve imports referenced by hoisted nested steps (#1944)
* [swc-plugin] Preserve imports referenced by hoisted nested steps Dead-code elimination ran before nested step functions were hoisted out of workflow bodies, so imports referenced only by hoisted step bodies were incorrectly stripped from the step bundle, causing a ReferenceError at runtime. Move DCE to run after hoisting in visit_mut_program. * [swc-plugin] Namespace nested step IDs under non-exported workflow functions Anonymous steps nested inside callback properties of a non-exported workflow function were registered with an unnamespaced step ID in step mode while the workflow-mode proxy looked them up under the workflow function name, causing a runtime 'step not found' failure. Set current_workflow_function_name in visit_mut_fn_decl for non-exported workflow functions to match the behavior in visit_mut_export_decl. Also clarify the fixture comment to distinguish step-mode and workflow-mode behavior per reviewer feedback. * [swc-plugin] Namespace nested step IDs across all workflow declaration shapes Extends the previous fix to cover all three non-exported workflow declaration forms (async function decl, const arrow, const fn-expr) by visiting the workflow body with workflow context before replacing it, and corrects the __internal_workflows manifest comment to report the same prefixed step IDs that are registered at runtime and looked up by the workflow-mode WORKFLOW_USE_STEP proxy. Adds a dedicated regression fixture covering all three shapes. |
||
|
|
4c165b6276 |
Fix backport AI permission, surface infra failures, and allow manual dispatch (#1943)
* Hoist AI model env, fix opencode external_directory permission, fail loud on AI infra errors Three related fixes triggered by the failed run on #1935: 1. Hoist the AI model name to a top-level `AI_MODEL` env var (`anthropic/claude-opus-4.7`); both `opencode run` invocations now interpolate `vercel/${AI_MODEL}` so the model is specified in exactly one place. 2. Switch `OPENCODE_PERMISSION` from the bare-string shortcut `"allow"` to the explicit object form `{"*":"allow","external_directory":"allow"}`. The shortcut was observed not to override `external_directory` (which defaults to "ask" and auto-rejects in non-interactive `opencode run`), causing the conflict-resolution AI to fail when reading scratch files it created under `/tmp/`. 3. The `Resolve conflicts with opencode` step no longer uses `continue-on-error`, and now distinguishes two outcomes via an AI- written outcome file (`.backport-conflict-outcome.json`): - `{"status":"resolved"}` — the legitimate clean path; cherry-pick continues and the backport PR is opened. - `{"status":"unresolved", ...}` — the legitimate "AI couldn't do it, hand off to a human" path; `resolved=false` is set and the conflict-failure comment is posted on the source PR. - Anything else (missing file, malformed JSON, unknown status) is treated as an opencode/AI Gateway infra failure: the step exits non-zero, the workflow fails red, and the misleading "couldn't resolve" comment is suppressed. The prompt + scratch files are also moved into the workspace so opencode never needs `external_directory` access anyway. * Allow manual workflow_dispatch with ref+model inputs; use AI_MODEL in PR body Add a `workflow_dispatch` trigger to the backport workflow with two optional inputs: - `ref` — commit SHA on `main` to back-port (defaults to `main` HEAD) - `model` — overrides the default AI model used by opencode for the decision and conflict-resolution steps (defaults to the workflow's hardcoded `AI_MODEL`) The top-level `AI_MODEL` env var now uses `${{ inputs.model || 'anthropic/claude-opus-4.7' }}` so manual runs pick up the override without changing anything else. Manual dispatch (like the `backport-stable` label) always forces a backport regardless of any AI verdict — the operator's intent is explicit by virtue of triggering the workflow. The PR body shows "Triggered manually via `workflow_dispatch`." in that case. The PR body's conflict-resolution attribution also now interpolates `${AI_MODEL}` (e.g. "opencode with `anthropic/claude-opus-4.7`") instead of hardcoding "Claude Opus" so the text stays accurate if the default model is later changed. * Address PR review: also detect leftover conflict markers in staged files The previous `Resolve conflicts with opencode` sanity check used `git diff --diff-filter=U` to detect unresolved cherry-pick conflicts, which only catches unmerged index entries. That misses the case where the AI runs `git add` on a file that still has `<<<<<<<` / `=======` / `>>>>>>>` markers in its content — git happily stages the broken file as a normal modification. Add a second check using `git diff --check --cached`, which emits `leftover conflict marker` lines when any staged content still has the standard markers. Grep specifically for that phrase so unrelated whitespace warnings don't trip the check. Also update the inline comment to accurately describe what each check covers (per Copilot's review on #1943). |
||
|
|
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. |
||
|
|
6c9c32e275 |
fix(docs): hide preview-only nav items in production (#1941)
The geistdocs facelift (#1666) dropped the VERCEL_ENV-based filter on nav items, so the "Internal" entry (preview: true) was visible on the production docs deployment. Restore the filter in the server-rendered Navbar and pass the filtered list to MobileMenu so both desktop and mobile honor it. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
0c997ce571 |
Auto-remove workflow packages from serverExternalPackages (#1481)
* Warn when serverExternalPackages hides workflow-enabled packages
Add a build-time warning when packages in serverExternalPackages contain
workflow code ('use step', 'use workflow', or serialization classes).
These packages are completely invisible to the workflow compiler when
externalized, causing silent runtime failures.
The warning detects workflow patterns via two methods:
- Fast path: check package.json dependencies for @workflow/serde
- Thorough path: read the package entry file and run pattern detection
Also adds documentation in the serialization guide about the
externalization footgun for 3rd-party packages.
* Auto-remove workflow packages from serverExternalPackages
When workflow-enabled dependencies are externalized in Next.js, compiler transforms are skipped and runtime failures follow. Detect those packages in withWorkflow, remove them from serverExternalPackages for the current build, and keep a generalized externalPackages warning fallback for non-Next builders.
* Address review feedback: add entry-point limitation comment and missing test case
|
||
|
|
537414849b |
Add missing changeset for Zod 4.4.x compatibility fix in @workflow/world (#1939)
PR #1902 landed without a changeset. This adds the missing patch-level changeset for @workflow/world describing the WorkflowRunSchema `.optional()` fix needed for Zod 4.4.x compatibility. |
||
|
|
254482e5e5 |
Push backport branch via GraphQL createCommitOnBranch for signed commits (#1937)
The repo has an enterprise-level branch ruleset requiring verified
signatures on every ref (`~ALL`), so a normal `git push` of a locally
cherry-picked commit is rejected ("Commits must have verified
signatures"). Replace the `git push` step with a GraphQL
`createCommitOnBranch` mutation, which signs commits automatically
with GitHub's internal key (the same way commits made via the web UI
are signed).
Walks the cherry-pick's diff against the parent (`stable` HEAD),
collects file additions (with binary-safe base64 contents read via
`git cat-file blob`) and deletions, ensures the backport branch
exists on the remote, then runs the mutation. Also updates the manual
conflict-resolution instructions in the failure comment to mention
that local cherry-picks must be signed (`git cherry-pick -S`)
because of the same ruleset.
Caveats:
- Authorship is lost — `createCommitOnBranch` always attributes
commits to the token owner (`github-actions[bot]`). The original
commit SHA is still referenced in the PR body.
- Non-regular files (executable bit, symlinks, submodules) are not
supported by the mutation; the step warns and proceeds with mode
100644 for affected paths.
|
||
|
|
e8ea90d496 |
Fix backport workflow opencode permission and surface AI failures (#1936)
The previous `OPENCODE_PERMISSION` value (`{"allow":["*"]}`) was the
wrong shape for opencode's permission config and was silently falling
through to defaults. Notably `external_directory` defaults to "ask",
which auto-rejects in non-interactive `opencode run` — causing the
`write` tool to fail when the AI tried to create the decision file
under `/tmp/`. Use the documented form (`"allow"` as the entire
permission config) and also move the decision/diff/prompt files into
the working directory so opencode doesn't need `external_directory`
permission at all.
Additionally, treat any opencode/AI Gateway failure (auth error,
rejected tool call, missing or malformed decision file) as a hard job
failure rather than silently defaulting to "no backport". A previous
expired AI Gateway key produced a green job that simply skipped the
backport without any indication of the underlying infra problem.
|
||
|
|
b1fc9adfa2 |
Restructure backport workflow with AI-driven decisions (#1934)
* Restructure backport workflow with AI-driven decisions Run the backport workflow on every push to main and have AI analyze each commit to decide whether to recommend a backport to stable, instead of relying on a manual backport-stable label. The action now always opens a PR for human review and never pushes directly to stable. The backport-stable label is preserved as a manual override that forces a backport regardless of the AI verdict. * Address PR review: randomized output delimiters and updated manual instructions - Use uuidgen-based delimiters when writing multiline values (PR title, body, AI reasoning) to $GITHUB_OUTPUT, so user-/model-controlled content cannot collide with or inject into the heredoc terminator. - Update the manual conflict-resolution instructions to push a backport branch and open a PR against stable, matching the new "never push directly to stable" policy. - Document the head-commit-only behavior of the push trigger inline in the workflow. |
||
|
|
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) | ||
|
|
540a2efb99 | [errors] Replace chalk import in @workfow/errors with inline ANSI shim (#1915) | ||
|
|
c56c4f6c7e |
Fix compatibility with Zod 4.4.x (#1902)
* Fix compatibility with Zod 4.4.x
* DCO Remediation Commit for Ziyak <ziyak97@gmail.com>
I, Ziyak <ziyak97@gmail.com>, hereby add my Signed-off-by to this commit:
|
||
|
|
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.
|
||
|
|
b883ea0de8 |
tarballs: redesign preview tarballs index page (#1911)
* tarballs: redesign preview tarballs index page Rebuild the static index page produced by `tarballs/scripts/pack.ts`: - Featured `workflow` package up top with prominent install command, copy button, and direct tarball download - Top-of-page metadata chips: short SHA (linked to commit), branch, PR number, build timestamp, package count + total size - Collapsible "What is this?" explainer - Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command for every row in place - Live filter input over the rest of the package list (with `/` shortcut) - Per-row install command, copy button, and direct download - Modern dark/light theme with system preference, Geist-inspired styling Also captures tarball size during pack and renders human-readable byte counts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: fix client-side interactivity broken by HTML-encoded JSON `escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in the embedded catalog JSON to `"`, so `JSON.parse(textContent)` threw on the first character and the IIFE bailed before attaching any event listeners — package-manager toggle, search filter, copy buttons, and the `/` shortcut were all dead UI on the deployed page. `<script type="application/json">` content is treated as text by the HTML parser; the only sequence that can break out is `</script>` (or `</` in legacy parsers). Replace `<` with the JSON `<` escape, which is legal per the JSON spec and prevents the breakout without needing entity encoding. Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the divisor is 1024. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: rewrite as Vite + Preact SPA with file breakdown, fix bundling Address TooTallNate's review feedback by replacing the hand-rolled HTML- in-template-literal approach with a small Vite + Preact SPA. The old ~600 lines of inlined HTML/CSS/JS in `pack.ts` is now `~80 lines of TSX`, fully type-checked. Layout: - `tarballs/index.html`, `vite.config.ts`, `tsconfig.json` at the root - `src/main.tsx` mounts the Preact app and fetches `/catalog.json` - `src/app.tsx` is the page (Header, FeaturedCard, PackageRow, etc.) - `src/catalog.ts` is the shared types + helpers (`buildInstallCommand`, `formatBytes`) - `src/icons.tsx`, `src/styles.css` - `scripts/pack.ts` is now data-only — it scans packages, packs tarballs, and writes `public/catalog.json` The eliminates several smells the reviewer called out: - The interactive script is now TypeScript with strict mode and JSX type checking instead of an inline `<script>` block - The `escapeHtml`-around-JSON-blob hack that broke client-side JS in the prior commit is gone; the SPA fetches `catalog.json` and parses it natively - Pack-time logic and presentation logic no longer share a file # Fix bundling: tarballs now actually contain compiled code While verifying real tarball sizes I noticed `workflow-serde.tgz` was only 828 bytes — it had `package.json`, `LICENSE.md`, `README.md` and *nothing* else, because each package's `files: ["dist"]` excludes sources but `dist/` hadn't been built. The Vercel build was running `pnpm --filter tarballs build`, which only builds the `tarballs` package itself — its workspace dependencies were never built. Switch `vercel.json#buildCommand` to `pnpm turbo run build --filter=tarballs`, which transitively builds dependencies first via the `dependsOn: ["^build"]` rule already in the root `turbo.json`. With the fix: workflow: 241 KiB → 252 KiB tarball, 916 KiB unpacked, 205 files @workflow/core: 59 KiB → 493 KiB tarball, 1.70 MiB unpacked, 236 files @workflow/serde: 828 B → 1.4 KiB tarball, 4.6 KiB unpacked, 7 files Add a smoke check that the `workflow` package has at least 5 files in its tarball — catches the regression directly. # Per-package contents view (packagephobia-style) `pack.ts` now also runs `tar -tvzf` on each tarball and records the file list with sizes. The SPA renders this as an expandable "What's inside?" disclosure per package, grouped by top-level directory (e.g. `dist/`, `docs/`) with proportional bars showing each group's share of the unpacked size, and the largest files listed below. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: replace tar shell-out with in-process tar reader The smoke check broke in CI: `'workflow' tarball only has 0 files`. Root cause is that `tar -tvzf` emits a different verbose layout on GNU tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) — the parser only matched the BSD column ordering, so on Linux every line was rejected and `fileCount` came out as 0. Replace the shell-out with a small in-process tar reader using `zlib.gunzipSync` + manual 512-byte block walk. ustar headers are trivially structured (name at offset 0, octal size at 124, typeflag at 156, ustar prefix at 345). We emit regular files only (`typeflag` `0` or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU long-name entries (`L`). Result is identical on every platform. Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches `tar -tvzf` exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: redesign per-package details with packagephobia-style stats The previous "What's inside?" view crammed nested directory groups, proportional bars, and per-group file lists into a `<details>` inside an already-narrow row. It was hard to read and harder to compare. Replace it with the layout packagephobia uses on its result page: - Two large headline metric tiles (Publish size / Unpacked size) with a big bold value, smaller unit, and small uppercase label. Modeled directly on packagephobia's `Stats` component but using our existing CSS variables so it tracks light/dark theme. - A single sortable file table beneath. Default is size-descending so the contributors to package size are immediately visible. Click a header to flip direction or switch sort key. Sticky header keeps the columns visible inside the scrollable region. Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles — they were the source of the "hard to use" feedback and don't add information that the flat sortable table doesn't already convey. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * tarballs: address Copilot review feedback (a11y, dev script, caching) - main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each tarballs deployment is immutable per commit, so HTTP caching is appropriate; forcing no-store made every visit re-download the full catalog (which now includes per-package file lists). - app.tsx (search input): add `aria-label="Filter packages"`. The visible label only contained an icon and placeholder, so screen readers had no name for the control. - app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` / `aria-selected` with plain buttons that use `aria-pressed`. The ARIA tab pattern requires arrow-key roving focus we never wired up; toggle buttons are the honest representation. Each button also gets an explicit `aria-label`. - app.tsx (row buttons): include the package name in the accessible label of every per-row copy/download button (and on the featured card too), so the screen reader buttons/links list distinguishes them. Added an `accessibleName` prop to `CopyButton`. - app.tsx (CopyButton): only flip to the "Copied" state when the write actually succeeded. Both the modern `navigator.clipboard` path and the `execCommand` fallback can fail; the new `writeToClipboard` helper returns success and the button shows a short "Failed" state if both paths fail. # Make `pnpm dev` work from a clean checkout The previous `dev: vite` couldn't actually serve the page because `/catalog.json` 404s and the SPA boots into the error fallback. Restructure the build layout to vite's conventional shape: - `public/` is now a true vite public dir — pack writes tarballs and catalog.json there. In dev, vite serves these at the root. - `dist/` is the production build output (vite copies public/ into it and adds index.html + assets/). - `vercel.json#outputDirectory` switches from `public` → `dist`. - `turbo.json` outputs updated to match. - `dev` chains pack before vite so the catalog exists when the dev server starts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9463df8045 | Remove extra changeset (#1922) | ||
|
|
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 |
||
|
|
7c45e9e213 |
Enforce per-(run, correlation) uniqueness for entity-creating events in world-postgres (#1878)
Adds a unique partial index on workflow_events(run_id, correlation_id, type) filtered to step_created/hook_created/wait_created, and translates the resulting unique-violation (pg code 23505, surfaced via DrizzleQueryError.cause) into EntityConflictError. The steps table already deduped via onConflictDoNothing, but the event row still inserted, leaving duplicate events in the log. Now both rows are kept consistent and the runtime's existing dedup catch path handles concurrent writers cleanly. |
||
|
|
5eb0b792b8 |
fix(world-vercel): add default request timeout to workflow-server HTTP calls (#1807)
* fix(core): add hard exit deadline to replay timeout handler When the replay timeout fires on attempt 4+ and tries to write a run_failed event, the process.exit(1) call is gated behind an await on world.events.create(). If workflow-server is unresponsive and the response never arrives, process.exit(1) is never reached, causing the function to idle until the platform kills it at maxDuration (e.g. 15m). Add a 30s hard exit deadline (setTimeout → process.exit) that fires independently of the run_failed write, guaranteeing the process exits promptly even when the server hangs. Made-with: Cursor * Update packages/core/src/runtime.ts Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Update .changeset/fix-replay-timeout-exit.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Update packages/core/src/runtime/constants.ts Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * fix(world-vercel): add default request timeout to workflow-server HTTP calls Move the hang mitigation from the core replay-timeout handler to the world-vercel transport layer, per review feedback. A default AbortSignal.timeout(60s) now applies to every makeRequest() call, covering all world.* API methods (not just the replay timeout path). Timeouts are converted to WorkflowWorldError so existing catch sites handle them uniformly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(world-vercel): drop incident-specific latency note from timeout comment Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(world-vercel): address review nits on makeRequest timeout - Compose per-request timeout with caller-provided AbortSignal via AbortSignal.any() so a future caller passing options.signal doesn't silently lose the hang protection (and vice versa). - Move the floating eslint-disable-next-line for the undici dispatcher cast back next to the actual `fetch(... as any)` call where the suppression applies, instead of pointing at `const fetchStart`. Both nits flagged by @VaguelySerious in the AI review on PR #1807. Made-with: Cursor * Apply suggestions from code review Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> --------- Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|
|
6dd5c72d8a |
Allow disabling step sourcemap with new sourcemap option in builders (#1842)
|
||
|
|
26de71b9f8 | [ci] Enable Vercel-prod e2e for tanstack-start (#1904) | ||
|
|
45d1eb2340 |
web: configure vercelPreset() for Vercel deployments (#1815)
* web: configure vercelPreset() for Vercel deployments Enables per-route bundle splitting, function-level configuration, and an accurate Deployment Summary for the packages/web Vercel deployment. Gated on WORKFLOW_WEB_VERCEL_BUILD (rather than the ambient VERCEL var) so that the standard build layout consumed by server.js (self-hosted deployments and the CLI's in-process server via @workflow/web/server) is still produced when the package is packed as a tarball by the docs Vercel deployment. Existing VERCEL-based checks in vite.config.ts have been migrated to this same variable for consistency. * web: allow WORKFLOW_WEB_VERCEL_BUILD through Turborepo Turborepo strips environment variables not declared in turbo.json, which prevented WORKFLOW_WEB_VERCEL_BUILD (set on the Vercel project) from reaching the build. Declare it in the env list and also include the .vercel/ directory (where the preset writes react-router-build-result.json) in the build outputs. * web: narrow Turbo output to react-router-build-result.json * web: address PR review feedback - Check WORKFLOW_WEB_VERCEL_BUILD === '1' explicitly (rejects '0', 'false', etc.) in both react-router.config.ts and vite.config.ts - Use double-quoted package name in changeset frontmatter to match the repo's dominant convention |