const retryableExitCodes = new Set([1, 124, 125, 137]);
const maxBackoffMs = 30_000;
export function createSpawnRetry(spawnOnce) {
    return (service, options, retryOptions) => {
        const normalizedRetryOptions = normalizeRetryOptions(retryOptions);
        const queue = createEventQueue();
        const result = runRetryingSpawn({
            service,
            options,
            retryOptions: normalizedRetryOptions,
            spawnOnce,
            emit: queue.push
        })
            .then((value) => {
            queue.close();
            return value;
        })
            .catch((error) => {
            queue.fail(error);
            throw error;
        });
        return {
            events: queue,
            result
        };
    };
}
export function defaultIsRetryable(result) {
    return retryableExitCodes.has(result.exitCode);
}
export function calculateBackoffMs(baseBackoffMs, completedAttempt) {
    return Math.min(baseBackoffMs * 2 ** (completedAttempt - 1), maxBackoffMs);
}
function normalizeRetryOptions(retryOptions) {
    if (!Number.isInteger(retryOptions.maxAttempts) || retryOptions.maxAttempts < 1) {
        throw new Error("spawn.retry maxAttempts must be an integer greater than or equal to 1.");
    }
    if (!Number.isFinite(retryOptions.backoffMs) || retryOptions.backoffMs < 0) {
        throw new Error("spawn.retry backoffMs must be a non-negative finite number.");
    }
    return {
        maxAttempts: retryOptions.maxAttempts,
        backoffMs: retryOptions.backoffMs,
        isRetryable: retryOptions.isRetryable ?? defaultIsRetryable
    };
}
async function runRetryingSpawn(input) {
    for (let attempt = 1; attempt <= input.retryOptions.maxAttempts; attempt += 1) {
        throwIfAborted(input.options.signal);
        const handle = input.spawnOnce(input.service, input.options);
        const events = forwardAttemptEvents(handle.events, attempt, input.emit);
        const result = await handle.result;
        await events;
        const isLastAttempt = attempt >= input.retryOptions.maxAttempts;
        if (result.exitCode === 0 || isLastAttempt || !input.retryOptions.isRetryable(result)) {
            return result;
        }
        const delayMs = calculateBackoffMs(input.retryOptions.backoffMs, attempt);
        input.emit(createWaitEvent(attempt, delayMs));
        await sleep(delayMs, input.options.signal);
    }
    throw new Error("spawn.retry reached an unreachable retry state.");
}
async function forwardAttemptEvents(events, attempt, emit) {
    for await (const event of events) {
        emit(prefixEvent(event, attempt));
    }
}
function prefixEvent(event, attempt) {
    const prefix = `attempt: ${attempt}`;
    if (event.event === "agent_message" || event.event === "reasoning") {
        return { ...event, text: `${prefix} ${event.text}` };
    }
    if (event.event === "error") {
        return { ...event, message: `${prefix} ${event.message}` };
    }
    if (event.event === "tool_start") {
        return { ...event, title: `${prefix} ${event.title}` };
    }
    if (event.event === "tool_complete") {
        return { ...event, path: `${prefix} ${event.path}` };
    }
    return {
        ...event,
        _meta: {
            ...(typeof event._meta === "object" && event._meta !== null ? event._meta : {}),
            attempt
        }
    };
}
function createWaitEvent(attempt, delayMs) {
    return {
        event: "agent_message",
        text: `attempt: ${attempt} wait ${delayMs}ms before retry`
    };
}
function sleep(delayMs, signal) {
    throwIfAborted(signal);
    return new Promise((resolve, reject) => {
        const timeout = setTimeout(() => {
            signal?.removeEventListener("abort", onAbort);
            resolve();
        }, delayMs);
        const onAbort = () => {
            clearTimeout(timeout);
            reject(createAbortError());
        };
        signal?.addEventListener("abort", onAbort, { once: true });
    });
}
function throwIfAborted(signal) {
    if (signal?.aborted) {
        throw createAbortError();
    }
}
function createAbortError() {
    const error = new Error("Agent spawn retry aborted");
    error.name = "AbortError";
    return error;
}
function createEventQueue() {
    const values = [];
    const waiters = [];
    let closed = false;
    let failure;
    const next = () => {
        if (values.length > 0) {
            return Promise.resolve({ value: values.shift(), done: false });
        }
        if (failure !== undefined) {
            return Promise.reject(failure);
        }
        if (closed) {
            return Promise.resolve({ value: undefined, done: true });
        }
        return new Promise((resolve, reject) => {
            waiters.push({ resolve, reject });
        });
    };
    return {
        push(value) {
            if (closed || failure !== undefined) {
                return;
            }
            const waiter = waiters.shift();
            if (waiter) {
                waiter.resolve({ value, done: false });
                return;
            }
            values.push(value);
        },
        close() {
            if (closed || failure !== undefined) {
                return;
            }
            closed = true;
            for (const waiter of waiters.splice(0)) {
                waiter.resolve({ value: undefined, done: true });
            }
        },
        fail(error) {
            if (closed || failure !== undefined) {
                return;
            }
            failure = error;
            for (const waiter of waiters.splice(0)) {
                waiter.reject(error);
            }
        },
        async *[Symbol.asyncIterator]() {
            while (true) {
                const item = await next();
                if (item.done) {
                    return;
                }
                yield item.value;
            }
        }
    };
}
