@llblab/pi-telegram 0.52.2 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
package/lib/queue.ts CHANGED
@@ -6,11 +6,10 @@
6
6
 
7
7
  import { createHash } from "node:crypto";
8
8
 
9
+ import { isPiStaleContextError } from "./pi.ts";
10
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
9
11
  import { isVoiceTurn } from "./voice.ts";
10
12
  import { isWireRecord as isQueuePayloadRecord } from "./wire.ts";
11
- import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
12
-
13
- // --- Queue Items ---
14
13
 
15
14
  export interface QueuedAttachment {
16
15
  path: string;
@@ -164,8 +163,7 @@ export interface TelegramQueueHandoffBase {
164
163
  admissionReceipts: TelegramQueueAdmissionReceipt[];
165
164
  }
166
165
 
167
- export interface TelegramPromptQueueHandoffPayload
168
- extends TelegramQueueHandoffBase {
166
+ export interface TelegramPromptQueueHandoffPayload extends TelegramQueueHandoffBase {
169
167
  kind: "prompt";
170
168
  sourceMessageIds: number[];
171
169
  queuedAttachments: QueuedAttachment[];
@@ -177,76 +175,182 @@ export interface TelegramPromptQueueHandoffPayload
177
175
  voiceReplyRequired?: boolean;
178
176
  }
179
177
 
180
- export interface TelegramControlQueueHandoffPayload
181
- extends TelegramQueueHandoffBase {
178
+ export interface TelegramControlQueueHandoffPayload extends TelegramQueueHandoffBase {
182
179
  kind: "control";
183
180
  controlType: PendingTelegramControlItem<unknown>["controlType"];
184
181
  }
185
182
 
186
183
  export type TelegramQueueHandoffPayload =
187
- | TelegramPromptQueueHandoffPayload
188
- | TelegramControlQueueHandoffPayload;
184
+ TelegramPromptQueueHandoffPayload | TelegramControlQueueHandoffPayload;
189
185
 
190
186
  export interface TelegramQueueHandoff {
191
187
  handoffToken: string;
192
188
  payload: TelegramQueueHandoffPayload;
193
189
  }
194
190
 
195
- function parseQueuePayloadReceipt(value: unknown, kind: TelegramQueueItemKind): TelegramQueueAdmissionReceipt | undefined {
196
- if (!isQueuePayloadRecord(value) || value.queueKind !== kind ||
197
- typeof value.receiptId !== "string" || !value.receiptId ||
198
- (value.journalBindingKey !== undefined && (typeof value.journalBindingKey !== "string" || !value.journalBindingKey.trim())) ||
199
- !Array.isArray(value.sourceUpdateIds) || value.sourceUpdateIds.length === 0 ||
200
- value.sourceUpdateIds.some((id, index) => !Number.isSafeInteger(id) || id < 0 ||
201
- (index > 0 && id <= (value.sourceUpdateIds as number[])[index - 1]!))) return undefined;
202
- return { queueKind: kind, receiptId: value.receiptId, sourceUpdateIds: value.sourceUpdateIds as number[],
203
- ...(typeof value.journalBindingKey === "string" ? { journalBindingKey: value.journalBindingKey } : {}) };
191
+ function parseQueuePayloadReceipt(
192
+ value: unknown,
193
+ kind: TelegramQueueItemKind,
194
+ ): TelegramQueueAdmissionReceipt | undefined {
195
+ if (
196
+ !isQueuePayloadRecord(value) ||
197
+ value.queueKind !== kind ||
198
+ typeof value.receiptId !== "string" ||
199
+ !value.receiptId ||
200
+ (value.journalBindingKey !== undefined &&
201
+ (typeof value.journalBindingKey !== "string" ||
202
+ !value.journalBindingKey.trim())) ||
203
+ !Array.isArray(value.sourceUpdateIds) ||
204
+ value.sourceUpdateIds.length === 0 ||
205
+ value.sourceUpdateIds.some(
206
+ (id, index) =>
207
+ !Number.isSafeInteger(id) ||
208
+ id < 0 ||
209
+ (index > 0 && id <= (value.sourceUpdateIds as number[])[index - 1]!),
210
+ )
211
+ )
212
+ return undefined;
213
+ return {
214
+ queueKind: kind,
215
+ receiptId: value.receiptId,
216
+ sourceUpdateIds: value.sourceUpdateIds as number[],
217
+ ...(typeof value.journalBindingKey === "string"
218
+ ? { journalBindingKey: value.journalBindingKey }
219
+ : {}),
220
+ };
204
221
  }
205
222
 
206
223
  /** Data decoder shared by authenticated IPC and cold storage; decoding grants no admission. */
207
- export function parseTelegramQueueHandoffPayload(value: unknown): TelegramQueueHandoffPayload | undefined {
208
- if (!isQueuePayloadRecord(value) || (value.kind !== "prompt" && value.kind !== "control")) return undefined;
209
- const target = isQueuePayloadRecord(value.target) && typeof value.target.chatId === "number"
210
- ? { chatId: value.target.chatId, ...(typeof value.target.threadId === "number" ? { threadId: value.target.threadId } : {}) } : undefined;
211
- const transportStamp = isQueuePayloadRecord(value.transportStamp) &&
212
- typeof value.transportStamp.profile === "string" && typeof value.transportStamp.generation === "string"
213
- ? { profile: value.transportStamp.profile, generation: value.transportStamp.generation } : undefined;
214
- if (!Number.isSafeInteger(value.chatId) || (value.target !== undefined && !target) ||
215
- (value.transportStamp !== undefined && !transportStamp) || !Number.isSafeInteger(value.replyToMessageId) ||
216
- (value.guestQueryId !== undefined && typeof value.guestQueryId !== "string") ||
217
- (value.guestInlineMessageId !== undefined && typeof value.guestInlineMessageId !== "string") ||
218
- !Number.isSafeInteger(value.queueOrder) || (value.queueLane !== "control" && value.queueLane !== "priority" && value.queueLane !== "default") ||
219
- !Number.isSafeInteger(value.laneOrder) || typeof value.statusSummary !== "string" || !Array.isArray(value.admissionReceipts)) return undefined;
220
- const admissionReceipts = value.admissionReceipts.map(receipt => parseQueuePayloadReceipt(receipt, value.kind as TelegramQueueItemKind));
221
- if (admissionReceipts.length === 0 || admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS ||
222
- admissionReceipts.some(receipt => !receipt)) return undefined;
223
- const base = { chatId: value.chatId as number, ...(target ? { target } : {}), ...(transportStamp ? { transportStamp } : {}),
224
+ export function parseTelegramQueueHandoffPayload(
225
+ value: unknown,
226
+ ): TelegramQueueHandoffPayload | undefined {
227
+ if (
228
+ !isQueuePayloadRecord(value) ||
229
+ (value.kind !== "prompt" && value.kind !== "control")
230
+ )
231
+ return undefined;
232
+ const target =
233
+ isQueuePayloadRecord(value.target) &&
234
+ typeof value.target.chatId === "number"
235
+ ? {
236
+ chatId: value.target.chatId,
237
+ ...(typeof value.target.threadId === "number"
238
+ ? { threadId: value.target.threadId }
239
+ : {}),
240
+ }
241
+ : undefined;
242
+ const transportStamp =
243
+ isQueuePayloadRecord(value.transportStamp) &&
244
+ typeof value.transportStamp.profile === "string" &&
245
+ typeof value.transportStamp.generation === "string"
246
+ ? {
247
+ profile: value.transportStamp.profile,
248
+ generation: value.transportStamp.generation,
249
+ }
250
+ : undefined;
251
+ if (
252
+ !Number.isSafeInteger(value.chatId) ||
253
+ (value.target !== undefined && !target) ||
254
+ (value.transportStamp !== undefined && !transportStamp) ||
255
+ !Number.isSafeInteger(value.replyToMessageId) ||
256
+ (value.guestQueryId !== undefined &&
257
+ typeof value.guestQueryId !== "string") ||
258
+ (value.guestInlineMessageId !== undefined &&
259
+ typeof value.guestInlineMessageId !== "string") ||
260
+ !Number.isSafeInteger(value.queueOrder) ||
261
+ (value.queueLane !== "control" &&
262
+ value.queueLane !== "priority" &&
263
+ value.queueLane !== "default") ||
264
+ !Number.isSafeInteger(value.laneOrder) ||
265
+ typeof value.statusSummary !== "string" ||
266
+ !Array.isArray(value.admissionReceipts)
267
+ )
268
+ return undefined;
269
+ const admissionReceipts = value.admissionReceipts.map((receipt) =>
270
+ parseQueuePayloadReceipt(receipt, value.kind as TelegramQueueItemKind),
271
+ );
272
+ if (
273
+ admissionReceipts.length === 0 ||
274
+ admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS ||
275
+ admissionReceipts.some((receipt) => !receipt)
276
+ )
277
+ return undefined;
278
+ const base = {
279
+ chatId: value.chatId as number,
280
+ ...(target ? { target } : {}),
281
+ ...(transportStamp ? { transportStamp } : {}),
224
282
  replyToMessageId: value.replyToMessageId as number,
225
- ...(typeof value.guestQueryId === "string" ? { guestQueryId: value.guestQueryId } : {}),
226
- ...(typeof value.guestInlineMessageId === "string" ? { guestInlineMessageId: value.guestInlineMessageId } : {}),
227
- queueOrder: value.queueOrder as number, queueLane: value.queueLane as TelegramQueueLane,
228
- laneOrder: value.laneOrder as number, statusSummary: value.statusSummary,
229
- admissionReceipts: admissionReceipts as TelegramQueueAdmissionReceipt[] };
283
+ ...(typeof value.guestQueryId === "string"
284
+ ? { guestQueryId: value.guestQueryId }
285
+ : {}),
286
+ ...(typeof value.guestInlineMessageId === "string"
287
+ ? { guestInlineMessageId: value.guestInlineMessageId }
288
+ : {}),
289
+ queueOrder: value.queueOrder as number,
290
+ queueLane: value.queueLane as TelegramQueueLane,
291
+ laneOrder: value.laneOrder as number,
292
+ statusSummary: value.statusSummary,
293
+ admissionReceipts: admissionReceipts as TelegramQueueAdmissionReceipt[],
294
+ };
230
295
  if (value.kind === "control") {
231
- return value.queueLane === "control" && (value.controlType === "status" || value.controlType === "model")
232
- ? { kind: "control", controlType: value.controlType, ...base } : undefined;
296
+ return value.queueLane === "control" &&
297
+ (value.controlType === "status" || value.controlType === "model")
298
+ ? { kind: "control", controlType: value.controlType, ...base }
299
+ : undefined;
233
300
  }
234
- if (value.queueLane === "control" || !Array.isArray(value.sourceMessageIds) || value.sourceMessageIds.some(id => !Number.isSafeInteger(id)) ||
235
- !Array.isArray(value.queuedAttachments) || value.queuedAttachments.some(attachment => !isQueuePayloadRecord(attachment) ||
236
- typeof attachment.path !== "string" || typeof attachment.fileName !== "string") ||
237
- !Array.isArray(value.content) || value.content.some(content => !isQueuePayloadRecord(content) ||
238
- (content.type === "text" ? typeof content.text !== "string" : content.type === "image"
239
- ? typeof content.data !== "string" || typeof content.mimeType !== "string" : true)) ||
240
- typeof value.historyText !== "string" || (value.priorityEmoji !== undefined && typeof value.priorityEmoji !== "string") ||
241
- (value.reactionSuppressionEmoji !== undefined && typeof value.reactionSuppressionEmoji !== "string") ||
242
- (value.voiceReplyPreferred !== undefined && typeof value.voiceReplyPreferred !== "boolean") ||
243
- (value.voiceReplyRequired !== undefined && typeof value.voiceReplyRequired !== "boolean")) return undefined;
244
- return { kind: "prompt", ...base, sourceMessageIds: value.sourceMessageIds as number[],
245
- queuedAttachments: value.queuedAttachments as QueuedAttachment[], content: value.content as TelegramPromptContent[], historyText: value.historyText,
246
- ...(typeof value.priorityEmoji === "string" ? { priorityEmoji: value.priorityEmoji } : {}),
247
- ...(typeof value.reactionSuppressionEmoji === "string" ? { reactionSuppressionEmoji: value.reactionSuppressionEmoji } : {}),
248
- ...(typeof value.voiceReplyPreferred === "boolean" ? { voiceReplyPreferred: value.voiceReplyPreferred } : {}),
249
- ...(typeof value.voiceReplyRequired === "boolean" ? { voiceReplyRequired: value.voiceReplyRequired } : {}) };
301
+ if (
302
+ value.queueLane === "control" ||
303
+ !Array.isArray(value.sourceMessageIds) ||
304
+ value.sourceMessageIds.some((id) => !Number.isSafeInteger(id)) ||
305
+ !Array.isArray(value.queuedAttachments) ||
306
+ value.queuedAttachments.some(
307
+ (attachment) =>
308
+ !isQueuePayloadRecord(attachment) ||
309
+ typeof attachment.path !== "string" ||
310
+ typeof attachment.fileName !== "string",
311
+ ) ||
312
+ !Array.isArray(value.content) ||
313
+ value.content.some(
314
+ (content) =>
315
+ !isQueuePayloadRecord(content) ||
316
+ (content.type === "text"
317
+ ? typeof content.text !== "string"
318
+ : content.type === "image"
319
+ ? typeof content.data !== "string" ||
320
+ typeof content.mimeType !== "string"
321
+ : true),
322
+ ) ||
323
+ typeof value.historyText !== "string" ||
324
+ (value.priorityEmoji !== undefined &&
325
+ typeof value.priorityEmoji !== "string") ||
326
+ (value.reactionSuppressionEmoji !== undefined &&
327
+ typeof value.reactionSuppressionEmoji !== "string") ||
328
+ (value.voiceReplyPreferred !== undefined &&
329
+ typeof value.voiceReplyPreferred !== "boolean") ||
330
+ (value.voiceReplyRequired !== undefined &&
331
+ typeof value.voiceReplyRequired !== "boolean")
332
+ )
333
+ return undefined;
334
+ return {
335
+ kind: "prompt",
336
+ ...base,
337
+ sourceMessageIds: value.sourceMessageIds as number[],
338
+ queuedAttachments: value.queuedAttachments as QueuedAttachment[],
339
+ content: value.content as TelegramPromptContent[],
340
+ historyText: value.historyText,
341
+ ...(typeof value.priorityEmoji === "string"
342
+ ? { priorityEmoji: value.priorityEmoji }
343
+ : {}),
344
+ ...(typeof value.reactionSuppressionEmoji === "string"
345
+ ? { reactionSuppressionEmoji: value.reactionSuppressionEmoji }
346
+ : {}),
347
+ ...(typeof value.voiceReplyPreferred === "boolean"
348
+ ? { voiceReplyPreferred: value.voiceReplyPreferred }
349
+ : {}),
350
+ ...(typeof value.voiceReplyRequired === "boolean"
351
+ ? { voiceReplyRequired: value.voiceReplyRequired }
352
+ : {}),
353
+ };
250
354
  }
251
355
 
252
356
  export interface TelegramQueueHandoffStageReceipt {
@@ -265,13 +369,14 @@ export interface TelegramQueueHandoffAcceptedOwner {
265
369
  handoffId?: string;
266
370
  }
267
371
 
268
- export interface TelegramQueueHandoffStageResult
269
- extends TelegramQueueHandoffStageReceipt {
372
+ export interface TelegramQueueHandoffStageResult extends TelegramQueueHandoffStageReceipt {
270
373
  queueOwner: TelegramQueueHandoffAcceptedOwner;
271
374
  }
272
375
 
273
376
  export interface TelegramQueueHandoffStagingRuntime {
274
- stage: (payload: TelegramQueueHandoffPayload) => TelegramQueueHandoffStageReceipt;
377
+ stage: (
378
+ payload: TelegramQueueHandoffPayload,
379
+ ) => TelegramQueueHandoffStageReceipt;
275
380
  accept: (receipt: TelegramQueueAdmissionReceipt) => boolean;
276
381
  cancel: (receipt: TelegramQueueAdmissionReceipt) => boolean;
277
382
  hasStaged: (receipt: TelegramQueueAdmissionReceipt) => boolean;
@@ -333,7 +438,9 @@ export function createTelegramQueueAdmissionReceipt(options: {
333
438
  (updateId) => !Number.isSafeInteger(updateId) || updateId < 0,
334
439
  )
335
440
  ) {
336
- throw new Error("Telegram queue admission update ids must be safe integers.");
441
+ throw new Error(
442
+ "Telegram queue admission update ids must be safe integers.",
443
+ );
337
444
  }
338
445
  const digest = createHash("sha256")
339
446
  .update(
@@ -352,14 +459,6 @@ export function createTelegramQueueAdmissionReceipt(options: {
352
459
  };
353
460
  }
354
461
 
355
- export function isTelegramQueueItemDurablyAdmitted<TContext = unknown>(
356
- item: TelegramQueueItem<TContext>,
357
- isReceiptCommitted: (receipt: TelegramQueueAdmissionReceipt) => boolean,
358
- ): boolean {
359
- assertTelegramQueueItemAdmissionValid(item);
360
- return (item.admissionReceipts ?? []).every(isReceiptCommitted);
361
- }
362
-
363
462
  export function getTelegramQueueLaneContract(
364
463
  lane: TelegramQueueLane,
365
464
  ): TelegramQueueLaneContract {
@@ -370,12 +469,6 @@ export function getTelegramQueueLaneContract(
370
469
  return contract;
371
470
  }
372
471
 
373
- export function getTelegramQueueItemAdmissionMode(
374
- item: Pick<TelegramQueueItem, "queueLane">,
375
- ): TelegramQueueAdmissionMode {
376
- return getTelegramQueueLaneContract(item.queueLane).admissionMode;
377
- }
378
-
379
472
  export function isTelegramQueueItemAdmissionValid(
380
473
  item: Pick<TelegramQueueItem, "kind" | "queueLane">,
381
474
  ): boolean {
@@ -385,10 +478,7 @@ export function isTelegramQueueItemAdmissionValid(
385
478
  }
386
479
 
387
480
  export function assertTelegramQueueItemAdmissionValid(
388
- item: Pick<
389
- TelegramQueueItem,
390
- "kind" | "queueLane" | "admissionReceipts"
391
- >,
481
+ item: Pick<TelegramQueueItem, "kind" | "queueLane" | "admissionReceipts">,
392
482
  ): void {
393
483
  if (!isTelegramQueueItemAdmissionValid(item)) {
394
484
  throw new Error(
@@ -413,9 +503,7 @@ export function assertTelegramQueueItemAdmissionValid(
413
503
  (index > 0 && updateId <= receipt.sourceUpdateIds[index - 1]!),
414
504
  )
415
505
  ) {
416
- throw new Error(
417
- `Invalid Telegram queue receipt for ${item.kind} item`,
418
- );
506
+ throw new Error(`Invalid Telegram queue receipt for ${item.kind} item`);
419
507
  }
420
508
  receiptIds.add(receipt.receiptId);
421
509
  }
@@ -542,6 +630,42 @@ export function createTelegramActiveTurnStore<
542
630
  };
543
631
  }
544
632
 
633
+ /** Current captured targets only; skipped/receipt-held items remain work, never historical census. */
634
+ export function observeTelegramTargetQueueWork(
635
+ target: TelegramQueueTarget & { threadId: number },
636
+ activeTurn: Pick<TelegramQueueItemBase, "chatId" | "target"> | undefined,
637
+ items: readonly Pick<TelegramQueueItemBase, "chatId" | "target">[],
638
+ ): { protected: boolean; unknown: boolean } {
639
+ let protectedWork = false,
640
+ unknown =
641
+ !Number.isSafeInteger(target.chatId) ||
642
+ !Number.isSafeInteger(target.threadId) ||
643
+ target.threadId <= 0;
644
+ const observe = (item: Pick<TelegramQueueItemBase, "chatId" | "target">) => {
645
+ if (item.chatId !== target.chatId && item.target?.chatId !== target.chatId)
646
+ return;
647
+ const captured = item.target;
648
+ if (
649
+ !captured ||
650
+ captured.chatId !== item.chatId ||
651
+ !Number.isSafeInteger(captured.threadId) ||
652
+ !captured.threadId ||
653
+ captured.threadId < 0
654
+ ) {
655
+ unknown = true;
656
+ return;
657
+ }
658
+ if (
659
+ captured.chatId === target.chatId &&
660
+ captured.threadId === target.threadId
661
+ )
662
+ protectedWork = true;
663
+ };
664
+ if (activeTurn) observe(activeTurn);
665
+ for (const item of items) observe(item);
666
+ return { protected: protectedWork, unknown };
667
+ }
668
+
545
669
  // --- Queue Mutations ---
546
670
 
547
671
  export function partitionTelegramQueueItemsForHistory<TContext = unknown>(
@@ -593,10 +717,7 @@ export function areTelegramQueueAdmissionReceiptsEqual(
593
717
  function getTelegramQueueAdmissionReceiptKey(
594
718
  receipt: TelegramQueueAdmissionReceipt,
595
719
  ): string {
596
- return JSON.stringify([
597
- receipt.receiptId,
598
- receipt.journalBindingKey ?? null,
599
- ]);
720
+ return JSON.stringify([receipt.receiptId, receipt.journalBindingKey ?? null]);
600
721
  }
601
722
 
602
723
  function isDuplicateTelegramQueueAdmission<TContext>(
@@ -661,7 +782,10 @@ export function createTelegramQueueHandoff<TContext>(input: {
661
782
  handoffToken: input.handoffToken,
662
783
  payload: createTelegramQueueHandoffPayload(input.item),
663
784
  };
664
- if (Buffer.byteLength(JSON.stringify(handoff)) > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) {
785
+ if (
786
+ Buffer.byteLength(JSON.stringify(handoff)) >
787
+ TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES
788
+ ) {
665
789
  throw new Error("Telegram queue handoff payload exceeds its byte limit.");
666
790
  }
667
791
  return handoff;
@@ -672,32 +796,14 @@ export function createTelegramQueueHandoffPayload<TContext>(
672
796
  ): TelegramQueueHandoffPayload {
673
797
  assertTelegramQueueItemAdmissionValid(item);
674
798
  if (!item.admissionReceipts?.length) {
675
- throw new Error("Telegram queue handoff requires durable admission receipts.");
799
+ throw new Error(
800
+ "Telegram queue handoff requires durable admission receipts.",
801
+ );
676
802
  }
677
803
  if (item.admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS) {
678
804
  throw new Error("Telegram queue handoff has too many admission receipts.");
679
805
  }
680
- if (item.kind === "control") {
681
- return structuredClone({
682
- kind: item.kind,
683
- controlType: item.controlType,
684
- chatId: item.chatId,
685
- ...(item.target ? { target: item.target } : {}),
686
- ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
687
- replyToMessageId: item.replyToMessageId,
688
- ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
689
- ...(item.guestInlineMessageId
690
- ? { guestInlineMessageId: item.guestInlineMessageId }
691
- : {}),
692
- queueOrder: item.queueOrder,
693
- queueLane: item.queueLane,
694
- laneOrder: item.laneOrder,
695
- statusSummary: item.statusSummary,
696
- admissionReceipts: item.admissionReceipts,
697
- });
698
- }
699
- return structuredClone({
700
- kind: item.kind,
806
+ const base: TelegramQueueHandoffBase = {
701
807
  chatId: item.chatId,
702
808
  ...(item.target ? { target: item.target } : {}),
703
809
  ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
@@ -711,6 +817,17 @@ export function createTelegramQueueHandoffPayload<TContext>(
711
817
  laneOrder: item.laneOrder,
712
818
  statusSummary: item.statusSummary,
713
819
  admissionReceipts: item.admissionReceipts,
820
+ };
821
+ if (item.kind === "control") {
822
+ return structuredClone({
823
+ kind: item.kind,
824
+ controlType: item.controlType,
825
+ ...base,
826
+ });
827
+ }
828
+ return structuredClone({
829
+ kind: item.kind,
830
+ ...base,
714
831
  sourceMessageIds: item.sourceMessageIds,
715
832
  queuedAttachments: item.queuedAttachments,
716
833
  content: item.content,
@@ -1003,9 +1120,11 @@ function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(
1003
1120
  removedCount: number;
1004
1121
  } {
1005
1122
  const index = items.findIndex((item) => {
1006
- return isPendingTelegramTurn(item) &&
1123
+ return (
1124
+ isPendingTelegramTurn(item) &&
1007
1125
  item.guestQueryId !== undefined &&
1008
- item.queueOrder === queueOrder;
1126
+ item.queueOrder === queueOrder
1127
+ );
1009
1128
  });
1010
1129
  if (index < 0) return { items, removedItems: [], removedCount: 0 };
1011
1130
  const removedItem = items[index] as PendingTelegramTurn;
@@ -1016,9 +1135,7 @@ function removeTelegramQueuedGuestPromptByOrder<TContext = unknown>(
1016
1135
  };
1017
1136
  }
1018
1137
 
1019
- export function applyTelegramQueuePromptReactionDisposition<
1020
- TContext = unknown,
1021
- >(
1138
+ export function applyTelegramQueuePromptReactionDisposition<TContext = unknown>(
1022
1139
  items: TelegramQueueItem<TContext>[],
1023
1140
  messageId: number,
1024
1141
  disposition: TelegramQueueReactionDisposition,
@@ -1028,39 +1145,41 @@ export function applyTelegramQueuePromptReactionDisposition<
1028
1145
  let nextItems = items;
1029
1146
  for (const [index, item] of items.entries()) {
1030
1147
  if (
1031
- !isPendingTelegramTurn(item) || item.queueLane === "control" ||
1148
+ !isPendingTelegramTurn(item) ||
1149
+ item.queueLane === "control" ||
1032
1150
  !isTelegramQueueItemInMessageScope(item, scope) ||
1033
1151
  !item.sourceMessageIds.includes(messageId)
1034
1152
  ) {
1035
1153
  continue;
1036
1154
  }
1037
- const isPriority = disposition.kind === "reaction-transition"
1038
- ? disposition.priorityEmoji === undefined
1039
- ? item.queueLane === "priority"
1040
- : disposition.priorityEmoji !== null
1041
- : disposition.kind === "priority" ||
1042
- disposition.kind === "priority-suppressed";
1155
+ const isPriority =
1156
+ disposition.kind === "reaction-transition"
1157
+ ? disposition.priorityEmoji === undefined
1158
+ ? item.queueLane === "priority"
1159
+ : disposition.priorityEmoji !== null
1160
+ : disposition.kind === "priority" ||
1161
+ disposition.kind === "priority-suppressed";
1043
1162
  const queueLane: TelegramQueueLane = isPriority ? "priority" : "default";
1044
- const laneOrder = item.queueLane === queueLane
1045
- ? item.laneOrder
1046
- : destinationLaneOrder;
1163
+ const laneOrder =
1164
+ item.queueLane === queueLane ? item.laneOrder : destinationLaneOrder;
1047
1165
  if (laneOrder === undefined) {
1048
1166
  throw new Error("Telegram destination lane order is unavailable.");
1049
1167
  }
1050
- const priorityEmoji = disposition.kind === "reaction-transition"
1051
- ? disposition.priorityEmoji === undefined
1052
- ? item.priorityEmoji
1053
- : disposition.priorityEmoji ?? undefined
1054
- : disposition.kind === "priority"
1055
- ? disposition.emoji
1056
- : disposition.kind === "priority-suppressed"
1057
- ? disposition.priorityEmoji
1058
- : undefined;
1168
+ const priorityEmoji =
1169
+ disposition.kind === "reaction-transition"
1170
+ ? disposition.priorityEmoji === undefined
1171
+ ? item.priorityEmoji
1172
+ : (disposition.priorityEmoji ?? undefined)
1173
+ : disposition.kind === "priority"
1174
+ ? disposition.emoji
1175
+ : disposition.kind === "priority-suppressed"
1176
+ ? disposition.priorityEmoji
1177
+ : undefined;
1059
1178
  const reactionSuppressionEmoji =
1060
1179
  disposition.kind === "reaction-transition"
1061
1180
  ? disposition.suppressionEmoji === undefined
1062
1181
  ? item.reactionSuppressionEmoji
1063
- : disposition.suppressionEmoji ?? undefined
1182
+ : (disposition.suppressionEmoji ?? undefined)
1064
1183
  : disposition.kind === "suppressed"
1065
1184
  ? disposition.emoji
1066
1185
  : disposition.kind === "priority-suppressed"
@@ -1595,13 +1714,20 @@ export interface TelegramAgentEndRuntimeDeps<
1595
1714
  updateStatus: () => void;
1596
1715
  dispatchNextQueuedTelegramTurn: () => void;
1597
1716
  scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
1598
- preparePreviewDelivery?: (isDeliveryActive: () => boolean) => Pick<
1599
- TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>,
1600
- "clearPreview" | "setPreviewPendingText" | "finalizeMarkdownPreview"
1601
- > | undefined;
1717
+ preparePreviewDelivery?: (
1718
+ isDeliveryActive: () => boolean,
1719
+ ) =>
1720
+ | Pick<
1721
+ TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>,
1722
+ "clearPreview" | "setPreviewPendingText" | "finalizeMarkdownPreview"
1723
+ >
1724
+ | undefined;
1602
1725
  preparePreviewClear?: (
1603
1726
  chatId: number,
1604
- options?: { target?: TelegramQueueTarget; isDeliveryActive?: () => boolean },
1727
+ options?: {
1728
+ target?: TelegramQueueTarget;
1729
+ isDeliveryActive?: () => boolean;
1730
+ },
1605
1731
  ) => () => Promise<void>;
1606
1732
  clearPreview: (
1607
1733
  chatId: number,
@@ -1681,7 +1807,9 @@ export interface TelegramAgentEndHookRuntimeDeps<
1681
1807
  extractAssistant: (
1682
1808
  messages: readonly TMessage[],
1683
1809
  ) => TelegramAgentEndAssistantResult;
1684
- isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
1810
+ isAssistantAlreadyPublished?: (
1811
+ assistant: TelegramAgentEndAssistantResult,
1812
+ ) => boolean;
1685
1813
  getFoldQueuedPromptsIntoHistory: () => boolean;
1686
1814
  resetRuntimeState: () => void;
1687
1815
  isSessionActive?: (ctx: TContext) => boolean;
@@ -1701,8 +1829,14 @@ export interface TelegramAgentEndHookRuntimeDeps<
1701
1829
  schedule: (task: () => Promise<void>) => void;
1702
1830
  cancel: () => void;
1703
1831
  };
1704
- preparePreviewDelivery?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewDelivery"];
1705
- preparePreviewClear?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewClear"];
1832
+ preparePreviewDelivery?: TelegramAgentEndRuntimeDeps<
1833
+ TTurn,
1834
+ TReplyMarkup
1835
+ >["preparePreviewDelivery"];
1836
+ preparePreviewClear?: TelegramAgentEndRuntimeDeps<
1837
+ TTurn,
1838
+ TReplyMarkup
1839
+ >["preparePreviewClear"];
1706
1840
  clearPreview: TelegramAgentEndRuntimeDeps<
1707
1841
  TTurn,
1708
1842
  TReplyMarkup
@@ -1833,16 +1967,28 @@ export function createTelegramAgentEndHook<
1833
1967
  ): Promise<void> => {
1834
1968
  if (deps.isSessionActive?.(ctx) === false) return;
1835
1969
  const turn = deps.getActiveTurn();
1836
- const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1970
+ const extractedAssistant =
1971
+ assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1837
1972
  const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
1838
1973
  ? { stopReason: extractedAssistant.stopReason }
1839
1974
  : extractedAssistant;
1840
- const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
1841
- const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
1842
- const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
1975
+ const hasPublication =
1976
+ !!assistant.text ||
1977
+ assistant.stopReason === "error" ||
1978
+ !!turn?.queuedAttachments.length;
1979
+ const reservation =
1980
+ turn && !turn.guestQueryId && hasPublication
1981
+ ? deps.reserveActiveTurnDelivery?.()
1982
+ : undefined;
1983
+ const scheduleDelivery =
1984
+ reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
1843
1985
  try {
1844
1986
  await deps.loadConfig?.();
1845
- if (deps.isSessionActive?.(ctx) === false || deps.getActiveTurn() !== turn) return;
1987
+ if (
1988
+ deps.isSessionActive?.(ctx) === false ||
1989
+ deps.getActiveTurn() !== turn
1990
+ )
1991
+ return;
1846
1992
  await handleTelegramAgentEndRuntime({
1847
1993
  turn,
1848
1994
  assistant,
@@ -1929,19 +2075,27 @@ export async function handleTelegramAgentEndRuntime<
1929
2075
  const isDeliveryActive = (): boolean =>
1930
2076
  deps.isSessionActive?.() !== false &&
1931
2077
  (!turn || deps.isTurnTransportActive?.(turn) !== false);
1932
- const preview = turn && !turn.guestQueryId ? deps.preparePreviewDelivery?.(isDeliveryActive) : undefined;
1933
- const setPreviewPendingText = preview?.setPreviewPendingText ?? deps.setPreviewPendingText;
1934
- const finalizeMarkdownPreview = preview?.finalizeMarkdownPreview ?? deps.finalizeMarkdownPreview;
2078
+ const preview =
2079
+ turn && !turn.guestQueryId
2080
+ ? deps.preparePreviewDelivery?.(isDeliveryActive)
2081
+ : undefined;
2082
+ const setPreviewPendingText =
2083
+ preview?.setPreviewPendingText ?? deps.setPreviewPendingText;
2084
+ const finalizeMarkdownPreview =
2085
+ preview?.finalizeMarkdownPreview ?? deps.finalizeMarkdownPreview;
1935
2086
  const clearPreview = turn
1936
- ? preview ? () => preview.clearPreview(turn.chatId, { target: turn.target })
1937
- : deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
1938
- ?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
2087
+ ? preview
2088
+ ? () => preview.clearPreview(turn.chatId, { target: turn.target })
2089
+ : (deps.preparePreviewClear?.(turn.chatId, {
2090
+ target: turn.target,
2091
+ isDeliveryActive,
2092
+ }) ?? (() => deps.clearPreview(turn.chatId, { target: turn.target })))
1939
2093
  : undefined;
1940
2094
  const updateStatusIgnoringStaleContext = (): void => {
1941
2095
  try {
1942
2096
  deps.updateStatus();
1943
2097
  } catch (error) {
1944
- if (!isTelegramStaleContextError(error)) throw error;
2098
+ if (!isPiStaleContextError(error)) throw error;
1945
2099
  }
1946
2100
  };
1947
2101
  if (!isDeliveryActive()) {
@@ -2092,9 +2246,11 @@ export async function handleTelegramAgentEndRuntime<
2092
2246
  if (!isDeliveryActive()) return;
2093
2247
  }
2094
2248
  if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
2095
- const errorMessage = assistant.errorMessage ||
2249
+ const errorMessage =
2250
+ assistant.errorMessage ||
2096
2251
  "Telegram bridge: Pi failed while processing the request.";
2097
- const isOperationAborted = endPlan.shouldSendAbortMessage ||
2252
+ const isOperationAborted =
2253
+ endPlan.shouldSendAbortMessage ||
2098
2254
  errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
2099
2255
  try {
2100
2256
  await deps.sendTextReply(
@@ -2118,7 +2274,8 @@ export async function handleTelegramAgentEndRuntime<
2118
2274
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
2119
2275
  return;
2120
2276
  }
2121
- if (finalText && turn.queuedAttachments.length === 0) setPreviewPendingText(finalText);
2277
+ if (finalText && turn.queuedAttachments.length === 0)
2278
+ setPreviewPendingText(finalText);
2122
2279
 
2123
2280
  if (!isDeliveryActive()) return;
2124
2281
  let richAttachmentDelivered = false;
@@ -2261,7 +2418,11 @@ export async function handleTelegramAgentEndRuntime<
2261
2418
  };
2262
2419
  if (
2263
2420
  deps.scheduleActiveTurnDelivery &&
2264
- (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
2421
+ (endPlan.kind === "text" ||
2422
+ endPlan.kind === "attachments-only" ||
2423
+ endPlan.shouldSendAbortMessage ||
2424
+ endPlan.shouldSendErrorMessage ||
2425
+ endPlan.shouldClearPreview)
2265
2426
  ) {
2266
2427
  deps.scheduleActiveTurnDelivery(deliverActiveTurn);
2267
2428
  return;
@@ -2352,7 +2513,13 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
2352
2513
  TContext,
2353
2514
  TQueueItem,
2354
2515
  TModel = unknown,
2355
- > extends TelegramRuntimeEventRecorderPort {
2516
+ >
2517
+ extends
2518
+ TelegramRuntimeEventRecorderPort,
2519
+ Omit<
2520
+ TelegramSessionShutdownRuntimeDeps<TQueueItem>,
2521
+ "isSessionActive" | "discardQueuedItems" | "applyState"
2522
+ > {
2356
2523
  getCurrentModel: (ctx: TContext) => TModel | undefined;
2357
2524
  loadConfig: () => Promise<void>;
2358
2525
  applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
@@ -2360,23 +2527,10 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
2360
2527
  prepareTempDir: () => Promise<unknown>;
2361
2528
  updateStatus: (ctx: TContext) => void;
2362
2529
  isSessionActive?: (ctx: TContext) => boolean;
2363
- unbindDeferredDispatchContext?: () => void;
2364
2530
  discardQueuedItems?: (ctx: TContext) => void;
2365
2531
  applySessionShutdownState: (
2366
2532
  state: TelegramSessionShutdownState<TQueueItem>,
2367
2533
  ) => void;
2368
- clearPendingMediaGroups: () => void;
2369
- clearModelMenuState: () => void;
2370
- getActiveTurnChatId: () => number | undefined;
2371
- getActiveTurnTarget?: () => TelegramQueueTarget | undefined;
2372
- clearPreview: (
2373
- chatId: number,
2374
- options?: { target?: TelegramQueueTarget },
2375
- ) => Promise<void>;
2376
- previewShutdownTimeoutMs?: number;
2377
- clearActiveTurn: () => void;
2378
- clearAbort: () => void;
2379
- stopPolling: () => Promise<void>;
2380
2534
  }
2381
2535
 
2382
2536
  export type TelegramSessionLifecycleHookEvent = unknown;
@@ -2401,9 +2555,8 @@ export function createTelegramSessionStateApplier<TQueueItem, TModel>(
2401
2555
  };
2402
2556
  }
2403
2557
 
2404
- export interface TelegramQueueMutationRuntimeDeps<
2405
- TContext,
2406
- > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2558
+ export interface TelegramQueueMutationRuntimeDeps<TContext>
2559
+ extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2407
2560
  ctx: TContext;
2408
2561
  hasPendingDispatch?: () => boolean;
2409
2562
  allocateLaneOrder?: () => number;
@@ -2414,9 +2567,8 @@ export interface TelegramQueueMutationRuntimeDeps<
2414
2567
  updateStatus: (ctx: TContext) => void;
2415
2568
  }
2416
2569
 
2417
- export interface TelegramQueueMutationControllerDeps<
2418
- TContext,
2419
- > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2570
+ export interface TelegramQueueMutationControllerDeps<TContext>
2571
+ extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2420
2572
  hasPendingDispatch?: () => boolean;
2421
2573
  allocateLaneOrder?: () => number;
2422
2574
  onItemsDiscarded?: (
@@ -2478,6 +2630,8 @@ export interface TelegramPromptEnqueueRuntimeDeps<
2478
2630
  updateStatus: () => void;
2479
2631
  dispatchNextQueuedTelegramTurn: () => void;
2480
2632
  assertExecutionCurrent?: () => void;
2633
+ /** Input-specific relocation must not fold or replace unrelated queued work. */
2634
+ preserveQueued?: boolean;
2481
2635
  onQueued?: (turn: PendingTelegramTurn) => void;
2482
2636
  }
2483
2637
 
@@ -2502,17 +2656,10 @@ export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
2502
2656
  messages: TMessage[],
2503
2657
  ctx: TContext,
2504
2658
  onQueued?: (turn: PendingTelegramTurn) => void,
2659
+ options?: { assertCurrent?: () => void; preserveQueued?: boolean },
2505
2660
  ) => Promise<PendingTelegramTurn>;
2506
2661
  }
2507
2662
 
2508
- function isTelegramStaleContextError(error: unknown): boolean {
2509
- return (
2510
- error instanceof Error &&
2511
- (error.message.includes("stale after session") ||
2512
- error.message.includes("stale ctx"))
2513
- );
2514
- }
2515
-
2516
2663
  export function buildTelegramSessionStartState<TModel = unknown>(
2517
2664
  currentModel: TModel | undefined,
2518
2665
  ): TelegramSessionStartState<TModel> {
@@ -2554,7 +2701,7 @@ export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
2554
2701
  try {
2555
2702
  deps.bindDeferredDispatchContext?.(deps.ctx);
2556
2703
  } catch (error) {
2557
- if (!isTelegramStaleContextError(error)) throw error;
2704
+ if (!isPiStaleContextError(error)) throw error;
2558
2705
  }
2559
2706
  deps.updateStatus();
2560
2707
  }
@@ -2733,7 +2880,7 @@ function updateTelegramQueueStatusRuntime<TContext>(
2733
2880
  try {
2734
2881
  deps.updateStatus(deps.ctx);
2735
2882
  } catch (error) {
2736
- if (!isTelegramStaleContextError(error)) throw error;
2883
+ if (!isPiStaleContextError(error)) throw error;
2737
2884
  }
2738
2885
  }
2739
2886
 
@@ -2741,11 +2888,18 @@ function commitReorderedTelegramQueueItemsRuntime<TContext>(
2741
2888
  items: TelegramQueueItem<TContext>[],
2742
2889
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2743
2890
  ): void {
2744
- const pendingHead = deps.hasPendingDispatch?.() ? deps.getQueuedItems()[0] : undefined;
2891
+ const pendingHead = deps.hasPendingDispatch?.()
2892
+ ? deps.getQueuedItems()[0]
2893
+ : undefined;
2745
2894
  const ordered = [...items].sort(compareTelegramQueueItems);
2746
2895
  if (pendingHead) {
2747
- const index = ordered.findIndex(item => item.queueOrder === pendingHead.queueOrder && item.kind === pendingHead.kind &&
2748
- item.chatId === pendingHead.chatId && item.replyToMessageId === pendingHead.replyToMessageId);
2896
+ const index = ordered.findIndex(
2897
+ (item) =>
2898
+ item.queueOrder === pendingHead.queueOrder &&
2899
+ item.kind === pendingHead.kind &&
2900
+ item.chatId === pendingHead.chatId &&
2901
+ item.replyToMessageId === pendingHead.replyToMessageId,
2902
+ );
2749
2903
  if (index > 0) ordered.unshift(ordered.splice(index, 1)[0]!);
2750
2904
  }
2751
2905
  deps.setQueuedItems(ordered);
@@ -2826,23 +2980,44 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2826
2980
  scope?: TelegramQueueMessageScope,
2827
2981
  ): boolean {
2828
2982
  const queuedItems = deps.getQueuedItems();
2829
- const suppression = disposition.kind === "reaction-transition" ? disposition.suppressionEmoji :
2830
- disposition.kind === "suppressed" ? disposition.emoji : disposition.kind === "priority-suppressed" ? disposition.suppressionEmoji : undefined;
2983
+ const suppression =
2984
+ disposition.kind === "reaction-transition"
2985
+ ? disposition.suppressionEmoji
2986
+ : disposition.kind === "suppressed"
2987
+ ? disposition.emoji
2988
+ : disposition.kind === "priority-suppressed"
2989
+ ? disposition.suppressionEmoji
2990
+ : undefined;
2831
2991
  // Source-addressed control-lane prompts are explicit continuations. Synthetic model-switch turns have no source IDs.
2832
2992
  // A Pi-owned dispatched head cannot be removed: agent_start must consume that exact item, not its successor.
2833
- const cancelled = suppression && deps.hasPendingDispatch ? queuedItems.filter((item, index) =>
2834
- isPendingTelegramTurn(item) && item.queueLane === "control" && isTelegramQueueItemInMessageScope(item, scope) &&
2835
- item.sourceMessageIds.includes(messageId) && !(index === 0 && deps.hasPendingDispatch!())) : [];
2993
+ const cancelled =
2994
+ suppression && deps.hasPendingDispatch
2995
+ ? queuedItems.filter(
2996
+ (item, index) =>
2997
+ isPendingTelegramTurn(item) &&
2998
+ item.queueLane === "control" &&
2999
+ isTelegramQueueItemInMessageScope(item, scope) &&
3000
+ item.sourceMessageIds.includes(messageId) &&
3001
+ !(index === 0 && deps.hasPendingDispatch!()),
3002
+ )
3003
+ : [];
2836
3004
  if (cancelled.length) {
2837
- if (cancelled.some(item => (item.admissionReceipts?.length ?? 0) > 0) && !deps.onItemsDiscarded) return false;
3005
+ if (
3006
+ cancelled.some((item) => (item.admissionReceipts?.length ?? 0) > 0) &&
3007
+ !deps.onItemsDiscarded
3008
+ )
3009
+ return false;
2838
3010
  deps.onItemsDiscarded?.(cancelled, deps.ctx);
2839
- deps.setQueuedItems(queuedItems.filter(item => !cancelled.includes(item)));
3011
+ deps.setQueuedItems(
3012
+ queuedItems.filter((item) => !cancelled.includes(item)),
3013
+ );
2840
3014
  updateTelegramQueueStatusRuntime(deps);
2841
3015
  return true;
2842
3016
  }
2843
3017
  const changesLane = queuedItems.some((item) => {
2844
3018
  if (
2845
- !isPendingTelegramTurn(item) || item.queueLane === "control" ||
3019
+ !isPendingTelegramTurn(item) ||
3020
+ item.queueLane === "control" ||
2846
3021
  !isTelegramQueueItemInMessageScope(item, scope) ||
2847
3022
  !item.sourceMessageIds.includes(messageId)
2848
3023
  ) {
@@ -2885,23 +3060,32 @@ export async function enqueueTelegramPromptTurnRuntime<
2885
3060
  deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
2886
3061
  ): Promise<PendingTelegramTurn> {
2887
3062
  deps.assertExecutionCurrent?.();
2888
- const historyOrders = new Set(planTelegramPromptEnqueue(
2889
- deps.getQueuedItems(),
2890
- deps.getFoldQueuedPromptsIntoHistory(),
2891
- ).historyTurns.map((turn) => turn.queueOrder));
2892
- deps.setFoldQueuedPromptsIntoHistory(false);
3063
+ const historyOrders = new Set(
3064
+ deps.preserveQueued
3065
+ ? []
3066
+ : planTelegramPromptEnqueue(
3067
+ deps.getQueuedItems(),
3068
+ deps.getFoldQueuedPromptsIntoHistory(),
3069
+ ).historyTurns.map((turn) => turn.queueOrder),
3070
+ );
3071
+ if (!deps.preserveQueued) deps.setFoldQueuedPromptsIntoHistory(false);
2893
3072
  const buildTurn = await deps.prepareTurn(messages);
2894
3073
  deps.assertExecutionCurrent?.();
2895
3074
  // Preserve the Pi-owned head until agent_start, plus later arrivals and current edits/reactions.
2896
3075
  const pendingDispatch = deps.hasPendingDispatch();
2897
3076
  const historyTurns: PendingTelegramTurn[] = [];
2898
3077
  const remainingItems = deps.getQueuedItems().filter((item, index) => {
2899
- if ((pendingDispatch && index === 0) || !isPendingTelegramTurn(item) ||
2900
- !historyOrders.has(item.queueOrder)) return true;
3078
+ if (
3079
+ (pendingDispatch && index === 0) ||
3080
+ !isPendingTelegramTurn(item) ||
3081
+ !historyOrders.has(item.queueOrder)
3082
+ )
3083
+ return true;
2901
3084
  historyTurns.push(item);
2902
3085
  return false;
2903
3086
  });
2904
3087
  const turn = buildTurn(historyTurns);
3088
+ deps.assertExecutionCurrent?.();
2905
3089
  deps.setQueuedItems(appendTelegramQueueItem(remainingItems, turn));
2906
3090
  deps.onQueued?.(turn);
2907
3091
  deps.updateStatus();
@@ -2916,15 +3100,18 @@ export function createTelegramPromptEnqueueController<
2916
3100
  deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>,
2917
3101
  ): TelegramPromptEnqueueController<TMessage, TContext> {
2918
3102
  return {
2919
- enqueue: (messages, ctx, onQueued) =>
3103
+ enqueue: (messages, ctx, onQueued, options) =>
2920
3104
  enqueueTelegramPromptTurnRuntime(messages, {
2921
3105
  ...deps,
2922
3106
  prepareTurn: (nextMessages) => deps.prepareTurn(nextMessages, ctx),
2923
3107
  updateStatus: () => deps.updateStatus(ctx),
2924
3108
  dispatchNextQueuedTelegramTurn: () =>
2925
3109
  deps.dispatchNextQueuedTelegramTurn(ctx),
2926
- assertExecutionCurrent: () =>
2927
- deps.assertExecutionCurrent?.(messages),
3110
+ assertExecutionCurrent: () => {
3111
+ deps.assertExecutionCurrent?.(messages);
3112
+ options?.assertCurrent?.();
3113
+ },
3114
+ preserveQueued: options?.preserveQueued,
2928
3115
  onQueued,
2929
3116
  }),
2930
3117
  };
@@ -3165,7 +3352,10 @@ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
3165
3352
  ) => void;
3166
3353
  onPromptDispatchStart: (chatId: number) => void;
3167
3354
  commitPromptDispatch?: (
3168
- item: Extract<TelegramQueueDispatchAction<TContext>, { kind: "prompt" }>["item"],
3355
+ item: Extract<
3356
+ TelegramQueueDispatchAction<TContext>,
3357
+ { kind: "prompt" }
3358
+ >["item"],
3169
3359
  ) => boolean;
3170
3360
  sendUserMessage: (
3171
3361
  content: Extract<
@@ -3191,7 +3381,10 @@ export interface TelegramQueueDispatchControllerDeps<
3191
3381
  sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
3192
3382
  onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
3193
3383
  commitPromptDispatch?: (
3194
- item: Extract<TelegramQueueDispatchAction<TContext>, { kind: "prompt" }>["item"],
3384
+ item: Extract<
3385
+ TelegramQueueDispatchAction<TContext>,
3386
+ { kind: "prompt" }
3387
+ >["item"],
3195
3388
  ctx: TContext,
3196
3389
  ) => boolean;
3197
3390
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
@@ -3232,7 +3425,9 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
3232
3425
  deps.onPromptDispatchStart(plan.item.chatId);
3233
3426
  try {
3234
3427
  if (deps.commitPromptDispatch && !deps.commitPromptDispatch(plan.item)) {
3235
- throw new Error("Telegram prompt dispatch could not be committed durably.");
3428
+ throw new Error(
3429
+ "Telegram prompt dispatch could not be committed durably.",
3430
+ );
3236
3431
  }
3237
3432
  deps.sendUserMessage(plan.item.content);
3238
3433
  } catch (error) {
@@ -3390,9 +3585,13 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3390
3585
  const nextItem = dispatchableItems[0];
3391
3586
  if (nextDispatchAnnouncementRequested) {
3392
3587
  if (nextDispatchAnnouncementAnchor) {
3393
- const anchorRetained = retainedItems.includes(nextDispatchAnnouncementAnchor);
3588
+ const anchorRetained = retainedItems.includes(
3589
+ nextDispatchAnnouncementAnchor,
3590
+ );
3394
3591
  const anchorTransportActive =
3395
- deps.isQueueItemTransportActive?.(nextDispatchAnnouncementAnchor) !== false;
3592
+ deps.isQueueItemTransportActive?.(
3593
+ nextDispatchAnnouncementAnchor,
3594
+ ) !== false;
3396
3595
  if (!anchorRetained || !anchorTransportActive) {
3397
3596
  nextDispatchAnnouncementRequested = false;
3398
3597
  nextDispatchAnnouncementAnchor = undefined;
@@ -3403,10 +3602,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3403
3602
  nextDispatchAnnouncementRequested = false;
3404
3603
  }
3405
3604
  }
3406
- if (
3407
- nextItem &&
3408
- deps.hasPendingInboundQueueMutationForItem?.(nextItem)
3409
- ) {
3605
+ if (nextItem && deps.hasPendingInboundQueueMutationForItem?.(nextItem)) {
3410
3606
  deps.updateStatus(ctx);
3411
3607
  return;
3412
3608
  }
@@ -3429,14 +3625,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3429
3625
  if (dispatchPlan.kind === "prompt") {
3430
3626
  const queueDrifted =
3431
3627
  currentItems.length !== dispatchBasisItems.length ||
3432
- currentItems.some((item, index) => item !== dispatchBasisItems[index]);
3628
+ currentItems.some(
3629
+ (item, index) => item !== dispatchBasisItems[index],
3630
+ );
3433
3631
  const dispatchEligibilityDrifted =
3434
3632
  !deps.canDispatch(ctx) ||
3435
- deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) === true ||
3436
- (deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false) ||
3437
- (deps.isQueueItemTransportActive?.(dispatchPlan.item) === false);
3633
+ deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) ===
3634
+ true ||
3635
+ deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false ||
3636
+ deps.isQueueItemTransportActive?.(dispatchPlan.item) === false;
3438
3637
  if (queueDrifted || dispatchEligibilityDrifted) {
3439
- const selectedItemRetained = currentItems.includes(dispatchPlan.item);
3638
+ const selectedItemRetained = currentItems.includes(
3639
+ dispatchPlan.item,
3640
+ );
3440
3641
  const selectedTransportActive =
3441
3642
  deps.isQueueItemTransportActive?.(dispatchPlan.item) !== false;
3442
3643
  nextDispatchAnnouncementRequested =
@@ -3445,7 +3646,11 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3445
3646
  ? dispatchPlan.item
3446
3647
  : undefined;
3447
3648
  deps.updateStatus(ctx);
3448
- if (nextDispatchAnnouncementRequested && queueDrifted && !dispatchEligibilityDrifted) {
3649
+ if (
3650
+ nextDispatchAnnouncementRequested &&
3651
+ queueDrifted &&
3652
+ !dispatchEligibilityDrifted
3653
+ ) {
3449
3654
  controller.dispatchNext(ctx);
3450
3655
  }
3451
3656
  return false;
@@ -3461,51 +3666,52 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3461
3666
  const executePlan = (): void => {
3462
3667
  if (!commitDispatchPlan()) return;
3463
3668
  executeTelegramQueueDispatchPlan(dispatchPlan, {
3464
- executeControlItem: (item) => {
3465
- controlDispatchPending = true;
3466
- const dispatchGeneration = deps.getDispatchGeneration?.();
3467
- deps.updateStatus(ctx);
3468
- void executeTelegramControlItemRuntime(item, {
3469
- ctx,
3470
- sendTextReply: deps.sendTextReply,
3471
- recordRuntimeEvent: deps.recordRuntimeEvent,
3472
- onSettled: (settledItem) => {
3473
- try {
3474
- deps.onControlSettled?.(settledItem, ctx);
3475
- } catch (error) {
3476
- deps.recordRuntimeEvent?.("control", error, {
3477
- phase: "receipt-settlement",
3478
- controlType: settledItem.controlType,
3479
- });
3480
- }
3481
- controlDispatchPending = false;
3482
- if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3483
- if (
3484
- dispatchGeneration !== undefined &&
3485
- deps.isDispatchGenerationActive &&
3486
- !deps.isDispatchGenerationActive(dispatchGeneration)
3487
- ) {
3488
- return;
3489
- }
3490
- deps.updateStatus(ctx);
3491
- controller.dispatchNext(ctx);
3492
- },
3493
- });
3494
- },
3495
- onPromptDispatchStart: (chatId) => {
3496
- deps.onPromptDispatchStart(ctx, chatId);
3497
- },
3498
- commitPromptDispatch: deps.commitPromptDispatch
3499
- ? (item) => deps.commitPromptDispatch!(item, ctx)
3500
- : undefined,
3501
- sendUserMessage: deps.sendUserMessage,
3502
- onPromptDispatchFailure: (message) => {
3503
- deps.onPromptDispatchFailure(ctx, message);
3504
- },
3505
- onIdle: () => {
3506
- deps.updateStatus(ctx);
3507
- },
3508
- });
3669
+ executeControlItem: (item) => {
3670
+ controlDispatchPending = true;
3671
+ const dispatchGeneration = deps.getDispatchGeneration?.();
3672
+ deps.updateStatus(ctx);
3673
+ void executeTelegramControlItemRuntime(item, {
3674
+ ctx,
3675
+ sendTextReply: deps.sendTextReply,
3676
+ recordRuntimeEvent: deps.recordRuntimeEvent,
3677
+ onSettled: (settledItem) => {
3678
+ try {
3679
+ deps.onControlSettled?.(settledItem, ctx);
3680
+ } catch (error) {
3681
+ deps.recordRuntimeEvent?.("control", error, {
3682
+ phase: "receipt-settlement",
3683
+ controlType: settledItem.controlType,
3684
+ });
3685
+ }
3686
+ controlDispatchPending = false;
3687
+ if (deps.hasDispatchContext && !deps.hasDispatchContext())
3688
+ return;
3689
+ if (
3690
+ dispatchGeneration !== undefined &&
3691
+ deps.isDispatchGenerationActive &&
3692
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3693
+ ) {
3694
+ return;
3695
+ }
3696
+ deps.updateStatus(ctx);
3697
+ controller.dispatchNext(ctx);
3698
+ },
3699
+ });
3700
+ },
3701
+ onPromptDispatchStart: (chatId) => {
3702
+ deps.onPromptDispatchStart(ctx, chatId);
3703
+ },
3704
+ commitPromptDispatch: deps.commitPromptDispatch
3705
+ ? (item) => deps.commitPromptDispatch!(item, ctx)
3706
+ : undefined,
3707
+ sendUserMessage: deps.sendUserMessage,
3708
+ onPromptDispatchFailure: (message) => {
3709
+ deps.onPromptDispatchFailure(ctx, message);
3710
+ },
3711
+ onIdle: () => {
3712
+ deps.updateStatus(ctx);
3713
+ },
3714
+ });
3509
3715
  };
3510
3716
  if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
3511
3717
  nextDispatchAnnouncementRequested = false;
@@ -3513,38 +3719,43 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3513
3719
  const announcementGeneration = nextDispatchAnnouncementGeneration;
3514
3720
  const dispatchGeneration = deps.getDispatchGeneration?.();
3515
3721
  deps.updateStatus(ctx);
3516
- void deps.sendTextReply(
3517
- dispatchPlan.item.chatId,
3518
- dispatchPlan.item.replyToMessageId,
3519
- "<b>⏩ Dispatching next queued turn.</b>",
3520
- { target: dispatchPlan.item.target, parseMode: "HTML" },
3521
- ).catch((error) => {
3522
- deps.recordRuntimeEvent?.("dispatch", error, {
3523
- phase: "next-announcement",
3524
- });
3525
- }).finally(() => {
3526
- try {
3527
- deps.reconcileNextDispatchAnnouncementReplyOwnership?.(
3528
- dispatchPlan.item,
3529
- );
3530
- } catch (error) {
3722
+ void deps
3723
+ .sendTextReply(
3724
+ dispatchPlan.item.chatId,
3725
+ dispatchPlan.item.replyToMessageId,
3726
+ "<b>⏩ Dispatching next queued turn.</b>",
3727
+ { target: dispatchPlan.item.target, parseMode: "HTML" },
3728
+ )
3729
+ .catch((error) => {
3531
3730
  deps.recordRuntimeEvent?.("dispatch", error, {
3532
- phase: "next-announcement-reply-ownership",
3731
+ phase: "next-announcement",
3533
3732
  });
3534
- }
3535
- controlDispatchPending = false;
3536
- if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3537
- if (
3538
- dispatchGeneration !== undefined &&
3539
- deps.isDispatchGenerationActive &&
3540
- !deps.isDispatchGenerationActive(dispatchGeneration)
3541
- ) return;
3542
- if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
3543
- if (nextDispatchAnnouncementRequested) controller.dispatchNext(ctx);
3544
- return;
3545
- }
3546
- executePlan();
3547
- });
3733
+ })
3734
+ .finally(() => {
3735
+ try {
3736
+ deps.reconcileNextDispatchAnnouncementReplyOwnership?.(
3737
+ dispatchPlan.item,
3738
+ );
3739
+ } catch (error) {
3740
+ deps.recordRuntimeEvent?.("dispatch", error, {
3741
+ phase: "next-announcement-reply-ownership",
3742
+ });
3743
+ }
3744
+ controlDispatchPending = false;
3745
+ if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3746
+ if (
3747
+ dispatchGeneration !== undefined &&
3748
+ deps.isDispatchGenerationActive &&
3749
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3750
+ )
3751
+ return;
3752
+ if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
3753
+ if (nextDispatchAnnouncementRequested)
3754
+ controller.dispatchNext(ctx);
3755
+ return;
3756
+ }
3757
+ executePlan();
3758
+ });
3548
3759
  return;
3549
3760
  }
3550
3761
  executePlan();