@llblab/pi-telegram 0.46.0 → 0.47.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 (199) hide show
  1. package/BACKLOG.md +0 -1
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +2 -2
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +4 -2
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +7 -3
  183. package/lib/bus-leader.ts +44 -19
  184. package/lib/bus.ts +4 -1
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +19 -0
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/telegram-api.ts +12 -1
  193. package/lib/thread-display.ts +96 -18
  194. package/package.json +56 -13
  195. package/scripts/build-dist.mjs +44 -0
  196. package/scripts/measure-bus.mjs +8 -1
  197. package/scripts/measure-workspace.mjs +8 -1
  198. package/skills/generative-apps/SKILL.md +1 -1
  199. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -0,0 +1,1965 @@
1
+ /**
2
+ * Telegram queue core contracts and pure planning helpers
3
+ * Zones: telegram queue, pi agent lifecycle, scheduling
4
+ * Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
5
+ */
6
+ import { createHash } from "node:crypto";
7
+ import { isVoiceTurn } from "./voice.js";
8
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
9
+ export const TELEGRAM_QUEUE_LANE_CONTRACTS = [
10
+ // Control lane intentionally accepts both direct controls and resume prompts.
11
+ // Model-switch continuations need prompt semantics but must run before queued user work.
12
+ // Do not admit ordinary user prompts here without an explicit control-flow reason.
13
+ {
14
+ lane: "control",
15
+ admissionMode: "control-queue",
16
+ dispatchRank: 0,
17
+ allowedKinds: ["control", "prompt"],
18
+ },
19
+ {
20
+ lane: "priority",
21
+ admissionMode: "priority-queue",
22
+ dispatchRank: 1,
23
+ allowedKinds: ["prompt"],
24
+ },
25
+ {
26
+ lane: "default",
27
+ admissionMode: "default-queue",
28
+ dispatchRank: 2,
29
+ allowedKinds: ["prompt"],
30
+ },
31
+ ];
32
+ export const TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES = 8 * 1024 * 1024;
33
+ export const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
34
+ export function createTelegramQueueAdmissionReceipt(options) {
35
+ if (options.sourceUpdateIds.length === 0)
36
+ return undefined;
37
+ const scope = options.scope.trim();
38
+ if (!scope) {
39
+ throw new Error("Telegram queue admission receipt scope is required.");
40
+ }
41
+ if (options.queueKind !== "prompt" && options.queueKind !== "control") {
42
+ throw new Error("Telegram queue admission receipt kind is invalid.");
43
+ }
44
+ const sourceUpdateIds = [...new Set(options.sourceUpdateIds)].sort((left, right) => left - right);
45
+ if (sourceUpdateIds.some((updateId) => !Number.isSafeInteger(updateId) || updateId < 0)) {
46
+ throw new Error("Telegram queue admission update ids must be safe integers.");
47
+ }
48
+ const digest = createHash("sha256")
49
+ .update(JSON.stringify({
50
+ scope,
51
+ queueKind: options.queueKind,
52
+ sourceUpdateIds,
53
+ }))
54
+ .digest("hex");
55
+ return {
56
+ queueKind: options.queueKind,
57
+ receiptId: `telegram-${options.queueKind}-v1-${digest}`,
58
+ sourceUpdateIds,
59
+ journalBindingKey: scope,
60
+ };
61
+ }
62
+ export function isTelegramQueueItemDurablyAdmitted(item, isReceiptCommitted) {
63
+ assertTelegramQueueItemAdmissionValid(item);
64
+ return (item.admissionReceipts ?? []).every(isReceiptCommitted);
65
+ }
66
+ export function getTelegramQueueLaneContract(lane) {
67
+ const contract = TELEGRAM_QUEUE_LANE_CONTRACTS.find((entry) => entry.lane === lane);
68
+ if (!contract)
69
+ throw new Error(`Unknown Telegram queue lane: ${lane}`);
70
+ return contract;
71
+ }
72
+ export function getTelegramQueueItemAdmissionMode(item) {
73
+ return getTelegramQueueLaneContract(item.queueLane).admissionMode;
74
+ }
75
+ export function isTelegramQueueItemAdmissionValid(item) {
76
+ return getTelegramQueueLaneContract(item.queueLane).allowedKinds.includes(item.kind);
77
+ }
78
+ export function assertTelegramQueueItemAdmissionValid(item) {
79
+ if (!isTelegramQueueItemAdmissionValid(item)) {
80
+ throw new Error(`Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`);
81
+ }
82
+ const receiptIds = new Set();
83
+ for (const receipt of item.admissionReceipts ?? []) {
84
+ const sourceUpdateIds = new Set(receipt.sourceUpdateIds);
85
+ if (receipt.queueKind !== item.kind ||
86
+ !receipt.receiptId ||
87
+ receiptIds.has(receipt.receiptId) ||
88
+ receipt.sourceUpdateIds.length === 0 ||
89
+ (receipt.journalBindingKey !== undefined &&
90
+ receipt.journalBindingKey.trim().length === 0) ||
91
+ sourceUpdateIds.size !== receipt.sourceUpdateIds.length ||
92
+ receipt.sourceUpdateIds.some((updateId, index) => !Number.isSafeInteger(updateId) ||
93
+ updateId < 0 ||
94
+ (index > 0 && updateId <= receipt.sourceUpdateIds[index - 1]))) {
95
+ throw new Error(`Invalid Telegram queue receipt for ${item.kind} item`);
96
+ }
97
+ receiptIds.add(receipt.receiptId);
98
+ }
99
+ }
100
+ function getTelegramQueueLaneRank(lane) {
101
+ return getTelegramQueueLaneContract(lane).dispatchRank;
102
+ }
103
+ export function isPendingTelegramTurn(item) {
104
+ return item.kind === "prompt";
105
+ }
106
+ export function createTelegramQueueStore(initialItems = []) {
107
+ let queuedItems = initialItems;
108
+ return {
109
+ getQueuedItems: () => queuedItems,
110
+ setQueuedItems: (items) => {
111
+ queuedItems = items;
112
+ },
113
+ hasQueuedItems: () => queuedItems.length > 0,
114
+ };
115
+ }
116
+ export function createTelegramTransportStampRuntime(deps) {
117
+ let profile;
118
+ let botToken;
119
+ let generation = 0;
120
+ const getStamp = function () {
121
+ const nextProfile = deps.getProfileName() ?? "default";
122
+ const nextBotToken = deps.getBotToken();
123
+ if (nextProfile !== profile || nextBotToken !== botToken) {
124
+ profile = nextProfile;
125
+ botToken = nextBotToken;
126
+ generation += 1;
127
+ }
128
+ return { profile: nextProfile, generation: String(generation) };
129
+ };
130
+ return {
131
+ getStamp,
132
+ isActive(stamp) {
133
+ if (!stamp)
134
+ return false;
135
+ const current = getStamp();
136
+ return (stamp.profile === current.profile &&
137
+ stamp.generation === current.generation);
138
+ },
139
+ };
140
+ }
141
+ export function createTelegramTransportStampedQueueStore(store, getTransportStamp) {
142
+ return {
143
+ getQueuedItems: store.getQueuedItems,
144
+ hasQueuedItems: store.hasQueuedItems,
145
+ setQueuedItems(items) {
146
+ const stamp = getTransportStamp();
147
+ store.setQueuedItems(items.map((item) => item.transportStamp ? item : { ...item, transportStamp: stamp }));
148
+ },
149
+ };
150
+ }
151
+ export function isTelegramQueueItemSkipped(item) {
152
+ return item.kind === "prompt" && Boolean(item.reactionSuppressionEmoji);
153
+ }
154
+ export function countExecutableTelegramQueueItems(items) {
155
+ return items.filter((item) => !isTelegramQueueItemSkipped(item)).length;
156
+ }
157
+ export function createTelegramQueueItemCountGetter(store) {
158
+ return () => countExecutableTelegramQueueItems(store.getQueuedItems());
159
+ }
160
+ export function createTelegramActiveTurnStore() {
161
+ let activeTurn;
162
+ return {
163
+ get: () => activeTurn,
164
+ has: () => !!activeTurn,
165
+ set: (turn) => {
166
+ activeTurn = { ...turn };
167
+ },
168
+ clear: () => {
169
+ activeTurn = undefined;
170
+ },
171
+ getChatId: () => activeTurn?.chatId,
172
+ getTarget: () => activeTurn?.target ? { ...activeTurn.target } : undefined,
173
+ getReplyToMessageId: () => activeTurn?.replyToMessageId,
174
+ getGuestQueryId: () => activeTurn?.guestQueryId,
175
+ getSourceMessageIds: () => activeTurn?.sourceMessageIds,
176
+ };
177
+ }
178
+ // --- Queue Mutations ---
179
+ export function partitionTelegramQueueItemsForHistory(items) {
180
+ const historyTurns = [];
181
+ const remainingItems = [];
182
+ for (const item of items) {
183
+ if (isPendingTelegramTurn(item)) {
184
+ historyTurns.push(item);
185
+ continue;
186
+ }
187
+ remainingItems.push(item);
188
+ }
189
+ return { historyTurns, remainingItems };
190
+ }
191
+ export function planTelegramPromptEnqueue(items, foldQueuedPromptsIntoHistory) {
192
+ if (!foldQueuedPromptsIntoHistory) {
193
+ return { historyTurns: [], remainingItems: items };
194
+ }
195
+ return partitionTelegramQueueItemsForHistory(items);
196
+ }
197
+ export function areTelegramQueueAdmissionReceiptsEqual(left, right) {
198
+ return (left.queueKind === right.queueKind &&
199
+ left.receiptId === right.receiptId &&
200
+ left.journalBindingKey === right.journalBindingKey &&
201
+ left.sourceUpdateIds.length === right.sourceUpdateIds.length &&
202
+ left.sourceUpdateIds.every((updateId, index) => updateId === right.sourceUpdateIds[index]));
203
+ }
204
+ function getTelegramQueueAdmissionReceiptKey(receipt) {
205
+ return JSON.stringify([
206
+ receipt.receiptId,
207
+ receipt.journalBindingKey ?? null,
208
+ ]);
209
+ }
210
+ function isDuplicateTelegramQueueAdmission(items, item) {
211
+ const incomingReceipts = item.admissionReceipts ?? [];
212
+ if (incomingReceipts.length === 0)
213
+ return false;
214
+ const queuedReceipts = new Map();
215
+ for (const queuedItem of items) {
216
+ for (const receipt of queuedItem.admissionReceipts ?? []) {
217
+ const key = getTelegramQueueAdmissionReceiptKey(receipt);
218
+ const existing = queuedReceipts.get(key);
219
+ if (existing)
220
+ existing.count += 1;
221
+ else
222
+ queuedReceipts.set(key, { receipt, count: 1 });
223
+ }
224
+ }
225
+ let duplicateCount = 0;
226
+ for (const incoming of incomingReceipts) {
227
+ const match = queuedReceipts.get(getTelegramQueueAdmissionReceiptKey(incoming));
228
+ if (!match)
229
+ continue;
230
+ if (match.count !== 1 ||
231
+ !areTelegramQueueAdmissionReceiptsEqual(match.receipt, incoming)) {
232
+ throw new Error(`Conflicting Telegram queue receipt: ${incoming.receiptId}`);
233
+ }
234
+ duplicateCount += 1;
235
+ }
236
+ if (duplicateCount === 0)
237
+ return false;
238
+ if (duplicateCount === incomingReceipts.length)
239
+ return true;
240
+ throw new Error("Telegram queue item overlaps an existing receipt.");
241
+ }
242
+ export function appendTelegramQueueItem(items, item) {
243
+ assertTelegramQueueItemAdmissionValid(item);
244
+ if (isDuplicateTelegramQueueAdmission(items, item))
245
+ return items;
246
+ return [...items, item];
247
+ }
248
+ export function createTelegramQueueHandoff(input) {
249
+ if (!input.handoffToken) {
250
+ throw new Error("Telegram queue handoff token is required.");
251
+ }
252
+ const handoff = {
253
+ handoffToken: input.handoffToken,
254
+ payload: createTelegramQueueHandoffPayload(input.item),
255
+ };
256
+ if (Buffer.byteLength(JSON.stringify(handoff)) > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) {
257
+ throw new Error("Telegram queue handoff payload exceeds its byte limit.");
258
+ }
259
+ return handoff;
260
+ }
261
+ export function createTelegramQueueHandoffPayload(item) {
262
+ assertTelegramQueueItemAdmissionValid(item);
263
+ if (!item.admissionReceipts?.length) {
264
+ throw new Error("Telegram queue handoff requires durable admission receipts.");
265
+ }
266
+ if (item.admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS) {
267
+ throw new Error("Telegram queue handoff has too many admission receipts.");
268
+ }
269
+ if (item.kind === "control") {
270
+ return structuredClone({
271
+ kind: item.kind,
272
+ controlType: item.controlType,
273
+ chatId: item.chatId,
274
+ ...(item.target ? { target: item.target } : {}),
275
+ ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
276
+ replyToMessageId: item.replyToMessageId,
277
+ ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
278
+ ...(item.guestInlineMessageId
279
+ ? { guestInlineMessageId: item.guestInlineMessageId }
280
+ : {}),
281
+ queueOrder: item.queueOrder,
282
+ queueLane: item.queueLane,
283
+ laneOrder: item.laneOrder,
284
+ statusSummary: item.statusSummary,
285
+ admissionReceipts: item.admissionReceipts,
286
+ });
287
+ }
288
+ return structuredClone({
289
+ kind: item.kind,
290
+ chatId: item.chatId,
291
+ ...(item.target ? { target: item.target } : {}),
292
+ ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
293
+ replyToMessageId: item.replyToMessageId,
294
+ ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
295
+ ...(item.guestInlineMessageId
296
+ ? { guestInlineMessageId: item.guestInlineMessageId }
297
+ : {}),
298
+ queueOrder: item.queueOrder,
299
+ queueLane: item.queueLane,
300
+ laneOrder: item.laneOrder,
301
+ statusSummary: item.statusSummary,
302
+ admissionReceipts: item.admissionReceipts,
303
+ sourceMessageIds: item.sourceMessageIds,
304
+ queuedAttachments: item.queuedAttachments,
305
+ content: item.content,
306
+ historyText: item.historyText,
307
+ ...(item.priorityEmoji ? { priorityEmoji: item.priorityEmoji } : {}),
308
+ ...(item.reactionSuppressionEmoji
309
+ ? { reactionSuppressionEmoji: item.reactionSuppressionEmoji }
310
+ : {}),
311
+ ...(item.voiceReplyPreferred !== undefined
312
+ ? { voiceReplyPreferred: item.voiceReplyPreferred }
313
+ : {}),
314
+ ...(item.voiceReplyRequired !== undefined
315
+ ? { voiceReplyRequired: item.voiceReplyRequired }
316
+ : {}),
317
+ });
318
+ }
319
+ export function restoreTelegramQueueHandoffPayload(payload, createControlExecution) {
320
+ const item = payload.kind === "prompt"
321
+ ? structuredClone(payload)
322
+ : {
323
+ ...structuredClone(payload),
324
+ execute: createControlExecution(payload),
325
+ };
326
+ assertTelegramQueueItemAdmissionValid(item);
327
+ return item;
328
+ }
329
+ function getTelegramQueueHandoffReceipt(payload) {
330
+ const receipt = payload.admissionReceipts[0];
331
+ if (!receipt || payload.admissionReceipts.length !== 1) {
332
+ throw new Error("Telegram queue handoff requires exactly one complete receipt.");
333
+ }
334
+ return receipt;
335
+ }
336
+ function findTelegramQueueReceiptItem(items, receipt) {
337
+ const receiptKey = getTelegramQueueAdmissionReceiptKey(receipt);
338
+ let match;
339
+ let matchedReceipt;
340
+ for (const item of items) {
341
+ for (const candidate of item.admissionReceipts ?? []) {
342
+ if (getTelegramQueueAdmissionReceiptKey(candidate) !== receiptKey) {
343
+ continue;
344
+ }
345
+ if (match) {
346
+ throw new Error(`Conflicting Telegram queue receipt: ${receipt.receiptId}`);
347
+ }
348
+ match = item;
349
+ matchedReceipt = candidate;
350
+ }
351
+ }
352
+ if (!match)
353
+ return undefined;
354
+ if (!matchedReceipt ||
355
+ !areTelegramQueueAdmissionReceiptsEqual(matchedReceipt, receipt)) {
356
+ throw new Error(`Conflicting Telegram queue receipt: ${receipt.receiptId}`);
357
+ }
358
+ return match;
359
+ }
360
+ export function removeTelegramQueueItemByReceipt(input) {
361
+ const current = input.store.getQueuedItems();
362
+ const item = findTelegramQueueReceiptItem(current, input.receipt);
363
+ if (!item)
364
+ return false;
365
+ input.store.setQueuedItems(current.filter((candidate) => candidate !== item));
366
+ return true;
367
+ }
368
+ export function stageTelegramQueueHandoffPayload(input) {
369
+ const receipt = getTelegramQueueHandoffReceipt(input.payload);
370
+ const item = restoreTelegramQueueHandoffPayload(input.payload, input.createControlExecution);
371
+ const current = input.store.getQueuedItems();
372
+ const next = appendTelegramQueueItem(current, item);
373
+ if (next !== current)
374
+ input.store.setQueuedItems(next);
375
+ return {
376
+ status: "staged",
377
+ receiptId: receipt.receiptId,
378
+ sourceUpdateIds: [...receipt.sourceUpdateIds],
379
+ };
380
+ }
381
+ export function createTelegramQueueHandoffStagingRuntime(input) {
382
+ const stagedStore = createTelegramQueueStore();
383
+ return {
384
+ stage(payload) {
385
+ const receipt = getTelegramQueueHandoffReceipt(payload);
386
+ const liveItem = findTelegramQueueReceiptItem(input.liveStore.getQueuedItems(), receipt);
387
+ if (liveItem) {
388
+ throw new Error(`Telegram queue handoff receipt ${receipt.receiptId} is already live.`);
389
+ }
390
+ return stageTelegramQueueHandoffPayload({
391
+ payload,
392
+ store: stagedStore,
393
+ createControlExecution: input.createControlExecution,
394
+ });
395
+ },
396
+ accept(receipt) {
397
+ const stagedItems = stagedStore.getQueuedItems();
398
+ const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
399
+ if (!stagedItem) {
400
+ return Boolean(findTelegramQueueReceiptItem(input.liveStore.getQueuedItems(), receipt));
401
+ }
402
+ const current = input.liveStore.getQueuedItems();
403
+ const next = appendTelegramQueueItem(current, stagedItem);
404
+ if (next !== current)
405
+ input.liveStore.setQueuedItems(next);
406
+ stagedStore.setQueuedItems(stagedItems.filter((candidate) => candidate !== stagedItem));
407
+ return true;
408
+ },
409
+ cancel(receipt) {
410
+ const stagedItems = stagedStore.getQueuedItems();
411
+ const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
412
+ if (!stagedItem)
413
+ return false;
414
+ stagedStore.setQueuedItems(stagedItems.filter((candidate) => candidate !== stagedItem));
415
+ return true;
416
+ },
417
+ hasStaged(receipt) {
418
+ return Boolean(findTelegramQueueReceiptItem(stagedStore.getQueuedItems(), receipt));
419
+ },
420
+ };
421
+ }
422
+ function getTelegramPromptTextSignature(item) {
423
+ return item.content
424
+ .filter((entry) => entry.type === "text")
425
+ .map((entry) => entry.text)
426
+ .join("\n");
427
+ }
428
+ function isDuplicateTelegramPromptTurn(left, right) {
429
+ return (left.chatId === right.chatId &&
430
+ left.target?.threadId === right.target?.threadId &&
431
+ left.replyToMessageId === right.replyToMessageId &&
432
+ getTelegramPromptTextSignature(left) ===
433
+ getTelegramPromptTextSignature(right));
434
+ }
435
+ export function appendTelegramPromptTurnOnce(items, turn) {
436
+ assertTelegramQueueItemAdmissionValid(turn);
437
+ if (isDuplicateTelegramQueueAdmission(items, turn)) {
438
+ return { items, appended: false };
439
+ }
440
+ const hasAdmissionReceipts = (turn.admissionReceipts?.length ?? 0) > 0;
441
+ const duplicate = !hasAdmissionReceipts &&
442
+ items.some((item) => isPendingTelegramTurn(item) &&
443
+ (item.admissionReceipts?.length ?? 0) === 0 &&
444
+ isDuplicateTelegramPromptTurn(item, turn));
445
+ if (duplicate)
446
+ return { items, appended: false };
447
+ return {
448
+ items: [...items, turn].sort(compareTelegramQueueItems),
449
+ appended: true,
450
+ };
451
+ }
452
+ export function compareTelegramQueueItems(left, right) {
453
+ assertTelegramQueueItemAdmissionValid(left);
454
+ assertTelegramQueueItemAdmissionValid(right);
455
+ const laneRankDelta = getTelegramQueueLaneRank(left.queueLane) -
456
+ getTelegramQueueLaneRank(right.queueLane);
457
+ if (laneRankDelta !== 0)
458
+ return laneRankDelta;
459
+ if (left.laneOrder !== right.laneOrder) {
460
+ return left.laneOrder - right.laneOrder;
461
+ }
462
+ return left.queueOrder - right.queueOrder;
463
+ }
464
+ function isTelegramQueueItemInMessageScope(item, scope) {
465
+ if (!scope)
466
+ return true;
467
+ if (typeof scope.chatId === "number" && item.chatId !== scope.chatId) {
468
+ return false;
469
+ }
470
+ if (typeof scope.threadId === "number") {
471
+ return item.target?.threadId === scope.threadId;
472
+ }
473
+ return true;
474
+ }
475
+ export function removeTelegramQueueItemsByMessageIds(items, messageIds, scope) {
476
+ if (messageIds.length === 0 || items.length === 0) {
477
+ return { items, removedItems: [], removedCount: 0 };
478
+ }
479
+ const deletedMessageIds = new Set(messageIds);
480
+ const nextItems = [];
481
+ const removedItems = [];
482
+ for (const item of items) {
483
+ const shouldRemove = isPendingTelegramTurn(item) &&
484
+ isTelegramQueueItemInMessageScope(item, scope) &&
485
+ item.sourceMessageIds.some((messageId) => deletedMessageIds.has(messageId));
486
+ (shouldRemove ? removedItems : nextItems).push(item);
487
+ }
488
+ return {
489
+ items: nextItems,
490
+ removedItems,
491
+ removedCount: removedItems.length,
492
+ };
493
+ }
494
+ export function applyTelegramQueuePromptReactionDisposition(items, messageId, disposition, destinationLaneOrder, scope) {
495
+ let nextItems = items;
496
+ for (const [index, item] of items.entries()) {
497
+ if (!isPendingTelegramTurn(item) ||
498
+ !isTelegramQueueItemInMessageScope(item, scope) ||
499
+ !item.sourceMessageIds.includes(messageId)) {
500
+ continue;
501
+ }
502
+ const isPriority = disposition.kind === "reaction-transition"
503
+ ? disposition.priorityEmoji === undefined
504
+ ? item.queueLane === "priority"
505
+ : disposition.priorityEmoji !== null
506
+ : disposition.kind === "priority" ||
507
+ disposition.kind === "priority-suppressed";
508
+ const queueLane = isPriority ? "priority" : "default";
509
+ const laneOrder = item.queueLane === queueLane
510
+ ? item.laneOrder
511
+ : destinationLaneOrder;
512
+ if (laneOrder === undefined) {
513
+ throw new Error("Telegram destination lane order is unavailable.");
514
+ }
515
+ const priorityEmoji = disposition.kind === "reaction-transition"
516
+ ? disposition.priorityEmoji === undefined
517
+ ? item.priorityEmoji
518
+ : disposition.priorityEmoji ?? undefined
519
+ : disposition.kind === "priority"
520
+ ? disposition.emoji
521
+ : disposition.kind === "priority-suppressed"
522
+ ? disposition.priorityEmoji
523
+ : undefined;
524
+ const reactionSuppressionEmoji = disposition.kind === "reaction-transition"
525
+ ? disposition.suppressionEmoji === undefined
526
+ ? item.reactionSuppressionEmoji
527
+ : disposition.suppressionEmoji ?? undefined
528
+ : disposition.kind === "suppressed"
529
+ ? disposition.emoji
530
+ : disposition.kind === "priority-suppressed"
531
+ ? disposition.suppressionEmoji
532
+ : undefined;
533
+ if (item.queueLane === queueLane &&
534
+ item.laneOrder === laneOrder &&
535
+ item.priorityEmoji === priorityEmoji &&
536
+ item.reactionSuppressionEmoji === reactionSuppressionEmoji) {
537
+ continue;
538
+ }
539
+ if (nextItems === items)
540
+ nextItems = [...items];
541
+ nextItems[index] = {
542
+ ...item,
543
+ queueLane,
544
+ laneOrder,
545
+ priorityEmoji,
546
+ reactionSuppressionEmoji,
547
+ };
548
+ }
549
+ return { items: nextItems, changed: nextItems !== items };
550
+ }
551
+ export function consumeDispatchedTelegramPrompt(items, hasPendingDispatch) {
552
+ if (!hasPendingDispatch) {
553
+ return { activeTurn: undefined, remainingItems: items };
554
+ }
555
+ const nextItem = items[0];
556
+ if (!nextItem || !isPendingTelegramTurn(nextItem)) {
557
+ return { activeTurn: undefined, remainingItems: items };
558
+ }
559
+ return { activeTurn: nextItem, remainingItems: items.slice(1) };
560
+ }
561
+ export function formatQueuedTelegramItemsStatus(items) {
562
+ const count = countExecutableTelegramQueueItems(items);
563
+ return count === 0 ? "" : ` +${count}`;
564
+ }
565
+ export function truncateTelegramQueueSummary(text, maxWords = 5, maxLength = 40) {
566
+ const normalized = text.replace(/\s+/g, " ").trim();
567
+ if (!normalized)
568
+ return "";
569
+ const words = normalized.split(" ");
570
+ let summary = words.slice(0, maxWords).join(" ");
571
+ if (summary.length === 0)
572
+ summary = normalized;
573
+ if (summary.length > maxLength) {
574
+ summary = summary.slice(0, maxLength).trimEnd();
575
+ }
576
+ return summary.length < normalized.length || words.length > maxWords
577
+ ? `${summary}…`
578
+ : summary;
579
+ }
580
+ export function canDispatchTelegramTurnState(state) {
581
+ return (!state.compactionInProgress &&
582
+ !state.hasActiveTelegramTurn &&
583
+ !state.hasPendingTelegramDispatch &&
584
+ state.isIdle &&
585
+ !state.hasPendingMessages);
586
+ }
587
+ export function createTelegramDispatchReadinessChecker(deps) {
588
+ return (ctx) => canDispatchTelegramTurnState({
589
+ compactionInProgress: deps.isCompactionInProgress(),
590
+ hasActiveTelegramTurn: deps.hasActiveTurn(),
591
+ hasPendingTelegramDispatch: deps.hasDispatchPending(),
592
+ isIdle: deps.isIdle(ctx),
593
+ hasPendingMessages: deps.hasPendingMessages(ctx),
594
+ });
595
+ }
596
+ export function buildPendingTelegramControlItem(options) {
597
+ return {
598
+ kind: "control",
599
+ controlType: options.controlType,
600
+ chatId: options.chatId,
601
+ ...(options.target ? { target: options.target } : {}),
602
+ replyToMessageId: options.replyToMessageId,
603
+ queueOrder: options.queueOrder,
604
+ queueLane: "control",
605
+ laneOrder: options.laneOrder,
606
+ statusSummary: options.statusSummary,
607
+ ...(options.admissionReceipts?.length
608
+ ? { admissionReceipts: structuredClone(options.admissionReceipts) }
609
+ : {}),
610
+ execute: options.execute,
611
+ };
612
+ }
613
+ export function createTelegramControlItemBuilder(deps) {
614
+ return (options) => buildPendingTelegramControlItem({
615
+ ...options,
616
+ queueOrder: deps.allocateItemOrder(),
617
+ laneOrder: deps.allocateControlOrder(),
618
+ });
619
+ }
620
+ export function planNextTelegramQueueAction(items, canDispatch) {
621
+ if (!canDispatch || items.length === 0) {
622
+ return { kind: "none", remainingItems: items };
623
+ }
624
+ const [firstItem, ...remainingItems] = items;
625
+ if (!firstItem) {
626
+ return { kind: "none", remainingItems: items };
627
+ }
628
+ assertTelegramQueueItemAdmissionValid(firstItem);
629
+ if (isPendingTelegramTurn(firstItem)) {
630
+ return { kind: "prompt", item: firstItem, remainingItems: items };
631
+ }
632
+ return { kind: "control", item: firstItem, remainingItems };
633
+ }
634
+ export function shouldDispatchAfterTelegramAgentEnd(options) {
635
+ if (!options.hasTurn)
636
+ return true;
637
+ if (options.stopReason === "aborted") {
638
+ return !options.foldQueuedPromptsIntoHistory;
639
+ }
640
+ return true;
641
+ }
642
+ export function buildTelegramAgentStartPlan(options) {
643
+ if (options.hasActiveTurn || !options.hasPendingDispatch) {
644
+ return {
645
+ activeTurn: undefined,
646
+ remainingItems: options.queuedItems,
647
+ shouldResetPendingModelSwitch: true,
648
+ shouldResetToolExecutions: true,
649
+ shouldClearDispatchPending: options.hasPendingDispatch,
650
+ shouldClearAbortHistory: !options.hasActiveTurn && !options.hasPendingDispatch,
651
+ };
652
+ }
653
+ const nextDispatch = consumeDispatchedTelegramPrompt(options.queuedItems, options.hasPendingDispatch);
654
+ return {
655
+ activeTurn: nextDispatch.activeTurn,
656
+ remainingItems: nextDispatch.remainingItems,
657
+ shouldResetPendingModelSwitch: true,
658
+ shouldResetToolExecutions: true,
659
+ shouldClearDispatchPending: options.hasPendingDispatch,
660
+ shouldClearAbortHistory: false,
661
+ };
662
+ }
663
+ export function handleTelegramAgentStartRuntime(deps) {
664
+ const startPlan = buildTelegramAgentStartPlan({
665
+ queuedItems: deps.queuedItems,
666
+ hasPendingDispatch: deps.hasPendingDispatch,
667
+ hasActiveTurn: deps.hasActiveTurn,
668
+ });
669
+ if (startPlan.shouldResetToolExecutions)
670
+ deps.resetToolExecutions();
671
+ if (startPlan.shouldResetPendingModelSwitch)
672
+ deps.resetPendingModelSwitch();
673
+ if (startPlan.shouldClearAbortHistory) {
674
+ deps.setFoldQueuedPromptsIntoHistory(false);
675
+ }
676
+ deps.setQueuedItems(startPlan.remainingItems);
677
+ if (startPlan.shouldClearDispatchPending)
678
+ deps.clearDispatchPending();
679
+ if (startPlan.activeTurn) {
680
+ const activeTurn = startPlan.activeTurn;
681
+ deps.setActiveTurn(activeTurn);
682
+ try {
683
+ deps.onPromptHandedOff?.(activeTurn);
684
+ }
685
+ catch (error) {
686
+ deps.recordRuntimeEvent?.("queue", error, {
687
+ phase: "prompt-handoff-receipt-settlement",
688
+ });
689
+ }
690
+ deps.createPreviewState();
691
+ deps.startTypingLoop();
692
+ }
693
+ deps.updateStatus();
694
+ }
695
+ export function createTelegramAgentStartHook(deps) {
696
+ return async (_event, ctx) => {
697
+ deps.setAbortHandler(ctx);
698
+ handleTelegramAgentStartRuntime({
699
+ queuedItems: deps.getQueuedItems(),
700
+ hasPendingDispatch: deps.hasPendingDispatch(),
701
+ hasActiveTurn: deps.hasActiveTurn(),
702
+ resetToolExecutions: deps.resetToolExecutions,
703
+ resetPendingModelSwitch: deps.resetPendingModelSwitch,
704
+ setQueuedItems: deps.setQueuedItems,
705
+ clearDispatchPending: deps.clearDispatchPending,
706
+ setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
707
+ setActiveTurn: deps.setActiveTurn,
708
+ onPromptHandedOff: (turn) => deps.onPromptHandedOff?.(turn, ctx),
709
+ createPreviewState: deps.createPreviewState,
710
+ recordRuntimeEvent: deps.recordRuntimeEvent,
711
+ startTypingLoop: () => deps.startTypingLoop(ctx),
712
+ updateStatus: () => deps.updateStatus(ctx),
713
+ });
714
+ };
715
+ }
716
+ export function getNextTelegramToolExecutionCount(options) {
717
+ if (!options.hasActiveTurn)
718
+ return options.currentCount;
719
+ if (options.event === "start") {
720
+ return options.currentCount + 1;
721
+ }
722
+ return Math.max(0, options.currentCount - 1);
723
+ }
724
+ export function handleTelegramToolExecutionStartRuntime(deps) {
725
+ deps.setActiveToolExecutions(getNextTelegramToolExecutionCount({
726
+ hasActiveTurn: deps.hasActiveTurn(),
727
+ currentCount: deps.getActiveToolExecutions(),
728
+ event: "start",
729
+ }));
730
+ }
731
+ export function handleTelegramToolExecutionEndRuntime(deps) {
732
+ const hasActiveTurn = deps.hasActiveTurn();
733
+ deps.setActiveToolExecutions(getNextTelegramToolExecutionCount({
734
+ hasActiveTurn,
735
+ currentCount: deps.getActiveToolExecutions(),
736
+ event: "end",
737
+ }));
738
+ if (hasActiveTurn)
739
+ deps.triggerPendingModelSwitchAbort();
740
+ }
741
+ export function createTelegramAgentLifecycleHooks(deps) {
742
+ const onAgentStart = createTelegramAgentStartHook(deps);
743
+ const deliverAgentEnd = createTelegramAgentEndHook(deps);
744
+ let retainedErrorEvent;
745
+ return {
746
+ onAgentStart,
747
+ async onAgentEnd(event, ctx) {
748
+ const turn = deps.getActiveTurn();
749
+ const assistant = turn ? deps.extractAssistant(event.messages) : {};
750
+ if (turn && assistant.stopReason === "error") {
751
+ retainedErrorEvent = event;
752
+ deps.recordRuntimeEvent?.("provider-retry", new Error("Retained Telegram turn after retryable agent error"), { phase: "retained", hasFinalText: !!assistant.text?.trim() });
753
+ return;
754
+ }
755
+ if (retainedErrorEvent) {
756
+ retainedErrorEvent = undefined;
757
+ deps.recordRuntimeEvent?.("provider-retry", new Error("Recovered retained Telegram turn after agent retry"), { phase: "recovered" });
758
+ }
759
+ await deliverAgentEnd(event, ctx, assistant);
760
+ },
761
+ async onAgentSettled(_event, ctx) {
762
+ const event = retainedErrorEvent;
763
+ if (!event)
764
+ return;
765
+ retainedErrorEvent = undefined;
766
+ deps.recordRuntimeEvent?.("provider-retry", new Error("Finalized retained Telegram turn after agent settled"), { phase: "settled-failure" });
767
+ await deliverAgentEnd(event, ctx);
768
+ },
769
+ clearRetainedAgentEnd() {
770
+ retainedErrorEvent = undefined;
771
+ },
772
+ ...createTelegramToolExecutionHooks(deps),
773
+ };
774
+ }
775
+ export function createTelegramToolExecutionHooks(deps) {
776
+ return {
777
+ onToolExecutionStart: () => {
778
+ handleTelegramToolExecutionStartRuntime(deps);
779
+ },
780
+ onToolExecutionEnd: (_event, ctx) => {
781
+ handleTelegramToolExecutionEndRuntime({
782
+ hasActiveTurn: deps.hasActiveTurn,
783
+ getActiveToolExecutions: deps.getActiveToolExecutions,
784
+ setActiveToolExecutions: deps.setActiveToolExecutions,
785
+ triggerPendingModelSwitchAbort: () => {
786
+ deps.triggerPendingModelSwitchAbort(ctx);
787
+ },
788
+ });
789
+ },
790
+ };
791
+ }
792
+ export function buildTelegramAgentEndPlan(options) {
793
+ const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
794
+ hasTurn: options.hasTurn,
795
+ stopReason: options.stopReason,
796
+ foldQueuedPromptsIntoHistory: options.foldQueuedPromptsIntoHistory,
797
+ });
798
+ if (!options.hasTurn) {
799
+ return {
800
+ kind: "no-turn",
801
+ shouldClearPreview: false,
802
+ shouldDispatchNext,
803
+ shouldSendErrorMessage: false,
804
+ shouldSendAttachmentNotice: false,
805
+ };
806
+ }
807
+ if (options.stopReason === "aborted") {
808
+ return {
809
+ kind: "aborted",
810
+ shouldClearPreview: true,
811
+ shouldDispatchNext,
812
+ shouldSendErrorMessage: false,
813
+ shouldSendAttachmentNotice: false,
814
+ };
815
+ }
816
+ if (options.stopReason === "error") {
817
+ return {
818
+ kind: "error",
819
+ shouldClearPreview: true,
820
+ shouldDispatchNext,
821
+ shouldSendErrorMessage: true,
822
+ shouldSendAttachmentNotice: false,
823
+ };
824
+ }
825
+ if (options.hasFinalText) {
826
+ return {
827
+ kind: "text",
828
+ shouldClearPreview: false,
829
+ shouldDispatchNext,
830
+ shouldSendErrorMessage: false,
831
+ shouldSendAttachmentNotice: false,
832
+ };
833
+ }
834
+ if (options.hasQueuedAttachments) {
835
+ return {
836
+ kind: "attachments-only",
837
+ shouldClearPreview: true,
838
+ shouldDispatchNext,
839
+ shouldSendErrorMessage: false,
840
+ shouldSendAttachmentNotice: true,
841
+ };
842
+ }
843
+ return {
844
+ kind: "empty",
845
+ shouldClearPreview: true,
846
+ shouldDispatchNext,
847
+ shouldSendErrorMessage: false,
848
+ shouldSendAttachmentNotice: false,
849
+ };
850
+ }
851
+ export function createTelegramAgentEndHook(deps) {
852
+ return async (event, ctx, assistantOverride) => {
853
+ if (deps.isSessionActive?.(ctx) === false)
854
+ return;
855
+ const turn = deps.getActiveTurn();
856
+ const assistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
857
+ const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
858
+ const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
859
+ const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
860
+ try {
861
+ await deps.loadConfig?.();
862
+ if (deps.isSessionActive?.(ctx) === false || deps.getActiveTurn() !== turn)
863
+ return;
864
+ await handleTelegramAgentEndRuntime({
865
+ turn,
866
+ assistant,
867
+ foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
868
+ resetRuntimeState: deps.resetRuntimeState,
869
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
870
+ isTurnTransportActive: deps.isTurnTransportActive,
871
+ waitForTypingIdle: deps.waitForTypingIdle,
872
+ waitForActivityIdle: deps.waitForActivityIdle,
873
+ updateStatus: () => deps.updateStatus(ctx),
874
+ dispatchNextQueuedTelegramTurn: () => {
875
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(deps.dispatchNextQueuedTelegramTurn);
876
+ },
877
+ scheduleActiveTurnDelivery: scheduleDelivery
878
+ ? (task) => scheduleDelivery(async () => {
879
+ if (deps.isSessionActive?.(ctx) === false)
880
+ return;
881
+ await task();
882
+ })
883
+ : undefined,
884
+ preparePreviewDelivery: deps.preparePreviewDelivery,
885
+ preparePreviewClear: deps.preparePreviewClear,
886
+ clearPreview: deps.clearPreview,
887
+ setPreviewPendingText: deps.setPreviewPendingText,
888
+ finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
889
+ sendMarkdownReply: deps.sendMarkdownReply,
890
+ sendTextReply: deps.sendTextReply,
891
+ sendQueuedAttachments: deps.sendQueuedAttachments,
892
+ sendRichAttachmentReply: deps.sendRichAttachmentReply,
893
+ answerGuestQuery: deps.answerGuestQuery,
894
+ sendGuestReply: deps.sendGuestReply,
895
+ editGuestReply: deps.editGuestReply,
896
+ stopGuestPlaceholder: deps.stopGuestPlaceholder,
897
+ sendGuestAttachment: deps.sendGuestAttachment,
898
+ sendGuestVoiceReply: deps.sendGuestVoiceReply,
899
+ planOutboundReply: deps.planOutboundReply,
900
+ sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
901
+ recordRuntimeEvent: deps.recordRuntimeEvent,
902
+ });
903
+ }
904
+ finally {
905
+ reservation?.cancel();
906
+ }
907
+ };
908
+ }
909
+ export async function handleTelegramAgentEndRuntime(deps) {
910
+ const { turn, assistant } = deps;
911
+ const rawFinalText = assistant.text;
912
+ let outboundReply = rawFinalText
913
+ ? deps.planOutboundReply?.(rawFinalText)
914
+ : undefined;
915
+ // Preserve the planned reply so voice-fallback can use stripped markdown + replyMarkup
916
+ const plannedReply = outboundReply;
917
+ // Transparent voice interception: when the turn is voice-tagged and the agent
918
+ // did not explicitly use <!-- telegram_voice --> markup, we automatically
919
+ // convert the whole response to voice.
920
+ const voiceInterceptionGuard = turn &&
921
+ isVoiceTurn(turn) &&
922
+ rawFinalText?.trim() &&
923
+ deps.planOutboundReply &&
924
+ (!outboundReply ||
925
+ (!outboundReply.voiceText && !outboundReply.voiceReplies?.length));
926
+ if (voiceInterceptionGuard) {
927
+ const voiceText = plannedReply !== undefined
928
+ ? plannedReply.markdown?.trim() || ""
929
+ : (rawFinalText ?? "");
930
+ outboundReply = outboundReply
931
+ ? { ...outboundReply, voiceText, markdown: "" }
932
+ : { markdown: "", voiceText };
933
+ }
934
+ const finalText = outboundReply ? outboundReply.markdown : rawFinalText;
935
+ const hasOutboundArtifacts = !!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
936
+ const replyMarkup = outboundReply?.replyMarkup;
937
+ const isDeliveryActive = () => deps.isSessionActive?.() !== false &&
938
+ (!turn || deps.isTurnTransportActive?.(turn) !== false);
939
+ const preview = turn && !turn.guestQueryId ? deps.preparePreviewDelivery?.(isDeliveryActive) : undefined;
940
+ const setPreviewPendingText = preview?.setPreviewPendingText ?? deps.setPreviewPendingText;
941
+ const finalizeMarkdownPreview = preview?.finalizeMarkdownPreview ?? deps.finalizeMarkdownPreview;
942
+ const clearPreview = turn
943
+ ? preview ? () => preview.clearPreview(turn.chatId, { target: turn.target })
944
+ : deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
945
+ ?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
946
+ : undefined;
947
+ const updateStatusIgnoringStaleContext = () => {
948
+ try {
949
+ deps.updateStatus();
950
+ }
951
+ catch (error) {
952
+ if (!isTelegramStaleContextError(error))
953
+ throw error;
954
+ }
955
+ };
956
+ if (!isDeliveryActive()) {
957
+ deps.resetRuntimeState();
958
+ updateStatusIgnoringStaleContext();
959
+ deps.dispatchNextQueuedTelegramTurn();
960
+ return;
961
+ }
962
+ deps.resetRuntimeState();
963
+ await deps.waitForTypingIdle?.();
964
+ if (!isDeliveryActive()) {
965
+ updateStatusIgnoringStaleContext();
966
+ deps.dispatchNextQueuedTelegramTurn();
967
+ return;
968
+ }
969
+ updateStatusIgnoringStaleContext();
970
+ const endPlan = buildTelegramAgentEndPlan({
971
+ hasTurn: !!turn,
972
+ stopReason: assistant.stopReason,
973
+ hasFinalText: !!finalText || hasOutboundArtifacts,
974
+ hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
975
+ foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
976
+ });
977
+ if (!turn) {
978
+ if (endPlan.shouldDispatchNext)
979
+ deps.dispatchNextQueuedTelegramTurn();
980
+ return;
981
+ }
982
+ if (turn.guestQueryId) {
983
+ if (turn.guestInlineMessageId && deps.stopGuestPlaceholder) {
984
+ try {
985
+ await deps.stopGuestPlaceholder(turn.guestInlineMessageId);
986
+ }
987
+ catch (error) {
988
+ deps.recordRuntimeEvent?.("delivery", error, {
989
+ phase: "guest-placeholder-stop",
990
+ guestQueryId: turn.guestQueryId,
991
+ });
992
+ }
993
+ }
994
+ if (turn.guestInlineMessageId && deps.editGuestReply) {
995
+ const experimentText = assistant.errorMessage
996
+ ? "Telegram bridge: Pi failed while processing the request."
997
+ : finalText;
998
+ if (experimentText) {
999
+ try {
1000
+ await deps.editGuestReply(turn.guestInlineMessageId, experimentText);
1001
+ deps.recordRuntimeEvent?.("guest", new Error("Guest ACK edited the guest answer"), { phase: "guest-ack-edited", guestQueryId: turn.guestQueryId });
1002
+ }
1003
+ catch (error) {
1004
+ deps.recordRuntimeEvent?.("delivery", error, {
1005
+ phase: "guest-ack-edit",
1006
+ guestQueryId: turn.guestQueryId,
1007
+ });
1008
+ }
1009
+ }
1010
+ else {
1011
+ deps.recordRuntimeEvent?.("delivery", new Error("Guest ACK turn produced no editable text"), { phase: "guest-ack-edit-empty", guestQueryId: turn.guestQueryId });
1012
+ }
1013
+ if (!isDeliveryActive())
1014
+ return;
1015
+ if (endPlan.shouldDispatchNext)
1016
+ deps.dispatchNextQueuedTelegramTurn();
1017
+ return;
1018
+ }
1019
+ if (assistant.errorMessage) {
1020
+ try {
1021
+ await deps.answerGuestQuery?.(turn.guestQueryId, "Telegram bridge: Pi failed while processing the request.");
1022
+ }
1023
+ catch (error) {
1024
+ deps.recordRuntimeEvent?.("delivery", error, {
1025
+ phase: "guest-error-reply",
1026
+ guestQueryId: turn.guestQueryId,
1027
+ });
1028
+ }
1029
+ if (endPlan.shouldDispatchNext)
1030
+ deps.dispatchNextQueuedTelegramTurn();
1031
+ return;
1032
+ }
1033
+ const [guestAttachment] = turn.queuedAttachments;
1034
+ if (guestAttachment && deps.sendGuestAttachment) {
1035
+ try {
1036
+ await deps.sendGuestAttachment(turn, guestAttachment, finalText || undefined);
1037
+ }
1038
+ catch (error) {
1039
+ deps.recordRuntimeEvent?.("delivery", error, {
1040
+ phase: "guest-attachment",
1041
+ guestQueryId: turn.guestQueryId,
1042
+ });
1043
+ }
1044
+ }
1045
+ else if (outboundReply &&
1046
+ (outboundReply.voiceText || outboundReply.voiceReplies?.length) &&
1047
+ deps.sendGuestVoiceReply) {
1048
+ try {
1049
+ await deps.sendGuestVoiceReply(turn, outboundReply, finalText || undefined);
1050
+ }
1051
+ catch (error) {
1052
+ deps.recordRuntimeEvent?.("delivery", error, {
1053
+ phase: "guest-voice",
1054
+ guestQueryId: turn.guestQueryId,
1055
+ });
1056
+ }
1057
+ }
1058
+ else if (finalText) {
1059
+ try {
1060
+ if (deps.sendGuestReply) {
1061
+ await deps.sendGuestReply(turn.guestQueryId, finalText);
1062
+ }
1063
+ else {
1064
+ await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1065
+ }
1066
+ }
1067
+ catch (error) {
1068
+ // Guest queries expire after Telegram's response timeout, so a slow
1069
+ // turn can fail the only delivery attempt. Record and continue the
1070
+ // agent-end lifecycle instead of rejecting the extension hook.
1071
+ deps.recordRuntimeEvent?.("delivery", error, {
1072
+ phase: "guest-reply",
1073
+ guestQueryId: turn.guestQueryId,
1074
+ });
1075
+ }
1076
+ }
1077
+ if (!isDeliveryActive())
1078
+ return;
1079
+ if (endPlan.shouldDispatchNext)
1080
+ deps.dispatchNextQueuedTelegramTurn();
1081
+ return;
1082
+ }
1083
+ if (!isDeliveryActive())
1084
+ return;
1085
+ const deliverActiveTurn = async () => {
1086
+ await deps.waitForActivityIdle?.();
1087
+ if (!isDeliveryActive())
1088
+ return;
1089
+ let previewCleared = false;
1090
+ const clearTurnPreview = async () => {
1091
+ if (previewCleared)
1092
+ return;
1093
+ await clearPreview?.();
1094
+ previewCleared = true;
1095
+ };
1096
+ if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
1097
+ await clearTurnPreview();
1098
+ if (!isDeliveryActive())
1099
+ return;
1100
+ }
1101
+ if (endPlan.shouldSendErrorMessage) {
1102
+ const errorMessage = assistant.errorMessage ||
1103
+ "Telegram bridge: Pi failed while processing the request.";
1104
+ const isOperationAborted = errorMessage.trim().replace(/\.$/, "") ===
1105
+ "This operation was aborted";
1106
+ await deps.sendTextReply(turn.chatId, turn.replyToMessageId, isOperationAborted
1107
+ ? "<b>⏹️ This operation was aborted.</b>"
1108
+ : errorMessage, {
1109
+ target: turn.target,
1110
+ ...(isOperationAborted ? { parseMode: "HTML" } : {}),
1111
+ });
1112
+ if (!isDeliveryActive())
1113
+ return;
1114
+ if (endPlan.shouldDispatchNext)
1115
+ deps.dispatchNextQueuedTelegramTurn();
1116
+ return;
1117
+ }
1118
+ if (finalText && turn.queuedAttachments.length === 0)
1119
+ setPreviewPendingText(finalText);
1120
+ if (!isDeliveryActive())
1121
+ return;
1122
+ let richAttachmentDelivered = false;
1123
+ if (endPlan.kind === "text" &&
1124
+ finalText &&
1125
+ !hasOutboundArtifacts &&
1126
+ deps.sendRichAttachmentReply) {
1127
+ try {
1128
+ richAttachmentDelivered = await deps.sendRichAttachmentReply(turn, finalText, { replyMarkup, isDeliveryActive });
1129
+ if (!isDeliveryActive())
1130
+ return;
1131
+ if (richAttachmentDelivered) {
1132
+ await clearTurnPreview();
1133
+ if (!isDeliveryActive())
1134
+ return;
1135
+ setPreviewPendingText("");
1136
+ }
1137
+ }
1138
+ catch (error) {
1139
+ if (!isDeliveryActive())
1140
+ return;
1141
+ deps.recordRuntimeEvent?.("delivery", error, {
1142
+ phase: "rich-attachment-commit-unknown",
1143
+ chatId: turn.chatId,
1144
+ });
1145
+ if (endPlan.shouldDispatchNext)
1146
+ deps.dispatchNextQueuedTelegramTurn();
1147
+ return;
1148
+ }
1149
+ }
1150
+ if (!isDeliveryActive())
1151
+ return;
1152
+ let queuedAttachmentsDelivered = false;
1153
+ if (!richAttachmentDelivered && turn.queuedAttachments.length > 0) {
1154
+ await clearTurnPreview();
1155
+ if (!isDeliveryActive())
1156
+ return;
1157
+ setPreviewPendingText("");
1158
+ await deps.sendQueuedAttachments(turn, { isDeliveryActive });
1159
+ if (!isDeliveryActive())
1160
+ return;
1161
+ queuedAttachmentsDelivered = true;
1162
+ }
1163
+ if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
1164
+ try {
1165
+ if (queuedAttachmentsDelivered) {
1166
+ await deps.sendMarkdownReply(turn.chatId, turn.replyToMessageId, finalText, { replyMarkup, target: turn.target });
1167
+ }
1168
+ else {
1169
+ const finalized = await finalizeMarkdownPreview(turn.chatId, finalText, turn.replyToMessageId, { replyMarkup, target: turn.target });
1170
+ if (!isDeliveryActive())
1171
+ return;
1172
+ if (!finalized) {
1173
+ await clearTurnPreview();
1174
+ if (!isDeliveryActive())
1175
+ return;
1176
+ await deps.sendMarkdownReply(turn.chatId, turn.replyToMessageId, finalText, { replyMarkup, target: turn.target });
1177
+ }
1178
+ }
1179
+ if (!isDeliveryActive())
1180
+ return;
1181
+ setPreviewPendingText("");
1182
+ }
1183
+ catch (error) {
1184
+ deps.recordRuntimeEvent?.("delivery", error, {
1185
+ phase: "final-text",
1186
+ chatId: turn.chatId,
1187
+ replyToMessageId: turn.replyToMessageId,
1188
+ });
1189
+ }
1190
+ }
1191
+ if (!isDeliveryActive())
1192
+ return;
1193
+ if (outboundReply && deps.sendOutboundReplyArtifacts) {
1194
+ try {
1195
+ await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1196
+ replyToPrompt: !finalText,
1197
+ isDeliveryActive,
1198
+ });
1199
+ if (!isDeliveryActive())
1200
+ return;
1201
+ }
1202
+ catch (error) {
1203
+ deps.recordRuntimeEvent?.("delivery", error, {
1204
+ phase: "voice-artifacts",
1205
+ chatId: turn.chatId,
1206
+ });
1207
+ if (!isDeliveryActive())
1208
+ return;
1209
+ if (isTelegramApiCommitUnknownError(error)) {
1210
+ if (endPlan.shouldDispatchNext)
1211
+ deps.dispatchNextQueuedTelegramTurn();
1212
+ return;
1213
+ }
1214
+ // Fallback only when voice delivery is not uncertain and text wasn't already delivered.
1215
+ if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
1216
+ try {
1217
+ const fallbackMarkdown = plannedReply?.markdown ||
1218
+ outboundReply?.voiceText ||
1219
+ rawFinalText;
1220
+ await deps.sendMarkdownReply(turn.chatId, turn.replyToMessageId, fallbackMarkdown, plannedReply?.replyMarkup || turn.target
1221
+ ? {
1222
+ replyMarkup: plannedReply?.replyMarkup,
1223
+ target: turn.target,
1224
+ }
1225
+ : undefined);
1226
+ }
1227
+ catch (fallbackError) {
1228
+ deps.recordRuntimeEvent?.("delivery", fallbackError, {
1229
+ phase: "voice-fallback-text",
1230
+ chatId: turn.chatId,
1231
+ });
1232
+ }
1233
+ }
1234
+ }
1235
+ }
1236
+ if (!isDeliveryActive())
1237
+ return;
1238
+ if (!richAttachmentDelivered && endPlan.shouldSendAttachmentNotice) {
1239
+ await deps.sendTextReply(turn.chatId, turn.replyToMessageId, "Attached requested file(s).", { target: turn.target });
1240
+ }
1241
+ if (!isDeliveryActive())
1242
+ return;
1243
+ if (!richAttachmentDelivered && !queuedAttachmentsDelivered) {
1244
+ await deps.sendQueuedAttachments(turn, { isDeliveryActive });
1245
+ }
1246
+ if (!isDeliveryActive())
1247
+ return;
1248
+ if (endPlan.shouldDispatchNext)
1249
+ deps.dispatchNextQueuedTelegramTurn();
1250
+ };
1251
+ if (deps.scheduleActiveTurnDelivery &&
1252
+ (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
1253
+ deps.scheduleActiveTurnDelivery(deliverActiveTurn);
1254
+ return;
1255
+ }
1256
+ await deliverActiveTurn();
1257
+ }
1258
+ export function createTelegramSessionStateApplier(deps) {
1259
+ return {
1260
+ applyStartState: (state) => {
1261
+ deps.setCurrentModel(state.currentTelegramModel);
1262
+ deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
1263
+ deps.syncCounters(state);
1264
+ deps.syncFlags(state);
1265
+ },
1266
+ applyShutdownState: (state) => {
1267
+ deps.setQueuedItems(state.queuedTelegramItems);
1268
+ deps.syncCounters(state);
1269
+ deps.syncFlags(state);
1270
+ deps.setCurrentModel(state.currentTelegramModel);
1271
+ deps.setPendingModelSwitch(state.pendingTelegramModelSwitch);
1272
+ },
1273
+ };
1274
+ }
1275
+ function isTelegramStaleContextError(error) {
1276
+ return (error instanceof Error &&
1277
+ (error.message.includes("stale after session") ||
1278
+ error.message.includes("stale ctx")));
1279
+ }
1280
+ export function buildTelegramSessionStartState(currentModel) {
1281
+ return {
1282
+ currentTelegramModel: currentModel,
1283
+ activeTelegramToolExecutions: 0,
1284
+ pendingTelegramModelSwitch: undefined,
1285
+ nextQueuedTelegramItemOrder: 0,
1286
+ nextQueuedTelegramControlOrder: 0,
1287
+ telegramTurnDispatchPending: false,
1288
+ compactionInProgress: false,
1289
+ };
1290
+ }
1291
+ export function buildTelegramSessionShutdownState() {
1292
+ return {
1293
+ queuedTelegramItems: [],
1294
+ nextQueuedTelegramItemOrder: 0,
1295
+ nextQueuedTelegramControlOrder: 0,
1296
+ currentTelegramModel: undefined,
1297
+ activeTelegramToolExecutions: 0,
1298
+ pendingTelegramModelSwitch: undefined,
1299
+ telegramTurnDispatchPending: false,
1300
+ compactionInProgress: false,
1301
+ foldQueuedPromptsIntoHistory: false,
1302
+ };
1303
+ }
1304
+ export async function startTelegramSessionRuntime(deps) {
1305
+ await deps.loadConfig();
1306
+ if (deps.isSessionActive?.() === false)
1307
+ return;
1308
+ deps.applyState(buildTelegramSessionStartState(deps.currentModel));
1309
+ await deps.prepareTempDir();
1310
+ if (deps.isSessionActive?.() === false)
1311
+ return;
1312
+ try {
1313
+ deps.bindDeferredDispatchContext?.(deps.ctx);
1314
+ }
1315
+ catch (error) {
1316
+ if (!isTelegramStaleContextError(error))
1317
+ throw error;
1318
+ }
1319
+ deps.updateStatus();
1320
+ }
1321
+ export async function shutdownTelegramSessionRuntime(deps) {
1322
+ if (deps.isSessionActive?.() === false)
1323
+ return;
1324
+ deps.unbindDeferredDispatchContext?.();
1325
+ await deps.stopPolling();
1326
+ if (deps.isSessionActive?.() === false)
1327
+ return;
1328
+ deps.discardQueuedItems?.();
1329
+ deps.applyState(buildTelegramSessionShutdownState());
1330
+ deps.clearPendingMediaGroups();
1331
+ deps.clearModelMenuState();
1332
+ const activeTurnChatId = deps.getActiveTurnChatId();
1333
+ if (activeTurnChatId !== undefined) {
1334
+ const target = deps.getActiveTurnTarget?.();
1335
+ const previewTimeoutMs = deps.previewShutdownTimeoutMs ?? 1000;
1336
+ let timeout;
1337
+ await Promise.race([
1338
+ deps.clearPreview(activeTurnChatId, target ? { target } : undefined),
1339
+ new Promise((resolve) => {
1340
+ timeout = setTimeout(resolve, previewTimeoutMs);
1341
+ }),
1342
+ ]).finally(() => {
1343
+ if (timeout)
1344
+ clearTimeout(timeout);
1345
+ });
1346
+ if (deps.isSessionActive?.() === false)
1347
+ return;
1348
+ }
1349
+ deps.clearActiveTurn();
1350
+ deps.clearAbort();
1351
+ }
1352
+ export function createTelegramSessionLifecycleRuntime(deps) {
1353
+ const stateApplier = createTelegramSessionStateApplier({
1354
+ setQueuedItems: deps.setQueuedItems,
1355
+ setCurrentModel: deps.setCurrentModel,
1356
+ setPendingModelSwitch: deps.setPendingModelSwitch,
1357
+ syncCounters: deps.syncCounters,
1358
+ syncFlags: deps.syncFlags,
1359
+ });
1360
+ return createTelegramSessionLifecycleHooks({
1361
+ getCurrentModel: deps.getCurrentModel,
1362
+ loadConfig: deps.loadConfig,
1363
+ applySessionStartState: stateApplier.applyStartState,
1364
+ bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
1365
+ prepareTempDir: deps.prepareTempDir,
1366
+ updateStatus: deps.updateStatus,
1367
+ isSessionActive: deps.isSessionActive,
1368
+ unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
1369
+ discardQueuedItems: deps.discardQueuedItems,
1370
+ applySessionShutdownState: stateApplier.applyShutdownState,
1371
+ clearPendingMediaGroups: deps.clearPendingMediaGroups,
1372
+ clearModelMenuState: deps.clearModelMenuState,
1373
+ getActiveTurnChatId: deps.getActiveTurnChatId,
1374
+ getActiveTurnTarget: deps.getActiveTurnTarget,
1375
+ clearPreview: deps.clearPreview,
1376
+ clearActiveTurn: deps.clearActiveTurn,
1377
+ clearAbort: deps.clearAbort,
1378
+ stopPolling: deps.stopPolling,
1379
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1380
+ });
1381
+ }
1382
+ export function createTelegramSessionLifecycleHooks(deps) {
1383
+ return {
1384
+ onSessionStart: async (_event, ctx) => {
1385
+ try {
1386
+ await startTelegramSessionRuntime({
1387
+ ctx,
1388
+ currentModel: deps.getCurrentModel(ctx),
1389
+ loadConfig: deps.loadConfig,
1390
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
1391
+ applyState: deps.applySessionStartState,
1392
+ bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
1393
+ prepareTempDir: deps.prepareTempDir,
1394
+ updateStatus: () => deps.updateStatus(ctx),
1395
+ });
1396
+ }
1397
+ catch (error) {
1398
+ deps.recordRuntimeEvent?.("session", error, { phase: "start" });
1399
+ throw error;
1400
+ }
1401
+ },
1402
+ onSessionShutdown: async (_event, ctx) => {
1403
+ try {
1404
+ await shutdownTelegramSessionRuntime({
1405
+ isSessionActive: () => ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
1406
+ unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
1407
+ discardQueuedItems: ctx === undefined || !deps.discardQueuedItems
1408
+ ? undefined
1409
+ : () => deps.discardQueuedItems(ctx),
1410
+ applyState: deps.applySessionShutdownState,
1411
+ clearPendingMediaGroups: deps.clearPendingMediaGroups,
1412
+ clearModelMenuState: deps.clearModelMenuState,
1413
+ getActiveTurnChatId: deps.getActiveTurnChatId,
1414
+ getActiveTurnTarget: deps.getActiveTurnTarget,
1415
+ clearPreview: deps.clearPreview,
1416
+ previewShutdownTimeoutMs: deps.previewShutdownTimeoutMs,
1417
+ clearActiveTurn: deps.clearActiveTurn,
1418
+ clearAbort: deps.clearAbort,
1419
+ stopPolling: deps.stopPolling,
1420
+ });
1421
+ }
1422
+ catch (error) {
1423
+ deps.recordRuntimeEvent?.("session", error, { phase: "shutdown" });
1424
+ throw error;
1425
+ }
1426
+ },
1427
+ };
1428
+ }
1429
+ export function createTelegramQueueMutationController(deps) {
1430
+ const buildRuntimeDeps = (ctx) => ({
1431
+ ...deps,
1432
+ ctx,
1433
+ });
1434
+ return {
1435
+ append: (item, ctx) => appendTelegramQueueItemRuntime(item, buildRuntimeDeps(ctx)),
1436
+ reorder: (ctx) => reorderTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1437
+ clear: (ctx) => clearTelegramQueueItemsRuntime(buildRuntimeDeps(ctx)),
1438
+ removeByMessageIds: (messageIds, ctx, scope) => removeTelegramQueueItemsByMessageIdsRuntime(messageIds, buildRuntimeDeps(ctx), scope),
1439
+ applyReactionByMessageId: (messageId, disposition, ctx, scope) => applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, buildRuntimeDeps(ctx), scope),
1440
+ };
1441
+ }
1442
+ function updateTelegramQueueStatusRuntime(deps) {
1443
+ try {
1444
+ deps.updateStatus(deps.ctx);
1445
+ }
1446
+ catch (error) {
1447
+ if (!isTelegramStaleContextError(error))
1448
+ throw error;
1449
+ }
1450
+ }
1451
+ function commitReorderedTelegramQueueItemsRuntime(items, deps) {
1452
+ deps.setQueuedItems([...items].sort(compareTelegramQueueItems));
1453
+ updateTelegramQueueStatusRuntime(deps);
1454
+ }
1455
+ function appendTelegramQueueItemRuntime(item, deps) {
1456
+ const currentItems = deps.getQueuedItems();
1457
+ const nextItems = appendTelegramQueueItem(currentItems, item);
1458
+ if (nextItems === currentItems)
1459
+ return;
1460
+ commitReorderedTelegramQueueItemsRuntime(nextItems, deps);
1461
+ }
1462
+ export function reorderTelegramQueueItemsRuntime(deps) {
1463
+ commitReorderedTelegramQueueItemsRuntime(deps.getQueuedItems(), deps);
1464
+ }
1465
+ export function clearTelegramQueueItemsRuntime(deps) {
1466
+ const removedItems = deps.getQueuedItems();
1467
+ const removedCount = removedItems.length;
1468
+ if (removedCount === 0)
1469
+ return 0;
1470
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
1471
+ deps.setQueuedItems([]);
1472
+ updateTelegramQueueStatusRuntime(deps);
1473
+ return removedCount;
1474
+ }
1475
+ export function removeTelegramQueueItemsByMessageIdsRuntime(messageIds, deps, scope) {
1476
+ const { items, removedItems, removedCount } = removeTelegramQueueItemsByMessageIds(deps.getQueuedItems(), messageIds, scope);
1477
+ if (removedCount === 0)
1478
+ return 0;
1479
+ deps.setQueuedItems(items);
1480
+ try {
1481
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
1482
+ }
1483
+ catch (error) {
1484
+ deps.recordRuntimeEvent?.("queue", error, {
1485
+ phase: "discard-receipt-settlement",
1486
+ });
1487
+ }
1488
+ updateTelegramQueueStatusRuntime(deps);
1489
+ return removedCount;
1490
+ }
1491
+ export function applyTelegramQueuePromptReactionDispositionRuntime(messageId, disposition, deps, scope) {
1492
+ const queuedItems = deps.getQueuedItems();
1493
+ const changesLane = queuedItems.some((item) => {
1494
+ if (!isPendingTelegramTurn(item) ||
1495
+ !isTelegramQueueItemInMessageScope(item, scope) ||
1496
+ !item.sourceMessageIds.includes(messageId)) {
1497
+ return false;
1498
+ }
1499
+ const queueLane = disposition.kind === "reaction-transition"
1500
+ ? disposition.priorityEmoji === undefined
1501
+ ? item.queueLane
1502
+ : disposition.priorityEmoji === null
1503
+ ? "default"
1504
+ : "priority"
1505
+ : disposition.kind === "priority" ||
1506
+ disposition.kind === "priority-suppressed"
1507
+ ? "priority"
1508
+ : "default";
1509
+ return item.queueLane !== queueLane;
1510
+ });
1511
+ const destinationLaneOrder = changesLane
1512
+ ? deps.allocateLaneOrder?.()
1513
+ : undefined;
1514
+ if (changesLane && destinationLaneOrder === undefined)
1515
+ return false;
1516
+ const { changed, items } = applyTelegramQueuePromptReactionDisposition(queuedItems, messageId, disposition, destinationLaneOrder, scope);
1517
+ if (!changed)
1518
+ return false;
1519
+ commitReorderedTelegramQueueItemsRuntime(items, deps);
1520
+ return true;
1521
+ }
1522
+ export async function enqueueTelegramPromptTurnRuntime(messages, deps) {
1523
+ deps.assertExecutionCurrent?.();
1524
+ const historyOrders = new Set(planTelegramPromptEnqueue(deps.getQueuedItems(), deps.getFoldQueuedPromptsIntoHistory()).historyTurns.map((turn) => turn.queueOrder));
1525
+ deps.setFoldQueuedPromptsIntoHistory(false);
1526
+ const buildTurn = await deps.prepareTurn(messages);
1527
+ deps.assertExecutionCurrent?.();
1528
+ // Preserve the Pi-owned head until agent_start, plus later arrivals and current edits/reactions.
1529
+ const pendingDispatch = deps.hasPendingDispatch();
1530
+ const historyTurns = [];
1531
+ const remainingItems = deps.getQueuedItems().filter((item, index) => {
1532
+ if ((pendingDispatch && index === 0) || !isPendingTelegramTurn(item) ||
1533
+ !historyOrders.has(item.queueOrder))
1534
+ return true;
1535
+ historyTurns.push(item);
1536
+ return false;
1537
+ });
1538
+ const turn = buildTurn(historyTurns);
1539
+ deps.setQueuedItems(appendTelegramQueueItem(remainingItems, turn));
1540
+ deps.onQueued?.(turn);
1541
+ deps.updateStatus();
1542
+ deps.dispatchNextQueuedTelegramTurn();
1543
+ return turn;
1544
+ }
1545
+ export function createTelegramPromptEnqueueController(deps) {
1546
+ return {
1547
+ enqueue: (messages, ctx, onQueued) => enqueueTelegramPromptTurnRuntime(messages, {
1548
+ ...deps,
1549
+ prepareTurn: (nextMessages) => deps.prepareTurn(nextMessages, ctx),
1550
+ updateStatus: () => deps.updateStatus(ctx),
1551
+ dispatchNextQueuedTelegramTurn: () => deps.dispatchNextQueuedTelegramTurn(ctx),
1552
+ assertExecutionCurrent: () => deps.assertExecutionCurrent?.(messages),
1553
+ onQueued,
1554
+ }),
1555
+ };
1556
+ }
1557
+ export function createTelegramControlQueueController(deps) {
1558
+ return {
1559
+ enqueue: (item, ctx, onQueued) => {
1560
+ deps.appendControlItem(item, ctx);
1561
+ onQueued?.(item);
1562
+ deps.dispatchNextQueuedTelegramTurn(ctx);
1563
+ },
1564
+ };
1565
+ }
1566
+ // --- Control Runtime ---
1567
+ function getTelegramQueueErrorMessage(error) {
1568
+ return error instanceof Error ? error.message : String(error);
1569
+ }
1570
+ export async function executeTelegramControlItemRuntime(item, deps) {
1571
+ try {
1572
+ await item.execute(deps.ctx);
1573
+ }
1574
+ catch (error) {
1575
+ const message = getTelegramQueueErrorMessage(error);
1576
+ deps.recordRuntimeEvent?.("control", error, {
1577
+ controlType: item.controlType,
1578
+ chatId: item.chatId,
1579
+ replyToMessageId: item.replyToMessageId,
1580
+ });
1581
+ await deps.sendTextReply(item.chatId, item.replyToMessageId, `Telegram control action failed: ${message}`, { target: item.target });
1582
+ }
1583
+ finally {
1584
+ deps.onSettled(item);
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Production debounce for deferred queue dispatch; the factory defaults to this
1589
+ * so the entrypoint wires ports instead of policy constants.
1590
+ */
1591
+ export const TELEGRAM_DEFERRED_DISPATCH_DELAY_MS = 50;
1592
+ export function createTelegramDeferredQueueDispatchRuntime(deps = {}) {
1593
+ let boundContext;
1594
+ let generation = 0;
1595
+ const timers = new Set();
1596
+ const delayMs = deps.delayMs ?? TELEGRAM_DEFERRED_DISPATCH_DELAY_MS;
1597
+ const setTimer = deps.setTimer ??
1598
+ ((callback, ms) => setTimeout(callback, ms));
1599
+ const clearTimer = deps.clearTimer ??
1600
+ ((timer) => clearTimeout(timer));
1601
+ const clearTimers = () => {
1602
+ for (const timer of timers)
1603
+ clearTimer(timer);
1604
+ timers.clear();
1605
+ };
1606
+ return {
1607
+ bind: (ctx) => {
1608
+ boundContext = ctx;
1609
+ generation += 1;
1610
+ },
1611
+ unbind: () => {
1612
+ boundContext = undefined;
1613
+ generation += 1;
1614
+ clearTimers();
1615
+ },
1616
+ isBound: () => boundContext !== undefined,
1617
+ getGeneration: () => generation,
1618
+ isGenerationActive: (expectedGeneration) => boundContext !== undefined && generation === expectedGeneration,
1619
+ request: (dispatchNextQueuedTelegramTurn) => {
1620
+ if (boundContext === undefined)
1621
+ return;
1622
+ const scheduledGeneration = generation;
1623
+ let timer;
1624
+ timer = setTimer(() => {
1625
+ timers.delete(timer);
1626
+ if (generation !== scheduledGeneration || boundContext === undefined)
1627
+ return;
1628
+ try {
1629
+ dispatchNextQueuedTelegramTurn(boundContext);
1630
+ }
1631
+ catch (error) {
1632
+ try {
1633
+ deps.recordRuntimeEvent?.("dispatch", error, {
1634
+ phase: "deferred-queue-dispatch",
1635
+ generation: scheduledGeneration,
1636
+ });
1637
+ }
1638
+ catch {
1639
+ // Timer diagnostics cannot escape the deferred owner.
1640
+ }
1641
+ }
1642
+ }, delayMs);
1643
+ timer.unref?.();
1644
+ timers.add(timer);
1645
+ },
1646
+ };
1647
+ }
1648
+ export function createTelegramQueueDispatchWatchdogRuntime(deps) {
1649
+ const intervalMs = deps.intervalMs ?? 1000;
1650
+ const setIntervalFn = deps.setInterval ?? ((callback, ms) => setInterval(callback, ms));
1651
+ const clearIntervalFn = deps.clearInterval ?? ((timer) => clearInterval(timer));
1652
+ let ctx;
1653
+ let interval;
1654
+ let dispatchInFlight = false;
1655
+ const tick = () => {
1656
+ if (ctx === undefined || dispatchInFlight || !deps.hasQueuedItems())
1657
+ return;
1658
+ dispatchInFlight = true;
1659
+ try {
1660
+ deps.dispatchNextQueuedTelegramTurn(ctx);
1661
+ }
1662
+ catch (error) {
1663
+ try {
1664
+ deps.recordRuntimeEvent?.("dispatch", error, {
1665
+ phase: "queue-watchdog",
1666
+ });
1667
+ }
1668
+ catch {
1669
+ // Watchdog diagnostics cannot escape the interval owner.
1670
+ }
1671
+ }
1672
+ finally {
1673
+ dispatchInFlight = false;
1674
+ }
1675
+ };
1676
+ const stop = () => {
1677
+ ctx = undefined;
1678
+ if (!interval)
1679
+ return;
1680
+ clearIntervalFn(interval);
1681
+ interval = undefined;
1682
+ };
1683
+ return {
1684
+ start: (nextCtx) => {
1685
+ ctx = nextCtx;
1686
+ if (!interval) {
1687
+ const nextInterval = setIntervalFn(tick, intervalMs);
1688
+ interval = nextInterval;
1689
+ nextInterval.unref?.();
1690
+ }
1691
+ tick();
1692
+ },
1693
+ stop,
1694
+ poke: tick,
1695
+ };
1696
+ }
1697
+ export function executeTelegramQueueDispatchPlan(plan, deps) {
1698
+ if (plan.kind === "none") {
1699
+ deps.onIdle();
1700
+ return;
1701
+ }
1702
+ if (plan.kind === "control") {
1703
+ deps.executeControlItem(plan.item);
1704
+ return;
1705
+ }
1706
+ deps.onPromptDispatchStart(plan.item.chatId);
1707
+ try {
1708
+ if (deps.commitPromptDispatch && !deps.commitPromptDispatch(plan.item)) {
1709
+ throw new Error("Telegram prompt dispatch could not be committed durably.");
1710
+ }
1711
+ deps.sendUserMessage(plan.item.content);
1712
+ }
1713
+ catch (error) {
1714
+ const message = getTelegramQueueErrorMessage(error);
1715
+ deps.onPromptDispatchFailure(message);
1716
+ }
1717
+ }
1718
+ export function createTelegramQueueDispatchRuntime(deps) {
1719
+ return createTelegramQueueDispatchController({
1720
+ getQueuedItems: deps.getQueuedItems,
1721
+ setQueuedItems: deps.setQueuedItems,
1722
+ canDispatch: createTelegramDispatchReadinessChecker({
1723
+ isCompactionInProgress: deps.isCompactionInProgress,
1724
+ hasActiveTurn: deps.hasActiveTurn,
1725
+ hasDispatchPending: deps.hasDispatchPending,
1726
+ isIdle: deps.isIdle,
1727
+ hasPendingMessages: deps.hasPendingMessages,
1728
+ }),
1729
+ hasDispatchContext: deps.hasDispatchContext,
1730
+ getDispatchGeneration: deps.getDispatchGeneration,
1731
+ isDispatchGenerationActive: deps.isDispatchGenerationActive,
1732
+ updateStatus: deps.updateStatus,
1733
+ sendTextReply: deps.sendTextReply,
1734
+ onPromptDispatchStart: deps.onPromptDispatchStart,
1735
+ commitPromptDispatch: deps.commitPromptDispatch,
1736
+ sendUserMessage: deps.sendUserMessage,
1737
+ onPromptDispatchFailure: deps.onPromptDispatchFailure,
1738
+ isQueueItemTransportActive: deps.isQueueItemTransportActive,
1739
+ hasPendingInboundQueueMutationForItem: deps.hasPendingInboundQueueMutationForItem,
1740
+ isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
1741
+ onControlSettled: deps.onControlSettled,
1742
+ onPromptSkipped: deps.onPromptSkipped,
1743
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1744
+ });
1745
+ }
1746
+ export function createTelegramQueueDispatchController(deps) {
1747
+ let controlDispatchPending = false;
1748
+ let nextDispatchAnnouncementRequested = false;
1749
+ let nextDispatchAnnouncementAnchor;
1750
+ const controller = {
1751
+ requestNextDispatchAnnouncement: () => {
1752
+ nextDispatchAnnouncementRequested = true;
1753
+ nextDispatchAnnouncementAnchor = undefined;
1754
+ },
1755
+ dispatchNext: (ctx) => {
1756
+ if (deps.hasDispatchContext && !deps.hasDispatchContext())
1757
+ return;
1758
+ if (controlDispatchPending) {
1759
+ deps.updateStatus(ctx);
1760
+ return;
1761
+ }
1762
+ const queuedItems = deps.getQueuedItems();
1763
+ const activeItems = [];
1764
+ const protectedInactiveItems = [];
1765
+ const retainedItems = [];
1766
+ let droppedInactiveItemCount = 0;
1767
+ for (const item of queuedItems) {
1768
+ if (!deps.isQueueItemTransportActive ||
1769
+ deps.isQueueItemTransportActive(item)) {
1770
+ activeItems.push(item);
1771
+ retainedItems.push(item);
1772
+ }
1773
+ else if ((item.admissionReceipts?.length ?? 0) > 0) {
1774
+ protectedInactiveItems.push(item);
1775
+ retainedItems.push(item);
1776
+ }
1777
+ else {
1778
+ droppedInactiveItemCount += 1;
1779
+ }
1780
+ }
1781
+ if (droppedInactiveItemCount > 0) {
1782
+ deps.setQueuedItems(retainedItems);
1783
+ if (retainedItems.length === 0) {
1784
+ nextDispatchAnnouncementRequested = false;
1785
+ nextDispatchAnnouncementAnchor = undefined;
1786
+ }
1787
+ deps.recordRuntimeEvent?.("dispatch", new Error("Dropped queue work from an inactive Telegram transport generation."), { phase: "transport-generation" });
1788
+ }
1789
+ const canDispatch = deps.canDispatch(ctx);
1790
+ let nextActiveIndex = 0;
1791
+ if (canDispatch) {
1792
+ while (nextActiveIndex < activeItems.length) {
1793
+ const candidate = activeItems[nextActiveIndex];
1794
+ if (!candidate ||
1795
+ candidate.kind !== "prompt" ||
1796
+ candidate.reactionSuppressionEmoji === undefined) {
1797
+ break;
1798
+ }
1799
+ if (deps.hasPendingInboundQueueMutationForItem?.(candidate)) {
1800
+ deps.updateStatus(ctx);
1801
+ return;
1802
+ }
1803
+ if (deps.isQueueItemAdmissionReady &&
1804
+ !deps.isQueueItemAdmissionReady(candidate)) {
1805
+ deps.updateStatus(ctx);
1806
+ return;
1807
+ }
1808
+ try {
1809
+ if (deps.onPromptSkipped && !deps.onPromptSkipped(candidate, ctx)) {
1810
+ deps.updateStatus(ctx, "Telegram skipped prompt could not be settled durably.");
1811
+ return;
1812
+ }
1813
+ }
1814
+ catch (error) {
1815
+ deps.recordRuntimeEvent?.("dispatch", error, {
1816
+ phase: "skip-receipt-settlement",
1817
+ });
1818
+ deps.updateStatus(ctx, "Telegram skipped prompt could not be settled durably.");
1819
+ return;
1820
+ }
1821
+ nextActiveIndex += 1;
1822
+ deps.setQueuedItems([
1823
+ ...activeItems.slice(nextActiveIndex),
1824
+ ...protectedInactiveItems,
1825
+ ]);
1826
+ }
1827
+ }
1828
+ const dispatchableItems = activeItems.slice(nextActiveIndex);
1829
+ const nextItem = dispatchableItems[0];
1830
+ if (nextDispatchAnnouncementRequested) {
1831
+ if (nextDispatchAnnouncementAnchor) {
1832
+ const anchorRetained = retainedItems.includes(nextDispatchAnnouncementAnchor);
1833
+ const anchorTransportActive = deps.isQueueItemTransportActive?.(nextDispatchAnnouncementAnchor) !== false;
1834
+ if (!anchorRetained || !anchorTransportActive) {
1835
+ nextDispatchAnnouncementRequested = false;
1836
+ nextDispatchAnnouncementAnchor = undefined;
1837
+ }
1838
+ }
1839
+ else if (nextItem) {
1840
+ nextDispatchAnnouncementAnchor = nextItem;
1841
+ }
1842
+ else if (retainedItems.length === 0) {
1843
+ nextDispatchAnnouncementRequested = false;
1844
+ }
1845
+ }
1846
+ if (nextItem &&
1847
+ deps.hasPendingInboundQueueMutationForItem?.(nextItem)) {
1848
+ deps.updateStatus(ctx);
1849
+ return;
1850
+ }
1851
+ if (nextItem &&
1852
+ deps.isQueueItemAdmissionReady &&
1853
+ !deps.isQueueItemAdmissionReady(nextItem)) {
1854
+ deps.updateStatus(ctx);
1855
+ return;
1856
+ }
1857
+ const dispatchBasisItems = deps.getQueuedItems();
1858
+ const dispatchPlan = planNextTelegramQueueAction(dispatchableItems, canDispatch);
1859
+ const commitDispatchPlan = () => {
1860
+ if (dispatchPlan.kind === "none")
1861
+ return true;
1862
+ const currentItems = deps.getQueuedItems();
1863
+ if (dispatchPlan.kind === "prompt") {
1864
+ const queueDrifted = currentItems.length !== dispatchBasisItems.length ||
1865
+ currentItems.some((item, index) => item !== dispatchBasisItems[index]);
1866
+ const dispatchEligibilityDrifted = !deps.canDispatch(ctx) ||
1867
+ deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) === true ||
1868
+ (deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false) ||
1869
+ (deps.isQueueItemTransportActive?.(dispatchPlan.item) === false);
1870
+ if (queueDrifted || dispatchEligibilityDrifted) {
1871
+ const selectedItemRetained = currentItems.includes(dispatchPlan.item);
1872
+ const selectedTransportActive = deps.isQueueItemTransportActive?.(dispatchPlan.item) !== false;
1873
+ nextDispatchAnnouncementRequested =
1874
+ selectedItemRetained && selectedTransportActive;
1875
+ nextDispatchAnnouncementAnchor = nextDispatchAnnouncementRequested
1876
+ ? dispatchPlan.item
1877
+ : undefined;
1878
+ deps.updateStatus(ctx);
1879
+ if (nextDispatchAnnouncementRequested && queueDrifted && !dispatchEligibilityDrifted) {
1880
+ controller.dispatchNext(ctx);
1881
+ }
1882
+ return false;
1883
+ }
1884
+ }
1885
+ deps.setQueuedItems([
1886
+ ...dispatchPlan.remainingItems,
1887
+ ...protectedInactiveItems,
1888
+ ]);
1889
+ nextDispatchAnnouncementAnchor = undefined;
1890
+ return true;
1891
+ };
1892
+ const executePlan = () => {
1893
+ if (!commitDispatchPlan())
1894
+ return;
1895
+ executeTelegramQueueDispatchPlan(dispatchPlan, {
1896
+ executeControlItem: (item) => {
1897
+ controlDispatchPending = true;
1898
+ const dispatchGeneration = deps.getDispatchGeneration?.();
1899
+ deps.updateStatus(ctx);
1900
+ void executeTelegramControlItemRuntime(item, {
1901
+ ctx,
1902
+ sendTextReply: deps.sendTextReply,
1903
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1904
+ onSettled: (settledItem) => {
1905
+ try {
1906
+ deps.onControlSettled?.(settledItem, ctx);
1907
+ }
1908
+ catch (error) {
1909
+ deps.recordRuntimeEvent?.("control", error, {
1910
+ phase: "receipt-settlement",
1911
+ controlType: settledItem.controlType,
1912
+ });
1913
+ }
1914
+ controlDispatchPending = false;
1915
+ if (deps.hasDispatchContext && !deps.hasDispatchContext())
1916
+ return;
1917
+ if (dispatchGeneration !== undefined &&
1918
+ deps.isDispatchGenerationActive &&
1919
+ !deps.isDispatchGenerationActive(dispatchGeneration)) {
1920
+ return;
1921
+ }
1922
+ deps.updateStatus(ctx);
1923
+ controller.dispatchNext(ctx);
1924
+ },
1925
+ });
1926
+ },
1927
+ onPromptDispatchStart: (chatId) => {
1928
+ deps.onPromptDispatchStart(ctx, chatId);
1929
+ },
1930
+ commitPromptDispatch: deps.commitPromptDispatch
1931
+ ? (item) => deps.commitPromptDispatch(item, ctx)
1932
+ : undefined,
1933
+ sendUserMessage: deps.sendUserMessage,
1934
+ onPromptDispatchFailure: (message) => {
1935
+ deps.onPromptDispatchFailure(ctx, message);
1936
+ },
1937
+ onIdle: () => {
1938
+ deps.updateStatus(ctx);
1939
+ },
1940
+ });
1941
+ };
1942
+ if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
1943
+ nextDispatchAnnouncementRequested = false;
1944
+ controlDispatchPending = true;
1945
+ const dispatchGeneration = deps.getDispatchGeneration?.();
1946
+ deps.updateStatus(ctx);
1947
+ void deps.sendTextReply(dispatchPlan.item.chatId, dispatchPlan.item.replyToMessageId, "<b>⏩ Dispatching next queued turn.</b>", { target: dispatchPlan.item.target }).catch((error) => {
1948
+ deps.recordRuntimeEvent?.("dispatch", error, { phase: "next-announcement" });
1949
+ }).finally(() => {
1950
+ controlDispatchPending = false;
1951
+ if (deps.hasDispatchContext && !deps.hasDispatchContext())
1952
+ return;
1953
+ if (dispatchGeneration !== undefined &&
1954
+ deps.isDispatchGenerationActive &&
1955
+ !deps.isDispatchGenerationActive(dispatchGeneration))
1956
+ return;
1957
+ executePlan();
1958
+ });
1959
+ return;
1960
+ }
1961
+ executePlan();
1962
+ },
1963
+ };
1964
+ return controller;
1965
+ }