/** * TelegramProgress is a ProgressPort that posts run progress to Telegram as * cleanly rendered HTML. It handles two kinds of event differently: * * - DISCRETE lifecycle events (stage-started, committed, verdict, done, ...) * are posted as their own fresh message — the durable run narrative. * * - HEARTBEAT ticks drive a single LIVE BUBBLE: the first tick of a step * sends one message, and subsequent ticks EDIT that same message in place * so the user sees an elapsed timer advance without the chat filling up * with a new line every few seconds. Telegram throttles edits, so ticks * inside a throttle window (default 15s) are dropped rather than sent. * * A discrete event CLOSES the active bubble: the next heartbeat opens a new one. * That keeps the live timer attached to the current activity and prevents a * stale bubble from ticking after the step it described has moved on. * * Read-only push notifications by design: approval stays on stdin (interactive * runs) or off entirely (unattended runs use gate: none, with the verifier as * the automated checkpoint), so there is no inbound Telegram channel to build. * All sends are best-effort by virtue of the fan-out sink swallowing errors; * this adapter focuses on WHAT to render. */ import type { ProgressEvent, ProgressPort } from "../ports/progress.ts"; import type { TelegramClient } from "../ports/telegram-client.ts"; export interface TelegramProgressDeps { /** Minimum ms between heartbeat-bubble edits. Default 15000. */ throttleMs?: number; /** Clock, injectable for tests. Default Date.now. */ now?: () => number; } export declare class TelegramProgress implements ProgressPort { private readonly client; private readonly throttleMs; private readonly now; /** The live heartbeat bubble's message id, while one is open. */ private bubbleId; /** When the bubble was last sent/edited, to enforce the throttle window. */ private lastEditAt; constructor(client: TelegramClient, deps?: TelegramProgressDeps); emit(event: ProgressEvent): Promise; private emitHeartbeat; private closeBubble; }