/**
 * Telegram Messaging Adapter — send/receive messages via Telegram Bot API.
 *
 * Uses long polling to receive messages. Supports forum topics
 * (each user gets a topic thread). Includes topic-session registry,
 * message logging, voice transcription, photo handling, stall detection,
 * auth gating, and delivery confirmation.
 *
 * No external dependencies — uses native fetch for Telegram API calls.
 */
import type { MessagingAdapter, Message, OutgoingMessage, IntelligenceProvider, IngressPosition } from '../core/types.js';
import { NotificationBatcher, NotificationTier } from './NotificationBatcher.js';
import type { ContentValidationConfig } from './TopicContentValidator.js';
import { MessagingEventBus } from './shared/MessagingEventBus.js';
import type { DetectedPrompt } from '../monitoring/PromptGate.js';
import { type FormatMode } from './TelegramMarkdownFormatter.js';
import { type AutonomousJournalSeam } from '../core/AutonomousSessions.js';
import { type AttentionTopicGuardConfig } from './AttentionTopicGuard.js';
export interface TelegramConfig {
    /** Bot token from @BotFather */
    token: string;
    /** Forum chat ID (the supergroup where topics live) */
    chatId: string;
    /** Polling interval in ms */
    pollIntervalMs?: number;
    /** Authorized Telegram user IDs (only these users' messages are processed) */
    authorizedUserIds?: number[];
    /**
     * Owner principal — the single Telegram user id who controls
     * security-sensitive decisions for this instance (notably the
     * consent gate for Tier-2 relay tunnels — see
     * specs/dev-infrastructure/tunnel-failure-resilience.md).
     *
     * Why a separate field from `authorizedUserIds`: that set is the
     * "who may interact with the bot" allowlist (multiple users may
     * legitimately use the bot for routine interaction), while
     * `ownerUserId` is the principal who alone may approve actions
     * with cross-user security impact (relay activation, credential
     * exposure, etc.). The GPT external review on the tunnel spec
     * specifically flagged that the consent gate must NOT trust the
     * broader authorizedUserIds set.
     *
     * `sendToOwnerDM()` uses this id as the private-chat target.
     * Defaults to `promptGate?.ownerId` for back-compat when unset.
     */
    ownerUserId?: number;
    /** Voice transcription provider: 'groq' or 'openai' (auto-detects if not set) */
    voiceProvider?: string;
    /** Stall detection timeout in minutes (default: 5, 0 to disable) */
    stallTimeoutMinutes?: number;
    /** Promise follow-through timeout in minutes (default: 10, 0 to disable) */
    promiseTimeoutMinutes?: number;
    /** Lifeline topic thread ID — the always-available channel. Auto-recreated if deleted. */
    lifelineTopicId?: number;
    /** Dashboard topic thread ID — auto-broadcasts tunnel URL on startup. */
    dashboardTopicId?: number;
    /** Dashboard PIN (for including in broadcast messages). */
    dashboardPin?: string;
    /** Content validation configuration — validates outbound messages against topic purpose */
    contentValidation?: ContentValidationConfig;
    /** Prompt Gate configuration for Telegram relay */
    promptGate?: {
        /** Telegram user ID of the session owner (only this user can respond to prompts) */
        ownerId?: number;
        /** Timeout in seconds for relay responses (default: 300 = 5 min) */
        relayTimeoutSeconds?: number;
    };
    /**
     * Hot-reloadable accessor for the Telegram format mode. Returning a falsy
     * value or `'legacy-passthrough'` preserves pre-PR2 behavior (byte-for-byte
     * passthrough, caller-supplied parse_mode honored). See
     * docs/specs/TELEGRAM-MARKDOWN-RENDERER-SPEC.md.
     */
    getFormatMode?: () => FormatMode | undefined;
    /** Hot-reloadable accessor for lint-strict mode. */
    getLintStrict?: () => boolean | undefined;
    /**
     * Per-source forum-topic circuit breaker. Caps how many NEW topics a single
     * attention `sourceContext` may spawn in a rolling window; past the budget,
     * items coalesce into one running notice topic instead of a wall of topics.
     * Defaults to DEFAULT_ATTENTION_TOPIC_GUARD (enabled) — the 2026-05-28
     * topic-flood lockdown. See AttentionTopicGuard.
     */
    attentionTopicGuard?: Partial<AttentionTopicGuardConfig>;
    /**
     * The LAST-RESORT ceiling on automatic forum-topic creation, enforced INSIDE
     * `createForumTopic` itself — the one place topics are born — so it covers
     * EVERY caller, current and future, regardless of what source labels they
     * pass (the 2026-06-05 flood dodged the attention guard's per-source budget
     * by giving every item a unique sourceContext; only the global ceiling
     * caught it after 8 leaked topics). Origins `user` and `system` are exempt
     * (operator-initiated topics and bounded create-once system topics);
     * everything else — including any caller that doesn't say — is counted.
     * Per-label budget + global ceiling, same engine as the attention guard.
     * Defaults are deliberately LOOSER than the attention guard (this is the
     * backstop, not the shaper): per-label 8, global 12, 10-min window.
     */
    topicCreationBudget?: Partial<AttentionTopicGuardConfig>;
    /**
     * The calm "Agent Health" lane. Routine self-health/housekeeping notices
     * (attention items with `lane:'agent-health'`) are collected into ONE named,
     * low-key topic that never spawns topic-after-topic — the antidote to a feature
     * hijacking Telegram with a wall of "stale but unkillable"-style notices. Only
     * items that explicitly opt in (`lane:'agent-health'`) are affected; every other
     * item is unchanged. Enabled by default; set `enabled:false` for pre-lane
     * behavior.
     */
    agentHealthLane?: {
        enabled?: boolean;
        /** Display name of the lane topic. Default '🩺 Agent Health'. */
        topicName?: string;
        /** Bounded ring of recently-seen entity keys (memory cap). Default 256. */
        maxTrackedKeys?: number;
        /** Suppress a same-key re-escalation posted within this window. Default 30 min. */
        dedupWindowMs?: number;
    };
}
export interface SendResult {
    /** Telegram message ID */
    messageId: number;
    /** Topic the message was sent to */
    topicId?: number;
}
export interface DashboardBroadcastResult {
    /** Whether an existing pinned dashboard message was edited instead of posting a new one. */
    edited: boolean;
    /** Telegram message ID when a new dashboard message was posted. */
    messageId?: number;
    /** Non-fatal follow-up failures after the message was delivered. */
    warnings: string[];
}
interface LogEntry {
    messageId: number;
    topicId: number | null;
    text: string;
    fromUser: boolean;
    timestamp: string;
    sessionName: string | null;
    senderName?: string;
    senderUsername?: string;
    telegramUserId?: number;
}
/**
 * Pre-dispatch hook for the agent-to-agent Telegram comms primitive. Adapter calls this
 * BEFORE normal onTopicMessage / this.handler dispatch on text messages. Returns
 * {handled:true} → dispatch is skipped (hook routed or dropped the a2a event);
 * {handled:false} → fall through to normal user flow. Spec: MENTOR-LIVE-READINESS §Fix 2b.
 */
