@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
@@ -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)
@@ -30,8 +31,9 @@ function getTelegramApiTempDir() {
30
31
  return resolveTelegramAttachmentsDir();
31
32
  }
32
33
  const TELEGRAM_TEMP_FILE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
34
+ const TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS = 6;
33
35
  const activeTelegramApiWorkspaceAdmissionOperationIds = new Set();
34
- 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);
35
37
  const TELEGRAM_NETWORK_FAMILY_ENV = "PI_TELEGRAM_NETWORK_FAMILY";
36
38
  const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
37
39
  "auto",
@@ -39,10 +41,32 @@ const TELEGRAM_NETWORK_FAMILY_VALUES = new Set([
39
41
  "ipv6",
40
42
  "ipv4-fallback",
41
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
+ }
42
61
  function parseTelegramApiTargetInteger(value) {
43
- const parsed = typeof value === "number" ? value :
44
- typeof value === "string" && /^-?\d+$/u.test(value) ? Number(value) : undefined;
45
- 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;
46
70
  }
47
71
  export function createTelegramApiTargetActivityRuntime() {
48
72
  const pending = new Map();
@@ -58,8 +82,10 @@ export function createTelegramApiTargetActivityRuntime() {
58
82
  const chatId = parseTelegramApiTargetInteger(body.chat_id);
59
83
  const threadId = parseTelegramApiTargetInteger(body.message_thread_id);
60
84
  const messageId = parseTelegramApiTargetInteger(body.message_id);
61
- const chatScoped = chatId !== undefined && threadId === undefined &&
62
- messageId !== undefined && messageScopedMethods.has(method);
85
+ const chatScoped = chatId !== undefined &&
86
+ threadId === undefined &&
87
+ messageId !== undefined &&
88
+ messageScopedMethods.has(method);
63
89
  if (chatId === undefined || (threadId === undefined && !chatScoped)) {
64
90
  return () => undefined;
65
91
  }
@@ -69,7 +95,10 @@ export function createTelegramApiTargetActivityRuntime() {
69
95
  if (existing)
70
96
  existing.count += 1;
71
97
  else
72
- pending.set(key, { target: { chatId, threadId: threadId }, count: 1 });
98
+ pending.set(key, {
99
+ target: { chatId, threadId: threadId },
100
+ count: 1,
101
+ });
73
102
  }
74
103
  else {
75
104
  pendingChats.set(chatId, (pendingChats.get(chatId) ?? 0) + 1);
@@ -95,8 +124,9 @@ export function createTelegramApiTargetActivityRuntime() {
95
124
  };
96
125
  },
97
126
  hasPendingTarget(target) {
98
- return pendingChats.has(target.chatId) ||
99
- (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}`)));
100
130
  },
101
131
  listPendingTargets() {
102
132
  return Array.from(pending.values(), ({ target }) => ({ ...target }));
@@ -215,14 +245,27 @@ function sanitizeFileName(name) {
215
245
  export function createTelegramAttachmentFileName(source, generatedName, botScope) {
216
246
  const chat = source.chat;
217
247
  const clean = (value) => sanitizeFileName(value.replace(/^@/u, "")).replace(/^[-.]+/u, "");
218
- const scope = source.scope ? clean(source.scope) : chat?.type === "private" || !chat
219
- ? botScope ? clean(botScope) : chat ? String(Math.abs(chat.id)) : ""
220
- : 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));
221
259
  const dot = generatedName.lastIndexOf(".");
222
260
  const extension = dot > 0 ? sanitizeFileName(generatedName.slice(dot)) : "";
223
- const tail = source.userFileName ? `-${clean(source.userFileName)}` : extension;
224
- return [sanitizeFileName(source.kind), scope, String(source.messageId)].filter(Boolean).join("-") +
225
- (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);
226
269
  }
227
270
  class TelegramApiMalformedSuccessError extends Error {
228
271
  constructor(method, detail) {
@@ -287,8 +330,9 @@ export function isTelegramStaleTargetHttpError(error) {
287
330
  }
288
331
  /** Only a parsed Telegram rejection of this method proves a request had no effect. */
289
332
  export function isTelegramApiRequestRejected(error, method) {
290
- return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
291
- error.rejectedRequestMethod === method;
333
+ return (error instanceof TelegramApiHttpError &&
334
+ error.rejectedRequestMethod !== undefined &&
335
+ error.rejectedRequestMethod === method);
292
336
  }
293
337
  export function isTelegramMessageNotModifiedError(error) {
294
338
  return (error instanceof Error && error.message.includes("message is not modified"));
@@ -321,13 +365,15 @@ export function isRetryableTelegramApiError(error) {
321
365
  (error.status !== undefined && error.status >= 500)));
322
366
  }
323
367
  export function getTelegramApiRetryAfterMs(error) {
324
- return error instanceof TelegramApiHttpError && error.retryAfterSeconds !== undefined
368
+ return error instanceof TelegramApiHttpError &&
369
+ error.retryAfterSeconds !== undefined
325
370
  ? Math.max(0, error.retryAfterSeconds * 1000)
326
371
  : undefined;
327
372
  }
328
373
  export function isTelegramMessageUnavailableError(error) {
329
- return error instanceof TelegramApiHttpError && error.status === 400 &&
330
- /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));
331
377
  }
332
378
  function getTelegramRetryDelayMs(error, attempt, baseDelayMs) {
333
379
  if (error instanceof TelegramApiHttpError &&
@@ -391,6 +437,24 @@ async function writeTelegramDownloadResponse(response, targetPath, maxFileSizeBy
391
437
  }
392
438
  await pipeline(Readable.from(response.body, { objectMode: false }), createTelegramDownloadLimitTransform(maxFileSizeBytes), createWriteStream(targetPath, { mode: 0o600 }));
393
439
  }
440
+ async function publishTelegramDownload(partPath, targetPath, signal) {
441
+ for (let attempt = 0;; attempt++) {
442
+ throwIfTelegramApiCallAborted(signal);
443
+ try {
444
+ await rename(partPath, targetPath);
445
+ return;
446
+ }
447
+ catch (error) {
448
+ const code = error?.code;
449
+ // Windows may briefly deny replacement while another publisher/scanner holds the file.
450
+ if (process.platform !== "win32" ||
451
+ (code !== "EPERM" && code !== "EACCES") ||
452
+ attempt + 1 >= TELEGRAM_DOWNLOAD_RENAME_MAX_ATTEMPTS)
453
+ throw error;
454
+ await sleepTelegramRetry(50 * 2 ** attempt, signal);
455
+ }
456
+ }
457
+ }
394
458
  async function removeTelegramPartialDownload(path) {
395
459
  try {
396
460
  await unlink(path);
@@ -421,8 +485,11 @@ async function parseTelegramApiResponse(response, method) {
421
485
  const retryAfterHeader = response.headers?.get("retry-after");
422
486
  const retryAfterSeconds = data?.parameters?.retry_after ??
423
487
  (retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
424
- throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false && data.error_code === response.status &&
425
- [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);
426
493
  }
427
494
  if (!data) {
428
495
  throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
@@ -621,6 +688,12 @@ function withTelegramTransportDiagnostics(error, details) {
621
688
  return transport ? { ...details, transport } : details;
622
689
  }
623
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
+ };
624
697
  const retrySafe = options?.retrySafety === "safe" ||
625
698
  (options?.retrySafety !== "non-idempotent" &&
626
699
  isTelegramApiMethodRetrySafe(method));
@@ -648,10 +721,17 @@ async function callTelegramWithRetry(method, request, options) {
648
721
  };
649
722
  for (let attempt = 0;; attempt += 1) {
650
723
  throwIfTelegramApiCallAborted(options?.signal);
724
+ assertTelegramApiCallAuthority(assertAuthority, requestIssued);
651
725
  try {
652
- 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;
653
729
  }
654
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);
655
735
  const retryable = isRetryableTelegramApiError(error) &&
656
736
  !(options?.retryRateLimit === false &&
657
737
  error instanceof TelegramApiHttpError &&
@@ -717,12 +797,16 @@ function assertTelegramBotTokenConfigured(botToken) {
717
797
  export async function callTelegram(botToken, method, body, options) {
718
798
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
719
799
  try {
720
- return await callTelegramWithRetry(method, async (family) => telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
721
- method: "POST",
722
- headers: { "content-type": "application/json" },
723
- body: JSON.stringify(body),
724
- signal: options?.signal,
725
- }, 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);
726
810
  }
727
811
  catch (error) {
728
812
  attachTelegramApiRequestTarget(error, body);
@@ -742,11 +826,14 @@ export async function fetchTelegramBotIdentity(botToken, fetchImpl = fetch) {
742
826
  */
743
827
  export async function callTelegramMultipart(botToken, method, fields, fileField, filePath, fileName, options) {
744
828
  const configuredBotToken = assertTelegramBotTokenConfigured(botToken);
829
+ options = options && { ...options };
830
+ assertTelegramApiCallAuthority(options?.assertAuthority, false);
745
831
  const fileBlob = await openAsBlob(filePath);
746
832
  try {
747
- return await callTelegramWithRetry(method, async (family) => {
833
+ return await callTelegramWithRetry(method, async (family, beforeRequest) => {
748
834
  if (family) {
749
835
  const multipart = await buildTelegramMultipartBody(fields, fileField, fileBlob, fileName);
836
+ beforeRequest();
750
837
  return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
751
838
  method: "POST",
752
839
  headers: { "content-type": multipart.contentType },
@@ -759,6 +846,7 @@ export async function callTelegramMultipart(botToken, method, fields, fileField,
759
846
  form.set(key, value);
760
847
  }
761
848
  form.set(fileField, fileBlob, fileName);
849
+ beforeRequest();
762
850
  return telegramFetch(`${TELEGRAM_API_BASE}/bot${configuredBotToken}/${method}`, {
763
851
  method: "POST",
764
852
  body: form,
@@ -787,7 +875,7 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
787
875
  assertTelegramFileSizeWithinLimit(contentLength ? Number.parseInt(contentLength, 10) : undefined, options?.maxFileSizeBytes);
788
876
  try {
789
877
  await writeTelegramDownloadResponse(response, partPath, options?.maxFileSizeBytes);
790
- await rename(partPath, targetPath);
878
+ await publishTelegramDownload(partPath, targetPath, options?.signal);
791
879
  }
792
880
  catch (error) {
793
881
  await removeTelegramPartialDownload(partPath);
@@ -795,20 +883,20 @@ export async function downloadTelegramFile(botToken, fileId, suggestedName, temp
795
883
  }
796
884
  return targetPath;
797
885
  }
798
- /** Tooltip-like callback answers omit a terminal sentence period; ellipses and other punctuation stay literal. */
799
- export function formatTelegramCallbackAnswerText(text) {
800
- return text?.replace(/(?<!\.)\.\s*$/u, "");
801
- }
802
886
  export async function answerTelegramCallbackQuery(botToken, callbackQueryId, text, options = {}) {
803
887
  try {
804
888
  await callTelegram(botToken, "answerCallbackQuery", text
805
889
  ? { callback_query_id: callbackQueryId, text }
806
- : { callback_query_id: callbackQueryId });
890
+ : { callback_query_id: callbackQueryId }, options.assertAuthority
891
+ ? { assertAuthority: options.assertAuthority }
892
+ : undefined);
807
893
  }
808
894
  catch (error) {
809
895
  options.recordRuntimeEvent?.("api", error, withTelegramTransportDiagnostics(error, {
810
896
  method: "answerCallbackQuery",
811
897
  }));
898
+ if (error instanceof TelegramApiAuthorityError)
899
+ throw error;
812
900
  }
813
901
  }
814
902
  export function createTelegramChatActionSender(sendChatAction, action) {
@@ -893,7 +981,8 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
893
981
  // The exclusive deletion permit replaces ordinary target admission.
894
982
  const target = authorize();
895
983
  const deleted = await client.call("deleteForumTopic", {
896
- chat_id: target.chatId, message_thread_id: target.threadId,
984
+ chat_id: target.chatId,
985
+ message_thread_id: target.threadId,
897
986
  }, { maxAttempts: 1, retrySafety: "non-idempotent" });
898
987
  if (deleted !== true)
899
988
  throw new Error("Telegram Workspace Thread deletion was not confirmed.");
@@ -906,7 +995,9 @@ export function createTelegramBridgeApiRuntime(deps) {
906
995
  await handler?.(error);
907
996
  }
908
997
  catch (recoveryError) {
909
- deps.recordRuntimeEvent("api", recoveryError, { phase: "stale-target-recovery" });
998
+ deps.recordRuntimeEvent("api", recoveryError, {
999
+ phase: "stale-target-recovery",
1000
+ });
910
1001
  }
911
1002
  };
912
1003
  const now = deps.now ?? Date.now;
@@ -1011,9 +1102,11 @@ export function createTelegramBridgeApiRuntime(deps) {
1011
1102
  }
1012
1103
  catch (error) {
1013
1104
  await recoverRequestError(recoverError, error);
1014
- if (method === "deleteMessage" && error instanceof TelegramApiHttpError &&
1015
- error.status === 400 && error.message ===
1016
- "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") {
1017
1110
  return true;
1018
1111
  }
1019
1112
  deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, { method }));
@@ -1057,7 +1150,7 @@ export function createTelegramBridgeApiRuntime(deps) {
1057
1150
  },
1058
1151
  deleteWebhook: (signal) => callRecorded("deleteWebhook", { drop_pending_updates: false }, { signal }),
1059
1152
  getUpdates: (body, signal) => callRecorded("getUpdates", body, { signal }),
1060
- setMyCommands: (commands) => callRecorded("setMyCommands", { commands }),
1153
+ setMyCommands: (commands, options) => callRecorded("setMyCommands", { commands }, options),
1061
1154
  sendChatAction: (chatId, action, options) => callRecorded("sendChatAction", {
1062
1155
  chat_id: chatId,
1063
1156
  action,
@@ -1094,13 +1187,13 @@ export function createTelegramBridgeApiRuntime(deps) {
1094
1187
  body.message_thread_id = options.message_thread_id;
1095
1188
  return callRecorded("sendMessageDraft", body);
1096
1189
  },
1097
- sendMessage: (body) => callRecorded("sendMessage", body),
1098
- sendRichMessage: (body) => callRecorded("sendRichMessage", body),
1190
+ sendMessage: (body, options) => callRecorded("sendMessage", body, options),
1191
+ sendRichMessage: (body, options) => callRecorded("sendRichMessage", body, options),
1099
1192
  sendRichMessageDraft: (body) => callRecorded("sendRichMessageDraft", body),
1100
- editMessageText: async (body) => {
1193
+ editMessageText: async (body, options) => {
1101
1194
  const recoverError = deps.captureRequestErrorHandler?.(body);
1102
1195
  try {
1103
- await deps.client.call("editMessageText", body);
1196
+ await deps.client.call("editMessageText", body, options);
1104
1197
  return "edited";
1105
1198
  }
1106
1199
  catch (error) {
@@ -1120,7 +1213,19 @@ export function createTelegramBridgeApiRuntime(deps) {
1120
1213
  reply_markup: replyMarkup,
1121
1214
  });
1122
1215
  },
1123
- 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
+ }
1124
1229
  try {
1125
1230
  await deps.client.answerCallbackQuery(callbackQueryId, text);
1126
1231
  }
@@ -1128,6 +1233,8 @@ export function createTelegramBridgeApiRuntime(deps) {
1128
1233
  deps.recordRuntimeEvent("api", error, withTelegramTransportDiagnostics(error, {
1129
1234
  method: "answerCallbackQuery",
1130
1235
  }));
1236
+ if (error instanceof TelegramApiAuthorityError)
1237
+ throw error;
1131
1238
  }
1132
1239
  },
1133
1240
  answerGuestQuery: (guestQueryId, text, options) => callRecorded("answerGuestQuery", buildTelegramAnswerGuestQueryBody(guestQueryId, text, options)),
@@ -1202,7 +1309,8 @@ export function createTelegramApiClient(getBotToken, options = {}) {
1202
1309
  }
1203
1310
  catch (error) {
1204
1311
  if (draftKey && isRetryableTelegramApiError(error)) {
1205
- 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)));
1206
1314
  }
1207
1315
  throw error;
1208
1316
  }
@@ -1213,8 +1321,12 @@ export function createTelegramApiClient(getBotToken, options = {}) {
1213
1321
  downloadFile: async (fileId, suggestedName, tempDir, options) => {
1214
1322
  return downloadTelegramFile(getBotToken(), fileId, suggestedName, tempDir, options);
1215
1323
  },
1216
- answerCallbackQuery: async (callbackQueryId, text) => {
1217
- 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);
1218
1330
  },
1219
1331
  };
1220
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;