@llblab/pi-telegram 0.51.6 → 0.52.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 (178) hide show
  1. package/AGENTS.md +15 -12
  2. package/BACKLOG.md +16 -9
  3. package/CHANGELOG.md +19 -0
  4. package/LICENSE +21 -0
  5. package/README.md +17 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +5 -1
  9. package/dist/lib/bindings.js +7 -1
  10. package/dist/lib/bus-api.js +8 -5
  11. package/dist/lib/bus-follower.d.ts +54 -56
  12. package/dist/lib/bus-follower.js +206 -139
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +0 -23
  24. package/dist/lib/commands.js +15 -15
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +258 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +183 -16
  34. package/dist/lib/journal.js +1175 -248
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.js +1 -1
  38. package/dist/lib/locks.d.ts +100 -12
  39. package/dist/lib/locks.js +380 -14
  40. package/dist/lib/logging.d.ts +30 -12
  41. package/dist/lib/logging.js +129 -72
  42. package/dist/lib/media.d.ts +28 -5
  43. package/dist/lib/media.js +26 -8
  44. package/dist/lib/menu-model.d.ts +0 -11
  45. package/dist/lib/menu-model.js +8 -8
  46. package/dist/lib/menu-settings.d.ts +0 -18
  47. package/dist/lib/menu-settings.js +18 -18
  48. package/dist/lib/menu-status.d.ts +4 -4
  49. package/dist/lib/menu-status.js +12 -7
  50. package/dist/lib/menu-thinking.d.ts +1 -1
  51. package/dist/lib/menu-thinking.js +1 -1
  52. package/dist/lib/menu.d.ts +1 -0
  53. package/dist/lib/menu.js +4 -3
  54. package/dist/lib/outbound-attachments.d.ts +0 -1
  55. package/dist/lib/outbound-attachments.js +1 -1
  56. package/dist/lib/outbound-buttons.js +1 -8
  57. package/dist/lib/outbound-markup.d.ts +1 -6
  58. package/dist/lib/outbound-markup.js +4 -21
  59. package/dist/lib/outbound.d.ts +0 -8
  60. package/dist/lib/outbound.js +5 -5
  61. package/dist/lib/paths.d.ts +50 -7
  62. package/dist/lib/paths.js +155 -17
  63. package/dist/lib/pi.d.ts +1 -0
  64. package/dist/lib/pi.js +2 -1
  65. package/dist/lib/polling.d.ts +0 -5
  66. package/dist/lib/polling.js +5 -5
  67. package/dist/lib/preview.d.ts +0 -3
  68. package/dist/lib/preview.js +3 -3
  69. package/dist/lib/prompt-templates.d.ts +0 -1
  70. package/dist/lib/prompt-templates.js +1 -1
  71. package/dist/lib/queue.d.ts +5 -15
  72. package/dist/lib/queue.js +94 -11
  73. package/dist/lib/recovery.d.ts +29 -9
  74. package/dist/lib/recovery.js +104 -33
  75. package/dist/lib/replies.d.ts +0 -4
  76. package/dist/lib/replies.js +1 -1
  77. package/dist/lib/routing.d.ts +74 -8
  78. package/dist/lib/routing.js +2280 -307
  79. package/dist/lib/sections.d.ts +0 -2
  80. package/dist/lib/sections.js +1 -1
  81. package/dist/lib/status.d.ts +44 -9
  82. package/dist/lib/status.js +149 -27
  83. package/dist/lib/sync.d.ts +0 -5
  84. package/dist/lib/sync.js +5 -4
  85. package/dist/lib/telegram-api.d.ts +13 -3
  86. package/dist/lib/telegram-api.js +38 -23
  87. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  88. package/dist/lib/thread-cleanup-manager.js +18 -21
  89. package/dist/lib/thread-naming.d.ts +32 -5
  90. package/dist/lib/thread-naming.js +191 -7
  91. package/dist/lib/thread-reconciler.d.ts +2 -0
  92. package/dist/lib/thread-reconciler.js +1 -1
  93. package/dist/lib/threads.d.ts +293 -59
  94. package/dist/lib/threads.js +1689 -369
  95. package/dist/lib/turns.d.ts +0 -1
  96. package/dist/lib/turns.js +1 -1
  97. package/dist/lib/updates.d.ts +191 -28
  98. package/dist/lib/updates.js +1048 -114
  99. package/dist/lib/wire.d.ts +12 -0
  100. package/dist/lib/wire.js +21 -0
  101. package/dist/lib/workspace-admission.d.ts +25 -17
  102. package/dist/lib/workspace-admission.js +95 -20
  103. package/dist/lib/workspace-identity.d.ts +20 -0
  104. package/dist/lib/workspace-identity.js +103 -0
  105. package/dist/lib/workspace-retirement.d.ts +43 -8
  106. package/dist/lib/workspace-retirement.js +148 -52
  107. package/dist/package.json +6 -6
  108. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  109. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  110. package/docs/README.md +1 -1
  111. package/docs/activity.md +1 -1
  112. package/docs/architecture.md +156 -36
  113. package/docs/callback-namespaces.md +1 -1
  114. package/docs/delivery.md +1 -1
  115. package/docs/generative-apps.md +2 -2
  116. package/docs/inbound.md +1 -1
  117. package/docs/multi-instance-bus.md +125 -19
  118. package/docs/public-api.md +3 -1
  119. package/docs/ui-style.md +23 -9
  120. package/docs/updates.md +1 -1
  121. package/lib/activity-verbosity.ts +5 -9
  122. package/lib/bindings.ts +7 -0
  123. package/lib/bus-api.ts +9 -4
  124. package/lib/bus-follower.ts +243 -224
  125. package/lib/bus-leader.ts +178 -31
  126. package/lib/bus-transport.ts +30 -8
  127. package/lib/bus.ts +110 -334
  128. package/lib/channel-posts.ts +29 -29
  129. package/lib/command-templates.ts +5 -5
  130. package/lib/commands.ts +15 -15
  131. package/lib/config.ts +12 -17
  132. package/lib/delivery.ts +2 -8
  133. package/lib/extension.ts +261 -72
  134. package/lib/generative-apps.ts +0 -22
  135. package/lib/inbound.ts +2 -2
  136. package/lib/journal.ts +1250 -325
  137. package/lib/keyboard.ts +2 -2
  138. package/lib/lifecycle.ts +1 -1
  139. package/lib/locks.ts +402 -23
  140. package/lib/logging.ts +151 -101
  141. package/lib/media.ts +51 -9
  142. package/lib/menu-model.ts +8 -8
  143. package/lib/menu-settings.ts +18 -18
  144. package/lib/menu-status.ts +11 -0
  145. package/lib/menu-thinking.ts +2 -2
  146. package/lib/menu.ts +5 -1
  147. package/lib/outbound-attachments.ts +1 -1
  148. package/lib/outbound-buttons.ts +1 -10
  149. package/lib/outbound-markup.ts +5 -24
  150. package/lib/outbound.ts +5 -5
  151. package/lib/paths.ts +177 -17
  152. package/lib/pi.ts +5 -2
  153. package/lib/polling.ts +5 -5
  154. package/lib/preview.ts +3 -3
  155. package/lib/prompt-templates.ts +1 -1
  156. package/lib/queue.ts +93 -12
  157. package/lib/recovery.ts +104 -44
  158. package/lib/replies.ts +1 -1
  159. package/lib/routing.ts +2010 -375
  160. package/lib/sections.ts +1 -1
  161. package/lib/status.ts +156 -36
  162. package/lib/sync.ts +4 -4
  163. package/lib/telegram-api.ts +53 -33
  164. package/lib/thread-cleanup-manager.ts +24 -21
  165. package/lib/thread-naming.ts +267 -9
  166. package/lib/thread-reconciler.ts +3 -1
  167. package/lib/threads.ts +1736 -488
  168. package/lib/turns.ts +1 -1
  169. package/lib/updates.ts +1094 -151
  170. package/lib/wire.ts +28 -0
  171. package/lib/workspace-admission.ts +97 -47
  172. package/lib/workspace-identity.ts +147 -0
  173. package/lib/workspace-retirement.ts +188 -89
  174. package/package.json +6 -6
  175. package/scripts/audit-exports.mjs +100 -0
  176. package/scripts/check-downgrade.mjs +80 -43
  177. package/skills/generated-control-surface/SKILL.md +3 -3
  178. package/skills/telegram-bridge/references/diagnosis.md +3 -3
