eve
Version:
Filesystem-first framework for durable backend AI agents that run anywhere.
84 lines • 4.63 kB
TypeScript
/**
* Wait-continuation dispatch: delay + idempotency-key selection.
*
* When V2 suspension processing observes a pending wait, it enqueues a
* delayed "continuation" message that fires once the wait elapses and
* drives the next replay (which completes the wait via the "complete
* elapsed waits" pass). This module decides the message's `delaySeconds`
* and `idempotencyKey`.
*
* The continuation is keyed on the wait's correlationId: while a wait is
* pending, every replay pass over the run re-observes it (e.g., once per
* step completion in `Promise.all([steps..., sleep()])`), and without
* dedupe each pass would enqueue another delayed continuation — each one
* a spurious full replay when the wait elapses, and each a fresh message
* that resets the delivery-attempt runaway guard. A key is attached in
* all cases: some worlds (e.g. world-postgres) serialize key-less
* workflow messages per run, which would park the continuation behind
* the handler's own inline step execution and defeat the race semantics
* the continuation exists to provide.
*
* The bare correlationId cannot be the key in every case, though: world
* dedupe windows outlive the first delivery (VQS keeps idempotency
* records until message-retention TTL; world-postgres keeps a
* completed-keys cache), so once a key has been used, a later enqueue
* under the same key is silently dropped. Any situation where a
* continuation is delivered while its wait is still pending therefore
* needs a fresh key for the re-enqueue, or the wait's timer is lost and
* the run stalls until unrelated traffic happens to wake it. Two such
* situations exist, each with its own key variation:
*
* - Waits longer than the maximum queue delay are chained: the delay is
* clamped to `WAIT_CONTINUATION_MAX_DELAY_SECONDS` (23h — VQS messages
* have a 24h retention limit, and one hour of buffer matches
* world-vercel's own clamp for delayed re-enqueues), so the
* continuation intentionally fires early, re-observes the wait, and
* must enqueue the next hop. The key is suffixed with the hop index
* (`ceil(timeoutSeconds / maxDelay)`): stable for every re-observation
* within the same hop window (so passes dedupe), decremented at each
* hop delivery (so the chain always advances). Worlds without a delay
* limit (world-postgres, world-local) simply take the same ≤23h hops.
*
* - Near-elapsed waits (≤2s remaining) get a second-bucketed suffix. A
* continuation delivered marginally early (clock skew between the
* enqueuing and handling hosts; the ceil() on the delay can leave a ~0
* margin) re-observes the wait as pending with ~1s remaining and must
* be able to enqueue a fresh short-delay retry. The bucket suffix
* keeps that retry enqueueable (its ≥1s delay guarantees a later
* bucket) while still collapsing same-instant duplicates.
*
* Mid-range waits (more than the near-elapsed threshold, at most one
* hop) use the bare correlationId: every re-observation targets the same
* deadline, so deduping to the first message is semantically lossless.
* Host clock skew beyond the near-elapsed threshold could in principle
* deliver such a continuation early enough to re-observe its wait and
* lose the re-enqueue to the burnt key; the threshold is the skew
* tolerance we accept for the benefit of exactly-one continuation per
* wait.
*/
/**
* Maximum `delaySeconds` for a single wait-continuation message. Waits
* longer than this are chained across multiple hops. 23 hours: VQS
* messages have a 24h retention limit; the 1h buffer mirrors
* world-vercel's `MAX_DELAY_SECONDS`.
*/
export declare const WAIT_CONTINUATION_MAX_DELAY_SECONDS = 82800;
/**
* Waits with at most this many seconds remaining use a second-bucketed
* idempotency key so an early-delivered continuation can re-enqueue its
* short-delay retry. This is also the host clock-skew tolerance for
* mid-range waits keyed on the bare correlationId.
*/
export declare const NEAR_ELAPSED_WAIT_THRESHOLD_SECONDS = 2;
export interface WaitContinuationDispatch {
delaySeconds: number;
idempotencyKey: string;
}
/**
* Computes the queue delay and idempotency key for a wait-continuation
* message. `timeoutSeconds` is the time until the wait's `resumeAt`
* (floored at 1s by the suspension handler); `waitCorrelationId`
* identifies the wait so repeated suspension passes dedupe.
*/
export declare function getWaitContinuationDispatch(timeoutSeconds: number, waitCorrelationId: string, now?: number): WaitContinuationDispatch;
//# sourceMappingURL=wait-continuation.d.ts.map