@llblab/pi-telegram 0.52.2 → 0.53.1

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 +15 -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 +65 -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 +252 -127
  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 +128 -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 +516 -301
  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
@@ -37,8 +37,8 @@ export interface TelegramOutboundMessagePlan {
37
37
  markdown: string;
38
38
  replyMarkup?: unknown;
39
39
  }
40
- export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOutboundAttachmentRuntimeEventRecorderPort {
41
- getDefaultChatId: () => number | undefined;
40
+ export interface TelegramOutboundMessageDeliveryPorts {
41
+ getDefaultChatId?: () => number | undefined;
42
42
  getDefaultTarget?: () => TelegramTarget | undefined;
43
43
  getActiveTurn?: () => {
44
44
  chatId: number;
@@ -63,6 +63,9 @@ export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOut
63
63
  replyMarkup?: unknown;
64
64
  }) => Promise<number | undefined>;
65
65
  }
66
+ export interface TelegramOutboundMessageToolRegistrationDeps extends TelegramOutboundMessageDeliveryPorts, TelegramOutboundAttachmentRuntimeEventRecorderPort {
67
+ getDefaultChatId: () => number | undefined;
68
+ }
66
69
  export interface TelegramQueuedOutboundAttachmentView {
67
70
  path: string;
68
71
  fileName: string;
@@ -169,7 +172,7 @@ export declare function deliverTelegramGuestCachedAttachment(options: {
169
172
  deleteMessage: (chatId: number, messageId: number) => Promise<void>;
170
173
  recordRuntimeEvent?: TelegramOutboundAttachmentRuntimeEventRecorderPort["recordRuntimeEvent"];
171
174
  }): Promise<void>;
172
- export declare function sendTelegramOutboundMessage(options: {
175
+ export declare function sendTelegramOutboundMessage(options: TelegramOutboundMessageDeliveryPorts & {
173
176
  text: string;
174
177
  media?: string;
175
178
  operationId?: string;
@@ -178,30 +181,6 @@ export declare function sendTelegramOutboundMessage(options: {
178
181
  threadId?: number;
179
182
  agentThread?: string | number;
180
183
  target?: TelegramTarget;
181
- getDefaultChatId?: () => number | undefined;
182
- getDefaultTarget?: () => TelegramTarget | undefined;
183
- getActiveTurn?: () => {
184
- chatId: number;
185
- target?: TelegramTarget;
186
- } | undefined;
187
- resolveAgentTarget?: (selector: TelegramBusAgentTargetSelector) => Promise<TelegramTarget & {
188
- threadId: number;
189
- }>;
190
- routeAgentMessage?: (message: TelegramBusAgentMessage) => Promise<void>;
191
- canSendDirect: () => boolean;
192
- planMessage: (markdown: string) => TelegramOutboundMessagePlan;
193
- sendMarkdownMessage: (chatId: number, markdown: string, options?: {
194
- replyMarkup?: unknown;
195
- target?: TelegramTarget;
196
- }) => Promise<number | undefined>;
197
- sendChannelMarkdownMessage?: (channel: number | string, markdown: string, options: {
198
- operationId: string;
199
- replyMarkup?: unknown;
200
- }) => Promise<number | undefined>;
201
- sendChannelMediaMessage?: (channel: number | string, mediaPath: string, markdown: string, options: {
202
- operationId: string;
203
- replyMarkup?: unknown;
204
- }) => Promise<number | undefined>;
205
184
  }): Promise<{
206
185
  content: Array<{
207
186
  type: "text";
@@ -8,10 +8,11 @@ import { basename } from "node:path";
8
8
  import { Type } from "@sinclair/typebox";
9
9
  import { isTelegramChannelPostValidationError, TelegramChannelPostValidationError, } from "./channel-posts.js";
10
10
  import { TELEGRAM_ATTACH_PROMPT_GUIDELINES, TELEGRAM_ATTACH_PROMPT_SNIPPET, TELEGRAM_MESSAGE_PROMPT_GUIDELINES, TELEGRAM_MESSAGE_PROMPT_SNIPPET, } from "./prompts.js";
11
- import { withTelegramReplyParameters, normalizeTelegramNativeMarkdown, } from "./replies.js";
11
+ import { normalizeTelegramNativeMarkdown, withTelegramReplyParameters, } from "./replies.js";
12
12
  import { getTelegramTargetThreadParams, } from "./target.js";
13
13
  const MAX_ATTACHMENTS_PER_TURN = 10;
14
14
  export const TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
15
+ // Deliberately local: outbound attachment delivery stays decoupled from Bot API helpers.
15
16
  export function getTelegramOutboundAttachmentByteLimitFromEnv(env, names, defaultValue = TELEGRAM_OUTBOUND_ATTACHMENT_DEFAULT_MAX_BYTES) {
16
17
  for (const name of names) {
17
18
  const rawValue = env[name]?.trim();
@@ -102,8 +103,14 @@ export function createTelegramRichOutboundAttachmentSender(deps) {
102
103
  if (!plan)
103
104
  return false;
104
105
  try {
105
- const result = await withTelegramReplyParameters(turn.chatId, turn.replyToMessageId, turn.target, (replyParameters) => deps.sendMultipart(plan.method, { ...plan.fields, ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}) }, plan.fileField, plan.filePath, plan.fileName));
106
- const messageId = result && typeof result === "object" &&
106
+ const result = await withTelegramReplyParameters(turn.chatId, turn.replyToMessageId, turn.target, (replyParameters) => deps.sendMultipart(plan.method, {
107
+ ...plan.fields,
108
+ ...(replyParameters
109
+ ? { reply_parameters: JSON.stringify(replyParameters) }
110
+ : {}),
111
+ }, plan.fileField, plan.filePath, plan.fileName));
112
+ const messageId = result &&
113
+ typeof result === "object" &&
107
114
  Number.isInteger(result.message_id)
108
115
  ? result.message_id
109
116
  : undefined;
@@ -162,8 +169,16 @@ function formatTelegramOutboundAttachmentToolResultText(count, mode = "queued")
162
169
  const verb = mode === "queued" ? "Queued" : "Sent";
163
170
  return ["", `${verb} ${count} Telegram attachment(s).`].join("\n");
164
171
  }
165
- function formatTelegramOutboundMessageToolResultText(chatId) {
166
- return ["", `Sent Telegram message to ${chatId}.`].join("\n");
172
+ function buildTelegramOutboundMessageToolResult(chatId, messageId) {
173
+ return {
174
+ content: [
175
+ {
176
+ type: "text",
177
+ text: ["", `Sent Telegram message to ${chatId}.`].join("\n"),
178
+ },
179
+ ],
180
+ details: { chatId, messageId },
181
+ };
167
182
  }
168
183
  function getTelegramMultipartTargetFields(target) {
169
184
  if (!target)
@@ -280,10 +295,9 @@ export function registerTelegramOutboundMessageTool(pi, deps) {
280
295
  minLength: 1,
281
296
  description: "Local single-file channel upload: .jpg/.jpeg/.png/.webp photo or .mp4 video; the text becomes its caption (max 1024 characters). Albums and other media types are rejected.",
282
297
  })),
283
- chat_id: Type.Optional(Type.Union([
284
- Type.Number(),
285
- Type.String({ pattern: "^@[A-Za-z0-9_]{5,32}$" }),
286
- ], { description: "Optional exact Telegram chat id or public channel @username" })),
298
+ chat_id: Type.Optional(Type.Union([Type.Number(), Type.String({ pattern: "^@[A-Za-z0-9_]{5,32}$" })], {
299
+ description: "Optional exact Telegram chat id or public channel @username",
300
+ })),
287
301
  thread_id: Type.Optional(Type.Number({
288
302
  description: "Optional Telegram topic thread id with chat_id",
289
303
  })),
@@ -318,11 +332,12 @@ export function registerTelegramOutboundMessageTool(pi, deps) {
318
332
  }
319
333
  catch (error) {
320
334
  const isChannelTarget = typeof params.chat_id === "string" || params.channel === true;
321
- const reportableError = isChannelTarget &&
322
- !isTelegramChannelPostValidationError(error)
335
+ const reportableError = isChannelTarget && !isTelegramChannelPostValidationError(error)
323
336
  ? new Error("Telegram channel publication failed; inspect the retained local record before retrying.")
324
337
  : error;
325
- deps.recordRuntimeEvent?.("message", reportableError, { phase: "direct" });
338
+ deps.recordRuntimeEvent?.("message", reportableError, {
339
+ phase: "direct",
340
+ });
326
341
  throw formatTelegramOutboundToolError(reportableError);
327
342
  }
328
343
  },
@@ -435,9 +450,7 @@ export async function deliverTelegramGuestCachedAttachment(options) {
435
450
  await options.answerGuestQuery(options.guestQueryId, result);
436
451
  }
437
452
  catch (error) {
438
- if (!answerAttempted &&
439
- options.answerGuestText &&
440
- options.fallbackText) {
453
+ if (!answerAttempted && options.answerGuestText && options.fallbackText) {
441
454
  answerAttempted = true;
442
455
  await options.answerGuestText(options.guestQueryId, options.fallbackText);
443
456
  }
@@ -464,10 +477,14 @@ export async function sendTelegramOutboundMessage(options) {
464
477
  assertTelegramDirectDeliveryAllowed(options.canSendDirect);
465
478
  const activeTurn = options.getActiveTurn?.();
466
479
  if (typeof options.chatId === "string" || options.channel === true) {
467
- if (!((typeof options.chatId === "string" && /^@[A-Za-z0-9_]{5,32}$/u.test(options.chatId)) ||
468
- (typeof options.chatId === "number" && Number.isSafeInteger(options.chatId) && options.chatId < 0)) ||
480
+ if (!((typeof options.chatId === "string" &&
481
+ /^@[A-Za-z0-9_]{5,32}$/u.test(options.chatId)) ||
482
+ (typeof options.chatId === "number" &&
483
+ Number.isSafeInteger(options.chatId) &&
484
+ options.chatId < 0)) ||
469
485
  options.threadId !== undefined ||
470
- options.target !== undefined || options.agentThread !== undefined) {
486
+ options.target !== undefined ||
487
+ options.agentThread !== undefined) {
471
488
  throw new Error("Telegram channel delivery requires one exact @username or negative numeric channel ID without a thread target.");
472
489
  }
473
490
  const plan = options.planMessage(options.text);
@@ -476,18 +493,13 @@ export async function sendTelegramOutboundMessage(options) {
476
493
  throw new TelegramChannelPostValidationError("Telegram channel media delivery requires direct leader transport ownership and operation identity.");
477
494
  }
478
495
  const messageId = await options.sendChannelMediaMessage(options.chatId, options.media, plan.markdown, { operationId: options.operationId, replyMarkup: plan.replyMarkup });
479
- return { content: [{ type: "text",
480
- text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
481
- details: { chatId: options.chatId, messageId } };
496
+ return buildTelegramOutboundMessageToolResult(options.chatId, messageId);
482
497
  }
483
498
  if (!options.sendChannelMarkdownMessage || !options.operationId) {
484
499
  throw new Error("Telegram channel delivery requires direct leader transport ownership and operation identity.");
485
500
  }
486
- const messageId = await options.sendChannelMarkdownMessage(options.chatId, plan.markdown, { operationId: options.operationId,
487
- replyMarkup: plan.replyMarkup });
488
- return { content: [{ type: "text",
489
- text: formatTelegramOutboundMessageToolResultText(options.chatId) }],
490
- details: { chatId: options.chatId, messageId } };
501
+ const messageId = await options.sendChannelMarkdownMessage(options.chatId, plan.markdown, { operationId: options.operationId, replyMarkup: plan.replyMarkup });
502
+ return buildTelegramOutboundMessageToolResult(options.chatId, messageId);
491
503
  }
492
504
  if (options.media !== undefined) {
493
505
  throw new TelegramChannelPostValidationError("telegram_message media uploads require channel delivery with an exact @username or negative numeric channel ID and channel: true.");
@@ -499,7 +511,7 @@ export async function sendTelegramOutboundMessage(options) {
499
511
  : activeTurn &&
500
512
  options.resolveAgentTarget &&
501
513
  options.routeAgentMessage &&
502
- ((options.target?.threadId !== undefined) ||
514
+ (options.target?.threadId !== undefined ||
503
515
  (options.chatId !== undefined && options.threadId !== undefined))
504
516
  ? {
505
517
  chatId: options.target?.chatId ?? options.chatId,
@@ -547,15 +559,7 @@ export async function sendTelegramOutboundMessage(options) {
547
559
  text: options.text,
548
560
  });
549
561
  }
550
- return {
551
- content: [
552
- {
553
- type: "text",
554
- text: formatTelegramOutboundMessageToolResultText(chatId),
555
- },
556
- ],
557
- details: { chatId, messageId },
558
- };
562
+ return buildTelegramOutboundMessageToolResult(chatId, messageId);
559
563
  }
560
564
  export async function sendTelegramOutboundFiles(options) {
561
565
  assertTelegramDirectDeliveryAllowed(options.canSendDirect);
@@ -599,7 +603,8 @@ export function createTelegramQueuedOutboundAttachmentSender(deps) {
599
603
  return async (turn, options) => {
600
604
  await sendQueuedTelegramOutboundAttachments(turn, {
601
605
  ...deps,
602
- isDeliveryActive: () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false,
606
+ isDeliveryActive: () => deps.isDeliveryActive?.() !== false &&
607
+ options?.isDeliveryActive?.() !== false,
603
608
  maxAttachmentSizeBytes: deps.maxAttachmentSizeBytes ?? TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES,
604
609
  });
605
610
  };
@@ -622,7 +627,9 @@ export async function sendQueuedTelegramOutboundAttachments(turn, deps) {
622
627
  const fieldName = isPhoto ? "photo" : "document";
623
628
  await withTelegramReplyParameters(turn.chatId, turn.replyToMessageId, turn.target, (replyParameters) => deps.sendMultipart(method, {
624
629
  chat_id: String(turn.chatId),
625
- ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
630
+ ...(replyParameters
631
+ ? { reply_parameters: JSON.stringify(replyParameters) }
632
+ : {}),
626
633
  ...getTelegramMultipartTargetFields(turn.target),
627
634
  }, fieldName, attachment.path, attachment.fileName));
628
635
  }
@@ -83,12 +83,14 @@ function escapeTelegramRichButtonText(text) {
83
83
  return text.replace(/[&<>"'`\\*_\[\]{}$~\r\n]/g, (character) => `&#${character.charCodeAt(0)};`);
84
84
  }
85
85
  function renderTelegramRichButtonRow(row) {
86
- return `<tg-button-row>${row.map((button) => {
86
+ return `<tg-button-row>${row
87
+ .map((button) => {
87
88
  const attributes = button.disabled
88
89
  ? 'type="disabled"'
89
90
  : `type="callback_data" data="${escapeTelegramRichButtonText(button.callback_data)}"`;
90
91
  return `<tg-button ${attributes}>${escapeTelegramRichButtonText(button.text)}</tg-button>`;
91
- }).join("")}</tg-button-row>`;
92
+ })
93
+ .join("")}</tg-button-row>`;
92
94
  }
93
95
  export function planTelegramButtonReply(markdown, deps) {
94
96
  const keyboard = [];
@@ -96,12 +98,14 @@ export function planTelegramButtonReply(markdown, deps) {
96
98
  const actions = payloadRows.map((row) => row.map(parseTelegramButtonAction));
97
99
  if (actions.some((row) => row.some((action) => !action)))
98
100
  return undefined;
99
- if (rich && actions.some((row) => {
100
- const projected = row.map((action) => action.disabled
101
- ? { text: action.text || "\u00a0", disabled: {} }
102
- : { text: action.text, callback_data: "x".repeat(64) });
103
- return row.length > 8 || renderTelegramRichButtonRow(projected).length > 32768;
104
- }))
101
+ if (rich &&
102
+ actions.some((row) => {
103
+ const projected = row.map((action) => action.disabled
104
+ ? { text: action.text || "\u00a0", disabled: {} }
105
+ : { text: action.text, callback_data: "x".repeat(64) });
106
+ return (row.length > 8 ||
107
+ renderTelegramRichButtonRow(projected).length > 32768);
108
+ }))
105
109
  return undefined;
106
110
  return actions.map((row) => row.map((action) => action.disabled
107
111
  ? { text: action.text || "\u00a0", disabled: {} }
@@ -131,7 +135,9 @@ export function planTelegramButtonReply(markdown, deps) {
131
135
  });
132
136
  const stripped = replaceTopLevelHtmlComments(withRichButtons, (comment) => {
133
137
  const command = "telegram_button";
134
- const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
138
+ const normalizedContent = comment.content
139
+ .replace(/^\s+/, "")
140
+ .replace(/^!/, "");
135
141
  if (!normalizedContent.startsWith(command))
136
142
  return comment.raw;
137
143
  const payloadRows = parseTelegramActionPayloadRows(comment, command);
@@ -186,19 +192,19 @@ export async function handleTelegramButtonCallbackQuery(query, ctx, deps) {
186
192
  const action = deps.resolveAction(query.data);
187
193
  if (!action) {
188
194
  if (query.data?.startsWith(`${TELEGRAM_BUTTON_CALLBACK_PREFIX}:`)) {
189
- await deps.answerCallbackQuery(query.id, "Button action expired.");
195
+ await deps.answerCallbackQuery(query.id, "Button action expired");
190
196
  return true;
191
197
  }
192
198
  return false;
193
199
  }
194
200
  if (action.disabled) {
195
- await deps.answerCallbackQuery(query.id, "Button action unavailable.");
201
+ await deps.answerCallbackQuery(query.id, "Button action unavailable");
196
202
  return true;
197
203
  }
198
204
  const chatId = query.message?.chat?.id;
199
205
  const messageId = query.message?.message_id;
200
206
  if (typeof chatId !== "number" || typeof messageId !== "number") {
201
- await deps.answerCallbackQuery(query.id, "Button action expired.");
207
+ await deps.answerCallbackQuery(query.id, "Button action expired");
202
208
  return true;
203
209
  }
204
210
  if (deps.invokeBoundAction) {
@@ -216,18 +222,18 @@ export async function handleTelegramButtonCallbackQuery(query, ctx, deps) {
216
222
  }
217
223
  }
218
224
  }
219
- await deps.answerCallbackQuery(query.id, "Done.");
225
+ await deps.answerCallbackQuery(query.id, "Done");
220
226
  return true;
221
227
  }
222
228
  }
223
229
  catch (error) {
224
- await deps.answerCallbackQuery(query.id, "Generative App action failed.");
230
+ await deps.answerCallbackQuery(query.id, "Generative App action failed");
225
231
  throw error;
226
232
  }
227
233
  }
228
234
  const enqueued = deps.enqueueButtonPrompt(query, action, ctx);
229
235
  if (enqueued === false) {
230
- await deps.answerCallbackQuery(query.id, "Already queued.");
236
+ await deps.answerCallbackQuery(query.id, "Already queued");
231
237
  return true;
232
238
  }
233
239
  const selectedMarkup = query.data && query.message?.reply_markup
@@ -236,6 +242,6 @@ export async function handleTelegramButtonCallbackQuery(query, ctx, deps) {
236
242
  if (selectedMarkup && deps.editMessageReplyMarkup) {
237
243
  await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
238
244
  }
239
- await deps.answerCallbackQuery(query.id, "Queued.");
245
+ await deps.answerCallbackQuery(query.id, "Queued");
240
246
  return true;
241
247
  }
@@ -17,10 +17,8 @@ export declare function replaceTelegramButtonFences(markdown: string, replace: (
17
17
  export declare function replaceTopLevelHtmlComments(markdown: string, replacer: (comment: TelegramTopLevelHtmlComment) => string): string;
18
18
  export declare function parseTelegramButtonPayloadRows(source: string): Record<string, unknown>[][] | undefined;
19
19
  export declare function parseTelegramActionPayloadRows(comment: TelegramTopLevelHtmlComment, command: string): Record<string, unknown>[][] | undefined;
20
- export declare function normalizeMarkdownAfterVoiceExtraction(markdown: string): string;
21
20
  export declare function stripTelegramCommentMarkupForPreview(markdown: string): string;
22
21
  export declare function stripTelegramCommentMarkupForDelivery(markdown: string): string;
23
- export declare function stripTelegramVoiceMarkupForPreview(markdown: string): string;
24
22
  export interface TelegramVoiceReplyItem {
25
23
  text: string;
26
24
  lang?: string;
@@ -192,7 +192,9 @@ function removeTelegramJsonTrailingCommas(source) {
192
192
  }
193
193
  function parseTelegramJsonObjectCandidate(source) {
194
194
  const normalized = removeTelegramJsonTrailingCommas(source);
195
- for (const candidate of normalized === source ? [source] : [source, normalized]) {
195
+ for (const candidate of normalized === source
196
+ ? [source]
197
+ : [source, normalized]) {
196
198
  try {
197
199
  const value = JSON.parse(candidate);
198
200
  if (isTelegramActionPayload(value))
@@ -207,36 +209,46 @@ function parseTelegramJsonObjectCandidate(source) {
207
209
  function looksLikeTelegramNamedJsonObject(source, offset) {
208
210
  return /^\{\s*"(?:[^"\\]|\\.)*"\s*:/u.test(source.slice(offset));
209
211
  }
210
- function parseTelegramActionPayload(comment, command) {
212
+ /** Blank one structured span so tolerant attribute parsing never reads keys inside rejected JSON or matrices. */
213
+ function blankTelegramActionSpan(text, start, end) {
214
+ return `${text.slice(0, start)}${" ".repeat(end - start)}${text.slice(end)}`;
215
+ }
216
+ /**
217
+ * Scans one action comment for its first accepted structured payload. Otherwise returns the attribute envelope
218
+ * with every structured span blanked; implausible `[` starts are skipped without a parse attempt.
219
+ */
220
+ function scanTelegramActionComment(comment, command, parseStructured) {
211
221
  let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
212
222
  if (!content.startsWith(command))
213
223
  return undefined;
214
224
  content = content.slice(command.length);
215
225
  let attributeEnvelope = content;
216
226
  for (let offset = 0; offset < content.length; offset += 1) {
217
- if (content[offset] !== "{" && content[offset] !== "[")
227
+ const char = content[offset];
228
+ if (char !== "{" && char !== "[")
218
229
  continue;
219
- if (content[offset] === "[" &&
220
- !isPlausibleTelegramMatrixStart(content, offset)) {
221
- const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
222
- if (noiseEnd !== undefined) {
223
- attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
224
- offset = noiseEnd - 1;
225
- }
226
- continue;
227
- }
228
- if (content[offset] === "{") {
229
- const parsed = parseTelegramAdaptiveActionPayloadRows(content.slice(offset), parseTelegramVoiceCompactActionPayload, { allowTrailing: true });
230
- if (parsed)
231
- return parsed.rows[0][0];
230
+ if (char === "{" || isPlausibleTelegramMatrixStart(content, offset)) {
231
+ const payload = parseStructured(content, offset);
232
+ if (payload !== undefined)
233
+ return { payload };
232
234
  }
233
235
  const end = findTelegramStructuredPayloadEnd(content, offset);
234
236
  if (end === undefined)
235
237
  continue;
236
- attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
238
+ attributeEnvelope = blankTelegramActionSpan(attributeEnvelope, offset, end);
237
239
  offset = end - 1;
238
240
  }
239
- return parseTolerantTelegramAttributes(attributeEnvelope, [
241
+ return { attributeEnvelope };
242
+ }
243
+ function parseTelegramActionPayload(comment, command) {
244
+ const scanned = scanTelegramActionComment(comment, command, (content, offset) => content[offset] === "{"
245
+ ? parseTelegramAdaptiveActionPayloadRows(content.slice(offset), parseTelegramVoiceCompactActionPayload, { allowTrailing: true })?.rows[0][0]
246
+ : undefined);
247
+ if (!scanned)
248
+ return undefined;
249
+ if ("payload" in scanned)
250
+ return scanned.payload;
251
+ return parseTolerantTelegramAttributes(scanned.attributeEnvelope, [
240
252
  "text",
241
253
  "value",
242
254
  "lang",
@@ -259,7 +271,10 @@ function parseTelegramButtonCompactActionPayload(atoms) {
259
271
  selectedStyle !== "danger" &&
260
272
  !(atoms.length === 4 && selectedStyle === ""))
261
273
  return undefined;
262
- if (atoms.length === 4 && !isDisabled && disabled !== "0" && disabled !== "false") {
274
+ if (atoms.length === 4 &&
275
+ !isDisabled &&
276
+ disabled !== "0" &&
277
+ disabled !== "false") {
263
278
  return undefined;
264
279
  }
265
280
  return {
@@ -464,9 +479,7 @@ function parseTelegramAdaptiveActionPayloadRows(source, parseCompactPayload, opt
464
479
  return rows.length > 0 ? rows : undefined;
465
480
  };
466
481
  skipWhitespace();
467
- const rows = source[offset] === "{"
468
- ? parseVerticalSequence()
469
- : parseMatrix();
482
+ const rows = source[offset] === "{" ? parseVerticalSequence() : parseMatrix();
470
483
  if (!rows)
471
484
  return undefined;
472
485
  skipWhitespace();
@@ -511,57 +524,32 @@ function isPlausibleTelegramMatrixStart(source, start) {
511
524
  let offset = start + 1;
512
525
  while (/\s/u.test(source[offset] ?? ""))
513
526
  offset += 1;
514
- return (source[offset] === "{" ||
515
- source[offset] === "[" ||
516
- source[offset] === "]");
527
+ return (source[offset] === "{" || source[offset] === "[" || source[offset] === "]");
517
528
  }
518
529
  export function parseTelegramButtonPayloadRows(source) {
519
530
  return parseTelegramAdaptiveActionPayloadRows(source.trim(), parseTelegramButtonCompactActionPayload, { maxCompactAtoms: 4 })?.rows;
520
531
  }
521
532
  export function parseTelegramActionPayloadRows(comment, command) {
522
- let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
523
- if (!content.startsWith(command))
533
+ const scanned = scanTelegramActionComment(comment, command, (content, offset) => parseTelegramAdaptiveActionPayloadRows(content.slice(offset), parseTelegramButtonCompactActionPayload, { allowTrailing: true, maxCompactAtoms: 4 })?.rows);
534
+ if (!scanned)
524
535
  return undefined;
525
- content = content.slice(command.length);
526
- let attributeEnvelope = content;
527
- for (let offset = 0; offset < content.length; offset += 1) {
528
- if (content[offset] !== "[" && content[offset] !== "{")
529
- continue;
530
- if (content[offset] === "[" &&
531
- !isPlausibleTelegramMatrixStart(content, offset)) {
532
- const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
533
- if (noiseEnd !== undefined) {
534
- attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
535
- offset = noiseEnd - 1;
536
- }
537
- continue;
538
- }
539
- const parsed = parseTelegramAdaptiveActionPayloadRows(content.slice(offset), parseTelegramButtonCompactActionPayload, { allowTrailing: true, maxCompactAtoms: 4 });
540
- if (parsed)
541
- return parsed.rows;
542
- const end = findTelegramStructuredPayloadEnd(content, offset);
543
- if (end === undefined)
544
- continue;
545
- attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
546
- offset = end - 1;
547
- }
548
- const attributes = parseTolerantTelegramAttributes(attributeEnvelope, [
549
- "label",
550
- "prompt",
551
- "value",
552
- "selected_style",
553
- "disabled",
554
- ]);
536
+ if ("payload" in scanned)
537
+ return scanned.payload;
538
+ const attributes = parseTolerantTelegramAttributes(scanned.attributeEnvelope, ["label", "prompt", "value", "selected_style", "disabled"]);
555
539
  if (!attributes)
556
540
  return undefined;
557
- return [[{
541
+ return [
542
+ [
543
+ {
558
544
  ...attributes,
559
545
  ...(attributes.disabled === "true" || attributes.disabled === "false"
560
546
  ? { disabled: attributes.disabled === "true" }
561
547
  : {}),
562
- }]];
548
+ },
549
+ ],
550
+ ];
563
551
  }
564
- export function normalizeMarkdownAfterVoiceExtraction(markdown) {
552
+ function normalizeMarkdownAfterVoiceExtraction(markdown) {
565
553
  return markdown.replace(/\n{3,}/g, "\n\n").trim();
566
554
  }
567
555
  function isTelegramCommentOnlyLinePrefix(value) {
@@ -608,9 +596,6 @@ export function stripTelegramCommentMarkupForDelivery(markdown) {
608
596
  : withoutClosedBlocks;
609
597
  return normalizeMarkdownAfterVoiceExtraction(deliveryMarkdown);
610
598
  }
611
- export function stripTelegramVoiceMarkupForPreview(markdown) {
612
- return stripTelegramCommentMarkupForPreview(markdown);
613
- }
614
599
  export function getTelegramActionString(payload, key) {
615
600
  const value = payload[key];
616
601
  if (typeof value !== "string")
@@ -624,7 +609,9 @@ export function planTelegramVoiceReply(markdown) {
624
609
  let rate;
625
610
  const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
626
611
  const command = "telegram_voice";
627
- const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
612
+ const normalizedContent = comment.content
613
+ .replace(/^\s+/, "")
614
+ .replace(/^!/, "");
628
615
  if (!normalizedContent.startsWith(command))
629
616
  return comment.raw;
630
617
  const payload = parseTelegramActionPayload(comment, command);
@@ -8,8 +8,8 @@ import { basename, extname } from "node:path";
8
8
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.js";
9
9
  import { withTelegramReplyParameters } from "./replies.js";
10
10
  import { getTelegramTargetThreadParams, } from "./target.js";
11
- import { getTelegramVoiceSynthesisProviders } from "./voice.js";
12
11
  import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
12
+ import { getTelegramVoiceSynthesisProviders } from "./voice.js";
13
13
  async function ensureTelegramVoiceFileFormat(filePath) {
14
14
  const ext = extname(filePath).toLowerCase();
15
15
  if (ext === ".opus" || ext === ".ogg")
@@ -38,11 +38,14 @@ export function createTelegramVoiceReplySender(deps, ports = {}) {
38
38
  return;
39
39
  await withTelegramReplyParameters(turn.chatId, options?.replyToPrompt === false ? undefined : turn.replyToMessageId, turn.target, (replyParameters) => deps.sendMultipart("sendVoice", {
40
40
  chat_id: String(turn.chatId),
41
- ...(replyParameters ? { reply_parameters: JSON.stringify(replyParameters) } : {}),
41
+ ...(replyParameters
42
+ ? { reply_parameters: JSON.stringify(replyParameters) }
43
+ : {}),
42
44
  ...(turn.target
43
45
  ? Object.fromEntries(Object.entries(getTelegramTargetThreadParams(turn.target)).map(([key, value]) => [key, String(value)]))
44
46
  : {}),
45
- ...(options?.replyMarkup !== undefined && options.replyMarkup !== null
47
+ ...(options?.replyMarkup !== undefined &&
48
+ options.replyMarkup !== null
46
49
  ? {
47
50
  reply_markup: typeof options.replyMarkup === "string"
48
51
  ? options.replyMarkup
@@ -4,12 +4,12 @@
4
4
  * Owns configured outbound handler execution, text transforms, public assistant-output reply composition and mutation fencing, voice-file generation/delivery, runtime-event bridge, and compatibility re-exports; assistant markup parsing lives in outbound-markup and button callback actions live in outbound-buttons
5
5
  */
6
6
  import type { TelegramAssistantSegmentEvent } from "./activity.ts";
7
- import * as Replies from "./replies.ts";
8
- import type { TelegramPreparedPreviewDelivery } from "./preview.ts";
9
- import type { TelegramEditMessageTextBody, TelegramSendMessageBody, TelegramSendRichMessageBody, TelegramSentMessage } from "./telegram-api.ts";
10
7
  import { type TelegramButtonActionStore, type TelegramOutboundButtonBinding, type TelegramOutboundButtonMarkup } from "./outbound-buttons.ts";
11
8
  import { type TelegramVoiceReplyItem } from "./outbound-markup.ts";
9
+ import type { TelegramPreparedPreviewDelivery } from "./preview.ts";
10
+ import * as Replies from "./replies.ts";
12
11
  import type { TelegramTarget } from "./target.ts";
12
+ import type { TelegramEditMessageTextBody, TelegramSendMessageBody, TelegramSendRichMessageBody, TelegramSentMessage } from "./telegram-api.ts";
13
13
  import { type CommandTemplateObjectConfig } from "./command-templates.ts";
14
14
  /**
15
15
  * Record a runtime event that appears in `/telegram-status`.
@@ -27,13 +27,13 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
27
27
  output?: string;
28
28
  timeout?: number | string;
29
29
  }
30
- export { normalizeMarkdownAfterVoiceExtraction, planTelegramVoiceReply, stripTelegramCommentMarkupForDelivery, stripTelegramCommentMarkupForPreview, stripTelegramVoiceMarkupForPreview, type TelegramVoiceReplyItem, type TelegramVoiceReplyPlan, } from "./outbound-markup.ts";
31
30
  export interface TelegramVoiceExecOptions {
32
31
  cwd?: string;
33
32
  timeout?: number;
34
33
  signal?: AbortSignal;
35
34
  stdin?: string;
36
35
  retry?: number;
36
+ recover?: () => Promise<void>;
37
37
  }
38
38
  export interface TelegramVoiceExecResult {
39
39
  stdout: string;
@@ -74,11 +74,11 @@ export interface TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup = unknown> {
74
74
  sendTextReply: (chatId: number, replyToMessageId: number | undefined, text: string, options?: {
75
75
  parseMode?: "HTML";
76
76
  target?: TelegramTarget;
77
- }) => Promise<number | undefined>;
77
+ } & Pick<Replies.TelegramReplyTargetOptions, "assertAuthority">) => Promise<number | undefined>;
78
78
  sendMarkdownReply: (chatId: number, replyToMessageId: number | undefined, markdown: string, options?: {
79
79
  replyMarkup?: TReplyMarkup;
80
80
  target?: TelegramTarget;
81
- }) => Promise<number | undefined>;
81
+ } & Pick<Replies.TelegramReplyTargetOptions, "assertAuthority">) => Promise<number | undefined>;
82
82
  cwd?: string;
83
83
  recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
84
84
  }