@llblab/pi-telegram 0.17.5 → 0.18.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.
Files changed (61) hide show
  1. package/AGENTS.md +67 -32
  2. package/BACKLOG.md +59 -19
  3. package/CHANGELOG.md +36 -15
  4. package/README.md +63 -35
  5. package/docs/README.md +3 -1
  6. package/docs/architecture.md +55 -23
  7. package/docs/callback-namespaces.md +1 -1
  8. package/docs/inbound.md +1 -1
  9. package/docs/locks.md +0 -2
  10. package/docs/multi-instance-bus.md +483 -0
  11. package/docs/outbound.md +4 -3
  12. package/docs/public-api.md +12 -10
  13. package/docs/sections.md +2 -2
  14. package/docs/ui-style.md +76 -0
  15. package/index.ts +789 -32
  16. package/lib/bindings.ts +68 -12
  17. package/lib/bus-api.ts +314 -0
  18. package/lib/bus-follower.ts +853 -0
  19. package/lib/bus-leader.ts +915 -0
  20. package/lib/bus.ts +866 -0
  21. package/lib/command-templates.ts +9 -11
  22. package/lib/commands.ts +133 -47
  23. package/lib/config.ts +53 -5
  24. package/lib/lifecycle.ts +23 -7
  25. package/lib/locks.ts +230 -66
  26. package/lib/media.ts +30 -2
  27. package/lib/menu-model.ts +48 -17
  28. package/lib/menu-queue.ts +51 -20
  29. package/lib/menu-settings.ts +9 -5
  30. package/lib/menu-status.ts +3 -0
  31. package/lib/menu-thinking.ts +3 -0
  32. package/lib/menu.ts +67 -26
  33. package/lib/outbound-attachments.ts +102 -17
  34. package/lib/outbound-buttons.ts +6 -2
  35. package/lib/outbound-voice.ts +31 -11
  36. package/lib/outbound.ts +6 -4
  37. package/lib/ownership.ts +119 -0
  38. package/lib/pi.ts +26 -3
  39. package/lib/polling.ts +477 -7
  40. package/lib/preview.ts +141 -88
  41. package/lib/prompt-templates.ts +3 -3
  42. package/lib/prompts.ts +80 -30
  43. package/lib/queue.ts +193 -91
  44. package/lib/rendering.ts +0 -25
  45. package/lib/replies.ts +187 -55
  46. package/lib/routing.ts +1673 -9
  47. package/lib/runtime-log.ts +123 -0
  48. package/lib/runtime.ts +84 -12
  49. package/lib/sections.ts +28 -21
  50. package/lib/setup.ts +1 -1
  51. package/lib/status.ts +532 -9
  52. package/lib/sync.ts +618 -0
  53. package/lib/target.ts +49 -0
  54. package/lib/telegram-api.ts +405 -40
  55. package/lib/text-groups.ts +5 -1
  56. package/lib/thread-reconciler.ts +915 -0
  57. package/lib/threads.ts +2205 -0
  58. package/lib/turns.ts +48 -3
  59. package/lib/updates.ts +355 -32
  60. package/package.json +24 -2
  61. package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/queue.ts CHANGED
@@ -67,9 +67,15 @@ export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[]
67
67
  },
68
68
  ] as const;
69
69
 
