@llblab/pi-telegram 0.16.5 → 0.17.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.
package/lib/preview.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Telegram preview streaming helpers
3
- * Zones: telegram outbound, streaming preview, rendering
3
+ * Zones: telegram outbound, native rich markdown drafts, fallback preview transport
4
4
  * Owns preview transport selection, runtime updates, and preview finalization
5
5
  */
6
6
 
@@ -10,23 +10,13 @@ import type {
10
10
  TelegramSendMessageBody,
11
11
  TelegramSentMessage,
12
12
  } from "./telegram-api.ts";
13
- import {
14
- buildTelegramPreviewSnapshot,
15
- MAX_MESSAGE_LENGTH,
16
- renderMarkdownPreviewText,
17
- renderTelegramMessage,
18
- type TelegramPreviewRenderStrategy,
19
- type TelegramPreviewSnapshot,
20
- type TelegramRenderedChunk,
21
- type TelegramRenderMode,
22
- } from "./rendering.ts";
23
-
24
13
  import { buildTelegramReplyParameters } from "./replies.ts";
25
14
  import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
26
15
  import { shouldSuppressPreviewForVoice } from "./voice.ts";
27
16
 
28
- const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
17
+ const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
29
18
  const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
19
+ const TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS = 4096;
30
20
 
31
21
  export type TelegramDraftSupport = "unknown" | "supported" | "unsupported";
32
22
 
@@ -37,7 +27,6 @@ export interface TelegramPreviewState {
37
27
  pendingText: string;
38
28
  lastSentText: string;
39
29
  lastSentParseMode?: "HTML";
40
- lastSentStrategy?: TelegramPreviewRenderStrategy;
41
30
  }
42
31
 
43
32
  export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
@@ -56,7 +45,6 @@ export interface TelegramPreviewRuntimeDeps<
56
45
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
57
46
  clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
58
47
  maxMessageLength: number;
59
- renderPreviewText: (markdown: string) => string;
60
48
  getDraftSupport: () => TelegramDraftSupport;
61
49
  setDraftSupport: (support: TelegramDraftSupport) => void;
62
50
  allocateDraftId: () => number;
@@ -81,21 +69,6 @@ export interface TelegramPreviewRuntimeDeps<
81
69
  text: string,
82
70
  options?: { parseMode?: "HTML" },
83
71
  ) => Promise<unknown>;
84
- renderTelegramMessage: (
85
- text: string,
86
- options?: { mode?: TelegramRenderMode },
87
- ) => TelegramRenderedChunk[];
88
- sendRenderedChunks: (
89
- chatId: number,
90
- chunks: TelegramRenderedChunk[],
91
- options?: { replyMarkup?: TReplyMarkup },
92
- ) => Promise<number | undefined>;
93
- editRenderedMessage: (
94
- chatId: number,
95
- messageId: number,
96
- chunks: TelegramRenderedChunk[],
97
- options?: { replyMarkup?: TReplyMarkup },
98
- ) => Promise<number | undefined>;
99
72
  canSend?: () => boolean;
