Resilient hook resume
Resilient resumeHook()
Superseded by durable hook resume:
resumeHook() now writes the hook_received event durably and only then
publishes the workflow wake, so the two-writer design, the queue-carried
payload, and the resilientResume flag described below are historical. The
(runId, resumeId) constraint remains, converging transport-level retries
of the producer's own write (and legacy hookInput redeliveries from older
producers).
Motivation
resumeHook() used to write the hook_received event and dispatch the workflow queue message strictly one after the other, so every resume paid two sequential round trips and a transient event-storage failure failed the whole resume even when the queue was healthy. This change runs both writes concurrently (cutting a round trip off resume latency) and, on the same path, brings resumeHook() to parity with resilient start(): a transient event-write failure no longer fails the resume when the payload can still be delivered through the queue.
Design
- On the fast path,
resumeHook()writes thehook_receivedevent and dispatches the workflow queue message concurrently (Promise.allSettled). The queue message carries ahookInputpayload: the dehydrated hook payload plus a client-mintedresumeIdidempotency key, the hook token, and a payload digest. - A
(runId, resumeId)dedup constraint keeps the two writers converging on exactly onehook_receivedevent: whichever lands first wins, and the other is resolved server-side as success rather than a duplicate. The queue consumer idempotently re-ensures the event fromhookInputbefore replay, so the resume is guaranteed even if the direct write never commits. - Replay also deduplicates:
hook_receivedevents sharing aresumeIdbelong to the same resume attempt, and only the first in the event log is delivered to workflow code. Even if a redelivery materializes the event twice, the payload reaches the workflow exactly once. - Queue dispatch failure is fatal: The run was not retriggered, so no consumer will re-ensure the event, and
resumeHook()throws. A transient event-write failure (429/5xx, a transport error, or an expected(runId, resumeId)conflict with the consumer's own re-ensure) is swallowed because the queue delivery still guarantees the resume; a terminal run surfaces asHookNotFoundError, and any other event-write error is rethrown. resumeHook()returnsResumedHook(exported fromworkflow/api), which extendsHookwith an optionalresilientResumeflag. The flag istrueonly when the direct write failed transiently and the resume was recovered through the queue; on the happy path and the sequential fallback it is absent.
Compatibility
The parallel fast path is gated per resume: it activates only when both the target run's queue consumer and the live backend independently attest dedup support (re-checked on every resume, so rollout and rollback both degrade safely). Otherwise (for oversized payloads or legacy runs), resumeHook() falls back to the original sequential write-then-dispatch path. Because runs keep executing on the deployment they were created on, a resume targeting a run from an older deployment uses the sequential path.