@llblab/pi-telegram 0.52.2 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
package/lib/outbound.ts CHANGED
@@ -9,17 +9,6 @@ import { mkdir } from "node:fs/promises";
9
9
  import { join } from "node:path";
10
10
 
11
11
  import type { TelegramAssistantSegmentEvent } from "./activity.ts";
12
- import { resolveTelegramAttachmentsDir } from "./paths.ts";
13
- import * as Replies from "./replies.ts";
14
- import type { TelegramPreparedPreviewDelivery } from "./preview.ts";
15
- import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
16
- import type {
17
- TelegramEditMessageTextBody,
18
- TelegramSendMessageBody,
19
- TelegramSendRichMessageBody,
20
- TelegramSentMessage,
21
- } from "./telegram-api.ts";
22
-
23
12
  import {
24
13
  planTelegramButtonReply,
25
14
  type TelegramButtonActionStore,
@@ -32,15 +21,32 @@ import {
32
21
  type TelegramVoiceReplyItem,
33
22
  } from "./outbound-markup.ts";
34
23
  import { createTelegramVoiceReplySender as createTelegramVoiceReplySenderWithPorts } from "./outbound-voice.ts";
24
+ import { resolveTelegramAttachmentsDir } from "./paths.ts";
25
+ import type { TelegramPreparedPreviewDelivery } from "./preview.ts";
26
+ import * as Replies from "./replies.ts";
35
27
  import type { TelegramTarget } from "./target.ts";
28
+ import type {
29
+ TelegramEditMessageTextBody,
30
+ TelegramSendMessageBody,
31
+ TelegramSendRichMessageBody,
32
+ TelegramSentMessage,
33
+ } from "./telegram-api.ts";
34
+ import {
35
+ assertTelegramApiCallAuthority,
36
+ isTelegramApiCommitUnknownError,
37
+ } from "./telegram-api.ts";
36
38
 
37
39
  const OUTBOUND_HANDLER_REGISTRY_KEY = "__piTelegramOutboundHandlers__";
38
40
  const VOICE_EVENT_RECORDER_KEY = "__piTelegramVoiceEventRecorder__";
39
41
 
40
42
  import {
41
43
  buildCommandTemplateInvocation,
42
- expandCommandTemplateConfigs,
43
- substituteCommandTemplateToken,
44
+ createCommandTemplateRecovery,
45
+ getCommandTemplateCompositionSteps,
46
+ shouldRunCommandTemplateConfig,
47
+ getCommandTemplateConfiguredTimeout,
48
+ getCommandTemplateStepTimeout,
49
+ resolveCommandTemplateNumericField,
44
50
  type CommandTemplateObjectConfig,
45
51
  } from "./command-templates.ts";
46
52
  const DEFAULT_VOICE_TIMEOUT_MS = 120_000;
@@ -87,22 +93,13 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
87
93
  timeout?: number | string;
88
94
  }
89
95
 
90
- export {
91
- normalizeMarkdownAfterVoiceExtraction,
92
- planTelegramVoiceReply,
93
- stripTelegramCommentMarkupForDelivery,
94
- stripTelegramCommentMarkupForPreview,
95
- stripTelegramVoiceMarkupForPreview,
96
- type TelegramVoiceReplyItem,
97
- type TelegramVoiceReplyPlan,
98
- } from "./outbound-markup.ts";
99
-
100
96
  export interface TelegramVoiceExecOptions {
101
97
  cwd?: string;
102
98
  timeout?: number;
103
99
  signal?: AbortSignal;
104
100
  stdin?: string;
105
101
  retry?: number;
102
+ recover?: () => Promise<void>;
106
103
  }
107
104
 