100
73
  recordRuntimeEvent?: (
101
74
  category: string,
@@ -162,7 +135,6 @@ export interface TelegramPreviewControllerDeps<
162
135
  > {
163
136
  getDefaultReplyToMessageId?: () => number | undefined;
164
137
  maxMessageLength?: number;
165
- renderPreviewText?: (markdown: string) => string;
166
138
  initialDraftSupport?: TelegramDraftSupport;
167
139
  sendDraft: (
168
140
  chatId: number,
@@ -186,22 +158,6 @@ export interface TelegramPreviewControllerDeps<
186
158
  text: string,
187
159
  options?: { parseMode?: "HTML" },
188
160
  ) => Promise<unknown>;
189
- renderTelegramMessage?: (
190
- text: string,
191
- options?: { mode?: TelegramRenderMode },
192
- ) => TelegramRenderedChunk[];
193
- sendRenderedChunks: (
194
- chatId: number,
195
- chunks: TelegramRenderedChunk[],
196
- replyToMessageId: number | undefined,
197
- options?: { replyMarkup?: TReplyMarkup },
198
- ) => Promise<number | undefined>;
199
- editRenderedMessage: (
200
- chatId: number,
201
- messageId: number,
202
- chunks: TelegramRenderedChunk[],
203
- options?: { replyMarkup?: TReplyMarkup },
204
- ) => Promise<number | undefined>;
205
161
  canSend?: () => boolean;
206
162
  throttleMs?: number;
207
163
  maxDraftId?: number;
@@ -229,18 +185,13 @@ export interface TelegramPreviewController<
229
185
  flush: (chatId: number) => Promise<void>;
230
186
  scheduleFlush: (chatId: number) => void;
231
187
  finalize: (chatId: number, replyToMessageId?: number) => Promise<boolean>;
232
- finalizeMarkdown: (
233
- chatId: number,
234
- markdown: string,
235
- replyToMessageId?: number,
236
- options?: { replyMarkup?: TReplyMarkup },
237
- ) => Promise<boolean>;
238
188
  }
239
189
 
240
190
  export interface TelegramPreviewMessageTransportDeps {
241
191
  sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
242
192
  editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
243
193
  buildReplyParameters?: (
194
+ chatId: number,
244
195
  replyToMessageId: number | undefined,
245
196
  ) => TelegramReplyParameters | undefined;
246
197
  }
@@ -252,7 +203,7 @@ export function createTelegramPreviewMessageTransport(
252
203
  deps.buildReplyParameters ?? buildTelegramReplyParameters;
253
204
  return {
254
205
  sendMessage: (chatId, text, options, replyToMessageId) => {
255
- const replyParameters = getReplyParameters(replyToMessageId);
206
+ const replyParameters = getReplyParameters(chatId, replyToMessageId);
256
207
  return deps.sendMessage({
257
208
  chat_id: chatId,
258
209
  text,
@@ -270,55 +221,13 @@ export function createTelegramPreviewMessageTransport(
270
221
  };
271
222
  }
272
223
 
273
- export interface TelegramPreviewRenderedChunkTransportDeps<
274
- TReplyMarkup = TelegramPreviewReplyMarkup,
275
- > {
276
- sendRenderedChunks: (
277
- chatId: number,
278
- chunks: TelegramRenderedChunk[],
279
- options?: {
280
- replyToMessageId?: number;
281
- replyMarkup?: TReplyMarkup;
282
- },
283
- ) => Promise<number | undefined>;
284
- editRenderedMessage: (
285
- chatId: number,
286
- messageId: number,
287
- chunks: TelegramRenderedChunk[],
288
- options?: { replyMarkup?: TReplyMarkup },
289
- ) => Promise<number | undefined>;
290
- }
291
-
292
- export function createTelegramPreviewRenderedChunkTransport<
293
- TReplyMarkup = TelegramPreviewReplyMarkup,
294
- >(
295
- deps: TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>,
296
- ): Pick<
297
- TelegramPreviewControllerDeps<TReplyMarkup>,
298
- "sendRenderedChunks" | "editRenderedMessage"
299
- > {
300
- return {
301
- sendRenderedChunks: (chatId, chunks, replyToMessageId, options) =>
302
- deps.sendRenderedChunks(chatId, chunks, {
303
- replyToMessageId,
304
- ...(options?.replyMarkup ? { replyMarkup: options.replyMarkup } : {}),
305
- }),
306
- editRenderedMessage: (chatId, messageId, chunks, options) =>
307
- deps.editRenderedMessage(chatId, messageId, chunks, options),
308
- };
309
- }
310
-
311
224
  export type TelegramPreviewControllerRuntimeDeps<
312
225
  TReplyMarkup = TelegramPreviewReplyMarkup,
313
226
  > = Omit<
314
227
  TelegramPreviewControllerDeps<TReplyMarkup>,
315
- | "sendMessage"
316
- | "editMessageText"
317
- | "sendRenderedChunks"
318
- | "editRenderedMessage"
228
+ "sendMessage" | "editMessageText"
319
229
  > &
320
- TelegramPreviewMessageTransportDeps &
321
- TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
230
+ TelegramPreviewMessageTransportDeps;
322
231
 
323
232
  export function createTelegramPreviewControllerRuntime<
324
233
  TReplyMarkup = TelegramPreviewReplyMarkup,
@@ -328,7 +237,6 @@ export function createTelegramPreviewControllerRuntime<
328
237
  return createTelegramPreviewController({
329
238
  getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
330
239
  maxMessageLength: deps.maxMessageLength,
331
- renderPreviewText: deps.renderPreviewText,
332
240
  initialDraftSupport: deps.initialDraftSupport,
333
241
  sendDraft: deps.sendDraft,
334
242
  ...createTelegramPreviewMessageTransport({
@@ -336,11 +244,6 @@ export function createTelegramPreviewControllerRuntime<
336
244
  editMessageText: deps.editMessageText,
337
245
  buildReplyParameters: deps.buildReplyParameters,
338
246
  }),
339
- renderTelegramMessage: deps.renderTelegramMessage,
340
- ...createTelegramPreviewRenderedChunkTransport({
341
- sendRenderedChunks: deps.sendRenderedChunks,
342
- editRenderedMessage: deps.editRenderedMessage,
343
- }),
344
247
  throttleMs: deps.throttleMs,
345
248
  maxDraftId: deps.maxDraftId,
346
249
  setTimer: deps.setTimer,
@@ -356,13 +259,91 @@ export interface TelegramAssistantPreviewRuntimeDeps<
356
259
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
357
260
  isAssistantMessage: (message: TMessage) => boolean;
358
261
  getMessageText: (message: TMessage) => string;
262
+ sendMarkdownReply: (
263
+ chatId: number,
264
+ replyToMessageId: number | undefined,
265
+ markdown: string,
266
+ options?: { replyMarkup?: TReplyMarkup },
267
+ ) => Promise<number | undefined>;
268
+ editMarkdownMessage?: (
269
+ chatId: number,
270
+ messageId: number,
271
+ markdown: string,
272
+ options?: { replyMarkup?: TReplyMarkup },
273
+ ) => Promise<unknown>;
359
274
  }
360
275
 
361
276
  export type TelegramAssistantPreviewRuntime<
362
277
  TMessage,
363
278
  TReplyMarkup = TelegramPreviewReplyMarkup,
364
279
  > = TelegramPreviewController<TReplyMarkup> &
365
- TelegramAssistantMessagePreviewHooks<TMessage>;
280
+ TelegramAssistantMessagePreviewHooks<TMessage> & {
281
+ finalizeMarkdown: (
282
+ chatId: number,
283
+ markdown: string,
284
+ replyToMessageId?: number,
285
+ options?: { replyMarkup?: TReplyMarkup },
286
+ ) => Promise<boolean>;
287
+ };
288
+
289
+ export function createTelegramNativeMarkdownMessageEditor<TReplyMarkup>(deps: {
290
+ editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
291
+ }): (
292
+ chatId: number,
293
+ messageId: number,
294
+ markdown: string,
295
+ options?: { replyMarkup?: TReplyMarkup },
296
+ ) => Promise<unknown> {
297
+ return (chatId, messageId, markdown, options) =>
298
+ deps.editMessageText({
299
+ chat_id: chatId,
300
+ message_id: messageId,
301
+ rich_message: { markdown, skip_entity_detection: true },
302
+ ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
303
+ });
304
+ }
305
+
306
+ export function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
307
+ getState: () => TelegramPreviewRuntimeState | undefined;
308
+ clear: (chatId: number) => Promise<void>;
309
+ discard?: () => void;
310
+ sendMarkdownReply: (
311
+ chatId: number,
312
+ replyToMessageId: number | undefined,
313
+ markdown: string,
314
+ options?: { replyMarkup?: TReplyMarkup },
315
+ ) => Promise<number | undefined>;
316
+ editMarkdownMessage?: (
317
+ chatId: number,
318
+ messageId: number,
319
+ markdown: string,
320
+ options?: { replyMarkup?: TReplyMarkup },
321
+ ) => Promise<unknown>;
322
+ }): (
323
+ chatId: number,
324
+ markdown: string,
325
+ replyToMessageId?: number,
326
+ options?: { replyMarkup?: TReplyMarkup },
327
+ ) => Promise<boolean> {
328
+ return async (chatId, markdown, replyToMessageId, options) => {
329
+ const state = deps.getState();
330
+ if (state?.mode === "message" && state.messageId !== undefined) {
331
+ try {
332
+ await deps.editMarkdownMessage?.(chatId, state.messageId, markdown, options);
333
+ await deps.clear(chatId);
334
+ return true;
335
+ } catch {
336
+ // Fall through to a fresh native Rich Message so final text is not lost.
337
+ }
338
+ }
339
+ if (state?.flushPromise) {
340
+ await state.flushPromise.catch(() => {});
341
+ }
342
+ await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
343
+ deps.discard?.();
344
+ return true;
345
+ };
346
+ }
366
347
 
367
348
  export function createTelegramAssistantPreviewRuntime<
368
349
  TMessage,
@@ -371,8 +352,16 @@ export function createTelegramAssistantPreviewRuntime<
371
352
  deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
372
353
  ): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
373
354
  const controller = createTelegramPreviewControllerRuntime(deps);
355
+ const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
356
+ getState: controller.getState,
357
+ clear: controller.clear,
358
+ discard: () => controller.setState(undefined),
359
+ sendMarkdownReply: deps.sendMarkdownReply,
360
+ editMarkdownMessage: deps.editMarkdownMessage,
361
+ });
374
362
  return {
375
363
  ...controller,
364
+ finalizeMarkdown: finalizeMarkdownPreview,
376
365
  ...createTelegramAssistantMessagePreviewHooks({
377
366
  getActiveTurn: deps.getActiveTurn,
378
367
  isAssistantMessage: deps.isAssistantMessage,
@@ -380,7 +369,7 @@ export function createTelegramAssistantPreviewRuntime<
380
369
  setState: controller.setState,
381
370
  createPreviewState: controller.createState,
382
371
  finalizePreview: controller.finalize,
383
- finalizeMarkdownPreview: controller.finalizeMarkdown,
372
+ finalizeMarkdownPreview,
384
373
  getMessageText: deps.getMessageText,
385
374
  schedulePreviewFlush: controller.scheduleFlush,
386
375
  }),
@@ -400,9 +389,8 @@ export function createTelegramPreviewController<
400
389
  setTimeout(callback, ms));
401
390
  const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
402
391
  const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
403
- const maxMessageLength = deps.maxMessageLength ?? MAX_MESSAGE_LENGTH;
404
- const renderPreview = deps.renderPreviewText ?? renderMarkdownPreviewText;
405
- const renderMessage = deps.renderTelegramMessage ?? renderTelegramMessage;
392
+ const maxMessageLength =
393
+ deps.maxMessageLength ?? TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS;
406
394
  let draftSupport = deps.initialDraftSupport ?? "unknown";
407
395
  let nextDraftId = 0;
408
396
  const getRuntimeDeps = (
@@ -418,7 +406,6 @@ export function createTelegramPreviewController<
418
406
  nextState.flushTimer = undefined;
419
407
  },
420
408
  maxMessageLength,
421
- renderPreviewText: renderPreview,
422
409
  getDraftSupport: () => draftSupport,
423
410
  setDraftSupport: (support) => {
424
411
  draftSupport = support;
@@ -436,15 +423,6 @@ export function createTelegramPreviewController<
436
423
  replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
437
424
  ),
438
425
  editMessageText: deps.editMessageText,
439
- renderTelegramMessage: renderMessage,
440
- sendRenderedChunks: (chatId, chunks, options) =>
441
- deps.sendRenderedChunks(
442
- chatId,
443
- chunks,
444
- replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
445
- options,
446
- ),
447
- editRenderedMessage: deps.editRenderedMessage,
448
426
  canSend: deps.canSend,
449
427
  recordRuntimeEvent: deps.recordRuntimeEvent,
450
428
  });
