@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
package/lib/replies.ts CHANGED
@@ -5,22 +5,27 @@
5
5
  */
6
6
 
7
7
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.ts";
8
+ import {
9
+ renderTelegramMessage,
10
+ type TelegramRenderedChunk,
11
+ type TelegramRenderMode,
12
+ } from "./rendering.ts";
8
13
  import {
9
14
  getTelegramTargetThreadParams,
10
15
  type TelegramTarget,
11
16
  } from "./target.ts";
12
- import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
13
17
  import type {
18
+ TelegramApiCallOptions,
14
19
  TelegramInputRichMessage,
15
20
  TelegramReplyParameters,
16
21
  TelegramSendRichMessageBody,
17
22
  TelegramSentMessage,
18
23
  } from "./telegram-api.ts";
19
24
  import {
20
- renderTelegramMessage,
21
- type TelegramRenderedChunk,
22
- type TelegramRenderMode,
23
- } from "./rendering.ts";
25
+ assertTelegramApiCallAuthority,
26
+ isTelegramApiCommitUnknownError,
27
+ TelegramApiAuthorityError,
28
+ } from "./telegram-api.ts";
24
29
 
25
30
  export {
26
31
  renderTelegramMessage,
@@ -37,36 +42,6 @@ export function renderTelegramMarkdownToHtmlDraft(markdown: string): string {
37
42
  export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
38
43
  export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
39
44
 
40
- // --- Reply Dedup ---
41
-
42
- /** Non-persistent reply deduplication for a single agent turn.
43
- * First reply to a prompt gets `reply_parameters.message_id`;
44
- * subsequent replies in the same turn skip it to avoid stacking
45
- * duplicate reply headers in the chat viewport. */
46
- export interface ReplyDedupRuntime {
47
- /** Returns true if this is the first reply for the given prompt
48
- * message id in the current turn. Side-effect: marks it replied. */
49
- shouldReply(promptMessageId: number): boolean;
50
- /** Reset the tracker when a new prompt enters the queue. */
51
- reset(): void;
52
- }
53
-
54
- export function createReplyDedupRuntime(): ReplyDedupRuntime {
55
- const replied = new Map<number, boolean>();
56
- return {
57
- shouldReply(promptMessageId: number): boolean {
58
- if (replied.has(promptMessageId)) return false;
59
- replied.set(promptMessageId, true);
60
- return true;
61
- },
62
- reset(): void {
63
- replied.clear();
64
- },
65
- };
66
- }
67
-
68
- // --- Transport-level dedup ---
69
-
70
45
  const lastRepliedToMessageIdByTarget = new Map<string, number>();
71
46
  const replyDedupPreservedOnNextReset = new Map<string, number>();
72
47
  let replyDedupGeneration = 0;
@@ -135,9 +110,13 @@ export async function withTelegramReplyParameters<T>(
135
110
  try {
136
111
  return await send(parameters);
137
112
  } catch (error) {
138
- if (parameters && !isTelegramApiCommitUnknownError(error)
139
- && generation === replyDedupGeneration
140
- && lastRepliedToMessageIdByTarget.get(key) === messageId) {
113
+ if (
114
+ parameters &&
115
+ !isTelegramApiCommitUnknownError(error) &&
116
+ !(error instanceof TelegramApiAuthorityError && error.requestIssued) &&
117
+ generation === replyDedupGeneration &&
118
+ lastRepliedToMessageIdByTarget.get(key) === messageId
119
+ ) {
141
120
  if (previous === undefined) lastRepliedToMessageIdByTarget.delete(key);
142
121
  else lastRepliedToMessageIdByTarget.set(key, previous);
143
122
  }
@@ -203,9 +182,7 @@ export function extractLatestAssistantMessageText(
203
182
  * is the latest earlier completed assistant message that carries text;
204
183
  * tool-use prefaces, errors, and aborts stay excluded.
205
184
  */
206
- export function extractRunAssistantMessage(
207
- messages: readonly unknown[],
208
- ): {
185
+ export function extractRunAssistantMessage(messages: readonly unknown[]): {
209
186
  text?: string;
210
187
  stopReason?: string;
211
188
  errorMessage?: string;
@@ -248,27 +225,36 @@ export interface TelegramReplyOwnershipRecorder {
248
225
 
249
226
  export interface TelegramReplyDeliveryDeps<TReplyMarkup> {
250
227
  recordOwnership?: TelegramReplyOwnershipRecorder["record"];
251
- sendMessage: (body: {
252
- chat_id: number;
253
- text: string;
254
- parse_mode?: "HTML";
255
- reply_markup?: TReplyMarkup;
256
- reply_parameters?: TelegramReplyParameters;
257
- reply_to_message_id?: number;
258
- message_thread_id?: number;
259
- }) => Promise<TelegramSentMessage>;
260
- editMessage: (body: {
261
- chat_id: number;
262
- message_id: number;
263
- text?: string;
264
- rich_message?: TelegramInputRichMessage;
265
- parse_mode?: "HTML";
266
- reply_markup?: TReplyMarkup;
267
- message_thread_id?: number;
268
- }) => Promise<unknown>;
228
+ sendMessage: (
229
+ body: {
230
+ chat_id: number;
231
+ text: string;
232
+ parse_mode?: "HTML";
233
+ reply_markup?: TReplyMarkup;
234
+ reply_parameters?: TelegramReplyParameters;
235
+ reply_to_message_id?: number;
236
+ message_thread_id?: number;
237
+ },
238
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
239
+ ) => Promise<TelegramSentMessage>;
240
+ editMessage: (
241
+ body: {
242
+ chat_id: number;
243
+ message_id: number;
244
+ text?: string;
245
+ rich_message?: TelegramInputRichMessage;
246
+ parse_mode?: "HTML";
247
+ reply_markup?: TReplyMarkup;
248
+ message_thread_id?: number;
249
+ },
250
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
251
+ ) => Promise<unknown>;
269
252
  }
270
253
 
271
- export interface TelegramReplyTargetOptions {
254
+ export interface TelegramReplyTargetOptions extends Pick<
255
+ TelegramApiCallOptions,
256
+ "assertAuthority"
257
+ > {
272
258
  target?: TelegramTarget;
273
259
  replyToMessageId?: number;
274
260
  }
@@ -316,27 +302,75 @@ export async function sendTelegramRenderedChunks<TReplyMarkup>(
316
302
  replyMarkup?: TReplyMarkup;
317
303
  },
318
304
  ): Promise<number | undefined> {
319
- assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
305
+ return sendTelegramChunkSequence(
306
+ chatId,
307
+ chunks,
308
+ options?.replyToMessageId,
309
+ options,
310
+ deps.recordOwnership,
311
+ (chunk, fields, callOptions) =>
312
+ deps.sendMessage(
313
+ {
314
+ chat_id: chatId,
315
+ text: chunk.text,
316
+ parse_mode: chunk.parseMode,
317
+ ...fields,
318
+ },
319
+ callOptions,
320
+ ),
321
+ );
322
+ }
323
+
324
+ /**
325
+ * Ordered chunk delivery: only the first chunk replies, only the last carries markup, and authority is rechecked
326
+ * before issuance and after every response/ownership publication.
327
+ */
328
+ async function sendTelegramChunkSequence<TChunk, TReplyMarkup>(
329
+ chatId: number,
330
+ chunks: readonly TChunk[],
331
+ replyToMessageId: number | undefined,
332
+ options:
333
+ (TelegramReplyTargetOptions & { replyMarkup?: TReplyMarkup }) | undefined,
334
+ recordOwnership: TelegramReplyOwnershipRecorder["record"] | undefined,
335
+ send: (
336
+ chunk: TChunk,
337
+ fields: {
338
+ reply_markup?: TReplyMarkup;
339
+ reply_parameters?: TelegramReplyParameters;
340
+ message_thread_id?: number;
341
+ },
342
+ callOptions: Pick<TelegramApiCallOptions, "assertAuthority"> | undefined,
343
+ ) => Promise<TelegramSentMessage>,
344
+ ): Promise<number | undefined> {
345
+ const target = options?.target && { ...options.target },
346
+ assertAuthority = options?.assertAuthority,
347
+ replyMarkup = options?.replyMarkup;
348
+ assertTelegramInlineKeyboardCallbackData(replyMarkup);
320
349
  let lastMessageId: number | undefined;
321
350
  for (const [index, chunk] of chunks.entries()) {
351
+ assertTelegramApiCallAuthority(assertAuthority, false);
322
352
  const sent = await withTelegramReplyParameters(
323
- chatId, index === 0 ? options?.replyToMessageId : undefined, options?.target,
324
- (replyParameters) => deps.sendMessage({
325
- chat_id: chatId,
326
- text: chunk.text,
327
- parse_mode: chunk.parseMode,
328
- reply_markup:
329
- index === chunks.length - 1 ? options?.replyMarkup : undefined,
330
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
331
- ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
332
- }),
353
+ chatId,
354
+ index === 0 ? replyToMessageId : undefined,
355
+ target,
356
+ async (replyParameters) => {
357
+ const sent = await send(
358
+ chunk,
359
+ {
360
+ reply_markup: index === chunks.length - 1 ? replyMarkup : undefined,
361
+ ...(replyParameters ? { reply_parameters: replyParameters } : {}),
362
+ ...(target ? getTelegramTargetThreadParams(target) : {}),
363
+ },
364
+ assertAuthority ? { assertAuthority } : undefined,
365
+ );
366
+ assertTelegramApiCallAuthority(assertAuthority, true);
367
+ return sent;
368
+ },
333
369
  );
370
+ assertTelegramApiCallAuthority(assertAuthority, true);
334
371
  lastMessageId = sent.message_id;
335
- deps.recordOwnership?.({
336
- chatId,
337
- messageId: sent.message_id,
338
- target: options?.target,
339
- });
372
+ recordOwnership?.({ chatId, messageId: sent.message_id, target });
373
+ assertTelegramApiCallAuthority(assertAuthority, true);
340
374
  }
341
375
  return lastMessageId;
342
376
  }
@@ -348,23 +382,35 @@ export async function editTelegramRenderedMessage<TReplyMarkup>(
348
382
  deps: TelegramReplyDeliveryDeps<TReplyMarkup>,
349
383
  options?: TelegramReplyTargetOptions & { replyMarkup?: TReplyMarkup },
350
384
  ): Promise<number | undefined> {
385
+ options = { ...options, target: options?.target && { ...options.target } };
386
+ const assertAuthority = options.assertAuthority;
387
+ assertTelegramApiCallAuthority(assertAuthority, false);
351
388
  assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
352
389
  if (chunks.length === 0) return messageId;
353
390
  const [firstChunk, ...remainingChunks] = chunks;
354
- deps.recordOwnership?.({ chatId, messageId, target: options?.target });
355
- await deps.editMessage({
356
- chat_id: chatId,
357
- message_id: messageId,
358
- text: firstChunk.text,
359
- parse_mode: firstChunk.parseMode,
360
- reply_markup:
361
- remainingChunks.length === 0 ? options?.replyMarkup : undefined,
362
- ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
363
- });
391
+ if (!assertAuthority)
392
+ deps.recordOwnership?.({ chatId, messageId, target: options.target });
393
+ await deps.editMessage(
394
+ {
395
+ chat_id: chatId,
396
+ message_id: messageId,
397
+ text: firstChunk.text,
398
+ parse_mode: firstChunk.parseMode,
399
+ reply_markup:
400
+ remainingChunks.length === 0 ? options?.replyMarkup : undefined,
401
+ ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
402
+ },
403
+ assertAuthority ? { assertAuthority } : undefined,
404
+ );
405
+ assertTelegramApiCallAuthority(assertAuthority, true);
406
+ if (assertAuthority)
407
+ deps.recordOwnership?.({ chatId, messageId, target: options.target });
408
+ assertTelegramApiCallAuthority(assertAuthority, true);
364
409
  if (remainingChunks.length > 0) {
365
410
  return sendTelegramRenderedChunks(chatId, remainingChunks, deps, {
366
411
  replyMarkup: options?.replyMarkup,
367
412
  target: options?.target,
413
+ assertAuthority,
368
414
  });
369
415
  }
370
416
  return messageId;
@@ -390,12 +436,14 @@ export async function sendTelegramPlainReply(
390
436
  deps: TelegramReplyRuntimeDeps,
391
437
  options?: TelegramTextReplyOptions,
392
438
  ): Promise<number | undefined> {
439
+ options = { ...options, target: options?.target && { ...options.target } };
393
440
  const chunks = deps.renderTelegramMessage(text, {
394
441
  mode: options?.parseMode === "HTML" ? "html" : "plain",
395
442
  });
396
443
  return deps.sendRenderedChunks(chunks, {
397
444
  target: options?.target,
398
445
  replyToMessageId: options?.replyToMessageId,
446
+ assertAuthority: options.assertAuthority,
399
447
  });
400
448
  }
401
449
 
@@ -431,37 +479,56 @@ function normalizeTelegramNativeMarkdownLine(line: string): string {
431
479
  );
432
480
  }
433
481
 
482
+ /** An open fenced code block: its marker character and the minimum closing run length. */
483
+ export interface TelegramMarkdownFence {
484
+ marker: "`" | "~";
485
+ length: number;
486
+ }
487
+
488
+ export function parseTelegramMarkdownFenceOpening(
489
+ line: string,
490
+ ): TelegramMarkdownFence | undefined {
491
+ const markerText = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
492
+ return markerText
493
+ ? { marker: markerText[0] as "`" | "~", length: markerText.length }
494
+ : undefined;
495
+ }
496
+
497
+ export function isTelegramMarkdownFenceClosing(
498
+ line: string,
499
+ fence: TelegramMarkdownFence,
500
+ ): boolean {
501
+ return new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line);
502
+ }
503
+
504
+ /** Fence state after one line: outside a fence a marker opens one; inside, a matching marker closes it. */
505
+ function advanceTelegramMarkdownFence(
506
+ fence: TelegramMarkdownFence | undefined,
507
+ line: string,
508
+ ): TelegramMarkdownFence | undefined {
509
+ if (!fence) return parseTelegramMarkdownFenceOpening(line);
510
+ return isTelegramMarkdownFenceClosing(line, fence) ? undefined : fence;
511
+ }
512
+
434
513
  function hasClosingDisplayMathDelimiter(
435
514
  lines: readonly string[],
436
515
  startIndex: number,
437
516
  ): boolean {
438
- let fence: { marker: "`" | "~"; length: number } | undefined;
517
+ let fence: TelegramMarkdownFence | undefined;
439
518
  for (let index = startIndex + 1; index < lines.length; index += 1) {
440
519
  const line = lines[index] ?? "";
441
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
442
520
  if (!fence && line.trim() === "$$") return true;
443
- if (!fence && fenceMatch) {
444
- const markerText = fenceMatch[1] ?? "```";
445
- fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
446
- continue;
447
- }
448
- if (
449
- fence &&
450
- new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
451
- ) {
452
- fence = undefined;
453
- }
521
+ fence = advanceTelegramMarkdownFence(fence, line);
454
522
  }
455
523
  return false;
456
524
  }
457
525
 
458
526
  export function normalizeTelegramNativeMarkdown(markdown: string): string {
459
527
  const lines = markdown.replace(/\r\n/g, "\n").split("\n");
460
- let fence: { marker: "`" | "~"; length: number } | undefined;
528
+ let fence: TelegramMarkdownFence | undefined;
461
529
  let displayMath = false;
462
530
  return lines
463
531
  .map((line, index) => {
464
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
465
532
  const inFence = fence !== undefined;
466
533
  if (!inFence && line.trim() === "$$") {
467
534
  if (displayMath) {
@@ -474,19 +541,9 @@ export function normalizeTelegramNativeMarkdown(markdown: string): string {
474
541
  }
475
542
  }
476
543
  if (displayMath) return line;
477
- if (!inFence && fenceMatch) {
478
- const markerText = fenceMatch[1] ?? "```";
479
- fence = {
480
- marker: markerText[0] as "`" | "~",
481
- length: markerText.length,
482
- };
483
- return line;
484
- }
485
- if (
486
- inFence &&
487
- new RegExp(`^ {0,3}${fence?.marker}{${fence?.length},}\\s*$`).test(line)
488
- ) {
489
- fence = undefined;
544
+ const nextFence = advanceTelegramMarkdownFence(fence, line);
545
+ if (nextFence !== fence) {
546
+ fence = nextFence;
490
547
  return line;
491
548
  }
492
549
  if (!inFence) return normalizeTelegramNativeMarkdownLine(line);
@@ -542,30 +599,19 @@ export function splitTelegramNativeMarkdown(markdown: string): string[] {
542
599
  function splitTelegramNativeMarkdownBlocks(markdown: string): string[] {
543
600
  const blocks: string[] = [];
544
601
  const current: string[] = [];
545
- let fence: { marker: "`" | "~"; length: number } | undefined;
602
+ let fence: TelegramMarkdownFence | undefined;
546
603
  const flush = (): void => {
547
604
  if (current.length === 0) return;
548
605
  blocks.push(current.join("\n"));
549
606
  current.length = 0;
550
607
  };
551
608
  for (const line of markdown.split("\n")) {
552
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
553
609
  if (!fence && line.trim().length === 0) {
554
610
  flush();
555
611
  continue;
556
612
  }
557
613
  current.push(line);
558
- if (!fence && fenceMatch) {
559
- const markerText = fenceMatch[1] ?? "```";
560
- fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
561
- continue;
562
- }
563
- if (
564
- fence &&
565
- new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
566
- ) {
567
- fence = undefined;
568
- }
614
+ fence = advanceTelegramMarkdownFence(fence, line);
569
615
  }
570
616
  flush();
571
617
  return blocks;
@@ -579,25 +625,14 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
579
625
  }
580
626
  const chunks: string[] = [];
581
627
  let current: string[] = [];
582
- let fence: { marker: "`" | "~"; length: number } | undefined;
628
+ let fence: TelegramMarkdownFence | undefined;
583
629
  for (const line of block.split("\n")) {
584
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
585
630
  if (!fence && current.length >= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
586
631
  chunks.push(current.join("\n"));
587
632
  current = [];
588
633
  }
589
634
  current.push(line);
590
- if (!fence && fenceMatch) {
591
- const markerText = fenceMatch[1] ?? "```";
592
- fence = { marker: markerText[0] as "`" | "~", length: markerText.length };
593
- continue;
594
- }
595
- if (
596
- fence &&
597
- new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)
598
- ) {
599
- fence = undefined;
600
- }
635
+ fence = advanceTelegramMarkdownFence(fence, line);
601
636
  }
602
637
  if (current.length > 0) chunks.push(current.join("\n"));
603
638
  return chunks;
@@ -606,7 +641,9 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
606
641
  function countTelegramNativeMarkdownBlocks(block: string): number {
607
642
  if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
608
643
  const lines = block.split("\n").filter((line) => line.trim().length > 0);
609
- if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))) {
644
+ if (
645
+ lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))
646
+ ) {
610
647
  return Math.max(1, lines.length);
611
648
  }
612
649
  return 1;
@@ -639,13 +676,9 @@ function splitTelegramNativeMarkdownLongFenceBlock(
639
676
  const lines = block.split("\n");
640
677
  const opening = lines[0] ?? "";
641
678
  const closing = lines[lines.length - 1] ?? "";
642
- const openingMatch = opening?.match(/^ {0,3}(`{3,}|~{3,})/);
643
- if (!openingMatch || !closing || lines.length < 2) return undefined;
644
- const markerText = openingMatch[1] ?? "```";
645
- const marker = markerText[0] as "`" | "~";
646
- if (
647
- !new RegExp(`^ {0,3}${marker}{${markerText.length},}\\s*$`).test(closing)
648
- ) {
679
+ const openingFence = parseTelegramMarkdownFenceOpening(opening);
680
+ if (!openingFence || !closing || lines.length < 2) return undefined;
681
+ if (!isTelegramMarkdownFenceClosing(closing, openingFence)) {
649
682
  return undefined;
650
683
  }
651
684
  const maxContentLength =
@@ -721,33 +754,23 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
721
754
  recordOwnership?: TelegramReplyOwnershipRecorder["record"];
722
755
  sendRichMessage: (
723
756
  body: TelegramSendRichMessageBody,
757
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
724
758
  ) => Promise<TelegramSentMessage>;
725
759
  },
726
760
  options?: TelegramReplyTargetOptions & { replyMarkup?: TReplyMarkup },
727
761
  ): Promise<number | undefined> {
728
- assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
729
- let lastMessageId: number | undefined;
730
- const chunks = splitTelegramNativeMarkdown(markdown);
731
- for (const [index, chunk] of chunks.entries()) {
732
- const sent = await withTelegramReplyParameters(
733
- chatId, index === 0 ? replyToMessageId : undefined, options?.target,
734
- (replyParameters) => deps.sendRichMessage({
735
- chat_id: chatId,
736
- rich_message: { markdown: chunk },
737
- reply_markup:
738
- index === chunks.length - 1 ? options?.replyMarkup : undefined,
739
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
740
- ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
741
- }),
742
- );
743
- lastMessageId = sent.message_id;
744
- deps.recordOwnership?.({
745
- chatId,
746
- messageId: sent.message_id,
747
- target: options?.target,
748
- });
749
- }
750
- return lastMessageId;
762
+ return sendTelegramChunkSequence(
763
+ chatId,
764
+ splitTelegramNativeMarkdown(markdown),
765
+ replyToMessageId,
766
+ options,
767
+ deps.recordOwnership,
768
+ (chunk, fields, callOptions) =>
769
+ deps.sendRichMessage(
770
+ { chat_id: chatId, rich_message: { markdown: chunk }, ...fields },
771
+ callOptions,
772
+ ),
773
+ );
751
774
  }
752
775
 
753
776
  async function sendTelegramNativeRichMessage(
@@ -755,16 +778,31 @@ async function sendTelegramNativeRichMessage(
755
778
  richMessage: TelegramInputRichMessage,
756
779
  deps: {
757
780
  recordOwnership?: TelegramReplyOwnershipRecorder["record"];
758
- sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
781
+ sendRichMessage: (
782
+ body: TelegramSendRichMessageBody,
783
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
784
+ ) => Promise<TelegramSentMessage>;
759
785
  },
760
786
  options?: TelegramReplyTargetOptions,
761
787
  ): Promise<number> {
762
- const sent = await deps.sendRichMessage({
763
- chat_id: chatId,
764
- rich_message: richMessage,
765
- ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
788
+ options = { ...options, target: options?.target && { ...options.target } };
789
+ const assertAuthority = options.assertAuthority;
790
+ assertTelegramApiCallAuthority(assertAuthority, false);
791
+ const sent = await deps.sendRichMessage(
792
+ {
793
+ chat_id: chatId,
794
+ rich_message: richMessage,
795
+ ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
796
+ },
797
+ assertAuthority ? { assertAuthority } : undefined,
798
+ );
799
+ assertTelegramApiCallAuthority(assertAuthority, true);
800
+ deps.recordOwnership?.({
801
+ chatId,
802
+ messageId: sent.message_id,
803
+ target: options?.target,
766
804
  });
767
- deps.recordOwnership?.({ chatId, messageId: sent.message_id, target: options?.target });
805
+ assertTelegramApiCallAuthority(assertAuthority, true);
768
806
  return sent.message_id;
769
807
  }
770
808
 
@@ -783,6 +821,7 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
783
821
  getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
784
822
  sendRichMessage: (
785
823
  body: TelegramSendRichMessageBody,
824
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
786
825
  ) => Promise<TelegramSentMessage>;
787
826
  }
788
827
 
@@ -805,6 +844,7 @@ export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
805
844
  text: string,
806
845
  mode: TelegramRenderMode,
807
846
  replyMarkup: TReplyMarkup,
847
+ options?: TelegramReplyTargetOptions,
808
848
  ) => Promise<void>;
809
849
  sendInteractiveMessage: (
810
850
  chatId: number,
@@ -836,6 +876,7 @@ export interface TelegramRenderedMessageDeliveryRuntimeDeps<
836
876
  getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
837
877
  sendRichMessage: (
838
878
  body: TelegramSendRichMessageBody,
879
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
839
880
  ) => Promise<TelegramSentMessage>;
840
881
  }
841
882
 
@@ -872,6 +913,7 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
872
913
  sendRenderedChunks: (chunks, chunkOptions) =>
873
914
  deps.replyTransport.sendRenderedChunks(chatId, chunks, {
874
915
  target: chunkOptions?.target,
916
+ assertAuthority: chunkOptions?.assertAuthority,
875
917
  replyToMessageId:
876
918
  chunkOptions?.replyToMessageId ?? replyToMessageId,
877
919
  }),
@@ -880,6 +922,10 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
880
922
  );
881
923
  },
882
924
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
925
+ options = {
926
+ ...options,
927
+ target: options?.target && { ...options.target },
928
+ };
883
929
  const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
884
930
  if (renderingMode === "html") {
885
931
  return deps.replyTransport.sendRenderedChunks(
@@ -889,6 +935,7 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
889
935
  replyMarkup: options?.replyMarkup,
890
936
  target: options?.target,
891
937
  replyToMessageId,
938
+ assertAuthority: options.assertAuthority,
892
939
  },
893
940
  );
894
941
  }
@@ -909,12 +956,17 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
909
956
  text,
910
957
  mode,
911
958
  replyMarkup,
959
+ options,
912
960
  ) => {
961
+ options = {
962
+ ...options,
963
+ target: options?.target && { ...options.target },
964
+ };
913
965
  await deps.replyTransport.editRenderedMessage(
914
966
  chatId,
915
967
  messageId,
916
968
  deps.renderTelegramMessage(text, { mode }),
917
- { replyMarkup },
969
+ { ...options, replyMarkup },
918
970
  );
919
971
  },
920
972
  sendInteractiveMessage: async (
@@ -924,6 +976,10 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
924
976
  replyMarkup,
925
977
  options,
926
978
  ) => {
979
+ options = {
980
+ ...options,
981
+ target: options?.target && { ...options.target },
982
+ };
927
983
  return deps.replyTransport.sendRenderedChunks(
928
984
  chatId,
929
985
  deps.renderTelegramMessage(text, { mode }),
@@ -931,40 +987,20 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
931
987
  replyMarkup,
932
988
  target: options?.target,
933
989
  replyToMessageId: options?.replyToMessageId,
990
+ assertAuthority: options.assertAuthority,
934
991
  },
935
992
  );
936
993
  },
937
994
  sendSectionRichMessage: (chatId, message, options) =>
938
- sendTelegramNativeRichMessage(chatId, message, {
939
- recordOwnership: deps.recordOwnership,
940
- sendRichMessage: deps.sendRichMessage,
941
- }, options),
942
- };
943
- }
944
-
945
- // --- Dedup-wrapped Reply Wrappers ---
946
-
947
- /** Wrap a sendTextReply with reply dedup so only the first message
948
- * in a turn carries reply metadata. */
949
- export function dedupSendTextReply(
950
- dedup: ReplyDedupRuntime,
951
- inner: (
952
- chatId: number,
953
- replyToMessageId: number | undefined,
954
- text: string,
955
- options?: TelegramTextReplyOptions,
956
- ) => Promise<number | undefined>,
957
- ): (
958
- chatId: number,
959
- replyToMessageId: number,
960
- text: string,
961
- options?: TelegramTextReplyOptions,
962
- ) => Promise<number | undefined> {
963
- return async (chatId, replyToMessageId, text, options) => {
964
- const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
965
- ? replyToMessageId
966
- : undefined;
967
- return inner(chatId, effectiveReplyTo, text, options);
995
+ sendTelegramNativeRichMessage(
996
+ chatId,
997
+ message,
998
+ {
999
+ recordOwnership: deps.recordOwnership,
1000
+ sendRichMessage: deps.sendRichMessage,
1001
+ },
1002
+ options,
1003
+ ),
968
1004
  };
969
1005
  }
970
1006
 
@@ -1174,7 +1210,10 @@ export function createTelegramGuestPlaceholderRuntime(
1174
1210
  parseMode: "HTML",
1175
1211
  });
1176
1212
  } catch (error) {
1177
- nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(error, intervalMs);
1213
+ nextDelayMs = getTelegramGuestPlaceholderRetryDelayMs(
1214
+ error,
1215
+ intervalMs,
1216
+ );
1178
1217
  deps.recordRuntimeEvent?.("guest", error, {
1179
1218
  phase: "guest-placeholder-edit",
1180
1219
  retryAfterMs: nextDelayMs,