108
105
  export interface TelegramVoiceExecResult {
@@ -216,13 +213,19 @@ export interface TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup = unknown> {
216
213
  chatId: number,
217
214
  replyToMessageId: number | undefined,
218
215
  text: string,
219
- options?: { parseMode?: "HTML"; target?: TelegramTarget },
216
+ options?: { parseMode?: "HTML"; target?: TelegramTarget } & Pick<
217
+ Replies.TelegramReplyTargetOptions,
218
+ "assertAuthority"
219
+ >,
220
220
  ) => Promise<number | undefined>;
221
221
  sendMarkdownReply: (
222
222
  chatId: number,
223
223
  replyToMessageId: number | undefined,
224
224
  markdown: string,
225
- options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
225
+ options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget } & Pick<
226
+ Replies.TelegramReplyTargetOptions,
227
+ "assertAuthority"
228
+ >,
226
229
  ) => Promise<number | undefined>;
227
230
  cwd?: string;
228
231
  recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
@@ -251,60 +254,21 @@ export interface TelegramOutboundTextPreviewRuntimeDeps<
251
254
  execCommand: TelegramVoiceReplySenderDeps["execCommand"];
252
255
  getHandlers?: () => TelegramOutboundHandlerConfig[] | undefined;
253
256
  finalizeMarkdownPreview: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"];
254
- preparePreviewDelivery?: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
257
+ preparePreviewDelivery?: (
258
+ isDeliveryActive: () => boolean,
259
+ ) => TelegramPreparedPreviewDelivery<TReplyMarkup>;
255
260
  cwd?: string;
256
261
  recordRuntimeEvent?: TelegramVoiceReplySenderDeps["recordRuntimeEvent"];
257
262
  }
258
263
 
259
264
  // --- Voice Reply Timeout Helpers ---
260
265
 
261
- function resolveOutboundNumericControlField(
262
- value: number | string | undefined,
263
- values: Record<string, unknown>,
264
- label: string,
265
- ): number | undefined {
266
- if (value === undefined) return undefined;
267
- const resolved =
268
- typeof value === "string"
269
- ? substituteCommandTemplateToken(value, values, label)
270
- : value;
271
- if (resolved === "") return undefined;
272
- const numeric = Number(resolved);
273
- if (!Number.isFinite(numeric) || numeric < 0)
274
- throw new Error(`Command template ${label} must be a non-negative number.`);
275
- return numeric;
276
- }
277
-
278
- function getVoiceReplyConfiguredTimeout(
279
- config: TelegramOutboundCommandTemplateConfig | undefined,
280
- ): number | undefined {
281
- const timeout = typeof config === "string" ? undefined : config?.timeout;
282
- return resolveOutboundNumericControlField(timeout, {}, "timeout");
283
- }
284
-
285
266
  function getVoiceReplyTimeout(
286
267
  config: TelegramOutboundCommandTemplateConfig | undefined,
287
268
  ): number {
288
- return getVoiceReplyConfiguredTimeout(config) ?? DEFAULT_VOICE_TIMEOUT_MS;
289
- }
290
-
291
- function getRemainingVoiceReplyTimeout(
292
- timeout: number,
293
- startedAt: number,
294
- ): number {
295
- return Math.max(1, timeout - (Date.now() - startedAt));
296
- }
297
-
298
- function getVoiceReplyCompositionStepTimeout(
299
- handlerTimeout: number,
300
- step: TelegramOutboundCommandTemplateConfig,
301
- startedAt: number,
302
- ): number {
303
- const remaining = getRemainingVoiceReplyTimeout(handlerTimeout, startedAt);
304
- const stepTimeout = getVoiceReplyConfiguredTimeout(step);
305
- return stepTimeout === undefined
306
- ? remaining
307
- : Math.min(stepTimeout, remaining);
269
+ return (
270
+ getCommandTemplateConfiguredTimeout(config) ?? DEFAULT_VOICE_TIMEOUT_MS
271
+ );
308
272
  }
309
273
 