@@ -464,6 +442,10 @@ export function createTelegramPreviewController<
464
442
  flush: (chatId) => flushTelegramPreview(chatId, getRuntimeDeps()),
465
443
  scheduleFlush: (chatId) => {
466
444
  if (!state || state.flushTimer) return;
445
+ if (throttleMs <= 0) {
446
+ void flushTelegramPreview(chatId, getRuntimeDeps());
447
+ return;
448
+ }
467
449
  state.flushTimer = setTimer(() => {
468
450
  void flushTelegramPreview(chatId, getRuntimeDeps());
469
451
  }, throttleMs);
@@ -471,13 +453,6 @@ export function createTelegramPreviewController<
471
453
  },
472
454
  finalize: (chatId, replyToMessageId) =>
473
455
  finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
474
- finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
475
- finalizeTelegramMarkdownPreview(
476
- chatId,
477
- markdown,
478
- getRuntimeDeps(replyToMessageId),
479
- options,
480
- ),
481
456
  };
482
457
  }
483
458
 
@@ -553,10 +528,7 @@ export function buildTelegramPreviewFinalText(
553
528
  ): string | undefined {
554
529
  const finalText = state.pendingText.trim();
555
530
  if (finalText) return finalText;
556
- if (
557
- state.lastSentStrategy === "rich-stable-blocks" ||
558
- state.lastSentParseMode === "HTML"
559
- ) {
531
+ if (state.lastSentParseMode === "HTML") {
560
532
  return undefined;
561
533
  }
562
534
  return state.lastSentText.trim() || undefined;
@@ -579,14 +551,16 @@ export function allocateTelegramDraftId(
579
551
  return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
580
552
  }
581
553
 
554
+ interface TelegramNativeMarkdownPreviewSnapshot {
555
+ text: string;
556
+ sourceText: string;
557
+ }
558
+
582
559
  export function shouldUseTelegramDraftPreview(options: {
583
560
  draftSupport: TelegramDraftSupport;
584
- snapshot?: TelegramPreviewSnapshot;
561
+ snapshot?: TelegramNativeMarkdownPreviewSnapshot;
585
562
  }): boolean {
586
- return (
587
- options.draftSupport !== "unsupported" &&
588
- (options.snapshot === undefined || options.snapshot.strategy === "plain")
589
- );
563
+ return options.draftSupport !== "unsupported";
590
564
  }
591
565
 
592
566
  export async function clearTelegramPreview<
@@ -594,12 +568,43 @@ export async function clearTelegramPreview<
594
568
  >(
595
569
  chatId: number,
596
570
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
571
+ options: { awaitFlush?: boolean } = {},
597
572
  ): Promise<void> {
598
- void chatId;
599
573
  const state = deps.getState();
600
574
  if (!state) return;
601
575
  deps.clearScheduledFlush(state);
576
+ if (state.flushPromise && options.awaitFlush !== false) {
577
+ state.flushRequested = false;
578
+ await state.flushPromise.catch(() => {});
579
+ if (deps.getState() !== state) return;
580
+ }
602
581
  deps.setState(undefined);
582
+ if (state.mode === "draft" && state.draftId !== undefined) {
583
+ try {
584
+ await deps.sendDraft(chatId, state.draftId, undefined);
585
+ } catch (error) {
586
+ deps.recordRuntimeEvent?.("preview", error, {
587
+ phase: "clear-draft",
588
+ chatId,
589
+ draftId: state.draftId,
590
+ });
591
+ }
592
+ }
593
+ }
594
+
595
+ function buildTelegramNativeMarkdownPreviewSnapshot(
596
+ state: TelegramPreviewState,
597
+ maxMessageLength: number,
598
+ ): TelegramNativeMarkdownPreviewSnapshot | undefined {
599
+ const sourceText = state.pendingText.trim();
600
+ if (!sourceText || sourceText === state.lastSentText) return undefined;
601
+ return {
602
+ text:
603
+ sourceText.length > maxMessageLength
604
+ ? sourceText.slice(0, maxMessageLength)
605
+ : sourceText,
606
+ sourceText,
607
+ };
603
608
  }
604
609
 
605
610
  async function performTelegramPreviewFlush<
@@ -610,15 +615,13 @@ async function performTelegramPreviewFlush<
610
615
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
611
616
  ): Promise<void> {
612
617
  if (deps.canSend && !deps.canSend()) {
613
- await clearTelegramPreview(chatId, deps);
618
+ await clearTelegramPreview(chatId, deps, { awaitFlush: false });
614
619
  return;
615
620
  }
616
- const snapshot = buildTelegramPreviewSnapshot({
621
+ const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
617
622
  state,
618
- maxMessageLength: deps.maxMessageLength,
619
- renderPreviewText: deps.renderPreviewText,
620
- renderTelegramMessage: deps.renderTelegramMessage,
621
- });
623
+ deps.maxMessageLength,
624
+ );
622
625
  if (!snapshot) return;
623
626
  if (
624
627
  shouldUseTelegramDraftPreview({
@@ -629,35 +632,28 @@ async function performTelegramPreviewFlush<
629
632
  const draftId = state.draftId ?? deps.allocateDraftId();
630
633
  state.draftId = draftId;
631
634
  try {
632
- await deps.sendDraft(chatId, draftId, snapshot.text);
635
+ await deps.sendDraft(chatId, draftId, snapshot.sourceText);
633
636
  deps.setDraftSupport("supported");
634
637
  state.mode = "draft";
635
638
  state.lastSentText = snapshot.text;
636
- state.lastSentParseMode = snapshot.parseMode;
637
- state.lastSentStrategy = snapshot.strategy;
639
+ state.lastSentParseMode = undefined;
638
640
  return;
639
641
  } catch {
640
642
  deps.setDraftSupport("unsupported");
641
643
  }
642
644
  }
643
645
  if (state.messageId === undefined) {
644
- const sent = await deps.sendMessage(chatId, snapshot.text, {
645
- parseMode: snapshot.parseMode,
646
- });
646
+ const sent = await deps.sendMessage(chatId, snapshot.text, undefined);
647
647
  state.messageId = sent.message_id;
648
648
  state.mode = "message";
649
649
  state.lastSentText = snapshot.text;
650
- state.lastSentParseMode = snapshot.parseMode;
651
- state.lastSentStrategy = snapshot.strategy;
650
+ state.lastSentParseMode = undefined;
652
651
  return;
653
652
  }
654
- await deps.editMessageText(chatId, state.messageId, snapshot.text, {
655
- parseMode: snapshot.parseMode,
656
- });
653
+ await deps.editMessageText(chatId, state.messageId, snapshot.text, undefined);
657
654
  state.mode = "message";
658
655
  state.lastSentText = snapshot.text;
659
- state.lastSentParseMode = snapshot.parseMode;
660
- state.lastSentStrategy = snapshot.strategy;
656
+ state.lastSentParseMode = undefined;
661
657
  }
662
658
 
663
659
  export async function flushTelegramPreview<
@@ -718,49 +714,9 @@ export async function finalizeTelegramPreview<
718
714
  }
719
715
  if (state.mode === "draft") {
720
716
  await deps.sendMessage(chatId, finalText);
721
- await clearTelegramPreview(chatId, deps);
717
+ deps.setState(undefined);
722
718
  return true;
723
719
  }
724
720
  deps.setState(undefined);
725
721
  return state.messageId !== undefined;
726
722
  }
