Files
Karthik Kalyan 2668e3325b Durable hook resume: write, then wake (#3841)
* test(core): reproduce lazy resume disposal race

* Fix durable hook resume race

* Fail closed on unknown hook wakes

* Improve unsupported hook wake diagnostics

* Address durable hook resume review feedback

* Harden producer-committed wake handling

* Serialize durable hook resume: write, then wake

resumeHook() now dispatches strictly serially: the hook_received event
is made durable first, and the workflow wake is published only after
the write is acknowledged. The wake is a plain runId message (the shape
the sequential path always published), so the producer-committed wake
barrier, its queue-message field, and the HOOK_RESUME_INPUT_VERSION
bump are all removed — no consumer or backend coordination is needed,
and either side rolls back independently to today's behavior.

The pre-write ops flush now partitions serialization ops: producer-push
uploads are awaited before the event commits (the payload must not
point at bytes still in flight), while consumer-settled reader ops — a
dehydrated WritableStream, e.g. a manual webhook's responseWritable —
are backgrounded. Awaiting those deadlocked the resume against its own
wake (webhookWorkflow failing across the whole e2e matrix).

Also: wake retries stop on definitive 4xx errors instead of burning the
retry budget; WORKFLOW_DISABLE_LAZY_HOOK_RESUME no longer gates
anything and is ignored; the internal resumeHookDurable alias is
removed.

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

* Address review: retry classification, wake dedup, 409 passthrough

- Wake retry classification now actually fires against @vercel/queue:
  its errors carry no status field, so classify by the World's
  deployment-unavailable hook, then numeric status, then the queue
  client's definitive-4xx error names.
- The wake publish carries idempotencyKey `hook-<resumeId>` on the
  claim path, so a retried publish whose response was lost dedups
  instead of costing a duplicate full replay.
- EntityConflictError (HTTP 409) from the durable write is no longer
  re-keyed to HookNotFoundError: every 409 the backend emits on this
  write today is transient (slot conflict past the server's retry
  budget, claim race) and committed nothing, so it surfaces retryable
  instead of presenting as a permanent 404.
- Stamp workflow.hook.resume_committed / wake_published span
  attributes after each leg resolves, making stranded resumes
  (committed event, no wake) queryable from traces.
- Document on the public resumeHook signature that passing the token
  (not a cached Hook) is what makes the write idempotent-on-retry.
- Changeset/changelog: note the ended-run behavior change (late
  webhook deliveries to finished runs now 404 instead of 202) and the
  409 passthrough.

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

---------

Co-authored-by: Pranay Prakash <pranay.gp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 23:18:01 +00:00
..