// Idle-recorder reaper. Walks the fs-watcher row index every `intervalMs`
// (and again on each watcher event) and SIGTERMs any specialist row that has
// been status:idle with no JSONL for longer than `recorderIdleTtlMs`. The
// Stop hook in the admin agent fires one recorder per turn; any recorder that
// errors-without-exit leaks a PTY. Without this loop, leaks compound until
// `/proc/sys/kernel/pty/max` or `nofile` is exhausted and the manager cannot
// spawn its own cleanup turn.
//
// Reap condition (all four must hold):
//   row.agent       !== null           // it's a specialist
//   row.status      === 'idle'         // not actively producing output
//   no jsonl on disk for this sessionId  // no transcript was ever written
//   row.updatedAt   older than TTL     // not a fresh spawn
//
// Task 250 + 260 — the reaper holds no PTY itself. Stop goes through
// `pty-spawner.stopSession`, which looks up the tracker in the
// module-scoped livePtys map, runs `systemctl --user stop --no-block`
// against the PTY's scope unit (the scope's TimeoutStopSec=5 handles
// SIGTERM→SIGKILL escalation in its own cgroup), and releases the
// master fd. The fs-watcher observes the PID file removal that follows
// and drops the row.
import { existsSync } from 'node:fs';
import { jsonlPathForSessionId } from './jsonl-path.js';
import { stopSession, getPtyTrackerForTests } from './pty-spawner.js';
import { scopeUnitName } from './systemd-scope.js';
export function createReaper(deps) {
    const now = deps.now ?? (() => Date.now());
    let timer = null;
    let unsubscribe = null;
    let started = false;
    function shouldReap(row) {
        if (row.agent === null)
            return false;
        if (row.status !== 'idle')
            return false;
        if (row.state !== 'live')
            return false;
        // Task 258 — derived: no transcript = no on-disk jsonl at canonical path.
        if (existsSync(jsonlPathForSessionId(deps.spawnCwd, row.sessionId)))
            return false;
        const idleMs = now() - row.updatedAt;
        return idleMs > deps.recorderIdleTtlMs;
    }
    function reapRow(row) {
        const idleMs = now() - row.updatedAt;
        // Task 250 + 260 — `pty-spawner.stopSession` looks the row up in
        // the module-scoped tracker map, issues `systemctl --user stop`
        // against the scope unit (TimeoutStopSec=5 escalates inside the
        // cgroup), and releases the master fd. Fire and forget — the
        // tick pass stays synchronous and the watcher emits the row's
        // archive transition once Claude Code's PID-file delete lands.
        const tracker = getPtyTrackerForTests(row.sessionId);
        const unit = tracker ? scopeUnitName(tracker.scopeUnitToken) : 'none';
        void stopSession({
            killGraceMs: deps.killGraceMs,
            logger: deps.logger,
            runSystemctl: deps.runSystemctl,
        }, row.sessionId);
        deps.logger(`[reaper] reaped sessionId=${row.sessionId} unit=${unit} agent=${row.agent} idle-ms=${idleMs} reason=stale-recorder`);
    }
    function tickOnce() {
        let scanned = 0;
        let reaped = 0;
        try {
            const rows = deps.watcher.all();
            scanned = rows.length;
            for (const row of rows) {
                if (shouldReap(row)) {
                    reapRow(row);
                    reaped += 1;
                }
            }
            deps.logger(`[reaper] tick rows-scanned=${scanned} reaped=${reaped}`);
        }
        catch (err) {
            const message = err instanceof Error ? err.message : String(err);
            deps.logger(`[reaper] tick-failed message=${JSON.stringify(message)}`);
        }
        return { scanned, reaped };
    }
    function start() {
        if (started)
            return;
        started = true;
        timer = setInterval(tickOnce, deps.intervalMs);
        // Don't keep the event loop alive for the reaper alone — the HTTP
        // server's listener handles that. Without unref(), shutdown via
        // process.exit is fine, but tests that close the watcher and expect
        // the process to drain would hang.
        if (typeof timer.unref === 'function')
            timer.unref();
        unsubscribe = deps.watcher.subscribe((event) => {
            // Only pid-source events change the lifecycle of a specialist row
            // in a way that could make it eligible. jsonl events disqualify
            // (jsonlPath !== null), so we skip them.
            if (event.source !== 'pid')
                return;
            tickOnce();
        });
        deps.logger(`[reaper] started intervalMs=${deps.intervalMs} recorderIdleTtlMs=${deps.recorderIdleTtlMs} killGraceMs=${deps.killGraceMs}`);
    }
    function stop() {
        if (!started)
            return;
        if (timer) {
            clearInterval(timer);
            timer = null;
        }
        if (unsubscribe) {
            unsubscribe();
            unsubscribe = null;
        }
        started = false;
        deps.logger(`[reaper] stopped`);
    }
    return { start, stop, tickOnce };
}
//# sourceMappingURL=reaper.js.map