727
-
728
- export async function finalizeTelegramMarkdownPreview<
729
- TReplyMarkup = TelegramPreviewReplyMarkup,
730
- >(
731
- chatId: number,
732
- markdown: string,
733
- deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
734
- options?: { replyMarkup?: TReplyMarkup },
735
- ): Promise<boolean> {
736
- const state = deps.getState();
737
- if (!state) return false;
738
- if (deps.canSend && !deps.canSend()) {
739
- await clearTelegramPreview(chatId, deps);
740
- return false;
741
- }
742
- await flushTelegramPreview(chatId, deps);
743
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
744
- if (chunks.length === 0) {
745
- await clearTelegramPreview(chatId, deps);
746
- return false;
747
- }
748
- try {
749
- if (state.mode === "draft") {
750
- await deps.sendRenderedChunks(chatId, chunks, options);
751
- await clearTelegramPreview(chatId, deps);
752
- return true;
753
- }
754
- if (state.messageId === undefined) return false;
755
- await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
756
- deps.setState(undefined);
757
- return true;
758
- } catch (error) {
759
- deps.recordRuntimeEvent?.("preview", error, {
760
- phase: "finalize-markdown",
761
- chatId,
762
- messageId: state.messageId,
763
- });
764
- return false;
765
- }
766
- }
package/lib/prompts.ts CHANGED
@@ -28,13 +28,13 @@ Telegram-originated turn context:
28
28
  - Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
