/**
 * AmbientContributionGate — the conservative "should I speak?" gate for Slack's
 * `considered`/ambient mode (Slack org integration, Pillar 1 §5.2 / §6.9).
 *
 * In `mention-only` mode an UNDIRECTED message (no @mention, not a DM) is dropped
 * — the agent stays quiet. Ambient mode keeps that default but adds ONE narrow
 * escape: for a channel that has EXPLICITLY opted into proactive contribution, an
 * undirected message may run this gate, which decides whether the agent has a
 * concrete, meaningful contribution worth making unprompted.
 *
 * ── THE INVARIANT: FAIL-TO-SILENCE ─────────────────────────────────────────────
 *
 * This gate can only ever make the agent QUIETER. It returns `speak: true` ONLY
 * when EVERY one of these holds:
 *   (a) the channel is explicitly ambient-opted-in (config — default OFF everywhere),
 *   (b) a hard per-channel rate-limit for the rolling window is NOT exceeded,
 *   (c) an LLM judges it can contribute MEANINGFULLY above a conservative
 *       confidence threshold AND explicitly says to speak.
 *
 * ANY failure, uncertainty, LLM error, missing provider, unparseable verdict, or
 * rate-limit breach → `speak: false`. There is NO path through this gate that
 * produces an over-speak on a degraded condition. This is the deliberate MIRROR of
 * the floor gate's fail-CLOSED (deny-on-error): here the safe direction is SILENCE.
 * (Spec §5.2 "Fail mode: fail to silence"; §11 "the ambient/should-speak gate fails
 * to silence".)
 *
 * ── DARK / OPT-IN ──────────────────────────────────────────────────────────────
 *
 * Ambient contribution is disabled for every channel by default. With NO ambient
 * config, this gate is never even constructed/consulted and `_handleMessage`
 * behaves byte-for-byte as today (mention-only drops undirected messages). The gate
 * runs ONLY for an explicitly-opted-in channel.
 *
 * The LLM is reached through instar's internal `IntelligenceProvider` (the same
 * injected-provider pattern as `LlmIntentClassifier` / `MessagingToneGate`), NOT a
 * raw API call. A `fast` model tier is used. The call is intentionally NOT marked
 * `gating: true`: a gating call would provider-SWAP on failure to keep an authority
 * decision alive, but the safe failure here is to stay silent — we WANT the error to
 * land in our catch and return `speak: false`, never to escalate to keep speaking.
 *
 * NOTE: this gate decides ONLY whether to PROCESS an undirected message. It performs
 * no Slack Web API calls and sends nothing itself — `_handleMessage` does the
 * processing/sending downstream exactly as it does for a directed message.
 *
 * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §5.2, §6.9, §11.
 */
import type { IntelligenceProvider } from '../core/types.js';
/** The gate's decision for a single undirected message. */
export interface AmbientDecision {
    /** True ONLY when every fail-to-silence condition holds. Default false. */
    speak: boolean;
    /** Machine-readable reason for the decision (for logging / FP measurement). */
    reason: AmbientDecisionReason;
    /** Optional human-readable detail (e.g. the LLM's named contribution). */
    detail?: string;
}
/**
 * Observability — a bounded, in-memory record of a SINGLE silence the gate is
 * about to return. Used only to populate the near-miss ring; never affects the
 * decision. No message text is stored — just the channel, reason, and how close
 * the (clamped) confidence was to the threshold.
 */
export interface AmbientSilenceSample {
    /** The Slack channel the silence occurred in. */
    channelId: string;
    /** Why the gate stayed silent. */
    reason: AmbientDecisionReason;
    /**
     * The LLM's clamped confidence for this evaluation, when one was produced
     * (only the LLM paths carry it; the channel-not-opted-in / rate-limited /
     * no-intelligence / llm-error / llm-unparseable paths leave it undefined).
     */
    confidence?: number;
    /** Whether this silence was within `nearMissDelta` below the speak threshold. */
    nearMiss: boolean;
    /** When the sample was taken (gate clock). */
    at: number;
}
/**
 * Per-channel ambient observability counters + a bounded ring of recent near-miss
 * silences. Pure aggregate — NO message content, NO per-event Telegram/topic
 * side-effect (so it can never flood). This is the read surface the observe-only
 * live test polls to measure the ambient gate's false-positive (wrongful-silence)
 * and false-negative (wrongful-speak) rates. Signal-only: reading or computing it
 * NEVER changes the speak/silence verdict.
 */