310
274
  function formatVoiceReplyExecutionFailure(
@@ -319,6 +283,12 @@ function formatVoiceReplyExecutionFailure(
319
283
  return parts.join("\n\n");
320
284
  }
321
285
 
286
+ function withCommandTemplateRecovery(
287
+ recover: (() => Promise<void>) | undefined,
288
+ ): Pick<TelegramVoiceExecOptions, "recover"> {
289
+ return recover ? { recover } : {};
290
+ }
291
+
322
292
  async function runVoiceReplyCommand(
323
293
  label: string,
324
294
  config: TelegramOutboundCommandTemplateConfig,
@@ -350,11 +320,18 @@ async function runVoiceReplyCommand(
350
320
  timeout: options.timeout,
351
321
  ...(typeof config === "object" && config.retry !== undefined
352
322
  ? {
353
- retry: resolveOutboundNumericControlField(
323
+ retry: resolveCommandTemplateNumericField(
354
324
  config.retry,
355
325
  {},
356
326
  "retry",
357
327
  ),
328
+ ...withCommandTemplateRecovery(
329
+ createCommandTemplateRecovery(config, values, {
330
+ cwd: options.cwd,
331
+ timeout: options.timeout,
332
+ execCommand: options.execCommand,
333
+ }),
334
+ ),
358
335
  }
359
336
  : {}),
360
337
  ...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
@@ -402,17 +379,6 @@ export function findTelegramOutboundHandlers(
402
379
  );
403
380
  }
404
381
 
405
- function getTelegramVoiceHandlerCompositionSteps(
406
- handler: TelegramOutboundHandlerConfig,
407
- ): TelegramOutboundCommandTemplateConfig[] {
408
- if (Array.isArray(handler.template)) {
409
- return expandCommandTemplateConfigs(
410
- handler,
411
- ) as TelegramOutboundCommandTemplateConfig[];
412
- }
413
- return [];
414
- }
415
-
416
382
  function extractVoiceReplyPath(stdout: string): string {
417
383
  const path = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
418
384
  if (!path) throw new Error("Voice generator did not print an output path");
@@ -465,7 +431,7 @@ async function generateTelegramVoiceReplyFileWithHandler(
465
431
  timeout: number;
466
432
  execCommand: TelegramVoiceReplySenderDeps["execCommand"];
467
433
  },
468
- ): Promise<string> {
434
+ ): Promise<string | undefined> {
469
435
  await mkdir(options.tempDir, { recursive: true });
470
436
  const artifactId = randomUUID();
471
437
  const values = getVoiceReplyTemplateValues(text, {
@@ -474,11 +440,14 @@ async function generateTelegramVoiceReplyFileWithHandler(
474
440
  mp3Path: join(options.tempDir, `${artifactId}-voice.mp3`),
475
441
  oggPath: join(options.tempDir, `${artifactId}-voice.ogg`),
476
442
  });
477
- const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
443
+ if (!shouldRunCommandTemplateConfig(options.handler, values))
444
+ return undefined;
445
+ const steps = getCommandTemplateCompositionSteps(options.handler);
478
446
  if (steps.length > 0) {
479
447
  const startedAt = Date.now();
480
448
  let stdout = text;
481
449
  for (const [index, step] of steps.entries()) {
450
+ if (!shouldRunCommandTemplateConfig(step, values)) continue;
482
451
  try {
483
452
  const result = await runVoiceReplyCommand(
484
453
  `Outbound voice template step ${index + 1}`,
@@ -486,10 +455,10 @@ async function generateTelegramVoiceReplyFileWithHandler(
486
455
  values,
487
456
  {
488
457
  cwd: options.cwd,
489
- timeout: getVoiceReplyCompositionStepTimeout(
458
+ timeout: getCommandTemplateStepTimeout(
490
459
  options.timeout,
491
460
  step,
492
- startedAt,
461
+ Date.now() - startedAt,
493
462
  ),
494
463
  execCommand: options.execCommand,
495
464
  stdin: stdout,
@@ -554,11 +523,13 @@ async function transformTelegramOutboundTextWithHandler(
554
523
  },
555
524
  ): Promise<string> {
556
525
  const values = getOutboundTextTemplateValues(text);
557
- const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
526
+ if (!shouldRunCommandTemplateConfig(options.handler, values)) return text;
527
+ const steps = getCommandTemplateCompositionSteps(options.handler);
558
528
  if (steps.length > 0) {
559
529
  const startedAt = Date.now();
560
530
  let stdout = text;
561
531
  for (const [index, step] of steps.entries()) {
532
+ if (!shouldRunCommandTemplateConfig(step, values)) continue;
562
533
  try {
563
534
  const result = await runVoiceReplyCommand(
564
535
  `Outbound text template step ${index + 1}`,
@@ -566,10 +537,10 @@ async function transformTelegramOutboundTextWithHandler(
566
537
  values,
567
538
  {
568
539
  cwd: options.cwd,
569
- timeout: getVoiceReplyCompositionStepTimeout(
540
+ timeout: getCommandTemplateStepTimeout(
570
541
  getVoiceReplyTimeout(options.handler),
571
542
  step,
572
- startedAt,
543
+ Date.now() - startedAt,
573
544
  ),
574
545
  execCommand: options.execCommand,
575
546
  stdin: stdout,
@@ -658,9 +629,7 @@ async function transformTelegramOutboundReplyMarkup<TReplyMarkup>(
658
629
  return { ...replyMarkup, inline_keyboard: translatedRows } as TReplyMarkup;
659
630
  }
660
631
 
661
- async function transformTelegramOutboundTextReply<
662
- TReplyMarkup = unknown,
663
- >(
632
+ async function transformTelegramOutboundTextReply<TReplyMarkup = unknown>(
664
633
  text: string,
665
634
  options: TelegramOutboundTextTransformOptions<TReplyMarkup>,
666
635
  ): Promise<TelegramOutboundTextTransformResult<TReplyMarkup>> {
@@ -687,37 +656,65 @@ export function createTelegramOutboundTextReplyRuntime<TReplyMarkup = unknown>(
687
656
  TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>,
688
657
  "sendTextReply" | "sendMarkdownReply"
689
658
  > {
659
+ const sendTextReply = deps.sendTextReply,
660
+ sendMarkdownReply = deps.sendMarkdownReply;
690
661
  return {
691
662
  sendTextReply: async (chatId, replyToMessageId, text, options) => {
663
+ const capturedOptions = options && {
664
+ ...options,
665
+ ...(options.target ? { target: { ...options.target } } : {}),
666
+ };
667
+ const assertAuthority = capturedOptions?.assertAuthority;
668
+ assertTelegramApiCallAuthority(assertAuthority, false);
692
669
  const transformed = await transformTelegramOutboundText(text, {
693
670
  handlers: deps.getHandlers?.(),
694
671
  cwd: deps.cwd,
695
672
  execCommand: deps.execCommand,
696
673
  recordRuntimeEvent: deps.recordRuntimeEvent,
697
674
  });
698
- return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
675
+ assertTelegramApiCallAuthority(assertAuthority, false);
676
+ const messageId = await sendTextReply(
677
+ chatId,
678
+ replyToMessageId,
679
+ transformed,
680
+ capturedOptions,
681
+ );
682
+ assertTelegramApiCallAuthority(assertAuthority, true);
683
+ return messageId;
699
684
  },
700
685
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
686
+ const capturedOptions = options && {
687
+ ...options,
688
+ ...(options.target ? { target: { ...options.target } } : {}),
689
+ };
690
+ const assertAuthority = capturedOptions?.assertAuthority;
691
+ assertTelegramApiCallAuthority(assertAuthority, false);
701
692
  const deliveryMarkdown = stripTelegramCommentMarkupForDelivery(markdown);
702
693
  if (!deliveryMarkdown) return undefined;
703
- const transformed = await transformTelegramOutboundTextReply(deliveryMarkdown, {
704
- handlers: deps.getHandlers?.(),
705
- cwd: deps.cwd,
706
- execCommand: deps.execCommand,
707
- recordRuntimeEvent: deps.recordRuntimeEvent,
708
- replyMarkup: options?.replyMarkup,
709
- });
710
- return deps.sendMarkdownReply(
694
+ const transformed = await transformTelegramOutboundTextReply(
695
+ deliveryMarkdown,
696
+ {
697
+ handlers: deps.getHandlers?.(),
698
+ cwd: deps.cwd,
699
+ execCommand: deps.execCommand,
700
+ recordRuntimeEvent: deps.recordRuntimeEvent,
701
+ replyMarkup: capturedOptions?.replyMarkup,
702
+ },
703
+ );
704
+ assertTelegramApiCallAuthority(assertAuthority, false);
705
+ const messageId = await sendMarkdownReply(
711
706
  chatId,
712
707
  replyToMessageId,
713
708
  transformed.text,
714
709
  {
715
- ...options,
710
+ ...capturedOptions,
716
711
  ...(transformed.replyMarkup
717
712
  ? { replyMarkup: transformed.replyMarkup }
718
713
  : {}),
719
714
  },
720
715
  );
716
+ assertTelegramApiCallAuthority(assertAuthority, true);
717
+ return messageId;
721
718
  },
722
719
  };
723
720
  }
@@ -728,16 +725,15 @@ export function createTelegramOutboundTextPreviewRuntime<
728
725
  deps: TelegramOutboundTextPreviewRuntimeDeps<TReplyMarkup>,
729
726
  ): {
730
727
  finalizeMarkdownPreview: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"];
731
- preparePreviewDelivery: (isDeliveryActive: () => boolean) => TelegramPreparedPreviewDelivery<TReplyMarkup> | undefined;
728
+ preparePreviewDelivery: (
729
+ isDeliveryActive: () => boolean,
730
+ ) => TelegramPreparedPreviewDelivery<TReplyMarkup> | undefined;
732
731
  } {
733
- const wrap = (
734
- finalize: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"],
735
- ): TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"] => async (
736
- chatId,
737
- markdown,
738
- replyToMessageId,
739
- options,
740
- ) => {
732
+ const wrap =
733
+ (
734
+ finalize: TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"],
735
+ ): TelegramPreparedPreviewDelivery<TReplyMarkup>["finalizeMarkdownPreview"] =>
736
+ async (chatId, markdown, replyToMessageId, options) => {
741
737
  const transformed = await transformTelegramOutboundTextReply(markdown, {
742
738
  handlers: deps.getHandlers?.(),
743
739
  cwd: deps.cwd,
@@ -745,23 +741,23 @@ export function createTelegramOutboundTextPreviewRuntime<
745
741
  recordRuntimeEvent: deps.recordRuntimeEvent,
746
742
  replyMarkup: options?.replyMarkup,
747
743
  });
748
- return finalize(
749
- chatId,
750
- transformed.text,
751
- replyToMessageId,
752
- {
753
- ...options,
754
- ...(transformed.replyMarkup
755
- ? { replyMarkup: transformed.replyMarkup }
756
- : {}),
757
- },
758
- );
744
+ return finalize(chatId, transformed.text, replyToMessageId, {
745
+ ...options,
746
+ ...(transformed.replyMarkup
747
+ ? { replyMarkup: transformed.replyMarkup }
748
+ : {}),
749
+ });
759
750
  };
760
751
  return {
761
752
  finalizeMarkdownPreview: wrap(deps.finalizeMarkdownPreview),
762
753
  preparePreviewDelivery(isDeliveryActive) {
763
754
  const prepared = deps.preparePreviewDelivery?.(isDeliveryActive);
764
- return prepared ? { ...prepared, finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview) } : undefined;
755
+ return prepared
756
+ ? {
757
+ ...prepared,
758
+ finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview),
759
+ }
760
+ : undefined;
765
761
  },
766
762
  };
767
763
  }
@@ -892,14 +888,21 @@ export function createTelegramOutboundReplyArtifactSender(
892
888
  options?: { replyToPrompt?: boolean; isDeliveryActive?: () => boolean },
893
889
  ): Promise<void> => {
894
890
  const isDeliveryActive = () =>
895
- deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false;
896
- const sendVoiceReply = createTelegramVoiceReplySender({ ...deps, isDeliveryActive });
891
+ deps.isDeliveryActive?.() !== false &&
892
+ options?.isDeliveryActive?.() !== false;
893
+ const sendVoiceReply = createTelegramVoiceReplySender({
894
+ ...deps,
895
+ isDeliveryActive,
896
+ });
897
897
  // Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
898
898
  const voiceReplies = plan.voiceReplies?.length
899
899
  ? plan.voiceReplies
900
900
  : plan.voiceText
901
901
  ? [{ text: plan.voiceText, lang: plan.lang, rate: plan.rate }]
902
902
  : [];
903
+ // A plan without voice content has nothing to deliver; absence of
904
+ // content is not a synthesis failure.
905
+ if (voiceReplies.length === 0) return;
903
906
 
904
907
  let anyDelivered = false;
905
908
 
package/lib/ownership.ts CHANGED
@@ -51,7 +51,11 @@ export interface TelegramMessageOwnershipStore {
51
51
 
52
52
  export type TelegramFollowerOwnershipView = Pick<
53
53
  TelegramBusFollowerView,
54
- "instanceId" | "connectedAtMs" | "profileKey" | "registrationGeneration" | "protocol"
54
+ | "instanceId"
55
+ | "connectedAtMs"
56
+ | "profileKey"
57
+ | "registrationGeneration"
58
+ | "protocol"
55
59
  >;
56
60
 
57
61
  export interface TelegramBusMessageOwnershipRuntime {
@@ -115,8 +119,7 @@ export function createTelegramBusMessageOwnershipRuntime(deps: {
115
119
  const follower = deps
116
120
  .listFollowers()
117
121
  .find(
118
- (candidate) =>
119
- candidate.profileKey === record.recipientBindingKey,
122
+ (candidate) => candidate.profileKey === record.recipientBindingKey,
120
123
  );
121
124
  return follower
122
125
  ? {
@@ -146,17 +149,23 @@ export function createTelegramBusMessageOwnershipRuntime(deps: {
146
149
  store,
147
150
  getForwardOwnership(chatId, messageId) {
148
151
  const record = store.get(chatId, messageId);
149
- if (!record?.ownerGeneration || !record.recipientBindingKey) return record;
150
- const follower = deps.listFollowers().find((candidate) =>
151
- candidate.instanceId === record.instanceId &&
152
- candidate.registrationGeneration === record.ownerGeneration &&
153
- candidate.profileKey === record.recipientBindingKey,
154
- );
152
+ if (!record?.ownerGeneration || !record.recipientBindingKey)
153
+ return record;
154
+ const follower = deps
155
+ .listFollowers()
156
+ .find(
157
+ (candidate) =>
158
+ candidate.instanceId === record.instanceId &&
159
+ candidate.registrationGeneration === record.ownerGeneration &&
160
+ candidate.profileKey === record.recipientBindingKey,
161
+ );
155
162
  // Protocol is live registration authority, not message-cache history.
156
163
  // Keep an incomplete known owner foreign so the forwarder fails closed.
157
164
  return follower?.protocol?.capabilities.includes(
158
165
  TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
159
- ) ? { ...record, protocolIdentity: follower.protocol } : record;
166
+ )
167
+ ? { ...record, protocolIdentity: follower.protocol }
168
+ : record;
160
169
  },
161
170
  recordLocal(input) {
162
171
  return store.record({ ...input, instanceId: deps.instanceId });