/**
 * Resume + reconcile algorithm (events doc v0.1.2 §4.3 + §4.3.1).
 *
 * Entry point for daemon restart / hand-off.  Walks the event log,
 * replays a snapshot, then drives reconcile decisions for each dangling
 * `effectAttempted` and writes terminal events for `pure skill`
 * activities that crashed mid-flight (workerLost path).
 *
 * Step 7 boundaries:
 *   - Resume DOES NOT execute activity logic; reconcile uses provider
 *     capabilities (`readOnlyLookup` / `idempotentSubmit`) to decide
 *     terminal state without re-issuing user-visible work beyond what
 *     idempotency guarantees.
 *   - Resume DOES NOT decide retry policy.  A `freshRetry` decision
 *     leaves the attempt dangling — the scheduler (Step 8+) is
 *     responsible for spawning the actual replacement attempt.
 *   - Dangling waits are left alone (waiting for external signal).
 *
 * Round 1 fixes (codex review of `1d14081`):
 *   F1 — replay surfaces the latest reconcileResult per attempt; resume
 *        consumes it before re-running the decision tree, so a crash
 *        between reconcileResult and the terminal event is recoverable.
 *   F2 — reconcilers receive the materialized effect input via the
 *        caller-supplied `loadEffectInput` callback.  Reconcilers that
 *        require input (e.g. Feishu — chatId/rootMessageId/content can't
 *        be reconstructed from idempotencyKey alone) fail explicitly
 *        when input is unrecoverable.
 *   F3 — `retryable` failures from idempotentSubmit do NOT terminate
 *        the attempt; the activity stays dangling and is surfaced in
 *        `ResumeResult.transientFailures` for the caller to retry.
 *   F4 — `resumeStarted` is written ONLY after a preflight validates
 *        the log is replayable; bad inputs throw without polluting the
 *        run event log.
 */
import type { EventLog } from './events/append.js';
import { type Snapshot, type AttemptState } from './events/replay.js';
import type { ActivityCanceledEvent, ActivityFailedEvent, ActivitySucceededEvent, ReconcileResultEvent, ResumeStartedEvent } from './events/types.js';
export type ReconcileCapability = 'readOnlyLookup' | 'idempotentSubmit' | 'none';
export type ReconcileDecision = 'replayed' | 'completedByIdempotentSubmit' | 'manual' | 'freshRetry';
export type ReadOnlyLookupResult = {
    found: true;
    externalRefs: Record<string, unknown>;
    evidence?: Record<string, unknown>;
} | {
    found: false;
    evidence?: Record<string, unknown>;
};
export type IdempotentSubmitResult = {
    ok: true;
    externalRefs: Record<string, unknown>;
    evidence?: Record<string, unknown>;
} | {
    ok: false;
    errorCode: string;
    errorClass: 'retryable' | 'fatal' | 'userFault' | 'manual';
    errorMessage: string;
    evidence?: Record<string, unknown>;
};
/**
 * Per-provider capability bundle.  Resume looks up the reconciler by the
 * `effectAttempted.provider` field; missing entries fall through to
 * manual/UnknownProviderError.
 *
 * Reconcilers receive the materialized effect input alongside the
 * idempotencyKey: providers like Feishu can't re-construct the request
 * body from the key alone (the key is a hash, not the body).  When the
 * caller doesn't supply `loadEffectInput`, resume treats the input as
 * `undefined`; reconcilers that NEED input MUST declare so via
 * `requiresEffectInput` so resume can fail fast with a clear error
 * instead of letting the reconciler silently misbehave.
 */
