import type { CreateEventParams, CreateEventRequest, Event, EventResult, HealthCheckPayload, RunDispatchContext, ValidQueueName, WorkflowRun, World } from '#compiled/@workflow/world/index.js';
import { type PayloadKey } from '../serialization/encryption.js';
/**
 * Validates a workflow name and returns the corresponding queue name.
 * Ensures the workflow name only contains safe characters before
 * interpolating it into the queue name string.
 */
export declare function getWorkflowQueueName(workflowName: string, namespace?: string): ValidQueueName;
/**
 * Result of a health check operation.
 */
export interface HealthCheckResult {
    healthy: boolean;
    /** Error message if health check failed */
    error?: string;
    /** Latency if the health check was successful */
    latencyMs?: number;
    /** Spec version of the responding deployment */
    specVersion?: number;
    /**
     * `@workflow/core` version of the responding deployment, used for
     * capability detection (see `getRunCapabilities`). Omitted when the
     * responding deployment did not provide the field as a string:
     * for example, an older `@workflow/core` that predates this field,
     * or a non-JSON plain-text health response.
     */
    workflowCoreVersion?: string;
    /**
     * The target run's X25519 public key (base64), returned only when the probe
     * carried a `runId` and the responding deployment has encryption enabled.
     *
     * Lets a cross-deployment `start()` seal the workflow arguments using a
     * response it was already waiting on, instead of making a separate
     * key-lookup request.
     */
    encryptionPublicKey?: string;
    /**
     * The responding deployment's `HOOK_RESUME_INPUT_VERSION`: the protocol
     * version at which the *consumer* (queue-message target) materializes the
     * `hook_received` event from `hookInput` on replay. A cross-deployment
     * `start()` stamps the *target's* value (not the caller's) into the new
     * run's `executionContext.hookResumeInputVersion`. Current producers write
     * the event durably before publishing the wake and do not read the marker;
     * OLDER producers still gate their lazy path on it, so it keeps being
     * stamped. Omitted when the responding deployment predates this field,
     * which fails that gate closed.
     */
    hookResumeInputVersion?: number;
}
/**
 * Checks if the given message is a health check payload.
 * If so, returns the parsed payload. Otherwise returns undefined.
 */
export declare function parseHealthCheckPayload(message: unknown): HealthCheckPayload | undefined;
/**
 * Handles a health check message by writing the result to the world's stream.
 * The caller can listen to this stream to get the health check response.
 *
 * @param healthCheck - The parsed health check payload
 */
export declare function handleHealthCheckMessage(healthCheck: HealthCheckPayload, worldSpecVersion?: number): Promise<void>;
export interface HealthCheckOptions {
    /** Timeout in milliseconds to wait for health check response. Default: 30000 (30s) */
    timeout?: number;
    /** Deployment ID to send the health check to. Falls back to process.env.VERCEL_DEPLOYMENT_ID. */
    deploymentId?: string;
    /**
     * The run id the caller is about to create. When set, the responding
     * deployment derives that run's public key locally and returns it as
     * `encryptionPublicKey`, letting a cross-deployment `start()` seal the
     * workflow arguments without a separate key lookup.
     */
    runId?: string;
    /**
     * Queue namespace of the target deployment (e.g. `'eve'` for topics like
     * `__eve_wkf_workflow_*`). Falls back to `WORKFLOW_QUEUE_NAMESPACE` in the
     * calling process. Cross-context callers (e.g. the observability
     * dashboard) must pass the target deployment's namespace explicitly:
     * the env fallback resolves in the caller's process, and a message
     * published to a mismatched topic has no consumer, so the check would
     * always time out.
     */
    namespace?: string;
}
export declare function healthCheck(world: World, options?: HealthCheckOptions): Promise<HealthCheckResult>;
/**
 * Appends events whose IDs are not already present in `target`.
 *
 * Pass the IDs currently present in `target` when appending repeatedly to the
 * same array. The set is updated alongside `target`.
 *
 * Events are appended in the order the World returned them, and are not
 * re-sorted. Every append source is already in canonical order relative to the
 * tail (a cursor-delimited page, or a write-response delta), so receipt order is
 * the order to keep, and re-sorting here would only cost a pass over the log.
 * Nothing downstream may assume the tail is the newest event. See
 * {@link maxEventSlot}.
 */
