"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
Object.defineProperty(exports, "__esModule", { value: true });
exports.__testing = exports.ReviewWorker = void 0;
const skillNotesWriter_1 = require("./skillNotesWriter");
const DEFAULT_POLL_INTERVAL_MS = 30 * 1000;
const DEFAULT_CONCURRENCY = 1;
const MAX_CONCURRENCY = 2;
const DEFAULT_DAILY_BUDGET = 100;
const DEFAULT_COOLDOWN_MS = 5 * 60 * 1000;
class ReviewWorker {
    constructor(opts) {
        this.timer = null;
        this.inflight = 0;
        this.cooldownUntil = new Map();
        this.stats = {
            attempted: 0,
            succeeded: 0,
            rejected: 0,
            failedTransient: 0,
            failedPermanent: 0,
            cooldownSkipped: 0,
            budgetExhausted: 0,
        };
        const concurrency = clamp(opts.concurrency ?? DEFAULT_CONCURRENCY, 1, MAX_CONCURRENCY);
        this.opts = {
            outbox: opts.outbox,
            executeReview: opts.executeReview,
            pollIntervalMs: opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
            concurrency,
            dailyBudget: opts.dailyBudget ?? DEFAULT_DAILY_BUDGET,
            perSkillCooldownMs: opts.perSkillCooldownMs ?? DEFAULT_COOLDOWN_MS,
            workerOwner: opts.workerOwner ?? `worker-${process.pid}`,
            clock: opts.clock ?? Date.now,
            notesDir: opts.notesDir,
            toolRegistry: opts.toolRegistry,
        };
    }
    /**
     * Start the poll loop. No-op if `SELF_IMPROVE_REVIEW_ENABLED` is not set
     * to a truthy value. Returns true if the worker actually started.
     */
    start() {
        if (this.timer)
            return true;
        if (!isReviewEnabled()) {
            console.log('[ReviewWorker] disabled by env (SELF_IMPROVE_REVIEW_ENABLED unset)');
            return false;
        }
        this.timer = setInterval(() => {
            // Background tick — swallow errors so a single failure doesn't kill
            // the interval timer. Stats are still updated by the inner methods.
            this.tick().catch(err => console.warn('[ReviewWorker] tick failed:', err.message));
        }, this.opts.pollIntervalMs);
        return true;
    }
    stop() {
        if (this.timer) {
            clearInterval(this.timer);
            this.timer = null;
        }
    }
    /**
     * Single iteration of the loop. Exposed for tests so we don't have to
     * twiddle `setInterval` timing.
     */
    async tick() {
        this.opts.outbox.expireStaleLeases(this.opts.clock());
        /** Local skip-set so the same cooldown-blocked job isn't picked twice in a tick. */
        const seenInTick = new Set();
        while (this.inflight < this.opts.concurrency) {
            if (this.opts.outbox.dailyJobCount(this.opts.clock()) >= this.opts.dailyBudget) {
                this.stats.budgetExhausted += 1;
                return;
            }
            const job = this.opts.outbox.leaseNext({
                workerOwner: this.opts.workerOwner,
                leaseDurationMs: this.opts.pollIntervalMs * 10,
            });
            if (!job)
                return;
            if (seenInTick.has(job.id)) {
                // Outbox handed back the same job — release and bail to avoid spin.
                this.opts.outbox.releaseLease(job.id);
                return;
            }
            const cooldownReason = this.checkCooldown(job);
            if (cooldownReason) {
                this.stats.cooldownSkipped += 1;
                this.opts.outbox.releaseLease(job.id);
                seenInTick.add(job.id);
                continue;
            }
            this.inflight += 1;
            this.stats.attempted += 1;
            // Fire-and-forget — concurrency is gated by `inflight` rather than awaiting.
            this.processJob(job).finally(() => {
                this.inflight -= 1;
            });
        }
    }
    checkCooldown(job) {
        const payload = job.payload;
        if (!payload?.skillId || !payload.failureModeHash)
            return null;
        const key = `${payload.skillId}::${payload.failureModeHash}`;
        const until = this.cooldownUntil.get(key);
        if (until && this.opts.clock() < until) {
            return `cooldown active for ${key} until ${new Date(until).toISOString()}`;
        }
        return null;
    }
    async processJob(job) {
        try {
            const result = await this.opts.executeReview(job.payload);
            if (!result.ok) {
                this.stats.failedTransient += 1;
                this.opts.outbox.markFailed(job.id, `${result.reason}: ${result.details}`);
                return;
            }
            const writeOutcome = (0, skillNotesWriter_1.writeSkillNote)(result.emission, {
                notesDir: this.opts.notesDir,
                now: this.opts.clock(),
                toolRegistry: this.opts.toolRegistry,
            });
            this.recordWriteOutcome(job, writeOutcome);
        }
        catch (err) {
            this.stats.failedTransient += 1;
            this.opts.outbox.markFailed(job.id, `unhandled: ${err.message}`);
        }
    }
    recordWriteOutcome(job, outcome) {
        if (outcome.ok) {
            this.stats.succeeded += 1;
            this.opts.outbox.markDone(job.id);
            const payload = job.payload;
            if (payload?.skillId && payload.failureModeHash) {
                this.cooldownUntil.set(`${payload.skillId}::${payload.failureModeHash}`, this.opts.clock() + this.opts.perSkillCooldownMs);
            }
            return;
        }
        if (outcome.reason === 'io_error') {
            // Transient — retry next tick.
            this.stats.failedTransient += 1;
            this.opts.outbox.markFailed(job.id, `io_error: ${outcome.details}`);
            return;
        }
        // Schema/scan/capacity rejections are the agent's fault; don't keep
        // retrying — mark permanently failed by exceeding the attempts cap.
        this.stats.rejected += 1;
        this.opts.outbox.markFailed(job.id, `${outcome.reason}: ${outcome.details}`, 1);
    }
}
exports.ReviewWorker = ReviewWorker;
function clamp(value, lo, hi) {
    return Math.max(lo, Math.min(hi, value));
}
function isReviewEnabled() {
    const v = process.env.SELF_IMPROVE_REVIEW_ENABLED;
    return v === '1' || v === 'true' || v === 'yes';
}
exports.__testing = {
    DEFAULT_POLL_INTERVAL_MS,
    DEFAULT_CONCURRENCY,
    MAX_CONCURRENCY,
    DEFAULT_DAILY_BUDGET,
    DEFAULT_COOLDOWN_MS,
};
//# sourceMappingURL=reviewWorker.js.map