* fix(sveltekit): patch server chunks with rollup-renamed __filename bindings
The adapter-node chunk patch skipped any chunk matching
/\b(const|let|var)\s+__(file|dir)name\b/ — but $ is not a regex word
character, so rollup-renamed declarations like `__filename$1` (produced
when adapter-node re-bundles the intermediate server output and our
banner's declaration collides) satisfied the check. Chunks that declared
only a renamed binding while a bundled CJS dependency referenced the
bare `__filename` were skipped, and the production server crashed at
boot (observed on main with the TypeScript compiler bundled via
cosmiconfig through @workflow/world-postgres).
Anchor both regexes with (?![\w$]) so renamed identifiers no longer
match. Verified: the sveltekit workbench production server now boots and
serves health checks (with and without a base path), and queue
deliveries from start() succeed.
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
* fix(sveltekit): keep the TypeScript compiler out of the server bundle
Since the world-target injection change, the sveltekit workbench's
hooks.server.ts imports @workflow/world-postgres, whose dependency chain
(graphile-worker -> cosmiconfig) reaches cosmiconfig's TS-config loader.
At runtime that loader's require('typescript') is lazy and never fires,
but SvelteKit bundles the whole chain into the server and rollup's CJS
conversion hoists it into an eager top-level evaluation — executing the
entire TypeScript compiler at boot and crashing the server
("__filename is not defined" inside the bundled compiler).
Alias 'typescript' to a stub module in the SvelteKit plugin, following
the existing pg-native pattern. Server output shrinks from 36MB to 11MB
and boots cleanly.
Verified: sveltekit workbench production build boots, serves
flow?__health (200), and start() runs execute with clean queue
deliveries. The chunk-patch regex fix from the previous commit stays as
hardening for any other CJS dependency that references __filename.
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
* Update .changeset/fix-sveltekit-filename-chunk-patch.md
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
---------
Signed-off-by: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
* cli: restore dynamic world loading for community backends
Since #2752 moved world selection to static injection, setupCliWorld only
constructed the vercel, local, and postgres worlds explicitly and threw
'Unsupported workflow backend' for anything else — breaking community
worlds (e.g. @workflow-worlds/turso) that previously loaded through the
dynamic createWorld() in @workflow/core/runtime.
Generalize the postgres-only dynamic path: any backend other than
vercel/local is now resolved from the user's project directory and loaded
via its createWorld() export, matching @workflow/web's world construction
(#2804), with clear errors when the package is missing or does not export
createWorld().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* cli: fall back to default.createWorld for CJS world packages
Review feedback on #2804: import() of a require-resolved CJS entry relies
on cjs-module-lexer to surface named exports; fall back to
mod.default.createWorld when detection fails.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Update .changeset/cli-generic-world-backends.md
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
* web: construct worlds explicitly instead of via static-injection stub
Since #2752, createWorld() from @workflow/core/runtime is a stub that
throws unless a framework build plugin aliases it to a world package.
The CLI was migrated to construct worlds directly, but @workflow/web
still called the stub, so 'workflow web' crashed with 'Workflow target
world was not statically injected' for local and postgres backends
(the vercel path was unaffected since it constructs the world directly).
Mirror the CLI: import the local world statically and resolve any other
configured world package from the inspected project's directory.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* web: fall back to default.createWorld for CJS world packages
Review feedback: import() of a require-resolved CJS entry relies on
cjs-module-lexer to surface named exports; fall back to
mod.default.createWorld when detection fails. Mirrored in the CLI in #2806.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The workbench workflows are shared across apps (nitro-v2/workflows
symlinks into files shared with workbench/example), and the shared
99_e2e.ts workflow imports @repo/lib/steps/paths-alias-test. nitro-v2's
tsconfig was missing the @repo/* path alias that nitro-v3 already has,
so building nitro-v2 with the Vercel preset failed at esbuild
resolution:
../example/workflows/99_e2e.ts: ERROR: Could not resolve
"@repo/lib/steps/paths-alias-test"
Mirror nitro-v3's alias. Verified NITRO_PRESET=vercel pnpm build now
succeeds (was failing on main before this change).
* Add workflow analytics world APIs
* web: read observability list views from world.analytics when available
Route the runs/steps/events/hooks list server actions through the optional
world.analytics namespace when the backend provides one, falling back to the
runtime storage APIs otherwise. Events listing only uses the analytics path
when no payload data is requested. Detail/get actions, streams, and mutations
are unchanged.
* web: keep events and hooks list reads on the runtime storage API
The Events tab and trace viewer derive step names and wait resumeAt from
resolved event payloads, and the hooks table needs the secret token and
ownerId for its resume/copy-token actions. The metadata-only analytics rows
do not carry these, so only the runs and steps list views use world.analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* web: read events list from world.analytics with a runtime-shape remap
The events list/trace consumers read only top-level eventType, correlationId,
and createdAt; event payloads are loaded lazily per event via fetchEvent(...,
'all') on the runtime path. Map the flat analytics event rows into the runtime
Event shape (reconstructing eventData.stepName) so fetchEvents and
fetchEventsByCorrelationId can use the analytics read path when available.
Hooks remain on the runtime path (they need the secret token + ownerId).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* web: list hooks from world.analytics, fetch token on demand
The hooks list now reads from the metadata-only world.analytics namespace
when the backend provides one (falling back to the runtime storage APIs
otherwise). A hook's secret token is no longer shipped in list rows — it is
fetched one hook at a time via world.hooks.get only when the user copies the
token or resumes the hook, keeping the secret out of bulk list responses.
Adds a fetchHookToken server action + RPC, a HookListItem type (Hook without
token), and a lazy HookTokenCell for the copy-token affordance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Handle analytics access metadata in web
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add workflow analytics world APIs
* web: read observability list views from world.analytics when available
Route the runs/steps/events/hooks list server actions through the optional
world.analytics namespace when the backend provides one, falling back to the
runtime storage APIs otherwise. Events listing only uses the analytics path
when no payload data is requested. Detail/get actions, streams, and mutations
are unchanged.
* web: keep events and hooks list reads on the runtime storage API
The Events tab and trace viewer derive step names and wait resumeAt from
resolved event payloads, and the hooks table needs the secret token and
ownerId for its resume/copy-token actions. The metadata-only analytics rows
do not carry these, so only the runs and steps list views use world.analytics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* web: read events list from world.analytics with a runtime-shape remap
The events list/trace consumers read only top-level eventType, correlationId,
and createdAt; event payloads are loaded lazily per event via fetchEvent(...,
'all') on the runtime path. Map the flat analytics event rows into the runtime
Event shape (reconstructing eventData.stepName) so fetchEvents and
fetchEventsByCorrelationId can use the analytics read path when available.
Hooks remain on the runtime path (they need the secret token + ownerId).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Handle analytics access metadata in web
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Add workflow analytics world APIs
* cli: read list views from world.analytics when available
inspect list views (runs, steps, events, hooks, sleeps) now read from the
optional world.analytics namespace when the active backend provides one,
falling back to the runtime storage APIs otherwise. Payload and detail views
are unchanged. Deprecate --with-data for list views; payloads are viewable
per-resource via 'inspect <resource> <id>'.
* cli: keep hook listing on the runtime storage API
The analytics read path omits ownerId (and the secret hook token), so routing
hook listing through it silently drops the ownerId column. Keep inspect hooks
on the runtime APIs, consistent with the web observability UI. Runs, steps,
events, and sleeps continue to use the analytics read path when available.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Handle analytics access metadata in CLI
* test(cli): preserve analytics pageInfo in json output
* fix(cli): paginate analytics sleeps output
* fix(cli): correct deprecation message flag name to --withData
The list-view deprecation warning referenced '--with-data', but the actual
oclif flag is '--withData' (with '-d' alias); '--with-data' errors with
"Nonexistent flag". Fix the warning text, the doc comment, and the changeset
to reference the real flag name.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cli): preserve inspect json array output
* fix(cli): fall back when analytics lists are empty
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(world-vercel): send x-vercel-queue-region on proxy-mode queue sends
Token/proxy clients (CLI, dashboards, external scripts) send queue
messages through api.vercel.com's /v1/workflow proxy, where the fixed
resolveBaseUrl override replaces the queue SDK's own
region -> <region>.vercel-queue.com base-URL resolution — so every
proxied send landed on the region-less VQS host, while the direct
in-function path already dials the regional host (iad1 today).
The proxy now routes sends to the region's VQS dataplane when the
x-vercel-queue-region header is present (vercel/api#79056, deployed).
Set it on every proxy-mode send from the region the QueueClient is
constructed with, giving proxy clients parity with the direct path.
Today that region is the static 'iad1'; when per-send region
resolution lands (#1981), the header automatically carries the
tag/option-derived region — this change is the transport mechanism,
not the policy.
Tests: proxy-mode construction carries the header matching the
client's region; the direct path doesn't.
* chore: trim comments, drop private repo reference
* chore: changeset for proxy-mode x-vercel-queue-region header
* doen
* Fix: Old test file `test/format-duration-precise.test.ts` still asserts two-decimal output ("45.20s", "1m 0.00s") that no longer matches the trimmed-zero output of `formatDurationPrecise`, so `pnpm test`/CI fails.
This commit fixes the issue reported at packages/web-shared/test/format-duration-precise.test.ts:15
## Bug
The PR changed `formatDurationPrecise` (in `packages/web-shared/src/lib/utils.ts`) to trim trailing zeros by wrapping the fractional value in `Number(x.toFixed(fractionDigits))`:
```ts
if (normalizedMs < MS_IN_MINUTE) {
return `${Number((normalizedMs / MS_IN_SECOND).toFixed(fractionDigits))}s`;
}
...
parts.push(`${Number(seconds.toFixed(fractionDigits))}s`);
```
`Number("45.20")` → `45.2`, `Number("0.00")` → `0`, so whole/half seconds now render without padding.
A new test file `packages/web-shared/src/lib/utils.test.ts` reflects this behavior, but the pre-existing `packages/web-shared/test/format-duration-precise.test.ts` was left untouched and still asserts the **old** padded output.
## Concrete trigger
Reproduced the actual function output (integer decomposition + trimmed zeros):
| Input | Old assertion | New actual output |
|-------|---------------|-------------------|
| `45200` | `45.20s` | `45.2s` |
| `999.6` | `1.00s` | `1s` |
| `999.5` | `1.00s` | `1s` |
| `59999` | `1m 0.00s` | `1m 0s` |
| `59995` | `1m 0.00s` | `1m 0s` |
| `119999` | `2m 0.00s` | `2m 0s` |
| `3659999` | `1h 1m 0.00s` | `1h 1m 0s` |
| `86459999` | `1d 1m 0.00s` | `1d 1m 0s` |
The root `vitest.config.ts` uses default include globs, so `test/*.test.ts` runs and these assertions fail, breaking CI. (I couldn't run vitest directly in the sandbox because dev deps weren't installed / `vitest/config` unresolved, so I reproduced the exact function logic in a standalone Node script to confirm the outputs.)
## Fix
Updated the stale assertions in `test/format-duration-precise.test.ts` to the trimmed-zero outputs, and adjusted the file-level doc comment (which referenced `"1m 0.00s"` / `"60.00s"`) to describe the current behavior.
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: mitul-s <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>
* Statically inject workflow world target
* Fix static world injection in host bundles
* Fix static world injection gaps
* Fix Vite Nitro server startup
* Fix Nitro pg-native aliasing
* Fix static world target CI gaps
* Fix static world dev rebuild gaps
* Avoid broad runtime alias in Nitro
* Refresh Next dev route for step HMR
* Externalize Nest target world
* Use canary HMR rediscovery timeout
* Bundle local world in Nest builds
* Dedupe world target helpers and fix SvelteKit chunk patch guard
* nice
* refactor(web-shared): simplify resizable detail panel internals
Inline single-use constants in DraggableBorder and make comments
self-contained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix compressed workflow data display
* Add OSS web no-key hydration regression
* Scope compression normalization to read paths; tidy hydration
Address review feedback on the compressed-data fix:
- world-vercel: keep gzip/zstd decompression on the o11y/display read
paths (getStep/getRun/getEvent/getWorkflowRunEvents/getHook) but not on
the runtime event-append path (world.events.create, createStep,
updateStep). That path is runtime-only and re-hydrates every payload via
the decompress-aware helpers, so decompressing at the adapter was
redundant work on the TTFB-sensitive run_started/inline-delta path and
skewed the runtime's deserialize compression telemetry to `codec: none`.
deserializeStep is now shape-only; normalizeStepData runs in the read
filter. Adds a regression test pinning the write-path pass-through.
- serialized-data: drop dead `errorRef`/`metadataRef` normalization (refs
are descriptor objects, never compressed byte payloads).
- web: in the wait-entity path, filter events by correlationId before
hydrating so an encryption key doesn't decrypt the whole event page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Optimize Next dev HMR rebuilds
* Fix Next dev HMR CI coverage
* Gate dev HMR logs behind opt-in flag
* Match workflow dev build logs to Next style
* Fix Next dev HMR changed-file classification
* Fix Windows port detection
* Relax HMR log wait in dev e2e
* Avoid canary workflow execution cache flakes
* Allow slower Turbopack HMR propagation in e2e
* Scope canary HMR fuzz execution assertions
* fix(web-shared): align metadata panel styling with attributes section
Render hook metadata as key-value rows inside a DetailCard, matching
the Attributes section instead of a raw JSON block with a separate label.
* fix(web-shared): align top detail panel rows with attributes styling
Reuse DetailKeyValueRow for Module, Step ID, timestamps, etc. — same
tighter spacing, typography, and no dividers as the Attributes section.
* fix(web-shared): collapse top detail rows into Metadata with mono values
Wrap Module, Step ID, timestamps, etc. in a Metadata DetailCard and
render all values in monospace, including copyable paths and IDs.
* fix(web-shared): use contained header style for Metadata section
Add a contained DetailCard variant with rounded bg header and drop my-2
in favor of py-2 padding on the section wrapper.
* refactor(web-shared): drop reserved badge and use cn for row classes
Remove ReservedBadge and showReservedBadge; keep reserved-key sorting only.
Use cn() for conditional mono font classes in DetailKeyValueRow.
* refactor(web-shared): drop contained DetailCard variant
Use the default section DetailCard for Metadata, matching Attributes.
* feat(web-shared): extend cn with custom tailwind-merge class groups
Add a dedicated cn module that understands text-heading, text-label,
text-copy, text-button, and material utilities when merging classes.
* chore: note cn tailwind-merge update in changeset
* fix(web-shared): remove my-2 from DetailCard summary rows
Move vertical spacing to py-2 on the section container instead.
* fix(web-shared): scope Metadata spacing override
Restore shared DetailCard summary spacing for Input, Output, Events, and
Attributes while keeping Metadata tighter with a summaryClassName override.
* refactor(web-shared): use CVA variants for detail row value styling
Replace the mono boolean and one-off Metadata summary override with CVA-backed row variants and semantic mono row wrapper.
* fix(web-shared): use section padding for DetailCard spacing
Move spacing from summary margins to section padding and add content top
spacing so collapsed and expanded detail cards both breathe consistently.
* fix(web-shared): remove expanded Metadata content gap
Keep DetailCard's default expanded content spacing for data panels, but let
Metadata rows start directly below the title via contentClassName merge.
* refactor(web-shared): use Tailwind classes for disabled detail card
* refactor(web-shared): use DetailCard compound content
* refactor(web-shared): rebuild DetailCard as a compound component
Replace the monolithic DetailCard (summary/trailing/disabled props plus
child-type reflection for content) with a context-driven compound API:
DetailCard + DetailCard.Trigger + DetailCard.Content. Drop the dead
trailing branch, expose data-slot/data-state, and migrate all call sites.
* refactor(web-shared): rename DetailCard to Collapsible
It's a generic collapsible section, not a card-specific component. Rename
the component, its parts, data-slots, and the file accordingly.
* refactor(web-shared): split Collapsible into all-in-one + parts
Export a batteries-included <Collapsible label> for the common case so
consumers don't recompose the trigger/content every time, plus
CollapsibleRoot/CollapsibleTrigger/CollapsibleContent for the few call
sites that need to override part styling. Drop the dot-notation namespace.
* refactor(web-shared): move Collapsible into ui directory
It's a generic UI primitive, not sidebar-specific.
* refactor(web-shared): import cn from lib/cn directly
Drop the cn re-export from lib/utils; consumers import it from its
actual source instead of routing through utils.
* refactor(web-shared): drop cn tailwind-merge changes from this PR
Move the extended cn (lib/cn) work to a separate PR; this branch keeps
using the existing cn from lib/utils.
* ship it
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* Redrive on transient workflow-server transport failures instead of failing the run
A firewall in front of workflow-server shedding load with sustained 429/503
makes undici's shared RetryAgent exhaust its retries and throw
UND_ERR_REQ_RETRY. That raw error was rethrown unwrapped, so it was
classified as USER_ERROR and the replay terminal branch wrote run_failed —
permanently failing a run on a transient blip (or, in an outage, falling back
to the ~5min queue visibility-timeout redrive).
- world-vercel: map exhausted-retry / socket / connect / DNS / timeout
failures to a typed WorkflowWorldError (code TRANSPORT/TIMEOUT) by walking
the fetch() cause chain.
- core: add isRetryableWorldError (429 / 5xx / TRANSPORT / TIMEOUT) and
rethrow such errors from the replay terminal branch so the queue redrives
quickly (1s->60s backoff) instead of failing the run. Reuse it in start()
and step_started handling.
- world-vercel: surface the Vercel firewall x-vercel-mitigated
(challenge/deny) header alongside x-vercel-id in error diagnostics and logs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Address review: back off + cap on every retry path; fix mock; refine scope
Revises the transport-error handling per PR review (VaguelySerious,
karthikscale3).
Blocking fix — step_started no longer self-enqueues a throttled defer for
transient world errors. Returning `{ type: 'throttled', timeoutSeconds: 1 }`
acked the delivery and enqueued a fresh message, resetting the delivery count
so the path never backed off and never reached MAX_QUEUE_DELIVERIES — an
unbounded flat-1s loop if step_started kept failing. It now throws, so the
error flows through the replay loop's retryable-world-error rethrow and earns
both the delivery-count backoff and the max-delivery cap. Throwing is safe on
step_started (the body hasn't run; a write that landed dedupes to skipped).
Also in this revision:
- Backoff that lasts: raise the queue handler-error retry ceiling 60s -> 900s.
VQS clamps each redelivery to its 900s SQS limit and adds its own post-32
exponential, so ramping our base toward 900s stretches survival from ~3.7h to
most of the 24h message-visibility window. Corrected the stale
MAX_QUEUE_DELIVERIES comment to match the real VQS schedule.
- Stop amplifying firewall challenges: the undici RetryAgent no longer retries
429 in-process (a challenge is a 429 the client can't solve). 429s surface
immediately as ThrottleError carrying x-vercel-mitigated / x-vercel-id, so
the diagnostic header now reaches us for the challenge case too.
- Track world faults as WORLD_CONTRACT_ERROR (not USER_ERROR) in
classifyRunError so an outage isn't attributed to user code.
- Fix queue.test.ts mock that `biome check --write` had rewritten from a
newable `function` into an arrow (broke `new QueueClient`); pin with a
biome-ignore.
All Vercel-specific logic stays in @workflow/world-vercel; @workflow/core
operates only on the generic WorkflowWorldError abstraction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Update .changeset/transport-error-redrive.md
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
* Trim changeset to a single sentence per review
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Route firewall-challenge 429s to the retryable transport path, not ThrottleError
A 429 carrying `x-vercel-mitigated: challenge` is a firewall challenge our
server-to-server client cannot solve, so it recurs for the life of the
incident. Mapping it to `ThrottleError` meant the `step_started` write deferred
it as `{ type: 'throttled' }`, which self-enqueues a FRESH queue message and
resets the delivery count — so it never backed off past `retryAfter` and never
reached `MAX_QUEUE_DELIVERIES`, hot-looping against an already-overloaded
firewall (the exact amplification this PR set out to remove, and contrary to
the "step_started can't loop unbounded" invariant, which only held for 5xx).
Map a challenge to a retryable transport `WorkflowWorldError` (`code:
'TRANSPORT'`) in both the v3 `makeRequest` and v4 `throwForErrorResponse`
(the hot event-write path) error mappings, via a shared `isFirewallChallenge429`
helper. It then propagates through the V1/V2 step paths and the replay loop's
retryable-world-error rethrow, earning the delivery-count backoff AND the
delivery cap. A genuine application-level 429 (no `challenge` mitigation) stays
a `ThrottleError` and keeps its `Retry-After`-paced defer.
Also correct the survival-window comments: with the 900s ceiling,
MAX_QUEUE_DELIVERIES=48 spans ~9-10h (~35,000s), not "the better part of 24h";
reaching 24h would need a higher delivery cap, not a higher per-hop ceiling.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>
Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
* fix(world-vercel): retry idempotent event POSTs in-process to avoid step re-execution
undici's RetryAgent never retries a POST, so a transient transport blip (UND_ERR_REQ_RETRY, ECONNRESET, socket/headers timeout, transient 5xx) when committing a step's terminal event bubbles out, the queue redelivers, and the step's user code re-executes with attempt++ even though it already ran to completion.
workflow-server makes these writes idempotent in outcome: entity handlers run before the event-log row is inserted and state transitions are conditional writes excluding terminal states, so a retry whose original landed throws before any row is written and surfaces as a 409 the SDK already handles. This adds a bounded in-process retry (new event-retry.ts) gated by a validated per-event EVENT_RETRY_ELIGIBILITY map, excluding step_started (double-increments attempt), step_retrying (appends a duplicate row), and hook_received (no server guard).
Complements #2666, which routes completion-persistence failures to queue redelivery instead of recording them as user failures; this avoids the redelivery (and re-execution) for transient blips.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(world-vercel): address review on event-POST retry
- Don't retry external cancellation: drop AbortError from the transient set (keep self-timeout TimeoutError), so a caller-requested abort isn't re-issued or stalled by the backoff budget.
- Add DEBUG-gated logging on each retry and on retry exhaustion so an in-process retry vs. a fall-through to queue redelivery is distinguishable in logs.
- Clarify docs: a landed retry surfaces as 409 for most types, but run_started/attr_set return 200 success (not a 409).
- Add createWorkflowRunEvent integration tests (events-retry.test.ts): eventType is threaded into the retry wrapper, and the 404->HookNotFoundError mapping still fires after the retry loop.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Peter Wielander <mittgfu@gmail.com>