export declare function appendUniqueEvents(target: Event[], events: readonly Event[], targetIds?: Set<string>): void;
/**
 * Inserts `event` into `target` at the position that keeps `target` ordered by
 * ascending `eventId`, or no-ops if an event with the same `eventId` is already
 * present (idempotent).
 *
 * `preloadedEvents` is loaded `sortOrder: 'asc'` and is never re-sorted
 * client-side, so a `hook_received` spliced in by the lazy-resume consumer must
 * land in `eventId` order: a plain `push` would place a late-committing
 * earlier event after events that sort before it, corrupting replay.
 *
 * Lexicographic string order is the log's order: a slot id is a fixed-width
 * zero-padded position, so comparing the strings compares the positions. This
 * needs no parse of its own for that reason, and the comparison is exact rather
 * than a reconstruction.
 */
export declare function insertEventByEventId(target: Event[], event: Event): void;
/**
 * Loads workflow run events by iterating through all pages of paginated
 * results. Events are returned in chronological (ascending) order for
 * deterministic workflow replay.
 *
 * @param runId - The workflow run ID.
 * @param afterCursor - If provided, only events after this cursor are
 *   returned (incremental load). If omitted, all events are returned.
 *   The returned cursor can be passed back in on a subsequent call for
 *   incremental loading.
 */
export declare function loadWorkflowRunEvents(runId: string, afterCursor?: string): Promise<LoadedEventLog>;
/**
 * The runtime's loaded event-log snapshot: the events replayed so far and the
 * cursor positioned after them. Handed to helpers that derive the precondition
 * snapshot from it; they do not mutate it.
 */
export interface LoadedEventLog {
    events: Event[];
    cursor: string | null;
}
/**
 * Extend a loaded log with a page that continues it — a listed page, or the
 * inline delta a write handed back — and move its read position with it.
 *
 * The cursor is only advanced when the page carries one, so a source with no
 * position of its own (a skipped-slot report) cannot walk the read position
 * past events it did not carry. Appending does not re-sort; see
 * {@link appendUniqueEvents} for why receipt order is the order to keep.
 */
export declare function appendEventLog(log: LoadedEventLog, appended: {
    events: readonly Event[];
    cursor?: string | null;
}): void;
/**
 * Whether a replay refuses to run over a log with a hole in it (see
 * {@link findEventSlotGap}). **On by default**; set
 * `WORKFLOW_SLOT_GAP_CHECK=0` to replay across holes instead.
 *
 * A World that allocates a position at the moment it commits leaves no hole
 * behind when a write fails, so density is a property the log has by
 * construction rather than one this check maintains. What the check is for is
 * the reads and the Worlds where that does not hold, and by the time it runs
 * the benign explanations are spent: a position missing because a concurrent
 * commit is not visible yet clears on a re-read, which is what
 * {@link settleEventSlotGap} does first.
 *
 * What is left is a hole that persists, and its two causes are
 * indistinguishable from the log. Either a World allocated the position outside
 * the commit and lost the write, in which case nothing happened there and
 * replaying past it is correct, or an event that did happen is missing, in
 * which case replaying past it decides a branch on absence and produces a wrong
 * result with nothing to show for it. Failing is the recoverable side of that
 * trade, and this is the way back out if a fleet turns out to carry holes of
 * the first kind.
 */
export declare function isSlotGapCheckEnabled(): boolean;
/**
 * Merge the events a bump-and-report write handed back into the log it was
 * derived from, and answer how many of them were new.
 *
 * Unlike {@link appendUniqueEvents}, this re-sorts. The reported events occupy
 * slots *below* the write that reported them, so appending them would put them
 * after events they precede. Sorting by id restores the World's canonical order
 * rather than guessing at it: a slot id is that order, written down.
 */
