@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
@@ -6,15 +6,15 @@
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { mkdir } from "node:fs/promises";
8
8
  import { join } from "node:path";
9
- import { resolveTelegramAttachmentsDir } from "./paths.js";
10
- import * as Replies from "./replies.js";
11
- import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
12
9
  import { planTelegramButtonReply, } from "./outbound-buttons.js";
13
10
  import { planTelegramVoiceReply, stripTelegramCommentMarkupForDelivery, } from "./outbound-markup.js";
14
11
  import { createTelegramVoiceReplySender as createTelegramVoiceReplySenderWithPorts } from "./outbound-voice.js";
12
+ import { resolveTelegramAttachmentsDir } from "./paths.js";
13
+ import * as Replies from "./replies.js";
14
+ import { assertTelegramApiCallAuthority, isTelegramApiCommitUnknownError, } from "./telegram-api.js";
15
15
  const OUTBOUND_HANDLER_REGISTRY_KEY = "__piTelegramOutboundHandlers__";
16
16
  const VOICE_EVENT_RECORDER_KEY = "__piTelegramVoiceEventRecorder__";
17
- import { buildCommandTemplateInvocation, expandCommandTemplateConfigs, substituteCommandTemplateToken, } from "./command-templates.js";
17
+ import { buildCommandTemplateInvocation, createCommandTemplateRecovery, getCommandTemplateCompositionSteps, shouldRunCommandTemplateConfig, getCommandTemplateConfiguredTimeout, getCommandTemplateStepTimeout, resolveCommandTemplateNumericField, } from "./command-templates.js";
18
18
  const DEFAULT_VOICE_TIMEOUT_MS = 120_000;
19
19
  export function bindTelegramRuntimeEventRecorder(recorder) {
20
20
  globalThis[VOICE_EVENT_RECORDER_KEY] = recorder;
@@ -25,7 +25,6 @@ export function recordTelegramRuntimeEvent(category, error, details) {
25
25
  recorder(category, error, details);
26
26
  }
27
27
  }
28
- export { normalizeMarkdownAfterVoiceExtraction, planTelegramVoiceReply, stripTelegramCommentMarkupForDelivery, stripTelegramCommentMarkupForPreview, stripTelegramVoiceMarkupForPreview, } from "./outbound-markup.js";
29
28
  // --- Programmatic Outbound Handler Registry Runtime ---