70
+ export interface TelegramQueueTarget {
71
+ chatId: number;
72
+ threadId?: number;
73
+ }
74
+
70
75
  export interface TelegramQueueItemBase {
71
76
  kind: TelegramQueueItemKind;
72
77
  chatId: number;
78
+ target?: TelegramQueueTarget;
73
79
  replyToMessageId: number;
74
80
  guestQueryId?: string;
75
81
  queueOrder: number;
@@ -123,6 +129,7 @@ export interface TelegramActiveTurnStore<
123
129
  set: (turn: TTurn) => void;
124
130
  clear: () => void;
125
131
  getChatId: () => number | undefined;
132
+ getTarget: () => TelegramQueueTarget | undefined;
126
133
  getReplyToMessageId: () => number | undefined;
127
134
  getGuestQueryId: () => string | undefined;
128
135
  getSourceMessageIds: () => number[] | undefined;
@@ -195,7 +202,7 @@ export function createTelegramQueueStore<TContext = unknown>(
195
202
  export function createTelegramQueueItemCountGetter<TContext = unknown>(
196
203
  store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
197
204
  ): () => number {
198
- return function getTelegramQueueItemCount() {
205
+ return () => {
199
206
  return store.getQueuedItems().length;
200
207
  };
201
208
  }
@@ -214,6 +221,8 @@ export function createTelegramActiveTurnStore<
214
221
  activeTurn = undefined;
215
222
  },
216
223
  getChatId: () => activeTurn?.chatId,
224
+ getTarget: () =>
225
+ activeTurn?.target ? { ...activeTurn.target } : undefined,
217
226
  getReplyToMessageId: () => activeTurn?.replyToMessageId,
218
227
  getGuestQueryId: () => activeTurn?.guestQueryId,
219
228
  getSourceMessageIds: () => activeTurn?.sourceMessageIds,
@@ -280,16 +289,40 @@ export function compareTelegramQueueItems<TContext = unknown>(
280
289
  return left.queueOrder - right.queueOrder;
281
290
  }
282
291
 
292
+ export interface TelegramQueueMessageScope {
293
+ chatId?: number;
294
+ threadId?: number;
295
+ }
296
+
297
+ function isTelegramQueueItemInMessageScope<TContext = unknown>(
298
+ item: TelegramQueueItem<TContext>,
299
+ scope: TelegramQueueMessageScope | undefined,
300
+ ): boolean {
301
+ if (!scope) return true;
302
+ if (typeof scope.chatId === "number" && item.chatId !== scope.chatId) {
303
+ return false;
304
+ }
305
+ if (typeof scope.threadId === "number") {
306
+ return item.target?.threadId === scope.threadId;
307
+ }
308
+ return true;
309
+ }
310
+
283
311
  export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
284
312
  items: TelegramQueueItem<TContext>[],
285
313
  messageIds: number[],
314
+ scope?: TelegramQueueMessageScope,
286
315
  ): { items: TelegramQueueItem<TContext>[]; removedCount: number } {
287
316
  if (messageIds.length === 0 || items.length === 0) {
288
317
  return { items, removedCount: 0 };
289
318
  }
290
319
  const deletedMessageIds = new Set(messageIds);
291
320
  const nextItems = items.filter((item) => {
292
- if (!isPendingTelegramTurn(item)) return true;
321
+ if (
322
+ !isPendingTelegramTurn(item) ||
323
+ !isTelegramQueueItemInMessageScope(item, scope)
324
+ )
325
+ return true;
293
326
  return !item.sourceMessageIds.some((messageId) =>
294
327
  deletedMessageIds.has(messageId),
295
328
  );
@@ -303,11 +336,13 @@ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
303
336
  export function clearTelegramQueuePromptPriority<TContext = unknown>(
304
337
  items: TelegramQueueItem<TContext>[],
305
338
  messageId: number,
339
+ scope?: TelegramQueueMessageScope,
306
340
  ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
307
341
  let changed = false;
308
342
  const nextItems = items.map((item) => {
309
343
  if (
310
344
  !isPendingTelegramTurn(item) ||
345
+ !isTelegramQueueItemInMessageScope(item, scope) ||
311
346
  !item.sourceMessageIds.includes(messageId) ||
312
347
  item.queueLane !== "priority"
313
348
  ) {
@@ -329,11 +364,13 @@ export function prioritizeTelegramQueuePrompt<TContext = unknown>(
329
364
  messageId: number,
330
365
  laneOrder: number,
331
366
  priorityEmoji = "⚡",
367
+ scope?: TelegramQueueMessageScope,
332
368
  ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
333
369
  let changed = false;
334
370
  const nextItems = items.map((item) => {
335
371
  if (
336
372
  !isPendingTelegramTurn(item) ||
373
+ !isTelegramQueueItemInMessageScope(item, scope) ||
337
374
  !item.sourceMessageIds.includes(messageId)
338
375
  ) {
339
376
  return item;
@@ -366,15 +403,6 @@ export function consumeDispatchedTelegramPrompt<TContext = unknown>(
366
403
  return { activeTurn: nextItem, remainingItems: items.slice(1) };
367
404
  }
368
405
 
369
- function formatTelegramQueueItemStatusSummary<TContext = unknown>(
370
- item: TelegramQueueItem<TContext>,
371
- ): string {
372
- if (item.queueLane === "priority") {
373
- return `${item.kind === "prompt" ? (item.priorityEmoji ?? "⚡") : "⚡"} ${item.statusSummary}`;
374
- }
375
- return item.statusSummary;
376
- }
377
-
378
406
  export function formatQueuedTelegramItemsStatus<TContext = unknown>(
379
407
  items: TelegramQueueItem<TContext>[],
380
408
  ): string {
@@ -434,6 +462,7 @@ export function createTelegramDispatchReadinessChecker<TContext>(
434
462
 
435
463
  export function buildPendingTelegramControlItem<TContext = unknown>(options: {
436
464
  chatId: number;
465
+ target?: TelegramQueueTarget;
437
466
  replyToMessageId: number;
438
467
  controlType: PendingTelegramControlItem<TContext>["controlType"];
439
468
  queueOrder: number;
@@ -445,6 +474,7 @@ export function buildPendingTelegramControlItem<TContext = unknown>(options: {
445
474
  kind: "control",
446
475
  controlType: options.controlType,
447
476
  chatId: options.chatId,
477
+ ...(options.target ? { target: options.target } : {}),
448
478
  replyToMessageId: options.replyToMessageId,
449
479
  queueOrder: options.queueOrder,
450
480
  queueLane: "control",
@@ -463,6 +493,7 @@ export function createTelegramControlItemBuilder<TContext = unknown>(
463
493
  deps: TelegramControlItemBuilderDeps,
464
494
  ): (options: {
465
495
  chatId: number;
496
+ target?: TelegramQueueTarget;
466
497
  replyToMessageId: number;
467
498
  controlType: PendingTelegramControlItem<TContext>["controlType"];
468
499
  statusSummary: string;
@@ -647,10 +678,10 @@ export function createTelegramAgentStartHook<
647
678
  TTurn extends PendingTelegramTurn,
648
679
  TContext = unknown,
649
680
  >(deps: TelegramAgentStartHookRuntimeDeps<TTurn, TContext>) {
650
- return async function onAgentStart(
681
+ return async (
651
682
  _event: TelegramAgentStartHookEvent,
652
683
  ctx: TContext,
653
- ): Promise<void> {
684
+ ): Promise<void> => {
654
685
  deps.setAbortHandler(ctx);
655
686
  handleTelegramAgentStartRuntime<TTurn, TContext>({
656
687
  queuedItems: deps.getQueuedItems(),
@@ -806,24 +837,29 @@ export interface TelegramAgentEndRuntimeDeps<
806
837
  waitForTypingIdle?: () => Promise<void>;
807
838
  updateStatus: () => void;
808
839
  dispatchNextQueuedTelegramTurn: () => void;
809
- clearPreview: (chatId: number) => Promise<void>;
840
+ scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
841
+ clearPreview: (
842
+ chatId: number,
843
+ options?: { target?: TelegramQueueTarget },
844
+ ) => Promise<void>;
810
845
  setPreviewPendingText: (text: string) => void;
811
846
  finalizeMarkdownPreview: (
812
847
  chatId: number,
813
848
  markdown: string,
814
849
  replyToMessageId: number,
815
- options?: { replyMarkup?: TReplyMarkup },
850
+ options?: { replyMarkup?: TReplyMarkup; target?: TelegramQueueTarget },
816
851
  ) => Promise<boolean>;
817
852
  sendMarkdownReply: (
818
853
  chatId: number,
819
854
  replyToMessageId: number | undefined,
820
855
  markdown: string,
821
- options?: { replyMarkup?: TReplyMarkup },
856
+ options?: { replyMarkup?: TReplyMarkup; target?: TelegramQueueTarget },
822
857
  ) => Promise<unknown>;
823
858
  sendTextReply: (
824
859
  chatId: number,
825
860
  replyToMessageId: number,
826
861
  text: string,
862
+ options?: { target?: TelegramQueueTarget },
827
863
  ) => Promise<unknown>;
828
864
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
829
865
  answerGuestQuery?: (
@@ -841,6 +877,7 @@ export interface TelegramAgentEndRuntimeDeps<
841
877
  options?: { replyToPrompt?: boolean },
842
878
  ) => Promise<void>;
843
879
  getDefaultChatId?: () => number | undefined;
880
+ getDefaultTarget?: () => TelegramQueueTarget | undefined;
844
881
  isProactivePushEnabled?: () => boolean;
845
882
  canSendProactivePush?: () => boolean;
846
883
  recordRuntimeEvent?: (
@@ -857,6 +894,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
857
894
  TReplyMarkup = unknown,
858
895
  > {
859
896
  getActiveTurn: () => TTurn | undefined;
897
+ loadConfig?: () => Promise<void>;
860
898
  extractAssistant: (
861
899
  messages: readonly TMessage[],
862
900
  ) => TelegramAgentEndAssistantResult;
@@ -868,7 +906,14 @@ export interface TelegramAgentEndHookRuntimeDeps<
868
906
  requestDeferredDispatchNextQueuedTelegramTurn: (
869
907
  dispatch: (ctx: TContext) => void,
870
908
  ) => void;
871
- clearPreview: (chatId: number) => Promise<void>;
909
+ scheduleActiveTurnDelivery?: TelegramAgentEndRuntimeDeps<
910
+ TTurn,
911
+ TReplyMarkup
912
+ >["scheduleActiveTurnDelivery"];
913
+ clearPreview: TelegramAgentEndRuntimeDeps<
914
+ TTurn,
915
+ TReplyMarkup
916
+ >["clearPreview"];
872
917
  setPreviewPendingText: (text: string) => void;
873
918
  finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<
874
919
  TTurn,
@@ -888,6 +933,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
888
933
  >["planOutboundReply"];
889
934
  sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
890
935
  getDefaultChatId?: TelegramAgentEndRuntimeDeps<TTurn>["getDefaultChatId"];
936
+ getDefaultTarget?: TelegramAgentEndRuntimeDeps<TTurn>["getDefaultTarget"];
891
937
  isProactivePushEnabled?: TelegramAgentEndRuntimeDeps<TTurn>["isProactivePushEnabled"];
892
938
  canSendProactivePush?: (ctx: TContext) => boolean;
893
939
  recordRuntimeEvent?: TelegramAgentEndRuntimeDeps<TTurn>["recordRuntimeEvent"];
@@ -976,10 +1022,11 @@ export function createTelegramAgentEndHook<
976
1022
  TReplyMarkup
977
1023
  >,
978
1024
  ) {
979
- return async function onAgentEnd(
1025
+ return async (
980
1026
  event: TelegramAgentEndHookEvent<TMessage>,
981
1027
  ctx: TContext,
982
- ): Promise<void> {
1028
+ ): Promise<void> => {
1029
+ await deps.loadConfig?.();
983
1030
  const turn = deps.getActiveTurn();
984
1031
  const proactiveEnabled = deps.isProactivePushEnabled?.() ?? false;
985
1032
  const canProactivePush = deps.canSendProactivePush?.(ctx) ?? false;
@@ -996,6 +1043,7 @@ export function createTelegramAgentEndHook<
996
1043
  deps.dispatchNextQueuedTelegramTurn,
997
1044
  );
998
1045
  },
1046
+ scheduleActiveTurnDelivery: deps.scheduleActiveTurnDelivery,
999
1047
  clearPreview: deps.clearPreview,
1000
1048
  setPreviewPendingText: deps.setPreviewPendingText,
1001
1049
  finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
@@ -1007,6 +1055,7 @@ export function createTelegramAgentEndHook<
1007
1055
  planOutboundReply: deps.planOutboundReply,
1008
1056
  sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
1009
1057
  getDefaultChatId: deps.getDefaultChatId,
1058
+ getDefaultTarget: deps.getDefaultTarget,
1010
1059
  isProactivePushEnabled: deps.isProactivePushEnabled,
1011
1060
  canSendProactivePush: () => canProactivePush,
1012
1061
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -1065,20 +1114,26 @@ export async function handleTelegramAgentEndRuntime<
1065
1114
  const canProactivePush = deps.canSendProactivePush?.() ?? false;
1066
1115
  if (proactiveEnabled && finalText && !assistant.errorMessage) {
1067
1116
  if (canProactivePush) {
1068
- const defaultChatId = deps.getDefaultChatId?.();
1117
+ const defaultTarget = deps.getDefaultTarget?.();
1118
+ const defaultChatId = defaultTarget?.chatId ?? deps.getDefaultChatId?.();
1069
1119
  if (defaultChatId !== undefined) {
1070
1120
  try {
1071
- await deps.sendMarkdownReply(defaultChatId, undefined, finalText);
1121
+ await deps.sendMarkdownReply(defaultChatId, undefined, finalText, {
1122
+ target: defaultTarget,
1123
+ });
1072
1124
  } catch (error) {
1073
1125
  deps.recordRuntimeEvent?.("proactive-push", error, {
1074
1126
  chatId: defaultChatId,
1127
+ threadId: defaultTarget?.threadId,
1075
1128
  });
1076
1129
  }
1077
1130
  }
1078
1131
  } else {
1079
1132
  deps.recordRuntimeEvent?.(
1080
1133
  "proactive-push",
1081
- new Error("Proactive push skipped because this instance does not own Telegram polling."),
1134
+ new Error(
1135
+ "Proactive push skipped because this instance does not own Telegram polling.",
1136
+ ),
1082
1137
  { phase: "ownership" },
1083
1138
  );
1084
1139
  }
@@ -1090,7 +1145,7 @@ export async function handleTelegramAgentEndRuntime<
1090
1145
  if (assistant.errorMessage) {
1091
1146
  await deps.answerGuestQuery?.(
1092
1147
  turn.guestQueryId,
1093
- "Telegram bridge: π failed while processing the request.",
1148
+ "Telegram bridge: Pi failed while processing the request.",
1094
1149
  );
1095
1150
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1096
1151
  return;
@@ -1106,86 +1161,99 @@ export async function handleTelegramAgentEndRuntime<
1106
1161
  return;
1107
1162
  }
1108
1163
  if (endPlan.shouldClearPreview) {
1109
- await deps.clearPreview(turn.chatId);
1164
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1110
1165
  }
1111
1166
  if (endPlan.shouldSendErrorMessage) {
1112
1167
  await deps.sendTextReply(
1113
1168
  turn.chatId,
1114
1169
  turn.replyToMessageId,
1115
1170
  assistant.errorMessage ||
1116
- "Telegram bridge: π failed while processing the request.",
1171
+ "Telegram bridge: Pi failed while processing the request.",
1172
+ { target: turn.target },
1117
1173
  );
1118
1174
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1119
1175
  return;
1120
1176
  }
1121
- if (finalText) deps.setPreviewPendingText(finalText);
1122
- if (!finalText && hasOutboundArtifacts) await deps.clearPreview(turn.chatId);
1123
- if (endPlan.kind === "text" && finalText) {
1124
- try {
1125
- const finalized = await deps.finalizeMarkdownPreview(
1126
- turn.chatId,
1127
- finalText,
1128
- turn.replyToMessageId,
1129
- { replyMarkup },
1130
- );
1131
- if (!finalized) {
1132
- await deps.clearPreview(turn.chatId);
1133
- await deps.sendMarkdownReply(
1177
+ const deliverActiveTurn = async () => {
1178
+ if (finalText) deps.setPreviewPendingText(finalText);
1179
+ if (!finalText && hasOutboundArtifacts)
1180
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1181
+ if (endPlan.kind === "text" && finalText) {
1182
+ try {
1183
+ const finalized = await deps.finalizeMarkdownPreview(
1134
1184
  turn.chatId,
1135
- turn.replyToMessageId,
1136
1185
  finalText,
1137
- { replyMarkup },
1186
+ turn.replyToMessageId,
1187
+ { replyMarkup, target: turn.target },
1138
1188
  );
1139
- }
1140
- } catch (error) {
1141
- deps.recordRuntimeEvent?.("delivery", error, {
1142
- phase: "final-text",
1143
- chatId: turn.chatId,
1144
- replyToMessageId: turn.replyToMessageId,
1145
- });
1146
- }
1147
- }
1148
- if (outboundReply && deps.sendOutboundReplyArtifacts) {
1149
- try {
1150
- await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1151
- replyToPrompt: !finalText,
1152
- });
1153
- } catch (error) {
1154
- deps.recordRuntimeEvent?.("delivery", error, {
1155
- phase: "voice-artifacts",
1156
- chatId: turn.chatId,
1157
- });
1158
- // Fallback to planned text when voice delivery fails and text wasn't already delivered
1159
- if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
1160
- try {
1161
- const fallbackMarkdown =
1162
- plannedReply?.markdown || outboundReply?.voiceText || rawFinalText;
1189
+ if (!finalized) {
1190
+ await deps.clearPreview(turn.chatId, { target: turn.target });
1163
1191
  await deps.sendMarkdownReply(
1164
1192
  turn.chatId,
1165
1193
  turn.replyToMessageId,
1166
- fallbackMarkdown,
1167
- plannedReply?.replyMarkup
1168
- ? { replyMarkup: plannedReply.replyMarkup }
1169
- : undefined,
1194
+ finalText,
1195
+ { replyMarkup, target: turn.target },
1170
1196
  );
1171
- } catch (fallbackError) {
1172
- deps.recordRuntimeEvent?.("delivery", fallbackError, {
1173
- phase: "voice-fallback-text",
1174
- chatId: turn.chatId,
1175
- });
1176
1197
  }
1198
+ } catch (error) {
1199
+ deps.recordRuntimeEvent?.("delivery", error, {
1200
+ phase: "final-text",
1201
+ chatId: turn.chatId,
1202
+ replyToMessageId: turn.replyToMessageId,
1203
+ });
1177
1204
  }
1178
1205
  }
1206
+ if (outboundReply && deps.sendOutboundReplyArtifacts) {
1207
+ try {
1208
+ await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1209
+ replyToPrompt: !finalText,
1210
+ });
1211
+ } catch (error) {
1212
+ deps.recordRuntimeEvent?.("delivery", error, {
1213
+ phase: "voice-artifacts",
1214
+ chatId: turn.chatId,
1215
+ });
1216
+ // Fallback to planned text when voice delivery fails and text wasn't already delivered
1217
+ if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
1218
+ try {
1219
+ const fallbackMarkdown =
1220
+ plannedReply?.markdown || outboundReply?.voiceText || rawFinalText;
1221
+ await deps.sendMarkdownReply(
1222
+ turn.chatId,
1223
+ turn.replyToMessageId,
1224
+ fallbackMarkdown,
1225
+ plannedReply?.replyMarkup || turn.target
1226
+ ? { replyMarkup: plannedReply?.replyMarkup, target: turn.target }
1227
+ : undefined,
1228
+ );
1229
+ } catch (fallbackError) {
1230
+ deps.recordRuntimeEvent?.("delivery", fallbackError, {
1231
+ phase: "voice-fallback-text",
1232
+ chatId: turn.chatId,
1233
+ });
1234
+ }
1235
+ }
1236
+ }
1237
+ }
1238
+ if (endPlan.shouldSendAttachmentNotice) {
1239
+ await deps.sendTextReply(
1240
+ turn.chatId,
1241
+ turn.replyToMessageId,
1242
+ "Attached requested file(s).",
1243
+ { target: turn.target },
1244
+ );
1245
+ }
1246
+ await deps.sendQueuedAttachments(turn);
1247
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1248
+ };
1249
+ if (
1250
+ deps.scheduleActiveTurnDelivery &&
1251
+ (endPlan.kind === "text" || endPlan.kind === "attachments-only")
1252
+ ) {
1253
+ deps.scheduleActiveTurnDelivery(deliverActiveTurn);
1254
+ return;
1179
1255
  }
1180
- if (endPlan.shouldSendAttachmentNotice) {
1181
- await deps.sendTextReply(
1182
- turn.chatId,
1183
- turn.replyToMessageId,
1184
- "Attached requested file(s).",
1185
- );
1186
- }
1187
- await deps.sendQueuedAttachments(turn);
1188
- if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1256
+ await deliverActiveTurn();
1189
1257
  }
1190
1258
 
1191
1259
  // --- Session Runtime ---
@@ -1255,7 +1323,11 @@ export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
1255
1323
  clearPendingMediaGroups: () => void;
1256
1324
  clearModelMenuState: () => void;
1257
1325
  getActiveTurnChatId: () => number | undefined;
1258
- clearPreview: (chatId: number) => Promise<void>;
1326
+ getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
1327
+ clearPreview: (
1328
+ chatId: number,
1329
+ options?: { target?: TelegramQueueTarget },
1330
+ ) => Promise<void>;
1259
1331
  clearActiveTurn: () => void;
1260
1332
  clearAbort: () => void;
1261
1333
  stopPolling: () => Promise<void>;
@@ -1279,7 +1351,11 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
1279
1351
  clearPendingMediaGroups: () => void;
1280
1352
  clearModelMenuState: () => void;
1281
1353
  getActiveTurnChatId: () => number | undefined;
1282
- clearPreview: (chatId: number) => Promise<void>;
1354
+ getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
1355
+ clearPreview: (
1356
+ chatId: number,
1357
+ options?: { target?: TelegramQueueTarget },
1358
+ ) => Promise<void>;
1283
1359
  clearActiveTurn: () => void;
1284
1360
  clearAbort: () => void;
1285
1361
  stopPolling: () => Promise<void>;
@@ -1328,12 +1404,21 @@ export interface TelegramQueueMutationController<TContext> {
1328
1404
  append: (item: TelegramQueueItem<TContext>, ctx: TContext) => void;
1329
1405
  reorder: (ctx: TContext) => void;
1330
1406
  clear: (ctx: TContext) => number;
1331
- removeByMessageIds: (messageIds: number[], ctx: TContext) => number;
1332
- clearPriorityByMessageId: (messageId: number, ctx: TContext) => boolean;
1407
+ removeByMessageIds: (
1408
+ messageIds: number[],
1409
+ ctx: TContext,
1410
+ scope?: TelegramQueueMessageScope,
1411
+ ) => number;
1412
+ clearPriorityByMessageId: (
1413
+ messageId: number,
1414
+ ctx: TContext,
1415
+ scope?: TelegramQueueMessageScope,
1416
+ ) => boolean;
1333
1417
  prioritizeByMessageId: (
1334
1418
  messageId: number,
1335
1419
  ctx: TContext,
1336
1420
  priorityEmoji?: string,
1421
+ scope?: TelegramQueueMessageScope,
1337
1422
  ) => boolean;
1338
1423
  }
1339
1424
 
@@ -1445,7 +1530,8 @@ export async function shutdownTelegramSessionRuntime<TQueueItem>(
1445
1530
  deps.clearModelMenuState();
1446
1531
  const activeTurnChatId = deps.getActiveTurnChatId();
1447
1532
  if (activeTurnChatId !== undefined) {
1448
- await deps.clearPreview(activeTurnChatId);
1533
+ const target = deps.getActiveTurnTarget?.();
1534
+ await deps.clearPreview(activeTurnChatId, target ? { target } : undefined);
1449
1535
  }
1450
1536
  deps.clearActiveTurn();
1451
1537
  deps.clearAbort();
@@ -1485,6 +1571,7 @@ export function createTelegramSessionLifecycleRuntime<
1485
1571
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
1486
1572
  clearModelMenuState: deps.clearModelMenuState,
1487
1573
  getActiveTurnChatId: deps.getActiveTurnChatId,
1574
+ getActiveTurnTarget: deps.getActiveTurnTarget,
1488
1575
  clearPreview: deps.clearPreview,
1489
1576
  clearActiveTurn: deps.clearActiveTurn,
1490
1577
  clearAbort: deps.clearAbort,
@@ -1526,6 +1613,7 @@ export function createTelegramSessionLifecycleHooks<
1526
1613
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
1527
1614
  clearModelMenuState: deps.clearModelMenuState,
1528
1615
  getActiveTurnChatId: deps.getActiveTurnChatId,
1616
+ getActiveTurnTarget: deps.getActiveTurnTarget,
1529
1617
  clearPreview: deps.clearPreview,
1530
1618
  clearActiveTurn: deps.clearActiveTurn,
1531
1619
  clearAbort: deps.clearAbort,
@@ -1553,18 +1641,24 @@ export function createTelegramQueueMutationController<TContext>(
1553
1641
  appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)),
1554
1642
  reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1555
1643
  clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1556
- removeByMessageIds: (messageIds, ctx) =>
1644
+ removeByMessageIds: (messageIds, ctx, scope) =>
1557
1645
  removeTelegramQueueItemsByMessageIdsRuntime(
1558
1646
  messageIds,
1559
1647
  buildRuntimeDeps(ctx),
1648
+ scope,
1649
+ ),
1650
+ clearPriorityByMessageId: (messageId, ctx, scope) =>
1651
+ clearTelegramQueuePromptPriorityRuntime(
1652
+ messageId,
1653
+ buildRuntimeDeps(ctx),
1654
+ scope,
1560
1655
  ),
1561
- clearPriorityByMessageId: (messageId, ctx) =>
1562
- clearTelegramQueuePromptPriorityRuntime(messageId, buildRuntimeDeps(ctx)),
1563
- prioritizeByMessageId: (messageId, ctx, priorityEmoji) =>
1656
+ prioritizeByMessageId: (messageId, ctx, priorityEmoji, scope) =>
1564
1657
  prioritizeTelegramQueuePromptRuntime(
1565
1658
  messageId,
1566
1659
  buildRuntimeDeps(ctx),
1567
1660
  priorityEmoji,
1661
+ scope,
1568
1662
  ),
1569
1663
  };
1570
1664
  }
@@ -1607,10 +1701,12 @@ export function clearTelegramQueueItemsRuntime<TContext>(
1607
1701
  export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
1608
1702
  messageIds: number[],
1609
1703
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1704
+ scope?: TelegramQueueMessageScope,
1610
1705
  ): number {
1611
1706
  const { items, removedCount } = removeTelegramQueueItemsByMessageIds(
1612
1707
  deps.getQueuedItems(),
1613
1708
  messageIds,
1709
+ scope,
1614
1710
  );
1615
1711
  if (removedCount === 0) return 0;
1616
1712
  deps.setQueuedItems(items);
@@ -1625,10 +1721,12 @@ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
1625
1721
  export function clearTelegramQueuePromptPriorityRuntime<TContext>(
1626
1722
  messageId: number,
1627
1723
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1724
+ scope?: TelegramQueueMessageScope,
1628
1725
  ): boolean {
1629
1726
  const { changed, items } = clearTelegramQueuePromptPriority(
1630
1727
  deps.getQueuedItems(),
1631
1728
  messageId,
1729
+ scope,
1632
1730
  );
1633
1731
  if (!changed) return false;
1634
1732
  deps.setQueuedItems(items);
@@ -1640,6 +1738,7 @@ export function prioritizeTelegramQueuePromptRuntime<TContext>(
1640
1738
  messageId: number,
1641
1739
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1642
1740
  priorityEmoji?: string,
1741
+ scope?: TelegramQueueMessageScope,
1643
1742
  ): boolean {
1644
1743
  const nextPriorityReactionOrder = deps.getNextPriorityReactionOrder?.();
1645
1744
  if (nextPriorityReactionOrder === undefined) return false;
@@ -1648,6 +1747,7 @@ export function prioritizeTelegramQueuePromptRuntime<TContext>(
1648
1747
  messageId,
1649
1748
  nextPriorityReactionOrder,
1650
1749
  priorityEmoji,
1750
+ scope,
1651
1751
  );
1652
1752
  if (!changed) return false;
1653
1753
  deps.setQueuedItems(items);
@@ -1728,6 +1828,7 @@ export interface TelegramControlRuntimeDeps<
1728
1828
  chatId: number,
1729
1829
  replyToMessageId: number,
1730
1830
  text: string,
1831
+ options?: { target?: TelegramQueueTarget },
1731
1832
  ) => Promise<number | undefined>;
1732
1833
  onSettled: () => void;
1733
1834
  }
@@ -1749,6 +1850,7 @@ export async function executeTelegramControlItemRuntime<TContext>(
1749
1850
  item.chatId,
1750
1851
  item.replyToMessageId,
1751
1852
  `Telegram control action failed: ${message}`,
1853
+ { target: item.target },
1752
1854
  );
1753
1855
  } finally {
1754
1856
  deps.onSettled();
package/lib/rendering.ts CHANGED
@@ -463,31 +463,6 @@ function matchMarkdownHeadingLine(line: string): RegExpMatchArray | null {
463
463
  return line.match(/^(\s*)#{1,6}\s+(.+)$/);
464
464
  }
465
465
 
466
- function endsWithMarkdownHeadingLine(markdown: string): boolean {
467
- const lines = markdown.split("\n");
468
- for (let index = lines.length - 1; index >= 0; index -= 1) {
469
- const line = lines[index] ?? "";
470
- if (line.trim().length === 0) continue;
471
- return matchMarkdownHeadingLine(line) !== null;
472
- }
473
- return false;
474
- }
475
-
476
- function splitLeadingMarkdownBlankLines(markdown: string): {
477
- blankLines: number;
478
- remainingText: string;
479
- } {
480
- const lines = markdown.split("\n");
481
- let start = 0;
482
- while (start < lines.length && (lines[start] ?? "").trim().length === 0) {
483
- start += 1;
484
- }
485
- return {
486
- blankLines: start,
487
- remainingText: lines.slice(start).join("\n"),
488
- };
489
- }
490
-
491
466
  // --- UI Markdown-to-Telegram-HTML Rendering ---
492
467
 
493
468
  function renderDelimitedInlineStyle(