export declare function mergeReportedEvents(target: Event[], events: readonly Event[]): number;
/** What {@link absorbSkippedSlotReport} did with a write's report. */
export interface SkippedSlotReport {
    /** How many of the reported events were new to the log. Zero if dropped. */
    added: number;
    /** How many events the report offered, whether or not they were taken. */
    offered: number;
    /** The report was truncated, so it was dropped whole instead of merged. */
    truncated: boolean;
}
/**
 * Apply a write's skipped-slot report to the log it was derived from, deciding
 * whether the report may be taken at all.
 *
 * Every replay-context write can come back carrying the events on the slots it
 * skipped over, and every caller wants the same thing from them: fold them in
 * so the writes that follow name a position above them, and so the replay
 * resuming from this log sees them without a reload.
 *
 * The one policy is that a **truncated report is dropped whole**. It covers a
 * span of positions but carries only some of the events on them, so merging it
 * would raise the log's highest position past a position whose event is
 * missing. Later writes read that maximum to say what they have seen, so each
 * would claim a position it never saw, and the World only reports the span a
 * write skips: it would never send the missing one. Dropping costs one more
 * round of the same events on the next write and keeps the log a prefix of the
 * truth, which the note above {@link mergeReportedEvents} explains is always an
 * available answer.
 *
 * Callers that log do so from the returned counts; the decision is not theirs
 * to re-derive.
 */
export declare function absorbSkippedSlotReport(target: Event[], result: {
    events?: readonly Event[];
    hasMore?: boolean;
}): SkippedSlotReport;
/**
 * The highest slot the loaded log occupies, or `undefined` for an empty log.
 *
 * The maximum, not the count, and the two are not interchangeable even though
 * a healthy log makes them equal. A World hands a position to the insert that
 * occupies it, so a write that never lands leaves no hole behind and the log
 * stays dense. What the count cannot survive is a *partial* read: a log
 * assembled from a truncated report, or read while a concurrent write is
 * committing, holds fewer events than its highest position. Counting those
 * would make the next write claim to have seen less than it has, so the World
 * would report the same events back to it on every attempt.
 *
 * A hole below the maximum is therefore a property of the read, not of the log,
 * which is what lets {@link settleEventSlotGap} re-read instead of giving up.
 *
 * @throws if any event id carries no slot. Every World the runtime replays
 * against numbers events by slot, so an id that does not is a broken log rather
 * than an older one, and a maximum derived by skipping it would understate the
 * log to every write that reads it.
 */
export declare function maxEventSlot(events: readonly Event[]): number | undefined;
/** A position the log skips over, described well enough to name in an error. */
export interface EventSlotGap {
    /** The lowest slot below the log's maximum that no event occupies. */
    firstMissingSlot: number;
    /** How many slots below the maximum no event occupies. */
    missingCount: number;
    /** The highest slot the log occupies. */
    maxSlot: number;
}
/**
 * The hole in a loaded log, or `undefined` when there is none to find.
 *
 * The World allocates every position, so a log that holds `n` events below slot
 * `n` is missing one. That matters before a replay
 * and nowhere else: the replay reads the log as the complete record of what has
 * happened, and an absent position is indistinguishable from an event that
 * never occurred. The branch it would have decided gets decided the other way,
 * and the run diverges quietly rather than failing.
 *
 * Order-independent, unlike the equivalent audit the World runs over a page it
 * just read. A loaded log is assembled from listed pages plus whatever a
 * bump-and-report write handed back, and while {@link mergeReportedEvents}
 * restores id order, a check that can fail a healthy run should not depend on
 * that having happened.
 *
 * The first slot is never counted. It belongs to `run_created`, which `start()`
 * posts concurrently with the queue send, so a log read in that window
 * legitimately begins at the second slot and fills in on its own. Every replay
 * that races a run's own start would otherwise report a hole.
 *
 * Returns `undefined` for an empty log, which has no density to check.
 *
 * @throws if any event id carries no slot, for the reason {@link maxEventSlot}
 * gives.
 */
