@llblab/pi-telegram 0.52.2 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
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;
@@ -2199,7 +2356,11 @@ export async function handleTelegramAgentEndRuntime<
2199
2356
  }
2200
2357
  }
2201
2358
  if (!isDeliveryActive()) return;
2202
- if (outboundReply && deps.sendOutboundReplyArtifacts) {
2359
+ if (
2360
+ outboundReply &&
2361
+ hasOutboundArtifacts &&
2362
+ deps.sendOutboundReplyArtifacts
2363
+ ) {
2203
2364
  try {
2204
2365
  await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
2205
2366
  replyToPrompt: !finalText,
@@ -2261,7 +2422,11 @@ export async function handleTelegramAgentEndRuntime<
2261
2422
  };
2262
2423
  if (
2263
2424
  deps.scheduleActiveTurnDelivery &&
2264
- (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
2425
+ (endPlan.kind === "text" ||
2426
+ endPlan.kind === "attachments-only" ||
2427
+ endPlan.shouldSendAbortMessage ||
2428
+ endPlan.shouldSendErrorMessage ||
2429
+ endPlan.shouldClearPreview)
2265
2430
  ) {
2266
2431
  deps.scheduleActiveTurnDelivery(deliverActiveTurn);
2267
2432
  return;
@@ -2352,7 +2517,13 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
2352
2517
  TContext,
2353
2518
  TQueueItem,
2354
2519
  TModel = unknown,
2355
- > extends TelegramRuntimeEventRecorderPort {
2520
+ >
2521
+ extends
2522
+ TelegramRuntimeEventRecorderPort,
2523
+ Omit<
2524
+ TelegramSessionShutdownRuntimeDeps<TQueueItem>,
2525
+ "isSessionActive" | "discardQueuedItems" | "applyState"
2526
+ > {
2356
2527
  getCurrentModel: (ctx: TContext) => TModel | undefined;
2357
2528
  loadConfig: () => Promise<void>;
2358
2529
  applySessionStartState: (state: TelegramSessionStartState<TModel>) => void;
@@ -2360,23 +2531,10 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
2360
2531
  prepareTempDir: () => Promise<unknown>;
2361
2532
  updateStatus: (ctx: TContext) => void;
2362
2533
  isSessionActive?: (ctx: TContext) => boolean;
2363
- unbindDeferredDispatchContext?: () => void;
2364
2534
  discardQueuedItems?: (ctx: TContext) => void;
2365
2535
  applySessionShutdownState: (
2366
2536
  state: TelegramSessionShutdownState<TQueueItem>,
2367
2537
  ) => 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
2538
  }
2381
2539
 
2382
2540
  export type TelegramSessionLifecycleHookEvent = unknown;
@@ -2401,9 +2559,8 @@ export function createTelegramSessionStateApplier<TQueueItem, TModel>(
2401
2559
  };
2402
2560
  }
2403
2561
 
2404
- export interface TelegramQueueMutationRuntimeDeps<
2405
- TContext,
2406
- > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2562
+ export interface TelegramQueueMutationRuntimeDeps<TContext>
2563
+ extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2407
2564
  ctx: TContext;
2408
2565
  hasPendingDispatch?: () => boolean;
2409
2566
  allocateLaneOrder?: () => number;
@@ -2414,9 +2571,8 @@ export interface TelegramQueueMutationRuntimeDeps<
2414
2571
  updateStatus: (ctx: TContext) => void;
2415
2572
  }
2416
2573
 
2417
- export interface TelegramQueueMutationControllerDeps<
2418
- TContext,
2419
- > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2574
+ export interface TelegramQueueMutationControllerDeps<TContext>
2575
+ extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
2420
2576
  hasPendingDispatch?: () => boolean;
2421
2577
  allocateLaneOrder?: () => number;
2422
2578
  onItemsDiscarded?: (
@@ -2478,6 +2634,8 @@ export interface TelegramPromptEnqueueRuntimeDeps<
2478
2634
  updateStatus: () => void;
2479
2635
  dispatchNextQueuedTelegramTurn: () => void;
2480
2636
  assertExecutionCurrent?: () => void;
2637
+ /** Input-specific relocation must not fold or replace unrelated queued work. */
2638
+ preserveQueued?: boolean;
2481
2639
  onQueued?: (turn: PendingTelegramTurn) => void;
2482
2640
  }
2483
2641
 
@@ -2502,17 +2660,10 @@ export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
2502
2660
  messages: TMessage[],
2503
2661
  ctx: TContext,
2504
2662
  onQueued?: (turn: PendingTelegramTurn) => void,
2663
+ options?: { assertCurrent?: () => void; preserveQueued?: boolean },
2505
2664
  ) => Promise<PendingTelegramTurn>;
2506
2665
  }
2507
2666
 
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
2667
  export function buildTelegramSessionStartState<TModel = unknown>(
2517
2668
  currentModel: TModel | undefined,
2518
2669
  ): TelegramSessionStartState<TModel> {
@@ -2554,7 +2705,7 @@ export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
2554
2705
  try {
2555
2706
  deps.bindDeferredDispatchContext?.(deps.ctx);
2556
2707
  } catch (error) {
2557
- if (!isTelegramStaleContextError(error)) throw error;
2708
+ if (!isPiStaleContextError(error)) throw error;
2558
2709
  }
2559
2710
  deps.updateStatus();
2560
2711
  }
@@ -2733,7 +2884,7 @@ function updateTelegramQueueStatusRuntime<TContext>(
2733
2884
  try {
2734
2885
  deps.updateStatus(deps.ctx);
2735
2886
  } catch (error) {
2736
- if (!isTelegramStaleContextError(error)) throw error;
2887
+ if (!isPiStaleContextError(error)) throw error;
2737
2888
  }
2738
2889
  }
2739
2890
 
@@ -2741,11 +2892,18 @@ function commitReorderedTelegramQueueItemsRuntime<TContext>(
2741
2892
  items: TelegramQueueItem<TContext>[],
2742
2893
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
2743
2894
  ): void {
2744
- const pendingHead = deps.hasPendingDispatch?.() ? deps.getQueuedItems()[0] : undefined;
2895
+ const pendingHead = deps.hasPendingDispatch?.()
2896
+ ? deps.getQueuedItems()[0]
2897
+ : undefined;
2745
2898
  const ordered = [...items].sort(compareTelegramQueueItems);
2746
2899
  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);
2900
+ const index = ordered.findIndex(
2901
+ (item) =>
2902
+ item.queueOrder === pendingHead.queueOrder &&
2903
+ item.kind === pendingHead.kind &&
2904
+ item.chatId === pendingHead.chatId &&
2905
+ item.replyToMessageId === pendingHead.replyToMessageId,
2906
+ );
2749
2907
  if (index > 0) ordered.unshift(ordered.splice(index, 1)[0]!);
2750
2908
  }
