/**
 * PresenceProxy — Intelligent Response Standby
 *
 * Monitors the gap between user messages and agent responses on Telegram,
 * providing tiered, LLM-generated status updates on the agent's behalf.
 *
 * Tier 1 (20s):  Haiku summarizes what the agent is doing
 * Tier 2 (2min): Haiku compares progress since Tier 1
 * Tier 3 (5min): Sonnet assesses if the agent is genuinely stuck
 *
 * All messages prefixed with 🔭 [Standby] to distinguish from agent responses.
 * Proxy messages do NOT count as agent responses for StallDetector.
 */
import type { IntelligenceProvider } from '../core/types.js';
import type { MessageLoggedEvent } from '../messaging/shared/MessagingEventBus.js';
import type { IntelligenceFramework } from '../core/intelligenceProviderFactory.js';
export interface PresenceProxyConfig {
    stateDir: string;
    intelligence: IntelligenceProvider;
    agentName: string;
    /**
     * The agent's resolved framework (codex-cli / claude-code). Used to pick
     * the correct pane activity-signal when the assessment LLM is unavailable
     * or returns an unparseable classification — so a stuck Codex session is
     * NOT blindly assumed "still working" forever (the 2026-05-23 incident,
     * where the LLM couldn't read the Codex pane and defaulted to active).
     * Absent → defaults to claude-code behavior (back-compat).
     */
    agentFramework?: IntelligenceFramework;
    captureSessionOutput: (sessionName: string, lines?: number) => string | null;
    getSessionForTopic: (topicId: number) => string | null;
    isSessionAlive: (sessionName: string) => boolean;
    sendMessage: (topicId: number, text: string, metadata?: ProxyMetadata) => Promise<void>;
    getAuthorizedUserIds: () => number[];
    getProcessTree: (sessionName: string) => ProcessInfo[];
    /** Check if agent sent any message to this topic after the given timestamp. Prevents race conditions. */
    hasAgentRespondedSince?: (topicId: number, sinceMs: number) => boolean;
    acquireTriageMutex?: (sessionName: string, holder: string) => boolean;
    releaseTriageMutex?: (sessionName: string, holder: string) => void;
    isTriageMutexHeld?: (sessionName: string) => string | null;
    triggerManualTriage?: (topicId: number, sessionName: string) => Promise<void>;
    /**
     * Optional shared per-topic proxy mutex (per PROMISE-BEACON-SPEC.md §A10).
     * When present, PresenceProxy acquires this before sending a tier message
     * so PromiseBeacon (⏳) and PresenceProxy (🔭) don't double-post to the
     * same topic within the same second.
     */
    acquireProxyMutex?: (topicId: number, holder: 'presence-proxy' | 'promise-beacon') => boolean;
    releaseProxyMutex?: (topicId: number, holder: 'presence-proxy' | 'promise-beacon') => void;
    /**
     * WS3 one-voice gate (MULTI-MACHINE-SEAMLESSNESS-SPEC, closes F18): when
     * wired, the election decides whether THIS machine speaks the 🔭 standby
     * voice for a topic — only the topic's owner machine answers; unknown
     * ownership fails toward speech via lease-holder/tiebreak so the pool is
     * never silent. PromiseBeacon already had this gate; PresenceProxy lacked
     * one entirely (both machines could answer for the same topic). Absent →
     * exactly today's behavior.
     */
    speakerElection?: import('./SpeakerElection.js').SpeakerElection;
    /**
     * BUILD-STALL-VISIBILITY-SPEC Fix 2 "Routing" — when a /build heartbeat has
     * landed on this topic recently, PresenceProxy suppresses its generic
     * Tier 2/3 standby so the user hears one progress voice per channel.
     * Returns true if a build heartbeat was recorded within the suppression
     * window (default 6 min). Absent/undefined = no build active = no suppression.
     */
    hasRecentBuildHeartbeat?: (topicId: number, windowMs?: number) => boolean;
    /**
     * When the RateLimitSentinel is actively recovering this topic's session from
     * a server-side throttle, it is already messaging the user (notice → check-ins
     * → recovered/escalated). PresenceProxy must stay silent so the user hears one
     * voice. Unlike the build-heartbeat suppression, this covers EVERY tier
     * (including Tier 1) — the sentinel's immediate notice already provides the
     * first signal of life. Absent/undefined = no suppression.
     */
    hasActiveRateLimitRecovery?: (topicId: number) => boolean;
    /**
     * BUILD-STALL-VISIBILITY-SPEC Fix 3 — long-tool-wait detector.
     * When enabled, detects "agent blocked on a long-running tool with no
     * interleaved text" via snapshot-hash diff + Cogitated-line presence,
     * and swaps the Tier 2/3 templated message to a tool-specific one.
     * Feature-flagged off by default for the introduction release.
     */
    longToolWaitDetector?: {
        enabled?: boolean;
        enterThresholdMs?: number;
        exitHysteresisMs?: number;
        escalationCapMs?: number;
    };
    recoverContextExhaustion?: (topicId: number, sessionName: string) => Promise<{
        recovered: boolean;
    }>;
    /**
     * Honest turn-receipts: when a recovery sentinel (ContextWedgeSentinel,
     * etc.) is ALREADY handling this session's stuck state, it owns the user
     * voice — PresenceProxy must not also message about the wedge. Wired to the
     * composed sentinel recovery checker. Absent/undefined = no suppression
     * (the honest classifier is then the only voice, which is still correct).
     */
    isStuckRecoveryActive?: (sessionName: string) => boolean;
    tier1DelayMs?: number;
    tier2DelayMs?: number;
    tier3DelayMs?: number;
    tier3RecheckDelayMs?: number;
    silenceDurationMs?: number;
    tier1Model?: 'fast' | 'balanced' | 'capable';
    tier2Model?: 'fast' | 'balanced' | 'capable';
    tier3Model?: 'fast' | 'balanced' | 'capable';
    maxTmuxLines?: {
        t1: number;
        t2: number;
        t3: number;
    };
    llmTimeoutMs?: {
        t1: number;
        t2: number;
        t3: number;
    };
    llmRateLimit?: {
        perTopicPerHour: number;
        tier3MaxRechecks: number;
        autoSilenceMinutes: number;
    };
    concurrentLlmCalls?: number;
    /**
     * Shared cross-monitor LLM queue (PROMISE-BEACON-SPEC follow-up).
     * When provided, all PresenceProxy LLM calls route through this queue
     * using the `interactive` lane, so PromiseBeacon's background lane can be
     * preempted when a tier message arrives and the daily spend cap is shared
     * end-to-end. When omitted, the internal legacy queue is used (back-compat).
     */
    sharedLlmQueue?: import('./LlmQueue.js').LlmQueue;
    allowExternalLlm?: boolean;
    credentialPatterns?: string[];
    prefix?: string;
    conversationHistoryMax?: number;
    __dev_timerMultiplier?: number;
}
export interface ProxyMetadata {
    source: 'presence-proxy';
    tier: number;
    isProxy: true;
}
export interface ProcessInfo {
    pid: number;
    command: string;
    elapsed?: string;
}
interface PresenceState {
    topicId: number;
    sessionName: string;
    userMessageAt: number;
    userMessageText: string;
    /**
     * Sanitized tmux snapshot captured at the instant the user message
     * arrived. Used to scope tier prompts so the standby summary describes
     * only post-message activity, not whatever the agent was doing before.
     */
    userMessageBaselineSnapshot: string | null;
    tier1FiredAt: number | null;
    tier1Snapshot: string | null;
    tier1SnapshotHash: string | null;
    tier2FiredAt: number | null;
    tier2Snapshot: string | null;
    tier2SnapshotHash: string | null;
    tier3FiredAt: number | null;
    tier3Assessment: 'working' | 'waiting' | 'stalled' | 'dead' | null;
    tier3Summary: string | null;
    tier3RecheckCount: number;
    silencedUntil: number | null;
    cancelled: boolean;
    llmCallCount: number;
    lastLlmCallAt: number;
    conversationHistory: Array<{
        role: 'user' | 'proxy' | 'ack';
        text: string;
        timestamp: number;
    }>;
    /**
     * Text of the most recent brief ack sent by the agent in response to the
     * current user message (e.g., "Got it, looking into this"). Recorded by
     * recordAgentMessage when isBriefAck(text) is true. Used by fireTier1 to
     * detect the "agent has only acked so far" case and emit a fixed
     * placeholder instead of an LLM summary that just paraphrases the ack.
     */
    lastAckText: string | null;
    lastAckAt: number | null;
}
export declare function sanitizeTmuxOutput(raw: string, extraPatterns?: string[]): string;
export declare function guardProxyOutput(text: string): {
    safe: boolean;
    reason?: string;
};
/**
 * Check if terminal output indicates quota exhaustion.
 * Returns a human-friendly message if detected, null otherwise.
 *
 * Only checks the LAST 15 lines of the snapshot, AND verifies
 * the quota error is the last substantive content. If there are
 * meaningful output lines AFTER the quota error (indicating the
 * session resumed work after quota reset), treat it as stale.
 */
