/**
 * Quota Exhaustion Detector — classifies WHY a session died.
 *
 * Pattern-matches tmux output from a dead session against known
 * failure signatures, cross-references with quota state to produce
 * a confidence-weighted classification.
 *
 * Use cases:
 * - Skip ledger: record quota_exhaustion vs crash vs normal_exit
 * - Telegram alerts: "session died because quota exhausted"
 * - Auto-recovery: trigger account switch on repeated quota deaths
 *
 * Ported from Dawn's dawn-server/src/quota/QuotaExhaustionDetector.ts,
 * simplified for general Instar use (no Telegram dependency).
 */
// Pattern groups for classification
const QUOTA_PATTERNS = [
    'overloaded_error',
    'rate_limit',
    '429',
    'quota.*exceeded',
    'usage.*limit',
    'too many requests',
    'resource_exhausted',
    'capacity',
    'throttl',
    'rate limit exceeded',
];
const CONTEXT_PATTERNS = [
    'context.*exhaust',
    'context.*limit',
    'context.*full',
    'token.*limit.*reached',
    'maximum.*context',
    'conversation is too long',
    'conversation too long',
    'error during compaction.*too long',
    'press esc twice to go up a few messages',
];
const NORMAL_COMPACTION_LIFECYCLE_PATTERNS = [
    'conversation compacted',
    'paused for context compaction',
    'context compaction.*resumed',
    'identity recovery.*post-compaction',
    'active projects.*post-compaction',
    'compaction recovery',
];
const EXPLICIT_CONTEXT_EXHAUSTION_PATTERNS = [
    'conversation too long',
    'conversation is too long',
    'error during compaction',
    'press esc twice to go up a few messages',
];
const CRASH_PATTERNS = [
    'SIGABRT',
    'SIGSEGV',
    'SIGKILL',
    'fatal error',
    'unhandled.*exception',
    'panic:',
    'stack overflow',
    'out of memory',
    'heap.*out',
];
const NORMAL_EXIT_PATTERNS = [
    'Session ended',
    'Goodbye!',
    'has been completed',
    'Auto-exit',
    '✓',
    'exited cleanly',
];
/**
 * Classify why a session died based on its terminal output and quota state.
 */
export function classifySessionDeath(tmuxOutput, quotaState) {
    if (!tmuxOutput || tmuxOutput.trim().length === 0) {
        return { cause: 'unknown', confidence: 'low', detail: 'No output captured' };
    }
    const output = tmuxOutput.toLowerCase();
    // Check each pattern group
    const quotaMatch = matchPatterns(output, QUOTA_PATTERNS);
    const contextMatch = matchPatterns(output, CONTEXT_PATTERNS);
    const crashMatch = matchPatterns(output, CRASH_PATTERNS);
    const normalMatch = matchPatterns(output, NORMAL_EXIT_PATTERNS);
    // Normal exit takes precedence if present
    if (normalMatch && !quotaMatch && !crashMatch) {
        return {
            cause: 'normal_exit',
            confidence: 'high',
            detail: `Matched normal exit pattern: "${normalMatch}"`,
        };
    }
    // Quota exhaustion — cross-reference with state for confidence
    if (quotaMatch) {
        let confidence = 'medium';
        // Cross-reference: if 5-hour is high, boost confidence
        if (quotaState) {
            const fiveHour = quotaState.fiveHourPercent;
            const weekly = quotaState.usagePercent;
            if (typeof fiveHour === 'number' && fiveHour > 90) {
                confidence = 'high';
            }
            else if (weekly > 85) {
                confidence = 'high';
            }
        }
        return {
            cause: 'quota_exhaustion',
            confidence,
            detail: `Matched quota pattern: "${quotaMatch}"` +
                (quotaState ? ` (weekly: ${quotaState.usagePercent}%, 5h: ${quotaState.fiveHourPercent ?? 'n/a'}%)` : ''),
        };
    }
    // Context exhaustion — boost confidence for strong signals
    if (contextMatch) {
        const strongSignals = ['conversation too long', 'conversation is too long', 'error during compaction'];
        const isStrong = strongSignals.some(s => output.includes(s));
        return {
            cause: 'context_exhausted',
            confidence: isStrong ? 'high' : 'medium',
            detail: `Matched context pattern: "${contextMatch}"`,
        };
    }
    // Crash
    if (crashMatch) {
        return {
            cause: 'crash',
            confidence: 'medium',
            detail: `Matched crash pattern: "${crashMatch}"`,
        };
    }
    return { cause: 'unknown', confidence: 'low', detail: 'No patterns matched' };
}
/**
 * Detect context exhaustion in tmux output from a LIVE session.
 * Unlike classifySessionDeath (which classifies after death),
 * this can be called on alive sessions to detect the error proactively.
 */