29
29
 
30
30
  Telegram-visible output:
31
- - Telegram is often phone-width; keep tables, dense list items, and compact text blocks at or below 37 visible cells when possible.
32
- - Count display width, not raw characters: emoji and some glyphs are wide, so prefer shorter labels when unsure.
31
+ - Telegram is mobile-first: keep answers easy to scan, use headings/lists when useful, and avoid unnecessarily huge blocks of text.
32
+ - For formulas, use math delimiters like \`$E = mc^2$\` for inline formulas and \`$$\\nE = mc^2\\n$$\` for block formulas; do not wrap formulas in backticks unless they should render as literal code.
33
33
  - Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
34
34
  - 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.
35
35
 
36
36
  Native outbound actions:
37
- - 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.
37
+ - Use normal Rich 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.
38
38
  - \`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 text, or \`<!-- telegram_voice: Short summary -->\` for one-line text 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.
39
39
  - \`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.
40
40
  - If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.
package/lib/queue.ts CHANGED
@@ -1439,6 +1439,7 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
1439
1439
  deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
1440
1440
  ): Promise<void> {
1441
1441
  deps.unbindDeferredDispatchContext?.();
1442
+ await deps.stopPolling();
1442
1443
  deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
1443
1444
  deps.clearPendingMediaGroups();
1444
1445
  deps.clearModelMenuState();
@@ -1448,7 +1449,6 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
1448
1449
  }
1449
1450
  deps.clearActiveTurn();
1450
1451
  deps.clearAbort();
1451
- await deps.stopPolling();
1452
1452
  }
1453
1453
 
1454
1454
  export type TelegramSessionLifecycleRuntimeDeps<