@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
@@ -7,15 +7,16 @@
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { createWriteStream, openAsBlob } from "node:fs";
10
- import { mkdir, readdir, rename, stat, unlink, writeFile } from "node:fs/promises";
10
+ import { mkdir, readdir, rename, stat, unlink, writeFile, } from "node:fs/promises";
11
11
  import { request as requestHttps } from "node:https";
12
12
  import { join } from "node:path";
13
- import { resolveTelegramAttachmentsDir } from "./paths.js";
14
13
  import { Readable, Transform } from "node:stream";
15
14
  import { pipeline } from "node:stream/promises";
15
+ import { resolveTelegramAttachmentsDir } from "./paths.js";
16
16
  const TELEGRAM_API_BASE = "https://api.telegram.org";
17
17
  export const TELEGRAM_FILE_MAX_BYTES = 50 * 1024 * 1024;
18
- export function getTelegramInboundFileByteLimitFromEnv(env, names, defaultValue = TELEGRAM_FILE_MAX_BYTES) {
18
+ /** First positive safe-integer byte limit among ordered env names, else the caller's default. */
19
+ export function getTelegramByteLimitFromEnv(env, names, defaultValue) {
19
20
  for (const name of names) {
20
21
  const rawValue = env[name]?.trim();
21
22
  if (!rawValue)
@@ -32,7 +33,7 @@ function getTelegramApiTempDir() {
32
33
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
33
34
  const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
34
35
  const activeTelegramApiWorkspaceAdmissionOperationIds = new Set();
35
- const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramInboundFileByteLimitFromEnv(process.env, ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"], TELEGRAM_FILE_MAX_BYTES);
36
+ const TELEGRAM_INBOUND_FILE_MAX_BYTES = getTelegramByteLimitFromEnv(process.env, ["PI_TELEGRAM_INBOUND_FILE_MAX_BYTES", "TELEGRAM_MAX_FILE_SIZE_BYTES"], TELEGRAM_FILE_MAX_BYTES);
36
37
  const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
37
38
  const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
38
39
  "auto",
@@ -40,10 +41,32 @@ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
40
41
  "ipv6",
41
42
  "ipv4-fallback",
42
43
  ]);
44
+ export class TelegramApiAuthorityError extends Error {
45
+ requestIssued;
46
+ constructor(requestIssued) {
47
+ super("Telegram API call authority is unavailable.");
48
+ this.name = "TelegramApiAuthorityError";
49
+ this.requestIssued = requestIssued;
50
+ }
51
+ }
52
+ /** Recheck caller authority, classifying refusal by whether a Bot API request was already issued. */
53
+ export function assertTelegramApiCallAuthority(assertAuthority, requestIssued) {
54
+ try {
55
+ assertAuthority?.();
56
+ }
57
+ catch {
58
+ throw new TelegramApiAuthorityError(requestIssued);
59
+ }
60
+ }
43
61
  function parseTelegramApiTargetInteger(value) {
44
- const parsed = typeof value === "number" ? value :
45
- typeof value === "string" && /^-?\d+$/u.test(value) ? Number(value) : undefined;
46
- return parsed !== undefined && Number.isSafeInteger(parsed) ? parsed : undefined;
62
+ const parsed = typeof value === "number"
63
+ ? value
64
+ : typeof value === "string" && /^-?\d+$/u.test(value)
65
+ ? Number(value)
66
+ : undefined;
67
+ return parsed !== undefined && Number.isSafeInteger(parsed)
68
+ ? parsed
69
+ : undefined;
47
70
  }
48
71
  export function createTelegramApiTargetActivityRuntime() {
49
72
  const pending = new Map();
@@ -59,8 +82,10 @@ export function createTelegramApiTargetActivityRuntime() {
59
82
  const chatId = parseTelegramApiTargetInteger(body.chat_id);
60
83
  const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
61
84
  const messageId = parseTelegramApiTargetInteger(body.message_id);
62
- const chatScoped = chatId !== undefined && threadId === undefined &&
63
- messageId !== undefined && messageScopedMethods.has(method);
85
+ const chatScoped = chatId !== undefined &&
86
+ threadId === undefined &&
87
+ messageId !== undefined &&
88
+ messageScopedMethods.has(method);
64
89
  if (chatId === undefined || (threadId === undefined && !chatScoped)) {
65
90
  return () => undefined;
66
91
  }
@@ -70,7 +95,10 @@ export function createTelegramApiTargetActivityRuntime() {
70
95
  if (existing)
71
96
  existing.count += 1;
72
97
  else
73
- pending.set(key, { target: { chatId, threadId: threadId }, count: 1 });
98
+ pending.set(key, {
99
+ target: { chatId, threadId: threadId },
100
+ count: 1,
101
+ });
74
102
  }
75
103
  else {
76
104
  pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
@@ -96,8 +124,9 @@ export function createTelegramApiTargetActivityRuntime() {
96
124
  };
97
125
  },
98
126
  hasPendingTarget(target) {
99
- return pendingChats.has(target.chatId) ||
100
- (target.threadId !== undefined && pending.has(`${target.chatId}:${target.threadId}`));
127
+ return (pendingChats.has(target.chatId) ||
128
+ (target.threadId !== undefined &&
129
+ pending.has(`${target.chatId}:${target.threadId}`)));
101
130
  },
102
131
  listPendingTargets() {
103
132
  return Array.from(pending.values(), ({ target }) => ({ ...target }));
@@ -216,14 +245,27 @@ function sanitizeFileName(name) {
216
245
  export function createTelegramAttachmentFileName(source, generatedName, botScope) {
217
246
  const chat = source.chat;
218
247
  const clean = (value) => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
219
- const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
220
- ? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
221
- : chat.username ? clean(chat.username) : String(Math.abs(chat.id));
248
+ const scope = source.scope
249
+ ? clean(source.scope)
250
+ : chat?.type === "private" || !chat
251
+ ? botScope
252
+ ? clean(botScope)
253
+ : chat
254
+ ? String(Math.abs(chat.id))
255
+ : ""
256
+ : chat.username
257
+ ? clean(chat.username)
258
+ : String(Math.abs(chat.id));
222
259
  const dot = generatedName.lastIndexOf(".");
223
260
  const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
224
- const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
225
- return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
226
- (source.index !== undefined ? `-${source.index}` : "") + tail;
261
+ const tail = source.userFileName
262
+ ? `-${clean(source.userFileName)}`
263
+ : extension;
264
+ return ([sanitizeFileName(source.kind), scope, String(source.messageId)]
265
+ .filter(Boolean)
266
+ .join("-") +
267
+ (source.index !== undefined ? `-${source.index}` : "") +
268
+ tail);
227
269
  }
228
270
  class TelegramApiMalformedSuccessError extends Error {
229
271
  constructor(method, detail) {
@@ -288,8 +330,9 @@ export function isTelegramStaleTargetHttpError(error) {
288
330
  }
289
331
  /** Only a parsed Telegram rejection of this method proves a request had no effect. */
290
332
  export function isTelegramApiRequestRejected(error, method) {
291
- return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
292
- error.rejectedRequestMethod === method;
333
+ return (error instanceof TelegramApiHttpError &&
334
+ error.rejectedRequestMethod !== undefined &&
335
+ error.rejectedRequestMethod === method);
293
336
  }
294
337
  export function isTelegramMessageNotModifiedError(error) {
295
338
  return (error instanceof Error && error.message.includes("message is not modified"));
@@ -322,13 +365,15 @@ export function isRetryableTelegramApiError(error) {
322
365
  (error.status !== undefined && error.status >= 500)));
323
366
  }
324
367
  export function getTelegramApiRetryAfterMs(error) {
325
- return error instanceof TelegramApiHttpError && error.retryAfterSeconds !== undefined
368
+ return error instanceof TelegramApiHttpError &&
369
+ error.retryAfterSeconds !== undefined
326
370
  ? Math.max(0, error.retryAfterSeconds * 1000)
327
371
  : undefined;
328
372
  }
329
373
  export function isTelegramMessageUnavailableError(error) {
330
- return error instanceof TelegramApiHttpError && error.status === 400 &&
331
- /Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(error.message);
374
+ return (error instanceof TelegramApiHttpError &&
375
+ error.status === 400 &&
376
+ /Bad Request: (message to edit not found|message not found|message_id_invalid)/iu.test(error.message));
332
377
  }
333
378
  function getTelegramRetryDelayMs(error, attempt, baseDelayMs) {
334
379
  if (error instanceof TelegramApiHttpError &&
@@ -402,7 +447,9 @@ async function publishTelegramDownload(partPath, targetPath, signal) {
402
447
  catch (error) {
403
448
  const code = error?.code;
404
449
  // Windows may briefly deny replacement while another publisher/scanner holds the file.
405
- if (process.platform !== "win32" || (code !== "EPERM" && code !== "EACCES") || attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS)
450
+ if (process.platform !== "win32" ||
451
+ (code !== "EPERM" && code !== "EACCES") ||
452
+ attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS)
406
453
  throw error;
407
454
  await sleepTelegramRetry(50 * 2 ** attempt, signal);
408
455
  }
@@ -438,8 +485,11 @@ async function parseTelegramApiResponse(response, method) {
438
485
  const retryAfterHeader = response.headers?.get("retry-after");
439
486
  const retryAfterSeconds = data?.parameters?.retry_after ??
440
487
  (retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
441
- throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false && data.error_code === response.status &&
442
- [400, 401, 403, 404, 429].includes(response.status) ? method : undefined);
488
+ throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false &&
489
+ data.error_code === response.status &&
490
+ [400, 401, 403, 404, 429].includes(response.status)
491
+ ? method
492
+ : undefined);
443
493
  }
444
494
  if (!data) {
445
495
  throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
@@ -638,6 +688,12 @@ function withTelegramTransportDiagnostics(error, details) {
638
688
  return transport ? { ...details, transport } : details;
639
689
  }
640
690
  async function callTelegramWithRetry(method, request, options) {
691
+ const assertAuthority = options?.assertAuthority;
692
+ let requestIssued = false;
693
+ const beforeRequest = () => {
694
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
695
+ requestIssued = true;
696
+ };
641
697
  const retrySafe = options?.retrySafety === "safe" ||
642
698
  (options?.retrySafety !== "non-idempotent" &&
643
699
  isTelegramApiMethodRetrySafe(method));
@@ -665,10 +721,17 @@ async function callTelegramWithRetry(method, request, options) {
665
721
  };
666
722
  for (let attempt = 0;; attempt += 1) {
667
723
  throwIfTelegramApiCallAborted(options?.signal);
724
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
668
725
  try {
669
- return unwrapTelegramApiResult(method, await parseTelegramApiResponse(await callTelegramTransportRequest(request, retrySafe), method));
726
+ const result = unwrapTelegramApiResult(method, await parseTelegramApiResponse(await callTelegramTransportRequest((family) => request(family, beforeRequest), retrySafe), method));
727
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
728
+ return result;
670
729
  }
671
730
  catch (error) {
731
+ // Authority refusal is never a transport failure, retry grant or acknowledged success.
732
+ if (error instanceof TelegramApiAuthorityError)
733
+ throw error;
734
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
672
735
  const retryable = isRetryableTelegramApiError(error) &&
673
736
  !(options?.retryRateLimit === false &&
674
737
  error instanceof TelegramApiHttpError &&
@@ -734,12 +797,16 @@ function assertTelegramBotTokenConfigured(botToken) {
734
797
  export async function callTelegram(botToken, method, body, options) {
735
798
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
736
799
  try {
737
- return await callTelegramWithRetry(method, async (family) => telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
738
- method: "POST",
739
- headers: { "content-type": "application/json" },
740
- body: JSON.stringify(body),
741
- signal: options?.signal,
742
- }, family), options);
800
+ return await callTelegramWithRetry(method, async (family, beforeRequest) => {
801
+ const encodedBody = JSON.stringify(body);
802
+ beforeRequest();
803
+ return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
804
+ method: "POST",
805
+ headers: { "content-type": "application/json" },
806
+ body: encodedBody,
807
+ signal: options?.signal,
808
+ }, family);
809
+ }, options);
743
810
  }
744
811
  catch (error) {
745
812
  attachTelegramApiRequestTarget(error, body);
@@ -759,11 +826,14 @@ export async function fetchTelegramBotIdentity(botToken, fetchImpl = fetch) {
759
826
  */
760
827
  export async function callTelegramMultipart(botToken, method, fields, fileField, filePath, fileName, options) {
761
828
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
829
+ options = options && { ...options };
830
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
762
831
  const fileBlob = await openAsBlob(filePath);
763
832
  try {
764
- return await callTelegramWithRetry(method, async (family) => {
833
+ return await callTelegramWithRetry(method, async (family, beforeRequest) => {
765
834
  if (family) {
766
835
  const multipart = await buildTelegramMultipartBody(fields, fileField, fileBlob, fileName);
836
+ beforeRequest();
767
837
  return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
768
838
  method: "POST",
769
839
  headers: { "content-type": multipart.contentType },
@@ -776,6 +846,7 @@ export async function callTelegramMultipart(botToken, method, fields, fileField,
776
846
  form.set(key, value);
777
847
  }
778
848
  form.set(fileField, fileBlob, fileName);
849
+ beforeRequest();
779
850
  return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
780
851
  method: "POST",
781
852
  body: form,
@@ -812,20 +883,20 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
812
883
  }
813
884
  return targetPath;
814
885
  }
815
- /** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
816
- export function formatTelegramCallbackAnswerText(text) {
817
- return text?.replace(/(?<!\.)\.\s*$/u, "");
818
- }
819
886
  export async function answerTelegramCallbackQuery(botToken, callbackQueryId, text, options = {}) {
820
887
  try {
821
888
  await callTelegram(botToken, "answerCallbackQuery", text
822
889
  ? { callback_query_id: callbackQueryId, text }
823
- : { callback_query_id: callbackQueryId });
890
+ : { callback_query_id: callbackQueryId }, options.assertAuthority
891
+ ? { assertAuthority: options.assertAuthority }
892
+ : undefined);
824
893
  }
825
894
  catch (error) {
826
895
  options.recordRuntimeEvent?.("api", error, withTelegramTransportDiagnostics(error, {
827
896
  method: "answerCallbackQuery",
828
897
  }));
898
+ if (error instanceof TelegramApiAuthorityError)
899
+ throw error;
829
900
  }
830
901
  }
831
902
  export function createTelegramChatActionSender(sendChatAction, action) {
@@ -910,7 +981,8 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
910
981
  // The exclusive deletion permit replaces ordinary target admission.
911
982
  const target = authorize();
912
983
  const deleted = await client.call("deleteForumTopic", {
913
- chat_id: target.chatId, message_thread_id: target.threadId,
984
+ chat_id: target.chatId,
985
+ message_thread_id: target.threadId,
914
986
  }, { maxAttempts: 1, retrySafety: "non-idempotent" });
915
987
  if (deleted !== true)
916
988
  throw new Error("Telegram Workspace Thread deletion was not confirmed.");
@@ -923,7 +995,9 @@ export function createTelegramBridgeApiRuntime(deps) {
923
995
  await handler?.(error);
924
996
  }
925
997
  catch (recoveryError) {
926
- deps.recordRuntimeEvent("api", recoveryError, { phase: "stale-target-recovery" });
998
+ deps.recordRuntimeEvent("api", recoveryError, {
999
+ phase: "stale-target-recovery",
1000
+ });
927
1001
  }
928
1002
  };
929
1003
  const now = deps.now ?? Date.now;
@@ -1028,9 +1102,11 @@ export function createTelegramBridgeApiRuntime(deps) {
1028
1102
  }
1029
1103
  catch (error) {
1030
1104
  await recoverRequestError(recoverError, error);
1031
- if (method === "deleteMessage" && error instanceof TelegramApiHttpError &&
1032
- error.status === 400 && error.message ===
1033
- "Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found") {
1105
+ if (method === "deleteMessage" &&
1106
+ error instanceof TelegramApiHttpError &&
1107
+ error.status === 400 &&
1108
+ error.message ===
1109
+ "Telegram API deleteMessage failed: HTTP 400: Bad Request: message to delete not found") {
1034
1110
  return true;
1035
1111
  }
1036
1112
  deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, { method }));
@@ -1074,7 +1150,7 @@ export function createTelegramBridgeApiRuntime(deps) {
1074
1150
  },
1075
1151
  deleteWebhook: (signal) => callRecorded("deleteWebhook", { drop_pending_updates: false }, { signal }),
1076
1152
  getUpdates: (body, signal) => callRecorded("getUpdates", body, { signal }),
1077
- setMyCommands: (commands) => callRecorded("setMyCommands", { commands }),
1153
+ setMyCommands: (commands, options) => callRecorded("setMyCommands", { commands }, options),
1078
1154
  sendChatAction: (chatId, action, options) => callRecorded("sendChatAction", {
1079
1155
  chat_id: chatId,
1080
1156
  action,
@@ -1111,13 +1187,13 @@ export function createTelegramBridgeApiRuntime(deps) {
1111
1187
  body.message_thread_id = options.message_thread_id;
1112
1188
  return callRecorded("sendMessageDraft", body);
1113
1189
  },
1114
- sendMessage: (body) => callRecorded("sendMessage", body),
1115
- sendRichMessage: (body) => callRecorded("sendRichMessage", body),
1190
+ sendMessage: (body, options) => callRecorded("sendMessage", body, options),
1191
+ sendRichMessage: (body, options) => callRecorded("sendRichMessage", body, options),
1116
1192
  sendRichMessageDraft: (body) => callRecorded("sendRichMessageDraft", body),
1117
- editMessageText: async (body) => {
1193
+ editMessageText: async (body, options) => {
1118
1194
  const recoverError = deps.captureRequestErrorHandler?.(body);
1119
1195
  try {
1120
- await deps.client.call("editMessageText", body);
1196
+ await deps.client.call("editMessageText", body, options);
1121
1197
  return "edited";
1122
1198
  }
1123
1199
  catch (error) {
@@ -1137,7 +1213,19 @@ export function createTelegramBridgeApiRuntime(deps) {
1137
1213
  reply_markup: replyMarkup,
1138
1214
  });
1139
1215
  },
1140
- answerCallbackQuery: async (callbackQueryId, text) => {
1216
+ answerCallbackQuery: async (callbackQueryId, text, options) => {
1217
+ if (options?.assertAuthority) {
1218
+ try {
1219
+ await callRecorded("answerCallbackQuery", text
1220
+ ? { callback_query_id: callbackQueryId, text }
1221
+ : { callback_query_id: callbackQueryId }, { assertAuthority: options.assertAuthority });
1222
+ }
1223
+ catch (error) {
1224
+ if (error instanceof TelegramApiAuthorityError)
1225
+ throw error;
1226
+ }
1227
+ return;
1228
+ }
1141
1229
  try {
1142
1230
  await deps.client.answerCallbackQuery(callbackQueryId, text);
1143
1231
  }
@@ -1145,6 +1233,8 @@ export function createTelegramBridgeApiRuntime(deps) {
1145
1233
  deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
1146
1234
  method: "answerCallbackQuery",
1147
1235
  }));
1236
+ if (error instanceof TelegramApiAuthorityError)
1237
+ throw error;
1148
1238
  }
1149
1239
  },
1150
1240
  answerGuestQuery: (guestQueryId, text, options) => callRecorded("answerGuestQuery", buildTelegramAnswerGuestQueryBody(guestQueryId, text, options)),
@@ -1219,7 +1309,8 @@ export function createTelegramApiClient(getBotToken, options = {}) {
1219
1309
  }
1220
1310
  catch (error) {
1221
1311
  if (draftKey && isRetryableTelegramApiError(error)) {
1222
- draftRetryNotBeforeByTarget.set(draftKey, Math.max(draftRetryNotBeforeByTarget.get(draftKey) ?? 0, now() + getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500)));
1312
+ draftRetryNotBeforeByTarget.set(draftKey, Math.max(draftRetryNotBeforeByTarget.get(draftKey) ?? 0, now() +
1313
+ getTelegramRetryDelayMs(error, 0, options?.retryBaseDelayMs ?? 500)));
1223
1314
  }
1224
1315
  throw error;
1225
1316
  }
@@ -1230,8 +1321,12 @@ export function createTelegramApiClient(getBotToken, options = {}) {
1230
1321
  downloadFile: async (fileId, suggestedName, tempDir, options) => {
1231
1322
  return downloadTelegramFile(getBotToken(), fileId, suggestedName, tempDir, options);
1232
1323
  },
1233
- answerCallbackQuery: async (callbackQueryId, text) => {
1234
- await answerTelegramCallbackQuery(getBotToken(), callbackQueryId, text, options);
1324
+ answerCallbackQuery: async (callbackQueryId, text, authorityOptions) => {
1325
+ const assertAuthority = authorityOptions?.assertAuthority;
1326
+ const callbackOptions = assertAuthority
1327
+ ? { ...options, assertAuthority }
1328
+ : options;
1329
+ await answerTelegramCallbackQuery(getBotToken(), callbackQueryId, text, callbackOptions);
1235
1330
  },
1236
1331
  };
1237
1332
  }
@@ -4,7 +4,7 @@
4
4
  * Owns conservative delayed grouping for Telegram text messages that look like automatic long-message splits
5
5
  */
6
6
  import { setTimeout as waitForTimeout } from "node:timers/promises";
7
- import { extractTelegramMessageText, } from "./media.js";
7
+ import { createTelegramPendingGroupLifecycle, extractTelegramMessageText, } from "./media.js";
8
8
  const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
9
9
  const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
10
10
  const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
@@ -155,6 +155,7 @@ export function createTelegramTextGroupController(options = {}) {
155
155
  : (timer) => {
156
156
  timer.abort();
157
157
  });
158
+ const lifecycle = createTelegramPendingGroupLifecycle(groups, clearTimer);
158
159
  return {
159
160
  prepareUpdateBatch(updates) {
160
161
  for (let index = 0; index + 1 < updates.length; index += 1) {
@@ -249,45 +250,9 @@ export function createTelegramTextGroupController(options = {}) {
249
250
  : undefined,
250
251
  });
251
252
  },
252
- async flushMessage(messageId) {
253
- for (const state of groups.values()) {
254
- if (!state.messages.some((message) => message.message_id === messageId)) {
255
- continue;
256
- }
257
- if (state.flushTimer)
258
- clearTimer(state.flushTimer);
259
- state.flushTimer = undefined;
260
- await state.dispatchNow?.();
261
- if (state.messages.some((message) => message.message_id === messageId) &&
262
- !state.dispatching) {
263
- await state.dispatchNow?.();
264
- }
265
- return true;
266
- }
267
- return false;
268
- },
269
- suspend: () => {
270
- for (const state of groups.values()) {
271
- state.suspended = true;
272
- if (state.flushTimer)
273
- clearTimer(state.flushTimer);
274
- state.flushTimer = undefined;
275
- }
276
- },
277
- resume: (context) => {
278
- for (const state of groups.values()) {
279
- state.context = context;
280
- state.suspended = false;
281
- if (!state.dispatching && !state.flushTimer)
282
- state.reschedule?.();
283
- }
284
- },
253
+ ...lifecycle,
285
254
  clear: () => {
286
- for (const state of groups.values()) {
287
- if (state.flushTimer)
288
- clearTimer(state.flushTimer);
289
- }
290
- groups.clear();
255
+ lifecycle.clear();
291
256
  plannedForwardCommentStarts.clear();
292
257
  plannedForwardCommentEnds.clear();
293
258
  },
@@ -64,8 +64,8 @@ export declare function planTelegramInactiveThreadCleanup(input: {
64
64
  threadId: number;
65
65
  }[];
66
66
  }): TelegramThreadCleanupCandidate[];
67
- export declare function captureTelegramInactiveThreadCleanupEvidence<TBinding extends TelegramThreadCleanupBindingSnapshot>(input: {
68
- profileName: string;
67
+ /** Read ports for the bindings and target claims that cleanup evidence is captured from. */
68
+ export interface TelegramInactiveThreadCleanupEvidencePorts<TBinding extends TelegramThreadCleanupBindingSnapshot> {
69
69
  listBindings(): readonly TBinding[];
70
70
  getProtection(binding: TBinding): {
71
71
  liveOwner: TelegramThreadCleanupProtectionState;
@@ -90,33 +90,12 @@ export declare function captureTelegramInactiveThreadCleanupEvidence<TBinding ex
90
90
  threadId: number;
91
91
  };
92
92
  }[];
93
+ }
94
+ export declare function captureTelegramInactiveThreadCleanupEvidence<TBinding extends TelegramThreadCleanupBindingSnapshot>(input: TelegramInactiveThreadCleanupEvidencePorts<TBinding> & {
95
+ profileName: string;
93
96
  }): Parameters<typeof planTelegramInactiveThreadCleanup>[0];
94
- export declare function createTelegramInactiveThreadCleanupReviewRuntime<TBinding extends TelegramThreadCleanupBindingSnapshot>(deps: {
97
+ export declare function createTelegramInactiveThreadCleanupReviewRuntime<TBinding extends TelegramThreadCleanupBindingSnapshot>(deps: TelegramInactiveThreadCleanupEvidencePorts<TBinding> & {
95
98
  getProfileName(): string;
96
- listBindings(): readonly TBinding[];
97
- getProtection(binding: TBinding): {
98
- liveOwner: TelegramThreadCleanupProtectionState;
99
- acceptedWork: TelegramThreadCleanupProtectionState;
100
- deliveryAuthority: TelegramThreadCleanupProtectionState;
101
- };
102
- listReservations(): readonly {
103
- target: {
104
- chatId: number;
105
- threadId: number;
106
- };
107
- }[];
108
- listPendingProvisions(): readonly {
109
- target?: {
110
- chatId: number;
111
- threadId: number;
112
- };
113
- }[];
114
- listPendingCleanups(): readonly {
115
- target: {
116
- chatId: number;
117
- threadId: number;
118
- };
119
- }[];
120
99
  getWorkStore(): TelegramThreadCleanupWorkStore;
121
100
  runWorkspaceOperation<T>(input: {
122
101
  operationId: string;