export function detectContextExhaustion(tmuxOutput) {
    if (!tmuxOutput || tmuxOutput.trim().length === 0) {
        return { matched: false, pattern: null, confidence: 'medium' };
    }
    const output = tmuxOutput.toLowerCase();
    if (matchPatterns(output, NORMAL_COMPACTION_LIFECYCLE_PATTERNS)
        && !matchPatterns(output, EXPLICIT_CONTEXT_EXHAUSTION_PATTERNS)) {
        return { matched: false, pattern: null, confidence: 'medium' };
    }
    const match = matchPatterns(output, CONTEXT_PATTERNS);
    if (!match) {
        return { matched: false, pattern: null, confidence: 'medium' };
    }
    // FALSE-POSITIVE GUARD (RUN-2 2026-06-06, topic 13435 flood). The bare phrase
    // "conversation (is) too long" appears as ordinary CONTENT whenever a session
    // discusses the conversation-too-long failure mode — an autonomous session
    // working ON that feature, a quoted error, or the recovery NOTICE this very
    // detector triggers ("Session hit \"conversation too long\" …"), which re-enters
    // the pane and self-amplifies one false positive into a flood. A REAL Claude
    // Code exhaustion error always renders WITH its CLI recovery framing — the
    // "press esc twice …" hint or an "error during compaction" line (see the
    // realistic fixture in the unit tests). Require that framing: the bare phrase,
    // alone, is content, not a live CLI error.
    const bareExhaustionPhrases = ['conversation too long', 'conversation is too long'];
    // Both wordings of the CLI recovery hint ("esc"/"escape"), kept anchored on
    // "twice" so it matches the real too-long prompt but not an unrelated
    // "press escape to stop" working-indicator line.
    const cliErrorFrames = ['press esc twice', 'press escape twice', 'error during compaction'];
    const hasBarePhrase = bareExhaustionPhrases.some(s => output.includes(s));
    const hasCliFrame = cliErrorFrames.some(s => output.includes(s));
    if (hasBarePhrase && !hasCliFrame) {
        // Phrase present only as content/narration — not the framed CLI error. (A
        // non-phrase soft signal like "context limit" is unaffected: hasBarePhrase
        // is false, so this guard does not apply and existing behavior is preserved.)
        return { matched: false, pattern: null, confidence: 'medium' };
    }
    const isStrong = hasBarePhrase || output.includes('error during compaction');
    return { matched: true, pattern: match, confidence: isStrong ? 'high' : 'medium' };
}
/**
 * Check if output matches any pattern in a group.
 * Returns the first matching pattern string, or null.
 */
export function matchPatterns(output, patterns) {
    for (const pattern of patterns) {
        try {
            if (new RegExp(pattern, 'i').test(output)) {
                return pattern;
            }
        }
        catch {
            // @silent-fallback-ok — regex fallback to string match
            if (output.includes(pattern.toLowerCase())) {
                return pattern;
            }
        }
    }
    return null;
}
//# sourceMappingURL=QuotaExhaustionDetector.js.map