export declare function findEventSlotGap(events: readonly Event[]): EventSlotGap | undefined;
/**
 * How many times a detected hole is re-read before the log is taken at its
 * word, and the backoff before each re-read (doubling per attempt).
 *
 * A hole can be transient. The World allocates a slot inside the insert that
 * occupies it, so two concurrent writers can collide, one retry past the other,
 * and the higher slot commit first, leaving a window in which the lower one is
 * genuinely absent from a strongly-consistent read and fills in a moment later.
 * The window is one commit wide, so a short backoff clears it; anything that
 * survives all three re-reads is a position no write will ever occupy.
 */
export declare const SLOT_GAP_RECHECK_ATTEMPTS = 3;
/**
 * Re-read a log that looks holey until the hole fills in or the re-reads run
 * out, and return the settled log alongside the hole that survived.
 *
 * Reads are strongly consistent, so a hole is not an artifact of *when* the log
 * was read, but it can be an artifact of a write that had not committed yet
 * (see {@link SLOT_GAP_RECHECK_ATTEMPTS}). Distinguishing the two costs a
 * re-read, which is only ever paid by a replay that already found a hole.
 *
 * The reload is full rather than incremental: the missing position is below the
 * log's maximum, so a cursor-anchored read starts past it and can never see it
 * arrive.
 */
export declare function settleEventSlotGap(runId: string, loaded: LoadedEventLog): Promise<{
    log: LoadedEventLog;
    gap: EventSlotGap | undefined;
}>;
/**
 * How much of its run's log a replay-context event creation had loaded when it
 * decided to write, as the highest slot that log occupies.
 *
 * One integer says it because the World keeps its positions dense: a writer
 * that names slot N is claiming to hold every event from 1 to N and nothing
 * above. The World answers by numbering the write above whatever the log has
 * actually reached and handing back the events on the slots in between: the
 * ones this writer decided without.
 *
 * Density is the World's invariant, not a claim about this particular read. A
 * reader that is short of a position it holds no event for names a lower N,
 * which understates what it has seen and only costs it a wider report. Naming
 * a position it cannot account for is the direction that is unsafe, which is
 * why {@link slotSnapshotParams} takes the maximum rather than the count.
 *
 * Its own object rather than a bare number so a call site cannot half-send it,
 * and so the empty case spreads to nothing.
 */
