Commit Graph

757 Commits

Author SHA1 Message Date
Vercel Release Bot 9c3213926f Version Packages (beta) (#1325)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@4.2.0-beta.69
2026-03-11 17:57:07 -07:00
Karthik Kalyan c4d86fac3d [web-shared] Improve workflow observability event list UX (#1337)
* Fix connector line showing for run events

* Improve row toggle

* improve error reporting

* add sorting for events

* add changeset

* update search to be best match
2026-03-11 16:51:09 -07:00
Pranay Prakash fb5a500ead feat: add exists getter to Run class (#1336)
* feat: add `exists` getter to `Run` class

Allow checking if a workflow run exists without catching errors.
Returns `true` if the run is found, `false` for WorkflowRunNotFoundError,
and re-throws any other errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* address PR feedback: use resolveData: 'none' in exists getter, fix docs wording

Pass { resolveData: 'none' } to world.runs.get() in the exists getter
to skip fetching input/output payloads. Update docs to say "without
throwing when the run is not found" instead of "without throwing an error".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-11 16:19:33 -07:00
Pranay Prakash 6d45c8ec36 Support client-side tools in DurableAgent (#1329)
* Support client-side tools in DurableAgent

Tools without an `execute` function now pause the agent loop and return
`clientToolCalls` in the result, enabling human-in-the-loop and
client-side tool execution patterns. Fixes #847.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Match AI SDK result shape for client-side tools

Use `toolCalls` and `toolResults` on DurableAgentStreamResult to match
the AI SDK's GenerateTextResult convention. Consumers find unresolved
client-side tool calls by diffing toolCalls vs toolResults — the same
pattern used with useChat/addToolOutput.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR review feedback

- Use iterMessages (not options.messages) so returned messages include
  the assistant tool-call message, enabling callers to resume
- Add resolved server/provider tool results to returned messages as a
  tool role message so callers don't re-execute when resuming
- Treat missing tools (!tool) as errors (will throw in executeTool),
  only tools that exist but lack execute are client-side
- Track tool-output-available chunks in allUIChunks when
  collectUIMessages is enabled so uiMessages is complete
- Add message assertions to tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clean up type casts in client-side tools path

Push directly to iterMessages (LanguageModelV2Prompt) instead of
double-casting through ModelMessage[]. Single cast at the return
point matches the existing pattern at the end of stream().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Reverse check in resolveProviderToolResult for early exit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-11 16:18:00 -07:00
Nathan Colosimo 3648109861 [world-postgres] [world-local] Execute Graphile jobs directly instead of defering to world-local queue (#1334)
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-03-11 16:11:07 -07:00
Nathan Rajlich 5040263bf9 Merge CLAUDE.md into AGENTS.md and symlink CLAUDE.md (#1326)
* Merge CLAUDE.md into AGENTS.md and symlink CLAUDE.md

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 12:56:25 -07:00
Peter Wielander d5ae817863 [web] Polish loading indicators (#1327) 2026-03-11 12:14:17 -07:00
Nathan Rajlich a10f699ec9 Fix flaky webhookWorkflow e2e test by polling instead of fixed sleep (#1328)
* Fix flaky webhookWorkflow e2e test by polling instead of fixed sleep

* Address review: fail fast on >3 hooks, use sleep import
2026-03-11 19:04:34 +00:00
Nathan Rajlich 825417acba feat: support deploymentId: 'latest' in start() to resolve most recent deployment (#1317)
* feat: support `deploymentId: 'latest'` in `start()` to resolve most recent deployment

Add resolveLatestDeploymentId() to the World interface and implement it
in world-vercel, which calls the Vercel API to resolve the latest
deployment for the current environment. When start() receives
deploymentId: 'latest', it invokes this method and replaces the sentinel
with the resolved deployment ID before passing it to event creation,
queue dispatch, and encryption key derivation.

* fix: address PR review feedback

- Use encodeURIComponent() for deployment ID in API URL path
- Avoid mutating caller-provided opts; pass resolved deploymentId via
  spread into getEncryptionKeyForRun context instead
- Add test asserting getEncryptionKeyForRun receives resolved ID
- Add test for OIDC token success path in resolve-latest-deployment
2026-03-11 18:20:40 +00:00
Nathan Colosimo d72c82220f Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation
* first pass

* fix failing test

* Update changeset to fix SWC compiler issue

Fix bug where the SWC compiler bug prunes step-only imports in the client-mode transformation

Signed-off-by: Nathan Rajlich <n@n8.io>

* DCO Remediation Commit for nathancolosimo <nathancolosimo@gmail.com>

I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 150d175e7d
I, nathancolosimo <nathancolosimo@gmail.com>, hereby add my Signed-off-by to this commit: 429747fcc3

Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Signed-off-by: nathancolosimo <nathancolosimo@gmail.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-11 09:11:19 -07:00
Peter Wielander d5bc418816 [web] [world-vercel] Ensure user-passed run IDs are URL encoded and call out self-hosted security (#1322) 2026-03-11 09:04:01 -07:00
Vercel Release Bot 5f4395de7b Version Packages (beta) (#1306) 2026-03-10 19:25:18 -07:00
Nathan Rajlich 54758f30ab Remove hard-coded VERCEL_DEPLOYMENT_KEY from nextjs-turbopack workbench (#1319)
Vercel now automatically sets VERCEL_DEPLOYMENT_KEY to a unique value
for every deployment, so the hard-coded value is no longer necessary.
2026-03-11 01:42:51 +00:00
Nathan Rajlich aa2f581b48 fix(web): move react-router deps to devDependencies (#1265)
* fix(web): move react-router deps to devDependencies

Add a custom entry.server.tsx (based on the default react-router template)
so that the @react-router/dev build plugin no longer requires
@react-router/node and isbot to be in the dependencies field of
package.json. This allows all react-router related packages to be
devDependencies since they are fully bundled at build time, leaving
express as the sole production dependency.

* Update packages/web/app/entry.server.tsx

Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
2026-03-11 01:42:40 +00:00
Nathan Rajlich b28a5fd0be fix(ai): use workspace:* for workflow peer dependency (#1320)
* fix(ai): use workspace:* for workflow peer dependency

The hardcoded version range in peerDependencies would fall out of sync
with the actual workflow package version, causing changeset warnings.
Using workspace:* lets pnpm resolve the correct version at publish time.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Nathan Rajlich <n@n8.io>

* add changeset for @workflow/ai peer dependency fix

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-11 01:39:40 +00:00
Nathan Rajlich 854a25f910 fix(core): pass resolved deploymentId to getEncryptionKeyForRun in start() (#1318)
The start() function resolved deploymentId from world.getDeploymentId() but
then passed the original opts (without the resolved value) to
getEncryptionKeyForRun. In external contexts (e2e tests, CLI) where
deploymentId is inferred from the environment rather than explicitly provided,
this caused getEncryptionKeyForRun to receive undefined deploymentId and
silently skip encryption for the initial workflow input.
2026-03-10 23:10:55 +00:00
Nathan Rajlich d842ce1c43 fix: surface 429 rate-limit errors in e2e tests and CLI (#1309)
Multiple layered issues caused encryption key 429 errors to produce
confusing, unrelated-looking test failures:

- awaitCommand() in e2e utils now rejects on non-zero exit codes
  instead of silently resolving (the most critical fix)
- cliInspectJson/cliHealthJson throw on empty stdout instead of
  falling back to '{}'
- maybeDecryptFields re-throws HTTP errors instead of silently
  falling back to encrypted placeholders
- fetchRunKey includes response body and status text in errors
- Added 429 to CLI status text map
2026-03-10 00:23:05 -07:00
Nathan Rajlich 4a6ddd82c0 fix(world-local): return HTTP 200 instead of 503 for queue timeout re-enqueue signals (#1307)
* fix(world-local): return HTTP 200 instead of 503 for queue timeout re-enqueue signals

The local queue used HTTP 503 (Service Unavailable) as an internal signal
for re-enqueueing after a timeout delay. This is misleading since 503 is
an error status, causing confusion when inspecting logs or network traffic.

Changed to return 200 with timeoutSeconds in the body, and updated the
consumer to detect the re-enqueue signal from the response body instead
of the status code.

* fix(world-local): handle timeoutSeconds: 0 and add queue re-enqueue tests

Address review feedback: timeoutSeconds === 0 should trigger an immediate
re-enqueue without delay, not be treated as a normal success. Also adds
test coverage for the handler response codes and re-enqueue behavior.
2026-03-10 06:29:18 +00:00
Peter Wielander 887cc2bd55 [web-shared] [cli] Refactor observability data fetching (#1261) 2026-03-10 01:36:03 +00:00
Karthik Kalyan 33101a2292 Move decrypt button to the sidepanel (#1308) 2026-03-09 15:55:43 -07:00
Nathan Rajlich 9781afb490 test: enable encryption on nextjs-turbopack workbench (#1262)
* test: enable encryption on nextjs-turbopack workbench

Set VERCEL_DEPLOYMENT_KEY to enable e2e encryption for the
nextjs-turbopack workbench project. This allows the e2e test
suite to exercise encrypted workflow runs alongside existing
unencrypted runs for backwards compatibility testing.

* fix: fetch encryption key from API in external contexts (e2e, CLI)

Use VERCEL=1 env var to distinguish serverless runtime (can use local
HKDF derivation) from external contexts like e2e test runners, CLI,
and web dashboard (must fetch derived key from Vercel API).

Previously, when VERCEL_DEPLOYMENT_ID matched but VERCEL_DEPLOYMENT_KEY
was absent (e.g., e2e runner has the deployment ID from CI but not the
key), getEncryptionKeyForRun returned undefined — silently skipping
decryption. Now external contexts always use the API fetch path.

* fix: add 429 retry with exponential backoff to fetchRunKey

The e2e runner calls fetchRunKey for every run, which can exceed the
API rate limit (30/min per team) when many tests run in parallel.
Add retry logic with exponential backoff + jitter (up to 5 retries)
and respect the Retry-After header when present.

* Fix: Missing NaN guard on `retry-after` header parsing causes `setTimeout(r, NaN)` to resolve immediately (0ms), triggering rapid-fire retries against a rate-limited API endpoint.

This commit fixes the issue reported at packages/world-vercel/src/encryption.ts:138

**Bug Analysis:**

In `packages/world-vercel/src/encryption.ts`, the `fetchRunKey` function handles HTTP 429 (rate limited) responses by reading the `retry-after` header and computing a delay. The original code was:

```js
const retryAfter = response.headers.get('retry-after');
const delay = retryAfter
  ? Number.parseInt(retryAfter, 10) * 1000
  : BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
```

Per RFC 9110, the `Retry-After` header can contain either a number of seconds OR an HTTP date string (e.g., `"Wed, 21 Oct 2015 07:28:00 GMT"`). When the header contains a date string or any non-numeric value, `Number.parseInt(retryAfter, 10)` returns `NaN`. Since the header string is truthy (non-empty), the ternary takes the parseInt path rather than the exponential backoff fallback. `NaN * 1000` evaluates to `NaN`, and `setTimeout(r, NaN)` resolves immediately (~0ms, confirmed by testing). This causes rapid-fire retries (up to 5 iterations with zero delay) against a rate-limited API endpoint, wasting resources and potentially worsening the rate-limiting situation.

**Fix:**

Parse the integer first, then check if the result is NaN before using it. If NaN, fall back to exponential backoff with jitter. This matches the established pattern already used in `packages/world-vercel/src/utils.ts` (lines 309-315) where the same header is properly guarded with `!Number.isNaN(parsed)`.

```js
const retryAfterHeader = response.headers.get('retry-after');
const parsedRetryAfter = retryAfterHeader
  ? Number.parseInt(retryAfterHeader, 10)
  : NaN;
const delay = !Number.isNaN(parsedRetryAfter)
  ? parsedRetryAfter * 1000
  : BASE_DELAY_MS * 2 ** attempt + Math.random() * 500;
```

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: TooTallNate <n@n8.io>

* fix: add --decrypt to e2e CLI inspect calls

With encryption enabled on the nextjs-turbopack workbench, CLI inspect
output shows encrypted data as placeholders. The e2e tests check for
actual output values, so they need --decrypt to see the real data.

* fix: remove manual 429 retry, rely on undici RetryAgent; add changeset

The shared RetryAgent from getDispatcher() already handles 429/5xx
retries with exponential backoff and Retry-After header support.
The manual retry loop in fetchRunKey was redundant.

Also add changeset for the VERCEL=1 external context fix.

* refactor: use @ts-expect-error on dispatcher instead of as any cast

---------

Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
2026-03-09 14:00:25 -07:00
Nathan Rajlich 83dbd46456 Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing (#1304)
* Stop reading WORKFLOW_VERCEL_* env vars at runtime to prevent unintended proxy routing

createWorld() in core no longer reads WORKFLOW_VERCEL_PROJECT, WORKFLOW_VERCEL_TEAM,
WORKFLOW_VERCEL_AUTH_TOKEN, WORKFLOW_VERCEL_ENV, or WORKFLOW_VERCEL_PROJECT_NAME from
process.env. These env vars are intended for CLI/observability tooling only, and
reading them at runtime caused all workflow traffic to route through the
api.vercel.com proxy when users mistakenly set them as Vercel project env vars.

The Vercel runtime already provides everything needed: OIDC tokens for auth,
VERCEL_PROJECT_ID for encryption context, and VERCEL_DEPLOYMENT_ID for world
selection. createWorld() now calls createVercelWorld() with no config.

A warning is emitted if the env vars are detected at runtime, telling users to
remove them.

The CLI and e2e tests are updated to call createVercelWorld() directly with an
explicit config object and inject via setWorld(), keeping the WORKFLOW_VERCEL_*
env vars scoped to tooling contexts only.

* Refactor inferVercelEnvVars to return config instead of relying on process.env

inferVercelEnvVars() now returns a VercelEnvVars object with the resolved
config values. setup.ts uses the returned object directly for
createVercelWorld() instead of re-reading from process.env via getEnvVars().

The writeEnvVars() calls inside inferVercelEnvVars are consolidated into a
single call at the end, retained only for the embedded web UI which reads
process.env as a fallback in its server actions. The scattered writeEnvVars
calls after each inference step are removed.

* Address review: consistent e2e gate and expanded warning

- Use WORKFLOW_VERCEL_ENV as the gate in both e2e.test.ts and
  bench.bench.ts for consistency (was WORKFLOW_VERCEL_AUTH_TOKEN in
  e2e.test.ts, WORKFLOW_VERCEL_ENV in bench.bench.ts)
- Expand the misconfiguration warning to also detect
  WORKFLOW_VERCEL_AUTH_TOKEN and WORKFLOW_VERCEL_ENV, listing the
  specific env vars that are set

* Add changeset

Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
2026-03-09 20:46:19 +00:00
Karthik Kalyan c59dd8c571 [web-shared] enrich sleep detail panel and fix attribute capitalization (#1305)
* Fix side panel

* add changeset

* Capitalization fix
2026-03-09 13:04:13 -07:00
Vercel Release Bot 0063180d65 Version Packages (beta) (#1291)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-07 20:33:25 +00:00
Pranay Prakash c71befe8ec fix(core): premature suspension when hooks have buffered payloads with concurrent pending entities (#1294)
* test: reproduce hook+sleep promiseQueue regression

Add failing tests that reproduce a regression from #1246 where the
sleep's WorkflowSuspension fires before all hook payloads are delivered
when a hook and sleep run concurrently.

Root cause: when the null event fires, the sleep queues a suspension
through promiseQueue. After the first hook payload resolves, subsequent
hook payload resolutions are queued AFTER the sleep suspension, causing
the workflow to terminate prematurely via Promise.race.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: expand tests to isolate bug to hook + pending entity pattern

Add control tests proving sequential steps are NOT affected by the
promiseQueue regression, isolating the bug to hooks specifically:

Failing (hook-based):
- hook + sleep: all 3 payloads → step invocation
- hook + sleep: 2 payloads → return
- hook + incomplete step: 2 payloads → return

Passing (step-based controls):
- sleep + sequential steps: both step events exist
- sleep + sequential steps: only 1st step completed
- incomplete step + sequential steps: all step events exist
- hook only (no concurrent entity): payloads + step

The bug is: any entity that queues a suspension through promiseQueue
at null-event time preempts hook payload delivery, because hooks
buffer payloads in payloadsQueue and only resolve them one-at-a-time
as the workflow code iterates. Steps are unaffected because each step
has its own events consumed before null fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): move WorkflowSuspension calls back to setTimeout(0) from promiseQueue

The promiseQueue refactor (#1246) moved ALL promise resolutions through
the queue, including WorkflowSuspension calls. This caused premature
termination when hooks had buffered payloads and a concurrent entity
(sleep or incomplete step) was pending:

1. Null event fires → all subscribers run
2. Sleep/step queues WorkflowSuspension via promiseQueue.then()
3. Hook's next payload resolution is also queued via promiseQueue.then()
4. Suspension fires first → Promise.race terminates workflow
5. Hook payload never delivered → infinite retry loop

Fix: move WorkflowSuspension calls back to setTimeout(0) (macrotask).
Suspensions fire AFTER all microtask-based deliveries (promiseQueue
resolve/reject for step results and hook payloads) have completed.
Non-suspension resolve/reject calls remain on promiseQueue for
deterministic ordering of data delivery.

Affected paths:
- step.ts: null event handler
- sleep.ts: null event handler
- hook.ts: null event handler, eventLogEmpty suspension, dispose suspension

* fix(core): use pendingDeliveries counter + scheduleWhenIdle for suspensions

Replace all nested setTimeout/promiseQueue suspension patterns with a
clean idle-polling mechanism:

1. pendingDeliveries counter: incremented before async hydration (step
   results, hook payloads), decremented in finally block after delivery
2. scheduleWhenIdle(ctx, fn): polls via setTimeout(0) → check counter →
   if > 0, wait for promiseQueue.then() → repeat. Only fires fn when
   pendingDeliveries reaches 0.

This correctly handles:
- Sync deserialization (no encryption): counter is 0, fires immediately
  after first setTimeout(0)
- Async deserialization (with encryption): waits for decryption to
  complete before firing
- Multi-round hook payload delivery: each createHookPromise() call
  increments the counter, preventing premature suspension between
  delivery rounds

Also adds payloadsQueue.length check to hook null handler — don't
trigger suspension if buffered payloads can satisfy pending awaits.

Tests now run in both sync and async modes (14 total = 7 scenarios x 2).
All 164 tests pass.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-07 11:02:08 -08:00
Peter Wielander 5e4ef657cf [web] Refactor event list view data fetching (#1288) 2026-03-06 15:50:47 -08:00
Peter Wielander d8daa2a9a9 [world] Add method to get individual events (#1287) 2026-03-06 14:35:12 -08:00
Nathan Rajlich 36a901d2d2 feat: expose workflow and step names in metadata functions (#1285)
* feat: expose workflow and step names in metadata functions

Add `workflowName` to `WorkflowMetadata` and `stepName` to `StepMetadata`,
making them available via `getWorkflowMetadata()` and `getStepMetadata()`.
This is useful for adding context to structured logs, collating logs during
investigations, and bucketing errors by workflow/step name.

Closes #1103

* add changeset

* Apply suggestion from @TooTallNate

Signed-off-by: Nathan Rajlich <n@n8.io>

---------

Signed-off-by: Nathan Rajlich <n@n8.io>
2026-03-06 21:51:16 +00:00
Vercel Release Bot 58afeb25c2 Version Packages (beta) (#1284)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@4.2.0-beta.66
2026-03-06 13:46:57 -08:00
Pranay Prakash 8b5a388a94 Support timeoutSeconds: 0 for immediate re-enqueue (#1290)
Previously, immediate re-enqueues used `timeoutSeconds: 1` as an
arbitrary minimum delay. This changes the convention so that
`timeoutSeconds: 0` signals an immediate re-enqueue:

- world-local: fix truthiness check so 0 triggers a 503 response
- world-vercel: omit `delaySeconds` from queue options when 0
- core: use `timeoutSeconds: 0` for hook conflict re-enqueues

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 13:18:15 -08:00
Pranay Prakash 15e6bc008c Fix docs typechecker to resolve workflow module types (#1271)
* Fix docs typechecker to actually resolve workflow module types

The docs typechecker was silently failing to resolve `workflow` imports
because:
1. repoRoot was computed 4 levels up instead of 3
2. No path mappings for workspace packages
3. Missing TS lib files for disposable and DOM iterables

This meant all imported symbols became `any` and type errors in docs
code samples went undetected.

Fixes:
- Fix repoRoot: '../../../..' -> '../../..'
- Add path mappings for workflow, @workflow/core, @workflow/ai, etc.
- Add lib.dom.iterable.d.ts and lib.esnext.disposable.d.ts
- Fix regex that ate first line of some code blocks
- Auto-skip error demonstration code blocks
- Add respondWith to global Request for webhook doc patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix docs typecheck failures against updated base branch

- Skip incomplete code samples (with `...`/`/* ... */`) from type checking
- Fix sleep("1 month") → sleep("30d") (valid ms duration)
- Fix run.result() → run.returnValue (correct Run API)
- Add skip-typecheck for docs with intentionally simplified types

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix doc code samples to typecheck instead of skipping them

- hook-conflict.mdx: use error.message.includes() instead of non-existent error.slug
- get-hook-by-token.mdx: cast hook.metadata to expected shape
- ai/index.mdx: fix tool inputSchema to match execute function params
- get-writable.mdx: use ModelMessage[], spread result.messages, use model placeholder
- Add myModel global placeholder for docs examples

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Narrow TS2307 ignore to only non-mapped modules

Instead of blanket-ignoring all "Cannot find module" errors (TS2307),
only ignore them for modules not in our explicit paths mappings.
This ensures workspace package imports (workflow, @workflow/core, etc.)
are validated while still allowing relative imports and framework deps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-06 11:45:40 -08:00
Pranay Prakash aadce97859 Add queue, event, push, subscribe keywords to workflow skill (#1286)
* feat: update SKILL.md keywords for better context matching

Add "queue, event, push, subscribe" to description triggers

Slack-Thread: https://vercel.slack.com/archives/C0ACY4D798Q/p1772785994953489?thread_ts=1772785994.953489&cid=C0ACY4D798Q
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

* feat: update skill version to 1.3 with new trigger keywords

Add trigger keywords: queue, event, push, subscribe.

Slack-Thread: https://vercel.slack.com/archives/C0ACY4D798Q/p1772785994953489?thread_ts=1772785994.953489&cid=C0ACY4D798Q
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>

---------

Co-authored-by: v0 <v0[bot]@users.noreply.github.com>
Co-authored-by: Pranay Prakash <1797812+pranaygp@users.noreply.github.com>
2026-03-06 11:40:10 -08:00
Peter Wielander dff00c9400 [core] Skip port inference in vercel environment (#1283) 2026-03-05 17:08:42 -08:00
Vercel Release Bot 596d105323 Version Packages (beta) (#1279) workflow@4.2.0-beta.65 2026-03-06 00:20:52 +00:00
Peter Wielander 456c1aa455 Add vitest plugin for testing full workflows without setting up a server (#1237) 2026-03-05 16:08:48 -08:00
Nathan Rajlich 97932d3086 fix: thread runId through stream inspection for encryption key resolution (#1277)
CLI showStream:
- Requires --run with --decrypt for encrypted stream decryption
- Warns when --decrypt is used without --run

Web stream reading:
- readStreamServerAction accepts runId parameter for key resolution
- Stream API route reads runId from query param
- readStream client function passes runId to the API route
- useStreamReader hook accepts and passes runId
- run-detail-view passes runId to useStreamReader

Removes getRunIdFromStreamId helper (stream IDs don't always share
the run's ULID, e.g. streams serialized across step/workflow boundaries).
2026-03-05 23:36:36 +00:00
Peter Wielander 1eaff36f19 [web] Don't resolve data on initial run entity fetch (#1278) 2026-03-05 22:02:59 +00:00
Peter Wielander 11dcb646d3 [world-local] [world-postgres] [cli] Validate local run ID and quiet dotenv logs (#1273) 2026-03-05 21:35:23 +00:00
Vercel Release Bot 597afa5174 Version Packages (beta) (#1250)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
workflow@4.2.0-beta.64
2026-03-05 13:01:55 -08:00
Nathan Rajlich 02f706fb99 fix(world-local, world-postgres): default hooks.list() sort order to ascending (#1275)
hooks.list() in world-local and world-postgres defaulted to descending
order, while world-vercel defaults to ascending (creation order). This
caused the webhookWorkflow e2e test to deadlock: hooks were returned
newest-first, so the test sent the first webhook request to the manual-
response hook instead of the default-response hook.

- world-local: default sortOrder to 'asc', fix getCreatedAt returning
  new Date(0) which broke ascending cursor pagination
- world-postgres: respect the sortOrder param (was hardcoded to desc)
  and default to 'asc'
2026-03-05 20:14:33 +00:00
Pranay Prakash adfe8b6b11 Add isWebhook flag to prevent hooks from being resumed via public webhook endpoint (#1270)
* Add isWebhook flag to prevent hooks from being resumed via public webhook endpoint

Hooks created with createHook() are now non-resumable via the public webhook
endpoint by default (isWebhook=false). Only hooks created with createWebhook()
set isWebhook=true, allowing them to be resumed via the public URL.

Also adds HookNotFoundError thrown by all world backends when a webhook
token doesn't match any hook, and an e2e test for the new behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix world-local: default isWebhook to false and fix test assertions

- Default isWebhook to false at write time in events-storage
- Default isWebhook to false at read time in hooks-storage (for old data)
- Update test assertions to match HookNotFoundError message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix: default isWebhook to true for backwards compat, add postgres migration

- Revert read-side default to `isWebhook ?? true` in world-local for
  backwards compatibility with existing hooks that predate the field
- Add postgres migration 0009 to add `is_webhook` column with default true
- Update drizzle schema to match

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 11:04:53 -08:00
Pranay Prakash a7ae7e9a61 Strengthen deterministic VM seed with additional run metadata (#1269)
* Strengthen deterministic VM seed with additional run metadata

Derive the PRNG seed from runId, workflowName, and startedAt instead of
runId alone. This makes the seed harder to predict from any single
piece of publicly exposed run information.

* fix(core): update hardcoded correlation IDs to match strengthened VM seed

---------

Co-authored-by: Nathan Rajlich <n@n8.io>
2026-03-05 11:03:28 -08:00
Pranay Prakash 1926296c67 Manually bump core packages to 4.2.x (#1272) 2026-03-05 10:48:58 -08:00
Pranay Prakash 30e24d441e Merge commit from fork
* Prevent deterministic tokens in createWebhook

Remove the `token` option from `WebhookOptions` to prevent unauthorized
access to the public webhook endpoint. Webhook tokens are now always
randomly generated. Deterministic tokens remain available for
`createHook()` with server-side `resumeHook()`.

**BREAKING CHANGE**: `createWebhook()` no longer accepts a `token` option.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address PR feedback: throw on token, revert unnecessary doc changes, replace examples

- createWebhook() now throws an error if token is passed at runtime
  instead of silently stripping it
- Revert unnecessary changes to create-hook.mdx (only keep
  slack_webhook -> slack_messages prefix change)
- Replace Slack webhook examples in create-webhook.mdx and hooks.mdx
  with generic event collector examples that make sense without
  deterministic tokens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address andresriancho's feedback: stricter token check, add unit test

- Use `token !== undefined` instead of `if (token)` for correctness
  (empty string should also be rejected)
- Add unit test verifying createWebhook({ token: "anything" }) throws
  the expected error message

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* change changeset to minor

* Fix docs typechecker to actually resolve workflow module types

The docs typechecker was silently failing to resolve `workflow` imports
because:
1. repoRoot was computed 4 levels up instead of 3
2. No path mappings for workspace packages
3. Missing TS lib files for disposable and DOM iterables

This meant all imported symbols became `any` and type errors in docs
code samples went undetected.

Fixes:
- Fix repoRoot: '../../../..' -> '../../..'
- Add path mappings for workflow, @workflow/core, @workflow/ai, etc.
- Add lib.dom.iterable.d.ts and lib.esnext.disposable.d.ts
- Fix regex that ate first line of some code blocks
- Auto-skip error demonstration code blocks
- Add respondWith to global Request for webhook doc patterns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "Fix docs typechecker to actually resolve workflow module types"

This reverts commit 02d42ac866e6db1b6d016339739344cdd9538e90.

* chore: trigger CI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-05 10:33:53 -08:00
Nathan Rajlich b68ed630ec fix(cli): read orgId from project entry in repo.json, not root (#1263)
The Vercel CLI's repo.json format puts orgId on each project entry,
not at the root level. The CLI was reading repoConfig.orgId (undefined)
instead of project.orgId, which meant teamId was never set. Without
teamId, the world-vercel URL selector used the direct vercel-workflow.com
URL instead of the api.vercel.com proxy, causing 401 errors because
vercel-workflow.com only accepts OIDC tokens (not CLI auth tokens).

Also fix the RepoProjectConfig/RepoProjectsConfig type definitions
to match the actual repo.json structure.
2026-03-04 23:22:46 +00:00
Nathan Rajlich bbe40ff00a Opt-in decryption for o11y tooling (CLI + web) (#1256)
* Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel

* update changeset

* Move HKDF key derivation server-side: API returns per-run derived key

* Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run

* Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook

* Split changeset into per-package descriptions for world, world-vercel, and core

* Remove unnecessary Uint8Array.from() wrapper around Buffer.from()

* Use zod to parse Vercel API response

* fix: restore world-vercel files to main versions

The rebase incorrectly picked up older versions of these files from
early encryption branch commits. The main versions are correct and
up-to-date.

* fix: add type cast for hydrateStepReturnValue return in hook.ts

* Make decryption an explicit opt-in for o11y tooling

* Restore encrypted data handling in o11y hydration layer

* Use EncryptedDataRef with util.inspect.custom for CLI encrypted data display

* Fix Decrypt button crash: use correct 'refresh' callback from useWorkflowResourceData

* Implement client-side decryption for web o11y with getEncryptionKeyForRun RPC

* Fix CLI decrypt: fetch WorkflowRun for key resolution, cache per runId

* Use named constructor pattern for encrypted data display in web o11y

* Decrypt event data when encryption key is available after Decrypt button click

* Lift encryption key to run-level state, auto-decrypt on fetch, fix field pollution

* Re-load expanded event data when encryption key becomes available

* Consolidate Decrypt to title bar Button, remove sidebar decrypt card

* Add hover tooltip to Decrypt button explaining scope and state

* Show flat Encrypted label for encrypted fields, use Lucide Lock icon in DataInspector

* Render eventData subfields individually to avoid encrypted markers in collapsed preview

* Revert: render eventData subfields individually

* Fix Lock icon vertical alignment in DataInspector encrypted label

* update changeset

* Update CLI, web, and stream callers for CryptoKey: importKey at resolution sites

* Pass teamId to the get-key endpoint

* fix: remove unused DataInspector import in events-list.tsx

* fix: restore world-vercel files to base branch versions

Cherry-pick conflict resolution incorrectly took the older opt-in-decrypt
versions of these files, reverting improvements from main (dispatcher,
createGetEncryptionKeyForRun extraction, nullable key response).

* fix: address PR review feedback

- Remove duplicate AttributePanel/EventsList rendering in entity-detail-panel.tsx.
  Thread encryptionKey into the existing EventsList render instead.
- Restore missing re-exports (isClassInstanceRef, isStreamId, isStreamRef)
  in web-shared/src/index.ts to maintain backwards compatibility.
- Add 'error' to replaceEncryptedWithMarkers field list in web-shared
  hydration.ts to match the decrypt path.
- Extend CLI hydration eventData decrypt/placeholder to cover all known
  serialized fields (output, metadata, payload) not just result/input.
- Add 'error' to CLI replaceEncryptedWithRef field list.
- Remove invalid encryptionKey option from useWorkflowResourceData call
  (hook doesn't support it yet), add TODO.
- Add 4 unit tests for hydrateDataWithKey in serialization-format.test.ts:
  encrypted+key decrypts, encrypted+noKey returns raw, non-encrypted
  hydrates normally, non-Uint8Array legacy data passes through.

* feat: thread encryptionKey through useWorkflowResourceData hook

Instead of leaving a TODO, implement the encryptionKey support directly:
- Add optional encryptionKey to useWorkflowResourceData options
- When key is available, use hydrateResourceIOWithKey (async decrypt)
  instead of hydrateResourceIO for all resource types
- Remove redundant hydrateResourceIO from fetchResourceWithCorrelationId

* fix: address comprehensive review feedback on PR #1256

High priority:
- Gate showStream key fetch on --decrypt flag, warn when --decrypt
  used without --run
- Fix workflow-server-actions.server.ts missing cryptoKey params
  (undefined for both getExternalRevivers and getDeserializeStream)
- Add hydration + decryption to listEvents (was completely missing)
- Fix error/eventData display: check isEncryptedMarker before
  hasDisplayContent so encrypted markers don't silently disappear

Medium priority:
- handleDecrypt: use toast.error() instead of console.error for
  user-visible feedback on key fetch failures
- CLI maybeDecryptFields: add try/catch with graceful fallback to
  encrypted placeholders + warning, also decrypt error field
- use-resource-data: wrap hook/sleep hydrate() in try/catch to
  prevent stuck loading state on decryption errors
- Decrypt button: also check run.error and step input/output for
  encrypted markers, not just run.input/output

Low priority:
- event-list-view: add .catch() to re-load useEffect promise
- Export ENCRYPTED_DISPLAY_NAME from hydration.ts and import in
  data-inspector.tsx instead of raw 'Encrypted' string
2026-03-04 13:17:09 -08:00
Nathan Rajlich 7618ac36c2 Wire AES-GCM encryption into serialization layer (#1251)
* fix(core): chain unconsumed event check onto promiseQueue to prevent false positives

The EventsConsumer's unconsumed event check (setTimeout(0)) was racing
against the promiseQueue's async deserialization. When parallel steps
completed and their hydrateStepReturnValue did real async work (e.g.,
decryption), the setTimeout(0) fired before the promise chain resolved
the step results and triggered the next subscribe() call. This caused
step_created events for sequential steps to be falsely flagged as
unconsumed/orphaned.

Fix: chain the unconsumed check onto the promiseQueue via getPromiseQueue()
so it only fires after all pending async work completes. Use
process.nextTick (not setTimeout) after the queue drains to give
synchronous subscribe() calls from resolved user code a chance to cancel.
Version-based cancellation replaces clearTimeout since the check is now
promise-based.

Adds getPromiseQueue option to EventsConsumerOptions. The workflow.ts
context uses a getter/setter to keep the promiseQueue holder in sync.

Reproduction test: parallel steps A+B with 10ms mock deserialization
delay, followed by sequential step C. Previously failed with
'Unconsumed event: step_created(C)'. Now passes.

* fix: chain hydrateWorkflowArguments onto promiseQueue to prevent false unconsumed events

The unconsumed event check was firing during the async gap between
run_started consumption and the workflow function subscribing its
first step callbacks. This happened because hydrateWorkflowArguments
is async, and during its await, the EventsConsumer advanced to
step_created events that had no subscriber yet.

Fix: chain hydrateWorkflowArguments onto the promiseQueue so the
unconsumed check (which waits for the queue to drain) doesn't fire
until after the workflow arguments are hydrated and the workflow
function has been invoked.

* fix: use setTimeout(0) macrotask for unconsumed check to ensure VM promise propagation completes

The process.nextTick-based unconsumed check was still racing against
VM promise propagation. After promiseQueue resolves and the user code's
resolve() fires, there are multiple microtask hops through the VM
boundary before the workflow code actually calls subscribe() for the
next steps. process.nextTick fires before those VM microtasks complete.

setTimeout(0) is a macrotask that is guaranteed to fire only after ALL
microtasks (including VM promise chain propagation) have drained. The
pendingUnconsumedTimeout handle is stored and cleared in subscribe()
to prevent keeping the event loop alive unnecessarily.

* fix: increase unconsumed event check delay to 100ms for cross-VM promise propagation

setTimeout(0) is insufficient because Node.js does not guarantee that
macrotasks fire after all cross-context (VM boundary) microtasks settle.
After promiseQueue resolves and resolve() fires in the host context,
there are multiple microtask hops through the VM boundary before the
workflow code actually calls subscribe(). A 100ms delay provides
sufficient time for this propagation while still detecting truly
orphaned events promptly.

Also update sleep.test.ts to wait 200ms for the unconsumed check.

* Add browser-compatible AES-GCM to core and HKDF key derivation to world-vercel

* update changeset

* Move HKDF key derivation server-side: API returns per-run derived key

* Refactor encrypt/decrypt to accept CryptoKey, export importKey for callers to import once per run

* Overload getEncryptionKeyForRun: accept context for start(), fetch WorkflowRun in resume-hook

* Split changeset into per-package descriptions for world, world-vercel, and core

* Remove unnecessary Uint8Array.from() wrapper around Buffer.from()

* Use zod to parse Vercel API response

* Wire encryption into serialization layer

* Wire AES-GCM encryption into serialization layer

* update changeset

* Add encryption unit tests: primitives, maybeEncrypt/maybeDecrypt, isEncrypted, complex type round-trips

* Accept CryptoKey in encrypt/decrypt, export importKey for callers to import once per run

* Fix review comments: cache stream encryption key, remove redundant casts, fix stale comments

* Trying to clean up some type non-sense

* fix: restore world-vercel files to main versions

The rebase incorrectly picked up older versions of these files from
early encryption branch commits. The main versions are correct and
up-to-date.

* fix: add type cast for hydrateStepReturnValue return in hook.ts

* fix: address review feedback on encryption PR

- Remove Vercel-specific error message from maybeDecrypt (core should
  not reference VERCEL_DEPLOYMENT_KEY)
- Move stream encryption/decryption from transport layer
  (WorkflowServerReadableStream/WritableStream) to framing layer
  (getSerializeStream/getDeserializeStream). Frame length headers stay
  in the clear so frame boundaries are always parseable regardless of
  transport chunking; encryption wraps the frame payload.
- Remove explicit Promise<unknown> return types from all 4 hydrate
  functions. On main these had inferred types (any from devalue),
  so callers didn't need casts. The encryption branch added explicit
  annotations that broke this.
- Revert unnecessary type casts in run.ts, step-handler.ts, hook.ts
  that were only needed due to the explicit Promise<unknown> annotations
- Revert closureVars type from unknown back to Record<string, any>
  in context-storage.ts to match the contract with getClosureVars
- Fix hydrateWorkflowArguments JSDoc for unused _runId parameter

* Revert more unnecessary changes

* cleanup: remove unused runId param, deduplicate processFrames, add legacy comments

- Remove unused _runId parameter from WorkflowServerReadableStream
  constructor and all 4 call sites
- Deduplicate processFrames decryption: decrypt first and reassign
  format/payload, then fall through to single deserialization path
- Add comments on all legacy non-Uint8Array branches explaining when
  this happens (specVersion 1 runs stored data as plain JSON arrays)
- Fix duplicate code block in hydrateStepReturnValue

* feat: wire cryptoKey through stream serialize/deserialize pipeline

Thread the encryption key through the entire stream serialization chain
so that ReadableStream and WritableStream values are encrypted/decrypted
at the framing level.

- Add optional cryptoKey param to getExternalReducers, getStepReducers,
  getExternalRevivers, getStepRevivers
- Pass cryptoKey to getSerializeStream/getDeserializeStream at all 8
  internal call sites within reducers/revivers
- Thread key from dehydrate/hydrate functions into their reducers/revivers
- Cache encryption key in Run class (resolved once via getEncryptionKey(),
  reused for returnValue, getReadable(), etc.)
- Make Run#getReadable() async to resolve the cached key before creating
  the deserialize stream
- Add encryptionKey to step context storage so getWritable() can access
  it during step execution

* fix: make cryptoKey required-but-nullable to prevent silent omission, add stream encryption tests

Change cryptoKey parameter from optional (cryptoKey?) to required-but-
nullable (cryptoKey: CryptoKey | undefined) on all 6 functions:
- getSerializeStream, getDeserializeStream
- getExternalReducers, getStepReducers
- getExternalRevivers, getStepRevivers

This ensures every call site must explicitly pass the key or undefined,
making it impossible to accidentally omit it and silently skip encryption.

Add 7 stream encryption round-trip tests:
- Encrypted frames have 'encr' prefix inside length header
- Full round-trip: encrypt serialize -> decrypt deserialize
- Concatenated encrypted frames (transport coalescing)
- Split encrypted frames (transport splitting)
- Error when encrypted data encountered without key
- No encryption when key is undefined
- Large payload round-trip

Full audit confirms all encryption key threading is complete:
- All 8 dehydrate/hydrate functions pass key to reducers/revivers
- All stream serialize/deserialize call sites pass key
- Run class caches key for reuse across returnValue and getReadable()
- Step context storage carries key for getWritable()

* fix: keep Run#getReadable() sync, resolve encryption key lazily in streams

- Revert Run#getReadable() to synchronous (non-breaking API).
  The encryption key is passed as a Promise through the chain and
  resolved lazily inside the first async transform() call.
- Add EncryptionKeyParam type alias that accepts CryptoKey, undefined,
  or Promise<CryptoKey | undefined>. Used by getSerializeStream,
  getDeserializeStream, and all reducer/reviver functions.
- Key promises are resolved once on first use via a keyState cache
  object inside each stream's transform closure.
- Fix CLI showStream to resolve encryption key from world when runId
  is provided via --run flag, instead of passing undefined.
- Remove incorrect CLI warning that --run is not supported for streams
  (it is now needed for encrypted stream decryption).

* .

* fix: address review feedback from PR #1251

- Fix 4 broken dehydrateWorkflowArguments calls in workflow.test.ts
  that were passing ops as runId (missing runId and key params)
- Use WorkflowRuntimeError instead of plain Error in decodeFormatPrefix
  for unknown serialization formats, for consistency and programmatic
  error handling
- Document maybeDecrypt throw behavior: callers should be aware this
  surfaces as a rejected promise during key rotation/misconfiguration
- Document key-fetch rejection timing in streams: promise rejection
  won't surface until the first chunk is processed
2026-03-04 12:19:49 -08:00
Nathan Rajlich 60bc9d5cb1 fix(core): chain unconsumed event check onto promiseQueue to prevent false positives (#1254)
* fix(core): chain unconsumed event check onto promiseQueue to prevent false positives

The EventsConsumer's unconsumed event check (setTimeout(0)) was racing
against the promiseQueue's async deserialization. When parallel steps
completed and their hydrateStepReturnValue did real async work (e.g.,
decryption), the setTimeout(0) fired before the promise chain resolved
the step results and triggered the next subscribe() call. This caused
step_created events for sequential steps to be falsely flagged as
unconsumed/orphaned.

Fix: chain the unconsumed check onto the promiseQueue via getPromiseQueue()
so it only fires after all pending async work completes. Use
process.nextTick (not setTimeout) after the queue drains to give
synchronous subscribe() calls from resolved user code a chance to cancel.
Version-based cancellation replaces clearTimeout since the check is now
promise-based.

Adds getPromiseQueue option to EventsConsumerOptions. The workflow.ts
context uses a getter/setter to keep the promiseQueue holder in sync.

Reproduction test: parallel steps A+B with 10ms mock deserialization
delay, followed by sequential step C. Previously failed with
'Unconsumed event: step_created(C)'. Now passes.

* fix: chain hydrateWorkflowArguments onto promiseQueue to prevent false unconsumed events

The unconsumed event check was firing during the async gap between
run_started consumption and the workflow function subscribing its
first step callbacks. This happened because hydrateWorkflowArguments
is async, and during its await, the EventsConsumer advanced to
step_created events that had no subscriber yet.

Fix: chain hydrateWorkflowArguments onto the promiseQueue so the
unconsumed check (which waits for the queue to drain) doesn't fire
until after the workflow arguments are hydrated and the workflow
function has been invoked.

* fix: use setTimeout(0) macrotask for unconsumed check to ensure VM promise propagation completes

The process.nextTick-based unconsumed check was still racing against
VM promise propagation. After promiseQueue resolves and the user code's
resolve() fires, there are multiple microtask hops through the VM
boundary before the workflow code actually calls subscribe() for the
next steps. process.nextTick fires before those VM microtasks complete.

setTimeout(0) is a macrotask that is guaranteed to fire only after ALL
microtasks (including VM promise chain propagation) have drained. The
pendingUnconsumedTimeout handle is stored and cleared in subscribe()
to prevent keeping the event loop alive unnecessarily.

* fix: increase unconsumed event check delay to 100ms for cross-VM promise propagation

setTimeout(0) is insufficient because Node.js does not guarantee that
macrotasks fire after all cross-context (VM boundary) microtasks settle.
After promiseQueue resolves and resolve() fires in the host context,
there are multiple microtask hops through the VM boundary before the
workflow code actually calls subscribe(). A 100ms delay provides
sufficient time for this propagation while still detecting truly
orphaned events promptly.

Also update sleep.test.ts to wait 200ms for the unconsumed check.

* add changeset
2026-03-04 18:52:38 +00:00
Nathan Rajlich c35b597552 fix(ci): fix npm 'latest' dist-tag not being set during release (#1255)
* fix(ci): fix npm 'latest' dist-tag not being set during release

The OIDC trusted publishing change (a78af46f) removed NPM_TOKEN from the
changesets/action step and added registry-url to actions/setup-node. This
caused the 'Add latest dist-tag' step to break for the unscoped 'workflow'
package:

- actions/setup-node writes an .npmrc at a temp path that references
  ${NODE_AUTH_TOKEN}, and sets NPM_CONFIG_USERCONFIG to point at it
- The dist-tag step was writing its own ~/.npmrc using NPM_TOKEN, but npm
  ignored it because NPM_CONFIG_USERCONFIG overrides the default path
- The OIDC token left over in NODE_AUTH_TOKEN worked for @workflow/* scoped
  packages (which already had 'latest' set) but returned E401 for the
  unscoped 'workflow' package

Fix: set NODE_AUTH_TOKEN to NPM_TOKEN_ELEVATED secret so npm picks up the
correct token from the .npmrc that actions/setup-node already configured.

Also fix silent failures: replace the piped while-read loop (which runs in
a subshell and swallows exit codes) with a for loop that tracks failures
and exits non-zero if any dist-tag operation fails.

* use process substitution instead of word-splitting for loop
2026-03-04 09:51:45 -08:00
Nathan Rajlich 860531d182 fix(core): ensure async deserialization resolves promises in event log order (#1246)
* fix(core): ensure async deserialization resolves promises in event log order

Add a sequential deserializationChain to WorkflowOrchestratorContext that
ensures step/hook hydration resolves in event log order even when individual
deserialization takes variable time (e.g. due to encryption/decryption).

Without this, concurrent async hydrations could resolve out of order,
breaking workflow replay determinism.

* test: add hook ordering test and afterEach mock cleanup

Address review feedback:
- Add hook-specific deserialization ordering test
- Add afterEach(vi.restoreAllMocks) to prevent spy leaks
- Remove manual mockRestore calls

* refactor(core): route all promise resolutions through sequential promiseQueue

- Rename deserializationChain -> promiseQueue (it now handles all
  event-driven promise resolutions, not just deserialization)
- Route step_failed, step_created corruption, unexpected event type,
  and null event suspensions through promiseQueue in step.ts
- Route hook_conflict, unexpected event type, null event suspensions,
  dispose suspensions, and eventLogEmpty suspensions through
  promiseQueue in hook.ts
- Route wait_completed and unexpected event type through promiseQueue
  in sleep.ts
- Remove all setTimeout(0) calls from step.ts, hook.ts, and sleep.ts
- Add stress tests: mixed step_completed/step_failed, 10 concurrent
  steps, sleep+step interleaving, interleaved concurrent steps

* fix: route createHookPromise conflict rejection through promiseQueue

- The hasConflict path in createHookPromise() was rejecting immediately,
  bypassing the promiseQueue and potentially settling out of event-log
  order relative to prior queued resolutions.
- Add missing runId and encryptionKey to sleep.test.ts context stub.
2026-03-03 14:42:08 -08:00