export interface AmbientChannelStats {
    channelId: string;
    /** Every shouldSpeak() call for this channel (spoke + silent). */
    evaluated: number;
    /** Decisions that returned speak=true. */
    spoke: number;
    /** Decisions that returned speak=false (the FP candidates). */
    silent: number;
    /**
     * Silences where the LLM's confidence was within `nearMissDelta` BELOW the
     * speak threshold — i.e. it nearly spoke. The highest-signal subset for tuning
     * the confidence floor.
     */
    nearMissSilent: number;
    /** Per-reason silence breakdown (channel-not-opted-in, rate-limited, …). */
    silentByReason: Partial<Record<AmbientDecisionReason, number>>;
}
export interface AmbientStats {
    /** Per-channel aggregate counters. */
    channels: AmbientChannelStats[];
    /** Bounded ring (most-recent-first) of recent near-miss silences for spot-inspection. */
    recentNearMisses: AmbientSilenceSample[];
    /** The near-miss delta in use (confidence within this far below the threshold). */
    nearMissDelta: number;
    /** The confidence floor a "speak" verdict must clear. */
    minConfidence: number;
    /** Cap on the recentNearMisses ring (bounded — no unbounded growth). */
    ringCapacity: number;
}
export type AmbientDecisionReason = 'channel-not-opted-in' | 'rate-limited' | 'no-intelligence' | 'llm-error' | 'llm-unparseable' | 'llm-declined' | 'low-confidence' | 'speak';
/** Per-channel ambient configuration. Default: ambient OFF. */
export interface AmbientChannelConfig {
    /** Channels explicitly opted into proactive contribution. Default: none. */
    enabledChannelIds?: string[];
    /**
     * Hard cap on proactive (unsolicited) messages per channel within the rolling
     * window. Conservative default: 1. A bot that barges in is worse than a silent
     * one, so this is deliberately tiny.
     */
    maxProactivePerChannel?: number;
    /** Rolling rate-limit window in ms. Default: 30 minutes. */
    windowMs?: number;
    /**
     * Conservative confidence floor for the LLM's "speak" verdict, in [0,1]. Below
     * this, the gate stays silent even if the LLM said speak. Default: 0.85 (high bar).
     */
    minConfidence?: number;
    /**
     * Observability only — a silence whose confidence lands within this far BELOW
     * `minConfidence` is flagged a "near-miss" (it nearly spoke). Default: 0.1. Has
     * NO effect on the verdict; it only classifies silences for the stats surface.
     */
    nearMissDelta?: number;
    /**
     * Observability only — cap on the in-memory ring of recent near-miss silence
     * samples kept for spot-inspection. Bounded so the gate can never grow unbounded.
     * Default: 50.
     */
    nearMissRingCapacity?: number;
}
export interface AmbientContributionGateDeps {
    /** Per-channel ambient configuration. Absent/empty ⇒ ambient OFF everywhere. */
    config?: AmbientChannelConfig;
    /**
     * The internal LLM provider (injected — never a direct framework import). When
     * absent, the gate stays SILENT (fail-to-silence): no provider ⇒ no contribution.
     */
    intelligence?: IntelligenceProvider;
    /** Per-call LLM timeout (ms). Default 8000. */
    timeoutMs?: number;
    /**
     * Optional observability hook — fires on EVERY decision with the reason, so the
     * FP-rate of proactive speaking can be measured before any aggressiveness change.
     * Best-effort: it never affects the returned decision.
     */
    onDecision?: (decision: AmbientDecision, channelId: string) => void;
    /** Injectable clock for deterministic tests. Defaults to Date.now. */
    now?: () => number;
}
/** Context the gate needs about the channel + message. */
export interface AmbientGateInput {
    /** The Slack channel id the undirected message arrived in. */
    channelId: string;
    /** The (cleaned) message text. */
    text: string;
    /** Optional channel name, purely for the LLM prompt context. */
    channelName?: string;
}
export declare class AmbientContributionGate {
    private readonly enabledChannels;
    private readonly maxProactive;
    private readonly windowMs;
    private readonly minConfidence;
    private readonly intelligence?;
    private readonly timeoutMs;
    private readonly onDecision?;
    private readonly now;
    private readonly nearMissDelta;
    private readonly nearMissRingCapacity;
    /**
     * Observability — bounded, in-memory aggregate of every decision, per channel.
     * Populated in recordDecisionStats() (called from decide() AFTER the verdict is
     * formed), so it can never change the verdict. No message content is stored. A
     * restart resets it; this is acceptable for an FP-rate measurement surface (the
     * durable per-decision ledger is the file-backed /permissions/decisions). The map
     * is keyed by channelId — bounded by the set of opted-in channels, which is small
     * and config-controlled.
     */
    private readonly channelStats;
    /** Bounded ring (newest-last) of recent near-miss silences for spot-inspection. */
    private readonly nearMissRing;
    /**
     * Rate-limit state lives HERE — a per-channel in-memory ring of the timestamps at
     * which the gate last returned speak=true. It is recorded by recordSpoke() (called
     * by _handleMessage only AFTER the gate cleared and the message is actually being
     * processed), so a speak=true that the caller decides not to act on does not
     * consume budget. In-memory is acceptable: a restart resetting the window can only
     * make the agent quieter for a moment (it never over-speaks), which is on the safe
     * side of the invariant. A future durable counter could replace this without
     * changing the contract.
     */
    private readonly proactiveTimestamps;
    constructor(deps?: AmbientContributionGateDeps);
    /** Is ANY channel opted into ambient contribution? Used to skip the gate entirely. */
    isAnyChannelEnabled(): boolean;
    /** Is this specific channel opted into ambient contribution? */
    isChannelEnabled(channelId: string): boolean;
    /**
     * Decide whether to speak on an UNDIRECTED message in an ambient channel.
     * FAIL-TO-SILENCE: returns { speak: false } on every degraded/uncertain path.
     */
    shouldSpeak(input: AmbientGateInput): Promise<AmbientDecision>;
    /**
     * Record that the agent actually spoke proactively in a channel (call AFTER the
     * gate cleared and _handleMessage commits to processing). Consumes one unit of the
     * rolling window budget. Idempotency is not required — each proactive turn is one
     * unit.
     */
    recordSpoke(channelId: string): void;
    /** How many proactive sends remain in the current window for a channel (for status). */
    remainingBudget(channelId: string): number;
    /** True iff the per-channel proactive budget is exhausted for the rolling window. */
    private isRateLimited;
    /** Count of proactive sends within the rolling window; prunes expired entries. */
    private recentCount;
    private decide;
    /**
     * Update the per-channel counters and (for a near-miss silence) the bounded ring.
     * A SILENCE is a "near-miss" when the LLM produced a confidence that fell within
     * `nearMissDelta` BELOW the speak threshold — i.e. it nearly spoke. Paths with no
     * confidence (not-opted-in, rate-limited, no-intelligence, llm-error, unparseable)
     * are never near-misses. Pure in-memory; no content stored; no side-effect.
     */
    private recordDecisionStats;
    /**
     * Read-only snapshot of the bounded observability aggregate. Safe to call any time
     * (returns copies — the caller can't mutate internal state). With no opted-in
     * channel the gate is never consulted, so `channels` stays empty and nothing is
     * recorded. This is the surface the observe-only live test polls.
     */
    getStats(): AmbientStats;
}
//# sourceMappingURL=AmbientContributionGate.d.ts.map