export interface SlotSnapshotParams {
    eventCount?: number;
}
/**
 * Build the slot snapshot to attach to a replay-context event creation.
 *
 * Empty for an empty log, which is the state a `run_created` write is issued
 * from: there is no position held yet to name.
 *
 * ## Who names a position
 *
 * A World can hand events back on a write's success response through two
 * params (`CreateEventParams` in `@workflow/world`): `eventCount`, which is
 * answered with the events on the positions the write skipped over (the
 * skipped-slot report), and `sinceCursor`, which is answered with everything
 * after that cursor (the inline delta). Both exist to save the writer a
 * reload. So the rule for which writes send them is not about the event type;
 * it is about whether **the process that writes holds a loaded log it will
 * keep deciding from**, its own next writes or the replay it resumes. A writer
 * with no log has nothing to merge a page into, and whoever decides next is a
 * fresh replay that loads the log anyway.
 *
 * Three gates, all of which must pass for a page to come back:
 *
 * 1. The writer names a position. Only the replay loop's `createEvent` seam
 *    (runtime.ts) and the suspension handler's `createGuarded` do; the QuickJS
 *    engine's `createEvent` (quickjs-entrypoint.ts) follows the same rule. Raw
 *    `world.events.create` calls send nothing.
 * 2. The World reads a page for that event type. A World may decline for types
 *    whose writer never holds a log (step executor writes, run-terminal
 *    writes), whatever the client sent.
 * 3. There is something to hand back: the report only when the write landed
 *    more than one position above `eventCount`; the delta always.
 *
 * | Event | Writer | Sends | Why |
 * |---|---|---|---|
 * | `run_created` | `start()` | nothing | no log exists yet |
 * | `run_started` | replay loop, first write of a delivery | `eventCount` when a log is loaded; no cursor, the `run_started` preload owns the response fields | the preload already returns the full log |
 * | `step_created`, `wait_created`, `hook_disposed`, `attr_set` | suspension handler (`createGuarded`) | `eventCount` | the handler keeps writing from, and resumes replay from, this log |
 * | `step_created` | queue-consumer re-ensure (raw) | nothing | runs from a queue message, no log |
 * | `hook_created` | suspension handler (node), `dispatchPendingOps` (QuickJS) | `eventCount` + `sinceCursor` (only when the suspension creates exactly one hook) | the delta is how an already-received hook resolves without a re-invocation; two creates against one cursor would give two deltas of which only the first could be taken |
 * | `hook_received` | replay loop lazy resume; handler in-suspension resume | `eventCount` (preload owns the cursor fields) | both replay from the log right after |
 * | `hook_received` | webhook / `resumeHook` (raw) | nothing | out-of-band, enqueues a delivery that loads the log |
 * | `wait_completed` | replay loop timer path | `eventCount` | the loop keeps replaying from the log |
 * | `wait_completed` | `runs.*` out-of-band (raw) | nothing | no log |
 * | `step_started`, `step_retrying` | step executor | nothing | no log; the next decision is a replay that reloads or receives the delta below |
 * | `step_completed`, `step_failed` | step executor | `sinceCursor` only, outside turbo, single inline step | this is where the inline execution loop gets its log back; in turbo or from a queued delivery a fresh replay loads it anyway |
 * | `step_failed` | suspension handler, unserializable input | `eventCount` | the handler holds a log; rare, and follows a `step_created` that drew the page |
 * | `attr_set` | `setAttributes()` API (raw) | nothing | out-of-band |
 * | `run_completed`, `run_failed`, `run_cancelled` | replay loop, replay budget, `runs.cancel` | `eventCount` where the node loop's seam stamps it (the QuickJS engine sends nothing); never `sinceCursor` (`deltaRequestCursor` excludes terminal types) | terminal: nothing replays the log afterwards, so a World reads no page for them |
 *
 * Batched writes (`events.createBatch`) are outside all of this: a batch carries
 * no per-event position and gets no page. The user-facing version of this table
 * is in `docs/content/docs/v5/how-it-works/event-sourcing.mdx`; keep the two in
 * step.
 *
 * The maximum rather than the length, for the reason {@link maxEventSlot}
 * gives: a partially-read log holds fewer events than its highest position, and
 * counting those would make the write claim to have seen less than it has, so
 * the World would report the same events back on every attempt.
 */
export declare function slotSnapshotParams(events: readonly Event[]): SlotSnapshotParams;
/**
 * The events a rejecting World attached to a `PreconditionFailedError`, when it
 * returned the ones the client's snapshot was missing inline.
 *
 * Returns `null` for anything else: no details, a World that did not implement
 * this, or a payload that does not narrow cleanly. Callers fall back to
 * reloading the event log, which is always correct; this is untrusted-shaped
 * data on a failure path, so nothing here is repaired.
 *
 * `runId` is the caller's run. Every event must belong to it: the delta is
 * merged straight into the replay's log, and one foreign event there is a
 * corrupt log rather than a corrected one: the replay would consume a
 * correlation id for an event that does not exist on this run.
 */
export declare function preconditionEventDelta(error: unknown, runId: string): {
    events: Event[];
    cursor: string | null;
} | null;
/** Creates one event on a bound run, carrying replay-recovery telemetry. */
export type EventCreator = (data: CreateEventRequest, params?: CreateEventParams) => Promise<EventResult>;
/**
 * Wraps a request/response handler and adds a health check "mode"
 * based on the presence of a `__health` query parameter.
 */
