@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
@@ -8,22 +8,31 @@
8
8
 
9
9
  import { randomUUID } from "node:crypto";
10
10
  import { createWriteStream, openAsBlob } from "node:fs";
11
- import { mkdir, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
11
+ import {
12
+ mkdir,
13
+ readdir,
14
+ rename,
15
+ stat,
16
+ unlink,
17
+ writeFile,
18
+ } from "node:fs/promises";
12
19
  import { request as requestHttps } from "node:https";
13
20
  import { join } from "node:path";
14
- import type { TelegramAttachmentSource } from "./media.ts";
15
- import { resolveTelegramAttachmentsDir } from "./paths.ts";
16
21
  import { Readable, Transform } from "node:stream";
17
22
  import { pipeline } from "node:stream/promises";
18
23
 
24
+ import type { TelegramAttachmentSource } from "./media.ts";
25
+ import { resolveTelegramAttachmentsDir } from "./paths.ts";
26
+
19
27
  const TELEGRAM_API_BASE = "https://api.telegram.org";
20
28
 
21
29
  export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
22
30
 
23
- export function getTelegramInboundFileByteLimitFromEnv(
31
+ /** First positive safe-integer byte limit among ordered env names, else the caller's default. */
32
+ export function getTelegramByteLimitFromEnv(
24
33
  env: NodeJS.ProcessEnv,
25
- names: string[],
26
- defaultValue = TELEGRAM_FILE_MAX_BYTES,
34
+ names: readonly string[],
35
+ defaultValue: number,
27
36
  ): number {
28
37
  for (const name of names) {
29
38
  const rawValue = env[name]?.trim();
@@ -40,7 +49,7 @@ function getTelegramApiTempDir(): string {
40
49
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
41
50
  const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
42
51
  const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
43
- const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
52
+ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramByteLimitFromEnv(
44
53
  process.env,
45
54
  ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
46
55
  TELEGRAM_FILE_MAX_BYTES,
@@ -302,8 +311,7 @@ export type TelegramInputRichBlock =
302
311
  };
303
312
 
304
313
  export type TelegramInputRichDraftBlock =
305
- | TelegramInputRichBlock
306
- | { type: "thinking"; text: TelegramRichText };
314
+ TelegramInputRichBlock | { type: "thinking"; text: TelegramRichText };
307
315
 
308
316
  export type TelegramInputRichMessage = TelegramInputRichMessageCommon &
309
317
  (
@@ -383,6 +391,8 @@ export interface TelegramApiRetryWait {
383
391
  }
384
392
 
385
393
  export interface TelegramApiCallOptions {
394
+ /** Local per-call effect authority; independent of source completion and transport ownership. */
395
+ assertAuthority?: () => void;
386
396
  signal?: AbortSignal;
387
397
  maxAttempts?: number;
388
398
  retryRateLimit?: boolean;
@@ -393,6 +403,27 @@ export interface TelegramApiCallOptions {
393
403
  onRetryWait?: (wait: TelegramApiRetryWait) => void;
394
404
  }
395
405
 
406
+ export class TelegramApiAuthorityError extends Error {
407
+ readonly requestIssued: boolean;
408
+ constructor(requestIssued: boolean) {
409
+ super("Telegram API call authority is unavailable.");
410
+ this.name = "TelegramApiAuthorityError";
411
+ this.requestIssued = requestIssued;
412
+ }
413
+ }
414
+
415
+ /** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
416
+ export function assertTelegramApiCallAuthority(
417
+ assertAuthority: (() => void) | undefined,
418
+ requestIssued: boolean,
419
+ ): void {
420
+ try {
421
+ assertAuthority?.();
422
+ } catch {
423
+ throw new TelegramApiAuthorityError(requestIssued);
424
+ }
425
+ }
426
+
396
427
  interface TelegramGetFileResult {
397
428
  file_path: string;
398
429
  file_size?: number;
@@ -447,7 +478,10 @@ export interface TelegramEditGuestInlineMessageContent {
447
478
  parseMode?: "HTML";
448
479
  }
449
480
 
450
- export interface TelegramAnswerCallbackQueryOptions {
481
+ export interface TelegramAnswerCallbackQueryOptions extends Pick<
482
+ TelegramApiCallOptions,
483
+ "assertAuthority"
484
+ > {
451
485
  recordRuntimeEvent?: (
452
486
  kind: "api",
453
487
  error: unknown,
@@ -478,6 +512,7 @@ export interface TelegramApiClient {
478
512
  answerCallbackQuery: (
479
513
  callbackQueryId: string,
480
514
  text?: string,
515
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
481
516
  ) => Promise<void>;
482
517
  answerGuestQuery?: (
483
518
  guestQueryId: string,
@@ -487,26 +522,32 @@ export interface TelegramApiClient {
487
522
  }
488
523
 
489
524
  export interface TelegramApiTargetActivityRuntime {
490
- begin: (
491
- method: string,
492
- body: Record<string, unknown>,
493
- ) => () => void;
525
+ begin: (method: string, body: Record<string, unknown>) => () => void;
494
526
  hasPendingTarget: (target: { chatId: number; threadId?: number }) => boolean;
495
527
  listPendingTargets: () => { chatId: number; threadId: number }[];
496
528
  listPendingChats: () => number[];
497
529
  }
498
530
 
499
531
  function parseTelegramApiTargetInteger(value: unknown): number | undefined {
500
- const parsed = typeof value === "number" ? value :
501
- typeof value === "string" && /^-?\d+$/u.test(value) ? Number(value) : undefined;
502
- return parsed !== undefined && Number.isSafeInteger(parsed) ? parsed : undefined;
532
+ const parsed =
533
+ typeof value === "number"
534
+ ? value
535
+ : typeof value === "string" && /^-?\d+$/u.test(value)
536
+ ? Number(value)
537
+ : undefined;
538
+ return parsed !== undefined && Number.isSafeInteger(parsed)
539
+ ? parsed
540
+ : undefined;
503
541
  }
504
542
 
505
543
  export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActivityRuntime {
506
- const pending = new Map<string, {
507
- target: { chatId: number; threadId: number };
508
- count: number;
509
- }>();
544
+ const pending = new Map<
545
+ string,
546
+ {
547
+ target: { chatId: number; threadId: number };
548
+ count: number;
549
+ }
550
+ >();
510
551
  const pendingChats = new Map<number, number>();
511
552
  const messageScopedMethods = new Set([
512
553
  "deleteMessage",
@@ -519,8 +560,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
519
560
  const chatId = parseTelegramApiTargetInteger(body.chat_id);
520
561
  const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
521
562
  const messageId = parseTelegramApiTargetInteger(body.message_id);
522
- const chatScoped = chatId !== undefined && threadId === undefined &&
523
- messageId !== undefined && messageScopedMethods.has(method);
563
+ const chatScoped =
564
+ chatId !== undefined &&
565
+ threadId === undefined &&
566
+ messageId !== undefined &&
567
+ messageScopedMethods.has(method);
524
568
  if (chatId === undefined || (threadId === undefined && !chatScoped)) {
525
569
  return () => undefined;
526
570
  }
@@ -528,7 +572,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
528
572
  if (key) {
529
573
  const existing = pending.get(key);
530
574
  if (existing) existing.count += 1;
531
- else pending.set(key, { target: { chatId, threadId: threadId! }, count: 1 });
575
+ else
576
+ pending.set(key, {
577
+ target: { chatId, threadId: threadId! },
578
+ count: 1,
579
+ });
532
580
  } else {
533
581
  pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
534
582
  }
@@ -548,8 +596,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
548
596
  };
549
597
  },
550
598
  hasPendingTarget(target) {
551
- return pendingChats.has(target.chatId) ||
552
- (target.threadId !== undefined && pending.has(`${target.chatId}:${target.threadId}`));
599
+ return (
600
+ pendingChats.has(target.chatId) ||
601
+ (target.threadId !== undefined &&
602
+ pending.has(`${target.chatId}:${target.threadId}`))
603
+ );
553
604
  },
554
605
  listPendingTargets() {
555
606
  return Array.from(pending.values(), ({ target }) => ({ ...target }));
@@ -577,15 +628,19 @@ export function createTelegramApiTargetTrackingClient(
577
628
  }
578
629
  };
579
630
  return {
580
- call: (method, body, options) => track(
581
- method,
582
- body,
583
- () => client.call(method, body, options),
584
- ),
631
+ call: (method, body, options) =>
632
+ track(method, body, () => client.call(method, body, options)),
585
633
  callMultipart: (method, fields, fileField, filePath, fileName, options) =>
586
- track(method, fields, () => client.callMultipart(
587
- method, fields, fileField, filePath, fileName, options,
588
- )),
634
+ track(method, fields, () =>
635
+ client.callMultipart(
636
+ method,
637
+ fields,
638
+ fileField,
639
+ filePath,
640
+ fileName,
641
+ options,
642
+ ),
643
+ ),
589
644
  downloadFile: client.downloadFile,
590
645
  answerCallbackQuery: client.answerCallbackQuery,
591
646
  ...(client.answerGuestQuery
@@ -620,13 +675,19 @@ export interface TelegramApiWorkspaceAdmissionPort {
620
675
  resumed: boolean;
621
676
  }
622
677
  | { kind: "blocked"; reason: "retirement-fenced" };
623
- releaseAdmission: (expected: TelegramApiWorkspaceAdmissionLeaseLike) => boolean;
678
+ releaseAdmission: (
679
+ expected: TelegramApiWorkspaceAdmissionLeaseLike,
680
+ ) => boolean;
624
681
  }
625
682
 
626
683
  export class TelegramApiWorkspaceAdmissionError extends Error {
627
- readonly code: "blocked" | "unavailable" | "release-lost" | "duplicate-operation";
684
+ readonly code:
685
+ "blocked" | "unavailable" | "release-lost" | "duplicate-operation";
628
686
 
629
- constructor(code: TelegramApiWorkspaceAdmissionError["code"], message: string) {
687
+ constructor(
688
+ code: TelegramApiWorkspaceAdmissionError["code"],
689
+ message: string,
690
+ ) {
630
691
  super(message);
631
692
  this.name = "TelegramApiWorkspaceAdmissionError";
632
693
  this.code = code;
@@ -744,8 +805,9 @@ export function createTelegramApiWorkspaceAdmissionClient(
744
805
  }
745
806
 
746
807
  export interface TelegramBridgeApiRuntimeDeps {
747
- captureRequestErrorHandler?: (body: Record<string, unknown>) =>
748
- ((error: unknown) => Promise<void>) | undefined;
808
+ captureRequestErrorHandler?: (
809
+ body: Record<string, unknown>,
810
+ ) => ((error: unknown) => Promise<void>) | undefined;
749
811
  client: TelegramApiClient;
750
812
  tempDir: string;
751
813
  maxFileSizeBytes: number;
@@ -762,20 +824,10 @@ export interface TelegramBridgeApiRuntimeDeps {
762
824
  chatActionMaxGates?: number;
763
825
  }
764
826
 
765
- export interface TelegramBridgeApiRuntime {
766
- call: <TResponse>(
767
- method: string,
768
- body: Record<string, unknown>,
769
- options?: TelegramApiCallOptions,
770
- ) => Promise<TResponse>;
771
- callMultipart: <TResponse>(
772
- method: string,
773
- fields: Record<string, string>,
774
- fileField: string,
775
- filePath: string,
776
- fileName: string,
777
- options?: TelegramApiCallOptions,
778
- ) => Promise<TResponse>;
827
+ export interface TelegramBridgeApiRuntime extends Pick<
828
+ TelegramApiClient,
829
+ "call" | "callMultipart"
830
+ > {
779
831
  downloadFile: (
780
832
  fileId: string,
781
833
  suggestedName: string,
@@ -788,6 +840,7 @@ export interface TelegramBridgeApiRuntime {
788
840
  ) => Promise<TelegramUpdate[]>;
789
841
  setMyCommands: (
790
842
  commands: readonly { command: string; description: string }[],
843
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
791
844
  ) => Promise<boolean>;
792
845
  sendChatAction: (
793
846
  chatId: number,
@@ -812,15 +865,20 @@ export interface TelegramBridgeApiRuntime {
812
865
  message_thread_id?: number;
813
866
  },
814
867
  ) => Promise<boolean>;
815
- sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
868
+ sendMessage: (
869
+ body: TelegramSendMessageBody,
870
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
871
+ ) => Promise<TelegramSentMessage>;
816
872
  sendRichMessage: (
817
873
  body: TelegramSendRichMessageBody,
874
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
818
875
  ) => Promise<TelegramSentMessage>;
819
876
  sendRichMessageDraft: (
820
877
  body: TelegramSendRichMessageDraftBody,
821
878
  ) => Promise<boolean>;
822
879
  editMessageText: (
823
880
  body: TelegramEditMessageTextBody,
881
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
824
882
  ) => Promise<"edited" | "unchanged">;
825
883
  editMessageReplyMarkup: (
826
884
  chatId: number,
@@ -830,6 +888,7 @@ export interface TelegramBridgeApiRuntime {
830
888
  answerCallbackQuery: (
831
889
  callbackQueryId: string,
832
890
  text?: string,
891
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
833
892
  ) => Promise<void>;
834
893
  answerGuestQuery: (
835
894
  guestQueryId: string,
@@ -870,15 +929,31 @@ export function createTelegramAttachmentFileName(
870
929
  botScope?: string,
871
930
  ): string {
872
931
  const chat = source.chat;
873
- const clean = (value: string): string => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
874
- const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
875
- ? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
876
- : chat.username ? clean(chat.username) : String(Math.abs(chat.id));
932
+ const clean = (value: string): string =>
933
+ sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
934
+ const scope = source.scope
935
+ ? clean(source.scope)
936
+ : chat?.type === "private" || !chat
937
+ ? botScope
938
+ ? clean(botScope)
939
+ : chat
940
+ ? String(Math.abs(chat.id))
941
+ : ""
942
+ : chat.username
943
+ ? clean(chat.username)
944
+ : String(Math.abs(chat.id));
877
945
  const dot = generatedName.lastIndexOf(".");
878
946
  const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
879
- const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
880
- return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
881
- (source.index !== undefined ? `-${source.index}` : "") + tail;
947
+ const tail = source.userFileName
948
+ ? `-${clean(source.userFileName)}`
949
+ : extension;
950
+ return (
951
+ [sanitizeFileName(source.kind), scope, String(source.messageId)]
952
+ .filter(Boolean)
953
+ .join("-") +
954
+ (source.index !== undefined ? `-${source.index}` : "") +
955
+ tail
956
+ );
882
957
  }
883
958
 
884
959
  class TelegramApiMalformedSuccessError extends Error {
@@ -961,14 +1036,23 @@ export function getTelegramApiErrorRequestTarget(
961
1036
  }
962
1037
 
963
1038
  export function isTelegramStaleTargetHttpError(error: unknown): boolean {
964
- if (!(error instanceof TelegramApiHttpError) || error.status !== 400) return false;
965
- return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(error.message);
1039
+ if (!(error instanceof TelegramApiHttpError) || error.status !== 400)
1040
+ return false;
1041
+ return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(
1042
+ error.message,
1043
+ );
966
1044
  }
967
1045
 
968
1046
  /** Only a parsed Telegram rejection of this method proves a request had no effect. */
969
- export function isTelegramApiRequestRejected(error: unknown, method: string): boolean {
970
- return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
971
- error.rejectedRequestMethod === method;
1047
+ export function isTelegramApiRequestRejected(
1048
+ error: unknown,
1049
+ method: string,
1050
+ ): boolean {
1051
+ return (
1052
+ error instanceof TelegramApiHttpError &&
1053
+ error.rejectedRequestMethod !== undefined &&
1054
+ error.rejectedRequestMethod === method
1055
+ );
972
1056
  }
973
1057
 
974
1058
  export function isTelegramMessageNotModifiedError(error: unknown): boolean {
@@ -1010,14 +1094,20 @@ export function isRetryableTelegramApiError(error: unknown): boolean {
1010
1094
  }
1011
1095
 
1012
1096
  export function getTelegramApiRetryAfterMs(error: unknown): number | undefined {
1013
- return error instanceof TelegramApiHttpError && error.retryAfterSeconds !== undefined
1097
+ return error instanceof TelegramApiHttpError &&
1098
+ error.retryAfterSeconds !== undefined
1014
1099
  ? Math.max(0, error.retryAfterSeconds * 1000)
1015
1100
  : undefined;
1016
1101
  }
1017
1102
 
1018
1103
  export function isTelegramMessageUnavailableError(error: unknown): boolean {
1019
- return error instanceof TelegramApiHttpError && error.status === 400 &&
1020
- /Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(error.message);
1104
+ return (
1105
+ error instanceof TelegramApiHttpError &&
1106
+ error.status === 400 &&
1107
+ /Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(
1108
+ error.message,
1109
+ )
1110
+ );
1021
1111
  }
1022
1112
 
1023
1113
  function getTelegramRetryDelayMs(
@@ -1042,10 +1132,7 @@ function throwIfTelegramApiCallAborted(signal: AbortSignal | undefined): void {
1042
1132
  if (signal?.aborted) throw getTelegramApiAbortReason(signal);
1043
1133
  }
1044
1134
 
1045
- function sleepTelegramRetry(
1046
- ms: number,
1047
- signal?: AbortSignal,
1048
- ): Promise<void> {
1135
+ function sleepTelegramRetry(ms: number, signal?: AbortSignal): Promise<void> {
1049
1136
  if (signal?.aborted) return Promise.reject(getTelegramApiAbortReason(signal));
1050
1137
  return new Promise((resolve, reject) => {
1051
1138
  const timeout = setTimeout(() => {
@@ -1120,7 +1207,12 @@ async function publishTelegramDownload(
1120
1207
  } catch (error) {
1121
1208
  const code = (error as NodeJS.ErrnoException | undefined)?.code;
1122
1209
  // Windows may briefly deny replacement while another publisher/scanner holds the file.
1123
- if (process.platform !== "win32" || (code !== "EPERM" && code !== "EACCES") || attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS) throw error;
1210
+ if (
1211
+ process.platform !== "win32" ||
1212
+ (code !== "EPERM" && code !== "EACCES") ||
1213
+ attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS
1214
+ )
1215
+ throw error;
1124
1216
  await sleepTelegramRetry(50 * 2 ** attempt, signal);
1125
1217
  }
1126
1218
  }
@@ -1162,8 +1254,11 @@ async function parseTelegramApiResponse<TResponse>(
1162
1254
  `Telegram API ${method} failed: ${status}${description}`,
1163
1255
  response.status,
1164
1256
  Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
1165
- data?.ok === false && data.error_code === response.status &&
1166
- [400, 401, 403, 404, 429].includes(response.status) ? method : undefined,
1257
+ data?.ok === false &&
1258
+ data.error_code === response.status &&
1259
+ [400, 401, 403, 404, 429].includes(response.status)
1260
+ ? method
1261
+ : undefined,
1167
1262
  );
1168
1263
  }
1169
1264
  if (!data) {
@@ -1451,9 +1546,18 @@ function withTelegramTransportDiagnostics(
1451
1546
 
1452
1547
  async function callTelegramWithRetry<TResponse>(
1453
1548
  method: string,
1454
- request: (family?: TelegramNetworkFamily) => Promise<Response>,
1549
+ request: (
1550
+ family: TelegramNetworkFamily | undefined,
1551
+ beforeRequest: () => void,
1552
+ ) => Promise<Response>,
1455
1553
  options: TelegramApiCallOptions | undefined,
1456
1554
  ): Promise<TResponse> {
1555
+ const assertAuthority = options?.assertAuthority;
1556
+ let requestIssued = false;
1557
+ const beforeRequest = () => {
1558
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1559
+ requestIssued = true;
1560
+ };
1457
1561
  const retrySafe =
1458
1562
  options?.retrySafety === "safe" ||
1459
1563
  (options?.retrySafety !== "non-idempotent" &&
@@ -1485,15 +1589,24 @@ async function callTelegramWithRetry<TResponse>(
1485
1589
  };
1486
1590
  for (let attempt = 0; ; attempt += 1) {
1487
1591
  throwIfTelegramApiCallAborted(options?.signal);
1592
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1488
1593
  try {
1489
- return unwrapTelegramApiResult(
1594
+ const result = unwrapTelegramApiResult(
1490
1595
  method,
1491
1596
  await parseTelegramApiResponse<TResponse>(
1492
- await callTelegramTransportRequest(request, retrySafe),
1597
+ await callTelegramTransportRequest(
1598
+ (family) => request(family, beforeRequest),
1599
+ retrySafe,
1600
+ ),
1493
1601
  method,
1494
1602
  ),
1495
1603
  );
1604
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1605
+ return result;
1496
1606
  } catch (error) {
1607
+ // Authority refusal is never a transport failure, retry grant or acknowledged success.
1608
+ if (error instanceof TelegramApiAuthorityError) throw error;
1609
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1497
1610
  const retryable =
1498
1611
  isRetryableTelegramApiError(error) &&
1499
1612
  !(
@@ -1577,17 +1690,20 @@ export async function callTelegram<TResponse>(
1577
1690
  try {
1578
1691
  return await callTelegramWithRetry(
1579
1692
  method,
1580
- async (family) =>
1581
- telegramFetch(
1693
+ async (family, beforeRequest) => {
1694
+ const encodedBody = JSON.stringify(body);
1695
+ beforeRequest();
1696
+ return telegramFetch(
1582
1697
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1583
1698
  {
1584
1699
  method: "POST",
1585
1700
  headers: { "content-type": "application/json" },
1586
- body: JSON.stringify(body),
1701
+ body: encodedBody,
1587
1702
  signal: options?.signal,
1588
1703
  },
1589
1704
  family,
1590
- ),
1705
+ );
1706
+ },
1591
1707
  options,
1592
1708
  );
1593
1709
  } catch (error) {
@@ -1628,11 +1744,13 @@ export async function callTelegramMultipart<TResponse>(
1628
1744
  options?: TelegramApiCallOptions,
1629
1745
  ): Promise<TResponse> {
1630
1746
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
1747
+ options = options && { ...options };
1748
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
1631
1749
  const fileBlob = await openAsBlob(filePath);
1632
1750
  try {
1633
1751
  return await callTelegramWithRetry(
1634
1752
  method,
1635
- async (family) => {
1753
+ async (family, beforeRequest) => {
1636
1754
  if (family) {
1637
1755
  const multipart = await buildTelegramMultipartBody(
1638
1756
  fields,
@@ -1640,6 +1758,7 @@ export async function callTelegramMultipart<TResponse>(
1640
1758
  fileBlob,
1641
1759
  fileName,
1642
1760
  );
1761
+ beforeRequest();
1643
1762
  return telegramFetch(
1644
1763
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1645
1764
  {
@@ -1656,6 +1775,7 @@ export async function callTelegramMultipart<TResponse>(
1656
1775
  form.set(key, value);
1657
1776
  }
1658
1777
  form.set(fileField, fileBlob, fileName);
1778
+ beforeRequest();
1659
1779
  return telegramFetch(
1660
1780
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1661
1781
  {
@@ -1721,11 +1841,6 @@ export async function downloadTelegramFile(
1721
1841
  return targetPath;
1722
1842
  }
1723
1843
 
1724
- /** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
1725
- export function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined {
1726
- return text?.replace(/(?<!\.)\.\s*$/u, "");
1727
- }
1728
-
1729
1844
  export async function answerTelegramCallbackQuery(
1730
1845
  botToken: string | undefined,
1731
1846
  callbackQueryId: string,
@@ -1739,6 +1854,9 @@ export async function answerTelegramCallbackQuery(
1739
1854
  text
1740
1855
  ? { callback_query_id: callbackQueryId, text }
1741
1856
  : { callback_query_id: callbackQueryId },
1857
+ options.assertAuthority
1858
+ ? { assertAuthority: options.assertAuthority }
1859
+ : undefined,
1742
1860
  );
1743
1861
  } catch (error) {
1744
1862
  options.recordRuntimeEvent?.(
@@ -1748,6 +1866,7 @@ export async function answerTelegramCallbackQuery(
1748
1866
  method: "answerCallbackQuery",
1749
1867
  }),
1750
1868
  );
1869
+ if (error instanceof TelegramApiAuthorityError) throw error;
1751
1870
  }
1752
1871
  }
1753
1872
 
@@ -1852,18 +1971,25 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1852
1971
  recordRuntimeEvent: deps.recordRuntimeEvent,
1853
1972
  });
1854
1973
  const admittedClient = deps.workspaceAdmission
1855
- ? createTelegramApiWorkspaceAdmissionClient(client, deps.workspaceAdmission, {
1856
- onReleaseError(error, method) {
1857
- deps.recordRuntimeEvent("api", error, {
1858
- phase: "workspace-admission-release",
1859
- method,
1860
- });
1974
+ ? createTelegramApiWorkspaceAdmissionClient(
1975
+ client,
1976
+ deps.workspaceAdmission,
1977
+ {
1978
+ onReleaseError(error, method) {
1979
+ deps.recordRuntimeEvent("api", error, {
1980
+ phase: "workspace-admission-release",
1981
+ method,
1982
+ });
1983
+ },
1861
1984
  },
1862
- })
1985
+ )
1863
1986
  : client;
1864
1987
  const runtime = createTelegramBridgeApiRuntime({
1865
1988
  client: deps.targetActivity
1866
- ? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
1989
+ ? createTelegramApiTargetTrackingClient(
1990
+ admittedClient,
1991
+ deps.targetActivity,
1992
+ )
1867
1993
  : admittedClient,
1868
1994
  tempDir: getTelegramApiTempDir(),
1869
1995
  maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
@@ -1877,10 +2003,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1877
2003
  async deleteWorkspaceThread(authorize) {
1878
2004
  // The exclusive deletion permit replaces ordinary target admission.
1879
2005
  const target = authorize();
1880
- const deleted = await client.call<boolean>("deleteForumTopic", {
1881
- chat_id: target.chatId, message_thread_id: target.threadId,
1882
- }, { maxAttempts: 1, retrySafety: "non-idempotent" });
1883
- if (deleted !== true) throw new Error("Telegram Workspace Thread deletion was not confirmed.");
2006
+ const deleted = await client.call<boolean>(
2007
+ "deleteForumTopic",
2008
+ {
2009
+ chat_id: target.chatId,
2010
+ message_thread_id: target.threadId,
2011
+ },
2012
+ { maxAttempts: 1, retrySafety: "non-idempotent" },
2013
+ );
2014
+ if (deleted !== true)
2015
+ throw new Error(
2016
+ "Telegram Workspace Thread deletion was not confirmed.",
2017
+ );
1884
2018
  },
1885
2019
  };
1886
2020
  }
@@ -1895,7 +2029,9 @@ export function createTelegramBridgeApiRuntime(
1895
2029
  try {
1896
2030
  await handler?.(error);
1897
2031
  } catch (recoveryError) {
1898
- deps.recordRuntimeEvent("api", recoveryError, { phase: "stale-target-recovery" });
2032
+ deps.recordRuntimeEvent("api", recoveryError, {
2033
+ phase: "stale-target-recovery",
2034
+ });
1899
2035
  }
1900
2036
  };
1901
2037
  const now = deps.now ?? Date.now;
@@ -1955,7 +2091,8 @@ export function createTelegramBridgeApiRuntime(
1955
2091
  }
1956
2092
  let gate = chatActionGates.get(chatActionKeys.action);
1957
2093
  if (!gate) {
1958
- if (chatActionGates.size >= chatActionMaxGates) return true as TResponse;
2094
+ if (chatActionGates.size >= chatActionMaxGates)
2095
+ return true as TResponse;
1959
2096
  gate = { notBeforeMs: 0 };
1960
2097
  chatActionGates.set(chatActionKeys.action, gate);
1961
2098
  }
@@ -2024,9 +2161,13 @@ export function createTelegramBridgeApiRuntime(
2024
2161
  return await deps.client.call<TResponse>(method, body, options);
2025
2162
  } catch (error) {
2026
2163
  await recoverRequestError(recoverError, error);
2027
- if (method === "deleteMessage" && error instanceof TelegramApiHttpError &&
2028
- error.status === 400 && error.message ===
2029
- "Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found") {
2164
+ if (
2165
+ method === "deleteMessage" &&
2166
+ error instanceof TelegramApiHttpError &&
2167
+ error.status === 400 &&
2168
+ error.message ===
2169
+ "Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found"
2170
+ ) {
2030
2171
  return true as TResponse;
2031
2172
  }
2032
2173
  deps.recordRuntimeEvent(
@@ -2083,7 +2224,11 @@ export function createTelegramBridgeApiRuntime(
2083
2224
  return await deps.client.downloadFile(
2084
2225
  fileId,
2085
2226
  source
2086
- ? createTelegramAttachmentFileName(source, suggestedName, deps.getBotScope?.())
2227
+ ? createTelegramAttachmentFileName(
2228
+ source,
2229
+ suggestedName,
2230
+ deps.getBotScope?.(),
2231
+ )
2087
2232
  : suggestedName,
2088
2233
  deps.tempDir,
2089
2234
  {
@@ -2107,8 +2252,8 @@ export function createTelegramBridgeApiRuntime(
2107
2252
  ),
2108
2253
  getUpdates: (body, signal) =>
2109
2254
  callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
2110
- setMyCommands: (commands) =>
2111
- callRecorded<boolean>("setMyCommands", { commands }),
2255
+ setMyCommands: (commands, options) =>
2256
+ callRecorded<boolean>("setMyCommands", { commands }, options),
2112
2257
  sendChatAction: (chatId, action, options) =>
2113
2258
  callRecorded<boolean>("sendChatAction", {
2114
2259
  chat_id: chatId,
@@ -2152,16 +2297,16 @@ export function createTelegramBridgeApiRuntime(
2152
2297
  body.message_thread_id = options.message_thread_id;
2153
2298
  return callRecorded<boolean>("sendMessageDraft", body);
2154
2299
  },
2155
- sendMessage: (body) =>
2156
- callRecorded<TelegramSentMessage>("sendMessage", body),
2157
- sendRichMessage: (body) =>
2158
- callRecorded<TelegramSentMessage>("sendRichMessage", body),
2300
+ sendMessage: (body, options) =>
2301
+ callRecorded<TelegramSentMessage>("sendMessage", body, options),
2302
+ sendRichMessage: (body, options) =>
2303
+ callRecorded<TelegramSentMessage>("sendRichMessage", body, options),
2159
2304
  sendRichMessageDraft: (body) =>
2160
2305
  callRecorded<boolean>("sendRichMessageDraft", body),
2161
- editMessageText: async (body) => {
2306
+ editMessageText: async (body, options) => {
2162
2307
  const recoverError = deps.captureRequestErrorHandler?.(body);
2163
2308
  try {
2164
- await deps.client.call("editMessageText", body);
2309
+ await deps.client.call("editMessageText", body, options);
2165
2310
  return "edited";
2166
2311
  } catch (error) {
2167
2312
  if (isTelegramMessageNotModifiedError(error)) return "unchanged";
@@ -2183,7 +2328,21 @@ export function createTelegramBridgeApiRuntime(
2183
2328
  reply_markup: replyMarkup,
2184
2329
  });
2185
2330
  },
2186
- answerCallbackQuery: async (callbackQueryId, text) => {
2331
+ answerCallbackQuery: async (callbackQueryId, text, options) => {
2332
+ if (options?.assertAuthority) {
2333
+ try {
2334
+ await callRecorded<boolean>(
2335
+ "answerCallbackQuery",
2336
+ text
2337
+ ? { callback_query_id: callbackQueryId, text }
2338
+ : { callback_query_id: callbackQueryId },
2339
+ { assertAuthority: options.assertAuthority },
2340
+ );
2341
+ } catch (error) {
2342
+ if (error instanceof TelegramApiAuthorityError) throw error;
2343
+ }
2344
+ return;
2345
+ }
2187
2346
  try {
2188
2347
  await deps.client.answerCallbackQuery(callbackQueryId, text);
2189
2348
  } catch (error) {
@@ -2194,6 +2353,7 @@ export function createTelegramBridgeApiRuntime(
2194
2353
  method: "answerCallbackQuery",
2195
2354
  }),
2196
2355
  );
2356
+ if (error instanceof TelegramApiAuthorityError) throw error;
2197
2357
  }
2198
2358
  },
2199
2359
  answerGuestQuery: (
@@ -2259,16 +2419,19 @@ export function createTelegramApiClient(
2259
2419
  const token = getBotToken();
2260
2420
  // Cooldown keys retain only the public bot-id prefix, not the credential.
2261
2421
  const botId = token?.match(/^(\d+):/)?.[1];
2262
- const isDraft = method === "sendMessageDraft" || method === "sendRichMessageDraft";
2263
- const draftKey = isDraft && botId
2264
- ? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
2265
- : undefined;
2422
+ const isDraft =
2423
+ method === "sendMessageDraft" || method === "sendRichMessageDraft";
2424
+ const draftKey =
2425
+ isDraft && botId
2426
+ ? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
2427
+ : undefined;
2266
2428
  if (draftKey) {
2267
2429
  const nowMs = now();
2268
2430
  for (const [key, deadline] of draftRetryNotBeforeByTarget) {
2269
2431
  if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
2270
2432
  }
2271
- if (draftRetryNotBeforeByTarget.has(draftKey)) return false as TResponse;
2433
+ if (draftRetryNotBeforeByTarget.has(draftKey))
2434
+ return false as TResponse;
2272
2435
  }
2273
2436
  try {
2274
2437
  // A draft is a replaceable snapshot, not a body to replay after backoff.
@@ -2299,10 +2462,18 @@ export function createTelegramApiClient(
2299
2462
  });
2300
2463
  } catch (error) {
2301
2464
  if (draftKey && isRetryableTelegramApiError(error)) {
2302
- draftRetryNotBeforeByTarget.set(draftKey, Math.max(
2303
- draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
2304
- now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500),
2305
- ));
2465
+ draftRetryNotBeforeByTarget.set(
2466
+ draftKey,
2467
+ Math.max(
2468
+ draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
2469
+ now() +
2470
+ getTelegramRetryDelayMs(
2471
+ error,
2472
+ 0,
2473
+ options?.retryBaseDelayMs ?? 500,
2474
+ ),
2475
+ ),
2476
+ );
2306
2477
  }
2307
2478
  throw error;
2308
2479
  }
@@ -2334,12 +2505,16 @@ export function createTelegramApiClient(
2334
2505
  options,
2335
2506
  );
2336
2507
  },
2337
- answerCallbackQuery: async (callbackQueryId, text) => {
2508
+ answerCallbackQuery: async (callbackQueryId, text, authorityOptions) => {
2509
+ const assertAuthority = authorityOptions?.assertAuthority;
2510
+ const callbackOptions = assertAuthority
2511
+ ? { ...options, assertAuthority }
2512
+ : options;
2338
2513
  await answerTelegramCallbackQuery(
2339
2514
  getBotToken(),
2340
2515
  callbackQueryId,
2341
2516
  text,
2342
- options,
2517
+ callbackOptions,
2343
2518
  );
2344
2519
  },
2345
2520
  };