@llblab/pi-telegram 0.13.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,9 +53,28 @@ export interface TelegramOutboundAttachmentToolRegistrationDeps extends Telegram
53
53
  maxAttachmentsPerTurn?: number;
54
54
  maxAttachmentSizeBytes?: number;
55
55
  getActiveTurn: () => TelegramOutboundAttachmentQueueTargetView | undefined;
56
+ getDefaultChatId?: () => number | undefined;
57
+ canSendDirect?: () => boolean;
58
+ sendMultipart?: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
56
59
  statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
57
60
  }
58
61
 
62
+ export interface TelegramOutboundMessagePlan {
63
+ markdown: string;
64
+ replyMarkup?: unknown;
65
+ }
66
+
67
+ export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
68
+ getDefaultChatId: () => number | undefined;
69
+ canSendDirect: () => boolean;
70
+ planMessage: (markdown: string) => TelegramOutboundMessagePlan;
71
+ sendMarkdownMessage: (
72
+ chatId: number,
73
+ markdown: string,
74
+ options?: { replyMarkup?: unknown },
75
+ ) => Promise<number | undefined>;
76
+ }
77
+
59
78
  export interface TelegramQueuedOutboundAttachmentView {
60
79
  path: string;
61
80
  fileName: string;
@@ -90,9 +109,71 @@ function formatTelegramOutboundAttachmentSizeLimitError(
90
109
  return path ? `${message}: ${path}` : message;
91
110
  }
92
111
 
93
- function formatTelegramOutboundAttachmentToolResultText(count: number): string {
112
+ function formatTelegramOutboundAttachmentToolResultText(
113
+ count: number,
114
+ mode: "queued" | "sent" = "queued",
115
+ ): string {
94
116
  // Pi's compact tool rows need an empty first line to visually separate header and result
95
- return ["", `Queued ${count} Telegram attachment(s).`].join("\n");
117
+ const verb = mode === "queued" ? "Queued" : "Sent";
118
+ return ["", `${verb} ${count} Telegram attachment(s).`].join("\n");
119
+ }
120
+
121
+ function formatTelegramOutboundMessageToolResultText(chatId: number): string {
122
+ return ["", `Sent Telegram message to ${chatId}.`].join("\n");
123
+ }
124
+
125
+ function assertTelegramDirectDeliveryAllowed(
126
+ canSendDirect: (() => boolean) | undefined,
127
+ ): void {
128
+ if (canSendDirect?.()) return;
129
+ throw new Error(
130
+ "Telegram direct delivery requires this π instance to own /telegram-connect",
131
+ );
132
+ }
133
+
134
+ function resolveTelegramOutboundChatId(options: {
135
+ chatId?: number;
136
+ getDefaultChatId?: () => number | undefined;
137
+ }): number {
138
+ const chatId = options.chatId ?? options.getDefaultChatId?.();
139
+ if (chatId === undefined) {
140
+ throw new Error(
141
+ "Telegram chat_id is required when no paired/default Telegram chat is available",
142
+ );
143
+ }
144
+ return chatId;
145
+ }
146
+
147
+ async function buildTelegramOutboundAttachmentViews(options: {
148
+ paths: string[];
149
+ maxAttachmentSizeBytes?: number;
150
+ statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
151
+ }): Promise<TelegramQueuedOutboundAttachmentView[]> {
152
+ const pendingAttachments: TelegramQueuedOutboundAttachmentView[] = [];
153
+ for (const inputPath of options.paths) {
154
+ const stats = await (options.statPath ?? stat)(inputPath);
155
+ if (!stats.isFile()) {
156
+ throw new Error(`Not a file: ${inputPath}`);
157
+ }
158
+ if (
159
+ options.maxAttachmentSizeBytes !== undefined &&
160
+ stats.size !== undefined &&
161
+ stats.size > options.maxAttachmentSizeBytes
162
+ ) {
163
+ throw new Error(
164
+ formatTelegramOutboundAttachmentSizeLimitError(
165
+ stats.size,
166
+ options.maxAttachmentSizeBytes,
167
+ inputPath,
168
+ ),
169
+ );
170
+ }
171
+ pendingAttachments.push({
172
+ path: inputPath,
173
+ fileName: basename(inputPath),
174
+ });
175
+ }
176
+ return pendingAttachments;
96
177
  }
97
178
 
98
179
  export function registerTelegramOutboundAttachmentTool(
@@ -107,24 +188,43 @@ export function registerTelegramOutboundAttachmentTool(
107
188
  name: "telegram_attach",
108
189
  label: "Telegram Attach",
109
190
  description:
110
- "Queue one or more local files to be sent with the next Telegram reply.",
111
- promptSnippet: "Queue local files to be sent with the next Telegram reply.",
191
+ "Queue one or more local files for the active Telegram reply, or send them immediately to Telegram when no Telegram turn is active.",
192
+ promptSnippet:
193
+ "Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.",
112
194
  promptGuidelines: [
113
195
  "When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
196
+ "When a local/TUI user explicitly asks to send a generated file to Telegram, telegram_attach can deliver it to the paired/default Telegram chat even without an active Telegram turn.",
114
197
  ],
115
198
  parameters: Type.Object({
116
199
  paths: Type.Array(
117
200
  Type.String({ description: "Local file path to attach" }),
118
201
  { minItems: 1, maxItems: maxAttachmentsPerTurn },
119
202
  ),
203
+ chat_id: Type.Optional(
204
+ Type.Number({
205
+ description:
206
+ "Optional Telegram chat id for immediate delivery when no Telegram turn is active",
207
+ }),
208
+ ),
209
+ caption: Type.Optional(
210
+ Type.String({
211
+ description:
212
+ "Optional caption for immediate delivery; ignored when queued for an active turn",
213
+ }),
214
+ ),
120
215
  }),
121
216
  async execute(_toolCallId, params) {
122
217
  try {
123
218
  return await queueTelegramOutboundAttachments({
124
219
  activeTurn: deps.getActiveTurn(),
125
220
  paths: params.paths,
221
+ chatId: params.chat_id,
222
+ caption: params.caption,
126
223
  maxAttachmentsPerTurn,
127
224
  maxAttachmentSizeBytes,
225
+ sendMultipart: deps.sendMultipart,
226
+ getDefaultChatId: deps.getDefaultChatId,
227
+ canSendDirect: deps.canSendDirect,
128
228
  statPath: deps.statPath,
129
229
  });
130
230
  } catch (error) {
@@ -138,6 +238,46 @@ export function registerTelegramOutboundAttachmentTool(
138
238
  });
139
239
  }
140
240
 
241
+ export function registerTelegramOutboundMessageTool(
242
+ pi: ExtensionAPI,
243
+ deps: TelegramOutboundMessageToolRegistrationDeps,
244
+ ): void {
245
+ pi.registerTool({
246
+ name: "telegram_message",
247
+ label: "Telegram Message",
248
+ description:
249
+ "Send a Markdown text message directly to the paired/default Telegram chat or an explicit chat_id. Hidden telegram_button comments in the text become attached inline prompt buttons.",
250
+ promptSnippet:
251
+ "Send direct Telegram Markdown text when the user explicitly asks for Telegram delivery outside the normal reply flow.",
252
+ promptGuidelines: [
253
+ "Use telegram_message only when the user explicitly asks to send a message to Telegram from the local/TUI side, or names a concrete Telegram delivery target.",
254
+ "Add buttons by embedding the same top-level telegram_button HTML comments used in normal Telegram replies; Telegram does not support standalone buttons.",
255
+ "Do not use this tool for ordinary Telegram-originated replies; answer normally so the bridge can deliver the active turn reply.",
256
+ ],
257
+ parameters: Type.Object({
258
+ text: Type.String({ description: "Message text to send" }),
259
+ chat_id: Type.Optional(
260
+ Type.Number({ description: "Optional Telegram chat id" }),
261
+ ),
262
+ }),
263
+ async execute(_toolCallId, params) {
264
+ try {
265
+ return await sendTelegramOutboundMessage({
266
+ text: params.text,
267
+ chatId: params.chat_id,
268
+ getDefaultChatId: deps.getDefaultChatId,
269
+ canSendDirect: deps.canSendDirect,
270
+ planMessage: deps.planMessage,
271
+ sendMarkdownMessage: deps.sendMarkdownMessage,
272
+ });
273
+ } catch (error) {
274
+ deps.recordRuntimeEvent?.("message", error, { phase: "direct" });
275
+ throw error;
276
+ }
277
+ },
278
+ });
279
+ }
280
+
141
281
  export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
142
282
  sendMultipart: (
143
283
  method: string,
@@ -163,14 +303,32 @@ export interface TelegramQueuedOutboundAttachmentDeliveryDeps {
163
303
  export async function queueTelegramOutboundAttachments(options: {
164
304
  activeTurn: TelegramOutboundAttachmentQueueTargetView | undefined;
165
305
  paths: string[];
306
+ chatId?: number;
307
+ caption?: string;
166
308
  maxAttachmentsPerTurn: number;
167
309
  maxAttachmentSizeBytes?: number;
310
+ sendMultipart?: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
311
+ getDefaultChatId?: () => number | undefined;
312
+ canSendDirect?: () => boolean;
168
313
  statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
169
314
  }): Promise<TelegramOutboundAttachmentToolResult> {
170
315
  if (!options.activeTurn) {
171
- throw new Error(
172
- "telegram_attach can only be used while replying to an active Telegram turn",
173
- );
316
+ if (!options.sendMultipart) {
317
+ throw new Error(
318
+ "telegram_attach can only queue files while replying to an active Telegram turn; provide Telegram send ports for immediate delivery",
319
+ );
320
+ }
321
+ return sendTelegramOutboundFiles({
322
+ paths: options.paths,
323
+ chatId: options.chatId,
324
+ caption: options.caption,
325
+ maxAttachmentsPerTurn: options.maxAttachmentsPerTurn,
326
+ maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
327
+ sendMultipart: options.sendMultipart,
328
+ getDefaultChatId: options.getDefaultChatId,
329
+ canSendDirect: options.canSendDirect,
330
+ statPath: options.statPath,
331
+ });
174
332
  }
175
333
  if (
176
334
  options.activeTurn.queuedAttachments.length + options.paths.length >
@@ -180,30 +338,11 @@ export async function queueTelegramOutboundAttachments(options: {
180
338
  `Attachment limit reached (${options.maxAttachmentsPerTurn})`,
181
339
  );
182
340
  }
183
- const pendingAttachments: TelegramQueuedOutboundAttachmentView[] = [];
184
- for (const inputPath of options.paths) {
185
- const stats = await (options.statPath ?? stat)(inputPath);
186
- if (!stats.isFile()) {
187
- throw new Error(`Not a file: ${inputPath}`);
188
- }
189
- if (
190
- options.maxAttachmentSizeBytes !== undefined &&
191
- stats.size !== undefined &&
192
- stats.size > options.maxAttachmentSizeBytes
193
- ) {
194
- throw new Error(
195
- formatTelegramOutboundAttachmentSizeLimitError(
196
- stats.size,
197
- options.maxAttachmentSizeBytes,
198
- inputPath,
199
- ),
200
- );
201
- }
202
- pendingAttachments.push({
203
- path: inputPath,
204
- fileName: basename(inputPath),
205
- });
206
- }
341
+ const pendingAttachments = await buildTelegramOutboundAttachmentViews({
342
+ paths: options.paths,
343
+ maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
344
+ statPath: options.statPath,
345
+ });
207
346
  options.activeTurn.queuedAttachments.push(...pendingAttachments);
208
347
  const added = pendingAttachments.map((attachment) => attachment.path);
209
348
  return {
@@ -217,6 +356,89 @@ export async function queueTelegramOutboundAttachments(options: {
217
356
  };
218
357
  }
219
358
 
359
+ export async function sendTelegramOutboundMessage(options: {
360
+ text: string;
361
+ chatId?: number;
362
+ getDefaultChatId?: () => number | undefined;
363
+ canSendDirect: () => boolean;
364
+ planMessage: (markdown: string) => TelegramOutboundMessagePlan;
365
+ sendMarkdownMessage: (
366
+ chatId: number,
367
+ markdown: string,
368
+ options?: { replyMarkup?: unknown },
369
+ ) => Promise<number | undefined>;
370
+ }): Promise<{
371
+ content: Array<{ type: "text"; text: string }>;
372
+ details: { chatId: number; messageId?: number };
373
+ }> {
374
+ assertTelegramDirectDeliveryAllowed(options.canSendDirect);
375
+ const chatId = resolveTelegramOutboundChatId({
376
+ chatId: options.chatId,
377
+ getDefaultChatId: options.getDefaultChatId,
378
+ });
379
+ const plan = options.planMessage(options.text);
380
+ const messageId = await options.sendMarkdownMessage(chatId, plan.markdown, {
381
+ replyMarkup: plan.replyMarkup,
382
+ });
383
+ return {
384
+ content: [{ type: "text", text: formatTelegramOutboundMessageToolResultText(chatId) }],
385
+ details: { chatId, messageId },
386
+ };
387
+ }
388
+
389
+ export async function sendTelegramOutboundFiles(options: {
390
+ paths: string[];
391
+ chatId?: number;
392
+ caption?: string;
393
+ maxAttachmentsPerTurn: number;
394
+ maxAttachmentSizeBytes?: number;
395
+ sendMultipart: TelegramQueuedOutboundAttachmentDeliveryDeps["sendMultipart"];
396
+ getDefaultChatId?: () => number | undefined;
397
+ canSendDirect?: () => boolean;
398
+ statPath?: (path: string) => Promise<{ isFile(): boolean; size?: number }>;
399
+ }): Promise<TelegramOutboundAttachmentToolResult & { details: { paths: string[]; chatId: number } }> {
400
+ assertTelegramDirectDeliveryAllowed(options.canSendDirect);
401
+ if (options.paths.length > options.maxAttachmentsPerTurn) {
402
+ throw new Error(
403
+ `Attachment limit reached (${options.maxAttachmentsPerTurn})`,
404
+ );
405
+ }
406
+ const chatId = resolveTelegramOutboundChatId({
407
+ chatId: options.chatId,
408
+ getDefaultChatId: options.getDefaultChatId,
409
+ });
410
+ const pendingAttachments = await buildTelegramOutboundAttachmentViews({
411
+ paths: options.paths,
412
+ maxAttachmentSizeBytes: options.maxAttachmentSizeBytes,
413
+ statPath: options.statPath,
414
+ });
415
+ for (const [index, attachment] of pendingAttachments.entries()) {
416
+ const isPhoto = isTelegramOutboundPhotoAttachmentPath(attachment.path);
417
+ const method = isPhoto ? "sendPhoto" : "sendDocument";
418
+ const fieldName = isPhoto ? "photo" : "document";
419
+ await options.sendMultipart(
420
+ method,
421
+ {
422
+ chat_id: String(chatId),
423
+ ...(options.caption && index === 0 ? { caption: options.caption } : {}),
424
+ },
425
+ fieldName,
426
+ attachment.path,
427
+ attachment.fileName,
428
+ );
429
+ }
430
+ const added = pendingAttachments.map((attachment) => attachment.path);
431
+ return {
432
+ content: [
433
+ {
434
+ type: "text",
435
+ text: formatTelegramOutboundAttachmentToolResultText(added.length, "sent"),
436
+ },
437
+ ],
438
+ details: { paths: added, chatId },
439
+ };
440
+ }
441
+
220
442
  export function createTelegramQueuedOutboundAttachmentSender(
221
443
  deps: TelegramQueuedOutboundAttachmentDeliveryDeps,
222
444
  ) {
package/lib/prompts.ts CHANGED
@@ -23,13 +23,13 @@ Telegram-visible output:
23
23
  - Telegram is often phone-width; keep tables, dense list items, and compact text blocks at or below 37 visible cells when possible.
24
24
  - Count display width, not raw characters: emoji and some glyphs are wide, so prefer shorter labels when unsure.
25
25
  - Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
26
- - For requested/generated files, call tool \`telegram_attach(local_path)\`; mentioning a local path in text does not send it.
26
+ - For requested/generated files, call \`telegram_attach(local_path)\`; during Telegram turns it attaches files to the active reply, and during explicit local/TUI Telegram-delivery requests it sends files directly to the paired/default chat or an explicit \`chat_id\`. If a local/TUI user explicitly asks to send a text message to Telegram, use \`telegram_message\` with Markdown text; embed the same top-level \`telegram_button\` comments when inline prompt buttons are needed, because Telegram buttons must belong to a message. Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
27
27
 
28
28
  Native outbound actions:
29
- - Use top-level column-zero hidden Markdown comments outside code, quotes, and lists; the bridge handles them after agent_end, so do not call or register transport/TTS/text-to-OGG tools.
29
+ - Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
30
30
  - \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line voice, or \`<!-- telegram_voice: Short summary -->\` for one-line voice with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
31
- - \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts.
32
- - If only hidden action comments would remain, add visible parent text like "Choose one:".
31
+ - \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
32
+ - If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.
33
33
  `;
34
34
 
35
35
  export function buildTelegramBridgeSystemPrompt(options: {
package/lib/queue.ts CHANGED
@@ -242,12 +242,12 @@ export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
242
242
 
243
243
  export function planTelegramPromptEnqueue<TContext = unknown>(
244
244
  items: TelegramQueueItem<TContext>[],
245
- preserveQueuedTurnsAsHistory: boolean,
245
+ foldQueuedPromptsIntoHistory: boolean,
246
246
  ): {
247
247
  historyTurns: PendingTelegramTurn[];
248
248
  remainingItems: TelegramQueueItem<TContext>[];
249
249
  } {
250
- if (!preserveQueuedTurnsAsHistory) {
250
+ if (!foldQueuedPromptsIntoHistory) {
251
251
  return { historyTurns: [], remainingItems: items };
252
252
  }
253
253
  return partitionTelegramQueueItemsForHistory(items);
@@ -512,11 +512,11 @@ export function planNextTelegramQueueAction<TContext = unknown>(
512
512
  export function shouldDispatchAfterTelegramAgentEnd(options: {
513
513
  hasTurn: boolean;
514
514
  stopReason?: string;
515
- preserveQueuedTurnsAsHistory: boolean;
515
+ foldQueuedPromptsIntoHistory: boolean;
516
516
  }): boolean {
517
517
  if (!options.hasTurn) return true;
518
518
  if (options.stopReason === "aborted") {
519
- return !options.preserveQueuedTurnsAsHistory;
519
+ return !options.foldQueuedPromptsIntoHistory;
520
520
  }
521
521
  return true;
522
522
  }
@@ -529,6 +529,7 @@ export interface TelegramAgentStartPlan<TContext = unknown> {
529
529
  shouldResetPendingModelSwitch: boolean;
530
530
  shouldResetToolExecutions: boolean;
531
531
  shouldClearDispatchPending: boolean;
532
+ shouldClearAbortHistory: boolean;
532
533
  }
533
534
 
534
535
  export interface TelegramAgentStartRuntimeDeps<
@@ -542,6 +543,7 @@ export interface TelegramAgentStartRuntimeDeps<
542
543
  resetPendingModelSwitch: () => void;
543
544
  setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
544
545
  clearDispatchPending: () => void;
546
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
545
547
  setActiveTurn: (turn: TTurn) => void;
546
548
  createPreviewState: () => void;
547
549
  startTypingLoop: () => void;
@@ -560,6 +562,7 @@ export interface TelegramAgentStartHookRuntimeDeps<
560
562
  resetPendingModelSwitch: () => void;
561
563
  setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
562
564
  clearDispatchPending: () => void;
565
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
563
566
  setActiveTurn: (turn: TTurn) => void;
564
567
  createPreviewState: () => void;
565
568
  startTypingLoop: (ctx: TContext) => void;
@@ -598,6 +601,8 @@ export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
598
601
  shouldResetPendingModelSwitch: true,
599
602
  shouldResetToolExecutions: true,
600
603
  shouldClearDispatchPending: options.hasPendingDispatch,
604
+ shouldClearAbortHistory:
605
+ !options.hasActiveTurn && !options.hasPendingDispatch,
601
606
  };
602
607
  }
603
608
  const nextDispatch = consumeDispatchedTelegramPrompt(
@@ -610,6 +615,7 @@ export function buildTelegramAgentStartPlan<TContext = unknown>(options: {
610
615
  shouldResetPendingModelSwitch: true,
611
616
  shouldResetToolExecutions: true,
612
617
  shouldClearDispatchPending: options.hasPendingDispatch,
618
+ shouldClearAbortHistory: false,
613
619
  };
614
620
  }
615
621
 
@@ -624,6 +630,9 @@ export function handleTelegramAgentStartRuntime<
624
630
  });
625
631
  if (startPlan.shouldResetToolExecutions) deps.resetToolExecutions();
626
632
  if (startPlan.shouldResetPendingModelSwitch) deps.resetPendingModelSwitch();
633
+ if (startPlan.shouldClearAbortHistory) {
634
+ deps.setFoldQueuedPromptsIntoHistory(false);
635
+ }
627
636
  deps.setQueuedItems(startPlan.remainingItems);
628
637
  if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending();
629
638
  if (startPlan.activeTurn) {
@@ -651,6 +660,7 @@ export function createTelegramAgentStartHook<
651
660
  resetPendingModelSwitch: deps.resetPendingModelSwitch,
652
661
  setQueuedItems: deps.setQueuedItems,
653
662
  clearDispatchPending: deps.clearDispatchPending,
663
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
654
664
  setActiveTurn: deps.setActiveTurn,
655
665
  createPreviewState: deps.createPreviewState,
656
666
  startTypingLoop: () => deps.startTypingLoop(ctx),
@@ -791,10 +801,10 @@ export interface TelegramAgentEndRuntimeDeps<
791
801
  > {
792
802
  turn: TTurn | undefined;
793
803
  assistant: TelegramAgentEndAssistantResult;
794
- preserveQueuedTurnsAsHistory: boolean;
804
+ foldQueuedPromptsIntoHistory: boolean;
795
805
  resetRuntimeState: () => void;
806
+ waitForTypingIdle?: () => Promise<void>;
796
807
  updateStatus: () => void;
797
- isCurrentOwner?: () => boolean;
798
808
  dispatchNextQueuedTelegramTurn: () => void;
799
809
  clearPreview: (chatId: number) => Promise<void>;
800
810
  setPreviewPendingText: (text: string) => void;
@@ -849,10 +859,10 @@ export interface TelegramAgentEndHookRuntimeDeps<
849
859
  extractAssistant: (
850
860
  messages: readonly TMessage[],
851
861
  ) => TelegramAgentEndAssistantResult;
852
- getPreserveQueuedTurnsAsHistory: () => boolean;
862
+ getFoldQueuedPromptsIntoHistory: () => boolean;
853
863
  resetRuntimeState: () => void;
864
+ waitForTypingIdle?: () => Promise<void>;
854
865
  updateStatus: (ctx: TContext) => void;
855
- isCurrentOwner?: (ctx: TContext) => boolean;
856
866
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
857
867
  requestDeferredDispatchNextQueuedTelegramTurn: (
858
868
  dispatch: (ctx: TContext) => void,
@@ -890,12 +900,12 @@ export function buildTelegramAgentEndPlan(options: {
890
900
  stopReason?: string;
891
901
  hasFinalText: boolean;
892
902
  hasQueuedAttachments: boolean;
893
- preserveQueuedTurnsAsHistory: boolean;
903
+ foldQueuedPromptsIntoHistory: boolean;
894
904
  }): TelegramAgentEndPlan {
895
905
  const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
896
906
  hasTurn: options.hasTurn,
897
907
  stopReason: options.stopReason,
898
- preserveQueuedTurnsAsHistory: options.preserveQueuedTurnsAsHistory,
908
+ foldQueuedPromptsIntoHistory: options.foldQueuedPromptsIntoHistory,
899
909
  });
900
910
  if (!options.hasTurn) {
901
911
  return {
@@ -974,12 +984,10 @@ export function createTelegramAgentEndHook<
974
984
  turn,
975
985
  assistant:
976
986
  turn || proactiveEnabled ? deps.extractAssistant(event.messages) : {},
977
- preserveQueuedTurnsAsHistory: deps.getPreserveQueuedTurnsAsHistory(),
987
+ foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
978
988
  resetRuntimeState: deps.resetRuntimeState,
989
+ waitForTypingIdle: deps.waitForTypingIdle,
979
990
  updateStatus: () => deps.updateStatus(ctx),
980
- isCurrentOwner: deps.isCurrentOwner
981
- ? () => deps.isCurrentOwner?.(ctx) ?? false
982
- : undefined,
983
991
  dispatchNextQueuedTelegramTurn: () => {
984
992
  deps.requestDeferredDispatchNextQueuedTelegramTurn(
985
993
  deps.dispatchNextQueuedTelegramTurn,
@@ -1039,17 +1047,14 @@ export async function handleTelegramAgentEndRuntime<
1039
1047
  !!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
1040
1048
  const replyMarkup = outboundReply?.replyMarkup;
1041
1049
  deps.resetRuntimeState();
1050
+ await deps.waitForTypingIdle?.();
1042
1051
  deps.updateStatus();
1043
- if (deps.isCurrentOwner && !deps.isCurrentOwner()) {
1044
- if (turn) await deps.clearPreview(turn.chatId);
1045
- return;
1046
- }
1047
1052
  const endPlan = buildTelegramAgentEndPlan({
1048
1053
  hasTurn: !!turn,
1049
1054
  stopReason: assistant.stopReason,
1050
1055
  hasFinalText: !!finalText || hasOutboundArtifacts,
1051
1056
  hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
1052
- preserveQueuedTurnsAsHistory: deps.preserveQueuedTurnsAsHistory,
1057
+ foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
1053
1058
  });
1054
1059
  if (!turn) {
1055
1060
  if (
@@ -1072,10 +1077,6 @@ export async function handleTelegramAgentEndRuntime<
1072
1077
  return;
1073
1078
  }
1074
1079
  if (turn.guestQueryId) {
1075
- if (deps.isCurrentOwner && !deps.isCurrentOwner()) {
1076
- if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1077
- return;
1078
- }
1079
1080
  if (assistant.errorMessage) {
1080
1081
  await deps.answerGuestQuery?.(
1081
1082
  turn.guestQueryId,
@@ -1199,7 +1200,7 @@ export interface TelegramSessionShutdownState<TQueueItem> {
1199
1200
  pendingTelegramModelSwitch: undefined;
1200
1201
  telegramTurnDispatchPending: boolean;
1201
1202
  compactionInProgress: boolean;
1202
- preserveQueuedTurnsAsHistory: boolean;
1203
+ foldQueuedPromptsIntoHistory: boolean;
1203
1204
  }
1204
1205
 
1205
1206
  export interface TelegramSessionRuntimeCounterState {
@@ -1212,7 +1213,7 @@ export interface TelegramSessionRuntimeFlagState {
1212
1213
  activeTelegramToolExecutions?: number;
1213
1214
  telegramTurnDispatchPending?: boolean;
1214
1215
  compactionInProgress?: boolean;
1215
- preserveQueuedTurnsAsHistory?: boolean;
1216
+ foldQueuedPromptsIntoHistory?: boolean;
1216
1217
  }
1217
1218
 
1218
1219
  export interface TelegramSessionStateApplierDeps<TQueueItem, TModel> {
@@ -1342,8 +1343,8 @@ export interface TelegramPromptEnqueueRuntimeDeps<
1342
1343
  TMessage,
1343
1344
  TContext = unknown,
1344
1345
  > extends TelegramQueueStore<TContext> {
1345
- getPreserveQueuedTurnsAsHistory: () => boolean;
1346
- setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
1346
+ getFoldQueuedPromptsIntoHistory: () => boolean;
1347
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1347
1348
  createTurn: (
1348
1349
  messages: TMessage[],
1349
1350
  historyTurns: PendingTelegramTurn[],
@@ -1356,8 +1357,8 @@ export interface TelegramPromptEnqueueControllerDeps<
1356
1357
  TMessage,
1357
1358
  TContext = unknown,
1358
1359
  > extends TelegramQueueStore<TContext> {
1359
- getPreserveQueuedTurnsAsHistory: () => boolean;
1360
- setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
1360
+ getFoldQueuedPromptsIntoHistory: () => boolean;
1361
+ setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1361
1362
  createTurn: (
1362
1363
  messages: TMessage[],
1363
1364
  historyTurns: PendingTelegramTurn[],
@@ -1406,7 +1407,7 @@ export function buildTelegramSessionShutdownState<
1406
1407
  pendingTelegramModelSwitch: undefined,
1407
1408
  telegramTurnDispatchPending: false,
1408
1409
  compactionInProgress: false,
1409
- preserveQueuedTurnsAsHistory: false,
1410
+ foldQueuedPromptsIntoHistory: false,
1410
1411
  };
1411
1412
  }
1412
1413
 
@@ -1654,9 +1655,9 @@ export async function enqueueTelegramPromptTurnRuntime<
1654
1655
  ): Promise<void> {
1655
1656
  const enqueuePlan = planTelegramPromptEnqueue(
1656
1657
  deps.getQueuedItems(),
1657
- deps.getPreserveQueuedTurnsAsHistory(),
1658
+ deps.getFoldQueuedPromptsIntoHistory(),
1658
1659
  );
1659
- deps.setPreserveQueuedTurnsAsHistory(false);
1660
+ deps.setFoldQueuedPromptsIntoHistory(false);
1660
1661
  const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
1661
1662
  deps.setQueuedItems(
1662
1663
  appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
package/lib/routing.ts CHANGED
@@ -342,7 +342,7 @@ export function createTelegramInboundRouteRuntime<
342
342
  message: TMessage,
343
343
  ctx: TContext,
344
344
  ): Promise<void> => {
345
- deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory(false);
345
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory(false);
346
346
  const continueMessage = {
347
347
  ...message,
348
348
  text: "continue",
@@ -374,8 +374,8 @@ export function createTelegramInboundRouteRuntime<
374
374
  clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
375
375
  hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
376
376
  clearQueuedTelegramItems: deps.queueMutationRuntime.clear,
377
- setPreserveQueuedTurnsAsHistory:
378
- deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
377
+ setFoldQueuedPromptsIntoHistory:
378
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
379
379
  abortCurrentTurn: deps.bridgeRuntime.abort.abortTurn,
380
380
  isIdle: deps.isIdle,
381
381
  hasPendingMessages: deps.hasPendingMessages,
@@ -420,10 +420,10 @@ export function createTelegramInboundRouteRuntime<
420
420
  TContext
421
421
  >({
422
422
  ...deps.telegramQueueStore,
423
- getPreserveQueuedTurnsAsHistory:
424
- deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
425
- setPreserveQueuedTurnsAsHistory:
426
- deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
423
+ getFoldQueuedPromptsIntoHistory:
424
+ deps.bridgeRuntime.lifecycle.shouldFoldQueuedPromptsIntoHistory,
425
+ setFoldQueuedPromptsIntoHistory:
426
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
427
427
  createTurn: promptTurnBuilder,
428
428
  updateStatus: deps.updateStatus,
429
429
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,