export interface AgentMessageHookInput {
    /** Raw message text (the body the marker parser inspects). */
    text: string;
    /** Telegram topic id (or GENERAL_TOPIC_ID fallback). */
    topicId: number;
    /** msg.from.is_bot (per Telegram's update shape). False for human users — the structural
     *  half of the a2a spoof defense. */
    senderIsBot: boolean;
    /** msg.sender_chat.id (when present — group bot-as-channel relay). */
    senderChatId?: string;
    /** Effective bot identity: sender_chat.id ?? from.id, stringified. */
    senderBotId?: string;
    /** Wall-clock now (ms) — passed in for testability. */
    now: number;
}
export type AgentMessageHook = (input: AgentMessageHookInput) => Promise<{
    handled: boolean;
}>;
export interface AttentionItem {
    id: string;
    title: string;
    summary: string;
    description?: string;
    category: string;
    priority: 'URGENT' | 'HIGH' | 'NORMAL' | 'LOW';
    status: 'OPEN' | 'ACKNOWLEDGED' | 'IN_PROGRESS' | 'DONE' | 'WONT_DO';
    sourceContext?: string;
    createdAt: string;
    updatedAt: string;
    topicId?: number;
    /**
     * True when the topic-flood guard coalesced this item into a shared "notices
     * coalesced" topic instead of giving it its own. Such items are NOT registered
     * in the per-item topic maps and are managed via /attention (PATCH / dashboard),
     * not per-topic /ack — so resolving one never closes the shared notice topic.
     */
    coalesced?: boolean;
    /**
     * Routing lane. When `'agent-health'`, this is a routine SELF-HEALTH /
     * housekeeping notice about the agent's OWN internal state (a stale-looking
     * session, a peer it can't reach, etc.) rather than something the USER must act
     * on. Such items are routed into ONE calm, persistently-named "🩺 Agent Health"
     * topic from the very first item — they never spawn their own per-item topic
     * (even under budget, even if mis-tagged HIGH), and same-entity re-escalations
     * are suppression-deduped so the lane stays quiet. Absent ⇒ today's behavior.
     */
    lane?: 'agent-health';
    /**
     * Stable per-entity key for Agent-Health-lane suppression dedup (e.g. a session
     * id). Decoupled from `id` (which may carry a per-episode suffix) so the same
     * entity re-escalating within the dedup window is suppressed rather than
     * reposted. Falls back to `sourceContext`, then `id`, when unset.
     */
    healthKey?: string;
    /** WS4.1 (MULTI-MACHINE-SEAMLESSNESS-SPEC §WS4.1): the machine this item
     *  belongs to. Stamped at read time in GET /attention (the store stays
     *  machine-agnostic); absent on a single-machine install. */
    machineId?: string;
    /** Display nickname of `machineId`, resolved from the pool registry at read
     *  time. */
    machineNickname?: string;
}
/**
 * Standard topic styles for visual organization in Telegram forum.
 * Colors are the 6 values Telegram's Bot API accepts for icon_color.
 * Emojis prefix topic names for at-a-glance scanning.
 */
export declare const TOPIC_STYLE: {
    /** Green — core infrastructure (Lifeline) */
    readonly SYSTEM: {
        readonly color: 9367192;
        readonly emoji: "🛡️";
    };
    /** Purple — automated recurring jobs */
    readonly JOB: {
        readonly color: 13338331;
        readonly emoji: "⚙️";
    };
    /** Green — interactive user sessions */
    readonly SESSION: {
        readonly color: 9367192;
        readonly emoji: "💬";
    };
    /** Blue — informational (Dashboard, Updates) */
    readonly INFO: {
        readonly color: 7322096;
        readonly emoji: "📢";
    };
    /** Yellow — needs user attention */
    readonly ALERT: {
        readonly color: 16766590;
        readonly emoji: "🔔";
    };
};
/**
 * Select an appropriate emoji for a topic based on its name.
 * Matches keywords case-insensitively. Falls back to 💬 for unmatched names.
 */
