SIGN IN SIGN UP

[world] Add optional `invoke` method (hook payloads only for now) (#4168)

* feat(world-postgres): add optional executor invocation for hooks

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* perf(world-postgres): notify invocation input and result waiters

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* fix(world-postgres): verify serialized executor deliveries

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* refactor(world): return invocation results and harden Postgres delivery

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* chore: reconcile main before signed merge

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* fix(world-postgres): replay drained inputs and return invocation errors

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* Fix: Transient/infra errors thrown during hook-resume invocation are captured and stored as a permanent, non-retryable outcome instead of being retried, which can leave a `hook_received` write uncommitted and suspend the workflow forever.

This commit fixes the issue reported at packages/world-postgres/src/queue.ts:322

## The bug

Commit `f944ec9` reworked the executor `invoke` path in `packages/world-postgres/src/queue.ts`:

```ts
const outcome = await captureInvocationOutcome(() => handler(message, metadata));
await invocations.respondOutcome(input.runId, input.requestId, outcome);
return; // delivery acked (HTTP 200), no retry
```

`captureInvocationOutcome` (`packages/errors/src/invocation.ts`) caught **every** error unconditionally and returned `{ ok: false, error }`. `respondOutcome` then persisted it (`result_version = 1`, `responded_at = now()`) and the handler returned normally, so Graphile acked the job — **no redelivery**.

For an `invoke` message the wrapped handler routes only to `handleInvocation` (`packages/core/src/runtime/invocations.ts`), which performs pure world I/O: `world.hooks.get`, `world.runs.get`, and the durable `world.events.create` that commits the `hook_received` event resuming the workflow. `f944ec9` also changed `handleInvocation` to **throw** on every failure (it previously returned `{status:'rejected'}` for hook-gone).

### Concrete failure mode

1.  `resumeHook` invokes with a stable `requestId`.
2.  The executor runs `handleInvocation`; `world.events.create` fails with a **transient** error (DB connection reset, deadlock, `40001` serialization failure — surfaced as a raw `DrizzleQueryError`/pg error, not a `WorkflowError`).
3.  `captureInvocationOutcome` catches it → `{ ok:false, error }`.
4.  `respondOutcome` stores the error permanently (`responded_at` set) and commits.
5.  Handler returns → HTTP 200 → Graphile acks → **no retry**.
6.  The `hook_received` event was never committed, so the run stays suspended.
7.  `world.invoke()`'s waiter reads the responded row and (via `unwrapInvocationOutcome`) throws the rehydrated error to the caller immediately.

**Before** `f944ec9`, a thrown error propagated out of the queue handler into `createQueueHandler`'s retry logic (Graphile redelivery), so a transient blip was retried transparently until the resume committed. The new code converts that into a permanent stored failure.

The intended contract (per `packages/world-postgres/test/invoke.test.ts` — *"returns persisted $name outcomes instead of timing out"*, which only exercises **known** `WorkflowWorldError`/`HookNotFoundError`/`WorkflowRunNotFoundError`/`RunExpiredError`/`EntityConflictError`) was to persist **terminal/deterministic** business errors so callers get the right error class instead of a 30s timeout — not to persist transient/unknown failures.

## The fix

Distinguish terminal errors (capture + store) from transient/unknown errors (re-throw so the delivery layer retries):

*   Added `isTerminalInvocationError` in `packages/errors/src/invocation.ts`. An error is terminal only when it is a class the `@workflow/errors` package owns (checked by `name` against the module registry, matching the existing `deserializeWorkflowError` approach) **and** it does not look transient — i.e. it has no `retryAfter` and no `status` of `>= 500` / `408` / `425` / `429`. Unknown/infra failures (raw `Error`, `DrizzleQueryError`, thrown non-Errors) and transient Workflow errors are therefore **not** terminal.
*   Gave `captureInvocationOutcome` an optional `shouldCapture` predicate (defaulting to capture-all, preserving its generic transport contract and existing unit tests). When it returns `false`, the error is re-thrown.
*   Both `invoke`-path call sites in `packages/world-postgres/src/queue.ts` now pass `isTerminalInvocationError`, so transient failures propagate to `createQueueHandler`'s Graphile retry (as before `f944ec9`) while terminal errors are still persisted (keeping the `invoke.test.ts` `deliveries === 1` behavior).

The known terminal cases thrown by `handleInvocation` remain captured: `INVALID_INPUT` (400), `HookNotFoundError`, `INVOCATION_DATA_EXPIRED` (410), `WorkflowRunNotFoundError`, `RunExpiredError`, and the deterministic `EntityConflictError` from correlated-event dedup. Added unit tests covering both the terminal and transient classifications. Type-checking passes for `@workflow/errors` and `@workflow/world-postgres`, and the `invocation.test.ts` suite passes.


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>

* chore: reconcile main and invocation retry coverage

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* style(errors): format invocation error fixture

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* docs: consolidate invocation changeset and trim Vercel notes

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* docs(world): define invoke single-runner guarantee

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

* docs: apply reviewed invoke documentation wording

Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>

---------

Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>
Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: VaguelySerious <mittgfu@gmail.com>
S
Shalabh Chaturvedi committed
97dccc99cac308e88bf97368f3d5236061abdfb6
Parent: f1f5b7d
Committed by GitHub <noreply@github.com> on 9/16/2026, 7:36:03 PM