"use strict";
// Curator heartbeat scheduler.
//
// Runs a 60-second heartbeat. Once an hour the heartbeat checks whether the
// configured maintenance interval has elapsed AND whether the system has been
// idle long enough. If both are true, the scheduler invokes runCurator. The
// kill switch always wins — if it's engaged, the curator is skipped.
Object.defineProperty(exports, "__esModule", { value: true });
exports.CuratorScheduler = void 0;
const logger_1 = require("../core/logger");
const tracing_1 = require("../core/tracing");
const curator_1 = require("./curator");
const HEARTBEAT_MS = 60_000;
const MAINTENANCE_CHECK_MS = 60 * 60 * 1000;
class CuratorScheduler {
    opts;
    heartbeat = null;
    lastMaintenanceCheckMs = 0;
    running = false;
    constructor(opts) {
        this.opts = opts;
    }
    start(now = new Date()) {
        if (this.heartbeat)
            return;
        this.lastMaintenanceCheckMs = now.getTime();
        this.heartbeat = setInterval(() => { this.tick().catch((error) => logger_1.logger.warn('Curator', 'Heartbeat tick failed', { error: error instanceof Error ? error.message : String(error) })); }, HEARTBEAT_MS);
        if (typeof this.heartbeat.unref === 'function')
            this.heartbeat.unref();
        logger_1.logger.info('Curator', 'Scheduler started', { intervalHours: this.opts.intervalHours, idleThresholdMinutes: this.opts.idleThresholdMinutes });
    }
    stop() {
        if (this.heartbeat)
            clearInterval(this.heartbeat);
        this.heartbeat = null;
    }
    /** Exposed for tests. Runs one tick of the heartbeat loop. */
    async tick(now = new Date()) {
        if (this.running)
            return { ranCurator: false, reason: 'curator already running' };
        if (!this.opts.isEnabled())
            return { ranCurator: false, reason: 'disabled' };
        if (this.opts.isKillSwitchActive())
            return { ranCurator: false, reason: 'kill switch' };
        const nowMs = now.getTime();
        if (nowMs - this.lastMaintenanceCheckMs < MAINTENANCE_CHECK_MS) {
            return { ranCurator: false, reason: 'within maintenance check window' };
        }
        this.lastMaintenanceCheckMs = nowMs;
        const intervalMs = this.opts.intervalHours * 60 * 60 * 1000;
        const sinceLastRun = nowMs - (this.opts.getLastRunMs() || 0);
        // Candidate-pressure accelerator: even if the long interval has not
        // elapsed, fire the curator when the pending queue is past threshold.
        // Idle gate still applies so we never interrupt active work.
        let candidatePressure = false;
        if (this.opts.getPendingCandidateCount) {
            const threshold = this.opts.runWhenCandidatesAtLeast ?? 25;
            try {
                const pending = await this.opts.getPendingCandidateCount();
                if (pending >= threshold)
                    candidatePressure = true;
            }
            catch (error) {
                logger_1.logger.warn('Curator', 'Candidate-pressure check failed', { error: error instanceof Error ? error.message : String(error) });
            }
        }
        if (sinceLastRun < intervalMs && !candidatePressure) {
            return { ranCurator: false, reason: 'interval not elapsed' };
        }
        const idleMs = nowMs - (this.opts.getLastUserActivityMs() || nowMs);
        const idleThresholdMs = this.opts.idleThresholdMinutes * 60 * 1000;
        if (idleMs < idleThresholdMs) {
            return { ranCurator: false, reason: 'system not idle' };
        }
        this.running = true;
        try {
            const summary = await (0, curator_1.runCurator)(this.opts.projectDir, this.opts.config, { isKillSwitchActive: this.opts.isKillSwitchActive, callModel: this.opts.callModel });
            this.opts.recordRunMs(nowMs);
            tracing_1.runtimeTracer.recordEvent('curator.scheduled_run', { archived: summary.archived.length, candidates: summary.staleCandidates.length, candidatePressure });
            logger_1.logger.info('Curator', 'Scheduled run completed', { archived: summary.archived.length, candidates: summary.staleCandidates.length, candidatePressure });
            return { ranCurator: true };
        }
        finally {
            this.running = false;
        }
    }
}
exports.CuratorScheduler = CuratorScheduler;
//# sourceMappingURL=scheduler.js.map