@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -25,8 +25,8 @@ import {
25
25
  TELEGRAM_MESSAGE_PROMPT_SNIPPET,
26
26
  } from "./prompts.ts";
27
27
  import {
28
- withTelegramReplyParameters,
29
28
  normalizeTelegramNativeMarkdown,
29
+ withTelegramReplyParameters,
30
30
  } from "./replies.ts";
31
31
  import {
32
32
  getTelegramTargetThreadParams,
@@ -37,6 +37,7 @@ const MAX_ATTACHMENTS_PER_TURN = 10;
37
37
 
38
38
  export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
39
39
 
40
+ // Deliberately local: outbound attachment delivery stays decoupled from Bot API helpers.
40
41
  export function getTelegramOutboundAttachmentByteLimitFromEnv(
41
42
  env: NodeJS.ProcessEnv,
42
43
  names: string[],
@@ -86,12 +87,10 @@ export interface TelegramOutboundMessagePlan {
86
87
  replyMarkup?: unknown;
87
88
  }
88
89
 
89
- export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
90
- getDefaultChatId: () => number | undefined;
90
+ export interface TelegramOutboundMessageDeliveryPorts {
91
+ getDefaultChatId?: () => number | undefined;
91
92
  getDefaultTarget?: () => TelegramTarget | undefined;
92
- getActiveTurn?: () =>
93
- | { chatId: number; target?: TelegramTarget }
94
- | undefined;
93
+ getActiveTurn?: () => { chatId: number; target?: TelegramTarget } | undefined;
95
94
  resolveAgentTarget?: (
96
95
  selector: TelegramBusAgentTargetSelector,
97
96
  ) => Promise<TelegramTarget & { threadId: number }>;
@@ -116,6 +115,13 @@ export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOut
116
115
  ) => Promise<number | undefined>;
117
116
  }
118
117
 
118
+ export interface TelegramOutboundMessageToolRegistrationDeps
119
+ extends
120
+ TelegramOutboundMessageDeliveryPorts,
121
+ TelegramOutboundAttachmentRuntimeEventRecorderPort {
122
+ getDefaultChatId: () => number | undefined;
123
+ }
124
+
119
125
  export interface TelegramQueuedOutboundAttachmentView {
120
126
  path: string;
121
127
  fileName: string;
@@ -136,7 +142,9 @@ class TelegramRichAttachmentCommitUnknownError extends Error {
136
142
  readonly kind = "commit-unknown" as const;
137
143
 
138
144
  constructor(cause: unknown) {
139
- super("Telegram Rich media upload may have committed without a message id.");
145
+ super(
146
+ "Telegram Rich media upload may have committed without a message id.",
147
+ );
140
148
  this.name = "TelegramRichAttachmentCommitUnknownError";
141
149
  this.cause = cause;
142
150
  }
@@ -180,15 +188,16 @@ export function planTelegramRichOutboundAttachment(options: {
180
188
  if (options.turn.queuedAttachments.length !== 1) return undefined;
181
189
  const attachment = options.turn.queuedAttachments[0]!;
182
190
  const normalizedPath = attachment.path.toLowerCase();
183
- const mediaType = normalizedPath.endsWith(".jpg") ||
184
- normalizedPath.endsWith(".jpeg") ||
185
- normalizedPath.endsWith(".png")
186
- ? "photo"
187
- : normalizedPath.endsWith(".mp4")
188
- ? "video"
189
- : normalizedPath.endsWith(".mp3")
190
- ? "audio"
191
- : undefined;
191
+ const mediaType =
192
+ normalizedPath.endsWith(".jpg") ||
193
+ normalizedPath.endsWith(".jpeg") ||
194
+ normalizedPath.endsWith(".png")
195
+ ? "photo"
196
+ : normalizedPath.endsWith(".mp4")
197
+ ? "video"
198
+ : normalizedPath.endsWith(".mp3")
199
+ ? "audio"
200
+ : undefined;
192
201
  if (!mediaType) return undefined;
193
202
  const mediaId = "artifact";
194
203
  const richMessage = {
@@ -237,18 +246,27 @@ export function createTelegramRichOutboundAttachmentSender(
237
246
  if (!plan) return false;
238
247
  try {
239
248
  const result = await withTelegramReplyParameters(
240
- turn.chatId, turn.replyToMessageId, turn.target,
241
- (replyParameters) => deps.sendMultipart(
242
- plan.method,
243
- { ...plan.fields, ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}) },
244
- plan.fileField,
245
- plan.filePath,
246
- plan.fileName,
247
- ),
249
+ turn.chatId,
250
+ turn.replyToMessageId,
251
+ turn.target,
252
+ (replyParameters) =>
253
+ deps.sendMultipart(
254
+ plan.method,
255
+ {
256
+ ...plan.fields,
257
+ ...(replyParameters
258
+ ? { reply_parameters: JSON.stringify(replyParameters) }
259
+ : {}),
260
+ },
261
+ plan.fileField,
262
+ plan.filePath,
263
+ plan.fileName,
264
+ ),
248
265
  );
249
266
  const messageId =
250
- result && typeof result === "object" &&
251
- Number.isInteger((result as { message_id?: unknown }).message_id)
267
+ result &&
268
+ typeof result === "object" &&
269
+ Number.isInteger((result as { message_id?: unknown }).message_id)
252
270
  ? (result as { message_id: number }).message_id
253
271
  : undefined;
254
272
  if (messageId === undefined) {
@@ -361,8 +379,22 @@ function formatTelegramOutboundAttachmentToolResultText(
361
379
  return ["", `${verb} ${count} Telegram attachment(s).`].join("\n");
362
380
  }
363
381
 
364
- function formatTelegramOutboundMessageToolResultText(chatId: number | string): string {
365
- return ["", `Sent Telegram message to ${chatId}.`].join("\n");
382
+ function buildTelegramOutboundMessageToolResult(
383
+ chatId: number | string,
384
+ messageId: number | undefined,
385
+ ): {
386
+ content: Array<{ type: "text"; text: string }>;
387
+ details: { chatId: number | string; messageId?: number };
388
+ } {
389
+ return {
390
+ content: [
391
+ {
392
+ type: "text",
393
+ text: ["", `Sent Telegram message to ${chatId}.`].join("\n"),
394
+ },
395
+ ],
396
+ details: { chatId, messageId },
397
+ };
366
398
  }
367
399
 
368
400
  function getTelegramMultipartTargetFields(
@@ -450,7 +482,9 @@ async function buildTelegramOutboundAttachmentViews(options: {
450
482
 
451
483
  function formatTelegramOutboundToolError(error: unknown): Error {
452
484
  const message = error instanceof Error ? error.message : String(error);
453
- return new Error(`\n${message.replace(/^\n+/u, "") || "Telegram outbound operation failed."}`);
485
+ return new Error(
486
+ `\n${message.replace(/^\n+/u, "") || "Telegram outbound operation failed."}`,
487
+ );
454
488
  }
455
489
 
456
490
  export function registerTelegramOutboundAttachmentTool(
@@ -540,19 +574,25 @@ export function registerTelegramOutboundMessageTool(
540
574
  }),
541
575
  ),
542
576
  chat_id: Type.Optional(
543
- Type.Union([
544
- Type.Number(),
545
- Type.String({ pattern: "^@[A-Za-z0-9_]{5,32}$" }),
546
- ], { description: "Optional exact Telegram chat id or public channel @username" }),
577
+ Type.Union(
578
+ [Type.Number(), Type.String({ pattern: "^@[A-Za-z0-9_]{5,32}$" })],
579
+ {
580
+ description:
581
+ "Optional exact Telegram chat id or public channel @username",
582
+ },
583
+ ),
547
584
  ),
548
585
  thread_id: Type.Optional(
549
586
  Type.Number({
550
587
  description: "Optional Telegram topic thread id with chat_id",
551
588
  }),
552
589
  ),
553
- channel: Type.Optional(Type.Boolean({
554
- description: "Set true only when a numeric chat_id is explicitly intended as a channel",
555
- })),
590
+ channel: Type.Optional(
591
+ Type.Boolean({
592
+ description:
593
+ "Set true only when a numeric chat_id is explicitly intended as a channel",
594
+ }),
595
+ ),
556
596
  thread: Type.Optional(
557
597
  Type.Union([Type.String(), Type.Number()], {
558
598
  description:
@@ -582,12 +622,17 @@ export function registerTelegramOutboundMessageTool(
582
622
  sendChannelMediaMessage: deps.sendChannelMediaMessage,
583
623
  });
584
624
  } catch (error) {
585
- const isChannelTarget = typeof params.chat_id === "string" || params.channel === true;
586
- const reportableError = isChannelTarget &&
587
- !isTelegramChannelPostValidationError(error)
588
- ? new Error("Telegram channel publication failed; inspect the retained local record before retrying.")
589
- : error;
590
- deps.recordRuntimeEvent?.("message", reportableError, { phase: "direct" });
625
+ const isChannelTarget =
626
+ typeof params.chat_id === "string" || params.channel === true;
627
+ const reportableError =
628
+ isChannelTarget && !isTelegramChannelPostValidationError(error)
629
+ ? new Error(
630
+ "Telegram channel publication failed; inspect the retained local record before retrying.",
631
+ )
632
+ : error;
633
+ deps.recordRuntimeEvent?.("message", reportableError, {
634
+ phase: "direct",
635
+ });
591
636
  throw formatTelegramOutboundToolError(reportableError);
592
637
  }
593
638
  },
@@ -702,7 +747,9 @@ export async function deliverTelegramGuestCachedAttachment(options: {
702
747
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
703
748
  recordRuntimeEvent?: TelegramOutboundAttachmentRuntimeEventRecorderPort["recordRuntimeEvent"];
704
749
  }): Promise<void> {
705
- const transport = getTelegramGuestAttachmentTransport(options.attachment.path);
750
+ const transport = getTelegramGuestAttachmentTransport(
751
+ options.attachment.path,
752
+ );
706
753
  let stagingMessageId: number | undefined;
707
754
  let answerAttempted = false;
708
755
  try {
@@ -725,7 +772,8 @@ export async function deliverTelegramGuestCachedAttachment(options: {
725
772
  const photo = [...(message.photo ?? [])]
726
773
  .sort((left, right) => (left.file_size ?? 0) - (right.file_size ?? 0))
727
774
  .at(-1);
728
- if (!photo?.file_id) throw new Error("Guest staging upload returned no photo file_id");
775
+ if (!photo?.file_id)
776
+ throw new Error("Guest staging upload returned no photo file_id");
729
777
  result = {
730
778
  type: "photo",
731
779
  id: "attachment-1",
@@ -765,11 +813,7 @@ export async function deliverTelegramGuestCachedAttachment(options: {
765
813
  answerAttempted = true;
766
814
  await options.answerGuestQuery(options.guestQueryId, result);
767
815
  } catch (error) {
768
- if (
769
- !answerAttempted &&
770
- options.answerGuestText &&
771
- options.fallbackText
772
- ) {
816
+ if (!answerAttempted && options.answerGuestText && options.fallbackText) {
773
817
  answerAttempted = true;
774
818
  await options.answerGuestText(options.guestQueryId, options.fallbackText);
775
819
  } else {
@@ -790,54 +834,39 @@ export async function deliverTelegramGuestCachedAttachment(options: {
790
834
  }
791
835
  }
792
836
 
793
- export async function sendTelegramOutboundMessage(options: {
794
- text: string;
795
- media?: string;
796
- operationId?: string;
797
- channel?: boolean;
798
- chatId?: number | string;
799
- threadId?: number;
800
- agentThread?: string | number;
801
- target?: TelegramTarget;
802
- getDefaultChatId?: () => number | undefined;
803
- getDefaultTarget?: () => TelegramTarget | undefined;
804
- getActiveTurn?: () =>
805
- | { chatId: number; target?: TelegramTarget }
806
- | undefined;
807
- resolveAgentTarget?: (
808
- selector: TelegramBusAgentTargetSelector,
809
- ) => Promise<TelegramTarget & { threadId: number }>;
810
- routeAgentMessage?: (message: TelegramBusAgentMessage) => Promise<void>;
811
- canSendDirect: () => boolean;
812
- planMessage: (markdown: string) => TelegramOutboundMessagePlan;
813
- sendMarkdownMessage: (
814
- chatId: number,
815
- markdown: string,
816
- options?: { replyMarkup?: unknown; target?: TelegramTarget },
817
- ) => Promise<number | undefined>;
818
- sendChannelMarkdownMessage?: (
819
- channel: number | string,
820
- markdown: string,
821
- options: { operationId: string; replyMarkup?: unknown },
822
- ) => Promise<number | undefined>;
823
- sendChannelMediaMessage?: (
824
- channel: number | string,
825
- mediaPath: string,
826
- markdown: string,
827
- options: { operationId: string; replyMarkup?: unknown },
828
- ) => Promise<number | undefined>;
829
- }): Promise<{
837
+ export async function sendTelegramOutboundMessage(
838
+ options: TelegramOutboundMessageDeliveryPorts & {
839
+ text: string;
840
+ media?: string;
841
+ operationId?: string;
842
+ channel?: boolean;
843
+ chatId?: number | string;
844
+ threadId?: number;
845
+ agentThread?: string | number;
846
+ target?: TelegramTarget;
847
+ },
848
+ ): Promise<{
830
849
  content: Array<{ type: "text"; text: string }>;
831
850
  details: { chatId: number | string; messageId?: number };
832
851
  }> {
833
852
  assertTelegramDirectDeliveryAllowed(options.canSendDirect);
834
853
  const activeTurn = options.getActiveTurn?.();
835
854
  if (typeof options.chatId === "string" || options.channel === true) {
836
- if (!((typeof options.chatId === "string" && /^@[A-Za-z0-9_]{5,32}$/u.test(options.chatId)) ||
837
- (typeof options.chatId === "number" && Number.isSafeInteger(options.chatId) && options.chatId < 0)) ||
838
- options.threadId !== undefined ||
839
- options.target !== undefined || options.agentThread !== undefined) {
840
- throw new Error("Telegram channel delivery requires one exact @username or negative numeric channel ID without a thread target.");
855
+ if (
856
+ !(
857
+ (typeof options.chatId === "string" &&
858
+ /^@[A-Za-z0-9_]{5,32}$/u.test(options.chatId)) ||
859
+ (typeof options.chatId === "number" &&
860
+ Number.isSafeInteger(options.chatId) &&
861
+ options.chatId < 0)
862
+ ) ||
863
+ options.threadId !== undefined ||
864
+ options.target !== undefined ||
865
+ options.agentThread !== undefined
866
+ ) {
867
+ throw new Error(
868
+ "Telegram channel delivery requires one exact @username or negative numeric channel ID without a thread target.",
869
+ );
841
870
  }
842
871
  const plan = options.planMessage(options.text);
843
872
  if (options.media !== undefined) {
@@ -847,21 +876,24 @@ export async function sendTelegramOutboundMessage(options: {
847
876
  );
848
877
  }
849
878
  const messageId = await options.sendChannelMediaMessage(
850
- options.chatId, options.media, plan.markdown,
851
- { operationId: options.operationId, replyMarkup: plan.replyMarkup });
852
- return { content: [{ type: "text",
853
- text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
854
- details: { chatId: options.chatId, messageId } };
879
+ options.chatId,
880
+ options.media,
881
+ plan.markdown,
882
+ { operationId: options.operationId, replyMarkup: plan.replyMarkup },
883
+ );
884
+ return buildTelegramOutboundMessageToolResult(options.chatId, messageId);
855
885
  }
856
886
  if (!options.sendChannelMarkdownMessage || !options.operationId) {
857
- throw new Error("Telegram channel delivery requires direct leader transport ownership and operation identity.");
887
+ throw new Error(
888
+ "Telegram channel delivery requires direct leader transport ownership and operation identity.",
889
+ );
858
890
  }
859
891
  const messageId = await options.sendChannelMarkdownMessage(
860
- options.chatId, plan.markdown, { operationId: options.operationId,
861
- replyMarkup: plan.replyMarkup });
862
- return { content: [{ type: "text",
863
- text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
864
- details: { chatId: options.chatId, messageId } };
892
+ options.chatId,
893
+ plan.markdown,
894
+ { operationId: options.operationId, replyMarkup: plan.replyMarkup },
895
+ );
896
+ return buildTelegramOutboundMessageToolResult(options.chatId, messageId);
865
897
  }
866
898
  if (options.media !== undefined) {
867
899
  throw new TelegramChannelPostValidationError(
@@ -876,7 +908,7 @@ export async function sendTelegramOutboundMessage(options: {
876
908
  : activeTurn &&
877
909
  options.resolveAgentTarget &&
878
910
  options.routeAgentMessage &&
879
- ((options.target?.threadId !== undefined) ||
911
+ (options.target?.threadId !== undefined ||
880
912
  (options.chatId !== undefined && options.threadId !== undefined))
881
913
  ? {
882
914
  chatId: options.target?.chatId ?? options.chatId,
@@ -930,15 +962,7 @@ export async function sendTelegramOutboundMessage(options: {
930
962
  text: options.text,
931
963
  });
932
964
  }
933
- return {
934
- content: [
935
- {
936
- type: "text",
937
- text: formatTelegramOutboundMessageToolResultText(chatId),
938
- },
939
- ],
940
- details: { chatId, messageId },
941
- };
965
+ return buildTelegramOutboundMessageToolResult(chatId, messageId);
942
966
  }
943
967
 
944
968
  export async function sendTelegramOutboundFiles(options: {
@@ -1017,7 +1041,9 @@ export function createTelegramQueuedOutboundAttachmentSender(
1017
1041
  ): Promise<void> => {
1018
1042
  await sendQueuedTelegramOutboundAttachments(turn, {
1019
1043
  ...deps,
1020
- isDeliveryActive: () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false,
1044
+ isDeliveryActive: () =>
1045
+ deps.isDeliveryActive?.() !== false &&
1046
+ options?.isDeliveryActive?.() !== false,
1021
1047
  maxAttachmentSizeBytes:
1022
1048
  deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
1023
1049
  });
@@ -1047,18 +1073,23 @@ export async function sendQueuedTelegramOutboundAttachments(
1047
1073
  const method = isPhoto ? "sendPhoto" : "sendDocument";
1048
1074
  const fieldName = isPhoto ? "photo" : "document";
1049
1075
  await withTelegramReplyParameters(
1050
- turn.chatId, turn.replyToMessageId, turn.target,
1051
- (replyParameters) => deps.sendMultipart(
1052
- method,
1053
- {
1054
- chat_id: String(turn.chatId),
1055
- ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
1056
- ...getTelegramMultipartTargetFields(turn.target),
1057
- },
1058
- fieldName,
1059
- attachment.path,
1060
- attachment.fileName,
1061
- ),
1076
+ turn.chatId,
1077
+ turn.replyToMessageId,
1078
+ turn.target,
1079
+ (replyParameters) =>
1080
+ deps.sendMultipart(
1081
+ method,
1082
+ {
1083
+ chat_id: String(turn.chatId),
1084
+ ...(replyParameters
1085
+ ? { reply_parameters: JSON.stringify(replyParameters) }
1086
+ : {}),
1087
+ ...getTelegramMultipartTargetFields(turn.target),
1088
+ },
1089
+ fieldName,
1090
+ attachment.path,
1091
+ attachment.fileName,
1092
+ ),
1062
1093
  );
1063
1094
  } catch (error) {
1064
1095
  if (deps.isDeliveryActive?.() === false) return;
@@ -169,24 +169,26 @@ export function createTelegramButtonActionStore(
169
169
  };
170
170
  }
171
171
 
172
- const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN =
173
- "☑️ **Choose an option:**";
172
+ const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN = "☑️ **Choose an option:**";
174
173
 
175
174
  function escapeTelegramRichButtonText(text: string): string {
176
- return text.replace(/[&<>"'`\\*_\[\]{}$~\r\n]/g, (character) =>
177
- `&#${character.charCodeAt(0)};`,
175
+ return text.replace(
176
+ /[&<>"'`\\*_\[\]{}$~\r\n]/g,
177
+ (character) => `&#${character.charCodeAt(0)};`,
178
178
  );
179
179
  }
180
180
 
181
181
  function renderTelegramRichButtonRow(
182
182
  row: TelegramOutboundButtonMarkup["inline_keyboard"][number],
183
183
  ): string {
184
- return `<tg-button-row>${row.map((button) => {
185
- const attributes = button.disabled
186
- ? 'type="disabled"'
187
- : `type="callback_data" data="${escapeTelegramRichButtonText(button.callback_data)}"`;
188
- return `<tg-button ${attributes}>${escapeTelegramRichButtonText(button.text)}</tg-button>`;
189
- }).join("")}</tg-button-row>`;
184
+ return `<tg-button-row>${row
185
+ .map((button) => {
186
+ const attributes = button.disabled
187
+ ? 'type="disabled"'
188
+ : `type="callback_data" data="${escapeTelegramRichButtonText(button.callback_data)}"`;
189
+ return `<tg-button ${attributes}>${escapeTelegramRichButtonText(button.text)}</tg-button>`;
190
+ })
191
+ .join("")}</tg-button-row>`;
190
192
  }
191
193
 
192
194
  export function planTelegramButtonReply(
@@ -202,40 +204,60 @@ export function planTelegramButtonReply(
202
204
  payloadRows: Record<string, unknown>[][],
203
205
  rich: boolean,
204
206
  ): TelegramOutboundButtonMarkup["inline_keyboard"] | undefined => {
205
- const actions = payloadRows.map((row) => row.map(parseTelegramButtonAction));
207
+ const actions = payloadRows.map((row) =>
208
+ row.map(parseTelegramButtonAction),
209
+ );
206
210
  if (actions.some((row) => row.some((action) => !action))) return undefined;
207
- if (rich && actions.some((row) => {
208
- const projected = row.map((action) => action!.disabled
209
- ? { text: action!.text || "\u00a0", disabled: {} }
210
- : { text: action!.text, callback_data: "x".repeat(64) });
211
- return row.length > 8 || renderTelegramRichButtonRow(projected).length > 32768;
212
- })) return undefined;
213
- return actions.map((row) => row.map((action) => action!.disabled
214
- ? { text: action!.text || "\u00a0", disabled: {} }
215
- : {
216
- text: action!.text,
217
- callback_data: deps.registerAction({
218
- ...action!,
219
- ...(deps.binding ? { binding: deps.binding } : {}),
220
- }),
221
- }));
211
+ if (
212
+ rich &&
213
+ actions.some((row) => {
214
+ const projected = row.map((action) =>
215
+ action!.disabled
216
+ ? { text: action!.text || "\u00a0", disabled: {} }
217
+ : { text: action!.text, callback_data: "x".repeat(64) },
218
+ );
219
+ return (
220
+ row.length > 8 ||
221
+ renderTelegramRichButtonRow(projected).length > 32768
222
+ );
223
+ })
224
+ )
225
+ return undefined;
226
+ return actions.map((row) =>
227
+ row.map((action) =>
228
+ action!.disabled
229
+ ? { text: action!.text || "\u00a0", disabled: {} }
230
+ : {
231
+ text: action!.text,
232
+ callback_data: deps.registerAction({
233
+ ...action!,
234
+ ...(deps.binding ? { binding: deps.binding } : {}),
235
+ }),
236
+ },
237
+ ),
238
+ );
222
239
  };
223
- const withRichButtons = replaceTelegramButtonFences(markdown, (payload, closed) => {
224
- if (!closed) return "";
225
- const payloadRows = parseTelegramButtonPayloadRows(payload);
226
- if (!payloadRows) return "";
227
- const rich = deps.rendering !== "html";
228
- const rows = buildRows(payloadRows, rich);
229
- if (!rows) return "";
230
- if (!rich) {
231
- keyboard.push(...rows);
232
- return "";
233
- }
234
- return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
235
- });
240
+ const withRichButtons = replaceTelegramButtonFences(
241
+ markdown,
242
+ (payload, closed) => {
243
+ if (!closed) return "";
244
+ const payloadRows = parseTelegramButtonPayloadRows(payload);
245
+ if (!payloadRows) return "";
246
+ const rich = deps.rendering !== "html";
247
+ const rows = buildRows(payloadRows, rich);
248
+ if (!rows) return "";
249
+ if (!rich) {
250
+ keyboard.push(...rows);
251
+ return "";
252
+ }
253
+ return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
254
+ },
255
+ );
236
256
  const stripped = replaceTopLevelHtmlComments(withRichButtons, (comment) => {
237
257
  const command = "telegram_button";
238
- const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
258
+ const normalizedContent = comment.content
259
+ .replace(/^\s+/, "")
260
+ .replace(/^!/, "");
239
261
  if (!normalizedContent.startsWith(command)) return comment.raw;
240
262
  const payloadRows = parseTelegramActionPayloadRows(comment, command);
241
263
  if (!payloadRows) return "";
@@ -297,7 +319,8 @@ export function markTelegramButtonSelected(
297
319
  let matched = false;
298
320
  const inlineKeyboard = replyMarkup.inline_keyboard.map((row) =>
299
321
  row.map((button) => {
300
- if (button.disabled || button.callback_data !== callbackData) return { ...button };
322
+ if (button.disabled || button.callback_data !== callbackData)
323
+ return { ...button };
301
324
  matched = true;
302
325
  return { ...button, style: selectedStyle };
303
326
  }),
@@ -314,21 +337,21 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
314
337
 
315
338
  if (!action) {
316
339
  if (query.data?.startsWith(`${TELEGRAM_BUTTON_CALLBACK_PREFIX}:`)) {
317
- await deps.answerCallbackQuery(query.id, "Button action expired.");
340
+ await deps.answerCallbackQuery(query.id, "Button action expired");
318
341
  return true;
319
342
  }
320
343
  return false;
321
344
  }
322
345
 
323
346
  if (action.disabled) {
324
- await deps.answerCallbackQuery(query.id, "Button action unavailable.");
347
+ await deps.answerCallbackQuery(query.id, "Button action unavailable");
325
348
  return true;
326
349
  }
327
350
 
328
351
  const chatId = query.message?.chat?.id;
329
352
  const messageId = query.message?.message_id;
330
353
  if (typeof chatId !== "number" || typeof messageId !== "number") {
331
- await deps.answerCallbackQuery(query.id, "Button action expired.");
354
+ await deps.answerCallbackQuery(query.id, "Button action expired");
332
355
  return true;
333
356
  }
334
357
 
@@ -344,24 +367,28 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
344
367
  );
345
368
  if (selectedMarkup && deps.editMessageReplyMarkup) {
346
369
  try {
347
- await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
370
+ await deps.editMessageReplyMarkup(
371
+ chatId,
372
+ messageId,
373
+ selectedMarkup,
374
+ );
348
375
  } catch {
349
376
  // The action already succeeded; old-surface styling is best-effort.
350
377
  }
351
378
  }
352
379
  }
353
- await deps.answerCallbackQuery(query.id, "Done.");
380
+ await deps.answerCallbackQuery(query.id, "Done");
354
381
  return true;
355
382
  }
356
383
  } catch (error) {
357
- await deps.answerCallbackQuery(query.id, "Generative App action failed.");
384
+ await deps.answerCallbackQuery(query.id, "Generative App action failed");
358
385
  throw error;
359
386
  }
360
387
  }
361
388
 
362
389
  const enqueued = deps.enqueueButtonPrompt(query, action, ctx);
363
390
  if (enqueued === false) {
364
- await deps.answerCallbackQuery(query.id, "Already queued.");
391
+ await deps.answerCallbackQuery(query.id, "Already queued");
365
392
  return true;
366
393
  }
367
394
  const selectedMarkup =
@@ -375,6 +402,6 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
375
402
  if (selectedMarkup && deps.editMessageReplyMarkup) {
376
403
  await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
377
404
  }
378
- await deps.answerCallbackQuery(query.id, "Queued.");
405
+ await deps.answerCallbackQuery(query.id, "Queued");
379
406
  return true;
380
407
  }