/**
 * Server-side polling for Harness pipeline executions.
 *
 * Used by harness_execute when the caller passes `wait: true` so the LLM
 * doesn't have to repeatedly call `harness_get` to check status. One MCP tool
 * call triggers the pipeline and returns when the execution reaches a terminal
 * status (or the timeout fires).
 *
 * Pure logic — receives the Registry + HarnessClient via DI so future callers
 * (e.g. a standalone `wait` execute action on `execution`) can reuse it.
 */
import { asNumber, asRecord, asString } from "./type-guards.js";
import { createLogger } from "./logger.js";
const log = createLogger("poll-execution");
/**
 * Statuses Harness considers terminal for a pipeline execution.
 * Anything else (Running, Queued, Paused, *Waiting, etc.) keeps polling.
 *
 * NOTE: `ApprovalWaiting`, `InterventionWaiting`, and `InputWaiting` are NOT
 * terminal — Harness keeps the execution "alive" pending human action. We poll
 * through them so wait blocks until a real outcome.
 */
export const TERMINAL_STATUSES = new Set([
    "Success",
    "Failed",
    "Aborted",
    "Expired",
    "Errored",
    "AbortedByFreeze",
    "IgnoreFailed",
    "Skipped",
]);
/** Statuses that indicate the execution failed and is worth diagnosing. */
export const FAILURE_STATUSES = new Set([
    "Failed",
    "Errored",
    "Aborted",
    "Expired",
    "AbortedByFreeze",
    "IgnoreFailed",
]);
class AbortError extends Error {
    constructor() {
        super("Polling aborted");
        this.name = "AbortError";
    }
}
/**
 * Sleep for `ms`, rejecting early if the signal aborts.
 * Cleans up listeners and timers in both branches to avoid leaks across long polls.
 */
function abortableSleep(ms, signal) {
    return new Promise((resolve, reject) => {
        if (signal?.aborted) {
            reject(new AbortError());
            return;
        }
        const timer = setTimeout(() => {
            if (signal)
                signal.removeEventListener("abort", onAbort);
            resolve();
        }, ms);
        const onAbort = () => {
            clearTimeout(timer);
            reject(new AbortError());
        };
        if (signal)
            signal.addEventListener("abort", onAbort, { once: true });
    });
}
/**
 * Compute next poll interval using exponential backoff.
 * Capped by maxIntervalMs. Multiplier 1.5 — gentle ramp that still slows
 * polling on long-running deploys.
 */
export function nextPollIntervalMs(initialMs, maxMs, pollCount) {
    const next = initialMs * Math.pow(1.5, pollCount);
    return Math.min(Math.floor(next), maxMs);
}
/**
 * Extract the bits we need from `execution.get` response shape:
 *   { pipelineExecutionSummary: { status, startTs, endTs, name, pipelineIdentifier, ... }, openInHarness, ... }
 */
function snapshotFromExecution(raw) {
    const root = asRecord(raw) ?? {};
    const pes = asRecord(root.pipelineExecutionSummary) ?? {};
    return {
        status: asString(pes.status),
        startTs: asNumber(pes.startTs),
        endTs: asNumber(pes.endTs),
        pipelineName: asString(pes.name),
        pipelineIdentifier: asString(pes.pipelineIdentifier),
        openInHarness: asString(root.openInHarness),
    };
}
/**
 * Poll a pipeline execution until it reaches a terminal status or the timeout fires.
 *
 * Errors during individual polls are tolerated for a few attempts (logged at
 * warn level) — Harness sometimes returns 5xx for a few seconds while an
 * execution is spinning up, and that shouldn't fail the whole wait.
 *
 * If the signal aborts, the function rejects with an AbortError so callers
 * can surface a "cancelled by client" message.
 */
export async function pollExecutionToTerminal(registry, client, opts) {
    const startedAt = Date.now();
    let pollCount = 0;
    let lastSnapshot = {};
    let consecutiveErrors = 0;
    const MAX_CONSECUTIVE_ERRORS = 5;
    // Initial sleep — gives the execution a moment to be persisted by Harness
    // before we ask for its status.
    await abortableSleep(opts.initialIntervalMs, opts.signal);
    while (true) {
        if (opts.signal?.aborted)
            throw new AbortError();
        const elapsed = Date.now() - startedAt;
        if (elapsed >= opts.timeoutMs) {
            return buildResult(opts.executionId, lastSnapshot, false, true, elapsed, pollCount);
        }
        pollCount++;
        try {
            const raw = await registry.dispatch(client, "execution", "get", {
                execution_id: opts.executionId,
                org_id: opts.orgId,
                project_id: opts.projectId,
                render_full_graph: false,
            }, opts.signal);
            lastSnapshot = snapshotFromExecution(raw);
            consecutiveErrors = 0;
            const status = lastSnapshot.status ?? "Unknown";
            try {
                await opts.onPoll?.(status, Date.now() - startedAt, pollCount);
            }
            catch (err) {
                log.warn("onPoll callback threw — ignoring", { error: String(err) });
            }
            if (TERMINAL_STATUSES.has(status)) {
                return buildResult(opts.executionId, lastSnapshot, true, false, Date.now() - startedAt, pollCount);
            }
        }
        catch (err) {
            if (err instanceof AbortError)
                throw err;
            consecutiveErrors++;
            log.warn("Poll failed, retrying", {
                executionId: opts.executionId,
                pollCount,
                consecutiveErrors,
                error: String(err),
            });
            if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
                throw new Error(`Polling execution ${opts.executionId} failed after ${consecutiveErrors} consecutive attempts: ${String(err)}`);
            }
        }
        // Schedule next poll.  If the next sleep would overshoot the timeout,
        // sleep only the remainder so we return promptly with timed_out=true.
        const interval = nextPollIntervalMs(opts.initialIntervalMs, opts.maxIntervalMs, pollCount);
        const remaining = opts.timeoutMs - (Date.now() - startedAt);
        if (remaining <= 0) {
            return buildResult(opts.executionId, lastSnapshot, false, true, Date.now() - startedAt, pollCount);
        }
        await abortableSleep(Math.min(interval, remaining), opts.signal);
    }
}
function buildResult(executionId, snapshot, isTerminal, timedOut, elapsedMs, pollCount) {
    return {
        execution_id: executionId,
        status: snapshot.status ?? "Unknown",
        is_terminal: isTerminal,
        timed_out: timedOut,
        elapsed_ms: elapsedMs,
        poll_count: pollCount,
        started_at: snapshot.startTs ? new Date(snapshot.startTs).toISOString() : undefined,
        ended_at: snapshot.endTs ? new Date(snapshot.endTs).toISOString() : undefined,
        pipeline: {
            name: snapshot.pipelineName,
            identifier: snapshot.pipelineIdentifier,
        },
        openInHarness: snapshot.openInHarness,
    };
}
export { AbortError };
//# sourceMappingURL=poll-execution.js.map