/** * Telegram queue and queue-runtime domain helpers * Owns queue items, queue mutations, dispatch and lifecycle planning, session resets, and queue-adjacent runtime helpers */ // --- Queue Items --- export interface QueuedAttachment { path: string; fileName: string; } export interface TelegramPromptTextContent { type: "text"; text: string; } export interface TelegramPromptImageContent { type: "image"; data: string; mimeType: string; } export type TelegramPromptContent = | TelegramPromptTextContent | TelegramPromptImageContent; export type TelegramQueueItemKind = "prompt" | "control"; export type TelegramQueueLane = "control" | "priority" | "default"; export type TelegramQueueAdmissionMode = | "control-queue" | "priority-queue" | "default-queue"; export interface TelegramQueueLaneContract { lane: TelegramQueueLane; admissionMode: TelegramQueueAdmissionMode; dispatchRank: number; allowedKinds: readonly TelegramQueueItemKind[]; } export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[] = [ { lane: "control", admissionMode: "control-queue", dispatchRank: 0, allowedKinds: ["control", "prompt"], }, { lane: "priority", admissionMode: "priority-queue", dispatchRank: 1, allowedKinds: ["prompt"], }, { lane: "default", admissionMode: "default-queue", dispatchRank: 2, allowedKinds: ["prompt"], }, ] as const; export interface TelegramQueueItemBase { kind: TelegramQueueItemKind; chatId: number; threadId?: number; replyToMessageId: number; queueOrder: number; queueLane: TelegramQueueLane; laneOrder: number; statusSummary: string; } export interface PendingTelegramTurn extends TelegramQueueItemBase { kind: "prompt"; sourceMessageIds: number[]; queuedAttachments: QueuedAttachment[]; content: TelegramPromptContent[]; historyText: string; } export interface PendingTelegramControlItem< TContext = unknown, > extends TelegramQueueItemBase { kind: "control"; controlType: "status" | "model"; execute: (ctx: TContext) => Promise; } export type TelegramQueueItem = | PendingTelegramTurn | PendingTelegramControlItem; export interface TelegramQueueStore { getQueuedItems: () => TelegramQueueItem[]; setQueuedItems: (items: TelegramQueueItem[]) => void; } export interface TelegramQueueStateStore< TContext = unknown, > extends TelegramQueueStore { hasQueuedItems: () => boolean; } export interface TelegramActiveTurnStore< TTurn extends PendingTelegramTurn = PendingTelegramTurn, > { get: () => TTurn | undefined; has: () => boolean; set: (turn: TTurn) => void; clear: () => void; getChatId: () => number | undefined; getReplyToMessageId: () => number | undefined; getSourceMessageIds: () => number[] | undefined; } export interface TelegramDispatchGuardState { compactionInProgress: boolean; hasActiveTelegramTurn: boolean; hasPendingTelegramDispatch: boolean; isIdle: boolean; hasPendingMessages: boolean; } export function getTelegramQueueLaneContract( lane: TelegramQueueLane, ): TelegramQueueLaneContract { const contract = TELEGRAM_QUEUE_LANE_CONTRACTS.find( (entry) => entry.lane === lane, ); if (!contract) throw new Error(`Unknown Telegram queue lane: ${lane}`); return contract; } export function getTelegramQueueItemAdmissionMode( item: Pick, ): TelegramQueueAdmissionMode { return getTelegramQueueLaneContract(item.queueLane).admissionMode; } export function isTelegramQueueItemAdmissionValid( item: Pick, ): boolean { return getTelegramQueueLaneContract(item.queueLane).allowedKinds.includes( item.kind, ); } export function assertTelegramQueueItemAdmissionValid( item: Pick, ): void { if (isTelegramQueueItemAdmissionValid(item)) return; throw new Error( `Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`, ); } function getTelegramQueueLaneRank(lane: TelegramQueueLane): number { return getTelegramQueueLaneContract(lane).dispatchRank; } export function isPendingTelegramTurn( item: TelegramQueueItem, ): item is PendingTelegramTurn { return item.kind === "prompt"; } export function createTelegramQueueStore( initialItems: TelegramQueueItem[] = [], ): TelegramQueueStateStore { let queuedItems = initialItems; return { getQueuedItems: () => queuedItems, setQueuedItems: (items) => { queuedItems = items; }, hasQueuedItems: () => queuedItems.length > 0, }; } export function createTelegramActiveTurnStore< TTurn extends PendingTelegramTurn = PendingTelegramTurn, >(): TelegramActiveTurnStore { let activeTurn: TTurn | undefined; return { get: () => activeTurn, has: () => !!activeTurn, set: (turn) => { activeTurn = { ...turn }; }, clear: () => { activeTurn = undefined; }, getChatId: () => activeTurn?.chatId, getReplyToMessageId: () => activeTurn?.replyToMessageId, getSourceMessageIds: () => activeTurn?.sourceMessageIds, }; } // --- Queue Mutations --- export function partitionTelegramQueueItemsForHistory( items: TelegramQueueItem[], ): { historyTurns: PendingTelegramTurn[]; remainingItems: TelegramQueueItem[]; } { const historyTurns: PendingTelegramTurn[] = []; const remainingItems: TelegramQueueItem[] = []; for (const item of items) { if (isPendingTelegramTurn(item)) { historyTurns.push(item); continue; } remainingItems.push(item); } return { historyTurns, remainingItems }; } export function planTelegramPromptEnqueue( items: TelegramQueueItem[], preserveQueuedTurnsAsHistory: boolean, ): { historyTurns: PendingTelegramTurn[]; remainingItems: TelegramQueueItem[]; } { if (!preserveQueuedTurnsAsHistory) { return { historyTurns: [], remainingItems: items }; } return partitionTelegramQueueItemsForHistory(items); } export function appendTelegramQueueItem< TContext = unknown, TItem extends TelegramQueueItem = TelegramQueueItem, >( items: TelegramQueueItem[], item: TItem, ): TelegramQueueItem[] { assertTelegramQueueItemAdmissionValid(item); return [...items, item]; } export function compareTelegramQueueItems( left: TelegramQueueItem, right: TelegramQueueItem, ): number { assertTelegramQueueItemAdmissionValid(left); assertTelegramQueueItemAdmissionValid(right); const laneRankDelta = getTelegramQueueLaneRank(left.queueLane) - getTelegramQueueLaneRank(right.queueLane); if (laneRankDelta !== 0) return laneRankDelta; if (left.laneOrder !== right.laneOrder) { return left.laneOrder - right.laneOrder; } return left.queueOrder - right.queueOrder; } export function removeTelegramQueueItemsByMessageIds( items: TelegramQueueItem[], messageIds: number[], ): { items: TelegramQueueItem[]; removedCount: number } { if (messageIds.length === 0 || items.length === 0) { return { items, removedCount: 0 }; } const deletedMessageIds = new Set(messageIds); const nextItems = items.filter((item) => { if (!isPendingTelegramTurn(item)) return true; return !item.sourceMessageIds.some((messageId) => deletedMessageIds.has(messageId), ); }); return { items: nextItems, removedCount: items.length - nextItems.length, }; } export function clearTelegramQueuePromptPriority( items: TelegramQueueItem[], messageId: number, ): { items: TelegramQueueItem[]; changed: boolean } { let changed = false; const nextItems = items.map((item) => { if ( !isPendingTelegramTurn(item) || !item.sourceMessageIds.includes(messageId) || item.queueLane !== "priority" ) { return item; } changed = true; return { ...item, queueLane: "default" as const, laneOrder: item.queueOrder, }; }); return { items: nextItems, changed }; } export function prioritizeTelegramQueuePrompt( items: TelegramQueueItem[], messageId: number, laneOrder: number, ): { items: TelegramQueueItem[]; changed: boolean } { let changed = false; const nextItems = items.map((item) => { if ( !isPendingTelegramTurn(item) || !item.sourceMessageIds.includes(messageId) ) { return item; } changed = true; return { ...item, queueLane: "priority" as const, laneOrder, }; }); return { items: nextItems, changed }; } export function consumeDispatchedTelegramPrompt( items: TelegramQueueItem[], hasPendingDispatch: boolean, ): { activeTurn?: PendingTelegramTurn; remainingItems: TelegramQueueItem[]; } { if (!hasPendingDispatch) { return { activeTurn: undefined, remainingItems: items }; } const nextItem = items[0]; if (!nextItem || !isPendingTelegramTurn(nextItem)) { return { activeTurn: undefined, remainingItems: items }; } return { activeTurn: nextItem, remainingItems: items.slice(1) }; } function formatTelegramQueueItemStatusSummary( item: TelegramQueueItem, ): string { if (item.queueLane === "priority") { return `⬆ ${item.statusSummary}`; } return item.statusSummary; } export function formatQueuedTelegramItemsStatus( items: TelegramQueueItem[], ): string { if (items.length === 0) return ""; const previewCount = 4; const summaries = items .slice(0, previewCount) .map(formatTelegramQueueItemStatusSummary) .filter(Boolean); if (summaries.length === 0) return ` +${items.length}`; const suffix = items.length > summaries.length ? ", …" : ""; return ` +${items.length}: [${summaries.join(", ")}${suffix}]`; } export function canDispatchTelegramTurnState( state: TelegramDispatchGuardState, ): boolean { return ( !state.compactionInProgress && !state.hasActiveTelegramTurn && !state.hasPendingTelegramDispatch && state.isIdle && !state.hasPendingMessages ); } export interface TelegramDispatchReadinessDeps { isCompactionInProgress: () => boolean; hasActiveTurn: () => boolean; hasDispatchPending: () => boolean; isIdle: (ctx: TContext) => boolean; hasPendingMessages: (ctx: TContext) => boolean; } export function createTelegramDispatchReadinessChecker( deps: TelegramDispatchReadinessDeps, ): (ctx: TContext) => boolean { return (ctx) => canDispatchTelegramTurnState({ compactionInProgress: deps.isCompactionInProgress(), hasActiveTelegramTurn: deps.hasActiveTurn(), hasPendingTelegramDispatch: deps.hasDispatchPending(), isIdle: deps.isIdle(ctx), hasPendingMessages: deps.hasPendingMessages(ctx), }); } export function buildPendingTelegramControlItem(options: { chatId: number; threadId?: number; replyToMessageId: number; controlType: PendingTelegramControlItem["controlType"]; queueOrder: number; laneOrder: number; statusSummary: string; execute: PendingTelegramControlItem["execute"]; }): PendingTelegramControlItem { return { kind: "control", controlType: options.controlType, chatId: options.chatId, threadId: options.threadId, replyToMessageId: options.replyToMessageId, queueOrder: options.queueOrder, queueLane: "control", laneOrder: options.laneOrder, statusSummary: options.statusSummary, execute: options.execute, }; } export interface TelegramControlItemBuilderDeps { allocateItemOrder: () => number; allocateControlOrder: () => number; } export function createTelegramControlItemBuilder( deps: TelegramControlItemBuilderDeps, ): (options: { chatId: number; threadId?: number; replyToMessageId: number; controlType: PendingTelegramControlItem["controlType"]; statusSummary: string; execute: PendingTelegramControlItem["execute"]; }) => PendingTelegramControlItem { return (options) => buildPendingTelegramControlItem({ ...options, queueOrder: deps.allocateItemOrder(), laneOrder: deps.allocateControlOrder(), }); } // --- Dispatch Planning --- export type TelegramQueueDispatchAction = | { kind: "none"; remainingItems: TelegramQueueItem[] } | { kind: "control"; item: PendingTelegramControlItem; remainingItems: TelegramQueueItem[]; } | { kind: "prompt"; item: PendingTelegramTurn; remainingItems: TelegramQueueItem[]; }; export function planNextTelegramQueueAction( items: TelegramQueueItem[], canDispatch: boolean, ): TelegramQueueDispatchAction { if (!canDispatch || items.length === 0) { return { kind: "none", remainingItems: items }; } const [firstItem, ...remainingItems] = items; if (!firstItem) { return { kind: "none", remainingItems: items }; } assertTelegramQueueItemAdmissionValid(firstItem); if (isPendingTelegramTurn(firstItem)) { return { kind: "prompt", item: firstItem, remainingItems: items }; } return { kind: "control", item: firstItem, remainingItems }; } export function shouldDispatchAfterTelegramAgentEnd(options: { hasTurn: boolean; stopReason?: string; preserveQueuedTurnsAsHistory: boolean; }): boolean { if (!options.hasTurn) return true; if (options.stopReason === "aborted") { return !options.preserveQueuedTurnsAsHistory; } return true; } // --- Agent Runtime --- export interface TelegramAgentStartPlan { activeTurn?: PendingTelegramTurn; remainingItems: TelegramQueueItem[]; shouldResetPendingModelSwitch: boolean; shouldResetToolExecutions: boolean; shouldClearDispatchPending: boolean; } export interface TelegramAgentStartRuntimeDeps< TTurn extends PendingTelegramTurn, TContext = unknown, > { queuedItems: TelegramQueueItem[]; hasPendingDispatch: boolean; hasActiveTurn: boolean; resetToolExecutions: () => void; resetPendingModelSwitch: () => void; setQueuedItems: (items: TelegramQueueItem[]) => void; clearDispatchPending: () => void; setActiveTurn: (turn: TTurn) => void; createPreviewState: () => void; startTypingLoop: () => void; updateStatus: () => void; } export interface TelegramAgentStartHookRuntimeDeps< TTurn extends PendingTelegramTurn, TContext = unknown, > { setAbortHandler: (ctx: TContext) => void; getQueuedItems: () => TelegramQueueItem[]; hasPendingDispatch: () => boolean; hasActiveTurn: () => boolean; resetToolExecutions: () => void; resetPendingModelSwitch: () => void; setQueuedItems: (items: TelegramQueueItem[]) => void; clearDispatchPending: () => void; setActiveTurn: (turn: TTurn) => void; createPreviewState: () => void; startTypingLoop: (ctx: TContext) => void; updateStatus: (ctx: TContext) => void; } export type TelegramAgentStartHookEvent = unknown; export interface TelegramToolExecutionRuntimeDeps { hasActiveTurn: () => boolean; getActiveToolExecutions: () => number; setActiveToolExecutions: (count: number) => void; } export interface TelegramToolExecutionEndRuntimeDeps extends TelegramToolExecutionRuntimeDeps { triggerPendingModelSwitchAbort: () => void; } export interface TelegramToolExecutionHookRuntimeDeps< TContext, > extends TelegramToolExecutionRuntimeDeps { triggerPendingModelSwitchAbort: (ctx: TContext) => unknown; } export type TelegramToolExecutionHookEvent = unknown; export function buildTelegramAgentStartPlan(options: { queuedItems: TelegramQueueItem[]; hasPendingDispatch: boolean; hasActiveTurn: boolean; }): TelegramAgentStartPlan { if (options.hasActiveTurn || !options.hasPendingDispatch) { return { activeTurn: undefined, remainingItems: options.queuedItems, shouldResetPendingModelSwitch: true, shouldResetToolExecutions: true, shouldClearDispatchPending: options.hasPendingDispatch, }; } const nextDispatch = consumeDispatchedTelegramPrompt( options.queuedItems, options.hasPendingDispatch, ); return { activeTurn: nextDispatch.activeTurn, remainingItems: nextDispatch.remainingItems, shouldResetPendingModelSwitch: true, shouldResetToolExecutions: true, shouldClearDispatchPending: options.hasPendingDispatch, }; } export function handleTelegramAgentStartRuntime< TTurn extends PendingTelegramTurn, TContext = unknown, >(deps: TelegramAgentStartRuntimeDeps): void { const startPlan = buildTelegramAgentStartPlan({ queuedItems: deps.queuedItems, hasPendingDispatch: deps.hasPendingDispatch, hasActiveTurn: deps.hasActiveTurn, }); if (startPlan.shouldResetToolExecutions) deps.resetToolExecutions(); if (startPlan.shouldResetPendingModelSwitch) deps.resetPendingModelSwitch(); deps.setQueuedItems(startPlan.remainingItems); if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending(); if (startPlan.activeTurn) { deps.setActiveTurn(startPlan.activeTurn as TTurn); deps.createPreviewState(); deps.startTypingLoop(); } deps.updateStatus(); } export function createTelegramAgentStartHook< TTurn extends PendingTelegramTurn, TContext = unknown, >(deps: TelegramAgentStartHookRuntimeDeps) { return async function onAgentStart( _event: TelegramAgentStartHookEvent, ctx: TContext, ): Promise { deps.setAbortHandler(ctx); handleTelegramAgentStartRuntime({ queuedItems: deps.getQueuedItems(), hasPendingDispatch: deps.hasPendingDispatch(), hasActiveTurn: deps.hasActiveTurn(), resetToolExecutions: deps.resetToolExecutions, resetPendingModelSwitch: deps.resetPendingModelSwitch, setQueuedItems: deps.setQueuedItems, clearDispatchPending: deps.clearDispatchPending, setActiveTurn: deps.setActiveTurn, createPreviewState: deps.createPreviewState, startTypingLoop: () => deps.startTypingLoop(ctx), updateStatus: () => deps.updateStatus(ctx), }); }; } export function getNextTelegramToolExecutionCount(options: { hasActiveTurn: boolean; currentCount: number; event: "start" | "end"; }): number { if (!options.hasActiveTurn) return options.currentCount; if (options.event === "start") { return options.currentCount + 1; } return Math.max(0, options.currentCount - 1); } export function handleTelegramToolExecutionStartRuntime( deps: TelegramToolExecutionRuntimeDeps, ): void { deps.setActiveToolExecutions( getNextTelegramToolExecutionCount({ hasActiveTurn: deps.hasActiveTurn(), currentCount: deps.getActiveToolExecutions(), event: "start", }), ); } export function handleTelegramToolExecutionEndRuntime( deps: TelegramToolExecutionEndRuntimeDeps, ): void { const hasActiveTurn = deps.hasActiveTurn(); deps.setActiveToolExecutions( getNextTelegramToolExecutionCount({ hasActiveTurn, currentCount: deps.getActiveToolExecutions(), event: "end", }), ); if (hasActiveTurn) deps.triggerPendingModelSwitchAbort(); } export type TelegramAgentLifecycleHooksRuntimeDeps< TTurn extends PendingTelegramTurn, TContext, TMessage, > = TelegramAgentStartHookRuntimeDeps & TelegramAgentEndHookRuntimeDeps & TelegramToolExecutionHookRuntimeDeps; export function createTelegramAgentLifecycleHooks< TTurn extends PendingTelegramTurn, TContext, TMessage, >(deps: TelegramAgentLifecycleHooksRuntimeDeps) { return { onAgentStart: createTelegramAgentStartHook(deps), onAgentEnd: createTelegramAgentEndHook(deps), ...createTelegramToolExecutionHooks(deps), }; } export function createTelegramToolExecutionHooks( deps: TelegramToolExecutionHookRuntimeDeps, ) { return { onToolExecutionStart: (): void => { handleTelegramToolExecutionStartRuntime(deps); }, onToolExecutionEnd: ( _event: TelegramToolExecutionHookEvent, ctx: TContext, ): void => { handleTelegramToolExecutionEndRuntime({ hasActiveTurn: deps.hasActiveTurn, getActiveToolExecutions: deps.getActiveToolExecutions, setActiveToolExecutions: deps.setActiveToolExecutions, triggerPendingModelSwitchAbort: () => { deps.triggerPendingModelSwitchAbort(ctx); }, }); }, }; } // --- Agent End Lifecycle --- export interface TelegramAgentEndPlan { kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty"; shouldClearPreview: boolean; shouldDispatchNext: boolean; shouldSendErrorMessage: boolean; shouldSendAttachmentNotice: boolean; } export interface TelegramAgentEndAssistantResult { text?: string; stopReason?: string; errorMessage?: string; } export interface TelegramAgentEndOutboundVoiceReply { text: string; lang?: string; rate?: string; } export interface TelegramAgentEndOutboundReplyPlan { markdown: string; replyMarkup?: TReplyMarkup; voiceText?: string; voiceReplies?: TelegramAgentEndOutboundVoiceReply[]; lang?: string; rate?: string; } export interface TelegramAgentEndRuntimeDeps< TTurn extends PendingTelegramTurn, > { turn: TTurn | undefined; assistant: TelegramAgentEndAssistantResult; preserveQueuedTurnsAsHistory: boolean; resetRuntimeState: () => void; updateStatus: () => void; dispatchNextQueuedTelegramTurn: () => void; clearPreview: (chatId: number) => Promise; setPreviewPendingText: (text: string) => void; finalizeMarkdownPreview: ( chatId: number, markdown: string, replyToMessageId: number, options?: { replyMarkup?: unknown; threadId?: number }, ) => Promise; sendMarkdownReply: ( chatId: number, replyToMessageId: number, markdown: string, options?: { replyMarkup?: unknown; threadId?: number }, ) => Promise; sendTextReply: ( chatId: number, replyToMessageId: number, text: string, options?: { threadId?: number }, ) => Promise; sendQueuedAttachments: (turn: TTurn) => Promise; planOutboundReply?: (markdown: string) => TelegramAgentEndOutboundReplyPlan; sendOutboundReplyArtifacts?: ( turn: TTurn, plan: TelegramAgentEndOutboundReplyPlan, options?: { replyToPrompt?: boolean }, ) => Promise; } export interface TelegramAgentEndHookRuntimeDeps< TTurn extends PendingTelegramTurn, TContext, TMessage, > { getActiveTurn: () => TTurn | undefined; extractAssistant: ( messages: readonly TMessage[], ) => TelegramAgentEndAssistantResult; getPreserveQueuedTurnsAsHistory: () => boolean; resetRuntimeState: () => void; updateStatus: (ctx: TContext) => void; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; requestDeferredDispatchNextQueuedTelegramTurn: ( dispatch: (ctx: TContext) => void, ) => void; clearPreview: (chatId: number) => Promise; setPreviewPendingText: (text: string) => void; finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps["finalizeMarkdownPreview"]; sendMarkdownReply: TelegramAgentEndRuntimeDeps["sendMarkdownReply"]; sendTextReply: TelegramAgentEndRuntimeDeps["sendTextReply"]; sendQueuedAttachments: (turn: TTurn) => Promise; planOutboundReply?: TelegramAgentEndRuntimeDeps["planOutboundReply"]; sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps["sendOutboundReplyArtifacts"]; } export interface TelegramAgentEndHookEvent { messages: readonly TMessage[]; } export function buildTelegramAgentEndPlan(options: { hasTurn: boolean; stopReason?: string; hasFinalText: boolean; hasQueuedAttachments: boolean; preserveQueuedTurnsAsHistory: boolean; }): TelegramAgentEndPlan { const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({ hasTurn: options.hasTurn, stopReason: options.stopReason, preserveQueuedTurnsAsHistory: options.preserveQueuedTurnsAsHistory, }); if (!options.hasTurn) { return { kind: "no-turn", shouldClearPreview: false, shouldDispatchNext, shouldSendErrorMessage: false, shouldSendAttachmentNotice: false, }; } if (options.stopReason === "aborted") { return { kind: "aborted", shouldClearPreview: true, shouldDispatchNext, shouldSendErrorMessage: false, shouldSendAttachmentNotice: false, }; } if (options.stopReason === "error") { return { kind: "error", shouldClearPreview: true, shouldDispatchNext, shouldSendErrorMessage: true, shouldSendAttachmentNotice: false, }; } if (options.hasFinalText) { return { kind: "text", shouldClearPreview: false, shouldDispatchNext, shouldSendErrorMessage: false, shouldSendAttachmentNotice: false, }; } if (options.hasQueuedAttachments) { return { kind: "attachments-only", shouldClearPreview: true, shouldDispatchNext, shouldSendErrorMessage: false, shouldSendAttachmentNotice: true, }; } return { kind: "empty", shouldClearPreview: true, shouldDispatchNext, shouldSendErrorMessage: false, shouldSendAttachmentNotice: false, }; } export function createTelegramAgentEndHook< TTurn extends PendingTelegramTurn, TContext, TMessage, >(deps: TelegramAgentEndHookRuntimeDeps) { return async function onAgentEnd( event: TelegramAgentEndHookEvent, ctx: TContext, ): Promise { const turn = deps.getActiveTurn(); await handleTelegramAgentEndRuntime({ turn, assistant: turn ? deps.extractAssistant(event.messages) : {}, preserveQueuedTurnsAsHistory: deps.getPreserveQueuedTurnsAsHistory(), resetRuntimeState: deps.resetRuntimeState, updateStatus: () => deps.updateStatus(ctx), dispatchNextQueuedTelegramTurn: () => { deps.requestDeferredDispatchNextQueuedTelegramTurn( deps.dispatchNextQueuedTelegramTurn, ); }, clearPreview: deps.clearPreview, setPreviewPendingText: deps.setPreviewPendingText, finalizeMarkdownPreview: deps.finalizeMarkdownPreview, sendMarkdownReply: deps.sendMarkdownReply, sendTextReply: deps.sendTextReply, sendQueuedAttachments: deps.sendQueuedAttachments, planOutboundReply: deps.planOutboundReply, sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts, }); }; } export async function handleTelegramAgentEndRuntime< TTurn extends PendingTelegramTurn, >(deps: TelegramAgentEndRuntimeDeps): Promise { const { turn, assistant } = deps; const rawFinalText = assistant.text; const outboundReply = rawFinalText ? deps.planOutboundReply?.(rawFinalText) : undefined; const finalText = outboundReply ? outboundReply.markdown : rawFinalText; const hasOutboundArtifacts = !!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length; const replyMarkup = outboundReply?.replyMarkup; deps.resetRuntimeState(); deps.updateStatus(); const endPlan = buildTelegramAgentEndPlan({ hasTurn: !!turn, stopReason: assistant.stopReason, hasFinalText: !!finalText || hasOutboundArtifacts, hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0, preserveQueuedTurnsAsHistory: deps.preserveQueuedTurnsAsHistory, }); if (!turn) { if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn(); return; } if (endPlan.shouldClearPreview) { await deps.clearPreview(turn.chatId); } if (endPlan.shouldSendErrorMessage) { await deps.sendTextReply( turn.chatId, turn.replyToMessageId, assistant.errorMessage || "Telegram bridge: pi failed while processing the request.", { threadId: turn.threadId }, ); if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn(); return; } if (finalText) deps.setPreviewPendingText(finalText); if (!finalText && hasOutboundArtifacts) await deps.clearPreview(turn.chatId); if (endPlan.kind === "text" && finalText) { const finalized = await deps.finalizeMarkdownPreview( turn.chatId, finalText, turn.replyToMessageId, { replyMarkup, threadId: turn.threadId }, ); if (!finalized) { await deps.clearPreview(turn.chatId); await deps.sendMarkdownReply( turn.chatId, turn.replyToMessageId, finalText, { replyMarkup, threadId: turn.threadId }, ); } } if (outboundReply && deps.sendOutboundReplyArtifacts) { await deps.sendOutboundReplyArtifacts(turn, outboundReply, { replyToPrompt: !finalText, }); } if (endPlan.shouldSendAttachmentNotice) { await deps.sendTextReply( turn.chatId, turn.replyToMessageId, "Attached requested file(s).", { threadId: turn.threadId }, ); } await deps.sendQueuedAttachments(turn); if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn(); } // --- Session Runtime --- export interface TelegramSessionStartState { currentTelegramModel: TModel | undefined; activeTelegramToolExecutions: number; pendingTelegramModelSwitch: undefined; nextQueuedTelegramItemOrder: number; nextQueuedTelegramControlOrder: number; telegramTurnDispatchPending: boolean; compactionInProgress: boolean; } export interface TelegramSessionShutdownState { queuedTelegramItems: TQueueItem[]; nextQueuedTelegramItemOrder: number; nextQueuedTelegramControlOrder: number; nextPriorityReactionOrder: number; currentTelegramModel: undefined; activeTelegramToolExecutions: number; pendingTelegramModelSwitch: undefined; telegramTurnDispatchPending: boolean; compactionInProgress: boolean; preserveQueuedTurnsAsHistory: boolean; } export interface TelegramSessionRuntimeCounterState { nextQueuedTelegramItemOrder?: number; nextQueuedTelegramControlOrder?: number; nextPriorityReactionOrder?: number; } export interface TelegramSessionRuntimeFlagState { activeTelegramToolExecutions?: number; telegramTurnDispatchPending?: boolean; compactionInProgress?: boolean; preserveQueuedTurnsAsHistory?: boolean; } export interface TelegramSessionStateApplierDeps { setQueuedItems: (items: TQueueItem[]) => void; setCurrentModel: (model: TModel | undefined) => void; setPendingModelSwitch: (selection: undefined) => void; syncCounters: (state: TelegramSessionRuntimeCounterState) => void; syncFlags: (state: TelegramSessionRuntimeFlagState) => void; } export interface TelegramSessionStateApplier { applyStartState: (state: TelegramSessionStartState) => void; applyShutdownState: (state: TelegramSessionShutdownState) => void; } export interface TelegramSessionStartRuntimeDeps { ctx: TContext; currentModel: TModel | undefined; loadConfig: () => Promise; applyState: (state: TelegramSessionStartState) => void; bindDeferredDispatchContext?: (ctx: TContext) => void; prepareTempDir: () => Promise; updateStatus: () => void; } export interface TelegramSessionShutdownRuntimeDeps { unbindDeferredDispatchContext?: () => void; applyState: (state: TelegramSessionShutdownState) => void; clearPendingMediaGroups: () => void; clearModelMenuState: () => void; getActiveTurnChatId: () => number | undefined; clearPreview: (chatId: number) => Promise; clearActiveTurn: () => void; clearAbort: () => void; stopPolling: () => Promise; } export interface TelegramSessionLifecycleHookRuntimeDeps< TContext, TQueueItem, TModel = unknown, > extends TelegramRuntimeEventRecorderPort { getCurrentModel: (ctx: TContext) => TModel | undefined; loadConfig: () => Promise; applySessionStartState: (state: TelegramSessionStartState) => void; bindDeferredDispatchContext?: (ctx: TContext) => void; prepareTempDir: () => Promise; updateStatus: (ctx: TContext) => void; unbindDeferredDispatchContext?: () => void; applySessionShutdownState: ( state: TelegramSessionShutdownState, ) => void; clearPendingMediaGroups: () => void; clearModelMenuState: () => void; getActiveTurnChatId: () => number | undefined; clearPreview: (chatId: number) => Promise; clearActiveTurn: () => void; clearAbort: () => void; stopPolling: () => Promise; } export type TelegramSessionLifecycleHookEvent = unknown; export function createTelegramSessionStateApplier( deps: TelegramSessionStateApplierDeps, ): TelegramSessionStateApplier { return { applyStartState: (state) => { deps.setCurrentModel(state.currentTelegramModel); deps.setPendingModelSwitch(state.pendingTelegramModelSwitch); deps.syncCounters(state); deps.syncFlags(state); }, applyShutdownState: (state) => { deps.setQueuedItems(state.queuedTelegramItems); deps.syncCounters(state); deps.syncFlags(state); deps.setCurrentModel(state.currentTelegramModel); deps.setPendingModelSwitch(state.pendingTelegramModelSwitch); }, }; } export interface TelegramQueueMutationRuntimeDeps< TContext, > extends TelegramQueueStore { ctx: TContext; getNextPriorityReactionOrder?: () => number; incrementNextPriorityReactionOrder?: () => void; updateStatus: (ctx: TContext) => void; } export interface TelegramQueueMutationControllerDeps< TContext, > extends TelegramQueueStore { getNextPriorityReactionOrder?: () => number; incrementNextPriorityReactionOrder?: () => void; updateStatus: (ctx: TContext) => void; } export interface TelegramQueueMutationController { append: (item: TelegramQueueItem, ctx: TContext) => void; reorder: (ctx: TContext) => void; clear: (ctx: TContext) => number; removeByMessageIds: (messageIds: number[], ctx: TContext) => number; clearPriorityByMessageId: (messageId: number, ctx: TContext) => boolean; prioritizeByMessageId: (messageId: number, ctx: TContext) => boolean; } export interface TelegramControlQueueControllerDeps { appendControlItem: ( item: PendingTelegramControlItem, ctx: TContext, ) => void; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; } export interface TelegramControlQueueController { enqueue: (item: PendingTelegramControlItem, ctx: TContext) => void; } export interface TelegramPromptEnqueueRuntimeDeps< TMessage, TContext = unknown, > extends TelegramQueueStore { getPreserveQueuedTurnsAsHistory: () => boolean; setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void; createTurn: ( messages: TMessage[], historyTurns: PendingTelegramTurn[], ) => Promise; updateStatus: () => void; dispatchNextQueuedTelegramTurn: () => void; } export interface TelegramPromptEnqueueControllerDeps< TMessage, TContext = unknown, > extends TelegramQueueStore { getPreserveQueuedTurnsAsHistory: () => boolean; setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void; createTurn: ( messages: TMessage[], historyTurns: PendingTelegramTurn[], ctx: TContext, ) => Promise; updateStatus: (ctx: TContext) => void; dispatchNextQueuedTelegramTurn: (ctx: TContext) => void; } export interface TelegramPromptEnqueueController { enqueue: (messages: TMessage[], ctx: TContext) => Promise; } export function buildTelegramSessionStartState( currentModel: TModel | undefined, ): TelegramSessionStartState { return { currentTelegramModel: currentModel, activeTelegramToolExecutions: 0, pendingTelegramModelSwitch: undefined, nextQueuedTelegramItemOrder: 0, nextQueuedTelegramControlOrder: 0, telegramTurnDispatchPending: false, compactionInProgress: false, }; } export function buildTelegramSessionShutdownState< TQueueItem, >(): TelegramSessionShutdownState { return { queuedTelegramItems: [], nextQueuedTelegramItemOrder: 0, nextQueuedTelegramControlOrder: 0, nextPriorityReactionOrder: 0, currentTelegramModel: undefined, activeTelegramToolExecutions: 0, pendingTelegramModelSwitch: undefined, telegramTurnDispatchPending: false, compactionInProgress: false, preserveQueuedTurnsAsHistory: false, }; } export async function startTelegramSessionRuntime( deps: TelegramSessionStartRuntimeDeps, ): Promise { await deps.loadConfig(); deps.applyState(buildTelegramSessionStartState(deps.currentModel)); await deps.prepareTempDir(); deps.bindDeferredDispatchContext?.(deps.ctx); deps.updateStatus(); } export async function shutdownTelegramSessionRuntime( deps: TelegramSessionShutdownRuntimeDeps, ): Promise { deps.unbindDeferredDispatchContext?.(); deps.applyState(buildTelegramSessionShutdownState()); deps.clearPendingMediaGroups(); deps.clearModelMenuState(); const activeTurnChatId = deps.getActiveTurnChatId(); if (activeTurnChatId !== undefined) { await deps.clearPreview(activeTurnChatId); } deps.clearActiveTurn(); deps.clearAbort(); await deps.stopPolling(); } export type TelegramSessionLifecycleRuntimeDeps< TContext, TQueueItem, TModel = unknown, > = Omit< TelegramSessionLifecycleHookRuntimeDeps, "applySessionStartState" | "applySessionShutdownState" > & TelegramSessionStateApplierDeps; export function createTelegramSessionLifecycleRuntime< TContext, TQueueItem, TModel = unknown, >(deps: TelegramSessionLifecycleRuntimeDeps) { const stateApplier = createTelegramSessionStateApplier({ setQueuedItems: deps.setQueuedItems, setCurrentModel: deps.setCurrentModel, setPendingModelSwitch: deps.setPendingModelSwitch, syncCounters: deps.syncCounters, syncFlags: deps.syncFlags, }); return createTelegramSessionLifecycleHooks({ getCurrentModel: deps.getCurrentModel, loadConfig: deps.loadConfig, applySessionStartState: stateApplier.applyStartState, bindDeferredDispatchContext: deps.bindDeferredDispatchContext, prepareTempDir: deps.prepareTempDir, updateStatus: deps.updateStatus, unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext, applySessionShutdownState: stateApplier.applyShutdownState, clearPendingMediaGroups: deps.clearPendingMediaGroups, clearModelMenuState: deps.clearModelMenuState, getActiveTurnChatId: deps.getActiveTurnChatId, clearPreview: deps.clearPreview, clearActiveTurn: deps.clearActiveTurn, clearAbort: deps.clearAbort, stopPolling: deps.stopPolling, recordRuntimeEvent: deps.recordRuntimeEvent, }); } export function createTelegramSessionLifecycleHooks< TContext, TQueueItem, TModel = unknown, >(deps: TelegramSessionLifecycleHookRuntimeDeps) { return { onSessionStart: async ( _event: TelegramSessionLifecycleHookEvent, ctx: TContext, ): Promise => { try { await startTelegramSessionRuntime({ ctx, currentModel: deps.getCurrentModel(ctx), loadConfig: deps.loadConfig, applyState: deps.applySessionStartState, bindDeferredDispatchContext: deps.bindDeferredDispatchContext, prepareTempDir: deps.prepareTempDir, updateStatus: () => deps.updateStatus(ctx), }); } catch (error) { deps.recordRuntimeEvent?.("session", error, { phase: "start" }); throw error; } }, onSessionShutdown: async (): Promise => { try { await shutdownTelegramSessionRuntime({ unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext, applyState: deps.applySessionShutdownState, clearPendingMediaGroups: deps.clearPendingMediaGroups, clearModelMenuState: deps.clearModelMenuState, getActiveTurnChatId: deps.getActiveTurnChatId, clearPreview: deps.clearPreview, clearActiveTurn: deps.clearActiveTurn, clearAbort: deps.clearAbort, stopPolling: deps.stopPolling, }); } catch (error) { deps.recordRuntimeEvent?.("session", error, { phase: "shutdown" }); throw error; } }, }; } export function createTelegramQueueMutationController( deps: TelegramQueueMutationControllerDeps, ): TelegramQueueMutationController { const buildRuntimeDeps = ( ctx: TContext, ): TelegramQueueMutationRuntimeDeps => ({ ...deps, ctx, }); return { append: (item, ctx) => appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)), reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)), clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)), removeByMessageIds: (messageIds, ctx) => removeTelegramQueueItemsByMessageIdsRuntime( messageIds, buildRuntimeDeps(ctx), ), clearPriorityByMessageId: (messageId, ctx) => clearTelegramQueuePromptPriorityRuntime(messageId, buildRuntimeDeps(ctx)), prioritizeByMessageId: (messageId, ctx) => prioritizeTelegramQueuePromptRuntime(messageId, buildRuntimeDeps(ctx)), }; } function appendTelegramQueueItemRuntime( item: TelegramQueueItem, deps: TelegramQueueMutationRuntimeDeps, ): void { deps.setQueuedItems(appendTelegramQueueItem(deps.getQueuedItems(), item)); reorderTelegramQueueItemsRuntime(deps); } export function reorderTelegramQueueItemsRuntime( deps: TelegramQueueMutationRuntimeDeps, ): void { deps.setQueuedItems( [...deps.getQueuedItems()].sort(compareTelegramQueueItems), ); deps.updateStatus(deps.ctx); } export function clearTelegramQueueItemsRuntime( deps: TelegramQueueMutationRuntimeDeps, ): number { const removedCount = deps.getQueuedItems().length; if (removedCount === 0) return 0; deps.setQueuedItems([]); deps.updateStatus(deps.ctx); return removedCount; } export function removeTelegramQueueItemsByMessageIdsRuntime( messageIds: number[], deps: TelegramQueueMutationRuntimeDeps, ): number { const { items, removedCount } = removeTelegramQueueItemsByMessageIds( deps.getQueuedItems(), messageIds, ); if (removedCount === 0) return 0; deps.setQueuedItems(items); deps.updateStatus(deps.ctx); return removedCount; } export function clearTelegramQueuePromptPriorityRuntime( messageId: number, deps: TelegramQueueMutationRuntimeDeps, ): boolean { const { changed, items } = clearTelegramQueuePromptPriority( deps.getQueuedItems(), messageId, ); if (!changed) return false; deps.setQueuedItems(items); reorderTelegramQueueItemsRuntime(deps); return true; } export function prioritizeTelegramQueuePromptRuntime( messageId: number, deps: TelegramQueueMutationRuntimeDeps, ): boolean { const nextPriorityReactionOrder = deps.getNextPriorityReactionOrder?.(); if (nextPriorityReactionOrder === undefined) return false; const { changed, items } = prioritizeTelegramQueuePrompt( deps.getQueuedItems(), messageId, nextPriorityReactionOrder, ); if (!changed) return false; deps.setQueuedItems(items); deps.incrementNextPriorityReactionOrder?.(); reorderTelegramQueueItemsRuntime(deps); return true; } export async function enqueueTelegramPromptTurnRuntime< TMessage, TContext = unknown, >( messages: TMessage[], deps: TelegramPromptEnqueueRuntimeDeps, ): Promise { const enqueuePlan = planTelegramPromptEnqueue( deps.getQueuedItems(), deps.getPreserveQueuedTurnsAsHistory(), ); deps.setPreserveQueuedTurnsAsHistory(false); const turn = await deps.createTurn(messages, enqueuePlan.historyTurns); deps.setQueuedItems( appendTelegramQueueItem(enqueuePlan.remainingItems, turn), ); deps.updateStatus(); deps.dispatchNextQueuedTelegramTurn(); } export function createTelegramPromptEnqueueController< TMessage, TContext = unknown, >( deps: TelegramPromptEnqueueControllerDeps, ): TelegramPromptEnqueueController { return { enqueue: (messages, ctx) => enqueueTelegramPromptTurnRuntime(messages, { ...deps, createTurn: (nextMessages, historyTurns) => deps.createTurn(nextMessages, historyTurns, ctx), updateStatus: () => deps.updateStatus(ctx), dispatchNextQueuedTelegramTurn: () => deps.dispatchNextQueuedTelegramTurn(ctx), }), }; } export function createTelegramControlQueueController( deps: TelegramControlQueueControllerDeps, ): TelegramControlQueueController { return { enqueue: (item, ctx) => { deps.appendControlItem(item, ctx); deps.dispatchNextQueuedTelegramTurn(ctx); }, }; } // --- Control Runtime --- function getTelegramQueueErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } export interface TelegramRuntimeEventRecorderPort { recordRuntimeEvent?: ( category: string, error: unknown, details?: Record, ) => void; } export interface TelegramControlRuntimeDeps< TContext, > extends TelegramRuntimeEventRecorderPort { ctx: TContext; sendTextReply: ( chatId: number, replyToMessageId: number, text: string, options?: { threadId?: number }, ) => Promise; onSettled: () => void; } export async function executeTelegramControlItemRuntime( item: PendingTelegramControlItem, deps: TelegramControlRuntimeDeps, ): Promise { try { await item.execute(deps.ctx); } catch (error) { const message = getTelegramQueueErrorMessage(error); deps.recordRuntimeEvent?.("control", error, { controlType: item.controlType, chatId: item.chatId, replyToMessageId: item.replyToMessageId, }); await deps.sendTextReply( item.chatId, item.replyToMessageId, `Telegram control action failed: ${message}`, ); } finally { deps.onSettled(); } } // --- Deferred Dispatch Runtime --- export interface TelegramDeferredQueueDispatchRuntimeDeps extends TelegramRuntimeEventRecorderPort { delayMs?: number; setTimer?: ( callback: () => void, ms: number, ) => ReturnType; clearTimer?: (timer: ReturnType) => void; } export interface TelegramDeferredQueueDispatchRuntime { bind: (ctx: TContext) => void; unbind: () => void; isBound: () => boolean; request: (dispatchNextQueuedTelegramTurn: (ctx: TContext) => void) => void; } export function createTelegramDeferredQueueDispatchRuntime( deps: TelegramDeferredQueueDispatchRuntimeDeps = {}, ): TelegramDeferredQueueDispatchRuntime { let boundContext: TContext | undefined; let generation = 0; const timers = new Set>(); const delayMs = deps.delayMs ?? 0; const setTimer = deps.setTimer ?? ((callback: () => void, ms: number): ReturnType => setTimeout(callback, ms)); const clearTimer = deps.clearTimer ?? ((timer: ReturnType): void => clearTimeout(timer)); const clearTimers = (): void => { for (const timer of timers) clearTimer(timer); timers.clear(); }; return { bind: (ctx) => { boundContext = ctx; generation += 1; }, unbind: () => { boundContext = undefined; generation += 1; clearTimers(); }, isBound: () => boundContext !== undefined, request: (dispatchNextQueuedTelegramTurn) => { if (boundContext === undefined) return; const scheduledGeneration = generation; let timer: ReturnType; timer = setTimer(() => { timers.delete(timer); if (generation !== scheduledGeneration || boundContext === undefined) return; dispatchNextQueuedTelegramTurn(boundContext); }, delayMs); timers.add(timer); }, }; } // --- Dispatch Runtime --- export interface TelegramDispatchRuntimeDeps { executeControlItem: ( item: Extract< TelegramQueueDispatchAction, { kind: "control" } >["item"], ) => void; onPromptDispatchStart: (chatId: number) => void; sendUserMessage: ( content: Extract< TelegramQueueDispatchAction, { kind: "prompt" } >["item"]["content"], ) => void; onPromptDispatchFailure: (message: string) => void; onIdle: () => void; } export interface TelegramQueueDispatchControllerDeps< TContext = unknown, > extends TelegramRuntimeEventRecorderPort { getQueuedItems: () => TelegramQueueItem[]; setQueuedItems: (items: TelegramQueueItem[]) => void; canDispatch: (ctx: TContext) => boolean; hasDispatchContext?: () => boolean; updateStatus: (ctx: TContext, error?: string) => void; sendTextReply: TelegramControlRuntimeDeps["sendTextReply"]; onPromptDispatchStart: (ctx: TContext, chatId: number) => void; sendUserMessage: TelegramDispatchRuntimeDeps["sendUserMessage"]; onPromptDispatchFailure: (ctx: TContext, message: string) => void; } export interface TelegramQueueDispatchController { dispatchNext: (ctx: TContext) => void; } export function executeTelegramQueueDispatchPlan( plan: TelegramQueueDispatchAction, deps: TelegramDispatchRuntimeDeps, ): void { if (plan.kind === "none") { deps.onIdle(); return; } if (plan.kind === "control") { deps.executeControlItem(plan.item); return; } deps.onPromptDispatchStart(plan.item.chatId); try { deps.sendUserMessage(plan.item.content); } catch (error) { const message = getTelegramQueueErrorMessage(error); deps.onPromptDispatchFailure(message); } } export type TelegramQueueDispatchRuntimeDeps = Omit< TelegramQueueDispatchControllerDeps, "canDispatch" > & TelegramDispatchReadinessDeps; export function createTelegramQueueDispatchRuntime( deps: TelegramQueueDispatchRuntimeDeps, ): TelegramQueueDispatchController { return createTelegramQueueDispatchController({ getQueuedItems: deps.getQueuedItems, setQueuedItems: deps.setQueuedItems, canDispatch: createTelegramDispatchReadinessChecker({ isCompactionInProgress: deps.isCompactionInProgress, hasActiveTurn: deps.hasActiveTurn, hasDispatchPending: deps.hasDispatchPending, isIdle: deps.isIdle, hasPendingMessages: deps.hasPendingMessages, }), hasDispatchContext: deps.hasDispatchContext, updateStatus: deps.updateStatus, sendTextReply: deps.sendTextReply, onPromptDispatchStart: deps.onPromptDispatchStart, sendUserMessage: deps.sendUserMessage, onPromptDispatchFailure: deps.onPromptDispatchFailure, recordRuntimeEvent: deps.recordRuntimeEvent, }); } export function createTelegramQueueDispatchController( deps: TelegramQueueDispatchControllerDeps, ): TelegramQueueDispatchController { let controlDispatchPending = false; const controller: TelegramQueueDispatchController = { dispatchNext: (ctx) => { if (deps.hasDispatchContext && !deps.hasDispatchContext()) return; if (controlDispatchPending) { deps.updateStatus(ctx); return; } const dispatchPlan = planNextTelegramQueueAction( deps.getQueuedItems(), deps.canDispatch(ctx), ); if (dispatchPlan.kind !== "none") { deps.setQueuedItems(dispatchPlan.remainingItems); } executeTelegramQueueDispatchPlan(dispatchPlan, { executeControlItem: (item) => { controlDispatchPending = true; deps.updateStatus(ctx); void executeTelegramControlItemRuntime(item, { ctx, sendTextReply: deps.sendTextReply, recordRuntimeEvent: deps.recordRuntimeEvent, onSettled: () => { controlDispatchPending = false; if (deps.hasDispatchContext && !deps.hasDispatchContext()) return; deps.updateStatus(ctx); controller.dispatchNext(ctx); }, }); }, onPromptDispatchStart: (chatId) => { deps.onPromptDispatchStart(ctx, chatId); }, sendUserMessage: deps.sendUserMessage, onPromptDispatchFailure: (message) => { deps.onPromptDispatchFailure(ctx, message); }, onIdle: () => { deps.updateStatus(ctx); }, }); }, }; return controller; }