/**
 * SentinelNotifier — the single delivery policy for the silently-stopped
 * sentinels (ActiveWorkSilenceSentinel + SocketDisconnectSentinel).
 *
 * Built in response to the 2026-05-22 topic-spam flood. Three problems were
 * stacked together; this module owns two of them (the detector fix lives in
 * sentinelWiring.ts):
 *
 *   1. Severity — sentinel monitoring is housekeeping. Every lifecycle
 *      transition (detected / nudged / recovered / escalated) is written to
 *      the logs (console + a JSONL audit trail) and, by default, NOTHING is
 *      sent to Telegram. Per the user: "anything that just needs to be logged
 *      for the system should not by default be sent to Telegram." This is
 *      stronger than throttling — routine noise is never GENERATED, not merely
 *      rate-limited.
 *
 *   2. Routing — when Telegram escalation is explicitly enabled, genuine
 *      recovery-failed escalations are COALESCED into ONE message and sent to
 *      ONE reused system topic. They never each spawn a brand-new forum topic
 *      (the old path went through /attention → createAttentionItem →
 *      createForumTopic, one topic per event, which produced the wall of
 *      "X went quiet" topics).
 *
 * Signal-vs-authority: this is a delivery sink, not a gate. It introduces no
 * blocking authority. The escalation message is a fixed plain-English template
 * with a yes/no CTA; the system-topic sender it is wired to may still apply the
 * outbound tone gate.
 *
 * Spec: docs/specs/silently-stopped-trio.md
 */
const DEFAULT_CONFIG = {
    telegramEscalation: false,
    coalesceWindowMs: 5_000,
};
export class SentinelNotifier {
    deps;
    cfg;
    pending = new Map();
    flushHandle = null;
    constructor(deps, cfg = {}) {
        this.deps = deps;
        this.cfg = { ...DEFAULT_CONFIG, ...cfg };
    }
    /** True if Telegram escalation is wired AND enabled. */
    get telegramEnabled() {
        return this.cfg.telegramEscalation === true && typeof this.deps.sendConsolidated === 'function';
    }
    /**
     * Record a routine lifecycle transition. Always log-only — never reaches
     * Telegram. This is the "housekeeping goes to the logs" path.
     */
    record(kind, sentinel, sessionName, detail) {
        this.write(kind, sentinel, sessionName, detail);
    }
    /**
     * A sentinel could not recover a genuinely-stuck session. Always logged. Sent
     * to Telegram (coalesced, single system topic) ONLY when escalation is
     * enabled; otherwise log-only.
     */
    escalate(sentinel, sessionName, text) {
        this.write('escalated', sentinel, sessionName, text);
        if (!this.telegramEnabled) {
            this.write('escalation-suppressed', sentinel, sessionName, 'telegramEscalation disabled (log-only)');
            return;
        }
        this.pending.set(sessionName, text);
        this.scheduleFlush();
    }
    /** Flush immediately (test seam + graceful shutdown). */
    async flushNow() {
        this.clearFlush();
        await this.flush();
    }
    /** Cancel any pending flush (graceful shutdown). */
    stop() {
        this.clearFlush();
        this.pending.clear();
    }
    scheduleFlush() {
        if (this.flushHandle)
            return; // a flush is already queued; coalesce into it
        const setTimer = this.deps.setTimer ?? setTimeout;
        this.flushHandle = setTimer(() => {
            this.flushHandle = null;
            void this.flush();
        }, this.cfg.coalesceWindowMs);
        if (this.flushHandle && typeof this.flushHandle.unref === 'function') {
            this.flushHandle.unref();
        }
    }
    clearFlush() {
        if (this.flushHandle) {
            (this.deps.clearTimer ?? clearTimeout)(this.flushHandle);
            this.flushHandle = null;
        }
    }
    async flush() {
        if (this.pending.size === 0)
            return;
        if (!this.deps.sendConsolidated) {
            this.pending.clear();
            return;
        }
        const entries = Array.from(this.pending.entries());
        this.pending.clear();
        const message = this.composeMessage(entries);
        try {
            const delivered = await this.deps.sendConsolidated(message);
            this.write(delivered ? 'escalation-sent' : 'notify-error', 'sentinel-notifier', entries.map(([name]) => name).join(','), delivered ? `consolidated ${entries.length} escalation(s)` : 'sendConsolidated returned false');
        }
        catch (err) {
            this.write('notify-error', 'sentinel-notifier', entries.map(([name]) => name).join(','), err instanceof Error ? err.message : String(err));
        }
    }
    /** Build ONE plain-English, CTA-bearing message for all coalesced sessions. */
    composeMessage(entries) {
        if (entries.length === 1) {
            return entries[0][1];
        }
        const names = entries.map(([name]) => `• ${friendly(name)}`).join('\n');
        return (`${entries.length} background sessions were working and then went quiet, and a gentle nudge didn't bring them back:\n` +
            `${names}\n\n` +
            `Want me to dig into them?`);
    }
    write(kind, sentinel, sessionName, detail) {
        const entry = {
            ts: new Date((this.deps.now ?? Date.now)()).toISOString(),
            kind,
            sentinel,
            sessionName,
            ...(detail ? { detail } : {}),
        };
        try {
            this.deps.log(entry);
        }
        catch {
            // A logging sink failure must never crash the monitoring path.
        }
    }
}
function friendly(sessionName) {
    return sessionName.replace(/^ai\.instar\./, '').replace(/-server$/, '').replace(/-lifeline$/, '');
}
//# sourceMappingURL=SentinelNotifier.js.map