mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
pranaygp/codex/bounded-workflow-cache
219 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
63f1a9906d |
[web-shared] Fix message-only error rendering (#2251)
* Fix message-only error rendering * Handle non-string error stacks |
||
|
|
9398065812 |
Cleaning up trace viewer code (#2252)
* cleanup * remove dead code * Delete .changeset/postgres-pin-event-now.md Signed-off-by: Mitul Shah <mitulxshah@gmail.com> * Update icons.tsx * Update event-list.tsx * Update timeline.tsx * Create trace-viewer-cleanup.md --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
ff66ee9f2b |
Version Packages (beta) (#2216)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
5bf2c167a5 | Add serializable reviver compatibility check (#2250) | ||
|
|
24a96d8301 | Fix HookConflictError web hydration (#2249) | ||
|
|
fe41b3be3c |
Reduce width on trace viewer detail pane (#2209)
* Update trace-viewer.tsx * Create narrow-trace-detail-pane.md |
||
|
|
275316fac4 |
Version Packages (beta) (#2183)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
445ec8c7e6 |
Add virtualization to the trace viewer (#2205)
* loading state * Update trace-viewer.tsx * Update use-trace-viewer.test.ts * virtualization * Create swift-parks-move.md Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
f0f002ae05 |
Trace viewer: scroll-load events past an auto-load cap (#2200)
* loading state * Update trace-viewer.tsx * Update use-trace-viewer.test.ts |
||
|
|
0606949e4a |
Add loading skeleton to the new trace viewer (#2164)
* Update run-detail-view.tsx * trace viewer skellyl * Add changeset for trace viewer loading skeleton Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Export TraceViewerSkeleton from web-shared Lets consumers render the trace viewer loading skeleton standalone (e.g. front's run-detail page during the initial run fetch). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Update trace-viewer-skeleton.tsx * Update trace-viewer-skeleton.tsx --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ab7e5ab7ba |
Add tooltip components + apply on up/down detail pane (#2163)
* tooltips * Apply suggestions from code review Co-authored-by: Mitul Shah <mitulxshah@gmail.com> Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
3d615fb78d |
Version Packages (beta) (#2162)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
ad5c068d7f |
[web-shared][web] Fix events tab search (#2107)
* Add server-backed exact ID search to the Events tab. Replace client-side substring filtering with API lookups for full correlation and event IDs so searches work beyond the first loaded page. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix exact ID search dimming and support wrun_ correlation IDs. Disable group dimming for server search results and accept run IDs in the exact ID parser so run-level correlation search works. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix dimmed row when searching by event ID for run-level events. Map selectedGroupKey to __run__ for run-level search results so the matched row is treated as related instead of dimmed. Co-authored-by: Cursor <cursoragent@cursor.com> * Remove run ID search from Events tab exact ID lookup. Workflow-server only accepts step, wait, and hook correlation IDs — not wrun_. Update the search placeholder and validation toast accordingly. Co-authored-by: Cursor <cursoragent@cursor.com> * Harden exact ID search UX and correlation fetch limits. Normalize lowercase ULIDs, scope Enter toasts to ID-like input, abort stale searches, disable search when unavailable, expand parser tests, and cap correlation pagination in workflow web. Co-authored-by: Cursor <cursoragent@cursor.com> * Fix search clear race and surface truncated correlation results. Guard successful exact-ID search against aborted requests, invalidate in-flight work when the input clears, and return truncation metadata from correlation pagination. Co-authored-by: Cursor <cursoragent@cursor.com> * Differentiate exact ID search errors from not-found results. Return a discriminated union from onExactIdSearch and show search errors in the Events tab instead of mislabeling them as missing IDs. Co-authored-by: Cursor <cursoragent@cursor.com> * Apply suggestion from @VaguelySerious Signed-off-by: Peter Wielander <mittgfu@gmail.com> --------- Signed-off-by: Peter Wielander <mittgfu@gmail.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
7e7d7e61d2 | Version Packages (beta) (#2147) | ||
|
|
b33c5ef120 |
Better search handling on the trace viewer (#2144)
* Update copyable-data-block.tsx * Update event-list.tsx * Create clever-spans-search.md * Update index.ts * Update detail-panel.tsx * Create icon-button.tsx * nice * nice * cleanup |
||
|
|
e8e52925ad |
fix(web-shared): match Geist Button hover, focus, and radius (#2143)
The shared Button's dark-mode hover relied on an unregistered `dark-theme:` Tailwind variant, so the inverted (default) button lost its hover style — and the background resolved to transparent when consumed by apps that supply their own Geist tokens (e.g. vercel/front). Use Geist's literal hover fallbacks driven by arbitrary ancestor-theme variants instead, render the previously missing focus-visible ring, and apply Geist's 4px tiny radius to the xs size. Authored to compile under both Tailwind v3 and v4. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
799cc09df3 |
Update zoom factor on the trace viewer timeline (#2041)
* Update trace-viewer.tsx * Create early-cooks-accept.md Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
2f19552035 |
Version Packages (beta) (#2140)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
1e6b1fdea2 |
Attributes MVP (experimental and write-only) and CI hardening (#2134)
* fix(core): scan inline sourcemaps during error remapping * Attributes MVP (experimental and write-only) (#2088) |
||
|
|
8633ebb9b3 |
fix(web-shared): make encrypted-data blur backwards compatible with Tailwind v3 (#2108)
* fix(web-shared): use inline blur style for tailwind v3 compatibility Co-authored-by: Mitul Shah <mitulxshah@gmail.com> * fix(web-shared): use blur-[4px] arbitrary value for tailwind v3/v4 compat Co-authored-by: Mitul Shah <mitulxshah@gmail.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> |
||
|
|
a490f584e0 | [web-shared] Fix "Queued for" duration for retried steps (#2087) | ||
|
|
34481af4b6 |
Detail Pane Cleanup (#1973)
* cleanup items * Update events-list.tsx * nice * Update attribute-panel.tsx * woo * remove toast * Fix: Unused variable `selectedResource` causes TypeScript build failure (TS6133) due to `noUnusedLocals: true` in tsconfig. This commit fixes the issue reported at packages/web-shared/src/components/new-trace-viewer/trace-viewer.tsx:462 **Bug explanation:** The variable `selectedResource` is declared on line 462 of `trace-viewer.tsx`: ```ts const selectedResource = selectedSpan?.resource as string | undefined; ``` However, all references to this variable were removed in the PR (the colored resource badge in the panel header was removed), leaving behind the unused declaration. The project's TypeScript configuration has `noUnusedLocals: true`, which causes TypeScript to emit error TS6133 for any declared-but-unused local variables. This is confirmed directly in the Vercel build logs: ``` @workflow/web-shared:build: src/components/new-trace-viewer/trace-viewer.tsx(462,9): error TS6133: 'selectedResource' is declared but its value is never read. ``` This caused the `@workflow/web-shared#build` task to fail with exit code 2, which in turn caused the entire Vercel deployment to fail. **Fix explanation:** Removed the unused `const selectedResource = selectedSpan?.resource as string | undefined;` declaration on line 462. The nearby `selectedResourceId` variable (which was NOT removed) remains in place and is still actively used in the JSX below. This is a minimal one-line deletion that resolves the build failure. Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: mitul-s <mitulxshah@gmail.com> * tweak * Update events-list.tsx * Update events-list.tsx * cleanup * Update attribute-panel.tsx * copy button * Create button.tsx * cleanup * rename `this` to Context * cleanup * Update button.tsx * avoid decrypt flashign * cleanup * Update attribute-panel.tsx * nav on detail card * polish * Update detail-card.tsx * Update trace-viewer.tsx * colors * Update attribute-panel.tsx * Update detail-card.tsx * Update copyable-data-block.tsx * Detail Pane + Other cleanup items (#2020) * polish * Update attribute-panel.tsx * Create wise-frogs-thank.md Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> |
||
|
|
8f9656deab |
Version Packages (beta) (#2026)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
753e39e3bb |
trace viewer bug fix + file cleanup (#2036)
* Update timeline.tsx * Update timeline.tsx * Update timeline.tsx * Update changeset title for trace viewer fix Signed-off-by: Mitul Shah <mitulxshah@gmail.com> * Update timeline.tsx --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
366f9cc967 |
Add reduced motion for the trace viewer (#2045)
* add reduced motion * Add reduced motion for trace viewer Implement reduced motion settings for the trace viewer. Signed-off-by: Mitul Shah <mitulxshah@gmail.com> --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
5a393a6ffc |
Trace Viewer colour updates (#2022)
* tweaks * Update trace-viewer.tsx * Update Trace Viewer colors Signed-off-by: Mitul Shah <mitulxshah@gmail.com> * Update timeline.tsx * Update event-list.tsx --------- Signed-off-by: Mitul Shah <mitulxshah@gmail.com> |
||
|
|
245d999031 |
Version Packages (beta) (#1997)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
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. |
||
|
|
b885f1f2d1 |
Version Packages (beta) (#1888)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
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> |
||
|
|
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. |
||
|
|
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> |
||
|
|
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.
|
||
|
|
1203dae70c |
Friendlier workflow errors (consolidated) (#1849)
* Introduce structured context-violation errors + Ansi renderer Phase 1: Add Ansi rendering helpers (frame, hint, note, help, code, inline) to @workflow/errors, and a chalk mock for readable snapshot tests. Phase 2: Add four context-violation error classes to @workflow/core (NotInWorkflowContextError, NotInStepContextError, NotInWorkflowOrStepContextError, UnavailableInWorkflowContextError) and apply them to all twelve user-facing throw sites so errors now include docs links and a structured "what/why/fix" frame. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review: tighten changeset, implement ansifyName, harden Ansi - Tighten phase 1 changeset to a single sentence (per pranaygp review) and switch to double-quoted frontmatter (per Copilot + repo convention). - Implement `ansifyName` to actually apply dim styling to workflow/ / step/ prefixes; add an `Ansi.dim` helper to `@workflow/errors` so callers don't need to import chalk directly. - Remove the `void getWorkflowMetadata;` workaround in context-errors.ts by dropping the unused value import (we only needed the type and symbol). - Render the plain-Error throw in `workflow/get-workflow-metadata.ts` with `Ansi.frame` + docs link so the VM path matches the structured-class styling from the sibling step path (still uses a plain Error to avoid the module-init cycle). - Guard `buildUnderline` against zero-length markers so a stray empty token can't produce a negative `String.repeat` count. * Structured runtime logger metadata + fold in replay-timeout logging Adds a `.child()` and `.forRun(runId, workflowName)` child-logger API to the structured logger so runtime/step code doesn't have to repeat `workflowRunId`/`workflowName`/`stepId` on every call. Normalizes error metadata to structured `errorName` / `errorMessage` / `errorStack` fields instead of ad-hoc `error: err.message` strings, and adds comments to silent catches that swallow expected idempotency conflicts. Also folds in the pending changes from #1812 so that PR can be closed: - Standardize the console prefix to `[workflow-sdk]`. - Split the replay-timeout log into a warn-while-retrying vs. error-when-giving-up, and surface the underlying error when we can't mark a timed-out run as failed. - Include the error stack in the "Fatal runtime error during workflow setup" log and in the top-level user-code workflow error log so the stack surfaces in flattened log drains. - Drop the `[Workflows] "<runId>" - ` prefix from `buildWorkflowSuspensionMessage` — the structured logger now attaches run context. Supersedes #1812. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use double-quoted changeset frontmatter per repo convention * Add SerializationError + apply to user-facing serialization sites Phase 4 of friendlier errors: introduce a `SerializationError` class with an optional `hint` and a docs link (workflow-sdk.dev/err/serialization-failed), and adopt it at every user-facing serialization boundary in @workflow/core: - Locked ReadableStream at a workflow boundary - Unregistered class / missing `classId` / missing `WORKFLOW_DESERIALIZE` - Attempting to return step functions to clients or call workflow functions directly - Webhook `respondWith()` called outside a step - `dehydrate*` / `getSerializeStream` failures (workflow args/return, step args/return, stream chunks) Internal invariants (format prefix length checks, unknown format bytes, missing `STREAM_NAME_SYMBOL`, encryption key/size guards, etc.) now throw `WorkflowRuntimeError` instead of plain `Error` so the classifier and logger treat them consistently. `formatSerializationError` now returns `{ message, hint }` so the hint fragment can be rendered with the standard SerializationError framing instead of being baked into the message string. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use double-quoted changeset frontmatter per repo convention * Presentation-only user vs SDK error attribution Add describeError() that derives attribution and class-aware hints from existing error classes + RUN_ERROR_CODES — no event data changes. Wire into step failures, max-delivery exhaustion, run failures, and fatal setup errors so terminal logs include errorAttribution and a hint for known error types. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Address review: describeError accepts precomputed errorCode + instanceof - `describeError(err, errorCode?)` now accepts an optional precomputed `RunErrorCode`. `classifyRunError(err)` only narrows to USER_ERROR / RUNTIME_ERROR, so the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches were previously unreachable from the step / run failure log sites. Callers that know the failure category (runtime.ts for replay timeout and max-deliveries exhaustion) now pass the code in. - Context-violation checks use `instanceof` against the actual classes from context-errors.ts instead of a name-string set. Type-safe + survives class renames. - Wire the new hints through to the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED log sites so those branches actually render a hint now. - 3 new tests cover the reachable code paths + precomputed-code override. - Changeset frontmatter switched to double quotes per repo convention. * Cosmetic consistency pass on remaining bare throws Internal invariants now use WorkflowRuntimeError so describeError attributes them to the SDK: missing startedAt, VM generateKey, closure-vars outside step context, ENOTSUP. defineHook().resume() formats schema validation failures as a readable list instead of a JSON blob. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Use double-quoted changeset frontmatter per repo convention * Data-driven describeRunError + expose via @workflow/core/describe-error Observability renderers read persisted run_failed / step_failed event data, not live Error instances. describeRunError takes { errorCode, errorName } and returns the same { attribution, hint } shape as describeError, so the CLI and web UI can derive user-vs-SDK framing from the event log directly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Friendlier build-time errors: WorkflowBuildError class + applications Add `WorkflowBuildError` class in `@workflow/errors` with optional `hint` for an actionable next step, and apply it in `@workflow/builders` at user-facing sites: failed esbuild phases, unresolved built-in steps, and empty esbuild output now throw `WorkflowBuildError` with a hint pointing at the likely fix. Runtime invariants remain plain `Error`. * Polish friendlier-errors rendering: drop functionName leak, simplify docs link, redirect stack - Drop the readonly `functionName` param-property on context-error classes so util.inspect no longer prints a trailing `{ functionName: 'foo()' }` block. - Replace the `DocLink` ("label: https://…") shape with a plain `DocsUrl` template-literal type. Error output now renders a single clean line: `docs: https://…` (new `Ansi.docs` helper) instead of the noisier "note: Read more about foo(): https://…". - Add throw helpers (`throwNotInWorkflowContext`, etc.) that call `Error.captureStackTrace(err, stackStartFn)` on V8 engines so the top frame of the thrown error points at the user's call site instead of at the gate function inside the framework. Callers pass themselves as the boundary. - Refactor `defineHook()` (both root and `/workflow`) to use named function closures rather than `this.create`/`this.resume`, since the stack redirect relies on a stable function identity that survives destructuring. - Update context-errors.test.ts to snapshot the new `docs:` framing and to add a regression test asserting the top stack frame is the user call site. * Consolidate friendlier-errors stack: fix ANSI leak + non-retry semantics Addresses PR review feedback across the 8-phase friendlier-errors stack and fixes issues surfaced by manual testing (createHook() inside a step): - ANSI no longer leaks into .message / .stack. Context-violation errors now store plain text on .message and render the colored framed form lazily via [util.inspect.custom] / toString(). Structured logs, log drains, CBOR-serialized events, and JSON payloads no longer contain raw \x1B[...m bytes. - Context violations are now fatal. ContextViolationError sets fatal = true; FatalError.is(err) recognizes any error with a fatal: true own property. Calling createHook() from a step no longer burns three retry attempts on a guaranteed-to-fail context violation. - Ansi helpers moved to @workflow/errors/ansi subpath so imports from @workflow/errors no longer pull chalk into consumers that only want error classes (addresses reviewer VaguelySerious). - Shared redirectStackToCaller helper in packages/core/src/capture-stack.ts, used by both context-errors.ts and workflow/get-workflow-metadata.ts (addresses Copilot review on #1849). - Structured framed content: ContextViolationError now takes a structured FramedContent (title segments + detail branches) and renders plain/pretty from the same source of truth. Tightens the eight existing phase changesets to 1-2 sentences each and adds four new scoped changesets (errors-ansi-subpath, context-errors-plain-message, context-errors-fatal, capture-stack-shared) for the followup fixes, so the final changelog history stays readable. * test: update step-handler mocks for scoped forRun() logger The runtime logger now uses .forRun(runId, name, {stepId, stepName}) to attach scope context, so 409-handling log calls no longer repeat {workflowRunId, stepId} in every metadata bag — those live on the scoped logger instance. Update the mock to return itself from forRun() and tighten assertions to check both the log args (errorName/errorMessage) and the forRun() scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Mark SerializationError fatal + route dehydration through step-failure path SerializationError now carries readonly fatal = true. Step-return dehydration is wrapped inside the user-code try/catch so that the resulting error flows through userCodeFailed → step_failed → FatalError.is() short-circuit instead of bubbling up as HTTP 500 and triggering a queue retry loop. Retrying a step that returned a non-POJO is guaranteed to fail the same way, so this saves ~20s and 3 near- identical error blocks per serialization failure. * Add logging snapshot tests + manual-test artifacts Snapshot tests lock in the exact shape of: - describeError() payloads (attribution, errorCode, hint) for every classification — plain Error, SerializationError, context-violation, WorkflowRuntimeError, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED. - The scoped-logger call signature for the two canonical runtime failure paths (fatal-bubble and hit-max-retries), so refactors of forRun() / child() metadata merging can't silently change what users see in their log drains. SerializationError now also has a direct test for readonly fatal=true + FatalError.is() recognition. pr-artifacts/ contains real log-output snapshots from running the nextjs-turbopack workbench against five error scenarios. These are reference material for reviewers and are flagged to be removed before merge. * Readable step-fatal logs: inline stack + friendly step/workflow names The step-level fatal-error log used to embed the full stack trace inside an `errorStack` string field in the metadata object, so util.inspect rendered it as a quote-escaped, line-continuation blob when the log hit the terminal — unreadable in practice. Move framing + stack into the log *message* (matching the workflow-level log in runtime.ts) and keep the metadata object compact with only the indexable structured fields (`errorAttribution`, `errorName`, `errorMessage`, `hint`, IDs). Log drains still get the same keys; humans now see a readable stack trace. Also introduce `formatStepName` / `formatWorkflowName` in `@workflow/utils` that render machine names (`step//./workflows/1_simple//add`) as `add (./workflows/1_simple)` in log framings, using the existing `parseStepName` / `parseWorkflowName` parsers. Applied to step-fatal, hit-max-retries, exceeded-max-retries, and workflow-threw log sites. Artifacts in pr-artifacts/ updated to show the new output shape, and renamed .log → .md since they're Markdown and IDE previews are nicer that way. * Opinionated pretty formatter for runtime structured-log metadata Replace util.inspect's default object dump (which quote-escapes multi-line stacks and paragraph hints into a single-line JSON-y blob) with a workflow-aware formatter that composes the entire log line into a single string passed to console.error / console.warn. Highlights of the new output: - Per-run / per-step IDs render with their parsed friendly names so users see `wrun_… · simple (./workflows/1_simple)` instead of just the raw `workflowName: 'workflow//./workflows/1_simple//simple'`. - Color-coded attribution badge (user error red / sdk error magenta) paired with the error class in bold. - Hints render as a paragraph under `hint:` rather than a backslash- `\n`-escaped string. - Drops redundant fields (errorStack always; errorMessage when it's already in the parent message) to avoid double-printing. - Unknown fields fall through as a sorted `key value` tail so we never silently drop log information. @workflow/errors/ansi gains bold/red/magenta helpers used by the formatter. The web / web-shared packages don't consume stderr — they read structured event payloads from the World event log — so this is presentation-only at the runtime layer. * ci(benchmarks): disable pnpm cache for getCommunityWorldsMatrix The job never runs `pnpm install` (it just calls `node` against a checked-in script), so the pnpm store path never exists. The post-job `actions/setup-node@v4` cache-save then fails with `Path Validation Error: Path(s) specified in the action for caching do(es) not exist` and red-X's the entire job even though the matrix step succeeded. The setup-workflow-dev composite already has a `cache-pnpm` opt-out input for this exact case — wire it through here. * Address PR review comments: inspect dedup, cause leak, retry-loop tests - ContextViolationError: util.inspect(err) duplicated every framed detail line because the stack-tail strip only sliced the first message line. V8's Error.stack reads `Name: messageLine1\n messageLine2\n at ...`, so for our multi-line `title\n╰▶ docs: …` messages every detail line was getting prepended twice (once in the pretty form, once via the unsliced message tail). Count the actual message lines and slice past all of them. Repro test asserts `╰▶ docs:` appears exactly once. - WorkflowError: stop assigning `cause: undefined` as an enumerable own property when no cause is provided. Subclasses (every error in this PR) inherit the parent constructor; the unconditional assignment polluted `util.inspect(err)` output with `{ cause: undefined, … }` on every no-cause instance. The `super(...)` call already conditionally sets `.cause` non-enumerably when `options.cause` is provided. - step-handler.test.ts: add a regression-gate suite that exercises the fatal-vs-retryable retry-loop wiring directly. Asserts that an error with `fatal: true` produces exactly one `step_failed` event with no `step_retrying`, and that a non-fatal `Error` retries via `step_retrying` on early attempts and emits `step_failed` once the retry budget is exhausted. Catches the silent-regression case where `fatal = true` is removed from a context-violation error class but the `FatalError.is()` unit tests stay green. * Consolidate changesets + remove pr-artifacts Address review feedback to drastically shorten the changesets — fold the 15 file-by-file entries into a single user-facing changeset for @workflow/core / errors / builders / utils. Also drop the pr-artifacts/ folder (reviewer-only log captures, no longer needed). * Polish runtime error logging: layout, stack trim, hint consolidation Five user-driven fixes from manual smoke-testing of #1849: 1. Logger layout. composeLogLine() now puts the structured-fields block (attribution badge, run/step IDs, error code) **between** the framing line and the stack body, instead of after it where 30+ lines of stack buried the most useful information. The framing stays at the top, stack at the bottom, structured info readable at a glance. 2. Stack trim. Drops framework-internal frames (`node_modules/.pnpm/`, `node:internal/`, Turbopack-bundled `node_modules__pnpm_*` chunks, `_next_dist_*` chunks) and caps the surviving frame count at 6 so the stack stays compact even on heavy async wrappers. Suppressed runs emit one summary line so users know the trim happened. 3. Wrapper-route noise. The nextjs-turbopack workbench's start route was catching `WorkflowRunFailedError` rejection on `Promise.race([readLoop(), run.returnValue])` and re-logging it via `console.error('Error in workflow stream:', error)` plus `controller.error(error)` — which then triggered Next.js's `⨯ failed to pipe response` overlay. The SDK already logs the failure cleanly upstream and the runId is on the response header, so the wrapper now closes the SSE stream cleanly on WorkflowRunFailedError. 4. Consistent framed `╰▶ hint:` / `╰▶ docs:` layout for all errors that carry a hint or docs slug. WorkflowError, SerializationError, and WorkflowBuildError now share one `appendFramedDetails` helper matching the box-drawing structure that ContextViolationError already used. Was: blank-line-separated `Learn more: <url>`. Now: one tree, indistinguishable from context-violation rendering. 5. Drop the duplicate logger-side `hint` field. Hints now live on the error message only — actionable hints get serialized into the event log, rehydrated on the workflow side, and shown in observability automatically. The previous logger-only hint duplicated stderr but never made it past the step boundary. Updated SerializationError hint to point at the foundations doc ("Ensure you're returning workflow serializable types. Check the serialization docs to see what's serializable: https://workflow-sdk.dev/docs/foundations/serialization") instead of the hardcoded `(plain objects, arrays, primitives, …)` list, which drifted out of sync as the supported types grew. Same hint reuses for step args, workflow args/return, stream messages, and any other site that goes through `formatSerializationError`. Also retitled the retry summary `3 retries` → `3 max retries` since "3 retries" next to "4 attempts" was ambiguous (already-happened vs. budget). * Trim error-card title + drop machine step name from persisted error - ErrorStackBlock (web observability): show just the first non-empty trimmed line of the error message in the card title with single-line truncation. Multi-line messages (`Failed to serialize step return value\n╰▶ hint: …`) were rendering the entire framed body in the title, pushing the copy button off-screen and burying the scannability of the headline. Full message stays in the body via the stack (V8 prepends `Name: message` to `Error.stack`), so no information is lost; hover-tooltip exposes the full title text. - Persisted error message: drop the `Step "step//./.../foo"` machine name from `Step failed after N retries: …` and `Step exceeded max retries (…)` strings. Observability already attributes the event to a specific step via the UI tree, and the CLI logger emits the friendly `Step foo (./...) hit max retries` framing on its own line. Embedding the raw `step//./...` machine name in the persisted message text was duplicate noise. * Update .changeset/friendlier-errors.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Update .changeset/pretty-log-format.md Co-authored-by: Peter Wielander <mittgfu@gmail.com> Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> * Update SerializationError snapshot tests for slug-less message The class no longer attaches a slug-based `╰▶ docs:` line — the foundations URL is embedded directly in the hint via the `formatSerializationError` helper in @workflow/core. Update the test expectations accordingly: - bare-title case is now a single line (no docs link) - hint case renders one `╰▶ hint: …` branch (no second branch) * Update serialization.test.ts hint assertions for foundations URL Four `should throw error for an unsupported type` cases were still asserting on the old hardcoded type list. Update to the new hint phrasing that points at the foundations doc, matching the change in `formatSerializationError` (`packages/core/src/serialization/errors.ts`). --------- Signed-off-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: Peter Wielander <mittgfu@gmail.com> |
||
|
|
9f3516ec28 |
Refactor: Extract serialization into modular architecture and wire into existing pipeline (#1299)
* Add serialization module foundation: types, codec interface, format prefix
Start of the serialization refactor (separate from snapshot-runtime).
New files:
- serialization/types.ts — SerializationFormat enum, SerializableSpecial
interface, Reducers/Revivers types
- serialization/codec.ts — Codec interface with formatPrefix, serialize,
deserialize, and optional deserializeLegacy
- serialization/format.ts — Format prefix encode/decode/peek, moved from
the monolithic serialization.ts
The Codec interface enables future alternative formats (CBOR, JSON) while
keeping the devalue implementation as the current default.
* Add reducers, devalue codec, encryption, and mode-specific modules
Serialization refactor Phase 1: create the new module structure alongside
the existing monolithic serialization.ts (which continues to work).
New files:
- serialization/reducers/common.ts — Date, Error, Map, Set, URL, BigInt,
typed arrays, Headers, Request, Response, RegExp, URLSearchParams
- serialization/reducers/class.ts — Class/Instance with WORKFLOW_SERIALIZE/
DESERIALIZE support
- serialization/reducers/step-function.ts — StepFunction with closure vars
- serialization/codec-devalue.ts — devalue Codec implementation
- serialization/encryption.ts — composable encrypt/decrypt layer
- serialization/workflow.ts — synchronous, no encryption, for VM use
- serialization/step.ts — async with encryption, for step handler
- serialization/client.ts — async with encryption, for start() API
- serialization/index.ts — re-exports all public API
- serialization/serialization.test.ts — 25 focused tests
All modes compose their reducer/reviver sets from the shared building blocks.
Cross-mode compatibility verified: data serialized in any mode can be
deserialized in any other mode (for common types).
Existing 108 serialization tests continue to pass unchanged.
* Add sub-path exports for workflow serialization module
- Add ./serialization/workflow export to @workflow/core package.json
- Add ./internal/serialization re-export to workflow meta-package
- The workflow bundle can now import serialize/deserialize via:
import { serialize, deserialize } from 'workflow/internal/serialization'
Full test suite passes: 493 tests across 22 files (including 25 new
serialization module tests).
* Address code review feedback
1. Fix reducer composition order: Class/Instance reducers now come BEFORE
common reducers in all three modes (workflow, step, client). This ensures
custom Error subclasses with WORKFLOW_SERIALIZE are handled by the
Instance reducer before the generic Error reducer (devalue uses
first-match-wins semantics).
2. Fix encryption decrypt() to fail fast when encrypted data is encountered
without a decryption key, instead of silently returning encrypted bytes
that would fail later with an unhelpful format error.
3. Remove Request/Response from common reducers — they don't have matching
common revivers, so including them caused asymmetric behavior (serialize
as Request, deserialize as plain object). Request/Response handling
belongs in mode-specific modules that can provide proper revivers.
4. Document Node.js dependency in the workflow serialization re-export.
The current implementation uses node:util and Buffer. For the QuickJS
VM (snapshot runtime), these will need polyfills — tracked separately.
* Move reducer/reviver composition into the devalue codec
The Codec interface now takes a SerializationMode ('workflow', 'step',
'client') instead of raw reducers/revivers. The reducer/reviver
composition is internal to the devalue codec implementation.
This is the right abstraction because reducers/revivers are devalue-
specific concepts. A future CBOR codec would handle Date, typed arrays,
Map, Set natively via the CBOR type system — it wouldn't use reducers
at all. A JSON codec would only support standard JSON types.
The mode-specific modules (workflow.ts, step.ts, client.ts) are now
simpler — they just pass the mode string to the codec.
* Replace SerializationFormatType enum with open-ended FormatPrefix type
The format prefix is now a branded string type validated by
isFormatPrefix() — any 4-character [a-z0-9] string is valid.
This removes the hard-coded enum of known formats, making the system
truly open for extension:
type FormatPrefix = string & { __brand: 'FormatPrefix' };
function isFormatPrefix(value: string): value is FormatPrefix;
The SerializationFormat object still provides well-known constants
('devl', 'encr') but they're now just typed constants, not an
exhaustive enum.
peekFormatPrefix() and decodeFormatPrefix() use isFormatPrefix() for
validation instead of checking against a known list. Unknown but valid
prefixes (e.g. 'cbor', 'json', 'v2b1') are accepted — the caller
decides whether they can handle the format.
6 new isFormatPrefix tests covering: valid strings, too short, too long,
uppercase, special characters. 1 new test for unknown-but-valid prefixes.
* Wire modular serialization modules into serialization.ts, add 138 unit tests
Replace duplicate format prefix, reducer/reviver, and encryption helper
code in the monolithic serialization.ts with imports from the modular
serialization/ directory. This completes the refactoring started in the
earlier additive-only commits.
Key changes:
- serialization.ts now imports types, format prefix, common/class/step-function
reducers and revivers, and encryption helpers from ./serialization/ modules
- Removed ~450 lines of duplicate code from serialization.ts
- Made encryption error messages consistent between old and new modules
- Added 138 comprehensive unit tests covering types, format prefix,
encryption, codec, all three reducer modules, all three mode modules,
cross-mode compatibility, and edge cases
- Updated one existing test assertion for new error message wording
* Address code review feedback
- encryption.ts: throw WorkflowRuntimeError instead of plain Error in
decrypt() to preserve the error contract from legacy maybeDecrypt()
- format.ts: document that open-ended prefix validation ([a-z0-9]{4})
is intentional for forward compatibility — callers check support
- errors.ts: extract duplicated formatSerializationError into shared
utility, remove 4 copies from workflow.ts, step.ts, client.ts
- codec-devalue.ts: document that globalThis default is a known
limitation; legacy dehydrate/hydrate path still supports custom global
* Fix codec-devalue.ts comment: clarify modular modules are not used in current runtime
The globalThis default is not a limitation for the current runtime —
all serialization goes through dehydrate*/hydrate* in serialization.ts
which passes the correct global. The modular modules are infrastructure
for the future snapshot runtime where serialization runs inside the VM.
* Wire dehydrate/hydrate functions through modular serialize/deserialize
The dehydrate*/hydrate* functions in serialization.ts now delegate to
the modular mode modules (workflowModule, stepModule, clientModule)
instead of directly calling devalue stringify/parse/unflatten.
Key changes:
- Extended Codec interface with CodecOptions (global, extraReducers,
extraRevivers) so the codec can receive VM globals and mode-specific
stream/Request/Response handlers
- devalueCodec threads global through to all reducer/reviver factories
so instanceof checks work across VM boundaries
- Mode modules (workflow.ts, step.ts, client.ts) accept CodecOptions
and pass them through to the codec
- dehydrate*/hydrate* functions now call module serialize/deserialize
with stream and Request/Response reducers/revivers passed as extras
- v1Compat path remains inline (pre-codec, uses stringify + revive)
- Error context strings preserved via try/catch re-wrapping
* Bump changeset from patch to minor for serialization refactor
Return types of public get*Reducers/get*Revivers functions narrowed
from Reducers/Revivers to Partial<Reducers>/Partial<Revivers>, which
is a TypeScript-level breaking change. Also adds new sub-path exports
(@workflow/core/serialization/workflow, workflow/internal/serialization)
which is additive. Minor bump is the appropriate semver for both.
* Remove unused workflow/internal/serialization re-export and @workflow/core/serialization/workflow sub-path
Both exports had zero consumers in the repo. The workflow/internal/serialization
export was previously removed on main in #1082 for the same reason. The
modular workflow.serialize/deserialize is still reachable via
@workflow/core/serialization when needed. These exports can be reintroduced
by the snapshot runtime branch if/when it actually needs them.
Also updates the changeset to drop the 'new sub-path exports' bullet.
* Downgrade changeset from minor to patch
After auditing actual consumers of the narrowed return types
(getExternalReducers/getWorkflowReducers/getExternalRevivers/getWorkflowRevivers
now return Partial<Reducers>/Partial<Revivers>), no in-repo or external
consumer indexes specific keys on the returned object in a way that would
break. The only internal caller that did (runtime/run.ts) was updated in
this same PR. The narrowing is type-safer but effectively invisible at
runtime and for idiomatic callers that spread or forward the object.
Since the refactor is internally restructuring only, patch is the
appropriate semver bump.
* Trim serialization-refactor changeset
* Dedup formatSerializationError: import from serialization/errors.ts
The legacy serialization.ts had its own inlined copy of
formatSerializationError. Now that the helper is exported from
serialization/errors.ts (already consumed by workflow.ts/step.ts/client.ts),
import it here too to keep the single source of truth.
|
||
|
|
eb8562ddda |
Version Packages (beta) (#1884)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
aeb495bccc |
[trace viewer] Polish items (#1872)
* wip
* context
* cleanup
* wip
* wip
* Update trace-viewer.tsx
* adjust hovers
* time marker
* tweak
* Update event-list.tsx
* header
* Update utils.ts
* search
* Update timeline.tsx
* ship
* Update timeline.tsx
* Update event-list.tsx
* Create icons.tsx
* Update trace-viewer.tsx
* Update utils.ts
* new trace viewrr
* Update workflow-trace-view.tsx
* Fix: Runtime crash when `trace` is `undefined`: `NewTraceViewerComponent` receives `undefined` (cast as `Trace`) and immediately accesses `trace.spans`, causing "Cannot read properties of undefined".
This commit fixes the issue reported at packages/web-shared/src/components/trace-viewer-new.tsx:28
**Bug Analysis:**
In `trace-viewer-new.tsx`, the `buildTrace` function returns `undefined` when `!run?.runId`. The result is stored in `trace` which has type `TraceWithMeta | undefined`. However, on line 28, `trace` is passed directly to `NewTraceViewerComponent` with a type assertion `trace as Trace`, which silences the TypeScript error but does not prevent the runtime crash.
Inside `NewTraceViewerComponent` (in `new-trace-viewer/trace-viewer.tsx` line 98), the component immediately accesses `trace.spans`:
```tsx
<ActiveSpanProvider spans={trace.spans}>
```
When `trace` is `undefined`, this produces: `TypeError: Cannot read properties of undefined (reading 'spans')`.
This happens whenever the component renders before `run.runId` is available — a normal scenario during initial loading.
The old `WorkflowTraceViewer` component (in `workflow-trace-view.tsx` line 953) correctly handles this with a `if (!trace)` guard that renders a loading skeleton. The new component lacks this guard.
**Fix:**
Added a null guard in `trace-viewer-new.tsx` that checks `if (!trace)` before rendering `NewTraceViewerComponent`. When trace is undefined, a simple loading placeholder is rendered instead. This prevents the crash and follows the same pattern as the existing `WorkflowTraceViewer`. The `as Trace` cast on line 28 is now safe because `trace` is guaranteed to be defined after the guard.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: mitul-s <mitulxshah@gmail.com>
* fixed edge view
* Update timeline.tsx
* ship it
* cleanup
* Update trace-viewer.tsx
* wip
* wip
* Add detail pane to the new trace viewer + cleanup (#1714)
* detail pane
* cleanup
* cleanup
* cleanu
* fix
* Update workflow-trace-view.tsx
* cleanup
* Update entity-detail-panel.tsx
* move sidebar provider into export
* dead code
* cleaning up more
* remove decorative indenting & output loader
* fix bars
* Update inspector-theme.ts
* cleanups
* Update copyable-data-block.tsx
* cleanup
* chonky
* Update timeline.tsx
* bug fxi
* marker lines
* wip
* changes
* Update event-list.tsx
* rounded
* Update event-list.tsx
* middle truncate component
* cleanup
* height updatres
* colour fixes
---------
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
|
||
|
|
8f9ca384ec | Version Packages (beta) (#1868) | ||
|
|
03dfab35e7 | Updated Workflow Trace viewer (#1454) | ||
|
|
9ea125427f |
Decode UTF-8 stream chunks (#1852)
* Decode typed array stream chunks * Render decoded stream bytes with raw view * Render decoded bytes in data inspector * Use generic byte inspector for streams * review feedback: narrow stream-display exports, fix tab a11y, add collapseRefs tests - Remove unused formatStreamChunkForDisplay/sanitizeStreamChunkForDisplay exports; keep only the formatArrayBufferViewForDisplay path actually used by DataInspector. - Replace broken role=tablist/role=tab on the Decoded/Bytes switcher with aria-pressed toggle-button semantics. - Export collapseRefs/isBytesDisplay and add regression tests covering typed-array detection (top-level, nested in object/array/Map/Set, DataView exclusion). * Replace eval with JSON.parse in serialization revive helper (#1848) * Replace eval with JSON.parse in serialization revive helper devalue.stringify() always produces valid JSON — special values (undefined, NaN, Infinity, -0) are encoded as negative integer sentinels. JSON.parse yields the same flattened array form that unflatten() expects, without the eval anti-pattern (VULN-918). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Drop redundant workflow package from changeset Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> * Add e2e test for UTF-8 parseable stream chunks Emits Uint8Array chunks containing multi-byte UTF-8 (Latin Extended, CJK, emoji, RTL Arabic) plus a UTF-8 encoded JSON document, and asserts each chunk round-trips through TextDecoder({ fatal: true }). Exercises the same decode path the web inspector relies on for typed-array stream values. Made-with: Cursor --------- Co-authored-by: Pranay Prakash <pranay.gp@gmail.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
5714c2335a | Version Packages (beta) (#1711) | ||
|
|
173756dc4d |
[docs] Rename workflowdevkit to workflowsdk and useworkflow.dev to workflow-sdk.dev (#1759)
* [docs] Rename workflowdevkit references to workflowsdk * [docs] Rename useworkflow.dev to workflow-sdk.dev * [chore] Add changeset for domain rename * [docs] Revert sitemap rewrite to useworkflow.dev (crawled-sitemap not yet available for new domain) |
||
|
|
ac09f40771 |
feat: add clickable Run reference rendering in observability UI (#1681)
* feat: add clickable Run reference rendering in observability UI
When a serialized Run object appears in step input/output data, it is
now rendered as a clickable purple badge showing the runId. Clicking
navigates to the target run's detail page.
Changes:
- serialization-format.ts: Add RunRef type, isRunRef(), serializedRunToRunRef(),
and 'Run' entry in observabilityRevivers
- data-inspector.tsx: Add RunRefInline component (purple badge), RunClickContext,
collapseRefs() to make refs non-expandable in ObjectInspector
- attribute-panel.tsx: Thread onRunClick prop, wrap in RunClickContext.Provider
- entity-detail-panel.tsx: Thread onRunClick prop
- run-trace-view.tsx: Thread onRunClick prop
- workflow-trace-view.tsx: Thread onRunClick prop, reset selected span on run change
- trace-span-construction.ts: Show step name for builtin steps instead of empty string
- hydration.ts: Re-export RunRef types
- run-detail-view.tsx: Add handleRunRefClick that navigates to /run/{targetRunId}
* fix: guard collapseRefs against class instances and memoize result
Only recurse into plain objects (prototype is Object.prototype or null)
to avoid stripping class instances like Date, Error, Map, etc. that
have their own rendering in NodeRenderer. Also memoize the collapsed
result to avoid recomputing on every render.
* fix: detect Run instances in Instance reviver instead of fake Run serde key
The Run class goes through the standard Instance serialization pipeline
(WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE), not a dedicated 'Run' key.
Move the RunRef detection into serializedInstanceToRef() which checks
if the className is 'Run' and the data contains a runId string, then
returns a RunRef instead of a generic ClassInstanceRef.
* fix: use serializedInstanceToRef in web and CLI Instance revivers
Both the web and CLI hydration layers override the observabilityRevivers
Instance handler with their own implementation (for react-inspector
named constructors and CLI inspect.custom respectively), bypassing the
RunRef detection in serializedInstanceToRef. Fix by calling
serializedInstanceToRef first and returning a RunRef when detected.
|
||
|
|
3eb5034dd8 |
[web-shared] Enable in-line decryption for data inspector in detail panel (#1722)
* decryption flow for detail panel data inspector * decryption flow for detail panel data inspector * update spinner |
||
|
|
df115fde8c |
[web][web-shared][core] Make encrypted markers clickable and detect encryption before span selection (#1716)
* Make encrypted markers clickable to trigger decryption and detect encryption at run level before span selection * Make encrypted markers clickable to trigger decryption and detect encryption at run level before span selection * Make encrypted markers clickable to trigger decryption and detect encryption at run level before span selection * Make encrypted markers clickable to trigger decryption and detect encryption at run level before span selection * Make encrypted markers clickable to trigger decryption and detect encryption at run level before span selection |
||
|
|
223e9cacb1 |
Version Packages (beta) (#1655)
Co-authored-by: workflow-devkit-release-bot[bot] <workflow-devkit-release-bot[bot]@users.noreply.github.com> |
||
|
|
38a642b4be |
[web-shared]: fix event payload loading (e.g. step_created) (#1685)
* Fix event loading issue for step_created event * Update tired-spiders-rhyme.md Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> * add isSystem to attribute panel * remove isSystem * remove hardcoding event data ref fields * fix stale closure --------- Signed-off-by: Karthik Kalyan <105607645+karthikscale3@users.noreply.github.com> |
||
|
|
a261b2128b | Version Packages (beta) (#1635) | ||
|
|
44a18048a5 |
Reset package versions to 4.0.0 so changesets produces 5.0.0-beta.0 (#1648)
The previous pre-release versions (4.x.y-beta.N) caused two issues:
- semver.inc('4.0.0-beta.N', 'major') returns 4.0.0, not 5.0.0
- Pre-release numbers carried over (beta.61 -> beta.62 instead of beta.0)
Setting all versions to 4.0.0 (non-pre-release) ensures a clean major
bump to 5.0.0-beta.0. Also removes @workflow/swc-playground-wasm from
the changeset and pre.json since it is a private package.
|