/** * Telegram command routing helpers * Zones: telegram controls, pi agent commands, queue controls * Owns Telegram slash-command normalization, bot command metadata, and pi-side command registration behind runtime ports */ import { pairTelegramUserIfNeeded, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./config.ts"; import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts"; import { escapeHtml } from "./rendering.ts"; import type { TelegramBridgeStatusLineOptions } from "./status.ts"; import { createTelegramControlItemBuilder, createTelegramControlQueueController, createTelegramQueueAdmissionReceipt, type PendingTelegramControlItem, type TelegramQueueAdmissionReceipt, } from "./queue.ts"; export interface ParsedTelegramCommand { name: string; args: string; } export interface TelegramBotCommandDefinition { command: string; description: string; } export interface TelegramPromptTemplateMenuCommand { command: string; description?: string; } const TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY = "__piTelegramCommandRegistry__"; const TELEGRAM_BOT_COMMAND_NAME_PATTERN = /^[a-z0-9_]{1,32}$/; export interface TelegramExtensionCommandContext { name: string; args: string; reply: (text: string) => Promise; enqueuePrompt: (prompt: string) => Promise; } export interface TelegramExtensionCommandRegistration { name: string; description?: string; order?: number; showInMenu?: boolean; emoji?: string; handler: (ctx: TelegramExtensionCommandContext) => Promise | void; } interface RegisteredTelegramExtensionCommand { name: string; description?: string; order: number; showInMenu: boolean; emoji?: string; handler: TelegramExtensionCommandRegistration["handler"]; } interface TelegramExtensionCommandRegistry { commands: Map; } function getOrCreateTelegramCommandRegistry(): TelegramExtensionCommandRegistry { const existing = (globalThis as Record)[ TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY ]; if ( existing && typeof existing === "object" && existing !== null && "commands" in existing && existing.commands instanceof Map ) { return existing as TelegramExtensionCommandRegistry; } const registry: TelegramExtensionCommandRegistry = { commands: new Map() }; (globalThis as Record)[ TELEGRAM_EXTENSION_COMMAND_REGISTRY_KEY ] = registry; return registry; } export function normalizeTelegramExtensionCommandName(name: string): string { return name.trim().replace(/^\/+/, "").toLowerCase(); } export function isTelegramExtensionCommandName(name: string): boolean { return TELEGRAM_BOT_COMMAND_NAME_PATTERN.test(name); } function normalizeTelegramExtensionCommandEmoji( emoji: string | undefined, ): string | undefined { const normalized = emoji?.trim(); return normalized ? normalized : undefined; } export function registerTelegramCommand( registration: TelegramExtensionCommandRegistration, ): () => void { const name = normalizeTelegramExtensionCommandName(registration.name); const showInMenu = registration.showInMenu ?? false; const emoji = normalizeTelegramExtensionCommandEmoji(registration.emoji); if (!isTelegramExtensionCommandName(name)) { throw new Error(`Invalid Telegram command name: ${registration.name}`); } if (showInMenu && !emoji) { throw new Error(`Visible Telegram command requires emoji: ${name}`); } if (emoji && emoji.length > 8) { throw new Error(`Telegram command emoji is too long: ${name}`); } if (isTelegramReservedCommandName(name)) { throw new Error( `Telegram command conflicts with built-in command: ${name}`, ); } const registry = getOrCreateTelegramCommandRegistry(); if (registry.commands.has(name)) { throw new Error(`Telegram command is already registered: ${name}`); } const command: RegisteredTelegramExtensionCommand = { name, description: registration.description, order: registration.order ?? 0, showInMenu, emoji, handler: registration.handler, }; registry.commands.set(name, command); return () => { if (registry.commands.get(name) === command) registry.commands.delete(name); }; } export function getTelegramExtensionCommands(): RegisteredTelegramExtensionCommand[] { return Array.from( getOrCreateTelegramCommandRegistry().commands.values(), ).sort((a, b) => a.order - b.order || a.name.localeCompare(b.name)); } export function findTelegramExtensionCommand( name: string | undefined, ): RegisteredTelegramExtensionCommand | undefined { if (!name) return undefined; return getOrCreateTelegramCommandRegistry().commands.get( normalizeTelegramExtensionCommandName(name), ); } export function clearTelegramExtensionCommands(): void { getOrCreateTelegramCommandRegistry().commands.clear(); } export const TELEGRAM_COMMAND_EMOJI = { start: "๐ŸŸข", status: "๐Ÿ“Š", model: "๐Ÿค–", thinking: "๐Ÿง ", compact: "๐Ÿ—œ", queue: "๐Ÿ”ข", thread: "๐Ÿงต", next: "โฉ", continue: "โ–ถ๏ธ", abort: "โน๏ธ", stop: "๐ŸŸฅ", } as const; export type TelegramCommandEmojiName = keyof typeof TELEGRAM_COMMAND_EMOJI; export function getTelegramCommandEmoji( command: TelegramCommandEmojiName, ): string { return TELEGRAM_COMMAND_EMOJI[command]; } export function formatTelegramCommandEmojiPrefix( command: TelegramCommandEmojiName, ): string { return `${getTelegramCommandEmoji(command)} `; } export function formatTelegramInformationHeading( emoji: string, text: string, ): string { return `${escapeHtml(emoji)} ${escapeHtml(text)}`; } export const TELEGRAM_COMPACTION_STARTED_TEXT = formatTelegramInformationHeading( getTelegramCommandEmoji("compact"), "Compaction started.", ); export const TELEGRAM_COMPACTION_COMPLETED_TEXT = formatTelegramInformationHeading("โœ…", "Compaction completed."); export const TELEGRAM_COMPACTION_STARTED_MARKDOWN = `**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`; export const TELEGRAM_COMPACTION_COMPLETED_MARKDOWN = "**โœ… Compaction completed.**"; function formatTelegramBotCommandDescription( command: TelegramCommandEmojiName, description: string, ): string { return `${formatTelegramCommandEmojiPrefix(command)}${description}`; } export const TELEGRAM_BUILTIN_BOT_COMMANDS: readonly TelegramBotCommandDefinition[] = [ { command: "start", description: formatTelegramBotCommandDescription( "start", "Open menu / Pair bridge", ), }, { command: "compact", description: formatTelegramBotCommandDescription( "compact", "Compact current session", ), }, { command: "next", description: formatTelegramBotCommandDescription( "next", "Force next turn", ), }, { command: "continue", description: formatTelegramBotCommandDescription( "continue", "Queue continue prompt", ), }, { command: "abort", description: formatTelegramBotCommandDescription("abort", "Abort Pi"), }, { command: "stop", description: formatTelegramBotCommandDescription( "stop", "Abort Pi & Clear queue", ), }, ]; export const TELEGRAM_BOT_COMMANDS = TELEGRAM_BUILTIN_BOT_COMMANDS; function getVisibleTelegramExtensionBotCommands(): TelegramBotCommandDefinition[] { return getTelegramExtensionCommands() .filter((command) => command.showInMenu && command.description) .map((command) => ({ command: command.name, description: `${command.emoji} ${command.description ?? command.name}`, })); } export function getTelegramReservedCommandNames(): string[] { return [ ...TELEGRAM_RESERVED_COMMAND_NAMES, ...getTelegramExtensionCommands().map((command) => command.name), ]; } export interface TelegramBotCommandRegistrationDeps { setMyCommands: ( commands: readonly TelegramBotCommandDefinition[], ) => Promise; } export async function registerTelegramBotCommands( deps: TelegramBotCommandRegistrationDeps, ): Promise { const extensionCommands = getVisibleTelegramExtensionBotCommands(); if (extensionCommands.length === 0) { await deps.setMyCommands(TELEGRAM_BOT_COMMANDS); return; } const compactCommandIndex = TELEGRAM_BOT_COMMANDS.findIndex( (command) => command.command === "compact", ); if (compactCommandIndex === -1) { await deps.setMyCommands([...TELEGRAM_BOT_COMMANDS, ...extensionCommands]); return; } await deps.setMyCommands([ ...TELEGRAM_BOT_COMMANDS.slice(0, compactCommandIndex + 1), ...extensionCommands, ...TELEGRAM_BOT_COMMANDS.slice(compactCommandIndex + 1), ]); } export function createTelegramBotCommandRegistrar( deps: TelegramBotCommandRegistrationDeps, ): () => Promise { let pending: Promise | undefined; return () => { if (pending) return pending; let request: Promise; request = registerTelegramBotCommands(deps).finally(() => { if (pending === request) pending = undefined; }); pending = request; return request; }; } export interface TelegramBridgeCommandStartPollingOptions { force?: boolean; forceFreshLeaderThread?: boolean; } export interface TelegramBridgeCommandStartPollingResult { ok: boolean; message?: string; canTakeover?: boolean; owner?: string; } export type TelegramPollingStartRecoveryResult = | { kind: "unhandled" } | { kind: "retry"; message: string } | { kind: "blocked"; message: string }; export interface TelegramBridgeCommandRegistrationDeps { promptForConfig: (ctx: ExtensionCommandContext, profileName?: string) => Promise; getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[]; reloadConfig: () => Promise; hasBotToken: () => boolean; startPolling: ( ctx: ExtensionCommandContext, options?: TelegramBridgeCommandStartPollingOptions, ) => | void | Promise | TelegramBridgeCommandStartPollingResult; stopPolling: () => Promise; recoverPollingStart?: ( error: unknown, ) => Promise; getDisconnectThreadName?: () => string | undefined; queueAgentConnectionContext?: (connected: boolean) => void; updateStatus: (ctx: ExtensionCommandContext) => void; getProfileNames?: () => string[]; activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise; activateProfileConfig?: ( ctx: ExtensionCommandContext, profileName: string, ) => Promise; } function parseTelegramProfileArg(args: string): string | undefined { const word = args.trim().split(/\s+/)[0]; if (!word || word.length === 0) return undefined; if (word.startsWith("-")) return undefined; return word === TELEGRAM_DEFAULT_PROFILE_NAME ? undefined : word; } function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string { return ctx.ui.theme.fg("accent", "pi-telegram"); } function formatTelegramTakeoverPrompt( ctx: ExtensionCommandContext, owner?: string, ): string { const theme = ctx.ui.theme; const action = theme.fg("warning", "move singleton lock here?"); const from = theme.fg("muted", "from:"); const to = theme.fg("muted", "to:"); const source = owner ?? "another Pi instance"; return `${action}\n\n${from} ${source}\n${to} ${ctx.cwd}`; } export function registerTelegramBridgeCommands( pi: ExtensionAPI, deps: TelegramBridgeCommandRegistrationDeps, ): void { pi.registerCommand("telegram-setup", { description: "Configure Telegram bot token. Use /telegram-setup for named profiles.", handler: async (args, ctx) => { await deps.promptForConfig(ctx, parseTelegramProfileArg(args)); }, }); pi.registerCommand("telegram-status", { description: "Show Telegram bridge status", handler: async (args, ctx) => { const verbose = /(^|\s)(--debug|debug|--verbose|verbose)(\s|$)/i.test( args, ); ctx.ui.notify(deps.getStatusLines({ verbose }).join("\n"), "info"); }, }); pi.registerCommand("telegram-connect", { description: "Start the Telegram bridge. Use /telegram-connect for named profiles.", handler: async (args, ctx) => { const profileName = parseTelegramProfileArg(args); if (profileName && deps.activateProfileConfig) { const ok = await deps.activateProfileConfig(ctx, profileName); if (!ok) { ctx.ui.notify(`Profile "${profileName}" not found.`, "error"); deps.updateStatus(ctx); return; } ctx.ui.notify(`Activated profile "${profileName}".`, "info"); } else { await (deps.activateDefaultProfileConfig?.(ctx) ?? deps.reloadConfig()); } if (!deps.hasBotToken()) { const profileNames = deps.getProfileNames?.() ?? []; if (!profileName && profileNames.length > 0) { ctx.ui.notify( `No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect or /telegram-setup to create a default profile.`, "info", ); deps.updateStatus(ctx); return; } await deps.promptForConfig(ctx, profileName); return; } let recoveryUsed = false; const startWithRecovery = async ( options: TelegramBridgeCommandStartPollingOptions, ): Promise => { try { return await deps.startPolling(ctx, options); } catch (error) { if (!deps.recoverPollingStart || recoveryUsed) throw error; const recovery = await deps.recoverPollingStart(error); if (recovery.kind === "unhandled") throw error; if (recovery.kind === "blocked") { return { ok: false, message: recovery.message }; } recoveryUsed = true; try { const retry = await deps.startPolling(ctx, options); if (!retry) { return { ok: true, message: recovery.message }; } return { ...retry, message: retry.ok ? `${recovery.message} ${retry.message ?? "Telegram bridge connected."}` : retry.message, }; } catch { return { ok: false, message: "Telegram temporary state was recovered, but the bridge could not restart. Restart this Pi instance and run /telegram-connect again.", }; } } }; let result = await startWithRecovery({ forceFreshLeaderThread: true, }); if (result && !result.ok && result.canTakeover) { const confirmed = await ctx.ui.confirm( formatTelegramTakeoverTitle(ctx), formatTelegramTakeoverPrompt(ctx, result.owner), ); if (!confirmed) { ctx.ui.notify("Telegram bridge takeover cancelled.", "info"); deps.updateStatus(ctx); return; } result = await startWithRecovery({ force: true, forceFreshLeaderThread: true, }); } if (result?.message) { ctx.ui.notify(result.message, result.ok ? "info" : "warning"); } if (!result || result.ok) { deps.queueAgentConnectionContext?.(true); } deps.updateStatus(ctx); }, }); pi.registerCommand("telegram-disconnect", { description: "Stop Telegram; in Threaded Mode, delete this instance's current thread", handler: async (_args, ctx) => { const threadName = deps.getDisconnectThreadName?.(); if (threadName) { const confirmed = await ctx.ui.confirm( ctx.ui.theme.fg("accent", "pi-telegram"), `Delete Telegram thread ${ctx.ui.theme.fg("warning", threadName)} and disconnect this Pi session?`, ); if (!confirmed) { ctx.ui.notify("Telegram disconnect cancelled.", "info"); deps.updateStatus(ctx); return; } } try { const message = await deps.stopPolling(); if (message) ctx.ui.notify(message, "info"); deps.queueAgentConnectionContext?.(false); } catch (error) { const detail = error instanceof Error ? error.message : String(error); ctx.ui.notify( `Telegram disconnect did not complete: ${detail} Keep this Pi session open, restore leader connectivity, inspect /telegram-status --debug, and retry /telegram-disconnect.`, "warning", ); throw error; } finally { deps.updateStatus(ctx); } }, }); } export const TELEGRAM_RESERVED_COMMAND_NAMES = [ "stop", "abort", "next", "continue", "status", "queue", "compact", "model", "thinking", "settings", "help", "start", ] as const; export type TelegramReservedCommandName = (typeof TELEGRAM_RESERVED_COMMAND_NAMES)[number]; const TELEGRAM_RESERVED_COMMAND_NAME_SET = new Set( TELEGRAM_RESERVED_COMMAND_NAMES, ); export function isTelegramReservedCommandName( commandName: string | undefined, ): commandName is TelegramReservedCommandName { return ( commandName !== undefined && TELEGRAM_RESERVED_COMMAND_NAME_SET.has(commandName) ); } export type TelegramCommandAction = | { kind: "ignore"; executionMode: "ignored" } | { kind: "stop"; executionMode: "immediate" } | { kind: "abort"; executionMode: "immediate" } | { kind: "next"; executionMode: "immediate" } | { kind: "continue"; executionMode: "immediate" } | { kind: "queue"; executionMode: "immediate" } | { kind: "compact"; executionMode: "immediate" } | { kind: "status"; executionMode: "immediate" } | { kind: "model"; executionMode: "immediate" } | { kind: "thinking"; executionMode: "immediate" } | { kind: "settings"; executionMode: "immediate" } | { kind: "help"; commandName: "help" | "start"; executionMode: "immediate"; }; export type TelegramCommandExecutionMode = "ignored" | "immediate"; export interface TelegramCommandActionDeps { handleStop: (message: TMessage, ctx: TContext) => Promise; handleAbort: (message: TMessage, ctx: TContext) => Promise; handleNext: (message: TMessage, ctx: TContext) => Promise; handleContinue: (message: TMessage, ctx: TContext) => Promise; handleQueue: (message: TMessage, ctx: TContext) => Promise; handleCompact: (message: TMessage, ctx: TContext) => Promise; handleStatus: (message: TMessage, ctx: TContext) => Promise; handleModel: (message: TMessage, ctx: TContext) => Promise; handleThinking: (message: TMessage, ctx: TContext) => Promise; handleSettings?: (message: TMessage, ctx: TContext) => Promise; handleHelp: ( message: TMessage, commandName: "help" | "start", ctx: TContext, ) => Promise; } export interface TelegramStopCommandDeps { hasAbortHandler: () => boolean; clearPendingModelSwitch: () => void; clearQueuedTelegramItems: () => number; setFoldQueuedPromptsIntoHistory: (fold: boolean) => void; abortCurrentTurn: () => void; updateStatus: () => void; sendTextReply: ( text: string, options?: { parseMode?: "HTML" }, ) => Promise; } export interface TelegramRuntimeEventRecorderPort { recordRuntimeEvent?: ( category: string, error: unknown, details?: Record, ) => void; } export interface TelegramCompactConfirmationReplyMarkup { inline_keyboard: { text: string; callback_data: string }[][]; } export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorderPort { isIdle: () => boolean; hasPendingMessages: () => boolean; hasActiveTelegramTurn: () => boolean; hasDispatchPending: () => boolean; hasQueuedTelegramItems: () => boolean; isCompactionInProgress: () => boolean; setCompactionInProgress: (inProgress: boolean) => void; updateStatus: () => void; dispatchNextQueuedTelegramTurn: () => void; requestDeferredDispatchNextQueuedTelegramTurn?: ( dispatch: () => void, ) => void; startTypingLoop?: () => void; stopTypingLoop?: () => void; compact: (callbacks: { onComplete: () => void; onError: (error: unknown) => void; }) => void; sendTextReply: ( text: string, options?: { parseMode?: "HTML" }, ) => Promise; suppressStartNotice?: boolean; } export interface TelegramCompactConfirmationDeps { sendInteractiveMessage: ( chatId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramCompactConfirmationReplyMarkup, options?: { target?: { chatId: number; threadId?: number } }, ) => Promise; } export interface TelegramCompactConfirmationCallbackQuery { id: string; data?: string; message?: { chat?: { id?: number }; message_id?: number; message_thread_id?: number; }; } export interface TelegramCompactConfirmationCallbackDeps { ctx: TContext; answerCallbackQuery: ( callbackQueryId: string, text?: string, ) => Promise; editInteractiveMessage: ( chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramCompactConfirmationReplyMarkup, ) => Promise; runCompact: ( ctx: TContext, chatId: number, replyToMessageId: number, target?: { chatId: number; threadId?: number }, ) => Promise; } export type TelegramControlCommandType = PendingTelegramControlItem["controlType"]; export interface TelegramCommandRuntimeMessage { chat: { id: number; type?: string; title?: string }; message_id: number; message_thread_id?: number; from?: { id?: number }; pi_telegram_source_update_id?: number; } export interface TelegramCommandMessageTarget { chatId: number; threadId?: number; replyToMessageId: number; } function canPairTelegramUserFromCommandMessage( message: TelegramCommandRuntimeMessage, ): boolean { return message.chat.type === undefined || message.chat.type === "private"; } export interface TelegramCommandTargetRuntimeDeps { enqueueControlItem: ( target: TelegramCommandMessageTarget, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise, admissionReceipts?: TelegramQueueAdmissionReceipt[], onQueued?: (item: PendingTelegramControlItem) => void, ) => void; getAdmissionScope?: () => string | undefined; getAdmissionJournalBinding?: () => string | undefined; onControlQueued?: ( message: TelegramCommandRuntimeMessage, receipt: TelegramQueueAdmissionReceipt, ) => void; showStatus: ( chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, ) => Promise; openModelMenu: ( chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, ) => Promise; openSettingsMenu?: ( chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, ) => Promise; sendTextReply: ( chatId: number, replyToMessageId: number, text: string, options?: { parseMode?: "HTML"; target?: { chatId: number; threadId?: number }; }, ) => Promise; } export interface TelegramCommandTargetRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, > { enqueueControlItem: ( message: TMessage, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise, ) => void; showStatus: (message: TMessage, ctx: TContext) => Promise; openModelMenu: (message: TMessage, ctx: TContext) => Promise; openSettingsMenu: (message: TMessage, ctx: TContext) => Promise; sendTextReply: ( message: TMessage, text: string, options?: { parseMode?: "HTML" }, ) => Promise; } export function getTelegramCommandMessageTarget( message: TelegramCommandRuntimeMessage, ): TelegramCommandMessageTarget { return { chatId: message.chat.id, threadId: typeof message.message_thread_id === "number" ? message.message_thread_id : undefined, replyToMessageId: message.message_id, }; } export interface TelegramCommandControlQueueRuntimeDeps { createControlItem: (options: { chatId: number; target?: { chatId: number; threadId?: number }; replyToMessageId: number; controlType: TelegramControlCommandType; statusSummary: string; admissionReceipts?: TelegramQueueAdmissionReceipt[]; execute: (ctx: TContext) => Promise; }) => PendingTelegramControlItem; appendControlItem: ( item: PendingTelegramControlItem, ctx: TContext, ) => void; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; } export function createTelegramCommandControlQueueRuntime( deps: TelegramCommandControlQueueRuntimeDeps, ): TelegramCommandTargetRuntimeDeps["enqueueControlItem"] { const controlQueueController = createTelegramControlQueueController({ appendControlItem: deps.appendControlItem, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, }); return createTelegramCommandControlEnqueueAdapter({ createControlItem: deps.createControlItem, enqueueControlItem: controlQueueController.enqueue, }); } export function createTelegramCommandControlEnqueueAdapter(deps: { createControlItem: (options: { chatId: number; target?: { chatId: number; threadId?: number }; replyToMessageId: number; controlType: TelegramControlCommandType; statusSummary: string; admissionReceipts?: TelegramQueueAdmissionReceipt[]; execute: (ctx: TContext) => Promise; }) => PendingTelegramControlItem; enqueueControlItem: ( item: PendingTelegramControlItem, ctx: TContext, onQueued?: (item: PendingTelegramControlItem) => void, ) => void; }): TelegramCommandTargetRuntimeDeps["enqueueControlItem"] { return ( target, ctx, controlType, statusSummary, execute, admissionReceipts, onQueued, ) => { deps.enqueueControlItem( deps.createControlItem({ ...target, controlType, statusSummary, ...(admissionReceipts?.length ? { admissionReceipts } : {}), execute, }), ctx, onQueued, ); }; } export type TelegramCommandTargetQueueRuntimeDeps = TelegramCommandControlQueueRuntimeDeps & Omit, "enqueueControlItem">; export function createTelegramCommandTargetQueueRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( deps: TelegramCommandTargetQueueRuntimeDeps, ): TelegramCommandTargetRuntime { return createTelegramCommandTargetRuntime({ enqueueControlItem: createTelegramCommandControlQueueRuntime({ createControlItem: deps.createControlItem, appendControlItem: deps.appendControlItem, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, }), getAdmissionScope: deps.getAdmissionScope, getAdmissionJournalBinding: deps.getAdmissionJournalBinding, onControlQueued: deps.onControlQueued, showStatus: deps.showStatus, openModelMenu: deps.openModelMenu, openSettingsMenu: deps.openSettingsMenu, sendTextReply: deps.sendTextReply, }); } export function createTelegramCommandTargetRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( deps: TelegramCommandTargetRuntimeDeps, ): TelegramCommandTargetRuntime { return { enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => { const sourceUpdateId = message.pi_telegram_source_update_id; const baseReceipt = typeof sourceUpdateId === "number" ? createTelegramQueueAdmissionReceipt({ queueKind: "control", scope: deps.getAdmissionScope?.() ?? "", sourceUpdateIds: [sourceUpdateId], }) : undefined; const journalBindingKey = deps.getAdmissionJournalBinding?.(); const receipt = baseReceipt ? { ...baseReceipt, ...(journalBindingKey ? { journalBindingKey } : {}), } : undefined; deps.enqueueControlItem( getTelegramCommandMessageTarget(message), ctx, controlType, statusSummary, execute, receipt ? [receipt] : undefined, receipt ? () => deps.onControlQueued?.(message, receipt) : undefined, ); }, showStatus: (message, ctx) => { const target = getTelegramCommandMessageTarget(message); return deps.showStatus( target.chatId, target.replyToMessageId, ctx, target.threadId, ); }, openModelMenu: (message, ctx) => { const target = getTelegramCommandMessageTarget(message); return deps.openModelMenu( target.chatId, target.replyToMessageId, ctx, target.threadId, ); }, openSettingsMenu: async (message, ctx) => { const target = getTelegramCommandMessageTarget(message); if (!deps.openSettingsMenu) { await deps.sendTextReply( target.chatId, target.replyToMessageId, formatTelegramInformationHeading( "๐Ÿšซ", "Settings menu is unavailable.", ), { target, parseMode: "HTML" }, ); return; } await deps.openSettingsMenu( target.chatId, target.replyToMessageId, ctx, target.threadId, ); }, sendTextReply: async (message, text, options) => { const target = getTelegramCommandMessageTarget(message); await deps.sendTextReply(target.chatId, target.replyToMessageId, text, { ...options, target, }); }, }; } export interface TelegramCommandOrPromptRuntimeDeps { extractRawText: (messages: TMessage[]) => string; shouldIgnoreMessages?: (messages: TMessage[]) => boolean; handleCommand: ( commandName: string | undefined, message: TMessage, ctx: TContext, ) => Promise; executeExtensionCommand?: ( command: ParsedTelegramCommand, message: TMessage, ctx: TContext, ) => Promise; expandPromptTemplateCommand?: ( commandName: string, args: string, ) => string | undefined; replaceMessageText: (message: TMessage, text: string) => TMessage; enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise; assertExecutionCurrent?: (message: TMessage) => void; } export interface TelegramCommandRuntimeDeps< TMessage extends TelegramCommandRuntimeMessage, TContext, > extends TelegramRuntimeEventRecorderPort { hasAbortHandler: () => boolean; clearPendingModelSwitch: () => void; hasQueuedTelegramItems: () => boolean; clearQueuedTelegramItems: (ctx: TContext) => number; setFoldQueuedPromptsIntoHistory: (fold: boolean) => void; abortCurrentTurn: () => void; isIdle: (ctx: TContext) => boolean; hasPendingMessages: (ctx: TContext) => boolean; hasActiveTelegramTurn: () => boolean; hasDispatchPending: () => boolean; isCompactionInProgress: () => boolean; setCompactionInProgress: (inProgress: boolean) => void; updateStatus: (ctx: TContext) => void; isContextActive?: (ctx: TContext) => boolean; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; requestDeferredDispatchNextQueuedTelegramTurn?: ( dispatch: (ctx: TContext) => void, ) => void; startTypingLoop?: ( ctx: TContext, chatId?: number, options?: { target?: { chatId: number; threadId?: number } }, ) => void; stopTypingLoop?: () => void; enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise; compact: ( ctx: TContext, callbacks: { onComplete: () => void; onError: (error: unknown) => void }, ) => void; enqueueControlItem: ( message: TMessage, ctx: TContext, controlType: TelegramControlCommandType, statusSummary: string, execute: (ctx: TContext) => Promise, ) => void; showStatus: (message: TMessage, ctx: TContext) => Promise; handleForumBootstrap?: ( message: TMessage, ctx: TContext, ) => Promise; openModelMenu: (message: TMessage, ctx: TContext) => Promise; openThinkingMenu: (message: TMessage, ctx: TContext) => Promise; openQueueMenu: (message: TMessage, ctx: TContext) => Promise; openSettingsMenu?: (message: TMessage, ctx: TContext) => Promise; getAllowedUserId: () => number | undefined; setAllowedUserId: (userId: number) => void; registerBotCommands: () => Promise; getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[]; persistConfig: () => Promise; sendTextReply: ( message: TMessage, text: string, options?: { parseMode?: "HTML" }, ) => Promise; sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"]; assertExecutionCurrent?: (message: TMessage) => void; } export const TELEGRAM_APP_MENU_INTRO_HTML = [ "Pi Telegram", "", `${formatTelegramCommandEmojiPrefix("start")}/start โ€” Open menu / Pair bridge`, `${formatTelegramCommandEmojiPrefix("compact")}/compact โ€” Compact current session`, `${formatTelegramCommandEmojiPrefix("next")}/next โ€” Force next turn`, `${formatTelegramCommandEmojiPrefix("continue")}/continue โ€” Queue continue prompt`, `${formatTelegramCommandEmojiPrefix("abort")}/abort โ€” Abort Pi`, `${formatTelegramCommandEmojiPrefix("stop")}/stop โ€” Abort Pi & Clear queue`, ].join("\n"); function escapeTelegramCommandMenuHtml(text: string): string { return text .replace(/&/g, "&") .replace(//g, ">"); } function buildTelegramPromptTemplateMenuHtml( promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [], ): string { if (promptTemplates.length === 0) return ""; return promptTemplates .map((template) => `๐Ÿงฉ /${escapeTelegramCommandMenuHtml(template.command)}`) .join("\n"); } function buildTelegramExtensionCommandMenuLines(): string[] { return getTelegramExtensionCommands() .filter((command) => command.showInMenu) .map((command) => { const prefix = `${escapeTelegramCommandMenuHtml(command.emoji ?? "")} /${escapeTelegramCommandMenuHtml(command.name)}`; if (!command.description) return prefix; return `${prefix} โ€” ${escapeTelegramCommandMenuHtml(command.description)}`; }); } function buildTelegramAppMenuIntroHtml(): string { const extensionLines = buildTelegramExtensionCommandMenuLines(); if (extensionLines.length === 0) return TELEGRAM_APP_MENU_INTRO_HTML; return [ "Pi Telegram", "", `${formatTelegramCommandEmojiPrefix("start")}/start โ€” Open menu / Pair bridge`, `${formatTelegramCommandEmojiPrefix("compact")}/compact โ€” Compact current session`, ...extensionLines, `${formatTelegramCommandEmojiPrefix("next")}/next โ€” Force next turn`, `${formatTelegramCommandEmojiPrefix("continue")}/continue โ€” Queue continue prompt`, `${formatTelegramCommandEmojiPrefix("abort")}/abort โ€” Abort Pi`, `${formatTelegramCommandEmojiPrefix("stop")}/stop โ€” Abort Pi & Clear queue`, ].join("\n"); } export function buildTelegramAppMenuHtml( statusHtml: string, promptTemplates: readonly TelegramPromptTemplateMenuCommand[] = [], ): string { const introHtml = buildTelegramAppMenuIntroHtml(); const promptTemplateHtml = buildTelegramPromptTemplateMenuHtml(promptTemplates); if (!promptTemplateHtml) return `${introHtml}\n\n${statusHtml}`; return `${introHtml}\n\n${promptTemplateHtml}\n\n${statusHtml}`; } export function createTelegramAppMenuHtmlBuilder(deps: { buildStatusHtml: (ctx: TContext) => string; getPromptTemplateCommands?: () => readonly TelegramPromptTemplateMenuCommand[]; }): (ctx: TContext) => string { return (ctx) => { return buildTelegramAppMenuHtml( deps.buildStatusHtml(ctx), deps.getPromptTemplateCommands?.(), ); }; } function getTelegramCommandErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } function formatTelegramCompactionFailure(error: unknown): string { let message = getTelegramCommandErrorMessage(error).trim(); const redundantPrefixes = [ "Compaction failed: ", "Turn prefix summarization failed: ", ]; let stripped = true; while (stripped) { stripped = false; for (const prefix of redundantPrefixes) { if (!message.startsWith(prefix)) continue; message = message.slice(prefix.length).trim(); stripped = true; } } const sentence = /[.!?]$/u.test(message) ? message : `${message}.`; return `Compaction failed! ${sentence}`; } export function parseTelegramCommand( text: string, ): ParsedTelegramCommand | undefined { const trimmed = text.trim(); if (!trimmed.startsWith("/")) return undefined; const [head, ...tail] = trimmed.split(/\s+/); const name = head.slice(1).split("@")[0]?.toLowerCase(); if (!name) return undefined; return { name, args: tail.join(" ").trim() }; } export const TELEGRAM_COMMAND_ACTIONS = { stop: { kind: "stop", executionMode: "immediate" }, abort: { kind: "abort", executionMode: "immediate" }, next: { kind: "next", executionMode: "immediate" }, continue: { kind: "continue", executionMode: "immediate" }, status: { kind: "status", executionMode: "immediate" }, queue: { kind: "queue", executionMode: "immediate" }, compact: { kind: "compact", executionMode: "immediate" }, model: { kind: "model", executionMode: "immediate" }, thinking: { kind: "thinking", executionMode: "immediate" }, settings: { kind: "settings", executionMode: "immediate" }, help: { kind: "help", commandName: "help", executionMode: "immediate" }, start: { kind: "help", commandName: "start", executionMode: "immediate" }, } as const satisfies Record; export function buildTelegramCommandAction( commandName: string | undefined, ): TelegramCommandAction { if (!isTelegramReservedCommandName(commandName)) { return { kind: "ignore", executionMode: "ignored" }; } return TELEGRAM_COMMAND_ACTIONS[commandName]; } export function getTelegramCommandExecutionMode( action: TelegramCommandAction, ): TelegramCommandExecutionMode { return action.executionMode; } function formatTelegramQueuedTurnCount(count: number): string { return count === 1 ? "1 queued turn" : `${count} queued turns`; } export async function handleTelegramStopCommand( deps: TelegramStopCommandDeps, ): Promise { deps.clearPendingModelSwitch(); const clearedCount = deps.clearQueuedTelegramItems(); deps.setFoldQueuedPromptsIntoHistory(false); if (!deps.hasAbortHandler()) { const clearedSuffix = clearedCount > 0 ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.` : ""; if (clearedCount > 0) deps.updateStatus(); await deps.sendTextReply( formatTelegramInformationHeading("๐Ÿ’ค", `No active turn.${clearedSuffix}`), { parseMode: "HTML" }, ); return; } deps.abortCurrentTurn(); deps.updateStatus(); const clearedSuffix = clearedCount > 0 ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.` : ""; await deps.sendTextReply( formatTelegramInformationHeading( "โน๏ธ", `Aborted current turn.${clearedSuffix}`, ), { parseMode: "HTML" }, ); } export async function handleTelegramAbortCommand(deps: { hasAbortHandler: () => boolean; hasActiveTelegramTurn: () => boolean; clearPendingModelSwitch: () => void; abortCurrentTurn: () => void; setFoldQueuedPromptsIntoHistory: (fold: boolean) => void; updateStatus: () => void; sendTextReply: ( text: string, options?: { parseMode?: "HTML" }, ) => Promise; }): Promise { deps.clearPendingModelSwitch(); if (!deps.hasAbortHandler()) { await deps.sendTextReply( formatTelegramInformationHeading("๐Ÿ’ค", "No active turn."), { parseMode: "HTML" }, ); return; } deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn()); deps.abortCurrentTurn(); deps.updateStatus(); await deps.sendTextReply( formatTelegramInformationHeading("โน๏ธ", "Aborted current turn."), { parseMode: "HTML" }, ); } export async function handleTelegramNextCommand(deps: { hasAbortHandler: () => boolean; isIdle: () => boolean; hasQueuedItems: () => boolean; clearPendingModelSwitch: () => void; abortCurrentTurn: () => void; dispatchNextQueuedTurn: () => void; clearFoldForDispatch: () => void; updateStatus: () => void; sendTextReply: ( text: string, options?: { parseMode?: "HTML" }, ) => Promise; }): Promise { deps.clearPendingModelSwitch(); if (!deps.hasQueuedItems()) { await deps.sendTextReply( formatTelegramInformationHeading("โŒ›", "Queue is empty."), { parseMode: "HTML" }, ); return; } if (!deps.isIdle() && deps.hasAbortHandler()) { deps.clearFoldForDispatch(); deps.abortCurrentTurn(); deps.updateStatus(); await deps.sendTextReply( formatTelegramInformationHeading( "โฉ", "Aborted! Dispatching next queued turn.", ), { parseMode: "HTML" }, ); return; } if (!deps.isIdle()) { await deps.sendTextReply( formatTelegramInformationHeading( "โณ", "Pi is busy. Send /abort or /stop first.", ), { parseMode: "HTML" }, ); return; } deps.dispatchNextQueuedTurn(); deps.updateStatus(); await deps.sendTextReply( formatTelegramInformationHeading("โ–ถ๏ธ", "Dispatching next queued turn."), { parseMode: "HTML" }, ); } export async function handleTelegramContinueCommand( message: TMessage, ctx: TContext, deps: { enqueueContinueTurn: (message: TMessage, ctx: TContext) => Promise; }, ): Promise { await deps.enqueueContinueTurn(message, ctx); } function dispatchNextQueuedTelegramTurnAfterCompact( deps: Pick< TelegramCompactCommandDeps, | "dispatchNextQueuedTelegramTurn" | "requestDeferredDispatchNextQueuedTelegramTurn" >, ): void { if (deps.requestDeferredDispatchNextQueuedTelegramTurn) { deps.requestDeferredDispatchNextQueuedTelegramTurn( deps.dispatchNextQueuedTelegramTurn, ); return; } deps.dispatchNextQueuedTelegramTurn(); } export function buildTelegramCompactConfirmationReplyMarkup(): TelegramCompactConfirmationReplyMarkup { return { inline_keyboard: [ [ { text: "๐Ÿ—œ Yes, compact", callback_data: "compact:confirm" }, { text: "โŒ No", callback_data: "compact:cancel" }, ], ], }; } export function getTelegramCompactConfirmationHtml(): string { return "Compact session?"; } export async function openTelegramCompactConfirmation( target: TelegramCommandMessageTarget, deps: TelegramCompactConfirmationDeps, ): Promise { await deps.sendInteractiveMessage( target.chatId, getTelegramCompactConfirmationHtml(), "html", buildTelegramCompactConfirmationReplyMarkup(), target.threadId !== undefined ? { target: { chatId: target.chatId, threadId: target.threadId } } : undefined, ); } export async function handleTelegramCompactConfirmationCallback( query: TelegramCompactConfirmationCallbackQuery, deps: TelegramCompactConfirmationCallbackDeps, ): Promise { if (query.data !== "compact:confirm" && query.data !== "compact:cancel") { return false; } const callbackMessage = query.message; const chatId = callbackMessage?.chat?.id; const messageId = callbackMessage?.message_id; if (typeof chatId !== "number" || typeof messageId !== "number") { await deps.answerCallbackQuery(query.id, "โŒ› Interactive message expired."); return true; } if (query.data === "compact:cancel") { await deps.editInteractiveMessage( chatId, messageId, "๐Ÿšซ Compaction cancelled.", "html", { inline_keyboard: [] }, ); await deps.answerCallbackQuery(query.id); return true; } await deps.editInteractiveMessage( chatId, messageId, TELEGRAM_COMPACTION_STARTED_TEXT, "html", { inline_keyboard: [] }, ); await deps.answerCallbackQuery(query.id); const threadId = callbackMessage?.message_thread_id; await deps.runCompact( deps.ctx, chatId, messageId, typeof threadId === "number" ? { chatId, threadId } : { chatId }, ); return true; } export async function handleTelegramCompactCommand( deps: TelegramCompactCommandDeps, ): Promise { if ( !deps.isIdle() || deps.hasPendingMessages() || deps.hasActiveTelegramTurn() || deps.hasDispatchPending() || deps.hasQueuedTelegramItems() || deps.isCompactionInProgress() ) { await deps.sendTextReply( formatTelegramInformationHeading( "โณ", "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.", ), { parseMode: "HTML" }, ); return; } deps.setCompactionInProgress(true); deps.updateStatus(); deps.startTypingLoop?.(); try { deps.compact({ onComplete: () => { deps.stopTypingLoop?.(); deps.setCompactionInProgress(false); deps.updateStatus(); dispatchNextQueuedTelegramTurnAfterCompact(deps); void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT, { parseMode: "HTML", }); }, onError: (error) => { deps.stopTypingLoop?.(); deps.setCompactionInProgress(false); deps.updateStatus(); dispatchNextQueuedTelegramTurnAfterCompact(deps); deps.recordRuntimeEvent?.("compact", error); void deps.sendTextReply( formatTelegramInformationHeading( "โš ๏ธ", formatTelegramCompactionFailure(error), ), { parseMode: "HTML" }, ); }, }); } catch (error) { deps.stopTypingLoop?.(); deps.setCompactionInProgress(false); deps.updateStatus(); deps.recordRuntimeEvent?.("compact", error); await deps.sendTextReply( formatTelegramInformationHeading( "โš ๏ธ", formatTelegramCompactionFailure(error), ), { parseMode: "HTML" }, ); return; } if (!deps.suppressStartNotice) { await deps.sendTextReply(TELEGRAM_COMPACTION_STARTED_TEXT, { parseMode: "HTML", }); } } function isTelegramStaleContextError(error: unknown): boolean { return ( error instanceof Error && (error.message.includes("stale after session") || error.message.includes("stale ctx")) ); } export async function handleTelegramStatusCommand(deps: { ctx: TContext; showStatus: (ctx: TContext) => Promise; }): Promise { try { await deps.showStatus(deps.ctx); } catch (error) { if (!isTelegramStaleContextError(error)) throw error; } } export async function handleTelegramModelCommand(deps: { ctx: TContext; openModelMenu: (ctx: TContext) => Promise; }): Promise { try { await deps.openModelMenu(deps.ctx); } catch (error) { if (!isTelegramStaleContextError(error)) throw error; } } export async function executeTelegramCommandAction( action: TelegramCommandAction, message: TMessage, ctx: TContext, deps: TelegramCommandActionDeps, ): Promise { switch (action.kind) { case "ignore": return false; case "stop": await deps.handleStop(message, ctx); return true; case "abort": await deps.handleAbort(message, ctx); return true; case "next": await deps.handleNext(message, ctx); return true; case "continue": await deps.handleContinue(message, ctx); return true; case "queue": await deps.handleQueue(message, ctx); return true; case "compact": await deps.handleCompact(message, ctx); return true; case "status": await deps.handleStatus(message, ctx); return true; case "model": await deps.handleModel(message, ctx); return true; case "thinking": await deps.handleThinking(message, ctx); return true; case "settings": if (!deps.handleSettings) return false; await deps.handleSettings(message, ctx); return true; case "help": await deps.handleHelp(message, action.commandName, ctx); return true; } } export interface TelegramCommandHandlerTargetRuntimeDeps< TMessage extends TelegramCommandRuntimeMessage, TContext, > extends Omit< TelegramCommandRuntimeDeps, | "enqueueControlItem" | "showStatus" | "openModelMenu" | "openSettingsMenu" | "sendTextReply" | "registerBotCommands" >, Omit, "createControlItem">, TelegramBotCommandRegistrationDeps { allocateItemOrder: () => number; allocateControlOrder: () => number; } export function createTelegramCommandHandlerTargetRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( deps: TelegramCommandHandlerTargetRuntimeDeps, ): ( commandName: string | undefined, message: TMessage, ctx: TContext, ) => Promise { const commandTargetRuntime = createTelegramCommandTargetQueueRuntime< TMessage, TContext >({ createControlItem: createTelegramControlItemBuilder({ allocateItemOrder: deps.allocateItemOrder, allocateControlOrder: deps.allocateControlOrder, }), appendControlItem: deps.appendControlItem, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, getAdmissionScope: deps.getAdmissionScope, getAdmissionJournalBinding: deps.getAdmissionJournalBinding, onControlQueued: deps.onControlQueued, showStatus: deps.showStatus, openModelMenu: deps.openModelMenu, openSettingsMenu: deps.openSettingsMenu, sendTextReply: deps.sendTextReply, }); return createTelegramCommandHandler({ hasAbortHandler: deps.hasAbortHandler, clearPendingModelSwitch: deps.clearPendingModelSwitch, hasQueuedTelegramItems: deps.hasQueuedTelegramItems, clearQueuedTelegramItems: deps.clearQueuedTelegramItems, setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory, abortCurrentTurn: deps.abortCurrentTurn, isIdle: deps.isIdle, hasPendingMessages: deps.hasPendingMessages, hasActiveTelegramTurn: deps.hasActiveTelegramTurn, hasDispatchPending: deps.hasDispatchPending, isCompactionInProgress: deps.isCompactionInProgress, setCompactionInProgress: deps.setCompactionInProgress, updateStatus: deps.updateStatus, isContextActive: deps.isContextActive, dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn, startTypingLoop: deps.startTypingLoop, stopTypingLoop: deps.stopTypingLoop, enqueueContinueTurn: deps.enqueueContinueTurn, compact: deps.compact, sendInteractiveMessage: deps.sendInteractiveMessage, enqueueControlItem: commandTargetRuntime.enqueueControlItem, showStatus: commandTargetRuntime.showStatus, openModelMenu: commandTargetRuntime.openModelMenu, openThinkingMenu: deps.openThinkingMenu, openQueueMenu: deps.openQueueMenu, openSettingsMenu: commandTargetRuntime.openSettingsMenu, handleForumBootstrap: deps.handleForumBootstrap, getAllowedUserId: deps.getAllowedUserId, setAllowedUserId: deps.setAllowedUserId, registerBotCommands: createTelegramBotCommandRegistrar({ setMyCommands: deps.setMyCommands, }), persistConfig: deps.persistConfig, sendTextReply: commandTargetRuntime.sendTextReply, recordRuntimeEvent: deps.recordRuntimeEvent, }); } export function createTelegramCommandHandler< TMessage extends TelegramCommandRuntimeMessage, TContext, >(deps: TelegramCommandRuntimeDeps) { return async ( commandName: string | undefined, message: TMessage, ctx: TContext, ): Promise => { return handleTelegramCommandRuntime(commandName, message, ctx, deps); }; } export function createTelegramCommandOrPromptRuntime( deps: TelegramCommandOrPromptRuntimeDeps, ) { return { dispatchMessages: async ( messages: TMessage[], ctx: TContext, ): Promise => { const firstMessage = messages[0]; if (!firstMessage) return; if (deps.shouldIgnoreMessages?.(messages)) return; deps.assertExecutionCurrent?.(firstMessage); const command = parseTelegramCommand(deps.extractRawText(messages)); const handled = await deps.handleCommand( command?.name, firstMessage, ctx, ); deps.assertExecutionCurrent?.(firstMessage); if (handled) return; if (command && deps.executeExtensionCommand) { const handledByExtension = await deps.executeExtensionCommand( command, messages[0]!, ctx, ); deps.assertExecutionCurrent?.(firstMessage); if (handledByExtension) return; } if (command?.name && deps.expandPromptTemplateCommand) { const expanded = deps.expandPromptTemplateCommand( command.name, command.args, ); if (expanded !== undefined) { deps.assertExecutionCurrent?.(firstMessage); await deps.enqueueTurn( [ deps.replaceMessageText(firstMessage, expanded), ...messages.slice(1), ], ctx, ); return; } } deps.assertExecutionCurrent?.(firstMessage); await deps.enqueueTurn(messages, ctx); }, }; } function scheduleTelegramCommandEffect( ctx: TContext, command: string, phase: string, deps: TelegramRuntimeEventRecorderPort & { isContextActive?: (ctx: TContext) => boolean; }, effect: () => Promise, assertExecutionCurrent?: () => void, ): void { void Promise.resolve() .then(async () => { if (deps.isContextActive?.(ctx) === false) return; assertExecutionCurrent?.(); await effect(); assertExecutionCurrent?.(); }) .catch((error) => { try { deps.recordRuntimeEvent?.("telegram-command", error, { command, phase, }); } catch { // Effect diagnostics cannot create an unhandled detached Promise. } }); } async function handleTelegramCommandRuntime< TMessage extends TelegramCommandRuntimeMessage, TContext, >( commandName: string | undefined, message: TMessage, ctx: TContext, deps: TelegramCommandRuntimeDeps, ): Promise { const assertExecutionCurrentFor = (nextMessage: TMessage) => (): void => deps.assertExecutionCurrent?.(nextMessage); const sendReplyFor = (nextMessage: TMessage) => async (text: string, options?: { parseMode?: "HTML" }) => { deps.assertExecutionCurrent?.(nextMessage); await deps.sendTextReply(nextMessage, text, options); deps.assertExecutionCurrent?.(nextMessage); }; const updateStatusFor = (commandCtx: TContext) => () => deps.updateStatus(commandCtx); return executeTelegramCommandAction( buildTelegramCommandAction(commandName), message, ctx, { handleStop: async (nextMessage, commandCtx) => { await handleTelegramStopCommand({ hasAbortHandler: deps.hasAbortHandler, clearPendingModelSwitch: deps.clearPendingModelSwitch, clearQueuedTelegramItems: () => deps.clearQueuedTelegramItems(commandCtx), setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory, abortCurrentTurn: deps.abortCurrentTurn, updateStatus: updateStatusFor(commandCtx), sendTextReply: sendReplyFor(nextMessage), }); }, handleAbort: async (nextMessage, commandCtx) => { await handleTelegramAbortCommand({ hasAbortHandler: deps.hasAbortHandler, hasActiveTelegramTurn: deps.hasActiveTelegramTurn, clearPendingModelSwitch: deps.clearPendingModelSwitch, abortCurrentTurn: deps.abortCurrentTurn, setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory, updateStatus: updateStatusFor(commandCtx), sendTextReply: sendReplyFor(nextMessage), }); }, handleNext: async (nextMessage, commandCtx) => { await handleTelegramNextCommand({ hasAbortHandler: deps.hasAbortHandler, isIdle: () => deps.isIdle(commandCtx), hasQueuedItems: deps.hasQueuedTelegramItems, clearPendingModelSwitch: deps.clearPendingModelSwitch, abortCurrentTurn: deps.abortCurrentTurn, dispatchNextQueuedTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx), clearFoldForDispatch: () => deps.setFoldQueuedPromptsIntoHistory(false), updateStatus: updateStatusFor(commandCtx), sendTextReply: sendReplyFor(nextMessage), }); }, handleContinue: async (nextMessage, commandCtx) => { await handleTelegramContinueCommand(nextMessage, commandCtx, { enqueueContinueTurn: deps.enqueueContinueTurn, }); }, handleQueue: async (nextMessage, commandCtx) => { scheduleTelegramCommandEffect( commandCtx, "queue", "menu-render", deps, () => deps.openQueueMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage), ); }, handleCompact: async (nextMessage, commandCtx) => { if (deps.sendInteractiveMessage) { await openTelegramCompactConfirmation( getTelegramCommandMessageTarget(nextMessage), { sendInteractiveMessage: deps.sendInteractiveMessage }, ); return; } await handleTelegramCompactCommand({ isIdle: () => deps.isIdle(commandCtx), hasPendingMessages: () => deps.hasPendingMessages(commandCtx), hasActiveTelegramTurn: deps.hasActiveTelegramTurn, hasDispatchPending: deps.hasDispatchPending, hasQueuedTelegramItems: deps.hasQueuedTelegramItems, isCompactionInProgress: deps.isCompactionInProgress, setCompactionInProgress: deps.setCompactionInProgress, updateStatus: updateStatusFor(commandCtx), dispatchNextQueuedTelegramTurn: () => deps.dispatchNextQueuedTelegramTurn(commandCtx), requestDeferredDispatchNextQueuedTelegramTurn: deps.requestDeferredDispatchNextQueuedTelegramTurn ? (dispatch) => deps.requestDeferredDispatchNextQueuedTelegramTurn?.(() => dispatch(), ) : undefined, compact: (callbacks) => deps.compact(commandCtx, callbacks), startTypingLoop: deps.startTypingLoop ? () => deps.startTypingLoop?.(commandCtx, nextMessage.chat.id, { target: getTelegramCommandMessageTarget(nextMessage), }) : undefined, stopTypingLoop: deps.stopTypingLoop, sendTextReply: sendReplyFor(nextMessage), recordRuntimeEvent: deps.recordRuntimeEvent, }); }, handleStatus: async (nextMessage, commandCtx) => { scheduleTelegramCommandEffect( commandCtx, "status", "menu-render", deps, () => deps.showStatus(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage), ); }, handleModel: async (nextMessage, commandCtx) => { scheduleTelegramCommandEffect( commandCtx, "model", "menu-render", deps, () => handleTelegramModelCommand({ ctx: commandCtx, openModelMenu: (controlCtx) => deps.openModelMenu(nextMessage, controlCtx), }), assertExecutionCurrentFor(nextMessage), ); }, handleThinking: async (nextMessage, commandCtx) => { scheduleTelegramCommandEffect( commandCtx, "thinking", "menu-render", deps, () => deps.openThinkingMenu(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage), ); }, handleSettings: deps.openSettingsMenu ? async (nextMessage, commandCtx) => { scheduleTelegramCommandEffect( commandCtx, "settings", "menu-render", deps, () => deps.openSettingsMenu!(nextMessage, commandCtx), assertExecutionCurrentFor(nextMessage), ); } : undefined, handleHelp: async (nextMessage, nextCommandName, commandCtx) => { if ( nextMessage.from?.id !== undefined && canPairTelegramUserFromCommandMessage(nextMessage) ) { await pairTelegramUserIfNeeded(nextMessage.from.id, { allowedUserId: deps.getAllowedUserId(), ctx: undefined, setAllowedUserId: deps.setAllowedUserId, persistConfig: deps.persistConfig, updateStatus: updateStatusFor(commandCtx), assertExecutionCurrent: assertExecutionCurrentFor(nextMessage), }); } const isContextActive = () => deps.isContextActive?.(commandCtx) !== false; scheduleTelegramCommandEffect( commandCtx, nextCommandName, "menu-render", deps, async () => { let forumBootstrapMessage: string | undefined; if (nextCommandName === "start" && deps.handleForumBootstrap) { forumBootstrapMessage = await deps.handleForumBootstrap( nextMessage, commandCtx, ); } if (!isContextActive()) return; if (forumBootstrapMessage) { await deps.sendTextReply(nextMessage, forumBootstrapMessage); } if (!isContextActive()) return; await deps.showStatus(nextMessage, commandCtx); }, assertExecutionCurrentFor(nextMessage), ); scheduleTelegramCommandEffect( commandCtx, nextCommandName, "bot-command-sync", deps, deps.registerBotCommands, assertExecutionCurrentFor(nextMessage), ); }, }, ); }