// engine/agent-spawner.ts — SPEC-688: semaphore + FIFO queue agent batch launcher
const HARD_CAP = 20;
const DEFAULT_SPAWNER_CONFIG = {
    maxConcurrentAgents: 5,
    defaultTimeoutMs: 600_000,
    defaultTokenBudget: 50_000,
};
function updateSlot(slots, index, patch) {
    const existing = slots[index];
    if (existing !== undefined) {
        Object.assign(existing, patch);
    }
}
/**
 * Launch a batch of agent requests with bounded concurrency (semaphore + FIFO queue).
 *
 * @param specId    - Identifies the spec owning this batch (for result correlation).
 * @param requests  - Ordered list of agent spawn requests.
 * @param execute   - Injected callback — receives a SpawnRequest, returns Promise<void>.
 * @param config    - Optional overrides for SpawnerConfig defaults.
 */
export async function spawnAgentBatch(specId, requests, execute, config = {}) {
    const merged = { ...DEFAULT_SPAWNER_CONFIG, ...config };
    const maxConcurrent = Math.min(merged.maxConcurrentAgents, HARD_CAP);
    // Build slots — all start as 'queued'
    const slots = requests.map((request) => ({
        request,
        status: 'queued',
        startedAt: null,
        completedAt: null,
    }));
    let running = 0;
    let queueIndex = 0;
    // Active in-flight promises; each removes itself upon settlement
    const active = new Set();
    function runSlot(index) {
        const req = slots[index]?.request;
        if (req === undefined) {
            return;
        }
        running++;
        updateSlot(slots, index, { status: 'running', startedAt: new Date().toISOString() });
        let timedOut = false;
        const timeoutHandle = setTimeout(() => {
            timedOut = true;
            const current = slots[index];
            if (current?.status === 'running') {
                updateSlot(slots, index, { status: 'timeout', completedAt: new Date().toISOString() });
            }
        }, req.timeoutMs);
        // Wrapper so the promise can remove itself from `active` after settlement
        const wrapper = { ref: null };
        const promise = execute(req)
            .then(() => {
            clearTimeout(timeoutHandle);
            if (!timedOut) {
                updateSlot(slots, index, { status: 'done', completedAt: new Date().toISOString() });
            }
        })
            .catch((err) => {
            clearTimeout(timeoutHandle);
            if (!timedOut) {
                const message = err instanceof Error ? err.message : String(err);
                updateSlot(slots, index, {
                    status: 'failed',
                    completedAt: new Date().toISOString(),
                    error: message,
                });
            }
        })
            .finally(() => {
            running--;
            if (wrapper.ref !== null) {
                active.delete(wrapper.ref);
            }
        });
        wrapper.ref = promise;
        active.add(promise);
    }
    function drain() {
        while (queueIndex < slots.length && running < maxConcurrent) {
            runSlot(queueIndex);
            queueIndex++;
        }
    }
    // Initial fill
    drain();
    // Race loop: wait for any slot to complete, then refill from queue
    while (running > 0 || queueIndex < slots.length) {
        if (active.size === 0) {
            break;
        }
        await Promise.race(active);
        drain();
    }
    // Ensure any remaining in-flight work settles
    await Promise.allSettled(Array.from(active));
    // Tally results
    let done = 0;
    let failed = 0;
    let timeout = 0;
    for (const slot of slots) {
        if (slot.status === 'done') {
            done++;
        }
        else if (slot.status === 'failed') {
            failed++;
        }
        else if (slot.status === 'timeout') {
            timeout++;
        }
    }
    return { specId, total: slots.length, done, failed, timeout, slots };
}
//# sourceMappingURL=agent-spawner.js.map