2751
2909
  deps.setQueuedItems(ordered);
@@ -2826,23 +2984,44 @@ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
2826
2984
  scope?: TelegramQueueMessageScope,
2827
2985
  ): boolean {
2828
2986
  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;
2987
+ const suppression =
2988
+ disposition.kind === "reaction-transition"
2989
+ ? disposition.suppressionEmoji
2990
+ : disposition.kind === "suppressed"
2991
+ ? disposition.emoji
2992
+ : disposition.kind === "priority-suppressed"
2993
+ ? disposition.suppressionEmoji
2994
+ : undefined;
2831
2995
  // Source-addressed control-lane prompts are explicit continuations. Synthetic model-switch turns have no source IDs.
2832
2996
  // 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!())) : [];
2997
+ const cancelled =
2998
+ suppression && deps.hasPendingDispatch
2999
+ ? queuedItems.filter(
3000
+ (item, index) =>
3001
+ isPendingTelegramTurn(item) &&
3002
+ item.queueLane === "control" &&
3003
+ isTelegramQueueItemInMessageScope(item, scope) &&
3004
+ item.sourceMessageIds.includes(messageId) &&
3005
+ !(index === 0 && deps.hasPendingDispatch!()),
3006
+ )
3007
+ : [];
2836
3008
  if (cancelled.length) {
2837
- if (cancelled.some(item => (item.admissionReceipts?.length ?? 0) > 0) && !deps.onItemsDiscarded) return false;
3009
+ if (
3010
+ cancelled.some((item) => (item.admissionReceipts?.length ?? 0) > 0) &&
3011
+ !deps.onItemsDiscarded
3012
+ )
3013
+ return false;
2838
3014
  deps.onItemsDiscarded?.(cancelled, deps.ctx);
2839
- deps.setQueuedItems(queuedItems.filter(item => !cancelled.includes(item)));
3015
+ deps.setQueuedItems(
3016
+ queuedItems.filter((item) => !cancelled.includes(item)),
3017
+ );
2840
3018
  updateTelegramQueueStatusRuntime(deps);
2841
3019
  return true;
2842
3020
  }
2843
3021
  const changesLane = queuedItems.some((item) => {
2844
3022
  if (
2845
- !isPendingTelegramTurn(item) || item.queueLane === "control" ||
3023
+ !isPendingTelegramTurn(item) ||
3024
+ item.queueLane === "control" ||
2846
3025
  !isTelegramQueueItemInMessageScope(item, scope) ||
2847
3026
  !item.sourceMessageIds.includes(messageId)
2848
3027
  ) {
@@ -2885,23 +3064,32 @@ export async function enqueueTelegramPromptTurnRuntime<
2885
3064
  deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
2886
3065
  ): Promise<PendingTelegramTurn> {
2887
3066
  deps.assertExecutionCurrent?.();
2888
- const historyOrders = new Set(planTelegramPromptEnqueue(
2889
- deps.getQueuedItems(),
2890
- deps.getFoldQueuedPromptsIntoHistory(),
2891
- ).historyTurns.map((turn) => turn.queueOrder));
2892
- deps.setFoldQueuedPromptsIntoHistory(false);
3067
+ const historyOrders = new Set(
3068
+ deps.preserveQueued
3069
+ ? []
3070
+ : planTelegramPromptEnqueue(
3071
+ deps.getQueuedItems(),
3072
+ deps.getFoldQueuedPromptsIntoHistory(),
3073
+ ).historyTurns.map((turn) => turn.queueOrder),
3074
+ );
3075
+ if (!deps.preserveQueued) deps.setFoldQueuedPromptsIntoHistory(false);
2893
3076
  const buildTurn = await deps.prepareTurn(messages);
2894
3077
  deps.assertExecutionCurrent?.();
2895
3078
  // Preserve the Pi-owned head until agent_start, plus later arrivals and current edits/reactions.
2896
3079
  const pendingDispatch = deps.hasPendingDispatch();
2897
3080
  const historyTurns: PendingTelegramTurn[] = [];
2898
3081
  const remainingItems = deps.getQueuedItems().filter((item, index) => {
2899
- if ((pendingDispatch && index === 0) || !isPendingTelegramTurn(item) ||
2900
- !historyOrders.has(item.queueOrder)) return true;
3082
+ if (
3083
+ (pendingDispatch && index === 0) ||
3084
+ !isPendingTelegramTurn(item) ||
3085
+ !historyOrders.has(item.queueOrder)
3086
+ )
3087
+ return true;
2901
3088
  historyTurns.push(item);
2902
3089
  return false;
2903
3090
  });
2904
3091
  const turn = buildTurn(historyTurns);
3092
+ deps.assertExecutionCurrent?.();
2905
3093
  deps.setQueuedItems(appendTelegramQueueItem(remainingItems, turn));
2906
3094
  deps.onQueued?.(turn);
2907
3095
  deps.updateStatus();
@@ -2916,15 +3104,18 @@ export function createTelegramPromptEnqueueController<
2916
3104
  deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>,
2917
3105
  ): TelegramPromptEnqueueController<TMessage, TContext> {
2918
3106
  return {
2919
- enqueue: (messages, ctx, onQueued) =>
3107
+ enqueue: (messages, ctx, onQueued, options) =>
2920
3108
  enqueueTelegramPromptTurnRuntime(messages, {
2921
3109
  ...deps,
2922
3110
  prepareTurn: (nextMessages) => deps.prepareTurn(nextMessages, ctx),
2923
3111
  updateStatus: () => deps.updateStatus(ctx),
2924
3112
  dispatchNextQueuedTelegramTurn: () =>
2925
3113
  deps.dispatchNextQueuedTelegramTurn(ctx),
2926
- assertExecutionCurrent: () =>
2927
- deps.assertExecutionCurrent?.(messages),
3114
+ assertExecutionCurrent: () => {
3115
+ deps.assertExecutionCurrent?.(messages);
3116
+ options?.assertCurrent?.();
3117
+ },
3118
+ preserveQueued: options?.preserveQueued,
2928
3119
  onQueued,
2929
3120
  }),
2930
3121
  };
@@ -3165,7 +3356,10 @@ export interface TelegramDispatchRuntimeDeps<TContext = unknown> {
3165
3356
  ) => void;
3166
3357
  onPromptDispatchStart: (chatId: number) => void;
3167
3358
  commitPromptDispatch?: (
3168
- item: Extract<TelegramQueueDispatchAction<TContext>, { kind: "prompt" }>["item"],
3359
+ item: Extract<
3360
+ TelegramQueueDispatchAction<TContext>,
3361
+ { kind: "prompt" }
3362
+ >["item"],
3169
3363
  ) => boolean;
3170
3364
  sendUserMessage: (
3171
3365
  content: Extract<
@@ -3191,7 +3385,10 @@ export interface TelegramQueueDispatchControllerDeps<
3191
3385
  sendTextReply: TelegramControlRuntimeDeps<TContext>["sendTextReply"];
3192
3386
  onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
3193
3387
  commitPromptDispatch?: (
3194
- item: Extract<TelegramQueueDispatchAction<TContext>, { kind: "prompt" }>["item"],
3388
+ item: Extract<
3389
+ TelegramQueueDispatchAction<TContext>,
3390
+ { kind: "prompt" }
3391
+ >["item"],
3195
3392
  ctx: TContext,
3196
3393
  ) => boolean;
3197
3394
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
@@ -3232,7 +3429,9 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
3232
3429
  deps.onPromptDispatchStart(plan.item.chatId);
3233
3430
  try {
3234
3431
  if (deps.commitPromptDispatch && !deps.commitPromptDispatch(plan.item)) {
3235
- throw new Error("Telegram prompt dispatch could not be committed durably.");
3432
+ throw new Error(
3433
+ "Telegram prompt dispatch could not be committed durably.",
3434
+ );
3236
3435
  }
3237
3436
  deps.sendUserMessage(plan.item.content);
3238
3437
  } catch (error) {
@@ -3390,9 +3589,13 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3390
3589
  const nextItem = dispatchableItems[0];
3391
3590
  if (nextDispatchAnnouncementRequested) {
3392
3591
  if (nextDispatchAnnouncementAnchor) {
3393
- const anchorRetained = retainedItems.includes(nextDispatchAnnouncementAnchor);
3592
+ const anchorRetained = retainedItems.includes(
3593
+ nextDispatchAnnouncementAnchor,
3594
+ );
3394
3595
  const anchorTransportActive =
3395
- deps.isQueueItemTransportActive?.(nextDispatchAnnouncementAnchor) !== false;
3596
+ deps.isQueueItemTransportActive?.(
3597
+ nextDispatchAnnouncementAnchor,
3598
+ ) !== false;
3396
3599
  if (!anchorRetained || !anchorTransportActive) {
3397
3600
  nextDispatchAnnouncementRequested = false;
3398
3601
  nextDispatchAnnouncementAnchor = undefined;
@@ -3403,10 +3606,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3403
3606
  nextDispatchAnnouncementRequested = false;
3404
3607
  }
3405
3608
  }
3406
- if (
3407
- nextItem &&
3408
- deps.hasPendingInboundQueueMutationForItem?.(nextItem)
3409
- ) {
3609
+ if (nextItem && deps.hasPendingInboundQueueMutationForItem?.(nextItem)) {
3410
3610
  deps.updateStatus(ctx);
3411
3611
  return;
3412
3612
  }
@@ -3429,14 +3629,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3429
3629
  if (dispatchPlan.kind === "prompt") {
3430
3630
  const queueDrifted =
3431
3631
  currentItems.length !== dispatchBasisItems.length ||
3432
- currentItems.some((item, index) => item !== dispatchBasisItems[index]);
3632
+ currentItems.some(
3633
+ (item, index) => item !== dispatchBasisItems[index],
3634
+ );
3433
3635
  const dispatchEligibilityDrifted =
3434
3636
  !deps.canDispatch(ctx) ||
3435
- deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) === true ||
3436
- (deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false) ||
3437
- (deps.isQueueItemTransportActive?.(dispatchPlan.item) === false);
3637
+ deps.hasPendingInboundQueueMutationForItem?.(dispatchPlan.item) ===
3638
+ true ||
3639
+ deps.isQueueItemAdmissionReady?.(dispatchPlan.item) === false ||
3640
+ deps.isQueueItemTransportActive?.(dispatchPlan.item) === false;
3438
3641
  if (queueDrifted || dispatchEligibilityDrifted) {
3439
- const selectedItemRetained = currentItems.includes(dispatchPlan.item);
3642
+ const selectedItemRetained = currentItems.includes(
3643
+ dispatchPlan.item,
3644
+ );
3440
3645
  const selectedTransportActive =
3441
3646
  deps.isQueueItemTransportActive?.(dispatchPlan.item) !== false;
3442
3647
  nextDispatchAnnouncementRequested =
@@ -3445,7 +3650,11 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3445
3650
  ? dispatchPlan.item
3446
3651
  : undefined;
3447
3652
  deps.updateStatus(ctx);
3448
- if (nextDispatchAnnouncementRequested && queueDrifted && !dispatchEligibilityDrifted) {
3653
+ if (
3654
+ nextDispatchAnnouncementRequested &&
3655
+ queueDrifted &&
3656
+ !dispatchEligibilityDrifted
3657
+ ) {
3449
3658
  controller.dispatchNext(ctx);
3450
3659
  }
3451
3660
  return false;
@@ -3461,51 +3670,52 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3461
3670
  const executePlan = (): void => {
3462
3671
  if (!commitDispatchPlan()) return;
3463
3672
  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
- });
3673
+ executeControlItem: (item) => {
3674
+ controlDispatchPending = true;
3675
+ const dispatchGeneration = deps.getDispatchGeneration?.();
3676
+ deps.updateStatus(ctx);
3677
+ void executeTelegramControlItemRuntime(item, {
3678
+ ctx,
3679
+ sendTextReply: deps.sendTextReply,
3680
+ recordRuntimeEvent: deps.recordRuntimeEvent,
3681
+ onSettled: (settledItem) => {
3682
+ try {
3683
+ deps.onControlSettled?.(settledItem, ctx);
3684
+ } catch (error) {
3685
+ deps.recordRuntimeEvent?.("control", error, {
3686
+ phase: "receipt-settlement",
3687
+ controlType: settledItem.controlType,
3688
+ });
3689
+ }
3690
+ controlDispatchPending = false;
3691
+ if (deps.hasDispatchContext && !deps.hasDispatchContext())
3692
+ return;
3693
+ if (
3694
+ dispatchGeneration !== undefined &&
3695
+ deps.isDispatchGenerationActive &&
3696
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3697
+ ) {
3698
+ return;
3699
+ }
3700
+ deps.updateStatus(ctx);
3701
+ controller.dispatchNext(ctx);
3702
+ },
3703
+ });
3704
+ },
3705
+ onPromptDispatchStart: (chatId) => {
3706
+ deps.onPromptDispatchStart(ctx, chatId);
3707
+ },
3708
+ commitPromptDispatch: deps.commitPromptDispatch
3709
+ ? (item) => deps.commitPromptDispatch!(item, ctx)
3710
+ : undefined,
3711
+ sendUserMessage: deps.sendUserMessage,
3712
+ onPromptDispatchFailure: (message) => {
3713
+ deps.onPromptDispatchFailure(ctx, message);
3714
+ },
3715
+ onIdle: () => {
3716
+ deps.updateStatus(ctx);
3717
+ },
3718
+ });
3509
3719
  };
3510
3720
  if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
3511
3721
  nextDispatchAnnouncementRequested = false;
@@ -3513,38 +3723,43 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
3513
3723
  const announcementGeneration = nextDispatchAnnouncementGeneration;
3514
3724
  const dispatchGeneration = deps.getDispatchGeneration?.();
3515
3725
  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) {
3726
+ void deps
3727
+ .sendTextReply(
3728
+ dispatchPlan.item.chatId,
3729
+ dispatchPlan.item.replyToMessageId,
3730
+ "<b>⏩ Dispatching next queued turn.</b>",
3731
+ { target: dispatchPlan.item.target, parseMode: "HTML" },
3732
+ )
3733
+ .catch((error) => {
3531
3734
  deps.recordRuntimeEvent?.("dispatch", error, {
3532
- phase: "next-announcement-reply-ownership",
3735
+ phase: "next-announcement",
3533
3736
  });
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
- });
3737
+ })
3738
+ .finally(() => {
3739
+ try {
3740
+ deps.reconcileNextDispatchAnnouncementReplyOwnership?.(
3741
+ dispatchPlan.item,
3742
+ );
3743
+ } catch (error) {
3744
+ deps.recordRuntimeEvent?.("dispatch", error, {
3745
+ phase: "next-announcement-reply-ownership",
3746
+ });
3747
+ }
3748
+ controlDispatchPending = false;
3749
+ if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
3750
+ if (
3751
+ dispatchGeneration !== undefined &&
3752
+ deps.isDispatchGenerationActive &&
3753
+ !deps.isDispatchGenerationActive(dispatchGeneration)
3754
+ )
3755
+ return;
3756
+ if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
3757
+ if (nextDispatchAnnouncementRequested)
3758
+ controller.dispatchNext(ctx);
3759
+ return;
3760
+ }
3761
+ executePlan();
3762
+ });
3548
3763
  return;
3549
3764
  }
3550
3765
  executePlan();