export declare function detectQuotaExhaustion(snapshot: string): string | null;
/**
 * Check if the terminal output indicates the session is idle at a prompt.
 * Returns true if the last substantive lines suggest the agent has finished
 * working and is waiting for new input.
 */
export declare function detectSessionIdle(snapshot: string): boolean;
/**
 * Framework-aware "session has finished and is parked at idle" detector.
 *
 * claude-code keeps the prompt-pattern detector (back-compat). For codex-cli
 * the idle composer (›) also renders WHILE the model is working, so prompt
 * presence is NOT a valid idle discriminator — the session is idle iff it does
 * NOT show the framework's active-work signal. Absent framework → claude-code
 * behavior. (2026-05-23: stuck Codex sessions were invisible to the
 * finished-check because the Claude prompt patterns never matched their pane,
 * so the "agent finished → stop heartbeats" early-exit never triggered — the
 * standby-flood half of the silently-stopped bug.)
 */
export declare function detectSessionFinished(snapshot: string, framework?: IntelligenceFramework): boolean;
/**
 * Deterministic stall assessment used when the tier-3 LLM is unavailable or
 * returns an unparseable classification. Instead of blindly assuming the
 * session is still "working" (which left stuck sessions escalating never — the
 * 2026-05-23 Codex incident, where the LLM couldn't read the Codex pane and the
 * fallback defaulted to active forever), fall back to the framework-aware
 * active-work signal: a pane with no active-work indicator is treated as
 * stalled so it surfaces to the user.
 */