export interface ProviderReconciler {
    readonly provider: string;
    /**
     * When `true`, resume refuses to call this reconciler without a
     * materialized effect input — i.e. it writes manual/InputUnrecoverable
     * if `loadEffectInput` is absent or throws.  Feishu sets this; schedule
     * does not (idempotencyKey is the full key).
     */
    readonly requiresEffectInput?: boolean;
    /**
     * Pure read against the provider keyed by `idempotencyKey`.  Has no
     * side effects; safe to call from resume even when we don't intend to
     * complete the effect.  Schedule has it (`getTask(id)`); Feishu does
     * not (no uuid-reverse-lookup API).
     */
    readOnlyLookup?(idempotencyKey: string, input: unknown): Promise<ReadOnlyLookupResult>;
    /**
     * Re-submit the effect with the same `idempotencyKey`.  MAY produce
     * the side effect for real (if the original pre-invoke crash never
     * reached the provider); provider dedupe inside TTL guarantees the
     * second submit returns the original ref instead of a duplicate.
     */
    idempotentSubmit?(idempotencyKey: string, input: unknown): Promise<IdempotentSubmitResult>;
    /**
     * Canonical form of the loaded effect input — used at resume time to
     * recompute `inputHash` and compare against `effectAttempted.inputHash`.
     * MUST mirror the executor's `canonicalInput` exactly; mismatched
     * canonicalization across resume/dispatch silently breaks idempotency.
     *
     * Reconcilers with `requiresEffectInput=true` SHOULD implement this;
     * resume writes `IdempotencyInputMismatch/manual` if a tampered or
     * drifted sidecar would otherwise reach the provider with a different
     * body than the original attempt promised.
     */
    canonicalInput?(input: unknown): unknown;
}
export type ResumeContext = {
    /** Authoritative event log for this run.  Resume writes events into it. */
    log: EventLog;
    /** Match `log.runId`; passed explicitly so the contract is visible. */
    runId: string;
    /** Daemon identifier for the resumeStarted audit event. */
    daemonId: string;
    /** Reconcilers keyed by provider name (`feishu-im`, `botmux-schedule`). */
    reconcilers: Map<string, ProviderReconciler>;
    /**
     * Load the materialized effect input that was passed to the original
     * attempt.  Required for providers that re-submit (Feishu).  Resume
     * passes the returned value to the reconciler's readOnlyLookup /
     * idempotentSubmit.
     *
     * v0: the caller (daemon) decides where to persist or recover this —
     * in-memory while alive, or some external storage on cold start.
     * Resume only consumes the callback.
     *
     * Returning `undefined` is treated as "input unrecoverable" and
     * triggers the manual/InputUnrecoverable path for reconcilers that
     * declared `requiresEffectInput`.
     */
    loadEffectInput?(activityId: string, attemptId: string): Promise<unknown>;
    /** Injectable clock for deterministic tests.  Defaults to Date.now. */
    now?: () => number;
};
export type ReconcileOutcome = {
    activityId: string;
    attemptId: string;
    idempotencyKey: string;
    provider: string;
    capability: ReconcileCapability;
    decision: ReconcileDecision;
    evidence: Record<string, unknown>;
    /**
     * Terminal event written as a consequence.  null for `replayed` (the
     * pre-existing terminal IS the consequence) and `freshRetry` (scheduler
     * issues a new attempt later, not Step 7's job).
     */
    terminalEvent: ActivitySucceededEvent | ActivityFailedEvent | null;
    /** The reconcileResult event written, or null if this outcome reused
     *  a pre-existing reconcileResult (recovery path — codex F1). */
    reconcileEvent: ReconcileResultEvent | null;
    /** True when this outcome recovered a prior crashed reconcile cycle
     *  rather than running the decision tree from scratch. */
    recovered: boolean;
};
export type WorkerCrashedOutcome = {
    activityId: string;
    attemptId: string;
    terminalEvent: ActivityFailedEvent;
};
/**
 * Recovery of a wait whose resolution event landed but whose activity
 * terminal was never written (crash between `waitResolved` /
 * `waitDeadlineExceeded` and the terminal).  Step 8: replay surfaces
 * these as `Snapshot.danglingWaitResolutions`; resume materializes the
 * terminal from the recorded resolution.
 */