export declare function withHealthCheck(handler: (req: Request) => Promise<Response>, worldSpecVersion?: number): (req: Request) => Promise<Response>;
/**
 * The lineage root of a loaded run: its `$rootRunId` attribute, or its own id
 * when it is itself a root.
 */
export declare function rootRunIdFrom(attributes: Record<string, string> | undefined, runId: string): string;
/**
 * The immutable run identity a step-execution message carries so its consumer
 * can start the step without a blocking `runs.get` — see
 * `RunDispatchContextSchema` in @workflow/world. Built at dispatch time from
 * the run row the producer already holds.
 */
export declare function runDispatchContext(run: Pick<WorkflowRun, 'runId' | 'deploymentId' | 'specVersion' | 'startedAt' | 'attributes'>): RunDispatchContext;
/**
 * Idempotency key for a step's background-dispatch queue message, scoped to
 * the step's IDENTITY, correlation id plus (hashed) step name, rather than
 * the bare correlation id.
 *
 * The scoping matters for resilient step dispatch under the precondition
 * guard: a guard-rejected `step_created` leaves its (revoked) step message in
 * flight, and the corrected replay may re-derive the same correlation id for
 * a DIFFERENT step. Under a bare-correlationId key the corrected replay's
 * dispatch would silently dedupe against the revoked in-flight message,
 * which then resolves `skipped` against the re-created entity (the server's
 * stepName fence rejects its bare start), and the legitimate step would
 * never be executed. Scoping by step name keeps every dedup property that
 * matters (crash recovery re-dispatch, concurrent handlers, the delayed
 * retry sharing the suspension re-dispatch's key: all name the same step)
 * while letting the corrected schedule's dispatch through.
 *
 * Every producer of a step-dispatch (or step-retry) message must use this
 * key. Cross-version mixing is not a concern: queue messages are pinned to
 * the deployment that produced them, so one run never sees two key schemes.
 */
export declare function stepDispatchIdempotencyKey(correlationId: string, stepName: string): string;
/**
 * Queues a message to the specified queue with tracing.
 */
export declare function queueMessage(world: World, ...args: Parameters<typeof world.queue>): Promise<void>;
/**
 * Publishes several messages to one logical queue, using the World's batch
 * send when it has one and falling back to concurrent single sends when it
 * does not.
 *
 * Rejects if ANY message failed to publish, because every caller so far wants
 * all-or-nothing: the recovery is to fail the delivery and let redelivery
 * republish the whole set, deduped by the per-message `idempotencyKey`. That
 * means a partial batch can leave some messages already out — which is
 * exactly why the keys are required rather than advisory.
 */
export declare function queueMessages(world: World, queueName: Parameters<typeof world.queue>[0], messages: readonly {
    message: Parameters<typeof world.queue>[1];
    opts?: Parameters<typeof world.queue>[2];
}[]): Promise<void>;
/**
 * Calculates the queue overhead time in milliseconds for a given message.
 */
export declare function getQueueOverhead(message: {
    requestedAt?: Date;
}): {
    [k: string]: number;
} | undefined;
/**
 * Resolve the run's full payload-encryption capability. This includes the
 * symmetric key and X25519 keypair needed to open cross-run sealed payloads.
 * Missing world support or key material resolves to `undefined`; lookup and
 * derivation failures propagate rather than silently falling back to plaintext.
 */
export declare function resolveRunEncryptionKey(world: World, runOrId: WorkflowRun | string, context?: Record<string, unknown>): Promise<PayloadKey | undefined>;
/**
 * Return a lazy, memoized accessor around {@link resolveRunEncryptionKey}.
 * Concurrent callers share the same promise, including its rejection.
 */
export declare function memoizeEncryptionKey(world: World, runOrId: WorkflowRun | string): () => Promise<PayloadKey | undefined>;
//# sourceMappingURL=helpers.d.ts.map