@llblab/pi-telegram 0.52.1 → 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 +10 -8
  2. package/BACKLOG.md +1 -16
  3. package/CHANGELOG.md +17 -0
  4. package/README.md +8 -10
  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 +640 -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 +34 -3
  44. package/dist/lib/locks.js +318 -119
  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 +23 -37
  76. package/dist/lib/polling.js +71 -52
  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 +6 -3
  95. package/dist/lib/sync.js +112 -45
  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 +165 -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 +36 -30
  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 +1207 -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 +703 -212
  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 +165 -149
  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 +488 -359
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +322 -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();
@@ -38,8 +47,9 @@ function getTelegramApiTempDir(): string {
38
47
  return resolveTelegramAttachmentsDir();
39
48
  }
40
49
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
50
+ const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
41
51
  const activeTelegramApiWorkspaceAdmissionOperationIds = new Set<string>();
42
- const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(
52
+ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramByteLimitFromEnv(
43
53
  process.env,
44
54
  ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"],
45
55
  TELEGRAM_FILE_MAX_BYTES,
@@ -301,8 +311,7 @@ export type TelegramInputRichBlock =
301
311
  };
302
312
 
303
313
  export type TelegramInputRichDraftBlock =
304
- | TelegramInputRichBlock
305
- | { type: "thinking"; text: TelegramRichText };
314
+ TelegramInputRichBlock | { type: "thinking"; text: TelegramRichText };
306
315
 
307
316
  export type TelegramInputRichMessage = TelegramInputRichMessageCommon &
308
317
  (
@@ -382,6 +391,8 @@ export interface TelegramApiRetryWait {
382
391
  }
383
392
 
384
393
  export interface TelegramApiCallOptions {
394
+ /** Local per-call effect authority; independent of source completion and transport ownership. */
395
+ assertAuthority?: () => void;
385
396
  signal?: AbortSignal;
386
397
  maxAttempts?: number;
387
398
  retryRateLimit?: boolean;
@@ -392,6 +403,27 @@ export interface TelegramApiCallOptions {
392
403
  onRetryWait?: (wait: TelegramApiRetryWait) => void;
393
404
  }
394
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
+
395
427
  interface TelegramGetFileResult {
396
428
  file_path: string;
397
429
  file_size?: number;
@@ -446,7 +478,10 @@ export interface TelegramEditGuestInlineMessageContent {
446
478
  parseMode?: "HTML";
447
479
  }
448
480
 
449
- export interface TelegramAnswerCallbackQueryOptions {
481
+ export interface TelegramAnswerCallbackQueryOptions extends Pick<
482
+ TelegramApiCallOptions,
483
+ "assertAuthority"
484
+ > {
450
485
  recordRuntimeEvent?: (
451
486
  kind: "api",
452
487
  error: unknown,
@@ -477,6 +512,7 @@ export interface TelegramApiClient {
477
512
  answerCallbackQuery: (
478
513
  callbackQueryId: string,
479
514
  text?: string,
515
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
480
516
  ) => Promise<void>;
481
517
  answerGuestQuery?: (
482
518
  guestQueryId: string,
@@ -486,26 +522,32 @@ export interface TelegramApiClient {
486
522
  }
487
523
 
488
524
  export interface TelegramApiTargetActivityRuntime {
489
- begin: (
490
- method: string,
491
- body: Record<string, unknown>,
492
- ) => () => void;
525
+ begin: (method: string, body: Record<string, unknown>) => () => void;
493
526
  hasPendingTarget: (target: { chatId: number; threadId?: number }) => boolean;
494
527
  listPendingTargets: () => { chatId: number; threadId: number }[];
495
528
  listPendingChats: () => number[];
496
529
  }
497
530
 
498
531
  function parseTelegramApiTargetInteger(value: unknown): number | undefined {
499
- const parsed = typeof value === "number" ? value :
500
- typeof value === "string" && /^-?\d+$/u.test(value) ? Number(value) : undefined;
501
- 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;
502
541
  }
503
542
 
504
543
  export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActivityRuntime {
505
- const pending = new Map<string, {
506
- target: { chatId: number; threadId: number };
507
- count: number;
508
- }>();
544
+ const pending = new Map<
545
+ string,
546
+ {
547
+ target: { chatId: number; threadId: number };
548
+ count: number;
549
+ }
550
+ >();
509
551
  const pendingChats = new Map<number, number>();
510
552
  const messageScopedMethods = new Set([
511
553
  "deleteMessage",
@@ -518,8 +560,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
518
560
  const chatId = parseTelegramApiTargetInteger(body.chat_id);
519
561
  const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
520
562
  const messageId = parseTelegramApiTargetInteger(body.message_id);
521
- const chatScoped = chatId !== undefined && threadId === undefined &&
522
- messageId !== undefined && messageScopedMethods.has(method);
563
+ const chatScoped =
564
+ chatId !== undefined &&
565
+ threadId === undefined &&
566
+ messageId !== undefined &&
567
+ messageScopedMethods.has(method);
523
568
  if (chatId === undefined || (threadId === undefined && !chatScoped)) {
524
569
  return () => undefined;
525
570
  }
@@ -527,7 +572,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
527
572
  if (key) {
528
573
  const existing = pending.get(key);
529
574
  if (existing) existing.count += 1;
530
- 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
+ });
531
580
  } else {
532
581
  pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
533
582
  }
@@ -547,8 +596,11 @@ export function createTelegramApiTargetActivityRuntime(): TelegramApiTargetActiv
547
596
  };
548
597
  },
549
598
  hasPendingTarget(target) {
550
- return pendingChats.has(target.chatId) ||
551
- (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
+ );
552
604
  },
553
605
  listPendingTargets() {
554
606
  return Array.from(pending.values(), ({ target }) => ({ ...target }));
@@ -576,15 +628,19 @@ export function createTelegramApiTargetTrackingClient(
576
628
  }
577
629
  };
578
630
  return {
579
- call: (method, body, options) => track(
580
- method,
581
- body,
582
- () => client.call(method, body, options),
583
- ),
631
+ call: (method, body, options) =>
632
+ track(method, body, () => client.call(method, body, options)),
584
633
  callMultipart: (method, fields, fileField, filePath, fileName, options) =>
585
- track(method, fields, () => client.callMultipart(
586
- method, fields, fileField, filePath, fileName, options,
587
- )),
634
+ track(method, fields, () =>
635
+ client.callMultipart(
636
+ method,
637
+ fields,
638
+ fileField,
639
+ filePath,
640
+ fileName,
641
+ options,
642
+ ),
643
+ ),
588
644
  downloadFile: client.downloadFile,
589
645
  answerCallbackQuery: client.answerCallbackQuery,
590
646
  ...(client.answerGuestQuery
@@ -619,13 +675,19 @@ export interface TelegramApiWorkspaceAdmissionPort {
619
675
  resumed: boolean;
620
676
  }
621
677
  | { kind: "blocked"; reason: "retirement-fenced" };
622
- releaseAdmission: (expected: TelegramApiWorkspaceAdmissionLeaseLike) => boolean;
678
+ releaseAdmission: (
679
+ expected: TelegramApiWorkspaceAdmissionLeaseLike,
680
+ ) => boolean;
623
681
  }
624
682
 
625
683
  export class TelegramApiWorkspaceAdmissionError extends Error {
626
- readonly code: "blocked" | "unavailable" | "release-lost" | "duplicate-operation";
684
+ readonly code:
685
+ "blocked" | "unavailable" | "release-lost" | "duplicate-operation";
627
686
 
628
- constructor(code: TelegramApiWorkspaceAdmissionError["code"], message: string) {
687
+ constructor(
688
+ code: TelegramApiWorkspaceAdmissionError["code"],
689
+ message: string,
690
+ ) {
629
691
  super(message);
630
692
  this.name = "TelegramApiWorkspaceAdmissionError";
631
693
  this.code = code;
@@ -743,8 +805,9 @@ export function createTelegramApiWorkspaceAdmissionClient(
743
805
  }
744
806
 
745
807
  export interface TelegramBridgeApiRuntimeDeps {
746
- captureRequestErrorHandler?: (body: Record<string, unknown>) =>
747
- ((error: unknown) => Promise<void>) | undefined;
808
+ captureRequestErrorHandler?: (
809
+ body: Record<string, unknown>,
810
+ ) => ((error: unknown) => Promise<void>) | undefined;
748
811
  client: TelegramApiClient;
749
812
  tempDir: string;
750
813
  maxFileSizeBytes: number;
@@ -761,20 +824,10 @@ export interface TelegramBridgeApiRuntimeDeps {
761
824
  chatActionMaxGates?: number;
762
825
  }
763
826
 
764
- export interface TelegramBridgeApiRuntime {
765
- call: <TResponse>(
766
- method: string,
767
- body: Record<string, unknown>,
768
- options?: TelegramApiCallOptions,
769
- ) => Promise<TResponse>;
770
- callMultipart: <TResponse>(
771
- method: string,
772
- fields: Record<string, string>,
773
- fileField: string,
774
- filePath: string,
775
- fileName: string,
776
- options?: TelegramApiCallOptions,
777
- ) => Promise<TResponse>;
827
+ export interface TelegramBridgeApiRuntime extends Pick<
828
+ TelegramApiClient,
829
+ "call" | "callMultipart"
830
+ > {
778
831
  downloadFile: (
779
832
  fileId: string,
780
833
  suggestedName: string,
@@ -787,6 +840,7 @@ export interface TelegramBridgeApiRuntime {
787
840
  ) => Promise<TelegramUpdate[]>;
788
841
  setMyCommands: (
789
842
  commands: readonly { command: string; description: string }[],
843
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
790
844
  ) => Promise<boolean>;
791
845
  sendChatAction: (
792
846
  chatId: number,
@@ -811,15 +865,20 @@ export interface TelegramBridgeApiRuntime {
811
865
  message_thread_id?: number;
812
866
  },
813
867
  ) => Promise<boolean>;
814
- sendMessage: (body: TelegramSendMessageBody) => Promise<TelegramSentMessage>;
868
+ sendMessage: (
869
+ body: TelegramSendMessageBody,
870
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
871
+ ) => Promise<TelegramSentMessage>;
815
872
  sendRichMessage: (
816
873
  body: TelegramSendRichMessageBody,
874
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
817
875
  ) => Promise<TelegramSentMessage>;
818
876
  sendRichMessageDraft: (
819
877
  body: TelegramSendRichMessageDraftBody,
820
878
  ) => Promise<boolean>;
821
879
  editMessageText: (
822
880
  body: TelegramEditMessageTextBody,
881
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
823
882
  ) => Promise<"edited" | "unchanged">;
824
883
  editMessageReplyMarkup: (
825
884
  chatId: number,
@@ -829,6 +888,7 @@ export interface TelegramBridgeApiRuntime {
829
888
  answerCallbackQuery: (
830
889
  callbackQueryId: string,
831
890
  text?: string,
891
+ options?: Pick<TelegramApiCallOptions, "assertAuthority">,
832
892
  ) => Promise<void>;
833
893
  answerGuestQuery: (
834
894
  guestQueryId: string,
@@ -869,15 +929,31 @@ export function createTelegramAttachmentFileName(
869
929
  botScope?: string,
870
930
  ): string {
871
931
  const chat = source.chat;
872
- const clean = (value: string): string => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
873
- const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
874
- ? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
875
- : 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));
876
945
  const dot = generatedName.lastIndexOf(".");
877
946
  const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
878
- const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
879
- return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
880
- (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
+ );
881
957
  }
882
958
 
883
959
  class TelegramApiMalformedSuccessError extends Error {
@@ -960,14 +1036,23 @@ export function getTelegramApiErrorRequestTarget(
960
1036
  }
961
1037
 
962
1038
  export function isTelegramStaleTargetHttpError(error: unknown): boolean {
963
- if (!(error instanceof TelegramApiHttpError) || error.status !== 400) return false;
964
- 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
+ );
965
1044
  }
966
1045
 
967
1046
  /** Only a parsed Telegram rejection of this method proves a request had no effect. */
968
- export function isTelegramApiRequestRejected(error: unknown, method: string): boolean {
969
- return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
970
- 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
+ );
971
1056
  }
972
1057
 
973
1058
  export function isTelegramMessageNotModifiedError(error: unknown): boolean {
@@ -1009,14 +1094,20 @@ export function isRetryableTelegramApiError(error: unknown): boolean {
1009
1094
  }
1010
1095
 
1011
1096
  export function getTelegramApiRetryAfterMs(error: unknown): number | undefined {
1012
- return error instanceof TelegramApiHttpError && error.retryAfterSeconds !== undefined
1097
+ return error instanceof TelegramApiHttpError &&
1098
+ error.retryAfterSeconds !== undefined
1013
1099
  ? Math.max(0, error.retryAfterSeconds * 1000)
1014
1100
  : undefined;
1015
1101
  }
1016
1102
 
1017
1103
  export function isTelegramMessageUnavailableError(error: unknown): boolean {
1018
- return error instanceof TelegramApiHttpError && error.status === 400 &&
1019
- /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
+ );
1020
1111
  }
1021
1112
 
1022
1113
  function getTelegramRetryDelayMs(
@@ -1041,10 +1132,7 @@ function throwIfTelegramApiCallAborted(signal: AbortSignal | undefined): void {
1041
1132
  if (signal?.aborted) throw getTelegramApiAbortReason(signal);
1042
1133
  }
1043
1134
 
1044
- function sleepTelegramRetry(
1045
- ms: number,
1046
- signal?: AbortSignal,
1047
- ): Promise<void> {
1135
+ function sleepTelegramRetry(ms: number, signal?: AbortSignal): Promise<void> {
1048
1136
  if (signal?.aborted) return Promise.reject(getTelegramApiAbortReason(signal));
1049
1137
  return new Promise((resolve, reject) => {
1050
1138
  const timeout = setTimeout(() => {
@@ -1106,6 +1194,30 @@ async function writeTelegramDownloadResponse(
1106
1194
  );
1107
1195
  }
1108
1196
 
1197
+ async function publishTelegramDownload(
1198
+ partPath: string,
1199
+ targetPath: string,
1200
+ signal?: AbortSignal,
1201
+ ): Promise<void> {
1202
+ for (let attempt = 0; ; attempt++) {
1203
+ throwIfTelegramApiCallAborted(signal);
1204
+ try {
1205
+ await rename(partPath, targetPath);
1206
+ return;
1207
+ } catch (error) {
1208
+ const code = (error as NodeJS.ErrnoException | undefined)?.code;
1209
+ // Windows may briefly deny replacement while another publisher/scanner holds the file.
1210
+ if (
1211
+ process.platform !== "win32" ||
1212
+ (code !== "EPERM" && code !== "EACCES") ||
1213
+ attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS
1214
+ )
1215
+ throw error;
1216
+ await sleepTelegramRetry(50 * 2 ** attempt, signal);
1217
+ }
1218
+ }
1219
+ }
1220
+
1109
1221
  async function removeTelegramPartialDownload(path: string): Promise<void> {
1110
1222
  try {
1111
1223
  await unlink(path);
@@ -1142,8 +1254,11 @@ async function parseTelegramApiResponse<TResponse>(
1142
1254
  `Telegram API ${method} failed: ${status}${description}`,
1143
1255
  response.status,
1144
1256
  Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
1145
- data?.ok === false && data.error_code === response.status &&
1146
- [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,
1147
1262
  );
1148
1263
  }
1149
1264
  if (!data) {
@@ -1431,9 +1546,18 @@ function withTelegramTransportDiagnostics(
1431
1546
 
1432
1547
  async function callTelegramWithRetry<TResponse>(
1433
1548
  method: string,
1434
- request: (family?: TelegramNetworkFamily) => Promise<Response>,
1549
+ request: (
1550
+ family: TelegramNetworkFamily | undefined,
1551
+ beforeRequest: () => void,
1552
+ ) => Promise<Response>,
1435
1553
  options: TelegramApiCallOptions | undefined,
1436
1554
  ): Promise<TResponse> {
1555
+ const assertAuthority = options?.assertAuthority;
1556
+ let requestIssued = false;
1557
+ const beforeRequest = () => {
1558
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1559
+ requestIssued = true;
1560
+ };
1437
1561
  const retrySafe =
1438
1562
  options?.retrySafety === "safe" ||
1439
1563
  (options?.retrySafety !== "non-idempotent" &&
@@ -1465,15 +1589,24 @@ async function callTelegramWithRetry<TResponse>(
1465
1589
  };
1466
1590
  for (let attempt = 0; ; attempt += 1) {
1467
1591
  throwIfTelegramApiCallAborted(options?.signal);
1592
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1468
1593
  try {
1469
- return unwrapTelegramApiResult(
1594
+ const result = unwrapTelegramApiResult(
1470
1595
  method,
1471
1596
  await parseTelegramApiResponse<TResponse>(
1472
- await callTelegramTransportRequest(request, retrySafe),
1597
+ await callTelegramTransportRequest(
1598
+ (family) => request(family, beforeRequest),
1599
+ retrySafe,
1600
+ ),
1473
1601
  method,
1474
1602
  ),
1475
1603
  );
1604
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
1605
+ return result;
1476
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);
1477
1610
  const retryable =
1478
1611
  isRetryableTelegramApiError(error) &&
1479
1612
  !(
@@ -1557,17 +1690,20 @@ export async function callTelegram<TResponse>(
1557
1690
  try {
1558
1691
  return await callTelegramWithRetry(
1559
1692
  method,
1560
- async (family) =>
1561
- telegramFetch(
1693
+ async (family, beforeRequest) => {
1694
+ const encodedBody = JSON.stringify(body);
1695
+ beforeRequest();
1696
+ return telegramFetch(
1562
1697
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1563
1698
  {
1564
1699
  method: "POST",
1565
1700
  headers: { "content-type": "application/json" },
1566
- body: JSON.stringify(body),
1701
+ body: encodedBody,
1567
1702
  signal: options?.signal,
1568
1703
  },
1569
1704
  family,
1570
- ),
1705
+ );
1706
+ },
1571
1707
  options,
1572
1708
  );
1573
1709
  } catch (error) {
@@ -1608,11 +1744,13 @@ export async function callTelegramMultipart<TResponse>(
1608
1744
  options?: TelegramApiCallOptions,
1609
1745
  ): Promise<TResponse> {
1610
1746
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
1747
+ options = options && { ...options };
1748
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
1611
1749
  const fileBlob = await openAsBlob(filePath);
1612
1750
  try {
1613
1751
  return await callTelegramWithRetry(
1614
1752
  method,
1615
- async (family) => {
1753
+ async (family, beforeRequest) => {
1616
1754
  if (family) {
1617
1755
  const multipart = await buildTelegramMultipartBody(
1618
1756
  fields,
@@ -1620,6 +1758,7 @@ export async function callTelegramMultipart<TResponse>(
1620
1758
  fileBlob,
1621
1759
  fileName,
1622
1760
  );
1761
+ beforeRequest();
1623
1762
  return telegramFetch(
1624
1763
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1625
1764
  {
@@ -1636,6 +1775,7 @@ export async function callTelegramMultipart<TResponse>(
1636
1775
  form.set(key, value);
1637
1776
  }
1638
1777
  form.set(fileField, fileBlob, fileName);
1778
+ beforeRequest();
1639
1779
  return telegramFetch(
1640
1780
  `${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`,
1641
1781
  {
@@ -1693,7 +1833,7 @@ export async function downloadTelegramFile(
1693
1833
  partPath,
1694
1834
  options?.maxFileSizeBytes,
1695
1835
  );
1696
- await rename(partPath, targetPath);
1836
+ await publishTelegramDownload(partPath, targetPath, options?.signal);
1697
1837
  } catch (error) {
1698
1838
  await removeTelegramPartialDownload(partPath);
1699
1839
  throw error;
@@ -1701,11 +1841,6 @@ export async function downloadTelegramFile(
1701
1841
  return targetPath;
1702
1842
  }
1703
1843
 
1704
- /** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
1705
- export function formatTelegramCallbackAnswerText(text: string | undefined): string | undefined {
1706
- return text?.replace(/(?<!\.)\.\s*$/u, "");
1707
- }
1708
-
1709
1844
  export async function answerTelegramCallbackQuery(
1710
1845
  botToken: string | undefined,
1711
1846
  callbackQueryId: string,
@@ -1719,6 +1854,9 @@ export async function answerTelegramCallbackQuery(
1719
1854
  text
1720
1855
  ? { callback_query_id: callbackQueryId, text }
1721
1856
  : { callback_query_id: callbackQueryId },
1857
+ options.assertAuthority
1858
+ ? { assertAuthority: options.assertAuthority }
1859
+ : undefined,
1722
1860
  );
1723
1861
  } catch (error) {
1724
1862
  options.recordRuntimeEvent?.(
@@ -1728,6 +1866,7 @@ export async function answerTelegramCallbackQuery(
1728
1866
  method: "answerCallbackQuery",
1729
1867
  }),
1730
1868
  );
1869
+ if (error instanceof TelegramApiAuthorityError) throw error;
1731
1870
  }
1732
1871
  }
1733
1872
 
@@ -1832,18 +1971,25 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1832
1971
  recordRuntimeEvent: deps.recordRuntimeEvent,
1833
1972
  });
1834
1973
  const admittedClient = deps.workspaceAdmission
1835
- ? createTelegramApiWorkspaceAdmissionClient(client, deps.workspaceAdmission, {
1836
- onReleaseError(error, method) {
1837
- deps.recordRuntimeEvent("api", error, {
1838
- phase: "workspace-admission-release",
1839
- method,
1840
- });
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
+ },
1841
1984
  },
1842
- })
1985
+ )
1843
1986
  : client;
1844
1987
  const runtime = createTelegramBridgeApiRuntime({
1845
1988
  client: deps.targetActivity
1846
- ? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
1989
+ ? createTelegramApiTargetTrackingClient(
1990
+ admittedClient,
1991
+ deps.targetActivity,
1992
+ )
1847
1993
  : admittedClient,
1848
1994
  tempDir: getTelegramApiTempDir(),
1849
1995
  maxFileSizeBytes: TELEGRAM_INBOUND_FILE_MAX_BYTES,
@@ -1857,10 +2003,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
1857
2003
  async deleteWorkspaceThread(authorize) {
1858
2004
  // The exclusive deletion permit replaces ordinary target admission.
1859
2005
  const target = authorize();
1860
- const deleted = await client.call<boolean>("deleteForumTopic", {
1861
- chat_id: target.chatId, message_thread_id: target.threadId,
1862
- }, { maxAttempts: 1, retrySafety: "non-idempotent" });
1863
- 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
+ );
1864
2018
  },
1865
2019
  };
1866
2020
  }
@@ -1875,7 +2029,9 @@ export function createTelegramBridgeApiRuntime(
1875
2029
  try {
1876
2030
  await handler?.(error);
1877
2031
  } catch (recoveryError) {
1878
- deps.recordRuntimeEvent("api", recoveryError, { phase: "stale-target-recovery" });
2032
+ deps.recordRuntimeEvent("api", recoveryError, {
2033
+ phase: "stale-target-recovery",
2034
+ });
1879
2035
  }
1880
2036
  };
1881
2037
  const now = deps.now ?? Date.now;
@@ -1935,7 +2091,8 @@ export function createTelegramBridgeApiRuntime(
1935
2091
  }
1936
2092
  let gate = chatActionGates.get(chatActionKeys.action);
1937
2093
  if (!gate) {
1938
- if (chatActionGates.size >= chatActionMaxGates) return true as TResponse;
2094
+ if (chatActionGates.size >= chatActionMaxGates)
2095
+ return true as TResponse;
1939
2096
  gate = { notBeforeMs: 0 };
1940
2097
  chatActionGates.set(chatActionKeys.action, gate);
1941
2098
  }
@@ -2004,9 +2161,13 @@ export function createTelegramBridgeApiRuntime(
2004
2161
  return await deps.client.call<TResponse>(method, body, options);
2005
2162
  } catch (error) {
2006
2163
  await recoverRequestError(recoverError, error);
2007
- if (method === "deleteMessage" && error instanceof TelegramApiHttpError &&
2008
- error.status === 400 && error.message ===
2009
- "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
+ ) {
2010
2171
  return true as TResponse;
2011
2172
  }
2012
2173
  deps.recordRuntimeEvent(
@@ -2063,7 +2224,11 @@ export function createTelegramBridgeApiRuntime(
2063
2224
  return await deps.client.downloadFile(
2064
2225
  fileId,
2065
2226
  source
2066
- ? createTelegramAttachmentFileName(source, suggestedName, deps.getBotScope?.())
2227
+ ? createTelegramAttachmentFileName(
2228
+ source,
2229
+ suggestedName,
2230
+ deps.getBotScope?.(),
2231
+ )
2067
2232
  : suggestedName,
2068
2233
  deps.tempDir,
2069
2234
  {
@@ -2087,8 +2252,8 @@ export function createTelegramBridgeApiRuntime(
2087
2252
  ),
2088
2253
  getUpdates: (body, signal) =>
2089
2254
  callRecorded<TelegramUpdate[]>("getUpdates", body, { signal }),
2090
- setMyCommands: (commands) =>
2091
- callRecorded<boolean>("setMyCommands", { commands }),
2255
+ setMyCommands: (commands, options) =>
2256
+ callRecorded<boolean>("setMyCommands", { commands }, options),
2092
2257
  sendChatAction: (chatId, action, options) =>
2093
2258
  callRecorded<boolean>("sendChatAction", {
2094
2259
  chat_id: chatId,
@@ -2132,16 +2297,16 @@ export function createTelegramBridgeApiRuntime(
2132
2297
  body.message_thread_id = options.message_thread_id;
2133
2298
  return callRecorded<boolean>("sendMessageDraft", body);
2134
2299
  },
2135
- sendMessage: (body) =>
2136
- callRecorded<TelegramSentMessage>("sendMessage", body),
2137
- sendRichMessage: (body) =>
2138
- callRecorded<TelegramSentMessage>("sendRichMessage", body),
2300
+ sendMessage: (body, options) =>
2301
+ callRecorded<TelegramSentMessage>("sendMessage", body, options),
2302
+ sendRichMessage: (body, options) =>
2303
+ callRecorded<TelegramSentMessage>("sendRichMessage", body, options),
2139
2304
  sendRichMessageDraft: (body) =>
2140
2305
  callRecorded<boolean>("sendRichMessageDraft", body),
2141
- editMessageText: async (body) => {
2306
+ editMessageText: async (body, options) => {
2142
2307
  const recoverError = deps.captureRequestErrorHandler?.(body);
2143
2308
  try {
2144
- await deps.client.call("editMessageText", body);
2309
+ await deps.client.call("editMessageText", body, options);
2145
2310
  return "edited";
2146
2311
  } catch (error) {
2147
2312
  if (isTelegramMessageNotModifiedError(error)) return "unchanged";
@@ -2163,7 +2328,21 @@ export function createTelegramBridgeApiRuntime(
2163
2328
  reply_markup: replyMarkup,
2164
2329
  });
2165
2330
  },
2166
- 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
+ }
2167
2346
  try {
2168
2347
  await deps.client.answerCallbackQuery(callbackQueryId, text);
2169
2348
  } catch (error) {
@@ -2174,6 +2353,7 @@ export function createTelegramBridgeApiRuntime(
2174
2353
  method: "answerCallbackQuery",
2175
2354
  }),
2176
2355
  );
2356
+ if (error instanceof TelegramApiAuthorityError) throw error;
2177
2357
  }
2178
2358
  },
2179
2359
  answerGuestQuery: (
@@ -2239,16 +2419,19 @@ export function createTelegramApiClient(
2239
2419
  const token = getBotToken();
2240
2420
  // Cooldown keys retain only the public bot-id prefix, not the credential.
2241
2421
  const botId = token?.match(/^(\d+):/)?.[1];
2242
- const isDraft = method === "sendMessageDraft" || method === "sendRichMessageDraft";
2243
- const draftKey = isDraft && botId
2244
- ? `${botId}:${String(body.chat_id)}:${String(body.message_thread_id ?? "all")}`
2245
- : 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;
2246
2428
  if (draftKey) {
2247
2429
  const nowMs = now();
2248
2430
  for (const [key, deadline] of draftRetryNotBeforeByTarget) {
2249
2431
  if (nowMs >= deadline) draftRetryNotBeforeByTarget.delete(key);
2250
2432
  }
2251
- if (draftRetryNotBeforeByTarget.has(draftKey)) return false as TResponse;
2433
+ if (draftRetryNotBeforeByTarget.has(draftKey))
2434
+ return false as TResponse;
2252
2435
  }
2253
2436
  try {
2254
2437
  // A draft is a replaceable snapshot, not a body to replay after backoff.
@@ -2279,10 +2462,18 @@ export function createTelegramApiClient(
2279
2462
  });
2280
2463
  } catch (error) {
2281
2464
  if (draftKey && isRetryableTelegramApiError(error)) {
2282
- draftRetryNotBeforeByTarget.set(draftKey, Math.max(
2283
- draftRetryNotBeforeByTarget.get(draftKey) ?? 0,
2284
- now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500),
2285
- ));
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
+ );
2286
2477
  }
2287
2478
  throw error;
2288
2479
  }
@@ -2314,12 +2505,16 @@ export function createTelegramApiClient(
2314
2505
  options,
2315
2506
  );
2316
2507
  },
2317
- answerCallbackQuery: async (callbackQueryId, text) => {
2508
+ answerCallbackQuery: async (callbackQueryId, text, authorityOptions) => {
2509
+ const assertAuthority = authorityOptions?.assertAuthority;
2510
+ const callbackOptions = assertAuthority
2511
+ ? { ...options, assertAuthority }
2512
+ : options;
2318
2513
  await answerTelegramCallbackQuery(
2319
2514
  getBotToken(),
2320
2515
  callbackQueryId,
2321
2516
  text,
2322
- options,
2517
+ callbackOptions,
2323
2518
  );
2324
2519
  },
2325
2520
  };