export type WaitRecoveryOutcome = {
    activityId: string;
    attemptId: string;
    /** What the recovery decided to write. */
    kind: 'succeeded' | 'failed';
    source: 'resolved' | 'deadlineExceeded';
    terminalEvent: ActivitySucceededEvent | ActivityFailedEvent;
};
/**
 * Recovery of a cancel whose request landed but whose activity
 * terminal was never written (crash between `cancelRequested` /
 * `cancelDelivered` and the terminal).  Step 9: replay surfaces these
 * as `Snapshot.danglingCancels`.
 *
 * The terminal resume writes depends on whether the cancelled
 * attempt also had a dangling `effectAttempted` (Step 9 round 1
 * finding 1):
 *
 *   - **No effectAttempted** (plain cancel): resume writes
 *     `activityCanceled` directly, with `cancelOriginEventId`
 *     pointing at the originating `cancelRequested`.
 *   - **effectAttempted present** (cancel + effect): resume FIRST
 *     runs reconcile to capture provider evidence (writes
 *     `reconcileResult` with cancel-coupled keys — see
 *     payloads.ts `ReconcileResultPayload` doc), THEN decides:
 *       - `completedByIdempotentSubmit` / `freshRetry` →
 *         `activityCanceled` (cancel wins as terminal reason; the
 *         provider's outcome is preserved in reconcileResult evidence)
 *       - `manual` (TTL expired / unknown provider / submit errored
 *         fatally) → `activityFailed{manual}` — we deliberately do
 *         NOT write `activityCanceled` because provider state is
 *         unknown and pretending the cancel cleanly succeeded would
 *         lie to forensics
 *       - `transient` → NO terminal; the activity stays dangling and
 *         the next resume cycle retries (surfaces as a
 *         `TransientReconcileFailure` on `ResumeResult.transientFailures`)
 */
export type CancelRecoveryOutcome = {
    activityId: string;
    attemptId: string;
    cancelOriginEventId: string;
    /** True if cancelDelivered was already written; false means we
     *  short-circuited a never-delivered cancel.  Both still terminate
     *  the activity (cancel intent is authoritative). */
    delivered: boolean;
    /**
     * What terminal we ended up writing.  `cancelled` is the regular
     * cancel terminal; `failed` is the "manual escalation" path when
     * reconcile evidence is non-conclusive (Step 9 finding 1).
     */
    kind: 'cancelled' | 'failed';
    /** Present when reconcile ran alongside cancel — i.e. the activity
     *  had a dangling effectAttempted.  Pure cancels (no effect) leave
     *  this undefined. */
    reconcileEvent?: ReconcileResultEvent;
    /**
     * The reconcile decision that informed the cancel terminal (only
     * populated when the activity had a dangling effectAttempted).
     * - For `completedByIdempotentSubmit` and `freshRetry`, terminal is
     *   `activityCanceled`.
     * - For `manual`, terminal is `activityFailed{manual}`.
     */
    reconcileDecision?: ReconcileDecision;
    terminalEvent: ActivityCanceledEvent | ActivityFailedEvent;
};
/**
 * Reconcile failures that resume DELIBERATELY does not terminate (codex
 * F3): a retryable provider failure during idempotentSubmit might mean
 * "request landed, response lost", and writing a manual terminal there
 * would freeze the activity in a wrong terminal state.  Resume reports
 * these back to the caller and leaves the activity dangling so the next
 * resume cycle can retry.
 */
export type TransientReconcileFailure = {
    activityId: string;
    attemptId: string;
    provider: string;
    idempotencyKey: string;
    errorCode: string;
    errorClass: 'retryable';
    errorMessage: string;
};
export type ResumeResult = {
    resumeStartedEvent: ResumeStartedEvent;
    /** Snapshot captured after `resumeStarted` is appended.  Returned for
     *  observability — caller can inspect dangling sets it consumed. */
    snapshot: Snapshot;
    reconcileOutcomes: ReconcileOutcome[];
    workerCrashedOutcomes: WorkerCrashedOutcome[];
    transientFailures: TransientReconcileFailure[];
    waitRecoveryOutcomes: WaitRecoveryOutcome[];
    cancelRecoveryOutcomes: CancelRecoveryOutcome[];
};
export declare function resume(ctx: ResumeContext): Promise<ResumeResult>;
export type { AttemptState };
//# sourceMappingURL=resume.d.ts.map