## Summary
- Adds `World.getEncryptionKeyForRun(run)` returning `Uint8Array | undefined` as the interface for retrieving per-run encryption keys
- Updates all 8 dehydrate/hydrate serialization functions to accept `key: Uint8Array | undefined`
- Updates runtime callers, CLI, and tests to thread the key parameter through
## Summary
- Makes all 8 dehydrate/hydrate serialization functions in `@workflow/core` async (returning `Promise<...>`)
- Updates all call sites across runtime, CLI, and test code to `await` the results
- Pure mechanical refactor — no functional changes, prerequisite for encryption support
Part 1 of the end-to-end encryption PR stack.
Move `x-workflow-run-id` and `x-workflow-step-id` header injection into
`world-vercel` queue implementation so callers don't need to set them
manually. The headers are extracted from the queue payload itself.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The CLI's `cancelRun` in `packages/cli/src/lib/inspect/run.ts` sent `{ eventType: 'run_cancelled' }` without `specVersion`, causing the Vercel API's `/v2/runs/{id}/events` endpoint to reject the request with a 400 error. The core runtime's `cancelRun` already handled this correctly, so the fix removes the redundant CLI wrapper and calls the core implementation directly.
The bug was introduced in 4966b72 ("implement event-sourced architecture (#621)") which changed the CLI cancel from `world.runs.cancel(runId)` to `world.events.create(runId, ...)` but omitted `specVersion` from the payload. The same commit correctly included it in `Run#cancel()` in core — it was simply missed in the CLI path. A later commit (86a7930) added v1Compat branching but still never added specVersion. The final merged state in a2b688d (PR #894) still omits it.
Also adds a CLI cancel e2e test that starts a long-running workflow, cancels it via the CLI command, and verifies the run status.
* Handle 409 conflict when completing waits that were already completed
When multiple concurrent workflow invocations race to complete the same
wait, the server returns 409 (conflict) for duplicates. This change
handles the 409 gracefully in both runtime.ts (sleep elapsed check) and
runs.ts (wakeUpRun), preventing crashes and treating already-completed
waits as successful.
Also updates event-sourcing docs to reflect that waits are now
materialized as entities in storage with atomic completion guarantees.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Point e2e tests at workflow-server preview for wait materialization branch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Materialize waits as entities in local and postgres worlds to prevent duplicate wait_completed events
Adds Wait type/schema to the shared world package and implements wait entity
materialization in both local (filesystem) and postgres world implementations,
matching the DynamoDB behavior. wait_created creates a wait entity with status
'waiting', and wait_completed transitions it to 'completed' with guards that
reject duplicates (409).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix waitId to use composite key for consistency with postgres world
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Clean up wait entities on terminal run states and register waits migration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Clear WORKFLOW_SERVER_URL_OVERRIDE for merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update changeset for all affected packages and restore server URL override
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Clear WORKFLOW_SERVER_URL_OVERRIDE now that server PR is merged
The server-side changes (vercel/workflow-server#265) have been merged to main
and deployed to production, so we no longer need to point at the preview URL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Remove "workflow/internal/serialization" export
Was only being used in these two e2e test files and the data being passed in those tests don't rely on any specialized data types, so just use JSON there.
* Fix: Removing the `workflow/internal/serialization` export from package.json breaks 5 files in `packages/world-testing` that still import `hydrateWorkflowReturnValue` from that path.
Co-authored-by: TooTallNate <n@n8.io>
* lockfile
---------
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Per TooTallNate's review on #1055: the setupWorkflowContext helper
has a circular reference where ctx.onWorkflowError is accessed via
closure before it's defined on the object. Add a clarifying comment.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(core): detect and fatal error on orphaned/invalid events in EventsConsumer
When an event log has duplicate or invalid events (e.g., 2 wait_completed for a single wait_created), the EventsConsumer gets stuck: the orphaned event has no callback to consume it, so eventIndex never advances, blocking all subsequent events and hanging the workflow forever. This adds deferred orphaned event detection that raises a WorkflowRuntimeError instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add corrupted-event-log error slug and docs page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add changeset for orphaned event detection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* address PR feedback on docs and JSDoc
- Add JSDoc to EventsConsumer constructor's onUnconsumedEvent parameter
- Rewrite docs to clarify this is a SDK/server bug, not user code
- Remove cancel/programmatic retry advice; run is already marked failed
- Emphasize error is uncatchable inside workflow code
- Add steps: upgrade package, retry via dashboard/CLI, report issue on GitHub
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* address PR review: structured options, required callback, reorder docs
- Change EventsConsumer constructor to use structured EventsConsumerOptions object
- Make onUnconsumedEvent required (all callsites provide it)
- Move corrupted-event-log card to last position in errors index
- Update all callsites in tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix docs code sample to pass type-checking
Use correct imports (getRun from workflow/api) and declare variables
to satisfy the docs-typecheck CI job.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* add e2e test for parallel sleep (Promise.all with 10x sleep)
Adds parallelSleepWorkflow that does Promise.all with 10 concurrent
sleep('1s') calls, and an e2e test asserting it completes or fails
within a reasonable time (not hanging).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix parallelSleep e2e test: await returnValue instead of checking status
run.status returns immediately and the workflow is still 'running'.
Use run.returnValue which polls until completion.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix parallelSleep e2e: assert parallel completion under 10s
The test verifies 10 concurrent sleep('1s') calls complete in parallel
(~1s) rather than serially (10s) or hanging indefinitely. 30s timeout
is sufficient since the workflow should finish in ~1-2s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Replaces the Chrome DevTools color scheme with one inspired by
Node.js util.inspect() ANSI stylizing: green strings, yellow
numbers/booleans, grey undefined, magenta dates, cyan functions,
and neutral property names. Adds an extended theme context to
color Date instances (react-inspector has no built-in theme key
for dates). Removes color-hash dependency which is no longer used.
Consolidate all ObjectInspector usage into a single DataInspector component
with shared theming, custom nodeRenderer for StreamRef/ClassInstanceRef, and
stream click context. Update all consumers (attribute-panel, events-list,
event-list-view, stream-viewer, conversation-view) to use DataInspector
instead of direct ObjectInspector or JSON.stringify. Fix class instance
hydration to create objects with dynamically-named constructors so
react-inspector shows proper class names via constructor.name.
* Use `@vercel/cli-auth` for auth token reading and OAuth refresh
Replace the manual auth.json reading logic in the CLI with the `@vercel/cli-auth` package, which handles credential storage via CredentialsStore and OAuth token refresh via the OAuth client.
Previously, the CLI would read the token from disk but had no refresh logic — if the token was expired, API calls would fail.
Now, getAuthToken() checks token expiry and automatically refreshes it using the stored refresh token before returning it.
* Remove dead logging
* [workflow o11y] rebase on latest main and keep targeted UI/builders changes
Rebase the branch intent onto latest main by preserving web-shared UI refactors and builders base-builder updates while taking main for hydration and data-fetching behavior elsewhere.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [workflow o11y] align web-shared hydration revivers with main
Revert the hydration reviver delta for URL, URLSearchParams, and Headers so web-shared matches main behavior while keeping the targeted UI/builders-only scope on this branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [workflow o11y] restore PR #1017 detail-panel decoupling
Bring the web-shared trace/detail panel files back in sync with main so PR #1017 behavior is preserved and not regressed on this branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [workflow o11y] restore PR #1018 react-inspector sidebar updates
Bring web-shared o11y rendering files back in sync with main so ObjectInspector-based sidebar rendering and related UI behavior from PR #1018 remain intact on this branch.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [workflow o11y] keep custom viewers and apply inspector rendering
Preserve the branch-specific event list and stream viewer UX while applying react-inspector rendering to payload/chunk data so complex hydrated values render correctly without reverting custom UI behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* [workflow o11y] trace viewer UX improvements and Geist alignment
- Add live tick animation, context menu, and cancel run support from PR #984
- Decouple side panel styling to use Geist design tokens (inline styles)
- Fix sleep span detail panel showing events instead of wait entity attributes
- Fix stream viewer flickering by removing unstable object deps
- Remove Chunks/Output toggle from stream viewer, show only chunks
- Add resolve hook modal, wake-up sleep, and cancel run plumbing
- Add loading skeleton for stream viewer
Co-authored-by: Cursor <cursoragent@cursor.com>
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
* Bug fixes
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix benchmark history continuity across metric name rename
The concurrent step benchmarks were renamed in beta.53 (dropping the
"stress test: " prefix), which broke the history API's exact-name lookup
against older gh-pages data. Add legacy alias fallbacks so charts show
continuous history across the rename boundary.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Paginate GitHub API calls and use generic legacy name fallback
Three fixes:
- Paginate gh-pages commits (was fetching 100 of 216, missing most history)
- Paginate tags (workflow@ tags were cut off beyond page 1)
- Paginate main branch commits for commits mode
- Replace hardcoded LEGACY_METRIC_ALIASES with generic "stress test: "
prefix fallback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Cache benchmark history with unstable_cache and aggressive CDN TTLs
The snapshot map (200+ GitHub API fetches) is now cached via
unstable_cache for 1 hour, shared across all parameter combinations.
Tags are cached separately. Individual fetch calls use force-cache
with long revalidation periods since historical data is immutable.
CDN cache bumped from 5min to 1h with 24h stale-while-revalidate,
matching how infrequently benchmark data changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* Retry 5xx errors from workflow-server in step handler
Add `withServerErrorRetry` helper that retries world calls on 5xx errors
with exponential backoff (500ms, 1s, 2s ≈ 3.5s total). Applied to all
`world.events.create` calls in the step handler so transient
workflow-server errors don't consume step attempts.
If retries are exhausted, the error is thrown to the queue for
higher-level retry without burning a step attempt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Address PR review comments on 5xx retry handling
- Fix misleading `maxAttempts` log field to `maxRetries` in withServerErrorRetry
- Update step-handler comment to accurately note that queue retries may
still consume step attempts since step_started has already incremented
- Add unit tests for withServerErrorRetry (7 tests covering success,
retry/backoff, exhaustion, and non-5xx passthrough)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Add tests for 429/5xx retry handling
Unit tests for withThrottleRetry and withServerErrorRetry helpers, plus
an e2e test that exercises the 5xx retry codepath during step execution
via run-scoped fault injection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Wrap VQS errors in WorkflowAPIError and add retry to queueMessage
VQS throws its own error types (InternalServerError, ConsumerDiscoveryError,
ConsumerRegistryNotConfiguredError) that don't match WorkflowAPIError.is().
Wrapping them at the world-vercel boundary enables withServerErrorRetry in
queueMessage() to automatically retry transient queue failures.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Update changeset to include @workflow/world-vercel
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Remove queue retry logic per review feedback
Queue retrying will be handled natively by the @vercel/queue client
instead. Reverts VQS error wrapping and withServerErrorRetry in
queueMessage().
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Replace the custom `JSON.stringify`-based data renderer with [`react-inspector`](https://www.npmjs.com/package/react-inspector)'s `ObjectInspector` component for proper display of all JavaScript types in the o11y sidebar panel.
## Before
Data was rendered via `JSON.stringify(value, null, 2)` in a `<pre>` block. Non-plain-object types like `Map`, `Set`, `URLSearchParams`, `Date`, `Error`, `RegExp`, and typed arrays all rendered as `{}` or lost their type information.
## After
Uses `react-inspector` (the same rendering engine as Chrome DevTools) to display data as expandable tree views with proper type labels:
- `URLSearchParams {size: 12}` with entries visible
- `Map(3)` / `Set(5)` with contents
- `Date "2026-02-11T..."`
- `Error {message: "...", stack: "..."}`
- `Uint8Array(32)` with byte values
## Implementation
- Replace `JsonBlock` function with `DataInspector` component using `ObjectInspector`
- Custom theme with transparent background and design system CSS variables (`--ds-gray-*`, `--font-mono`)
- Dark/light mode support via `useDarkMode` hook
- `StreamRef` and `ClassInstanceRef` objects still render as custom components (clickable stream links and styled class cards)
- Expand level defaults to 2 for a good overview without overwhelming
## Dependencies
- Added `react-inspector@9` to `@workflow/web-shared`
## Summary
Render the `EntityDetailPanel` (step/hook/event detail sidebar) outside the trace viewer's `TraceViewerContextProvider`, so hydrated data no longer passes through the web worker's `postMessage` structured clone boundary.
## Problem
The trace viewer uses a web worker for span layout calculations, sending data via `postMessage` which requires structured cloning. After moving hydration client-side, the hydrated data (containing `URLSearchParams`, `Headers`, and other non-structurally-cloneable types) was embedded in trace span attributes and sent to the worker, causing `DOMException: URLSearchParams object could not be cloned`.
## Solution
Render the detail panel as a **sibling** to the trace timeline instead of nested inside the context:
```
Before: After:
TraceViewerContext [flex row]
└─ Timeline ├─ TraceViewerContext
└─ Panel (inside worker) │ └─ Timeline (worker)
└─ Panel (direct props)
```
- `WorkflowTraceViewer`: flex layout with timeline + panel as siblings
- `SelectionBridge`: lightweight component inside context that reads the selected span and passes it up via callback
- `EntityDetailPanel`: no longer imports `useTraceViewer()`, receives selected span info via `selectedSpan` prop
- Panel has independent width/resize state (no longer coupled to the trace viewer context)
- Strip `input`/`output`/`error` from trace span attributes to keep worker data lightweight
## Also fixes
- Stale `customPanelComponent` bug — the panel was stored in context state during initialization and never updated, causing data to be lost on re-renders
## Summary
Split the serialization/deserialization logic into environment-specific layers so data hydration can happen client-side in the browser. This is a prerequisite for e2e encryption where decryption keys are only available in the browser.
## Architecture
### Layer 1: `@workflow/core/serialization-format` (new, browser-safe)
- Format prefix encoding/decoding (`devl`, future `encr`, etc.)
- Generic `hydrateData()` dispatch — handles Uint8Array (v2 binary), legacy arrays (v1), and plain values
- `hydrateResourceIO(resource, revivers)` resource-type dispatcher (step/hook/event/workflow field mapping)
- `ClassInstanceRef` (plain data class, no `node:util` dependency)
- `StreamRef`, type guards (`isStreamRef`, `isStreamId`, `isClassInstanceRef`), utility functions (`extractStreamIds`, `truncateId`)
- Shared `observabilityRevivers` for stream/class/step display overrides
- 36 unit tests covering all of the above
### Layer 2: Environment-specific revivers
- **`@workflow/web-shared`** (`lib/hydration.ts`) — browser-safe revivers using `atob()` for base64, real `URLSearchParams`/`Headers`/`URL` instances, `ClassInstanceRef` for UI rendering
- **`@workflow/cli`** (`lib/inspect/hydration.ts`) — Node.js revivers using `Buffer.from()` for base64, `CLIClassInstanceRef` with `util.inspect.custom` for CLI output
Each module exports a pre-bound `hydrateResourceIO(resource)` that uses its environment's revivers.
### Removed: `@workflow/core/observability`
- Deleted `observability.ts` and `observability.test.ts` entirely (no remaining consumers)
- Removed `"./observability"` export from `@workflow/core/package.json`
- Removed the `workflow` package's `internal/observability.ts` re-export
- All functionality has been split between `serialization-format.ts` (shared types/utilities) and the environment-specific hydration modules
## Web package changes
- Server passes raw world data through without hydration (CBOR preserves `Uint8Array`)
- Client calls `hydrateResourceIO` from `@workflow/web-shared` after receiving CBOR-decoded data
- No Vite `node:*` stubs needed since `@workflow/core/serialization-format` is browser-safe
- Optimized: event hydration finds the matching event before hydrating (instead of hydrating all)
- Reduced server log noise from handled API errors (4xx errors no longer logged)
## Packages affected
- `@workflow/core` — new `serialization-format` export (with tests), removed `observability` export
- `@workflow/web-shared` — new `lib/hydration.ts` with browser-safe revivers
- `@workflow/cli` — new `lib/inspect/hydration.ts` with Node.js revivers, updated `output.ts` import
- `@workflow/web` — client-side hydration, removed server-side hydration
- `workflow` — removed `internal/observability.ts` re-export
## Summary
- Replace Next.js App Router with React Router v7.13.0 framework mode (Vite-based), eliminating the large `next` dependency from the web, CLI, and workflow metapackages
- Serve the web UI in-process from the CLI via Express instead of spawning `next start` as a child process
- Switch RPC transport from JSON to CBOR to preserve binary data types across the wire
- Replace `nuqs` URL state management with React Router's `useSearchParams`
- Replace Next.js server actions with an RPC resource route (`/api/rpc`) and a thin CBOR-based client
## Motivation
The `next` package is ~300MB installed and was the single largest dependency in the monorepo. It also required spawning a separate child process from the CLI to run the o11y web server, adding complexity around process lifecycle management, port readiness polling, and environment variable forwarding.
With React Router framework mode, the web package builds to a standard Express-compatible server bundle that the CLI can import and serve directly in its own process.
## What changed
**Framework swap (`@workflow/web`):**
- `next.config.ts` / `postcss.config.mjs` → `react-router.config.ts` / `vite.config.ts`
- `src/` directory → `app/` directory (React Router convention)
- `src/app/layout.tsx` + `layout-client.tsx` → `app/root.tsx`
- `src/app/page.tsx` → `app/routes/home.tsx`
- `src/app/run/[runId]/page.tsx` → `app/routes/run-detail.tsx`
- Path alias `@/` → `~/`
- Removed all `'use client'` / `'use server'` directives
**Data transport:**
- Server actions → RPC resource route at `/api/rpc` with CBOR encoding
- CBOR preserves `Uint8Array` and other binary types natively (no base64 overhead)
- Stream reading → dedicated `/api/stream/:streamId` resource route
**URL state:**
- `nuqs` (`useQueryState`) → `useSearchParams` from `react-router`
**Fonts:**
- `next/font/google` → Geist `.woff2` files referenced directly from `node_modules/geist` via `@font-face` in CSS
**CLI integration (`@workflow/cli`):**
- `import('@workflow/web/server').then(m => m.startServer(port))`
- No child process, no readiness polling, no cleanup handlers
**Radix UI compatibility:**
- `onSubmit` preventDefault on `AlertDialogContent` and `SheetContent` to prevent Radix's internal `<form method="dialog">` from triggering React Router route actions
- Catch-all action on root route for any stray POSTs
## Dependencies removed
- `next`, `swr`, `nuqs`, `@tailwindcss/postcss`
## Dependencies added
- `react-router` / `@react-router/dev` / `@react-router/node` / `@react-router/express` (all `7.13.0`)
- `express`, `vite`, `@tailwindcss/vite`, `cbor-x`, `isbot`, `cross-env`
- `geist` (devDep)
Stream chunks were serialized as raw devalue text without the 'devl'
format prefix, unlike step inputs/outputs. This meant stream data
couldn't go through the same hydration/deserialization pipeline.
Core changes (serialization.ts):
- getSerializeStream: each chunk is now individually wrapped with
encodeWithFormatPrefix('devl', payload) — same as step data
- getDeserializeStream: handles both format-prefixed chunks (current)
and legacy newline-delimited text (backwards compatible)
World-local changes (streamer.ts):
- Stream chunk files now use .bin extension instead of .json, since
the content is binary (1-byte eof flag + serialized chunk data)
- Add listFilesByExtension() utility to fs.ts for generic file listing
* chore: add changeset
* world-local: add backwards compat for legacy .json chunk files
When reading stream chunks, list both .bin (current) and .json
(legacy) files so streams written before the extension change
continue to work.
* core: add format prefix and length-prefix framing to stream chunks
Stream chunks were serialized as raw devalue text without the 'devl'
format prefix or any framing, unlike step inputs/outputs.
Core changes (serialization.ts):
- getSerializeStream: each chunk is now wrapped with
encodeWithFormatPrefix('devl', payload) inside a length-prefixed
frame: [4-byte big-endian length][format-prefixed payload]
- getDeserializeStream: handles both length-prefixed framed chunks
(current) and legacy newline-delimited text (backwards compatible)
- Added 6 unit tests covering framing, round-trip, concatenation,
splitting, and legacy fallback
World-local changes (streamer.ts):
- Stream chunk files now use .bin extension instead of .json
- readFromStream lists both .bin and .json files for backwards compat
- Updated streamer.test.ts to expect .bin extension
Split changesets: core (format + framing) and world-local (.bin extension)