export declare function selectTopicEmoji(topicName: string): string;
export declare class TelegramAdapter implements MessagingAdapter {
    readonly platform = "telegram";
    private config;
    private handler;
    private polling;
    /** True when this adapter is actively polling for messages (false in send-only mode). */
    get isPolling(): boolean;
    private pollTimeout;
    private lastUpdateId;
    private startedAt;
    private consecutivePollErrors;
    private lastPollError;
    private fatalPollReason;
    private pollStoppedAt;
    private pending401Retry;
    private notAForum;
    private notAForumWarned;
    private topicToSession;
    private sessionToTopic;
    private topicToName;
    private topicToPurpose;
    private registryPath;
    private messageLogPath;
    private offsetPath;
    private stateDir;
    /** Per-bot state root. Equals stateDir for the primary bot; a namespaced sub-dir for
     *  non-primary adapters (multi-instance isolation — spec MENTOR-LIVE-READINESS §Fix 2b).
     *  Only the per-bot state files (registry/messages/offset/attention) live here; shared
     *  config lookups stay on stateDir. */
    private botStateDir;
    /** When true, start() skips ensureLifelineTopic() — a non-primary bot must not create a
     *  second Lifeline topic in the chat. */
    private suppressLifelineAutoCreate;
    /** Pre-dispatch hook for the agent-to-agent Telegram comms primitive (spec
     *  MENTOR-LIVE-READINESS §Fix 2b). When set, the text-dispatch path calls this BEFORE
     *  onTopicMessage / this.handler. If it returns {handled:true}, normal dispatch is
     *  skipped (the hook routed the message to a role-handler OR dropped it as an a2a
     *  security event). If {handled:false}, normal user flow continues. Other message types
     *  (voice/photo/document) cannot carry the [a2a:…] marker and bypass the hook.
     *  See installAgentMessageHook for the production binding. */
    private agentMessageHook?;
    /** Optional coherence-journal seam (COHERENCE-JOURNAL-SPEC §3.3). The adapter
     *  holds no StateManager, so the emergency-stop path's stopAutonomousTopic call
     *  cannot reach the wired journal on its own. server.ts injects this seam so the
     *  sentinel emergency-stop emits the autonomous-run `stopped` event like every
     *  other stop funnel. Undefined → the stop stays silent (best-effort, never blocks). */
    private coherenceJournalSeam?;
    private attentionItemToTopic;
    private attentionTopicToItem;
    private attentionItems;
    private attentionFilePath;
    private attentionTopicGuard;
    private topicCreationGuard;
    private floodNoticeTopicByBucket;
    private floodNoticePending;
    private attentionSuppressedLogPath;
    /** Resolved lane config (defaults applied in the constructor). */
    private agentHealthLaneCfg;
    /** The single reused lane topic id (created lazily, once). */
    private agentHealthTopicId;
    /** In-flight single creation guard (no double-create race). */
    private agentHealthPending;
    /** entity key -> last-posted epoch ms (suppression-dedup ring, insertion-ordered). */
    private agentHealthKeyRing;
    private pendingMessages;
    private stallCheckInterval;
    private pendingPromises;
    onTopicMessage: ((message: Message) => void | Promise<void>) | null;
    onInterruptSession: ((sessionName: string) => Promise<boolean>) | null;
    onRestartSession: ((sessionName: string, topicId: number) => Promise<void>) | null;
    onListSessions: (() => Array<{
        name: string;
        tmuxSession: string;
        status: string;
        alive: boolean;
    }>) | null;
    onIsSessionAlive: ((tmuxSession: string) => boolean) | null;
    onIsSessionActive: ((tmuxSession: string) => Promise<boolean>) | null;
    /**
     * Route-command handler. Called when a user posts `/route <framework>`
     * in a topic. The handler is responsible for persisting the new
     * framework binding and triggering a respawn of the topic's session.
     * Returns a short status string for the adapter to echo back to the
     * topic, or null on failure.
     */
    onRouteCommand: ((topicId: number, framework: string | null, userId?: number) => Promise<{
        ok: boolean;
        message: string;
    }>) | null;
    /**
     * Topic Profile command handler (TOPIC-PROFILE-SPEC §10.1 — the /topic
     * power-user surface; the conversational surface is PRIMARY). Receives the
     * raw argument text plus the AUTHENTICATED sender uid (`msg.from.id`).
     */
    onTopicProfileCommand: ((topicId: number, argText: string, userId: number) => Promise<{
        ok: boolean;
        message: string;
    }>) | null;
    /**
     * Local-model command handler. Called when a user posts
     * `/local-model <provider> [model]` (or `/local-model off`) in a
     * topic. The handler validates the provider/model is reachable,
     * persists the per-topic local-model binding, and triggers a
     * respawn so the new session spawns under Codex --oss
     * --local-provider <provider>.
     *
     * Conversational counterpart of editing config.json — Justin's
     * "every config change reachable via Telegram" rule.
     *
     * - provider = null + model = null → "show status" (no mutation).
     * - provider = 'off' / 'none' → clear the topic's override (revert
     *   to cloud Codex).
     */
    onLocalModelCommand: ((topicId: number, provider: string | null, model: string | null) => Promise<{
        ok: boolean;
        message: string;
    }>) | null;
    onMessageLogged: ((entry: {
        messageId: number;
        topicId: number | null;
        text: string;
        fromUser: boolean;
        timestamp: string;
        sessionName: string | null;
        senderName?: string;
        senderUsername?: string;
        telegramUserId?: number;
    }) => void) | null;
    /**
     * Outbound relay for a TOKENLESS standby (bug #7). When this adapter has no bot
     * token — a multi-machine pool standby serving a session moved to it — it cannot
     * call the Telegram API directly. If this is wired, sendToTopic routes the send
     * through it (the server wires it to POST the Telegram-OWNING router's
     * /telegram/reply/:topicId), so a moved session's replies reach the user without
     * the standby ever polling/sending on the shared bot (preserving the single-owner
     * invariant that avoids the 409-poller-conflict incident). Returns the sent
     * message's SendResult, or null if the relay could not deliver.
     */
    outboundRelay: ((topicId: number, text: string, options?: {
        silent?: boolean;
        kindMetadata?: Record<string, unknown>;
    }) => Promise<SendResult | null>) | null;
    /**
     * True when a `sendToTopic` will RELAY through the lease holder rather than
     * send directly — i.e. this is a tokenless pool standby with `outboundRelay`
     * wired. Mirrors the exact `sendToTopic` branch condition (no usable string
     * bot token + a relay available). Callers use this to skip work the HOLDER
     * will do anyway: e.g. the `/telegram/reply` route skips its LOCAL tone gate
     * for a relayed reply, because (a) the reply is already the agent's finalized
     * output, (b) the Telegram-owning holder runs ITS tone gate on receipt, and
     * (c) running the standby's tone gate adds a serial LLM call to every
     * cross-machine reply — which, under a rate-limited circuit, waits up to
     * `MessagingToneGate` rateLimitWaitMs (120s) BEFORE the relay even starts.
     * That double-gate + pre-relay stall is a real robustness defect on the
     * standby reply path.
     */
    willRelay(): boolean;
    onSentinelIntercept: ((message: string, topicId: number) => Promise<{
        category: 'emergency-stop' | 'pause' | 'redirect' | 'normal';
        action: {
            type: string;
            message?: string;
        };
        reason?: string;
    } | null>) | null;
    onSentinelKillSession: ((sessionName: string) => boolean) | null;
    /** Durable Inbound Message Queue §3.6: settle a stopped topic's queued
     *  custody (terminal operator-stop + PIS cleanup + loss report). Wired by
     *  the server when the queue engine is live; null = no queue, no-op. */
    onSentinelStopCustody: ((topicId: number) => void) | null;
    onSentinelPauseSession: ((sessionName: string) => void) | null;
    onAttentionStatusChange: ((itemId: string, status: string) => Promise<void>) | null;
    onSwitchAccountRequest: ((target: string, replyTopicId: number) => Promise<void>) | null;
    onQuotaStatusRequest: ((replyTopicId: number) => Promise<void>) | null;
    onLoginRequest: ((email: string | null, replyTopicId: number) => Promise<void>) | null;
    onClassifySessionDeath: ((sessionName: string) => Promise<{
        cause: string;
        detail: string;
    } | null>) | null;
    /** LLM-powered stall triage — called instead of generic stall alert when set */
    onStallDetected: ((topicId: number, sessionName: string, messageText: string, injectedAt: number) => Promise<{
        resolved: boolean;
    }>) | null;
    /** Get triage status for a topic — returns null if no active triage, or status summary */
    onGetTriageStatus: ((topicId: number) => {
        active: boolean;
        classification?: string;
        checkCount: number;
        lastCheck?: string;
    } | null) | null;
    onGetRegistrationPolicy: (() => {
        policy: string;
        contactHint?: string;
        agentName?: string;
    }) | null;
    onNotifyAdminJoinRequest: ((request: {
        name: string;
        username?: string;
        telegramUserId: number;
    }) => Promise<void>) | null;
    onValidateInviteCode: ((code: string, telegramUserId: number) => Promise<{
        valid: boolean;
        error?: string;
    }>) | null;
    onStartMiniOnboarding: ((telegramUserId: number, firstName: string, username?: string) => Promise<void>) | null;
    private unknownUserRateLimit;
    private static readonly UNKNOWN_USER_COOLDOWN_MS;
    private batcher;
    intelligence: IntelligenceProvider | null;
    onFlushNotifications: ((replyTopicId: number) => Promise<void>) | null;
    private callbackRegistry;
    private pendingPromptReply;
    private promptGateDisclosureSent;
    /**
     * Tunnel-consent callback handler. Registered by TunnelManager via
     * `setTunnelConsentHandler`. Receives the parsed action ('grant' |
     * 'decline') + the nonce; returns a short status string for the
     * answerCallbackQuery toast. The owner-principal check happens in
     * processCallbackQuery BEFORE this is invoked, so the handler can
     * trust the click came from the owner.
     */
    private tunnelConsentHandler;
    /**
     * UNIFIED-SESSION-LIFECYCLE bonus — fired when a Telegram forum topic is
     * renamed. Wired in server.ts to update the bound session's display name
     * (display `name` only — never the tmuxSession key or id). Fire-and-forget.
     */
    private topicRenamedHandler;
    /** Wire the tunnel-consent callback handler (called by TunnelManager). */
    setTunnelConsentHandler(fn: ((action: 'grant' | 'decline', nonce: string) => Promise<string>) | null): void;
    /**
     * Wire the topic-renamed callback (UNIFIED-SESSION-LIFECYCLE bonus). Fires
     * fire-and-forget when a Telegram forum topic is renamed; consumers update
     * the bound session's display `name` only (never the tmuxSession key or id).
     */
    setTopicRenamedHandler(fn: ((topicId: number, newName: string) => void | Promise<void>) | null): void;
    /**
     * Send a consent prompt to the owner DM with two inline buttons
     * (approve / decline). The buttons carry callback_data of the form
     * `tc:g:<nonce>` / `tc:d:<nonce>` (well under Telegram's 64-byte
     * callback_data limit — `tc:g:` is 5 chars + a 32-hex nonce = 37).
     * Returns the message id on success, null on failure (no owner,
     * owner hasn't DM'd the bot, network error) — fire-and-forget.
     */
    sendOwnerConsentPrompt(text: string, nonce: string): Promise<number | null>;
    /** Callback to inject a response into a tmux session. Wired by server.ts. */
    onPromptResponse: ((sessionName: string, key: string) => boolean) | null;
    /** Callback to inject text input into a tmux session. Wired by server.ts. */
    onPromptTextResponse: ((sessionName: string, text: string) => boolean) | null;
    /** Callback when relay lease should extend idle timeout for a session */
    onRelayLeaseStart: ((sessionName: string) => void) | null;
    /** Callback when relay lease is released (response received or timeout) */
    onRelayLeaseEnd: ((sessionName: string) => void) | null;
    /**
     * Per-topic monotonic content version — bumped on every message logged for the
     * topic. The live-tail streamer polls this (getTopicContentVersion) as its
     * cheap change-detector: an unchanged version means the topic's tail content
     * is byte-identical, so the streamer skips serializing it entirely (the
     * 2026-06-05 event-loop-stall fix — building every topic's history every tick
     * blocked the loop for seconds).
     */
    private topicContentVersion;
    /**
     * In-memory recent-tail cache backing getTopicHistory — seeded lazily from the
     * JSONL on a topic's first read, then maintained on every append. Bounds the
     * cost of repeated history reads to O(limit) instead of a full synchronous
     * re-read of the (up to 75k-line) message log per call. Holds the most recent
     * TAIL_CACHE_LIMIT entries per topic, which covers every production caller
     * (live-tail + handoff hash use 500, respawn history far less). Log rotation
     * is irrelevant to it — rotation only drops OLD lines, and the cache only
     * holds the newest.
     */
    private topicTailCache;
    /** One-time batch seed done (all live topics seeded in a single file pass). */
    private tailCacheSeeded;
    private static readonly TAIL_CACHE_LIMIT;
    private sharedLogger;
    private sharedRegistry;
    private sharedStallDetector;
    private sharedCommandRouter;
    private sharedAuthGate;
    private eventBus;
    /** Get the event bus for external subscribers (Phase 1e). Returns null if flag is off. */
    getEventBus(): MessagingEventBus | null;
    constructor(config: TelegramConfig, stateDir: string, opts?: {
        subDir?: string;
        suppressLifelineAutoCreate?: boolean;
    });
    /**
     * Register all Telegram commands with the shared CommandRouter (Phase 1a).
     * Each command delegates back to the existing handler logic.
     */
    private registerSharedCommands;
    start(): Promise<void>;
    stop(): Promise<void>;
    /**
     * Stable provider-level identity for an inbound update — the Telegram
     * update_id. Used by the message-processing ledger so a redelivered update
     * (retry / reconnect / transfer-window overlap) is recognized and not
     * re-acted-on.
     */
    dedupeKey(rawEvent: unknown): string;
    /** The durable resumable position — the long-poll update_id offset. */
    getIngressPosition(): IngressPosition;
    /**
     * Stop the inbound loop deterministically and return the durable position
     * AFTER the stop (the persisted offset), so a handoff resumes from exactly
     * where this machine left off — never replaying or skipping.
     */
    stopConsuming(): Promise<IngressPosition>;
    /** Resume the inbound loop from exactly the given position. */
    resumeConsuming(position: IngressPosition): Promise<void>;
    send(message: OutgoingMessage): Promise<SendResult>;
    /**
     * Log an inbound user message that arrived via an external path (e.g. Lifeline
     * forwarding through /internal/telegram-forward). This ensures the message
     * appears in both JSONL and TopicMemory even when the normal polling handler
     * didn't receive it.
     */
    logInboundMessage(entry: {
        messageId: number;
        topicId: number;
        text: string;
        timestamp: string;
        senderName?: string;
        senderUsername?: string;
        telegramUserId?: number;
    }): void;
    /**
     * Send a message to a specific forum topic.
     * Returns the Telegram message ID for delivery confirmation.
     */
    sendToTopic(topicId: number, text: string, options?: {
        silent?: boolean;
        skipStallClear?: boolean;
        /** Kind metadata (messageKind/senderClass/advisoryAck…) forwarded to
         *  the holder on a RELAYED send so the kind survives the cross-machine
         *  hop (spec outbound-jargon-filepath-gap §2.5). Direct sends ignore it
         *  — the local route already consumed it. */
        kindMetadata?: Record<string, unknown>;
    }): Promise<SendResult>;
    /**
     * Send a notification through the batcher, falling back to direct send.
     * Use this for internal system notifications that should be batched.
     */
    notifyTopic(topicId: number, text: string, tier: NotificationTier, category: string): Promise<void>;
    /**
     * Configure the notification batcher. Call before start() to enable batching.
     * The batcher's send function is wired to sendToTopic automatically.
     */
    configureBatcher(config?: {
        summaryIntervalMinutes?: number;
        digestIntervalMinutes?: number;
        quietHours?: {
            enabled: boolean;
            start: string;
            end: string;
        };
    }): NotificationBatcher;
    /**
     * Get the notification batcher (if configured).
     */
    getBatcher(): NotificationBatcher | null;
    /**
     * Create a forum topic in the supergroup.
     *
     * THE TOPIC-CREATION CHOKEPOINT. Every topic this process creates is born
     * here, so the last-resort flood ceiling lives here (see
     * `topicCreationBudget` on the config). `opts.origin` declares who is
     * asking:
     *   - 'user'   — a human asked for this topic (operator command, inbound
     *                conversation). Exempt: humans are self-rate-limiting.
     *   - 'system' — a bounded, create-once-then-reuse infrastructure topic
     *                (Lifeline, Dashboard, Updates, the flood-notice topic
     *                itself). Exempt: cardinality is fixed by design.
     *   - 'auto'   — anything a feature decided to create on its own. Counted.
     *                THIS IS THE DEFAULT — a caller that doesn't declare an
     *                origin is budgeted, so a future mis-wired feature is
     *                bounded without having to know this mechanism exists.
     * Past the budget this throws TopicFloodBudgetError — the same failure
     * shape as a Telegram 429, which every caller already survives.
     */
    createForumTopic(name: string, iconColor?: number, opts?: {
        origin?: 'user' | 'system' | 'auto';
        label?: string;
    }): Promise<{
        topicId: number;
        name: string;
    }>;
    /**
     * Edit a forum topic's name and/or icon color.
     * Best-effort — silently ignores failures (topic may not exist).
     */
    editForumTopic(topicId: number, name?: string, iconColor?: number): Promise<boolean>;
    /**
     * Find an existing topic by name, or create a new one if none exists.
     * Prevents duplicate topics when sessions respawn or the server restarts.
     */
    findOrCreateForumTopic(name: string, iconColor?: number, opts?: {
        origin?: 'user' | 'system' | 'auto';
        label?: string;
    }): Promise<{
        topicId: number;
        name: string;
        reused: boolean;
    }>;
    /**
     * Get the Lifeline topic ID (if configured).
     */
    getLifelineTopicId(): number | undefined;
    /**
     * Owner principal — the single Telegram user id who controls
     * security-sensitive decisions for this instance. Falls back to the
     * promptGate.ownerId for back-compat when ownerUserId isn't
     * explicitly configured.
     */
    getOwnerUserId(): number | undefined;
    /**
     * Send a private DM to the owner principal. Telegram's bot API
     * permits the bot to initiate a DM only after the user has messaged
     * the bot at least once; if the user has never DM'd the bot, the
     * send will fail with HTTP 403 ("forbidden: bot can't initiate
     * conversation with a user"). Callers should treat a null return
     * as a non-fatal degradation, NOT a fatal error.
     *
     * Per the tunnel-failure-resilience spec (Part 3 — two-channel
     * notification), this is the ONLY channel that ever carries the
     * URL + PIN + signed view links. Group topics get status text
     * only.
     *
     * Returns the SendResult on success; null on any failure (no
     * owner configured, owner never DM'd the bot, network error,
     * etc.). All failure paths are logged-not-thrown to preserve the
     * fire-and-forget contract callers expect for outbound messaging.
     */
    sendToOwnerDM(text: string): Promise<SendResult | null>;
    /**
     * Ensure the Lifeline topic exists. If it was deleted, recreate it.
     * Called on startup and can be called periodically.
     */
    ensureLifelineTopic(): Promise<number | null>;
    /**
     * Persist the Lifeline topic ID back to config.json so it survives restarts.
     */
    private persistLifelineTopicId;
    /**
     * Get the Dashboard topic ID (if configured).
     */
    getDashboardTopicId(): number | undefined;
    /**
     * Whether the chat supports forum topics. False if we detected
     * "the chat is not a forum" from the Telegram API.
     */
    get isForumChat(): boolean;
    /**
     * Ensure the Dashboard topic exists. Creates it on first run, verifies on restart.
     * Same resilience pattern as the lifeline topic.
     */
    ensureDashboardTopic(): Promise<number | null>;
    /**
     * Broadcast the dashboard URL to the Dashboard topic.
     *
     * Edit-in-place pattern: instead of posting a new message each restart (which
     * creates unread badges), we edit the existing pinned message. This means the
     * Dashboard topic never shows as "unread" — it's a quiet reference the user
     * checks when they need the link.
     *
     * Fallback: if the pinned message was deleted or doesn't exist yet, we send
     * a new one, pin it, and save its ID for future edits.
     */
    broadcastDashboardUrl(url: string, tunnelType: 'quick' | 'named'): Promise<DashboardBroadcastResult>;
    private formatDashboardMessage;
    private loadDashboardMessageId;
    private saveDashboardMessageId;
    /**
     * Persist the Dashboard topic ID back to config.json.
     */
    private persistDashboardTopicId;
    /**
     * Close a forum topic.
     */
    closeForumTopic(topicId: number): Promise<boolean>;
    onMessage(handler: (message: Message) => Promise<void>): void;
    /**
     * Install the agent-to-agent Telegram comms pre-dispatch hook. The hook fires on text
     * messages BEFORE onTopicMessage / this.handler dispatch — see AgentMessageHook docs
     * above. Setting to undefined disables the hook (falls back to pure user-message flow).
     */
    setAgentMessageHook(hook: AgentMessageHook | undefined): void;
    /**
     * Inject the coherence-journal seam (COHERENCE-JOURNAL-SPEC §3.3). The adapter
     * has no StateManager, so its emergency-stop path threads this seam into
     * stopAutonomousTopic so the autonomous-run `stopped` event is emitted on a
     * sentinel-driven stop. server.ts wires this right after state.setCoherenceJournal.
     */
    setCoherenceJournalSeam(seam: AutonomousJournalSeam | undefined): void;
    /**
     * Dispatch an inbound text to the installed agent-message hook (if any) BEFORE
     * normal user-message routing. Returns true iff the hook claimed the message
     * (`{handled: true}`); the caller MUST then short-circuit and NOT route the
     * message to the user-message flow.
     *
     * Why this is public: in send-only mode the primary adapter doesn't poll —
     * the lifeline polls and forwards via `/internal/telegram-forward`. The
     * forwarded path calls `onTopicMessage` directly, bypassing the polling
     * text-dispatch site where the hook lives. Exposing this dispatcher lets the
     * forward handler invoke the same gate before falling through, so the
     * mentee receiver wiring fires regardless of message origin.
     *
     * Spoof defense: the hook needs `senderIsBot` AND `senderChatId` to decide
     * whether a marker-bearing message is from a real bot or a user typing a
     * marker-shaped string (spec §Routing matrix:
     * `agent-marker-spoofed-by-user`). The caller passes through what it has;
     * `senderBotId` is derived as `senderChatId` (group bot-as-channel) or the
     * passed `rawFromId` when `senderIsBot` is true (DM / topic post by a bot).
     */
    dispatchAgentMessageHook(ctx: {
        text: string;
        topicId: number;
        senderIsBot: boolean;
        senderChatId?: string;
        senderBotId?: string;
        rawFromId?: string;
        now?: number;
    }): Promise<boolean>;
    resolveUser(channelIdentifier: string): Promise<string | null>;
    /**
     * Check if a message is from an authorized user.
     * If no authorizedUserIds configured, all messages are accepted.
     */
    private isAuthorized;
    /**
     * Public read-only authorization check for a sender id (Know Your Principal #898,
     * increment 2d). Wraps the private `isAuthorized` so the lifeline-forward route can
     * decide whether an inbound sender is an authorized operator BEFORE binding them as
     * the topic operator — an UNAUTHORIZED sender must never become the operator
     * (that would re-open the "Caroline" cross-principal bug). Accepts number|string
     * (the route carries the id as a string); a blank/non-numeric id returns false.
     * Same trust model as `isAuthorized`: with no `authorizedUserIds` allowlist, every
     * authenticated sender is accepted (the agent already serves everyone there).
     */
    isAuthorizedSender(userId: number | string): boolean;
    /**
     * Handle a message from an unknown/unauthorized Telegram user.
     * Checks the registration policy and responds appropriately:
     * - admin-only: Gated message + notify admin
     * - invite-only: Ask for invite code
     * - open: Start mini-onboarding (rate limited)
     *
     * Rate-limited to prevent spam from the same unknown user.
     */
    private handleUnknownUser;
    registerTopicSession(topicId: number, sessionName: string, topicName?: string): void;
    unregisterTopic(topicId: number): void;
    getSessionForTopic(topicId: number): string | null;
    /**
     * Get all active topic→session mappings.
     * Used by TopicResumeMap heartbeat to proactively persist UUIDs.
     */
    getAllTopicSessions(): Map<number, string>;
    getTopicForSession(sessionName: string): number | null;
    /**
     * Disk-backed reverse lookup: re-reads the persisted topic-session registry
     * and returns the topic bound to `sessionName`, or null. This is the fallback
     * for {@link getTopicForSession} when the in-memory map misses because the
     * binding was registered AFTER this process loaded the registry.
     *
     * The concrete gap (2026-05-28): a `--no-telegram` server's in-memory
     * `sessionToTopic` reflects only its boot-time registry snapshot, while the
     * lifeline keeps writing new topic↔session bindings to the file as long-lived
     * dev sessions come up. A session bound after the server booted therefore
     * resolves to null in-memory — which made ContextWedgeSentinel recovery bail
     * with `not_telegram_bound` and leave the wedged session dead. A fresh disk
     * read closes that hole on the (rare) recovery path without changing the
     * hot-path semantics of getTopicForSession.
     *
     * Pure read: does NOT mutate the in-memory maps (the respawn path re-registers
     * the new session for the topic anyway).
     */
    resolveTopicForSessionFromDisk(sessionName: string): number | null;
    getTopicName(topicId: number): string | null;
    /**
     * The topic IDs this adapter has registered a session for — the set of "live"
     * conversations. Used by the live-tail streamer (spec §8 G3b) to enumerate the
     * active topics whose tail should be pushed to the standby.
     */
    getKnownTopicIds(): number[];
    /**
     * Actively resolve a topic's name from Telegram by sending a temporary probe message
     * that replies to the topic's service message (whose message_id = topic_id).
     * The API response includes reply_to_message.forum_topic_created.name.
     * The probe message is deleted immediately after.
     */
    resolveTopicName(topicId: number): Promise<string | null>;
    /**
     * Resolve all topic names that are still using the fallback "topic-NNNN" pattern.
     * Called on startup to backfill names for topics created before name tracking.
     */
    resolveUnknownTopicNames(): Promise<void>;
    /**
     * Set the purpose for a topic (e.g., "billing", "technical").
     * Purpose is used for outbound content validation.
     */
    setTopicPurpose(topicId: number, purpose: string): void;
    /**
     * Get the purpose for a topic. Checks runtime map first, then config.
     * Returns null if no purpose is set (permissive — all content allowed).
     */
    getTopicPurpose(topicId: number): string | null;
    /**
     * Get all topic purposes (runtime + config merged).
     */
    getAllTopicPurposes(): Record<number, string>;
    /**
     * Validate outbound content against topic purpose.
     * Returns the validation result. Callers decide how to handle rejection.
     */
    validateOutboundContent(topicId: number, text: string, options?: {
        bypass?: boolean;
    }): {
        allowed: boolean;
        reason: string | null;
        detectedCategory: string | null;
        topicPurpose: string | null;
        suggestion: string | null;
    };
    /**
     * Classify content using the configured categories.
     * Useful for debugging and API endpoints.
     */
    classifyContent(text: string): {
        category: string | null;
        confidence: string;
        matchedKeywords: string[];
    };
    /**
     * Get all topic-session mappings (for admin/debug UIs).
     */
    getAllTopicMappings(): Array<{
        topicId: number;
        sessionName: string;
        topicName: string | null;
    }>;
    /**
     * Track that a message was injected into a session.
     * Used by stall detection to alert if no response comes back.
     */
    trackMessageInjection(topicId: number, sessionName: string, messageText: string): void;
    private clearStallForTopic;
    /**
     * Public interface for external callers (e.g., StallTriageNurse) to clear
     * stall tracking for a topic after successful recovery.
     */
    clearStallTracking(topicId: number): void;
    /** Clear promise tracking for a topic (e.g., after successful recovery) */
    clearPromiseTracking(topicId: number): void;
    /** Detect "work-in-progress" messages that imply the agent will follow up */
    private isPromiseMessage;
    /** Detect messages that indicate the agent delivered on its promise */
    private isFollowThroughMessage;
    /**
     * LLM gate for fallback stall/promise alerts.
     *
     * Before sending a user-facing alert about a stall or expired promise,
     * check with the intelligence provider whether the alert is warranted.
     * This prevents false positives when the StallTriageNurse is unavailable.
     *
     * Returns true if the alert should be sent, false to suppress.
     * If no intelligence provider is available, returns true (fail-open for safety).
     */
    private confirmStallAlert;
    /** Get all active topic-session mappings (used by SessionMonitor) */
    getActiveTopicSessions(): Map<number, string>;
    /** Get recent message log entries for analysis */
    getMessageLog(limit?: number): Array<{
        topicId: number;
        text: string;
        fromUser: boolean;
        timestamp: string;
    }>;
    private checkForStalls;
    /**
     * Handle stall events from the shared StallDetector (Phase 1c).
     * Bridges shared events back to Telegram-specific alert logic
     * (triage nurse, quota classification, LLM gate, user notifications).
     */
    private handleSharedStallEvent;
    getStatus(): {
        started: boolean;
        uptime: number | null;
        pendingStalls: number;
        pendingPromises: number;
        topicMappings: number;
        lastError: string | null;
        consecutivePollErrors: number;
        fatalReason: '401' | 'network' | 'no-usable-bot-token' | null;
        stoppedAt: string | null;
    };
    /**
     * Download a file from Telegram by file_id.
     */
    private downloadFile;
    /**
     * Resolve voice transcription provider from config or environment.
     * Checks explicit config, then env vars, then auto-detects.
     */
    private resolveTranscriptionProvider;
    /**
     * Transcribe a voice message using the configured provider.
     */
    private transcribeVoice;
    /**
     * Download a photo from Telegram and save it locally.
     * Returns the local file path.
     */
    private downloadPhoto;
    /**
     * Download a document from Telegram and save it locally.
     * Preserves the original filename when available.
     * Returns the local file path.
     */
    private downloadDocument;
    /**
     * Process Telegram commands. Returns true if the message was a command.
     */
    handleCommand(text: string, topicId: number, userId: number): Promise<boolean>;
    /**
     * Search the message log with flexible filters.
     * Supports text query, topicId filter, date range, and pagination.
     */
    searchLog(opts?: {
        query?: string;
        topicId?: number;
        since?: Date;
        limit?: number;
    }): LogEntry[];
    /**
     * Get message log statistics.
     */
    getLogStats(): {
        totalMessages: number;
        logSizeBytes: number;
        logPath: string;
    };
    /**
     * Get recent messages for a topic (for thread history on respawn, the live-tail
     * streamer, and the handoff hash).
     *
     * Served from the in-memory tail cache after a one-time lazy seed from the
     * JSONL — the file is NOT re-read per call. (Pre-fix, the live-tail streamer
     * called this for every known topic every 5s, each call synchronously reading
     * the full multi-MB log: the measured cause of the 2026-06-05 event-loop
     * stalls.) Requests beyond the cache window fall through to a direct file scan.
     */
    getTopicHistory(topicId: number, limit?: number): LogEntry[];
    /**
     * Single-pass cache seed: parse the JSONL once and build the recent tail for
     * every topic with a registered session (the set the live-tail streamer
     * enumerates). Topics with no entries get an empty array so they never
     * trigger a per-topic file scan of their own.
     */
    private seedTailCacheFromLog;
    /** Full JSONL scan for a topic's most recent entries (the cache seed / fallback path). */
    private scanLogForTopic;
    /**
     * Cheap, monotonic per-topic change signal for the live-tail streamer: bumped
     * on every message logged for the topic. Equal versions ⇒ identical tail
     * content, so the streamer can skip serializing unchanged topics.
     */
    getTopicContentVersion(topicId: number): number;
    private appendToLog;
    /** Keep only the last 75,000 lines when log exceeds 100,000 lines.
     *  High limits because message history is core agent memory.
     *  At ~200 bytes/line average, 100k lines ~ 20MB — fine for a dedicated machine. */
    private maybeRotateLog;
    /**
     * Create an attention item and its Telegram topic.
     */
    createAttentionItem(item: Omit<AttentionItem, 'createdAt' | 'updatedAt' | 'status' | 'topicId'>): Promise<AttentionItem>;
    /**
     * Build a calm, named, actionable self-health notice. Resolves a session's
     * topic id to its HUMAN topic name (never emits a bare `topic-<n>`), and ends
     * with a plain-language next step the user can just reply to. Used by the
     * self-health escalators so every Agent-Health notice reads like
     * "Heads-up on the 'EXO 3.0' session — … reply 'check EXO 3.0' …".
     */
    buildHealthNotice(opts: {
        sessionName: string;
        topicId?: number;
        what: string;
        nextStep: string;
    }): {
        title: string;
        summary: string;
    };
    /** Derive the suppression-dedup entity key for an Agent-Health-lane item. */
    private healthKeyFor;
    /**
     * Route a self-health notice into the ONE calm "🩺 Agent Health" lane topic.
     * Per-entity suppression dedup: a same-key re-escalation within the dedup
     * window is logged + suppressed (not reposted) so the lane stays quiet. The
     * lane topic is created lazily once and reused. Returns the lane topic id, or
     * null if Telegram is unavailable (the item is still recorded in the store).
     */
    private routeToAgentHealthLane;
    /** Lazily create (once) and return the reused "🩺 Agent Health" lane topic id. */
    private ensureAgentHealthLaneTopic;
    /**
     * Append a suppressed attention item to the audit trail. This is the
     * "housekeeping goes to the logs" path for the topic-flood guard — the item
     * is preserved, just not given its own forum topic. Size-capped with a single
     * rotation (the flood is exactly the failure mode that grows this fastest).
     */
    private writeSuppressedAttentionLog;
    /**
     * Route a coalesced attention item into ONE reused notice topic for its bucket
     * (the source key, or the shared global bucket when the global cap tripped).
     * The topic is created lazily once per bucket and reused thereafter (so a
     * flapping source does not churn a new topic per episode). Concurrent coalesced
     * items for one bucket share a single in-flight creation (no double-create
     * race). Returns the notice topicId, or null if Telegram is unavailable (the
     * item is still recorded + logged).
     */
    private routeToFloodNotice;
    /** Lazily create (once) and return the reused notice topic id for a bucket. */
    private ensureFloodNoticeTopic;
    /**
     * Update attention item status. Called by /ack, /done, /wontdo, /reopen commands.
     */
    updateAttentionStatus(itemId: string, status: AttentionItem['status']): Promise<boolean>;
    /**
     * Get all attention items, optionally filtered by status.
     */
    getAttentionItems(status?: string): AttentionItem[];
    /**
     * Get a specific attention item.
     */
    getAttentionItem(itemId: string): AttentionItem | undefined;
    /**
     * Check if a topic is an attention topic.
     */
    isAttentionTopic(topicId: number): boolean;
    /**
     * Handle commands in attention topics (/ack, /done, /wontdo, /reopen).
     * Returns true if handled, false if not an attention command.
     */
    handleAttentionCommand(topicId: number, text: string): Promise<boolean>;
    private loadAttentionItems;
    private saveAttentionItems;
    private escapeHtml;
    private loadRegistry;
    private saveRegistry;
    private loadOffset;
    private saveOffset;
    private poll;
    /**
     * Process a single Telegram update (text, voice, photo, or callback query).
     */
    private processUpdate;
    /**
     * Handle an incoming voice message: download, transcribe, route as text.
     */
    private handleVoiceMessage;
    /**
     * Handle an incoming photo message: download, save, route with path.
     */
    private handlePhotoMessage;
    /**
     * Handle an incoming document message: download, save, route with path.
     */
    private handleDocumentMessage;
    /**
     * Relay a detected prompt to a Telegram topic with inline keyboard buttons.
     * For prompts with options: sends buttons. For questions: sends text asking for reply.
     * Returns the Telegram message ID of the relay message.
     */
    relayPrompt(topicId: number, prompt: DetectedPrompt): Promise<number>;
    /**
     * Format a detected prompt into a Telegram HTML message body.
     */
    private formatPromptMessage;
    /**
     * Handle a forwarded callback query from the Lifeline process.
     * In send-only mode the server doesn't poll for callbacks, so the
     * Lifeline forwards them via /internal/telegram-callback.
     */
    handleForwardedCallback(query: any): Promise<void>;
    /**
     * Handle a Telegram callback query from an inline keyboard button press.
     */
    private processCallbackQuery;
    /**
     * Handle a text reply to a Prompt Gate relay message (for text-input prompts).
     * Returns true if the message was intercepted, false to fall through to normal routing.
     */
    private handlePendingPromptReply;
    /**
     * Edit a Telegram message with retry on failure.
     * Uses exponential backoff (1s, 2s, 4s) for up to 3 attempts.
     */
    private editMessageWithRetry;
    /**
     * Clean up Prompt Gate state for a session (call when session ends).
     */
    cleanupPromptGate(sessionName: string): void;
    /**
     * Proactively prune expired relay prompts and send timeout messages.
     * Call periodically (e.g. every 60s) to handle cases where no new message
     * arrives to trigger the expiry check in handlePendingPromptReply.
     */
    pruneExpiredRelays(): Promise<void>;
    /**
     * Stop the callback registry (call on adapter shutdown).
     */
    stopPromptGate(): void;
    private getUpdates;
    private apiCall;
}
/**
 * Shared formatter wire-up used by both TelegramAdapter.apiCall and
 * TelegramLifeline.apiCall. Pure function for testability.
 *
 * For non-send methods, or when mode is undefined / `'legacy-passthrough'`,
 * returns params unchanged. For `sendMessage` / `editMessageText` in a
 * formatting mode, runs the formatter on `params.text` and overrides
 * `params.parse_mode` with the formatter's output.
 *
 * Callers may opt out per-call by passing `params._isPlainRetry = true`
 * (internal flag for the 400 retry path).
 */
export declare function applyTelegramFormatter(method: string, params: Record<string, unknown>, configMode: FormatMode | undefined): {
    outgoingParams: Record<string, unknown>;
    originalParams: Record<string, unknown>;
    didFormat: boolean;
    isPlainRetry: boolean;
};
export {};
//# sourceMappingURL=TelegramAdapter.d.ts.map