export declare function deterministicStallAssessment(snapshot: string | null, framework?: IntelligenceFramework): 'working' | 'stalled';
/**
 * Returns true if `text` looks like a brief acknowledgement from the agent
 * — i.e., short and STARTS with a forward-looking ack phrase, OR very short
 * regardless of phrasing. Brief acks should NOT cancel PresenceProxy tier
 * timers; only substantive replies should.
 *
 * Hard caps:
 *   - Empty / whitespace-only → not an ack (no-op message)
 *   - Length <= 12 chars (e.g., "ok", "👍", "Got it.") → ack
 *   - Length <= 200 chars AND ack pattern appears in the OPENING (first 60
 *     chars after stripping the leading word/punctuation) → ack
 *   - Length > 200 chars → never an ack (substantive)
 *
 * The "opening only" rule matters: a substantive multi-sentence plan can
 * casually contain "I will" or "looking into" deep in the body without
 * being an ack. Acks are openers — the phrase shows up at the very start.
 */
export declare function isBriefAck(text: string | null | undefined): boolean;
/**
 * Given the agent's terminal pane captured AT user-message arrival
 * (`baseline`) and a later snapshot (`current`), return only the content
 * that has appeared since the baseline.
 *
 * Both inputs are sanitized tmux pane captures of the same fixed window,
 * so the bottom of `baseline` overlaps with somewhere in the middle of
 * `current`. We anchor on the last few non-empty lines of `baseline`,
 * find them in `current`, and return everything after.
 *
 * If the anchor can't be located (terminal scrolled past the baseline
 * entirely, e.g., during a very busy build), we conservatively return
 * the whole `current` snapshot — better to over-include than to lose
 * post-message activity. Callers receive an `anchored` flag so prompts
 * can label the snapshot accurately.
 */
export declare function extractDeltaSinceBaseline(current: string | null, baseline: string | null): {
    delta: string;
    anchored: boolean;
    hasNewActivity: boolean;
};
/**
 * Returns true when the post-message terminal delta contains nothing
 * substantive beyond the agent's own brief ack text.
 *
 * Used by Tier 1 to skip the LLM call in the common case where the agent
 * has only acked the user at the 20-second mark. The LLM otherwise just
 * paraphrases the ack into a generic "Agent acknowledged and is looking
 * into it" summary, which adds no information.
 *
 * Heuristic:
 *   - Compute the post-message delta from baseline → current.
 *   - If the delta is short (<= 350 chars trimmed) — which it is when the
 *     only new content is a brief ack typed into the prompt — treat it as
 *     ack-only. We don't require the ack text to literally appear in the
 *     terminal because the pane may have wrapped / formatted it differently.
 *   - If the delta is longer, fall through to the LLM path so genuine
 *     substantive activity still gets summarized.
 *
 * Bias: false-positive (treating real early progress as ack-only) costs
 * one fixed-string Tier 1 message; false-negative (treating an ack-only
 * delta as substantive) costs the original generic-LLM-summary regression.
 * 350 chars is generous toward the ack-only path because that's the bug
 * being fixed.
 */