package/dist/lib/queue.js CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { createHash } from "node:crypto";
7
7
  import { isVoiceTurn } from "./voice.js";
8
+ import { isWireRecord as isQueuePayloadRecord } from "./wire.js";
8
9
  import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
9
10
  export const TELEGRAM_QUEUE_LANE_CONTRACTS = [
10
11
  // Control lane intentionally accepts both direct controls and resume prompts.
@@ -30,7 +31,67 @@ export const TELEGRAM_QUEUE_LANE_CONTRACTS = [
30
31
  },
31
32
  ];
32
33
  export const TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES = 8 * 1024 * 1024;
33
- export const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
34
+ const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
35
+ function parseQueuePayloadReceipt(value, kind) {
36
+ if (!isQueuePayloadRecord(value) || value.queueKind !== kind ||
37
+ typeof value.receiptId !== "string" || !value.receiptId ||
38
+ (value.journalBindingKey !== undefined && (typeof value.journalBindingKey !== "string" || !value.journalBindingKey.trim())) ||
39
+ !Array.isArray(value.sourceUpdateIds) || value.sourceUpdateIds.length === 0 ||
40
+ value.sourceUpdateIds.some((id, index) => !Number.isSafeInteger(id) || id < 0 ||
41
+ (index > 0 && id <= value.sourceUpdateIds[index - 1])))
42
+ return undefined;
43
+ return { queueKind: kind, receiptId: value.receiptId, sourceUpdateIds: value.sourceUpdateIds,
44
+ ...(typeof value.journalBindingKey === "string" ? { journalBindingKey: value.journalBindingKey } : {}) };
45
+ }
46
+ /** Data decoder shared by authenticated IPC and cold storage; decoding grants no admission. */
47
+ export function parseTelegramQueueHandoffPayload(value) {
48
+ if (!isQueuePayloadRecord(value) || (value.kind !== "prompt" && value.kind !== "control"))
49
+ return undefined;
50
+ const target = isQueuePayloadRecord(value.target) && typeof value.target.chatId === "number"
51
+ ? { chatId: value.target.chatId, ...(typeof value.target.threadId === "number" ? { threadId: value.target.threadId } : {}) } : undefined;
52
+ const transportStamp = isQueuePayloadRecord(value.transportStamp) &&
53
+ typeof value.transportStamp.profile === "string" && typeof value.transportStamp.generation === "string"
54
+ ? { profile: value.transportStamp.profile, generation: value.transportStamp.generation } : undefined;
55
+ if (!Number.isSafeInteger(value.chatId) || (value.target !== undefined && !target) ||
56
+ (value.transportStamp !== undefined && !transportStamp) || !Number.isSafeInteger(value.replyToMessageId) ||
57
+ (value.guestQueryId !== undefined && typeof value.guestQueryId !== "string") ||
58
+ (value.guestInlineMessageId !== undefined && typeof value.guestInlineMessageId !== "string") ||
59
+ !Number.isSafeInteger(value.queueOrder) || (value.queueLane !== "control" && value.queueLane !== "priority" && value.queueLane !== "default") ||
60
+ !Number.isSafeInteger(value.laneOrder) || typeof value.statusSummary !== "string" || !Array.isArray(value.admissionReceipts))
61
+ return undefined;
62
+ const admissionReceipts = value.admissionReceipts.map(receipt => parseQueuePayloadReceipt(receipt, value.kind));
63
+ if (admissionReceipts.length === 0 || admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS ||
64
+ admissionReceipts.some(receipt => !receipt))
65
+ return undefined;
66
+ const base = { chatId: value.chatId, ...(target ? { target } : {}), ...(transportStamp ? { transportStamp } : {}),
67
+ replyToMessageId: value.replyToMessageId,
68
+ ...(typeof value.guestQueryId === "string" ? { guestQueryId: value.guestQueryId } : {}),
69
+ ...(typeof value.guestInlineMessageId === "string" ? { guestInlineMessageId: value.guestInlineMessageId } : {}),
70
+ queueOrder: value.queueOrder, queueLane: value.queueLane,
71
+ laneOrder: value.laneOrder, statusSummary: value.statusSummary,
72
+ admissionReceipts: admissionReceipts };
73
+ if (value.kind === "control") {
74
+ return value.queueLane === "control" && (value.controlType === "status" || value.controlType === "model")
75
+ ? { kind: "control", controlType: value.controlType, ...base } : undefined;
76
+ }
77
+ if (value.queueLane === "control" || !Array.isArray(value.sourceMessageIds) || value.sourceMessageIds.some(id => !Number.isSafeInteger(id)) ||
78
+ !Array.isArray(value.queuedAttachments) || value.queuedAttachments.some(attachment => !isQueuePayloadRecord(attachment) ||
79
+ typeof attachment.path !== "string" || typeof attachment.fileName !== "string") ||
80
+ !Array.isArray(value.content) || value.content.some(content => !isQueuePayloadRecord(content) ||
81
+ (content.type === "text" ? typeof content.text !== "string" : content.type === "image"
82
+ ? typeof content.data !== "string" || typeof content.mimeType !== "string" : true)) ||
83
+ typeof value.historyText !== "string" || (value.priorityEmoji !== undefined && typeof value.priorityEmoji !== "string") ||
84
+ (value.reactionSuppressionEmoji !== undefined && typeof value.reactionSuppressionEmoji !== "string") ||
85
+ (value.voiceReplyPreferred !== undefined && typeof value.voiceReplyPreferred !== "boolean") ||
86
+ (value.voiceReplyRequired !== undefined && typeof value.voiceReplyRequired !== "boolean"))
87
+ return undefined;
88
+ return { kind: "prompt", ...base, sourceMessageIds: value.sourceMessageIds,
89
+ queuedAttachments: value.queuedAttachments, content: value.content, historyText: value.historyText,
90
+ ...(typeof value.priorityEmoji === "string" ? { priorityEmoji: value.priorityEmoji } : {}),
91
+ ...(typeof value.reactionSuppressionEmoji === "string" ? { reactionSuppressionEmoji: value.reactionSuppressionEmoji } : {}),
92
+ ...(typeof value.voiceReplyPreferred === "boolean" ? { voiceReplyPreferred: value.voiceReplyPreferred } : {}),
93
+ ...(typeof value.voiceReplyRequired === "boolean" ? { voiceReplyRequired: value.voiceReplyRequired } : {}) };
94
+ }
34
95
  export function createTelegramQueueAdmissionReceipt(options) {
35
96
  if (options.sourceUpdateIds.length === 0)
36
97
  return undefined;
@@ -100,7 +161,7 @@ export function assertTelegramQueueItemAdmissionValid(item) {
100
161
  function getTelegramQueueLaneRank(lane) {
101
162
  return getTelegramQueueLaneContract(lane).dispatchRank;
102
163
  }
103
- export function isPendingTelegramTurn(item) {
164
+ function isPendingTelegramTurn(item) {
104
165
  return item.kind === "prompt";
105
166
  }
106
167
  export function createTelegramQueueStore(initialItems = []) {
@@ -148,7 +209,7 @@ export function createTelegramTransportStampedQueueStore(store, getTransportStam
148
209
  },
149
210
  };
150
211
  }
151
- export function isTelegramQueueItemSkipped(item) {
212
+ function isTelegramQueueItemSkipped(item) {
152
213
  return item.kind === "prompt" && Boolean(item.reactionSuppressionEmoji);
153
214
  }
154
215
  export function countExecutableTelegramQueueItems(items) {
@@ -503,7 +564,7 @@ export function removeTelegramQueueItemsByMessageIds(items, messageIds, scope) {
503
564
  removedCount: removedItems.length,
504
565
  };
505
566
  }
506
- export function removeTelegramQueuedGuestPromptByOrder(items, queueOrder) {
567
+ function removeTelegramQueuedGuestPromptByOrder(items, queueOrder) {
507
568
  const index = items.findIndex((item) => {
508
569
  return isPendingTelegramTurn(item) &&
509
570
  item.guestQueryId !== undefined &&
@@ -521,7 +582,7 @@ export function removeTelegramQueuedGuestPromptByOrder(items, queueOrder) {
521
582
  export function applyTelegramQueuePromptReactionDisposition(items, messageId, disposition, destinationLaneOrder, scope) {
522
583
  let nextItems = items;
523
584
  for (const [index, item] of items.entries()) {
524
- if (!isPendingTelegramTurn(item) ||
585
+ if (!isPendingTelegramTurn(item) || item.queueLane === "control" ||
525
586
  !isTelegramQueueItemInMessageScope(item, scope) ||
526
587
  !item.sourceMessageIds.includes(messageId)) {
527
588
  continue;
@@ -1489,7 +1550,15 @@ function updateTelegramQueueStatusRuntime(deps) {
1489
1550
  }
1490
1551
  }
1491
1552
  function commitReorderedTelegramQueueItemsRuntime(items, deps) {
1492
- deps.setQueuedItems([...items].sort(compareTelegramQueueItems));
1553
+ const pendingHead = deps.hasPendingDispatch?.() ? deps.getQueuedItems()[0] : undefined;
1554
+ const ordered = [...items].sort(compareTelegramQueueItems);
1555
+ if (pendingHead) {
1556
+ const index = ordered.findIndex(item => item.queueOrder === pendingHead.queueOrder && item.kind === pendingHead.kind &&
1557
+ item.chatId === pendingHead.chatId && item.replyToMessageId === pendingHead.replyToMessageId);
1558
+ if (index > 0)
1559
+ ordered.unshift(ordered.splice(index, 1)[0]);
1560
+ }
1561
+ deps.setQueuedItems(ordered);
1493
1562
  updateTelegramQueueStatusRuntime(deps);
1494
1563
  }
1495
1564
  function appendTelegramQueueItemRuntime(item, deps) {
@@ -1499,7 +1568,7 @@ function appendTelegramQueueItemRuntime(item, deps) {
1499
1568
  return;
1500
1569
  commitReorderedTelegramQueueItemsRuntime(nextItems, deps);
1501
1570
  }
1502
- export function reorderTelegramQueueItemsRuntime(deps) {
1571
+ function reorderTelegramQueueItemsRuntime(deps) {
1503
1572
  commitReorderedTelegramQueueItemsRuntime(deps.getQueuedItems(), deps);
1504
1573
  }
1505
1574
  export function clearTelegramQueueItemsRuntime(deps) {
@@ -1528,7 +1597,7 @@ export function removeTelegramQueueItemsByMessageIdsRuntime(messageIds, deps, sc
1528
1597
  updateTelegramQueueStatusRuntime(deps);
1529
1598
  return removedCount;
1530
1599
  }
1531
- export function removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, deps) {
1600
+ function removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, deps) {
1532
1601
  const { items, removedItems, removedCount } = removeTelegramQueuedGuestPromptByOrder(deps.getQueuedItems(), queueOrder);
1533
1602
  if (removedCount === 0)
1534
1603
  return false;
@@ -1541,8 +1610,22 @@ export function removeTelegramQueuedGuestPromptByOrderRuntime(queueOrder, deps)
1541
1610
  }
1542
1611
  export function applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, deps, scope) {
1543
1612
  const queuedItems = deps.getQueuedItems();
1613
+ const suppression = disposition.kind === "reaction-transition" ? disposition.suppressionEmoji :
1614
+ disposition.kind === "suppressed" ? disposition.emoji : disposition.kind === "priority-suppressed" ? disposition.suppressionEmoji : undefined;
1615
+ // Source-addressed control-lane prompts are explicit continuations. Synthetic model-switch turns have no source IDs.
1616
+ // A Pi-owned dispatched head cannot be removed: agent_start must consume that exact item, not its successor.
1617
+ const cancelled = suppression && deps.hasPendingDispatch ? queuedItems.filter((item, index) => isPendingTelegramTurn(item) && item.queueLane === "control" && isTelegramQueueItemInMessageScope(item, scope) &&
1618
+ item.sourceMessageIds.includes(messageId) && !(index === 0 && deps.hasPendingDispatch())) : [];
1619
+ if (cancelled.length) {
1620
+ if (cancelled.some(item => (item.admissionReceipts?.length ?? 0) > 0) && !deps.onItemsDiscarded)
1621
+ return false;
1622
+ deps.onItemsDiscarded?.(cancelled, deps.ctx);
1623
+ deps.setQueuedItems(queuedItems.filter(item => !cancelled.includes(item)));
1624
+ updateTelegramQueueStatusRuntime(deps);
1625
+ return true;
1626
+ }
1544
1627
  const changesLane = queuedItems.some((item) => {
1545
- if (!isPendingTelegramTurn(item) ||
1628
+ if (!isPendingTelegramTurn(item) || item.queueLane === "control" ||
1546
1629
  !isTelegramQueueItemInMessageScope(item, scope) ||
1547
1630
  !item.sourceMessageIds.includes(messageId)) {
1548
1631
  return false;
@@ -1639,7 +1722,7 @@ export async function executeTelegramControlItemRuntime(item, deps) {
1639
1722
  * Production debounce for deferred queue dispatch; the factory defaults to this
1640
1723
  * so the entrypoint wires ports instead of policy constants.
1641
1724
  */
1642
- export const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
1725
+ const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
1643
1726
  export function createTelegramDeferredQueueDispatchRuntime(deps = {}) {
1644
1727
  let boundContext;
1645
1728
  let generation = 0;
@@ -2004,7 +2087,7 @@ export function createTelegramQueueDispatchController(deps) {
2004
2087
  const announcementGeneration = nextDispatchAnnouncementGeneration;
2005
2088
  const dispatchGeneration = deps.getDispatchGeneration?.();
2006
2089
  deps.updateStatus(ctx);
2007
- void deps.sendTextReply(dispatchPlan.item.chatId, dispatchPlan.item.replyToMessageId, "<b>⏩ Dispatching next queued turn.</b>", { target: dispatchPlan.item.target }).catch((error) => {
2090
+ void deps.sendTextReply(dispatchPlan.item.chatId, dispatchPlan.item.replyToMessageId, "<b>⏩ Dispatching next queued turn.</b>", { target: dispatchPlan.item.target, parseMode: "HTML" }).catch((error) => {
2008
2091
  deps.recordRuntimeEvent?.("dispatch", error, {
2009
2092
  phase: "next-announcement",
2010
2093
  });
@@ -3,7 +3,6 @@
3
3
  * Zones: filesystem diagnostics, unclean-shutdown recovery
4
4
  * Owns fail-safe classification of temporary ownership and routing artifacts
5
5
  */
6
- import { renameSync } from "node:fs";
7
6
  import { type TelegramFileTransactionOptions } from "./locks.ts";
8
7
  export type TelegramRuntimeArtifactKind = "owners" | "state" | "transaction";
9
8
  export interface TelegramRuntimeCorruptArtifact {
@@ -35,19 +34,19 @@ export type TelegramRuntimeRecoveryResult = {
35
34
  } | {
36
35
  kind: "blocked-live-owner";
37
36
  livePids: number[];
38
- quarantineDir?: string;
37
+ deletedPaths?: string[];
39
38
  } | {
40
39
  kind: "recovered";
41
40
  artifacts: TelegramRuntimeCorruptArtifact[];
42
- quarantineDir: string;
41
+ /** Damaged disposable artifacts deleted per the approved corruption policy; nothing is quarantined. */
42
+ deletedPaths: string[];
43
43
  };
44
44
  export interface TelegramRuntimeRecoveryOptions extends TelegramRuntimeRecoveryClassificationOptions {
45
45
  recoveryTransactionPath?: string;
46
- quarantineRoot?: string;
47
46
  pid?: number;
48
47
  getNowMs?: () => number;
49
- quarantineRename?: typeof renameSync;
50
- quarantineRenameRetryDelayMs?: number;
48
+ /** Fault-injection seam; production uses bounded-retry recursive removal. */
49
+ removePath?: (path: string) => void;
51
50
  transactionOptions?: TelegramFileTransactionOptions;
52
51
  }
53
52
  export type TelegramPollingStartRecoveryDecision = {
@@ -74,13 +73,34 @@ export interface TelegramPollingStartRecoveryHandlerDeps {
74
73
  */
75
74
  export declare function classifyTelegramRuntimeRecovery(options: TelegramRuntimeRecoveryClassificationOptions): TelegramRuntimeRecoveryClassification;
76
75
  /**
77
- * Quarantine classifier-approved disposable corruption under two guards.
76
+ * Delete classifier-approved disposable corruption under two guards.
78
77
  *
79
78
  * A dedicated recovery transaction serializes recoverers. The ownership
80
79
  * transaction then prevents a new Telegram owner from appearing between the
81
- * final classification and mutation. Every artifact is renamed within its
82
- * filesystem; durable config and diagnostics never enter the candidate set.
80
+ * final classification and mutation. Damaged ownership debris and canonical
81
+ * state are deleted (operator policy: unfinished Restores in unreadable state
82
+ * are acceptable loss). Durable config and diagnostics never enter the set.
83
83
  */
84
84
  export declare function recoverTelegramRuntimeState(options: TelegramRuntimeRecoveryOptions): TelegramRuntimeRecoveryResult;
85
+ /**
86
+ * Remove recovery folders written by earlier releases (runtime root and session folders).
87
+ * Current releases delete damaged files instead of quarantining them; nothing reads these copies.
88
+ */
89
+ export declare function removeTelegramLegacyRecoveryStorage(runtimeDir: string): string[];
90
+ export interface TelegramSessionFolderSweeperDeps {
91
+ getSessionsDir: () => string;
92
+ getProfileName: () => string | undefined;
93
+ /** Sessions holding a Workspace slot binding, live registrations and this process's own session. */
94
+ getKeptSessionIds: () => Iterable<string | undefined>;
95
+ getNowMs?: () => number;
96
+ intervalMs?: number;
97
+ }
98
+ /**
99
+ * Leader housekeeping (operator policy): a session without a Workspace slot loses its
100
+ * current-profile journal family; the folder disappears once no profile uses it.
101
+ */
102
+ export declare function createTelegramSessionFolderSweeper(deps: TelegramSessionFolderSweeperDeps): {
103
+ sweep: () => string[];
104
+ };
85
105
  /** Build the `/telegram-connect` recovery boundary around runtime artifacts. */
86
106
  export declare function createTelegramPollingStartRecoveryHandler(deps: TelegramPollingStartRecoveryHandlerDeps): () => Promise<TelegramPollingStartRecoveryDecision>;
@@ -3,10 +3,10 @@
3
3
  * Zones: filesystem diagnostics, unclean-shutdown recovery
4
4
  * Owns fail-safe classification of temporary ownership and routing artifacts
5
5
  */
6
- import { randomUUID } from "node:crypto";
7
- import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, } from "node:fs";
6
+ import { existsSync, lstatSync, readFileSync, readdirSync, rmdirSync, rmSync, } from "node:fs";
8
7
  import { basename, dirname, join } from "node:path";
9
- import { isProcessAlive as defaultIsProcessAlive, parseTelegramLockEntry, renameTelegramPathWithRetry, TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, withTelegramFileTransaction, } from "./locks.js";
8
+ import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix } from "./paths.js";
9
+ import { isProcessAlive as defaultIsProcessAlive, parseTelegramLockEntry, TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, withTelegramFileTransaction, } from "./locks.js";
10
10
  const OWNER_FILE_PATTERN = /^owner\.([A-Za-z0-9-]+)\.json$/u;
11
11
  const RECLAIM_FILE_PATTERN = /^owner\.reclaim\.(\d+)\.([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.json$/u;
12
12
  function corruption(kind, path, reason) {
@@ -40,6 +40,9 @@ function inspectState(path) {
40
40
  if (!existsSync(path))
41
41
  return { source: "state", ownerPids: [] };
42
42
  try {
43
+ const stat = lstatSync(path);
44
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 8 * 1024 * 1024)
45
+ return corruption("state", path, `${basename(path)} cannot be safely inspected`);
43
46
  JSON.parse(readFileSync(path, "utf8"));
44
47
  return { source: "state", ownerPids: [] };
45
48
  }
@@ -136,19 +139,19 @@ export function classifyTelegramRuntimeRecovery(options) {
136
139
  : { kind: "recoverable-corruption", artifacts };
137
140
  }
138
141
  /**
139
- * Quarantine classifier-approved disposable corruption under two guards.
142
+ * Delete classifier-approved disposable corruption under two guards.
140
143
  *
141
144
  * A dedicated recovery transaction serializes recoverers. The ownership
142
145
  * transaction then prevents a new Telegram owner from appearing between the
143
- * final classification and mutation. Every artifact is renamed within its
144
- * filesystem; durable config and diagnostics never enter the candidate set.
146
+ * final classification and mutation. Damaged ownership debris and canonical
147
+ * state are deleted (operator policy: unfinished Restores in unreadable state
148
+ * are acceptable loss). Durable config and diagnostics never enter the set.
145
149
  */
146
150
  export function recoverTelegramRuntimeState(options) {
147
- const pid = options.pid ?? process.pid;
148
151
  const transactionPath = options.transactionPath ?? `${options.ownersPath}.transaction`;
149
152
  const recoveryTransactionPath = options.recoveryTransactionPath ??
150
153
  join(dirname(options.ownersPath), "runtime-recovery.transaction");
151
- const quarantineRoot = options.quarantineRoot ?? join(dirname(options.ownersPath), "recovery");
154
+ const removePath = options.removePath ?? ((path) => rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 }));
152
155
  const classificationOptions = {
153
156
  ownersPath: options.ownersPath,
154
157
  statePaths: options.statePaths,
@@ -167,29 +170,18 @@ export function recoverTelegramRuntimeState(options) {
167
170
  livePids: initial.livePids,
168
171
  };
169
172
  }
170
- let quarantineDir;
173
+ const deletedPaths = [];
171
174
  const recoveredArtifacts = [];
172
- const ensureQuarantineDir = () => {
173
- if (quarantineDir)
174
- return quarantineDir;
175
- quarantineDir = join(quarantineRoot, `${options.getNowMs?.() ?? Date.now()}-${pid}-${randomUUID()}`);
176
- mkdirSync(quarantineDir, { recursive: true, mode: 0o700 });
177
- return quarantineDir;
178
- };
179
- const quarantineArtifact = (artifact) => {
175
+ const deleteArtifact = (artifact) => {
180
176
  if (!existsSync(artifact.path))
181
177
  return;
182
- const destination = join(ensureQuarantineDir(), basename(artifact.path));
183
- if (renameTelegramPathWithRetry(artifact.path, destination, {
184
- rename: options.quarantineRename,
185
- retryDelayMs: options.quarantineRenameRetryDelayMs,
186
- })) {
187
- recoveredArtifacts.push(artifact);
188
- }
178
+ removePath(artifact.path);
179
+ deletedPaths.push(artifact.path);
180
+ recoveredArtifacts.push(artifact);
189
181
  };
190
182
  for (const artifact of initial.artifacts) {
191
183
  if (artifact.kind === "transaction")
192
- quarantineArtifact(artifact);
184
+ deleteArtifact(artifact);
193
185
  }
194
186
  return withTelegramFileTransaction(transactionPath, () => {
195
187
  const current = classifyTelegramRuntimeRecovery({
@@ -200,25 +192,107 @@ export function recoverTelegramRuntimeState(options) {
200
192
  return {
201
193
  kind: "blocked-live-owner",
202
194
  livePids: current.livePids,
203
- quarantineDir,
195
+ ...(deletedPaths.length > 0 ? { deletedPaths } : {}),
204
196
  };
205
197
  }
206
198
  if (current.kind === "recoverable-corruption") {
207
199
  for (const artifact of current.artifacts) {
208
200
  if (artifact.kind !== "transaction")
209
- quarantineArtifact(artifact);
201
+ deleteArtifact(artifact);
210
202
  }
211
203
  }
212
- return recoveredArtifacts.length > 0 && quarantineDir
204
+ return recoveredArtifacts.length > 0
213
205
  ? {
214
206
  kind: "recovered",
215
207
  artifacts: recoveredArtifacts,
216
- quarantineDir,
208
+ deletedPaths,
217
209
  }
218
210
  : { kind: "not-needed" };
219
211
  }, options.transactionOptions);
220
212
  }, options.transactionOptions);
221
213
  }
214
+ /**
215
+ * Remove recovery folders written by earlier releases (runtime root and session folders).
216
+ * Current releases delete damaged files instead of quarantining them; nothing reads these copies.
217
+ */
218
+ export function removeTelegramLegacyRecoveryStorage(runtimeDir) {
219
+ const removed = [];
220
+ const remove = (path) => {
221
+ try {
222
+ if (!lstatSync(path).isDirectory())
223
+ return;
224
+ rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
225
+ removed.push(path);
226
+ }
227
+ catch {
228
+ // Best-effort housekeeping; a later startup retries.
229
+ }
230
+ };
231
+ remove(join(runtimeDir, "recovery"));
232
+ let sessions = [];
233
+ try {
234
+ sessions = readdirSync(join(runtimeDir, "sessions"));
235
+ }
236
+ catch { /* no sessions yet */ }
237
+ for (const session of sessions)
238
+ remove(join(runtimeDir, "sessions", session, "recovery"));
239
+ return removed;
240
+ }
241
+ const TELEGRAM_SESSION_SWEEP_INTERVAL_MS = 10 * 60 * 1000;
242
+ /**
243
+ * Leader housekeeping (operator policy): a session without a Workspace slot loses its
244
+ * current-profile journal family; the folder disappears once no profile uses it.
245
+ */
246
+ export function createTelegramSessionFolderSweeper(deps) {
247
+ const getNowMs = deps.getNowMs ?? Date.now;
248
+ const intervalMs = deps.intervalMs ?? TELEGRAM_SESSION_SWEEP_INTERVAL_MS;
249
+ let lastSweepAtMs;
250
+ return {
251
+ sweep() {
252
+ const now = getNowMs();
253
+ if (lastSweepAtMs !== undefined && now - lastSweepAtMs < intervalMs)
254
+ return [];
255
+ lastSweepAtMs = now;
256
+ const sessionsDir = deps.getSessionsDir();
257
+ const kept = new Set(deps.getKeptSessionIds());
258
+ const suffix = getTelegramProfilePathSuffix(deps.getProfileName()).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
259
+ const family = new RegExp(`^journal\\.[a-f0-9]{16}${suffix}\\.json(?:\\.segments|\\.retained)?$`, "u");
260
+ const removed = [];
261
+ let names;
262
+ try {
263
+ names = readdirSync(sessionsDir);
264
+ }
265
+ catch {
266
+ return removed;
267
+ }
268
+ for (const name of names) {
269
+ const sessionId = decodeTelegramSessionDirectoryName(name);
270
+ if (sessionId === undefined || kept.has(sessionId))
271
+ continue;
272
+ const folder = join(sessionsDir, name);
273
+ try {
274
+ if (!lstatSync(folder).isDirectory())
275
+ continue;
276
+ for (const entry of readdirSync(folder)) {
277
+ if (!family.test(entry))
278
+ continue;
279
+ const path = join(folder, entry);
280
+ rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 50 });
281
+ removed.push(path);
282
+ }
283
+ if (readdirSync(folder).length === 0) {
284
+ rmdirSync(folder);
285
+ removed.push(folder);
286
+ }
287
+ }
288
+ catch {
289
+ // Best-effort housekeeping; the next sweep retries.
290
+ }
291
+ }
292
+ return removed;
293
+ },
294
+ };
295
+ }
222
296
  /** Build the `/telegram-connect` recovery boundary around runtime artifacts. */
223
297
  export function createTelegramPollingStartRecoveryHandler(deps) {
224
298
  return async () => {
@@ -257,10 +331,7 @@ export function createTelegramPollingStartRecoveryHandler(deps) {
257
331
  });
258
332
  }
259
333
  try {
260
- const recovery = recoverTelegramRuntimeState({
261
- ownersPath,
262
- statePaths,
263
- });
334
+ const recovery = recoverTelegramRuntimeState({ ownersPath, statePaths });
264
335
  if (recovery.kind === "blocked-live-owner") {
265
336
  return {
266
337
  kind: "blocked",
@@ -117,10 +117,6 @@ export declare function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
117
117
  }, options?: TelegramReplyTargetOptions & {
118
118
  replyMarkup?: TReplyMarkup;
119
119
  }): Promise<number | undefined>;
120
- export declare function sendTelegramNativeRichMessage(chatId: number, richMessage: TelegramInputRichMessage, deps: {
121
- recordOwnership?: TelegramReplyOwnershipRecorder["record"];
122
- sendRichMessage: (body: TelegramSendRichMessageBody) => Promise<TelegramSentMessage>;
123
- }, options?: TelegramReplyTargetOptions): Promise<number>;
124
120
  export type TelegramAssistantRenderingMode = "rich" | "html";
125
121
  export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
126
122
  renderTelegramMessage: (text: string, options?: {
@@ -498,7 +498,7 @@ export async function sendTelegramNativeMarkdownReply(chatId, replyToMessageId,
498
498
  }
499
499
  return lastMessageId;
500
500
  }
501
- export async function sendTelegramNativeRichMessage(chatId, richMessage, deps, options) {
501
+ async function sendTelegramNativeRichMessage(chatId, richMessage, deps, options) {
502
502
  const sent = await deps.sendRichMessage({
503
503
  chat_id: chatId,
504
504
  rich_message: richMessage,
@@ -7,6 +7,7 @@ import * as Bus from "./bus.ts";
7
7
  import * as Commands from "./commands.ts";
8
8
  import type { TelegramConfigStore } from "./config.ts";
9
9
  import type { TelegramInboundHandlerRuntime } from "./inbound.ts";
10
+ import type { TelegramUpdateJournalQueuedCompletion, TelegramUpdateJournalQueuedReceiptEvidence } from "./journal.ts";
10
11
  import * as Media from "./media.ts";
11
12
  import * as Menu from "./menu.ts";
12
13
  import * as Model from "./model.ts";
@@ -15,11 +16,12 @@ import * as PromptTemplates from "./prompt-templates.ts";
15
16
  import * as Queue from "./queue.ts";
16
17
  import type { TelegramBridgeRuntime } from "./runtime.ts";
17
18
  import type { TelegramSectionRegistry } from "./sections.ts";
18
- import type { TelegramInputRichMessage } from "./telegram-api.ts";
19
+ import type { TelegramApiCallOptions, TelegramInputRichMessage } from "./telegram-api.ts";
19
20
  import * as TextGroups from "./text-groups.ts";
20
21
  import * as ThreadReconciler from "./thread-reconciler.ts";
21
22
  import type { TelegramInstanceThreadIdentityCandidate, TelegramTopicTargetRecord } from "./threads.ts";
22
23
  import * as Turns from "./turns.ts";
24
+ import type { createTelegramWorkspaceExternalProtectionCapture } from "./workspace-retirement.ts";
23
25
  interface TelegramPromptPeerView {
24
26
  id?: unknown;
25
27
  is_bot?: unknown;
@@ -37,9 +39,37 @@ export declare function resolveTelegramGuestPromptPeer(input: {
37
39
  guestBotCallerChat?: TelegramPromptPeerView;
38
40
  ownerUserId?: number;
39
41
  }): string | undefined;
42
+ /** Stable file scope of the remote Guest Mode peer: username, else numeric id; never the bot's own scope. */
43
+ export declare function resolveTelegramGuestFileScope(input: Parameters<typeof resolveTelegramGuestPromptPeer>[0]): string;
40
44
  import * as Threads from "./threads.ts";
41
45
  import * as Updates from "./updates.ts";
46
+ /** One admitted Restore attempt through recipient readiness, never source dispatch or cleanup. */
47
+ export declare function advanceTelegramWorkspaceRestore(input: {
48
+ request: Threads.TelegramWorkspaceRestoreRequest;
49
+ authority: Threads.TelegramWorkspaceRestoreAuthority;
50
+ restoreStore: Threads.TelegramWorkspaceRestore;
51
+ getRecipient: () => Threads.TelegramWorkspaceRestoreRecipient | undefined;
52
+ /** Adapter proves current canonical ownership and authenticates observations; apply needs a fresh issuance. */
53
+ runRecipient: (input: {
54
+ intent: Threads.TelegramWorkspaceRestoreIntent;
55
+ mode: "apply" | "inspect";
56
+ isCurrent: () => boolean;
57
+ }) => Promise<{
58
+ operationId: string;
59
+ recipient: Threads.TelegramWorkspaceRestoreRecipient;
60
+ target: Threads.TelegramWorkspaceRestoreRequest["target"];
61
+ slot: string;
62
+ ready: boolean;
63
+ } | undefined>;
64
+ /**
65
+ * Recovery for a same-session successor already on the relocated target: it may receive the first grant, but only
66
+ * read-only inspection runs. It never commits a new relocation or applies a target.
67
+ */
68
+ inspectOnly?: true;
69
+ }): Promise<Threads.TelegramWorkspaceRestoreIntent | undefined>;
42
70
  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.";
43
73
  export declare function isTelegramAllTabCommandExpired(message: {
44
74
  date?: number;
45
75
  message_thread_id?: number;
@@ -66,7 +96,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
66
96
  configStore: Pick<TelegramConfigStore, "get" | "getAllowedUserId" | "persistAllowedUserId" | "persist"> & {
67
97
  set?: TelegramConfigStore["set"];
68
98
  };
69
- callApi?: <TResponse>(method: string, body: Record<string, unknown>) => Promise<TResponse>;
99
+ callApi?: <TResponse>(method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<TResponse>;
70
100
  getCurrentInstanceId?: () => string | undefined;
71
101
  getAdmissionScope?: () => string | undefined;
72
102
  getAdmissionJournalBinding?: () => string | undefined;
@@ -87,11 +117,33 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
87
117
  handleTelegramTopicLifecycleUpdate?: (lifecycle: Updates.TelegramTopicLifecycleUpdate<TMessage>, ctx: TContext) => Promise<void> | void;
88
118
  handleTelegramThreadTargetObserved?: (target: Threads.TelegramTopicTargetRecord["target"], ctx: TContext) => Promise<void> | void;
89
119
  foreignOwnedUpdateForwarder?: Updates.TelegramForeignOwnedUpdateForwarder<TContext, Updates.TelegramMessageReactionUpdated, TCallbackQuery, TMessage>;
90
- replaceFollowerThreadTarget?: (input: {
91
- record: Threads.TelegramTopicTargetRecord;
92
- target: Threads.TelegramTopicTargetRecord["target"];
93
- oldTarget: Threads.TelegramTopicTargetRecord["target"];
94
- }) => Promise<boolean>;
120
+ getWorkspaceRestoreStore?: () => Threads.TelegramWorkspaceRestore | undefined;
121
+ captureWorkspaceExternalProtection?: ReturnType<typeof createTelegramWorkspaceExternalProtectionCapture>;
122
+ /** Strict committed abandonment plus retained original; shared by Restore and temporary-input cancellation. */
123
+ inspectRestoreSourceAbandonment?: (updateId: number, journalBindingKey: string) => Threads.TelegramTemporaryThreadCancellationEvidence | undefined;
124
+ inspectRoutingInputGroupExpiry?: (input: Threads.TelegramTemporaryThreadInput) => readonly Threads.TelegramTemporaryThreadCancellationEvidence[] | undefined;
125
+ /** Strict active-journal observation only; a hint or missing source never substitutes for this ACK. */
126
+ inspectRestoreSourceCompletion?: (expected: Updates.TelegramDeferredSourceEvidence & {
127
+ completionSha256: string;
128
+ }) => (Updates.TelegramDeferredSourceEvidence & {
129
+ completionSha256: string;
130
+ }) | undefined;
131
+ inspectRestoreQueuedReceipt?: (expected: TelegramUpdateJournalQueuedCompletion & {
132
+ journalBindingKey: string;
133
+ }) => TelegramUpdateJournalQueuedReceiptEvidence | undefined;
134
+ hasWorkspaceRestoreAuthority?: () => boolean;
135
+ /** 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;
137
+ /** Quiet period after the last cancelled input before one cleanup attempt; defaults to 1000 ms. */
138
+ temporaryThreadCleanupDelayMs?: number;
139
+ getSessionGeneration?: () => number;
140
+ workspaceRestoreRecipient?: {
141
+ getSessionId: (ctx: TContext) => string | undefined;
142
+ getCwd: (ctx: TContext) => string | undefined;
143
+ getLeaderIdentity: Threads.TelegramLeaderThreadStateRuntime["getIdentity"];
144
+ followerRegistry: Pick<Bus.TelegramBusFollowerRegistry, "get" | "register">;
145
+ runFollower: ReturnType<typeof Bus.createTelegramBusWorkspaceRestoreController>;
146
+ };
95
147
  bridgeRuntime: TelegramBridgeRuntime;
96
148
  activeTurnRuntime: Queue.TelegramActiveTurnStore;
97
149
  mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage, TContext>;
@@ -183,7 +235,21 @@ export declare function createTelegramInboundRouteRuntime<TUpdate extends Update
183
235
  message?: TMessage;
184
236
  edited_message?: TMessage;
185
237
  callback_query?: TCallbackQuery;
186
- }, TMessage extends TelegramRoutedMessage, TCallbackQuery extends TelegramRoutedCallbackQuery, TContext, TModel extends Model.MenuModel>(deps: TelegramInboundRouteRuntimeDeps<TMessage, TCallbackQuery, TContext, TModel>): Updates.TelegramUpdateRuntimeController<TContext, TUpdate>;
238
+ }, TMessage extends TelegramRoutedMessage, TCallbackQuery extends TelegramRoutedCallbackQuery, TContext, TModel extends Model.MenuModel>(deps: TelegramInboundRouteRuntimeDeps<TMessage, TCallbackQuery, TContext, TModel>): Updates.TelegramUpdateRuntimeController<TContext, TUpdate> & {
239
+ expireRoutingInput: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["expireRoutingInput"]>;
240
+ shouldReviewHistoricalInput: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["shouldReviewHistoricalInput"]>;
241
+ shouldHoldPendingInput: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["shouldHoldPendingInput"]>;
242
+ forgetPreviousWorld(input: Updates.TelegramHeldSourcePreparation<TContext>, captureTransport?: (ctx: TContext) => (() => boolean) | undefined): Promise<{
243
+ forgotten: number;
244
+ deleted: number;
245
+ }>;
246
+ beforeQueueReceiptPublished: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["beforeQueueReceiptPublished"]>;
247
+ onQueueReceiptCommitted: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["onQueueReceiptCommitted"]>;
248
+ onQueueReceiptCompleted: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["onQueueReceiptCompleted"]>;
249
+ onUpdateCompleted: NonNullable<Updates.TelegramUpdateWorkerRuntimeDeps<TContext>["onUpdateCompleted"]>;
250
+ onWorkspaceRestoreRecipientObserved(follower: Bus.TelegramBusFollowerView, isCurrent: () => boolean, ctx: TContext | undefined): Promise<void> | undefined;
251
+ waitForRestoreSettlement(): Promise<void>;
252
+ };
187
253
  export interface TelegramAssistantOutputAuthority<TTransportStamp> {
188
254
  transportStamp: TTransportStamp;
189
255
  route: "direct" | "follower" | "none";