30
29
  function getOrCreateOutboundHandlerRegistry() {
31
30
  const existing = globalThis[OUTBOUND_HANDLER_REGISTRY_KEY];
@@ -62,35 +61,8 @@ function getTelegramOutboundProgrammaticHandlers(kind) {
62
61
  return [...(registry.handlers.get(kind) ?? [])];
63
62
  }
64
63
  // --- Voice Reply Timeout Helpers ---
65
- function resolveOutboundNumericControlField(value, values, label) {
66
- if (value === undefined)
67
- return undefined;
68
- const resolved = typeof value === "string"
69
- ? substituteCommandTemplateToken(value, values, label)
70
- : value;
71
- if (resolved === "")
72
- return undefined;
73
- const numeric = Number(resolved);
74
- if (!Number.isFinite(numeric) || numeric < 0)
75
- throw new Error(`Command template ${label} must be a non-negative number.`);
76
- return numeric;
77
- }
78
- function getVoiceReplyConfiguredTimeout(config) {
79
- const timeout = typeof config === "string" ? undefined : config?.timeout;
80
- return resolveOutboundNumericControlField(timeout, {}, "timeout");
81
- }
82
64
  function getVoiceReplyTimeout(config) {
83
- return getVoiceReplyConfiguredTimeout(config) ?? DEFAULT_VOICE_TIMEOUT_MS;
84
- }
85
- function getRemainingVoiceReplyTimeout(timeout, startedAt) {
86
- return Math.max(1, timeout - (Date.now() - startedAt));
87
- }
88
- function getVoiceReplyCompositionStepTimeout(handlerTimeout, step, startedAt) {
89
- const remaining = getRemainingVoiceReplyTimeout(handlerTimeout, startedAt);
90
- const stepTimeout = getVoiceReplyConfiguredTimeout(step);
91
- return stepTimeout === undefined
92
- ? remaining
93
- : Math.min(stepTimeout, remaining);
65
+ return (getCommandTemplateConfiguredTimeout(config) ?? DEFAULT_VOICE_TIMEOUT_MS);
94
66
  }
95
67
  function formatVoiceReplyExecutionFailure(label, result) {
96
68
  const parts = [
@@ -102,6 +74,9 @@ function formatVoiceReplyExecutionFailure(label, result) {
102
74
  parts.push(`stdout:\n${result.stdout.trimEnd()}`);
103
75
  return parts.join("\n\n");
104
76
  }
77
+ function withCommandTemplateRecovery(recover) {
78
+ return recover ? { recover } : {};
79
+ }
105
80
  async function runVoiceReplyCommand(label, config, values, options) {
106
81
  if (!options.execCommand) {
107
82
  throw new Error("execCommand is required for command template execution");
@@ -115,7 +90,12 @@ async function runVoiceReplyCommand(label, config, values, options) {
115
90
  timeout: options.timeout,
116
91
  ...(typeof config === "object" && config.retry !== undefined
117
92
  ? {
118
- retry: resolveOutboundNumericControlField(config.retry, {}, "retry"),
93
+ retry: resolveCommandTemplateNumericField(config.retry, {}, "retry"),
94
+ ...withCommandTemplateRecovery(createCommandTemplateRecovery(config, values, {
95
+ cwd: options.cwd,
96
+ timeout: options.timeout,
97
+ execCommand: options.execCommand,
98
+ })),
119
99
  }
120
100
  : {}),
121
101
  ...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
@@ -150,12 +130,6 @@ export function findTelegramOutboundHandlers(handlers, type) {
150
130
  typeof handler === "object" &&
151
131
  outboundHandlerMatchesType(handler, type));
152
132
  }
153
- function getTelegramVoiceHandlerCompositionSteps(handler) {
154
- if (Array.isArray(handler.template)) {
155
- return expandCommandTemplateConfigs(handler);
156
- }
157
- return [];
158
- }
159
133
  function extractVoiceReplyPath(stdout) {
160
134
  const path = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
161
135
  if (!path)
@@ -194,15 +168,19 @@ async function generateTelegramVoiceReplyFileWithHandler(text, options) {
194
168
  mp3Path: join(options.tempDir, `${artifactId}-voice.mp3`),
195
169
  oggPath: join(options.tempDir, `${artifactId}-voice.ogg`),
196
170
  });
197
- const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
171
+ if (!shouldRunCommandTemplateConfig(options.handler, values))
172
+ return undefined;
173
+ const steps = getCommandTemplateCompositionSteps(options.handler);
198
174
  if (steps.length > 0) {
199
175
  const startedAt = Date.now();
200
176
  let stdout = text;
201
177
  for (const [index, step] of steps.entries()) {
178
+ if (!shouldRunCommandTemplateConfig(step, values))
179
+ continue;
202
180
  try {
203
181
  const result = await runVoiceReplyCommand(`Outbound voice template step ${index + 1}`, step, values, {
204
182
  cwd: options.cwd,
205
- timeout: getVoiceReplyCompositionStepTimeout(options.timeout, step, startedAt),
183
+ timeout: getCommandTemplateStepTimeout(options.timeout, step, Date.now() - startedAt),
206
184
  execCommand: options.execCommand,
207
185
  stdin: stdout,
208
186
  });
@@ -243,15 +221,19 @@ function getOutboundTextTemplateValues(text) {
243
221
  }
244
222
  async function transformTelegramOutboundTextWithHandler(text, options) {
245
223
  const values = getOutboundTextTemplateValues(text);
246
- const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
224
+ if (!shouldRunCommandTemplateConfig(options.handler, values))
225
+ return text;
226
+ const steps = getCommandTemplateCompositionSteps(options.handler);
247
227
  if (steps.length > 0) {
248
228
  const startedAt = Date.now();
249
229
  let stdout = text;
250
230
  for (const [index, step] of steps.entries()) {
231
+ if (!shouldRunCommandTemplateConfig(step, values))
232
+ continue;
251
233
  try {
252
234
  const result = await runVoiceReplyCommand(`Outbound text template step ${index + 1}`, step, values, {
253
235
  cwd: options.cwd,
254
- timeout: getVoiceReplyCompositionStepTimeout(getVoiceReplyTimeout(options.handler), step, startedAt),
236
+ timeout: getCommandTemplateStepTimeout(getVoiceReplyTimeout(options.handler), step, Date.now() - startedAt),
255
237
  execCommand: options.execCommand,
256
238
  stdin: stdout,
257
239
  });
@@ -328,17 +310,33 @@ async function transformTelegramOutboundTextReply(text, options) {
328
310
  return { text: transformedText, ...(replyMarkup ? { replyMarkup } : {}) };
329
311
  }
330
312
  export function createTelegramOutboundTextReplyRuntime(deps) {
313
+ const sendTextReply = deps.sendTextReply, sendMarkdownReply = deps.sendMarkdownReply;
331
314
  return {
332
315
  sendTextReply: async (chatId, replyToMessageId, text, options) => {
316
+ const capturedOptions = options && {
317
+ ...options,
318
+ ...(options.target ? { target: { ...options.target } } : {}),
319
+ };
320
+ const assertAuthority = capturedOptions?.assertAuthority;
321
+ assertTelegramApiCallAuthority(assertAuthority, false);
333
322
  const transformed = await transformTelegramOutboundText(text, {
334
323
  handlers: deps.getHandlers?.(),
335
324
  cwd: deps.cwd,
336
325
  execCommand: deps.execCommand,
337
326
  recordRuntimeEvent: deps.recordRuntimeEvent,
338
327
  });
339
- return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
328
+ assertTelegramApiCallAuthority(assertAuthority, false);
329
+ const messageId = await sendTextReply(chatId, replyToMessageId, transformed, capturedOptions);
330
+ assertTelegramApiCallAuthority(assertAuthority, true);
331
+ return messageId;
340
332
  },
341
333
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
334
+ const capturedOptions = options && {
335
+ ...options,
336
+ ...(options.target ? { target: { ...options.target } } : {}),
337
+ };
338
+ const assertAuthority = capturedOptions?.assertAuthority;
339
+ assertTelegramApiCallAuthority(assertAuthority, false);
342
340
  const deliveryMarkdown = stripTelegramCommentMarkupForDelivery(markdown);
343
341
  if (!deliveryMarkdown)
344
342
  return undefined;
@@ -347,14 +345,17 @@ export function createTelegramOutboundTextReplyRuntime(deps) {
347
345
  cwd: deps.cwd,
348
346
  execCommand: deps.execCommand,
349
347
  recordRuntimeEvent: deps.recordRuntimeEvent,
350
- replyMarkup: options?.replyMarkup,
348
+ replyMarkup: capturedOptions?.replyMarkup,
351
349
  });
352
- return deps.sendMarkdownReply(chatId, replyToMessageId, transformed.text, {
353
- ...options,
350
+ assertTelegramApiCallAuthority(assertAuthority, false);
351
+ const messageId = await sendMarkdownReply(chatId, replyToMessageId, transformed.text, {
352
+ ...capturedOptions,
354
353
  ...(transformed.replyMarkup
355
354
  ? { replyMarkup: transformed.replyMarkup }
356
355
  : {}),
357
356
  });
357
+ assertTelegramApiCallAuthority(assertAuthority, true);
358
+ return messageId;
358
359
  },
359
360
  };
360
361
  }
@@ -378,7 +379,12 @@ export function createTelegramOutboundTextPreviewRuntime(deps) {
378
379
  finalizeMarkdownPreview: wrap(deps.finalizeMarkdownPreview),
379
380
  preparePreviewDelivery(isDeliveryActive) {
380
381
  const prepared = deps.preparePreviewDelivery?.(isDeliveryActive);
381
- return prepared ? { ...prepared, finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview) } : undefined;
382
+ return prepared
383
+ ? {
384
+ ...prepared,
385
+ finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview),
386
+ }
387
+ : undefined;
382
388
  },
383
389
  };
384
390
  }
@@ -431,14 +437,22 @@ export function createTelegramOutboundReplyPlanner(store, getRenderingMode = ()
431
437
  // --- Outbound Reply Artifacts ---
432
438
  export function createTelegramOutboundReplyArtifactSender(deps) {
433
439
  return async (turn, plan, options) => {
434
- const isDeliveryActive = () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false;
435
- const sendVoiceReply = createTelegramVoiceReplySender({ ...deps, isDeliveryActive });
440
+ const isDeliveryActive = () => deps.isDeliveryActive?.() !== false &&
441
+ options?.isDeliveryActive?.() !== false;
442
+ const sendVoiceReply = createTelegramVoiceReplySender({
443
+ ...deps,
444
+ isDeliveryActive,
445
+ });
436
446
  // Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
437
447
  const voiceReplies = plan.voiceReplies?.length
438
448
  ? plan.voiceReplies
439
449
  : plan.voiceText
440
450
  ? [{ text: plan.voiceText, lang: plan.lang, rate: plan.rate }]
441
451
  : [];
452
+ // A plan without voice content has nothing to deliver; absence of
453
+ // content is not a synthesis failure.
454
+ if (voiceReplies.length === 0)
455
+ return;
442
456
  let anyDelivered = false;
443
457
  for (const reply of voiceReplies) {
444
458
  if (!isDeliveryActive())
@@ -52,12 +52,16 @@ export function createTelegramBusMessageOwnershipRuntime(deps) {
52
52
  const record = store.get(chatId, messageId);
53
53
  if (!record?.ownerGeneration || !record.recipientBindingKey)
54
54
  return record;
55
- const follower = deps.listFollowers().find((candidate) => candidate.instanceId === record.instanceId &&
55
+ const follower = deps
56
+ .listFollowers()
57
+ .find((candidate) => candidate.instanceId === record.instanceId &&
56
58
  candidate.registrationGeneration === record.ownerGeneration &&
57
59
  candidate.profileKey === record.recipientBindingKey);
58
60
  // Protocol is live registration authority, not message-cache history.
59
61
  // Keep an incomplete known owner foreign so the forwarder fails closed.
60
- return follower?.protocol?.capabilities.includes(TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) ? { ...record, protocolIdentity: follower.protocol } : record;
62
+ return follower?.protocol?.capabilities.includes(TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION)
63
+ ? { ...record, protocolIdentity: follower.protocol }
64
+ : record;
61
65
  },
62
66
  recordLocal(input) {
63
67
  return store.record({ ...input, instanceId: deps.instanceId });
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Telegram bridge path resolution for Pi-compatible runtimes
3
+ * Zones: telemetry paths, filesystem, runtime identity
4
+ * Owns agent-dir detection and extension-local path derivation
5
+ */
1
6
  export declare const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
2
7
  export interface TelegramAgentDirResolutionInput {
3
8
  env?: Partial<Pick<NodeJS.ProcessEnv, "PI_CODING_AGENT_DIR">>;
package/dist/lib/paths.js CHANGED
@@ -2,16 +2,11 @@
2
2
  * Telegram bridge path resolution for Pi-compatible runtimes
3
3
  * Zones: telemetry paths, filesystem, runtime identity
4
4
  * Owns agent-dir detection and extension-local path derivation
5
- *
6
- * This domain is path-only: it resolves directories and file paths from
7
- * environment and runtime identity. Its only filesystem read canonicalizes the
8
- * existing agent-dir prefix; it does not read config, manage state, or import
9
- * broader Telegram domains.
10
5
  */
11
6
  import { createHash } from "node:crypto";
12
7
  import { realpathSync } from "node:fs";
13
8
  import { homedir } from "node:os";
14
- import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
15
10
  export const TELEGRAM_DEFAULT_PROFILE_NAME = "default";
16
11
  /**
17
12
  * Resolve the agent data directory for the current Pi-compatible runtime.
@@ -41,9 +36,13 @@ export function resolveAgentDir(input = {}) {
41
36
  * Equality proves spelling only, not file identity, consumer closure or migration readiness.
42
37
  */
43
38
  export function requireTelegramStoragePathReference(path, expectedPath) {
44
- if (typeof path !== "string" || typeof expectedPath !== "string" ||
45
- !isAbsolute(path) || resolve(path) !== path ||
46
- !isAbsolute(expectedPath) || resolve(expectedPath) !== expectedPath || path !== expectedPath) {
39
+ if (typeof path !== "string" ||
40
+ typeof expectedPath !== "string" ||
41
+ !isAbsolute(path) ||
42
+ resolve(path) !== path ||
43
+ !isAbsolute(expectedPath) ||
44
+ resolve(expectedPath) !== expectedPath ||
45
+ path !== expectedPath) {
47
46
  throw new Error("Telegram storage reference does not match its approved absolute path.");
48
47
  }
49
48
  return path;
@@ -55,11 +54,12 @@ export function resolveTelegramConfigPath() {
55
54
  /**
56
55
  * Resolve symlinks in the longest existing prefix of `path` and keep the missing suffix. Strict journal reads require
57
56
  * canonical anchors, so a symlinked agent directory (for example macOS `/var` → `/private/var`) must not leak in.
58
- * Relative input is returned unchanged so callers' exact-absolute-path guards still reject it. Windows keeps its
59
- * spelling: strict journal reads are POSIX-only there, and resolving a drive-less root would add a drive letter.
57
+ * Relative input is returned unchanged so callers' exact-absolute-path guards still reject it. On Windows only
58
+ * drive-letter and UNC paths resolve; resolving a drive-less root would add the current drive.
60
59
  */
61
60
  function canonicalizeExistingPrefix(path) {
62
- if (!isAbsolute(path) || process.platform === "win32")
61
+ if (!isAbsolute(path) ||
62
+ (process.platform === "win32" && !/^(?:[a-zA-Z]:[\\/]|\\\\)/u.test(path)))
63
63
  return path;
64
64
  const absolute = resolve(path);
65
65
  const missing = [];
@@ -68,7 +68,8 @@ function canonicalizeExistingPrefix(path) {
68
68
  return join(realpathSync(current), ...missing.reverse());
69
69
  }
70
70
  catch (error) {
71
- if (error.code !== "ENOENT" || dirname(current) === current)
71
+ if (error.code !== "ENOENT" ||
72
+ dirname(current) === current)
72
73
  return absolute;
73
74
  missing.push(basename(current));
74
75
  }
@@ -140,7 +141,10 @@ export function getTelegramJournalPublicationPaths(path, runtimeDir) {
140
141
  requireTelegramStoragePathReference(runtimeDir, join(dirname(dirname(path)), "runtime"));
141
142
  if (basename(dirname(path)) !== "journals")
142
143
  throw new Error("Telegram service journal must use its journals namespace.");
143
- return { transactionPath: join(runtimeDir, `${basename(path)}.transaction`), temporaryBasePath: join(runtimeDir, basename(path)) };
144
+ return {
145
+ transactionPath: join(runtimeDir, `${basename(path)}.transaction`),
146
+ temporaryBasePath: join(runtimeDir, basename(path)),
147
+ };
144
148
  }
145
149
  /** Durable inactive Thread cleanup work-set journal. */
146
150
  export function resolveTelegramThreadCleanupWorkPath(agentDir = resolveAgentDir(), profileName) {
@@ -186,12 +190,17 @@ export function encodeTelegramSessionDirectoryName(sessionId) {
186
190
  let name = "";
187
191
  bytes.forEach((byte, index) => {
188
192
  const char = String.fromCharCode(byte);
189
- const safe = /[a-z0-9_-]/u.test(char) || (char === "." && index > 0 && index < bytes.length - 1);
190
- name += safe ? char : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
193
+ const safe = /[a-z0-9_-]/u.test(char) ||
194
+ (char === "." && index > 0 && index < bytes.length - 1);
195
+ name += safe
196
+ ? char
197
+ : `%${byte.toString(16).toUpperCase().padStart(2, "0")}`;
191
198
  });
192
199
  if (WINDOWS_RESERVED_NAME.test(name))
193
200
  name = `%${name.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}${name.slice(1)}`;
194
- return name.length <= TELEGRAM_SESSION_DIRECTORY_MAX_LENGTH ? name : undefined;
201
+ return name.length <= TELEGRAM_SESSION_DIRECTORY_MAX_LENGTH
202
+ ? name
203
+ : undefined;
195
204
  }
196
205
  /** Inverse of `encodeTelegramSessionDirectoryName`; undefined unless the name is exactly canonical. */
197
206
  export function decodeTelegramSessionDirectoryName(name) {
@@ -210,7 +219,9 @@ export function decodeTelegramSessionDirectoryName(name) {
210
219
  bytes.push(name.charCodeAt(index));
211
220
  }
212
221
  const decoded = Buffer.from(bytes).toString("utf8");
213
- return encodeTelegramSessionDirectoryName(decoded) === name ? decoded : undefined;
222
+ return encodeTelegramSessionDirectoryName(decoded) === name
223
+ ? decoded
224
+ : undefined;
214
225
  }
215
226
  /** Root of every session folder (<agentDir>/tmp/pi-telegram/sessions). */
216
227
  export function resolveTelegramSessionsDir(agentDir = resolveAgentDir()) {
@@ -227,7 +238,10 @@ export function resolveTelegramSessionDir(sessionId, agentDir = resolveAgentDir(
227
238
  export function getTelegramRecipientJournalHash(recipientBindingKey) {
228
239
  if (!recipientBindingKey)
229
240
  throw new Error("Telegram session journal binding key is required.");
230
- return createHash("sha256").update(recipientBindingKey).digest("hex").slice(0, 16);
241
+ return createHash("sha256")
242
+ .update(recipientBindingKey)
243
+ .digest("hex")
244
+ .slice(0, 16);
231
245
  }
232
246
  /** Durable per-session journal (`sessions/<session id>/journal.<hash>[.<profile>].json`); the name never reveals a role. */
233
247
  export function resolveTelegramSessionJournalPath(sessionId, recipientBindingKey, agentDir = resolveAgentDir(), profileName) {
@@ -240,8 +254,10 @@ export function resolveTelegramSessionPollingJournalPath(sessionId, agentDir = r
240
254
  /** Accept only a canonical `sessions/<id>/inbox[.<profile>].json` beneath this agent's runtime. */
241
255
  export function isTelegramSessionPollingJournalPath(path, agentDir = resolveAgentDir()) {
242
256
  const parts = relative(resolveTelegramSessionsDir(agentDir), path).split(sep);
243
- return isAbsolute(path) && parts.length === 2 && decodeTelegramSessionDirectoryName(parts[0]) !== undefined &&
244
- /^inbox(?:\.[a-zA-Z0-9._-]+)?\.json$/u.test(parts[1]);
257
+ return (isAbsolute(path) &&
258
+ parts.length === 2 &&
259
+ decodeTelegramSessionDirectoryName(parts[0]) !== undefined &&
260
+ /^inbox(?:\.[a-zA-Z0-9._-]+)?\.json$/u.test(parts[1]));
245
261
  }
246
262
  /** Runtime event log (<agentDir>/tmp/pi-telegram/logs.jsonl). */
247
263
  export function resolveTelegramRuntimeLogPath(agentDir = resolveAgentDir()) {
package/dist/lib/pi.d.ts CHANGED
@@ -42,6 +42,8 @@ export interface PiSettingsManager {
42
42
  }
43
43
  export type PiSlashCommandInfo = SlashCommandInfo;
44
44
  export type PiRunMode = "tui" | "rpc" | "json" | "print";
45
+ /** Pi rejects a context captured before session replacement with a stale-context error; this is not a domain failure. */
46
+ export declare function isPiStaleContextError(error: unknown): boolean;
45
47
  export declare function getExtensionContextMode(ctx: unknown): PiRunMode | undefined;
46
48
  export declare function isExtensionContextPassiveRunMode(ctx: unknown): boolean;
47
49
  export declare function canStartPollingInExtensionContext(ctx: unknown): boolean;
package/dist/lib/pi.js CHANGED
@@ -7,6 +7,12 @@ import { SettingsManager, } from "@earendil-works/pi-coding-agent";
7
7
  function isPiRunMode(value) {
8
8
  return (value === "tui" || value === "rpc" || value === "json" || value === "print");
9
9
  }
10
+ /** Pi rejects a context captured before session replacement with a stale-context error; this is not a domain failure. */
11
+ export function isPiStaleContextError(error) {
12
+ return (error instanceof Error &&
13
+ (error.message.includes("stale after session") ||
14
+ error.message.includes("stale ctx")));
15
+ }
10
16
  export function getExtensionContextMode(ctx) {
11
17
  const mode = typeof ctx === "object" && ctx !== null
12
18
  ? ctx.mode
@@ -50,7 +56,8 @@ export function createExtensionApiRuntimePorts(api) {
50
56
  function readEnabledModels(value) {
51
57
  if (value === undefined)
52
58
  return undefined;
53
- if (Array.isArray(value) && value.every((entry) => typeof entry === "string")) {
59
+ if (Array.isArray(value) &&
60
+ value.every((entry) => typeof entry === "string")) {
54
61
  return [...value];
55
62
  }
56
63
  throw new TypeError("Host settings enabledModels must be a string array or undefined.");
@@ -225,13 +225,8 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
225
225
  restoreFollowerWithOwner: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
226
226
  stopFollowerRegistration: () => void;
227
227
  }
228
- export interface TelegramThreadAwarePollingDeps<TContext, TOwner> extends TelegramStartupThreadCapabilityProbeDeps {
229
- lifecycle?: TelegramThreadCapabilityLifecycle;
230
- isBusRuntimeEnabled: () => boolean;
231
- isTopicModeUnavailableError: (error: unknown) => boolean;
232
- getPollingStartedWithTelegramBus: () => boolean;
233
- setPollingStartedWithTelegramBus: (started: boolean) => void;
234
- setForceFreshLeaderThreadOnNextStart: (forceFresh: boolean) => void;
228
+ /** Classic/leader polling and follower registration controls that thread-aware polling switches between. */
229
+ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
235
230
  startClassicPolling: (ctx: TContext) => MaybePromise<void>;
236
231
  stopClassicPolling: () => Promise<void>;
237
232
  startBusLeaderPolling: (ctx: TContext) => Promise<void>;
@@ -242,25 +237,21 @@ export interface TelegramThreadAwarePollingDeps<TContext, TOwner> extends Telegr
242
237
  restoreFollowerWithLeader?: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
243
238
  hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
244
239
  stopFollowerRegistration: () => void;
240
+ isBusRuntimeEnabled: () => boolean;
241
+ isTopicModeUnavailableError: (error: unknown) => boolean;
245
242
  }
246
- export interface TelegramThreadCapabilityOrchestrationDeps<TContext, TOwner> extends TelegramThreadCapabilityReaderDeps {
243
+ export interface TelegramThreadAwarePollingDeps<TContext, TOwner> extends TelegramStartupThreadCapabilityProbeDeps, TelegramThreadAwarePollingControls<TContext, TOwner> {
244
+ lifecycle?: TelegramThreadCapabilityLifecycle;
245
+ getPollingStartedWithTelegramBus: () => boolean;
246
+ setPollingStartedWithTelegramBus: (started: boolean) => void;
247
+ setForceFreshLeaderThreadOnNextStart: (forceFresh: boolean) => void;
248
+ }
249
+ export interface TelegramThreadCapabilityOrchestrationDeps<TContext, TOwner> extends TelegramThreadCapabilityReaderDeps, TelegramThreadAwarePollingControls<TContext, TOwner> {
247
250
  state: TelegramThreadCapabilityStateRuntime;
248
251
  topicTargetStore: TelegramThreadCapabilityStore;
249
- isBusRuntimeEnabled: () => boolean;
250
252
  ownsLock: (ctx: TContext) => boolean;
251
253
  isFollowerRegistered?: () => boolean;
252
- startClassicPolling: (ctx: TContext) => MaybePromise<void>;
253
- stopClassicPolling: () => Promise<void>;
254
- startBusLeaderPolling: (ctx: TContext) => Promise<void>;
255
- stopBusLeaderPolling: () => Promise<void>;
256
- startLeaderHealth: () => void;
257
- stopLeaderHealth: () => void;
258
- registerFollowerWithLeader: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
259
- restoreFollowerWithLeader?: (ctx: TContext, owner: TOwner) => Promise<boolean | undefined>;
260
- hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
261
254
  suspendLiveThreadTarget?: () => void;
262
- stopFollowerRegistration: () => void;
263
- isTopicModeUnavailableError: (error: unknown) => boolean;
264
255
  updateStatus: (ctx: TContext) => void;
265
256
  recordEvent: (category: string, message: unknown, details?: Record<string, unknown>) => void;
266
257
  }
@@ -308,10 +299,8 @@ export interface TelegramPollingBatchAdmissionDeps<TUpdate extends TelegramUpdat
308
299
  onPhaseChange?: (phase: TelegramPollingWorkPhase, currentUpdateId?: number) => void;
309
300
  }
310
301
  export declare function admitTelegramPollingUpdateBatch<TUpdate extends TelegramUpdate>(deps: TelegramPollingBatchAdmissionDeps<TUpdate>): Promise<TelegramPollingBatchAdmissionResult>;
311
- export interface TelegramPollLoopDeps<TUpdate extends TelegramUpdate, TContext = unknown> extends TelegramRuntimeEventRecorderPort {
312
- ctx: TContext;
313
- signal: AbortSignal;
314
- config: TelegramPollingConfig;
302
+ /** Bot API, journal and progress ports shared by one poll loop and its restartable runner. */
303
+ export interface TelegramPollLoopTransportPorts<TUpdate extends TelegramUpdate> extends TelegramRuntimeEventRecorderPort {
315
304
  deleteWebhook: (signal: AbortSignal) => Promise<unknown>;
316
305
  getUpdates: (body: Record<string, unknown>, signal: AbortSignal) => Promise<TUpdate[]>;
317
306
  getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
@@ -320,26 +309,21 @@ export interface TelegramPollLoopDeps<TUpdate extends TelegramUpdate, TContext =
320
309
  getAcceptedThroughUpdateId?: () => number | undefined;
321
310
  getJournalEntryCount: () => number;
322
311
  signalUpdateWorker: () => void;
312
+ onPhaseChange?: (phase: TelegramPollingWorkPhase, currentUpdateId?: number) => void;
313
+ onSuccessfulResponse?: (updateCount: number) => void;
314
+ }
315
+ export interface TelegramPollLoopDeps<TUpdate extends TelegramUpdate, TContext = unknown> extends TelegramPollLoopTransportPorts<TUpdate> {
316
+ ctx: TContext;
317
+ signal: AbortSignal;
318
+ config: TelegramPollingConfig;
323
319
  onErrorStatus: (message: string) => void;
324
320
  onStatusReset: () => void;
325
321
  sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
326
- onPhaseChange?: (phase: TelegramPollingWorkPhase, currentUpdateId?: number) => void;
327
- onSuccessfulResponse?: (updateCount: number) => void;
328
322
  }
329
- export interface TelegramPollLoopRunnerDeps<TUpdate extends TelegramUpdate, TContext = unknown> extends TelegramRuntimeEventRecorderPort {
323
+ export interface TelegramPollLoopRunnerDeps<TUpdate extends TelegramUpdate, TContext = unknown> extends TelegramPollLoopTransportPorts<TUpdate> {
330
324
  getConfig: () => TelegramPollingConfig;
331
- deleteWebhook: (signal: AbortSignal) => Promise<unknown>;
332
- getUpdates: (body: Record<string, unknown>, signal: AbortSignal) => Promise<TUpdate[]>;
333
- getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
334
- persistConfig: (config: TelegramPollingConfig) => Promise<void>;
335
- appendUpdateBatch: (updates: readonly TUpdate[], acceptedThroughUpdateId?: number) => MaybePromise<unknown>;
336
- getAcceptedThroughUpdateId?: () => number | undefined;
337
- getJournalEntryCount: () => number;
338
- signalUpdateWorker: () => void;
339
325
  updateStatus: (ctx: TContext, message?: string) => void;
340
326
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
341
- onPhaseChange?: (phase: TelegramPollingWorkPhase, currentUpdateId?: number) => void;
342
- onSuccessfulResponse?: (updateCount: number) => void;
343
327
  }
344
328
  export declare function sleepTelegramPollingRetry(ms: number, signal?: AbortSignal): Promise<void>;
345
329
  export declare function createTelegramPollLoopRunner<TUpdate extends TelegramUpdate, TContext = unknown>(deps: TelegramPollLoopRunnerDeps<TUpdate, TContext>): (ctx: TContext, signal: AbortSignal) => Promise<void>;