export declare function isPostMessageDeltaAckOnly(current: string | null, baseline: string | null, ackText: string | null): boolean;
interface ToolWaitState {
    /** Last time agent emitted new text (snapshot hash changed). */
    lastAgentTextAt: number;
    /** When the current tool wait started (snapshot first detected unchanged with Cogitated). */
    toolStartedAt: number | null;
    /** Tool name extracted from snapshot, or 'unknown'. */
    toolName: string | null;
    /** When we entered long-wait state. */
    longWaitEnteredAt: number | null;
    /** When the one-time escalation message fired. */
    longWaitEscalatedAt: number | null;
    /** Sustained-text-recovery cursor — when uninterrupted new-text first started. */
    sustainedTextSince: number | null;
}
export declare class PresenceProxy {
    private config;
    private states;
    private timers;
    private llmQueue;
    private stateDir;
    private started;
    private tier1DelayMs;
    private tier2DelayMs;
    private tier3DelayMs;
    private tier3RecheckDelayMs;
    private silenceDurationMs;
    private prefix;
    private maxConversationHistory;
    private rateLimit;
    private toolWaitState;
    private toolWaitEnabled;
    private toolWaitEnterMs;
    private toolWaitExitHysteresisMs;
    private toolWaitEscalationCapMs;
    constructor(config: PresenceProxyConfig);
    start(): void;
    stop(): void;
    /**
     * Called when a message is logged. Starts/resets timers for user messages,
     * cancels proxy for agent messages.
     */
    onMessageLogged(event: MessageLoggedEvent): void;
    /**
     * Handle user commands: unstick, restart, quiet, resume
     */
    handleCommand(topicId: number, command: string, userId: number): Promise<boolean>;
    private handleUserMessage;
    private handleAgentMessage;
    private scheduleTier;
    private fireTier;
    private fireTier1;
    private fireTier2;
    private fireTier3;
    private handleQuiet;
    private handleResume;
    private handleUnstick;
    private handleRestart;
    /**
     * Build the snapshot block for tier prompts. When a baseline snapshot was
     * captured at user-message arrival, return the post-message delta plus an
     * explanatory header so the LLM scopes its summary to NEW activity. Falls
     * back to the full snapshot if no baseline exists or the anchor can't be
     * located.
     */
    private buildScopedSnapshotBlock;
    private buildTier1Prompt;
    private buildConversationPrompt;
    private buildTier2Prompt;
    private buildTier3Prompt;
    private callLlm;
    private sendProxyMessage;
    /** System/delivery messages that should NOT be treated as real agent responses.
     *  Thin wrapper over the shared classifier — kept as a method so existing
     *  instance-method callsites don't change. */
    private isSystemMessage;
    private checkRateLimit;
    private clearTimersForTopic;
    private cleanupState;
    private persistState;
    private recoverFromRestart;
    getState(topicId: number): PresenceState | undefined;
    getActiveTopics(): number[];
    private getOrCreateToolWaitState;
    /**
     * Record that the agent emitted new text (snapshot hash differs from last
     * tier capture). Resets the tool-wait timer; if currently in long-wait,
     * starts/extends a sustained-text window for hysteresis-based exit.
     */
    recordAgentText(topicId: number, _snapshotHash: string | null): void;
    /**
     * Record that a tool appears to be running (caller-driven path). Optional —
     * snapshot-diff path in fireTier* derives the same signal.
     */
    recordToolWait(topicId: number, toolName: string): ToolWaitState | null;
    /**
     * Returns the long-tool-wait swap message for a topic, or null if the
     * detector isn't tripped. Side-effects: enters long-wait state on first
     * crossing, fires the one-time escalation message at the cap.
     *
     * Called from fireTier2 / fireTier3 AFTER snapshot capture and AFTER the
     * heartbeat-suppression check (so a live /build heartbeat takes priority).
     */
    getLongToolWaitMessage(topicId: number): string | null;
    /**
     * Update the tool-wait detector based on a snapshot diff. Called from
     * fireTier2/fireTier3 after capturing the new snapshot.
     *
     * - If the hash matches the prior tier's hash AND a Cogitated marker is
     *   present, the agent is sitting in a single tool call → start/keep the
     *   toolStartedAt baseline.
     * - If the hash differs, treat it as new agent text (recordAgentText path).
     */
    private updateToolWaitFromSnapshot;
}
export {};
//# sourceMappingURL=PresenceProxy.d.ts.map