@llblab/pi-telegram 0.52.2 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -4,9 +4,9 @@
4
4
  * Owns native assistant replies, rendered UI delivery, guest placeholder rotation, reply transport wiring, and plain text replies
5
5
  */
6
6
  import { assertTelegramInlineKeyboardCallbackData } from "./keyboard.js";
7
- import { getTelegramTargetThreadParams, } from "./target.js";
8
- import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
9
7
  import { renderTelegramMessage, } from "./rendering.js";
8
+ import { getTelegramTargetThreadParams, } from "./target.js";
9
+ import { assertTelegramApiCallAuthority, isTelegramApiCommitUnknownError, TelegramApiAuthorityError, } from "./telegram-api.js";
10
10
  export { renderTelegramMessage, };
11
11
  export function renderTelegramMarkdownToHtmlDraft(markdown) {
12
12
  return renderTelegramMessage(markdown, { mode: "markdown" })
@@ -15,21 +15,6 @@ export function renderTelegramMarkdownToHtmlDraft(markdown) {
15
15
  }
16
16
  export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
17
17
  export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
18
- export function createReplyDedupRuntime() {
19
- const replied = new Map();
20
- return {
21
- shouldReply(promptMessageId) {
22
- if (replied.has(promptMessageId))
23
- return false;
24
- replied.set(promptMessageId, true);
25
- return true;
26
- },
27
- reset() {
28
- replied.clear();
29
- },
30
- };
31
- }
32
- // --- Transport-level dedup ---
33
18
  const lastRepliedToMessageIdByTarget = new Map();
34
19
  const replyDedupPreservedOnNextReset = new Map();
35
20
  let replyDedupGeneration = 0;
@@ -80,9 +65,11 @@ export async function withTelegramReplyParameters(chatId, messageId, target, sen
80
65
  return await send(parameters);
81
66
  }
82
67
  catch (error) {
83
- if (parameters && !isTelegramApiCommitUnknownError(error)
84
- && generation === replyDedupGeneration
85
- && lastRepliedToMessageIdByTarget.get(key) === messageId) {
68
+ if (parameters &&
69
+ !isTelegramApiCommitUnknownError(error) &&
70
+ !(error instanceof TelegramApiAuthorityError && error.requestIssued) &&
71
+ generation === replyDedupGeneration &&
72
+ lastRepliedToMessageIdByTarget.get(key) === messageId) {
86
73
  if (previous === undefined)
87
74
  lastRepliedToMessageIdByTarget.delete(key);
88
75
  else
@@ -172,32 +159,49 @@ export function buildTelegramReplyTransport(deps) {
172
159
  };
173
160
  }
174
161
  export async function sendTelegramRenderedChunks(chatId, chunks, deps, options) {
175
- assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
162
+ return sendTelegramChunkSequence(chatId, chunks, options?.replyToMessageId, options, deps.recordOwnership, (chunk, fields, callOptions) => deps.sendMessage({
163
+ chat_id: chatId,
164
+ text: chunk.text,
165
+ parse_mode: chunk.parseMode,
166
+ ...fields,
167
+ }, callOptions));
168
+ }
169
+ /**
170
+ * Ordered chunk delivery: only the first chunk replies, only the last carries markup, and authority is rechecked
171
+ * before issuance and after every response/ownership publication.
172
+ */
173
+ async function sendTelegramChunkSequence(chatId, chunks, replyToMessageId, options, recordOwnership, send) {
174
+ const target = options?.target && { ...options.target }, assertAuthority = options?.assertAuthority, replyMarkup = options?.replyMarkup;
175
+ assertTelegramInlineKeyboardCallbackData(replyMarkup);
176
176
  let lastMessageId;
177
177
  for (const [index, chunk] of chunks.entries()) {
178
- const sent = await withTelegramReplyParameters(chatId, index === 0 ? options?.replyToMessageId : undefined, options?.target, (replyParameters) => deps.sendMessage({
179
- chat_id: chatId,
180
- text: chunk.text,
181
- parse_mode: chunk.parseMode,
182
- reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
183
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
184
- ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
185
- }));
186
- lastMessageId = sent.message_id;
187
- deps.recordOwnership?.({
188
- chatId,
189
- messageId: sent.message_id,
190
- target: options?.target,
178
+ assertTelegramApiCallAuthority(assertAuthority, false);
179
+ const sent = await withTelegramReplyParameters(chatId, index === 0 ? replyToMessageId : undefined, target, async (replyParameters) => {
180
+ const sent = await send(chunk, {
181
+ reply_markup: index === chunks.length - 1 ? replyMarkup : undefined,
182
+ ...(replyParameters ? { reply_parameters: replyParameters } : {}),
183
+ ...(target ? getTelegramTargetThreadParams(target) : {}),
184
+ }, assertAuthority ? { assertAuthority } : undefined);
185
+ assertTelegramApiCallAuthority(assertAuthority, true);
186
+ return sent;
191
187
  });
188
+ assertTelegramApiCallAuthority(assertAuthority, true);
189
+ lastMessageId = sent.message_id;
190
+ recordOwnership?.({ chatId, messageId: sent.message_id, target });
191
+ assertTelegramApiCallAuthority(assertAuthority, true);
192
192
  }
193
193
  return lastMessageId;
194
194
  }
195
195
  export async function editTelegramRenderedMessage(chatId, messageId, chunks, deps, options) {
196
+ options = { ...options, target: options?.target && { ...options.target } };
197
+ const assertAuthority = options.assertAuthority;
198
+ assertTelegramApiCallAuthority(assertAuthority, false);
196
199
  assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
197
200
  if (chunks.length === 0)
198
201
  return messageId;
199
202
  const [firstChunk, ...remainingChunks] = chunks;
200
- deps.recordOwnership?.({ chatId, messageId, target: options?.target });
203
+ if (!assertAuthority)
204
+ deps.recordOwnership?.({ chatId, messageId, target: options.target });
201
205
  await deps.editMessage({
202
206
  chat_id: chatId,
203
207
  message_id: messageId,
@@ -205,22 +209,29 @@ export async function editTelegramRenderedMessage(chatId, messageId, chunks, dep
205
209
  parse_mode: firstChunk.parseMode,
206
210
  reply_markup: remainingChunks.length === 0 ? options?.replyMarkup : undefined,
207
211
  ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
208
- });
212
+ }, assertAuthority ? { assertAuthority } : undefined);
213
+ assertTelegramApiCallAuthority(assertAuthority, true);
214
+ if (assertAuthority)
215
+ deps.recordOwnership?.({ chatId, messageId, target: options.target });
216
+ assertTelegramApiCallAuthority(assertAuthority, true);
209
217
  if (remainingChunks.length > 0) {
210
218
  return sendTelegramRenderedChunks(chatId, remainingChunks, deps, {
211
219
  replyMarkup: options?.replyMarkup,
212
220
  target: options?.target,
221
+ assertAuthority,
213
222
  });
214
223
  }
215
224
  return messageId;
216
225
  }
217
226
  export async function sendTelegramPlainReply(text, deps, options) {
227
+ options = { ...options, target: options?.target && { ...options.target } };
218
228
  const chunks = deps.renderTelegramMessage(text, {
219
229
  mode: options?.parseMode === "HTML" ? "html" : "plain",
220
230
  });
221
231
  return deps.sendRenderedChunks(chunks, {
222
232
  target: options?.target,
223
233
  replyToMessageId: options?.replyToMessageId,
234
+ assertAuthority: options.assertAuthority,
224
235
  });
225
236
  }
226
237
  function normalizeIndentedTelegramNativeMarkdownList(line) {
@@ -242,22 +253,28 @@ function normalizeTelegramNativeMarkdownLine(line) {
242
253
  result = result.replace(/(^|[^\\$])\$([A-Z][A-Z0-9]{1,})(?!\$)(?=\b|[.,;:)/-])/g, (_match, prefix, ticker) => `${prefix}\\$${ticker}`);
243
254
  return result.replace(/\u0000(\d+)\u0000/g, (_match, index) => codeSpans[Number(index)] ?? "");
244
255
  }
256
+ export function parseTelegramMarkdownFenceOpening(line) {
257
+ const markerText = line.match(/^ {0,3}(`{3,}|~{3,})/)?.[1];
258
+ return markerText
259
+ ? { marker: markerText[0], length: markerText.length }
260
+ : undefined;
261
+ }
262
+ export function isTelegramMarkdownFenceClosing(line, fence) {
263
+ return new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line);
264
+ }
265
+ /** Fence state after one line: outside a fence a marker opens one; inside, a matching marker closes it. */
266
+ function advanceTelegramMarkdownFence(fence, line) {
267
+ if (!fence)
268
+ return parseTelegramMarkdownFenceOpening(line);
269
+ return isTelegramMarkdownFenceClosing(line, fence) ? undefined : fence;
270
+ }
245
271
  function hasClosingDisplayMathDelimiter(lines, startIndex) {
246
272
  let fence;
247
273
  for (let index = startIndex + 1; index < lines.length; index += 1) {
248
274
  const line = lines[index] ?? "";
249
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
250
275
  if (!fence && line.trim() === "$$")
251
276
  return true;
252
- if (!fence && fenceMatch) {
253
- const markerText = fenceMatch[1] ?? "```";
254
- fence = { marker: markerText[0], length: markerText.length };
255
- continue;
256
- }
257
- if (fence &&
258
- new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
259
- fence = undefined;
260
- }
277
+ fence = advanceTelegramMarkdownFence(fence, line);
261
278
  }
262
279
  return false;
263
280
  }
@@ -267,7 +284,6 @@ export function normalizeTelegramNativeMarkdown(markdown) {
267
284
  let displayMath = false;
268
285
  return lines
269
286
  .map((line, index) => {
270
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
271
287
  const inFence = fence !== undefined;
272
288
  if (!inFence && line.trim() === "$$") {
273
289
  if (displayMath) {
@@ -281,17 +297,9 @@ export function normalizeTelegramNativeMarkdown(markdown) {
281
297
  }
282
298
  if (displayMath)
283
299
  return line;
284
- if (!inFence && fenceMatch) {
285
- const markerText = fenceMatch[1] ?? "```";
286
- fence = {
287
- marker: markerText[0],
288
- length: markerText.length,
289
- };
290
- return line;
291
- }
292
- if (inFence &&
293
- new RegExp(`^ {0,3}${fence?.marker}{${fence?.length},}\\s*$`).test(line)) {
294
- fence = undefined;
300
+ const nextFence = advanceTelegramMarkdownFence(fence, line);
301
+ if (nextFence !== fence) {
302
+ fence = nextFence;
295
303
  return line;
296
304
  }
297
305
  if (!inFence)
@@ -349,21 +357,12 @@ function splitTelegramNativeMarkdownBlocks(markdown) {
349
357
  current.length = 0;
350
358
  };
351
359
  for (const line of markdown.split("\n")) {
352
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
353
360
  if (!fence && line.trim().length === 0) {
354
361
  flush();
355
362
  continue;
356
363
  }
357
364
  current.push(line);
358
- if (!fence && fenceMatch) {
359
- const markerText = fenceMatch[1] ?? "```";
360
- fence = { marker: markerText[0], length: markerText.length };
361
- continue;
362
- }
363
- if (fence &&
364
- new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
365
- fence = undefined;
366
- }
365
+ fence = advanceTelegramMarkdownFence(fence, line);
367
366
  }
368
367
  flush();
369
368
  return blocks;
@@ -376,21 +375,12 @@ function splitTelegramNativeMarkdownCountedBlocks(block) {
376
375
  let current = [];
377
376
  let fence;
378
377
  for (const line of block.split("\n")) {
379
- const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
380
378
  if (!fence && current.length >= TELEGRAM_RICH_MESSAGE_MAX_BLOCKS) {
381
379
  chunks.push(current.join("\n"));
382
380
  current = [];
383
381
  }
384
382
  current.push(line);
385
- if (!fence && fenceMatch) {
386
- const markerText = fenceMatch[1] ?? "```";
387
- fence = { marker: markerText[0], length: markerText.length };
388
- continue;
389
- }
390
- if (fence &&
391
- new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
392
- fence = undefined;
393
- }
383
+ fence = advanceTelegramMarkdownFence(fence, line);
394
384
  }
395
385
  if (current.length > 0)
396
386
  chunks.push(current.join("\n"));
@@ -427,12 +417,10 @@ function splitTelegramNativeMarkdownLongFenceBlock(block) {
427
417
  const lines = block.split("\n");
428
418
  const opening = lines[0] ?? "";
429
419
  const closing = lines[lines.length - 1] ?? "";
430
- const openingMatch = opening?.match(/^ {0,3}(`{3,}|~{3,})/);
431
- if (!openingMatch || !closing || lines.length < 2)
420
+ const openingFence = parseTelegramMarkdownFenceOpening(opening);
421
+ if (!openingFence || !closing || lines.length < 2)
432
422
  return undefined;
433
- const markerText = openingMatch[1] ?? "```";
434
- const marker = markerText[0];
435
- if (!new RegExp(`^ {0,3}${marker}{${markerText.length},}\\s*$`).test(closing)) {
423
+ if (!isTelegramMarkdownFenceClosing(closing, openingFence)) {
436
424
  return undefined;
437
425
  }
438
426
  const maxContentLength = TELEGRAM_RICH_MESSAGE_MAX_CHARS - opening.length - closing.length - 2;
@@ -478,33 +466,24 @@ function findTelegramNativeMarkdownSplitIndex(text, hardLimit = TELEGRAM_RICH_ME
478
466
  return hardLimit;
479
467
  }
480
468
  export async function sendTelegramNativeMarkdownReply(chatId, replyToMessageId, markdown, deps, options) {
481
- assertTelegramInlineKeyboardCallbackData(options?.replyMarkup);
482
- let lastMessageId;
483
- const chunks = splitTelegramNativeMarkdown(markdown);
484
- for (const [index, chunk] of chunks.entries()) {
485
- const sent = await withTelegramReplyParameters(chatId, index === 0 ? replyToMessageId : undefined, options?.target, (replyParameters) => deps.sendRichMessage({
486
- chat_id: chatId,
487
- rich_message: { markdown: chunk },
488
- reply_markup: index === chunks.length - 1 ? options?.replyMarkup : undefined,
489
- ...(replyParameters ? { reply_parameters: replyParameters } : {}),
490
- ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
491
- }));
492
- lastMessageId = sent.message_id;
493
- deps.recordOwnership?.({
494
- chatId,
495
- messageId: sent.message_id,
496
- target: options?.target,
497
- });
498
- }
499
- return lastMessageId;
469
+ return sendTelegramChunkSequence(chatId, splitTelegramNativeMarkdown(markdown), replyToMessageId, options, deps.recordOwnership, (chunk, fields, callOptions) => deps.sendRichMessage({ chat_id: chatId, rich_message: { markdown: chunk }, ...fields }, callOptions));
500
470
  }
501
471
  async function sendTelegramNativeRichMessage(chatId, richMessage, deps, options) {
472
+ options = { ...options, target: options?.target && { ...options.target } };
473
+ const assertAuthority = options.assertAuthority;
474
+ assertTelegramApiCallAuthority(assertAuthority, false);
502
475
  const sent = await deps.sendRichMessage({
503
476
  chat_id: chatId,
504
477
  rich_message: richMessage,
505
478
  ...(options?.target ? getTelegramTargetThreadParams(options.target) : {}),
479
+ }, assertAuthority ? { assertAuthority } : undefined);
480
+ assertTelegramApiCallAuthority(assertAuthority, true);
481
+ deps.recordOwnership?.({
482
+ chatId,
483
+ messageId: sent.message_id,
484
+ target: options?.target,
506
485
  });
507
- deps.recordOwnership?.({ chatId, messageId: sent.message_id, target: options?.target });
486
+ assertTelegramApiCallAuthority(assertAuthority, true);
508
487
  return sent.message_id;
509
488
  }
510
489
  export function createTelegramRenderedMessageDeliveryRuntime(deps) {
@@ -531,17 +510,23 @@ export function createTelegramRenderedMessageRuntime(deps) {
531
510
  renderTelegramMessage: deps.renderTelegramMessage,
532
511
  sendRenderedChunks: (chunks, chunkOptions) => deps.replyTransport.sendRenderedChunks(chatId, chunks, {
533
512
  target: chunkOptions?.target,
513
+ assertAuthority: chunkOptions?.assertAuthority,
534
514
  replyToMessageId: chunkOptions?.replyToMessageId ?? replyToMessageId,
535
515
  }),
536
516
  }, options);
537
517
  },
538
518
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
519
+ options = {
520
+ ...options,
521
+ target: options?.target && { ...options.target },
522
+ };
539
523
  const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
540
524
  if (renderingMode === "html") {
541
525
  return deps.replyTransport.sendRenderedChunks(chatId, deps.renderTelegramMessage(markdown, { mode: "markdown" }), {
542
526
  replyMarkup: options?.replyMarkup,
543
527
  target: options?.target,
544
528
  replyToMessageId,
529
+ assertAuthority: options.assertAuthority,
545
530
  });
546
531
  }
547
532
  return sendTelegramNativeMarkdownReply(chatId, replyToMessageId, markdown, {
@@ -549,14 +534,23 @@ export function createTelegramRenderedMessageRuntime(deps) {
549
534
  sendRichMessage: deps.sendRichMessage,
550
535
  }, options);
551
536
  },
552
- editInteractiveMessage: async (chatId, messageId, text, mode, replyMarkup) => {
553
- await deps.replyTransport.editRenderedMessage(chatId, messageId, deps.renderTelegramMessage(text, { mode }), { replyMarkup });
537
+ editInteractiveMessage: async (chatId, messageId, text, mode, replyMarkup, options) => {
538
+ options = {
539
+ ...options,
540
+ target: options?.target && { ...options.target },
541
+ };
542
+ await deps.replyTransport.editRenderedMessage(chatId, messageId, deps.renderTelegramMessage(text, { mode }), { ...options, replyMarkup });
554
543
  },
555
544
  sendInteractiveMessage: async (chatId, text, mode, replyMarkup, options) => {
545
+ options = {
546
+ ...options,
547
+ target: options?.target && { ...options.target },
548
+ };
556
549
  return deps.replyTransport.sendRenderedChunks(chatId, deps.renderTelegramMessage(text, { mode }), {
557
550
  replyMarkup,
558
551
  target: options?.target,
559
552
  replyToMessageId: options?.replyToMessageId,
553
+ assertAuthority: options.assertAuthority,
560
554
  });
561
555
  },
562
556
  sendSectionRichMessage: (chatId, message, options) => sendTelegramNativeRichMessage(chatId, message, {
@@ -565,17 +559,6 @@ export function createTelegramRenderedMessageRuntime(deps) {
565
559
  }, options),
566
560
  };
567
561
  }
568
- // --- Dedup-wrapped Reply Wrappers ---
569
- /** Wrap a sendTextReply with reply dedup so only the first message
570
- * in a turn carries reply metadata. */
571
- export function dedupSendTextReply(dedup, inner) {
572
- return async (chatId, replyToMessageId, text, options) => {
573
- const effectiveReplyTo = dedup.shouldReply(replyToMessageId)
574
- ? replyToMessageId
575
- : undefined;
576
- return inner(chatId, effectiveReplyTo, text, options);
577
- };
578
- }
579
562
  /**
580
563
  * Guest reply sender: answers guest queries with native Rich Markdown content.
581
564
  * Guest queries use InlineQueryResult input_message_content rather than chat
@@ -43,6 +43,71 @@ export declare function resolveTelegramGuestPromptPeer(input: {
43
43
  export declare function resolveTelegramGuestFileScope(input: Parameters<typeof resolveTelegramGuestPromptPeer>[0]): string;
44
44
  import * as Threads from "./threads.ts";
45
45
  import * as Updates from "./updates.ts";
46
+ /** Prepare the selected leader's exact retained originals; no copy, adoption, acceptance proof or dispatch. */
47
+ export declare function prepareTelegramLiveLeaderOriginal(input: {
48
+ request: Threads.TelegramWorkspaceRestoreRequest;
49
+ messages: readonly unknown[];
50
+ isCurrent(): boolean;
51
+ }): Updates.TelegramLiveDeferredInputPreparation | undefined;
52
+ export type TelegramLiveRebindCleanupIssue = {
53
+ status: "not-ready";
54
+ } | {
55
+ status: "finished";
56
+ operationId: string;
57
+ cleanup: ThreadReconciler.TelegramLiveRebindCleanupOutcome;
58
+ recorded: boolean;
59
+ };
60
+ /** Default live-rebind cleanup pacing: quick early attempts, then minutely, within a 15-minute window. */
61
+ export declare const TELEGRAM_LIVE_REBIND_CLEANUP_SCHEDULE: Readonly<{
62
+ delaysMs: readonly number[];
63
+ intervalMs: 60000;
64
+ windowMs: number;
65
+ }>;
66
+ /** Chooser-scoped saved command reference; it is never retained after the chooser record. */
67
+ type TelegramLiveRebindSelectedCommandReference = {
68
+ operationId: string;
69
+ preparedSource: Bus.TelegramBusPreparedCommandSource;
70
+ selectedCommand: Bus.TelegramBusSelectedCommandInput;
71
+ };
72
+ /** One warm live attempt with exact post-release peer donor settlement; no startup recovery, Restore ACK or cleanup. */
73
+ export declare function createTelegramLiveRebindCoordinator(input: {
74
+ request: Threads.TelegramWorkspaceRestoreRequest;
75
+ /** Captures caller source/reference, admission, profile, transport and Pi lifetime. */
76
+ authority: Threads.TelegramWorkspaceRestoreAuthority;
77
+ messages: readonly unknown[];
78
+ restoreStore: Threads.TelegramWorkspaceRestore;
79
+ threadStore: Pick<Threads.TelegramTopicTargetStore, "withWorkspaceRestoreSnapshot">;
80
+ getRecipient(): (Threads.TelegramWorkspaceRestoreRecipient & {
81
+ bindingKey: string;
82
+ }) | undefined;
83
+ leader?: {
84
+ apply(intent: Threads.TelegramWorkspaceLiveRebindIntent, mode: "apply" | "inspect", isCurrent: () => boolean): Promise<void>;
85
+ /** Synchronous local identity only; safe inside a Workspace publication fence. */
86
+ isApplied(intent: Threads.TelegramWorkspaceLiveRebindIntent): boolean;
87
+ } & ({
88
+ /** Confirm ordinary queue admission of these same bound messages, never handler replay. */
89
+ continue(messages: readonly unknown[], isCurrent: () => boolean): Promise<boolean>;
90
+ /** Observe only the same issued admission through its existing receipt owner; never re-enqueue. */
91
+ observeAdmission?(): boolean;
92
+ complete?: never;
93
+ } | {
94
+ /** Issue one semantic completion; the adapter owns detached delivery and errors, not a removal ACK. */
95
+ complete(messages: readonly unknown[], isCurrent: () => boolean): void;
96
+ continue?: never;
97
+ });
98
+ follower?: {
99
+ run: ReturnType<typeof Bus.createTelegramBusLiveRebindController>;
100
+ /** Explicit staged singleton command branch; absence preserves ordinary live input behavior. */
101
+ selectedCommand?: Bus.TelegramBusSelectedCommandInput;
102
+ };
103
+ recordRuntimeEvent?: (category: string, error: unknown, details: Record<string, unknown>) => void;
104
+ }): {
105
+ /** Correlation only; a later cleanup attempt rereads and revalidates the canonical row itself. */
106
+ operationId: string;
107
+ /** Body-free copy of the authenticated saved command reference: correlation only, never authority, an ACK or a cleanup permit. */
108
+ selectedCommandReference(): TelegramLiveRebindSelectedCommandReference | undefined;
109
+ advance(): Promise<"protected" | "unknown" | "released">;
110
+ };
46
111
  /** One admitted Restore attempt through recipient readiness, never source dispatch or cleanup. */
47
112
  export declare function advanceTelegramWorkspaceRestore(input: {
48
113
  request: Threads.TelegramWorkspaceRestoreRequest;
@@ -68,8 +133,8 @@ export declare function advanceTelegramWorkspaceRestore(input: {
68
133
  inspectOnly?: true;
69
134
  }): Promise<Threads.TelegramWorkspaceRestoreIntent | undefined>;
70
135
  export declare const TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS: number;
71
- /** One user-facing answer for expired or previous-process routing controls. */
72
- export declare const TELEGRAM_ROUTING_CHOICE_EXPIRED = "\u231B Routing choice expired.";
136
+ /** The toast for expired or previous-process routing controls; the chooser notice is written separately. */
137
+ export declare const TELEGRAM_ROUTING_CHOICE_EXPIRED = "Routing choice expired";
73
138
  export declare function isTelegramAllTabCommandExpired(message: {
74
139
  date?: number;
75
140
  message_thread_id?: number;
@@ -132,17 +197,36 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
132
197
  journalBindingKey: string;
133
198
  }) => TelegramUpdateJournalQueuedReceiptEvidence | undefined;
134
199
  hasWorkspaceRestoreAuthority?: () => boolean;
200
+ /** Exact live root authority; capability advertisement alone cannot replace owned profile/session/recipient fences. */
201
+ hasWorkspaceLiveRebindAuthority?: () => boolean;
135
202
  /** Strict complete namespace plus exact current/historical references; only an empty result clears journal protection. */
136
- inspectTemporaryThreadSources?: (target: Queue.TelegramQueueTarget, requiredJournalBindingKeys: readonly string[]) => readonly number[] | undefined;
203
+ inspectTemporaryThreadSources?: (target: Queue.TelegramQueueTarget, requiredJournalBindingKeys: readonly string[], ownInputs?: readonly Threads.TelegramTemporaryThreadInput[]) => readonly number[] | undefined;
137
204
  /** Quiet period after the last cancelled input before one cleanup attempt; defaults to 1000 ms. */
138
205
  temporaryThreadCleanupDelayMs?: number;
206
+ /** Live-rebind cleanup attempt pacing; the bounded window ends in `not-issued`, never a time-based deletion. */
207
+ liveRebindCleanupSchedule?: {
208
+ delaysMs: readonly number[];
209
+ intervalMs: number;
210
+ windowMs: number;
211
+ };
139
212
  getSessionGeneration?: () => number;
140
213
  workspaceRestoreRecipient?: {
141
214
  getSessionId: (ctx: TContext) => string | undefined;
142
215
  getCwd: (ctx: TContext) => string | undefined;
143
216
  getLeaderIdentity: Threads.TelegramLeaderThreadStateRuntime["getIdentity"];
217
+ /** Read-only current leader work; source/chooser lifetime is not recipient authority. */
218
+ observeLeaderWork?: (oldTarget: Queue.TelegramQueueTarget & {
219
+ threadId: number;
220
+ }, ctx: TContext) => Bus.TelegramBusLiveRebindWorkState;
144
221
  followerRegistry: Pick<Bus.TelegramBusFollowerRegistry, "get" | "register">;
145
222
  runFollower: ReturnType<typeof Bus.createTelegramBusWorkspaceRestoreController>;
223
+ liveFollower?: {
224
+ /** Pure trusted assembly availability (Commands/native/menu and local protocol), never future target activation. */
225
+ isSelectedCommandAvailable?(follower: Readonly<Bus.TelegramBusFollowerView>, name: string): boolean;
226
+ /** Exact session journal identity from the existing registration/journal owners, never the profile routing key. */
227
+ getJournalBindingKey(follower: Bus.TelegramBusFollowerView): string | undefined;
228
+ run: ReturnType<typeof Bus.createTelegramBusLiveRebindController>;
229
+ };
146
230
  };
147
231
  bridgeRuntime: TelegramBridgeRuntime;
148
232
  activeTurnRuntime: Queue.TelegramActiveTurnStore;
@@ -154,9 +238,11 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
154
238
  currentModelRuntime: Model.CurrentModelRuntime<TContext, TModel>;
155
239
  modelSwitchController: Model.TelegramModelSwitchController<TContext, Model.ScopedTelegramModel<TModel>>;
156
240
  menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
241
+ /** Thinking controls refuse while the active Telegram turn expects a voice reply. */
242
+ isVoiceReplyActive?: () => boolean;
157
243
  updateSettingsMenuMessage?: (state: Menu.TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
158
- openQueueMenu: (chatId: number, replyToMessageId: number, ctx: TContext) => Promise<void>;
159
- openSettingsMenu?: (chatId: number, replyToMessageId: number, ctx: TContext) => Promise<void>;
244
+ openQueueMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
245
+ openSettingsMenu?: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
160
246
  settingsMenuCallbackHandler?: (query: TCallbackQuery, ctx: TContext) => Promise<boolean>;
161
247
  queueMenuCallbackHandler: (query: TCallbackQuery, ctx: TContext) => Promise<boolean>;
162
248
  buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
@@ -184,12 +270,16 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
184
270
  };
185
271
  }) => void;
186
272
  stopTypingLoop?: () => void;
187
- answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
188
- editInteractiveMessage?: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: Menu.TelegramReplyMarkup) => Promise<void>;
273
+ answerCallbackQuery: (callbackQueryId: string, text?: string, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
274
+ editInteractiveMessage?: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: Menu.TelegramReplyMarkup, options?: {
275
+ target?: Queue.TelegramQueueTarget;
276
+ assertAuthority?: TelegramApiCallOptions["assertAuthority"];
277
+ }) => Promise<void>;
189
278
  editMessageReplyMarkup?: (chatId: number, messageId: number, replyMarkup: OutboundHandlers.TelegramOutboundButtonMarkup) => Promise<void>;
190
279
  sendInteractiveMessage?: (chatId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: Menu.TelegramReplyMarkup, options?: {
191
280
  target?: Queue.TelegramQueueTarget;
192
281
  replyToMessageId?: number;
282
+ assertAuthority?: TelegramApiCallOptions["assertAuthority"];
193
283
  }) => Promise<number | undefined>;
194
284
  deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
195
285
  answerGuestQuery: (guestQueryId: string, text?: string) => Promise<void>;
@@ -202,8 +292,11 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
202
292
  sendTextReply: (chatId: number, replyToMessageId: number, text: string, options?: {
203
293
  parseMode?: "HTML";
204
294
  target?: Queue.TelegramQueueTarget;
295
+ assertAuthority?: TelegramApiCallOptions["assertAuthority"];
205
296
  }) => Promise<number | undefined>;
206
297
  setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
298
+ /** Captures independent recipient authority, never the source execution or dialog handle. */
299
+ captureThreadNameRecipientAuthority?: (target: Queue.TelegramQueueTarget, ctx: TContext) => (() => void) | undefined;
207
300
  validateThreadName?: (threadName: string) => string | undefined;
208
301
  renameCurrentThread?: Commands.TelegramThreadDisplayNameRenamePort;
209
302
  resetCurrentThreadName?: Commands.TelegramThreadDisplayNameResetPort;
@@ -223,6 +316,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
223
316
  onError: (error: unknown) => void;
224
317
  }) => void;
225
318
  recordRuntimeEvent?: (category: string, error: unknown, details?: Record<string, unknown>) => void;
319
+ beginCommandEffectWork?: Commands.TelegramCommandRuntimeDeps<TMessage, TContext>["beginCommandEffectWork"];
226
320
  sectionRegistry?: TelegramSectionRegistry;
227
321
  sendSectionRichMessage?: (chatId: number, message: TelegramInputRichMessage, options?: {
228
322
  target?: {
@@ -249,6 +343,25 @@ export declare function createTelegramInboundRouteRuntime<TUpdate extends Update
249
343
  onUpdateCompleted: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["onUpdateCompleted"]>;
250
344
  onWorkspaceRestoreRecipientObserved(follower: Bus.TelegramBusFollowerView, isCurrent: () => boolean, ctx: TContext | undefined): Promise<void> | undefined;
251
345
  waitForRestoreSettlement(): Promise<void>;
346
+ /** Commands-owned registry port for dormant scoped recipient assembly, not ordinary update replay. */
347
+ prepareHeldCommand: ReturnType<typeof Commands.createTelegramCommandHandlerTargetRuntime<TMessage, TContext>>["prepareHeldCommand"];
348
+ canPrepareHeldCommand: ReturnType<typeof Commands.createTelegramCommandHandlerTargetRuntime<TMessage, TContext>>["canPrepareHeldCommand"];
349
+ /** Fresh body-free recipient observation only; never reconstructs an attempt or grants cleanup. */
350
+ observeLiveRebindLeaderWork(intent: Threads.TelegramWorkspaceLiveRebindIntent, ctx: TContext): Promise<Bus.TelegramBusLiveRebindWorkObservation | undefined>;
351
+ /** Fresh non-destructive candidate only; no retained idle, cleanup action or deletion grant. */
352
+ prepareLiveRebindLeaderCleanup(intent: Threads.TelegramWorkspaceLiveRebindIntent, ctx: TContext): Promise<ThreadReconciler.TelegramLiveRebindCleanupPreparation | undefined>;
353
+ /** Requires the authenticated peer's exact retained released carrier; never recreates save/apply/release. */
354
+ prepareLiveRebindFollowerCleanup(intent: Threads.TelegramWorkspaceLiveRebindIntent, ctx: TContext): Promise<ThreadReconciler.TelegramLiveRebindCleanupPreparation | undefined>;
355
+ /**
356
+ * One cleanup attempt under fresh admission: a fresh clear sample, then the durable issue marker, one unretried
357
+ * deletion and the terminal record. `not-ready` leaves the released row unissued; `undefined` is lost authority.
358
+ */
359
+ issueLiveRebindLeaderCleanup(intent: Threads.TelegramWorkspaceLiveRebindIntent, ctx: TContext): Promise<TelegramLiveRebindCleanupIssue | undefined>;
360
+ issueLiveRebindFollowerCleanup(intent: Threads.TelegramWorkspaceLiveRebindIntent, ctx: TContext): Promise<TelegramLiveRebindCleanupIssue | undefined>;
361
+ /** Coordinator-only prompt admission; caller retains Workspace/canonical and exact recipient authority. */
362
+ continueLiveRebindPrompt(messages: readonly TMessage[], ctx: TContext, target: Queue.TelegramQueueTarget & {
363
+ threadId: number;
364
+ }, isCurrent: () => boolean): Promise<boolean>;
252
365
  };
253
366
  export interface TelegramAssistantOutputAuthority<TTransportStamp> {
254
367
  transportStamp: TTransportStamp;