@llblab/pi-telegram 0.27.11 → 0.28.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.
package/lib/queue.ts CHANGED
@@ -4,6 +4,8 @@
4
4
  * Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
5
5
  */
6
6
 
7
+ import { createHash } from "node:crypto";
8
+
7
9
  import { isVoiceTurn } from "./voice.ts";
8
10
 
9
11
  // --- Queue Items ---
@@ -29,6 +31,17 @@ export type TelegramPromptContent =
29
31
 
30
32
  export type TelegramQueueItemKind = "prompt" | "control";
31
33
  export type TelegramQueueLane = "control" | "priority" | "default";
34
+ export type TelegramQueueReactionDisposition =
35
+ | { kind: "default" }
36
+ | { kind: "priority"; emoji: string }
37
+ | { kind: "suppressed"; emoji: string };
38
+
39
+ export interface TelegramQueueAdmissionReceipt {
40
+ queueKind: TelegramQueueItemKind;
41
+ receiptId: string;
42
+ sourceUpdateIds: readonly number[];
43
+ journalBindingKey?: string;
44
+ }
32
45
  export type TelegramQueueAdmissionMode =
33
46
  "control-queue" | "priority-queue" | "default-queue";
34
47
 
@@ -90,6 +103,7 @@ export interface TelegramQueueItemBase {
90
103
  queueLane: TelegramQueueLane;
91
104
  laneOrder: number;
92
105
  statusSummary: string;
106
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
93
107
  }
94
108
 
95
109
  export interface PendingTelegramTurn extends TelegramQueueItemBase {
@@ -99,6 +113,7 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
99
113
  content: TelegramPromptContent[];
100
114
  historyText: string;
101
115
  priorityEmoji?: string;
116
+ reactionSuppressionEmoji?: string;
102
117
 
103
118
  /** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
104
119
  voiceReplyPreferred?: boolean;
@@ -117,6 +132,78 @@ export interface PendingTelegramControlItem<
117
132
  export type TelegramQueueItem<TContext = unknown> =
118
133
  PendingTelegramTurn | PendingTelegramControlItem<TContext>;
119
134
 
135
+ export const TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES = 8 * 1024 * 1024;
136
+ export const TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS = 256;
137
+
138
+ export interface TelegramQueueHandoffBase {
139
+ chatId: number;
140
+ target?: TelegramQueueTarget;
141
+ transportStamp?: TelegramTransportStamp;
142
+ replyToMessageId: number;
143
+ guestQueryId?: string;
144
+ queueOrder: number;
145
+ queueLane: TelegramQueueLane;
146
+ laneOrder: number;
147
+ statusSummary: string;
148
+ admissionReceipts: TelegramQueueAdmissionReceipt[];
149
+ }
150
+
151
+ export interface TelegramPromptQueueHandoffPayload
152
+ extends TelegramQueueHandoffBase {
153
+ kind: "prompt";
154
+ sourceMessageIds: number[];
155
+ queuedAttachments: QueuedAttachment[];
156
+ content: TelegramPromptContent[];
157
+ historyText: string;
158
+ priorityEmoji?: string;
159
+ reactionSuppressionEmoji?: string;
160
+ voiceReplyPreferred?: boolean;
161
+ voiceReplyRequired?: boolean;
162
+ }
163
+
164
+ export interface TelegramControlQueueHandoffPayload
165
+ extends TelegramQueueHandoffBase {
166
+ kind: "control";
167
+ controlType: PendingTelegramControlItem<unknown>["controlType"];
168
+ }
169
+
170
+ export type TelegramQueueHandoffPayload =
171
+ | TelegramPromptQueueHandoffPayload
172
+ | TelegramControlQueueHandoffPayload;
173
+
174
+ export interface TelegramQueueHandoff {
175
+ handoffToken: string;
176
+ payload: TelegramQueueHandoffPayload;
177
+ }
178
+
179
+ export interface TelegramQueueHandoffStageReceipt {
180
+ status: "staged";
181
+ receiptId: string;
182
+ sourceUpdateIds: readonly number[];
183
+ }
184
+
185
+ export interface TelegramQueueHandoffAcceptedOwner {
186
+ instanceId: string;
187
+ processId: number;
188
+ processBirthId: string;
189
+ sessionGeneration: number;
190
+ acquisitionId: string;
191
+ acquiredAtMs: number;
192
+ handoffId?: string;
193
+ }
194
+
195
+ export interface TelegramQueueHandoffStageResult
196
+ extends TelegramQueueHandoffStageReceipt {
197
+ queueOwner: TelegramQueueHandoffAcceptedOwner;
198
+ }
199
+
200
+ export interface TelegramQueueHandoffStagingRuntime {
201
+ stage: (payload: TelegramQueueHandoffPayload) => TelegramQueueHandoffStageReceipt;
202
+ accept: (receipt: TelegramQueueAdmissionReceipt) => boolean;
203
+ cancel: (receipt: TelegramQueueAdmissionReceipt) => boolean;
204
+ hasStaged: (receipt: TelegramQueueAdmissionReceipt) => boolean;
205
+ }
206
+
120
207
  export interface TelegramQueueStore<TContext = unknown> {
121
208
  getQueuedItems: () => TelegramQueueItem<TContext>[];
122
209
  setQueuedItems: (items: TelegramQueueItem<TContext>[]) => void;
@@ -150,6 +237,54 @@ export interface TelegramDispatchGuardState {
150
237
  hasPendingMessages: boolean;
151
238
  }
152
239
 
240
+ export function createTelegramQueueAdmissionReceipt(options: {
241
+ queueKind: TelegramQueueItemKind;
242
+ scope: string;
243
+ sourceUpdateIds: readonly number[];
244
+ }): TelegramQueueAdmissionReceipt | undefined {
245
+ if (options.sourceUpdateIds.length === 0) return undefined;
246
+ const scope = options.scope.trim();
247
+ if (!scope) {
248
+ throw new Error("Telegram queue admission receipt scope is required.");
249
+ }
250
+ if (options.queueKind !== "prompt" && options.queueKind !== "control") {
251
+ throw new Error("Telegram queue admission receipt kind is invalid.");
252
+ }
253
+ const sourceUpdateIds = [...new Set(options.sourceUpdateIds)].sort(
254
+ (left, right) => left - right,
255
+ );
256
+ if (
257
+ sourceUpdateIds.some(
258
+ (updateId) => !Number.isSafeInteger(updateId) || updateId < 0,
259
+ )
260
+ ) {
261
+ throw new Error("Telegram queue admission update ids must be safe integers.");
262
+ }
263
+ const digest = createHash("sha256")
264
+ .update(
265
+ JSON.stringify({
266
+ scope,
267
+ queueKind: options.queueKind,
268
+ sourceUpdateIds,
269
+ }),
270
+ )
271
+ .digest("hex");
272
+ return {
273
+ queueKind: options.queueKind,
274
+ receiptId: `telegram-${options.queueKind}-v1-${digest}`,
275
+ sourceUpdateIds,
276
+ journalBindingKey: scope,
277
+ };
278
+ }
279
+
280
+ export function isTelegramQueueItemDurablyAdmitted<TContext = unknown>(
281
+ item: TelegramQueueItem<TContext>,
282
+ isReceiptCommitted: (receipt: TelegramQueueAdmissionReceipt) => boolean,
283
+ ): boolean {
284
+ assertTelegramQueueItemAdmissionValid(item);
285
+ return (item.admissionReceipts ?? []).every(isReceiptCommitted);
286
+ }
287
+
153
288
  export function getTelegramQueueLaneContract(
154
289
  lane: TelegramQueueLane,
155
290
  ): TelegramQueueLaneContract {
@@ -175,12 +310,40 @@ export function isTelegramQueueItemAdmissionValid(
175
310
  }
176
311
 
177
312
  export function assertTelegramQueueItemAdmissionValid(
178
- item: Pick<TelegramQueueItem, "kind" | "queueLane">,
313
+ item: Pick<
314
+ TelegramQueueItem,
315
+ "kind" | "queueLane" | "admissionReceipts"
316
+ >,
179
317
  ): void {
180
- if (isTelegramQueueItemAdmissionValid(item)) return;
181
- throw new Error(
182
- `Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`,
183
- );
318
+ if (!isTelegramQueueItemAdmissionValid(item)) {
319
+ throw new Error(
320
+ `Invalid Telegram queue admission: ${item.kind} item cannot use ${item.queueLane} lane`,
321
+ );
322
+ }
323
+ const receiptIds = new Set<string>();
324
+ for (const receipt of item.admissionReceipts ?? []) {
325
+ const sourceUpdateIds = new Set(receipt.sourceUpdateIds);
326
+ if (
327
+ receipt.queueKind !== item.kind ||
328
+ !receipt.receiptId ||
329
+ receiptIds.has(receipt.receiptId) ||
330
+ receipt.sourceUpdateIds.length === 0 ||
331
+ (receipt.journalBindingKey !== undefined &&
332
+ receipt.journalBindingKey.trim().length === 0) ||
333
+ sourceUpdateIds.size !== receipt.sourceUpdateIds.length ||
334
+ receipt.sourceUpdateIds.some(
335
+ (updateId, index) =>
336
+ !Number.isSafeInteger(updateId) ||
337
+ updateId < 0 ||
338
+ (index > 0 && updateId <= receipt.sourceUpdateIds[index - 1]!),
339
+ )
340
+ ) {
341
+ throw new Error(
342
+ `Invalid Telegram queue receipt for ${item.kind} item`,
343
+ );
344
+ }
345
+ receiptIds.add(receipt.receiptId);
346
+ }
184
347
  }
185
348
 
186
349
  function getTelegramQueueLaneRank(lane: TelegramQueueLane): number {
@@ -317,6 +480,69 @@ export function planTelegramPromptEnqueue<TContext = unknown>(
317
480
  return partitionTelegramQueueItemsForHistory(items);
318
481
  }
319
482
 
483
+ export function areTelegramQueueAdmissionReceiptsEqual(
484
+ left: TelegramQueueAdmissionReceipt,
485
+ right: TelegramQueueAdmissionReceipt,
486
+ ): boolean {
487
+ return (
488
+ left.queueKind === right.queueKind &&
489
+ left.receiptId === right.receiptId &&
490
+ left.journalBindingKey === right.journalBindingKey &&
491
+ left.sourceUpdateIds.length === right.sourceUpdateIds.length &&
492
+ left.sourceUpdateIds.every(
493
+ (updateId, index) => updateId === right.sourceUpdateIds[index],
494
+ )
495
+ );
496
+ }
497
+
498
+ function getTelegramQueueAdmissionReceiptKey(
499
+ receipt: TelegramQueueAdmissionReceipt,
500
+ ): string {
501
+ return JSON.stringify([
502
+ receipt.receiptId,
503
+ receipt.journalBindingKey ?? null,
504
+ ]);
505
+ }
506
+
507
+ function isDuplicateTelegramQueueAdmission<TContext>(
508
+ items: TelegramQueueItem<TContext>[],
509
+ item: TelegramQueueItem<TContext>,
510
+ ): boolean {
511
+ const incomingReceipts = item.admissionReceipts ?? [];
512
+ if (incomingReceipts.length === 0) return false;
513
+ const queuedReceipts = new Map<
514
+ string,
515
+ { receipt: TelegramQueueAdmissionReceipt; count: number }
516
+ >();
517
+ for (const queuedItem of items) {
518
+ for (const receipt of queuedItem.admissionReceipts ?? []) {
519
+ const key = getTelegramQueueAdmissionReceiptKey(receipt);
520
+ const existing = queuedReceipts.get(key);
521
+ if (existing) existing.count += 1;
522
+ else queuedReceipts.set(key, { receipt, count: 1 });
523
+ }
524
+ }
525
+ let duplicateCount = 0;
526
+ for (const incoming of incomingReceipts) {
527
+ const match = queuedReceipts.get(
528
+ getTelegramQueueAdmissionReceiptKey(incoming),
529
+ );
530
+ if (!match) continue;
531
+ if (
532
+ match.count !== 1 ||
533
+ !areTelegramQueueAdmissionReceiptsEqual(match.receipt, incoming)
534
+ ) {
535
+ throw new Error(
536
+ `Conflicting Telegram queue receipt: ${incoming.receiptId}`,
537
+ );
538
+ }
539
+ duplicateCount += 1;
540
+ }
541
+ if (duplicateCount === 0) return false;
542
+ if (duplicateCount === incomingReceipts.length) return true;
543
+ throw new Error("Telegram queue item overlaps an existing receipt.");
544
+ }
545
+
320
546
  export function appendTelegramQueueItem<
321
547
  TContext = unknown,
322
548
  TItem extends TelegramQueueItem<TContext> = TelegramQueueItem<TContext>,
@@ -325,9 +551,236 @@ export function appendTelegramQueueItem<
325
551
  item: TItem,
326
552
  ): TelegramQueueItem<TContext>[] {
327
553
  assertTelegramQueueItemAdmissionValid(item);
554
+ if (isDuplicateTelegramQueueAdmission(items, item)) return items;
328
555
  return [...items, item];
329
556
  }
330
557
 
558
+ export function createTelegramQueueHandoff<TContext>(input: {
559
+ handoffToken: string;
560
+ item: TelegramQueueItem<TContext>;
561
+ }): TelegramQueueHandoff {
562
+ if (!input.handoffToken) {
563
+ throw new Error("Telegram queue handoff token is required.");
564
+ }
565
+ const handoff = {
566
+ handoffToken: input.handoffToken,
567
+ payload: createTelegramQueueHandoffPayload(input.item),
568
+ };
569
+ if (Buffer.byteLength(JSON.stringify(handoff)) > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) {
570
+ throw new Error("Telegram queue handoff payload exceeds its byte limit.");
571
+ }
572
+ return handoff;
573
+ }
574
+
575
+ export function createTelegramQueueHandoffPayload<TContext>(
576
+ item: TelegramQueueItem<TContext>,
577
+ ): TelegramQueueHandoffPayload {
578
+ assertTelegramQueueItemAdmissionValid(item);
579
+ if (!item.admissionReceipts?.length) {
580
+ throw new Error("Telegram queue handoff requires durable admission receipts.");
581
+ }
582
+ if (item.admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS) {
583
+ throw new Error("Telegram queue handoff has too many admission receipts.");
584
+ }
585
+ if (item.kind === "control") {
586
+ return structuredClone({
587
+ kind: item.kind,
588
+ controlType: item.controlType,
589
+ chatId: item.chatId,
590
+ ...(item.target ? { target: item.target } : {}),
591
+ ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
592
+ replyToMessageId: item.replyToMessageId,
593
+ ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
594
+ queueOrder: item.queueOrder,
595
+ queueLane: item.queueLane,
596
+ laneOrder: item.laneOrder,
597
+ statusSummary: item.statusSummary,
598
+ admissionReceipts: item.admissionReceipts,
599
+ });
600
+ }
601
+ return structuredClone({
602
+ kind: item.kind,
603
+ chatId: item.chatId,
604
+ ...(item.target ? { target: item.target } : {}),
605
+ ...(item.transportStamp ? { transportStamp: item.transportStamp } : {}),
606
+ replyToMessageId: item.replyToMessageId,
607
+ ...(item.guestQueryId ? { guestQueryId: item.guestQueryId } : {}),
608
+ queueOrder: item.queueOrder,
609
+ queueLane: item.queueLane,
610
+ laneOrder: item.laneOrder,
611
+ statusSummary: item.statusSummary,
612
+ admissionReceipts: item.admissionReceipts,
613
+ sourceMessageIds: item.sourceMessageIds,
614
+ queuedAttachments: item.queuedAttachments,
615
+ content: item.content,
616
+ historyText: item.historyText,
617
+ ...(item.priorityEmoji ? { priorityEmoji: item.priorityEmoji } : {}),
618
+ ...(item.reactionSuppressionEmoji
619
+ ? { reactionSuppressionEmoji: item.reactionSuppressionEmoji }
620
+ : {}),
621
+ ...(item.voiceReplyPreferred !== undefined
622
+ ? { voiceReplyPreferred: item.voiceReplyPreferred }
623
+ : {}),
624
+ ...(item.voiceReplyRequired !== undefined
625
+ ? { voiceReplyRequired: item.voiceReplyRequired }
626
+ : {}),
627
+ });
628
+ }
629
+
630
+ export function restoreTelegramQueueHandoffPayload<TContext>(
631
+ payload: TelegramQueueHandoffPayload,
632
+ createControlExecution: (
633
+ payload: TelegramControlQueueHandoffPayload,
634
+ ) => PendingTelegramControlItem<TContext>["execute"],
635
+ ): TelegramQueueItem<TContext> {
636
+ const item: TelegramQueueItem<TContext> =
637
+ payload.kind === "prompt"
638
+ ? structuredClone(payload)
639
+ : {
640
+ ...structuredClone(payload),
641
+ execute: createControlExecution(payload),
642
+ };
643
+ assertTelegramQueueItemAdmissionValid(item);
644
+ return item;
645
+ }
646
+
647
+ function getTelegramQueueHandoffReceipt(
648
+ payload: TelegramQueueHandoffPayload,
649
+ ): TelegramQueueAdmissionReceipt {
650
+ const receipt = payload.admissionReceipts[0];
651
+ if (!receipt || payload.admissionReceipts.length !== 1) {
652
+ throw new Error(
653
+ "Telegram queue handoff requires exactly one complete receipt.",
654
+ );
655
+ }
656
+ return receipt;
657
+ }
658
+
659
+ function findTelegramQueueReceiptItem<TContext>(
660
+ items: readonly TelegramQueueItem<TContext>[],
661
+ receipt: TelegramQueueAdmissionReceipt,
662
+ ): TelegramQueueItem<TContext> | undefined {
663
+ const receiptKey = getTelegramQueueAdmissionReceiptKey(receipt);
664
+ let match: TelegramQueueItem<TContext> | undefined;
665
+ let matchedReceipt: TelegramQueueAdmissionReceipt | undefined;
666
+ for (const item of items) {
667
+ for (const candidate of item.admissionReceipts ?? []) {
668
+ if (getTelegramQueueAdmissionReceiptKey(candidate) !== receiptKey) {
669
+ continue;
670
+ }
671
+ if (match) {
672
+ throw new Error(
673
+ `Conflicting Telegram queue receipt: ${receipt.receiptId}`,
674
+ );
675
+ }
676
+ match = item;
677
+ matchedReceipt = candidate;
678
+ }
679
+ }
680
+ if (!match) return undefined;
681
+ if (
682
+ !matchedReceipt ||
683
+ !areTelegramQueueAdmissionReceiptsEqual(matchedReceipt, receipt)
684
+ ) {
685
+ throw new Error(`Conflicting Telegram queue receipt: ${receipt.receiptId}`);
686
+ }
687
+ return match;
688
+ }
689
+
690
+ export function removeTelegramQueueItemByReceipt<TContext>(input: {
691
+ receipt: TelegramQueueAdmissionReceipt;
692
+ store: TelegramQueueStore<TContext>;
693
+ }): boolean {
694
+ const current = input.store.getQueuedItems();
695
+ const item = findTelegramQueueReceiptItem(current, input.receipt);
696
+ if (!item) return false;
697
+ input.store.setQueuedItems(current.filter((candidate) => candidate !== item));
698
+ return true;
699
+ }
700
+
701
+ export function stageTelegramQueueHandoffPayload<TContext>(input: {
702
+ payload: TelegramQueueHandoffPayload;
703
+ store: TelegramQueueStore<TContext>;
704
+ createControlExecution: (
705
+ payload: TelegramControlQueueHandoffPayload,
706
+ ) => PendingTelegramControlItem<TContext>["execute"];
707
+ }): TelegramQueueHandoffStageReceipt {
708
+ const receipt = getTelegramQueueHandoffReceipt(input.payload);
709
+ const item = restoreTelegramQueueHandoffPayload(
710
+ input.payload,
711
+ input.createControlExecution,
712
+ );
713
+ const current = input.store.getQueuedItems();
714
+ const next = appendTelegramQueueItem(current, item);
715
+ if (next !== current) input.store.setQueuedItems(next);
716
+ return {
717
+ status: "staged",
718
+ receiptId: receipt.receiptId,
719
+ sourceUpdateIds: [...receipt.sourceUpdateIds],
720
+ };
721
+ }
722
+
723
+ export function createTelegramQueueHandoffStagingRuntime<TContext>(input: {
724
+ liveStore: TelegramQueueStore<TContext>;
725
+ createControlExecution: (
726
+ payload: TelegramControlQueueHandoffPayload,
727
+ ) => PendingTelegramControlItem<TContext>["execute"];
728
+ }): TelegramQueueHandoffStagingRuntime {
729
+ const stagedStore = createTelegramQueueStore<TContext>();
730
+ return {
731
+ stage(payload) {
732
+ const receipt = getTelegramQueueHandoffReceipt(payload);
733
+ const liveItem = findTelegramQueueReceiptItem(
734
+ input.liveStore.getQueuedItems(),
735
+ receipt,
736
+ );
737
+ if (liveItem) {
738
+ throw new Error(
739
+ `Telegram queue handoff receipt ${receipt.receiptId} is already live.`,
740
+ );
741
+ }
742
+ return stageTelegramQueueHandoffPayload({
743
+ payload,
744
+ store: stagedStore,
745
+ createControlExecution: input.createControlExecution,
746
+ });
747
+ },
748
+ accept(receipt) {
749
+ const stagedItems = stagedStore.getQueuedItems();
750
+ const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
751
+ if (!stagedItem) {
752
+ return Boolean(
753
+ findTelegramQueueReceiptItem(
754
+ input.liveStore.getQueuedItems(),
755
+ receipt,
756
+ ),
757
+ );
758
+ }
759
+ const current = input.liveStore.getQueuedItems();
760
+ const next = appendTelegramQueueItem(current, stagedItem);
761
+ if (next !== current) input.liveStore.setQueuedItems(next);
762
+ stagedStore.setQueuedItems(
763
+ stagedItems.filter((candidate) => candidate !== stagedItem),
764
+ );
765
+ return true;
766
+ },
767
+ cancel(receipt) {
768
+ const stagedItems = stagedStore.getQueuedItems();
769
+ const stagedItem = findTelegramQueueReceiptItem(stagedItems, receipt);
770
+ if (!stagedItem) return false;
771
+ stagedStore.setQueuedItems(
772
+ stagedItems.filter((candidate) => candidate !== stagedItem),
773
+ );
774
+ return true;
775
+ },
776
+ hasStaged(receipt) {
777
+ return Boolean(
778
+ findTelegramQueueReceiptItem(stagedStore.getQueuedItems(), receipt),
779
+ );
780
+ },
781
+ };
782
+ }
783
+
331
784
  function getTelegramPromptTextSignature(item: PendingTelegramTurn): string {
332
785
  return item.content
333
786
  .filter(
@@ -355,10 +808,18 @@ export function appendTelegramPromptTurnOnce<TContext = unknown>(
355
808
  turn: PendingTelegramTurn,
356
809
  ): { items: TelegramQueueItem<TContext>[]; appended: boolean } {
357
810
  assertTelegramQueueItemAdmissionValid(turn);
358
- const duplicate = items.some(
359
- (item) =>
360
- isPendingTelegramTurn(item) && isDuplicateTelegramPromptTurn(item, turn),
361
- );
811
+ if (isDuplicateTelegramQueueAdmission(items, turn)) {
812
+ return { items, appended: false };
813
+ }
814
+ const hasAdmissionReceipts = (turn.admissionReceipts?.length ?? 0) > 0;
815
+ const duplicate =
816
+ !hasAdmissionReceipts &&
817
+ items.some(
818
+ (item) =>
819
+ isPendingTelegramTurn(item) &&
820
+ (item.admissionReceipts?.length ?? 0) === 0 &&
821
+ isDuplicateTelegramPromptTurn(item, turn),
822
+ );
362
823
  if (duplicate) return { items, appended: false };
363
824
  return { items: [...items, turn], appended: true };
364
825
  }
@@ -402,78 +863,80 @@ export function removeTelegramQueueItemsByMessageIds<TContext = unknown>(
402
863
  items: TelegramQueueItem<TContext>[],
403
864
  messageIds: number[],
404
865
  scope?: TelegramQueueMessageScope,
405
- ): { items: TelegramQueueItem<TContext>[]; removedCount: number } {
866
+ ): {
867
+ items: TelegramQueueItem<TContext>[];
868
+ removedItems: TelegramQueueItem<TContext>[];
869
+ removedCount: number;
870
+ } {
406
871
  if (messageIds.length === 0 || items.length === 0) {
407
- return { items, removedCount: 0 };
872
+ return { items, removedItems: [], removedCount: 0 };
408
873
  }
409
874
  const deletedMessageIds = new Set(messageIds);
410
- const nextItems = items.filter((item) => {
411
- if (
412
- !isPendingTelegramTurn(item) ||
413
- !isTelegramQueueItemInMessageScope(item, scope)
414
- )
415
- return true;
416
- return !item.sourceMessageIds.some((messageId) =>
417
- deletedMessageIds.has(messageId),
418
- );
419
- });
875
+ const nextItems: TelegramQueueItem<TContext>[] = [];
876
+ const removedItems: TelegramQueueItem<TContext>[] = [];
877
+ for (const item of items) {
878
+ const shouldRemove =
879
+ isPendingTelegramTurn(item) &&
880
+ isTelegramQueueItemInMessageScope(item, scope) &&
881
+ item.sourceMessageIds.some((messageId) =>
882
+ deletedMessageIds.has(messageId),
883
+ );
884
+ (shouldRemove ? removedItems : nextItems).push(item);
885
+ }
420
886
  return {
421
887
  items: nextItems,
422
- removedCount: items.length - nextItems.length,
888
+ removedItems,
889
+ removedCount: removedItems.length,
423
890
  };
424
891
  }
425
892
 
426
- export function clearTelegramQueuePromptPriority<TContext = unknown>(
893
+ export function applyTelegramQueuePromptReactionDisposition<
894
+ TContext = unknown,
895
+ >(
427
896
  items: TelegramQueueItem<TContext>[],
428
897
  messageId: number,
898
+ disposition: TelegramQueueReactionDisposition,
899
+ priorityLaneOrder?: number,
429
900
  scope?: TelegramQueueMessageScope,
430
901
  ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
431
- let changed = false;
432
- const nextItems = items.map((item) => {
902
+ let nextItems = items;
903
+ for (const [index, item] of items.entries()) {
433
904
  if (
434
905
  !isPendingTelegramTurn(item) ||
435
906
  !isTelegramQueueItemInMessageScope(item, scope) ||
436
- !item.sourceMessageIds.includes(messageId) ||
437
- item.queueLane !== "priority"
907
+ !item.sourceMessageIds.includes(messageId)
438
908
  ) {
439
- return item;
909
+ continue;
440
910
  }
441
- changed = true;
442
- return {
443
- ...item,
444
- queueLane: "default" as const,
445
- laneOrder: item.queueOrder,
446
- priorityEmoji: undefined,
447
- };
448
- });
449
- return { items: nextItems, changed };
450
- }
451
-
452
- export function prioritizeTelegramQueuePrompt<TContext = unknown>(
453
- items: TelegramQueueItem<TContext>[],
454
- messageId: number,
455
- laneOrder: number,
456
- priorityEmoji = "⚡",
457
- scope?: TelegramQueueMessageScope,
458
- ): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
459
- let changed = false;
460
- const nextItems = items.map((item) => {
911
+ const queueLane: TelegramQueueLane =
912
+ disposition.kind === "priority" ? "priority" : "default";
913
+ const laneOrder =
914
+ disposition.kind === "priority" ? priorityLaneOrder : item.queueOrder;
915
+ if (laneOrder === undefined) {
916
+ throw new Error("Telegram priority reaction order is unavailable.");
917
+ }
918
+ const priorityEmoji =
919
+ disposition.kind === "priority" ? disposition.emoji : undefined;
920
+ const reactionSuppressionEmoji =
921
+ disposition.kind === "suppressed" ? disposition.emoji : undefined;
461
922
  if (
462
- !isPendingTelegramTurn(item) ||
463
- !isTelegramQueueItemInMessageScope(item, scope) ||
464
- !item.sourceMessageIds.includes(messageId)
923
+ item.queueLane === queueLane &&
924
+ item.laneOrder === laneOrder &&
925
+ item.priorityEmoji === priorityEmoji &&
926
+ item.reactionSuppressionEmoji === reactionSuppressionEmoji
465
927
  ) {
466
- return item;
928
+ continue;
467
929
  }
468
- changed = true;
469
- return {
930
+ if (nextItems === items) nextItems = [...items];
931
+ nextItems[index] = {
470
932
  ...item,
471
- queueLane: "priority" as const,
933
+ queueLane,
472
934
  laneOrder,
473
935
  priorityEmoji,
936
+ reactionSuppressionEmoji,
474
937
  };
475
- });
476
- return { items: nextItems, changed };
938
+ }
939
+ return { items: nextItems, changed: nextItems !== items };
477
940
  }
478
941
 
479
942
  export function consumeDispatchedTelegramPrompt<TContext = unknown>(
@@ -558,6 +1021,7 @@ export function buildPendingTelegramControlItem<TContext = unknown>(options: {
558
1021
  queueOrder: number;
559
1022
  laneOrder: number;
560
1023
  statusSummary: string;
1024
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
561
1025
  execute: PendingTelegramControlItem<TContext>["execute"];
562
1026
  }): PendingTelegramControlItem<TContext> {
563
1027
  return {
@@ -570,6 +1034,9 @@ export function buildPendingTelegramControlItem<TContext = unknown>(options: {
570
1034
  queueLane: "control",
571
1035
  laneOrder: options.laneOrder,
572
1036
  statusSummary: options.statusSummary,
1037
+ ...(options.admissionReceipts?.length
1038
+ ? { admissionReceipts: structuredClone(options.admissionReceipts) }
1039
+ : {}),
573
1040
  execute: options.execute,
574
1041
  };
575
1042
  }
@@ -587,6 +1054,7 @@ export function createTelegramControlItemBuilder<TContext = unknown>(
587
1054
  replyToMessageId: number;
588
1055
  controlType: PendingTelegramControlItem<TContext>["controlType"];
589
1056
  statusSummary: string;
1057
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
590
1058
  execute: PendingTelegramControlItem<TContext>["execute"];
591
1059
  }) => PendingTelegramControlItem<TContext> {
592
1060
  return (options) =>
@@ -656,7 +1124,7 @@ export interface TelegramAgentStartPlan<TContext = unknown> {
656
1124
  export interface TelegramAgentStartRuntimeDeps<
657
1125
  TTurn extends PendingTelegramTurn,
658
1126
  TContext = unknown,
659
- > {
1127
+ > extends TelegramRuntimeEventRecorderPort {
660
1128
  queuedItems: TelegramQueueItem<TContext>[];
661
1129
  hasPendingDispatch: boolean;
662
1130
  hasActiveTurn: boolean;
@@ -666,6 +1134,7 @@ export interface TelegramAgentStartRuntimeDeps<
666
1134
  clearDispatchPending: () => void;
667
1135
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
668
1136
  setActiveTurn: (turn: TTurn) => void;
1137
+ onPromptHandedOff?: (turn: TTurn) => void;
669
1138
  createPreviewState: () => void;
670
1139
  startTypingLoop: () => void;
671
1140
  updateStatus: () => void;
@@ -674,7 +1143,7 @@ export interface TelegramAgentStartRuntimeDeps<
674
1143
  export interface TelegramAgentStartHookRuntimeDeps<
675
1144
  TTurn extends PendingTelegramTurn,
676
1145
  TContext = unknown,
677
- > {
1146
+ > extends TelegramRuntimeEventRecorderPort {
678
1147
  setAbortHandler: (ctx: TContext) => void;
679
1148
  getQueuedItems: () => TelegramQueueItem<TContext>[];
680
1149
  hasPendingDispatch: () => boolean;
@@ -685,6 +1154,7 @@ export interface TelegramAgentStartHookRuntimeDeps<
685
1154
  clearDispatchPending: () => void;
686
1155
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
687
1156
  setActiveTurn: (turn: TTurn) => void;
1157
+ onPromptHandedOff?: (turn: TTurn, ctx: TContext) => void;
688
1158
  createPreviewState: () => void;
689
1159
  startTypingLoop: (ctx: TContext) => void;
690
1160
  updateStatus: (ctx: TContext) => void;
@@ -757,7 +1227,15 @@ export function handleTelegramAgentStartRuntime<
757
1227
  deps.setQueuedItems(startPlan.remainingItems);
758
1228
  if (startPlan.shouldClearDispatchPending) deps.clearDispatchPending();
759
1229
  if (startPlan.activeTurn) {
760
- deps.setActiveTurn(startPlan.activeTurn as TTurn);
1230
+ const activeTurn = startPlan.activeTurn as TTurn;
1231
+ deps.setActiveTurn(activeTurn);
1232
+ try {
1233
+ deps.onPromptHandedOff?.(activeTurn);
1234
+ } catch (error) {
1235
+ deps.recordRuntimeEvent?.("queue", error, {
1236
+ phase: "prompt-handoff-receipt-settlement",
1237
+ });
1238
+ }
761
1239
  deps.createPreviewState();
762
1240
  deps.startTypingLoop();
763
1241
  }
@@ -783,7 +1261,9 @@ export function createTelegramAgentStartHook<
783
1261
  clearDispatchPending: deps.clearDispatchPending,
784
1262
  setFoldQueuedPromptsIntoHistory: deps.setFoldQueuedPromptsIntoHistory,
785
1263
  setActiveTurn: deps.setActiveTurn,
1264
+ onPromptHandedOff: (turn) => deps.onPromptHandedOff?.(turn, ctx),
786
1265
  createPreviewState: deps.createPreviewState,
1266
+ recordRuntimeEvent: deps.recordRuntimeEvent,
787
1267
  startTypingLoop: () => deps.startTypingLoop(ctx),
788
1268
  updateStatus: () => deps.updateStatus(ctx),
789
1269
  });
@@ -1619,18 +2099,26 @@ export function createTelegramSessionStateApplier<TQueueItem, TModel>(
1619
2099
 
1620
2100
  export interface TelegramQueueMutationRuntimeDeps<
1621
2101
  TContext,
1622
- > extends TelegramQueueStore<TContext> {
2102
+ > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
1623
2103
  ctx: TContext;
1624
2104
  getNextPriorityReactionOrder?: () => number;
1625
2105
  incrementNextPriorityReactionOrder?: () => void;
2106
+ onItemsDiscarded?: (
2107
+ items: readonly TelegramQueueItem<TContext>[],
2108
+ ctx: TContext,
2109
+ ) => void;
1626
2110
  updateStatus: (ctx: TContext) => void;
1627
2111
  }
1628
2112
 
1629
2113
  export interface TelegramQueueMutationControllerDeps<
1630
2114
  TContext,
1631
- > extends TelegramQueueStore<TContext> {
2115
+ > extends TelegramQueueStore<TContext>, TelegramRuntimeEventRecorderPort {
1632
2116
  getNextPriorityReactionOrder?: () => number;
1633
2117
  incrementNextPriorityReactionOrder?: () => void;
2118
+ onItemsDiscarded?: (
2119
+ items: readonly TelegramQueueItem<TContext>[],
2120
+ ctx: TContext,
2121
+ ) => void;
1634
2122
  updateStatus: (ctx: TContext) => void;
1635
2123
  }
1636
2124
 
@@ -1643,17 +2131,12 @@ export interface TelegramQueueMutationController<TContext> {
1643
2131
  ctx: TContext,
1644
2132
  scope?: TelegramQueueMessageScope,
1645
2133
  ) => number;
1646
- clearPriorityByMessageId: (
2134
+ applyReactionByMessageId: (
1647
2135
  messageId: number,
2136
+ disposition: TelegramQueueReactionDisposition,
1648
2137
  ctx: TContext,
1649
2138
  scope?: TelegramQueueMessageScope,
1650
2139
  ) => boolean;
1651
- prioritizeByMessageId: (
1652
- messageId: number,
1653
- ctx: TContext,
1654
- priorityEmoji?: string,
1655
- scope?: TelegramQueueMessageScope,
1656
- ) => boolean;
1657
2140
  }
1658
2141
 
1659
2142
  export interface TelegramControlQueueControllerDeps<TContext> {
@@ -1665,7 +2148,11 @@ export interface TelegramControlQueueControllerDeps<TContext> {
1665
2148
  }
1666
2149
 
1667
2150
  export interface TelegramControlQueueController<TContext> {
1668
- enqueue: (item: PendingTelegramControlItem<TContext>, ctx: TContext) => void;
2151
+ enqueue: (
2152
+ item: PendingTelegramControlItem<TContext>,
2153
+ ctx: TContext,
2154
+ onQueued?: (item: PendingTelegramControlItem<TContext>) => void,
2155
+ ) => void;
1669
2156
  }
1670
2157
 
1671
2158
  export interface TelegramPromptEnqueueRuntimeDeps<
@@ -1680,6 +2167,8 @@ export interface TelegramPromptEnqueueRuntimeDeps<
1680
2167
  ) => Promise<PendingTelegramTurn>;
1681
2168
  updateStatus: () => void;
1682
2169
  dispatchNextQueuedTelegramTurn: () => void;
2170
+ assertExecutionCurrent?: () => void;
2171
+ onQueued?: (turn: PendingTelegramTurn) => void;
1683
2172
  }
1684
2173
 
1685
2174
  export interface TelegramPromptEnqueueControllerDeps<
@@ -1695,10 +2184,15 @@ export interface TelegramPromptEnqueueControllerDeps<
1695
2184
  ) => Promise<PendingTelegramTurn>;
1696
2185
  updateStatus: (ctx: TContext) => void;
1697
2186
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
2187
+ assertExecutionCurrent?: (messages: TMessage[]) => void;
1698
2188
  }
1699
2189
 
1700
2190
  export interface TelegramPromptEnqueueController<TMessage, TContext = unknown> {
1701
- enqueue: (messages: TMessage[], ctx: TContext) => Promise<void>;
2191
+ enqueue: (
2192
+ messages: TMessage[],
2193
+ ctx: TContext,
2194
+ onQueued?: (turn: PendingTelegramTurn) => void,
2195
+ ) => Promise<PendingTelegramTurn>;
1702
2196
  }
1703
2197
 
1704
2198
  function isTelegramStaleContextError(error: unknown): boolean {
@@ -1903,54 +2397,65 @@ export function createTelegramQueueMutationController<TContext>(
1903
2397
  buildRuntimeDeps(ctx),
1904
2398
  scope,
1905
2399
  ),
1906
- clearPriorityByMessageId: (messageId, ctx, scope) =>
1907
- clearTelegramQueuePromptPriorityRuntime(
1908
- messageId,
1909
- buildRuntimeDeps(ctx),
1910
- scope,
1911
- ),
1912
- prioritizeByMessageId: (messageId, ctx, priorityEmoji, scope) =>
1913
- prioritizeTelegramQueuePromptRuntime(
2400
+ applyReactionByMessageId: (messageId, disposition, ctx, scope) =>
2401
+ applyTelegramQueuePromptReactionDispositionRuntime(
1914
2402
  messageId,
2403
+ disposition,
1915
2404
  buildRuntimeDeps(ctx),
1916
- priorityEmoji,
1917
2405
  scope,
1918
2406
  ),
1919
2407
  };
1920
2408
  }
1921
2409
 
2410
+ function updateTelegramQueueStatusRuntime<TContext>(
2411
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2412
+ ): void {
2413
+ try {
2414
+ deps.updateStatus(deps.ctx);
2415
+ } catch (error) {
2416
+ if (!isTelegramStaleContextError(error)) throw error;
2417
+ }
2418
+ }
2419
+
2420
+ function commitReorderedTelegramQueueItemsRuntime<TContext>(
2421
+ items: TelegramQueueItem<TContext>[],
2422
+ deps: TelegramQueueMutationRuntimeDeps<TContext>,
2423
+ ): void {
2424
+ deps.setQueuedItems([...items].sort(compareTelegramQueueItems));
2425
+ updateTelegramQueueStatusRuntime(deps);
2426
+ }
2427
+
1922
2428
  function appendTelegramQueueItemRuntime<TContext>(
1923
2429
  item: TelegramQueueItem<TContext>,
1924
2430
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1925
2431
  ): void {
1926
- deps.setQueuedItems(appendTelegramQueueItem(deps.getQueuedItems(), item));
1927
- reorderTelegramQueueItemsRuntime(deps);
2432
+ const currentItems = deps.getQueuedItems();
2433
+ const nextItems = appendTelegramQueueItem(currentItems, item);
2434
+ if (nextItems === currentItems) return;
2435
+ commitReorderedTelegramQueueItemsRuntime(nextItems, deps);
1928
2436
  }
1929
2437
 
1930
2438
  export function reorderTelegramQueueItemsRuntime<TContext>(
1931
2439
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1932
2440
  ): void {
1933
- deps.setQueuedItems(
1934
- [...deps.getQueuedItems()].sort(compareTelegramQueueItems),
1935
- );
1936
- try {
1937
- deps.updateStatus(deps.ctx);
1938
- } catch (error) {
1939
- if (!isTelegramStaleContextError(error)) throw error;
1940
- }
2441
+ commitReorderedTelegramQueueItemsRuntime(deps.getQueuedItems(), deps);
1941
2442
  }
1942
2443
 
1943
2444
  export function clearTelegramQueueItemsRuntime<TContext>(
1944
2445
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1945
2446
  ): number {
1946
- const removedCount = deps.getQueuedItems().length;
2447
+ const removedItems = deps.getQueuedItems();
2448
+ const removedCount = removedItems.length;
1947
2449
  if (removedCount === 0) return 0;
1948
2450
  deps.setQueuedItems([]);
1949
2451
  try {
1950
- deps.updateStatus(deps.ctx);
2452
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
1951
2453
  } catch (error) {
1952
- if (!isTelegramStaleContextError(error)) throw error;
2454
+ deps.recordRuntimeEvent?.("queue", error, {
2455
+ phase: "discard-receipt-settlement",
2456
+ });
1953
2457
  }
2458
+ updateTelegramQueueStatusRuntime(deps);
1954
2459
  return removedCount;
1955
2460
  }
1956
2461
 
@@ -1959,56 +2464,50 @@ export function removeTelegramQueueItemsByMessageIdsRuntime<TContext>(
1959
2464
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1960
2465
  scope?: TelegramQueueMessageScope,
1961
2466
  ): number {
1962
- const { items, removedCount } = removeTelegramQueueItemsByMessageIds(
1963
- deps.getQueuedItems(),
1964
- messageIds,
1965
- scope,
1966
- );
2467
+ const { items, removedItems, removedCount } =
2468
+ removeTelegramQueueItemsByMessageIds(
2469
+ deps.getQueuedItems(),
2470
+ messageIds,
2471
+ scope,
2472
+ );
1967
2473
  if (removedCount === 0) return 0;
1968
2474
  deps.setQueuedItems(items);
1969
2475
  try {
1970
- deps.updateStatus(deps.ctx);
2476
+ deps.onItemsDiscarded?.(removedItems, deps.ctx);
1971
2477
  } catch (error) {
1972
- if (!isTelegramStaleContextError(error)) throw error;
2478
+ deps.recordRuntimeEvent?.("queue", error, {
2479
+ phase: "discard-receipt-settlement",
2480
+ });
1973
2481
  }
2482
+ updateTelegramQueueStatusRuntime(deps);
1974
2483
  return removedCount;
1975
2484
  }
1976
2485
 
1977
- export function clearTelegramQueuePromptPriorityRuntime<TContext>(
1978
- messageId: number,
1979
- deps: TelegramQueueMutationRuntimeDeps<TContext>,
1980
- scope?: TelegramQueueMessageScope,
1981
- ): boolean {
1982
- const { changed, items } = clearTelegramQueuePromptPriority(
1983
- deps.getQueuedItems(),
1984
- messageId,
1985
- scope,
1986
- );
1987
- if (!changed) return false;
1988
- deps.setQueuedItems(items);
1989
- reorderTelegramQueueItemsRuntime(deps);
1990
- return true;
1991
- }
1992
-
1993
- export function prioritizeTelegramQueuePromptRuntime<TContext>(
2486
+ export function applyTelegramQueuePromptReactionDispositionRuntime<TContext>(
1994
2487
  messageId: number,
2488
+ disposition: TelegramQueueReactionDisposition,
1995
2489
  deps: TelegramQueueMutationRuntimeDeps<TContext>,
1996
- priorityEmoji?: string,
1997
2490
  scope?: TelegramQueueMessageScope,
1998
2491
  ): boolean {
1999
- const nextPriorityReactionOrder = deps.getNextPriorityReactionOrder?.();
2000
- if (nextPriorityReactionOrder === undefined) return false;
2001
- const { changed, items } = prioritizeTelegramQueuePrompt(
2492
+ const priorityLaneOrder =
2493
+ disposition.kind === "priority"
2494
+ ? deps.getNextPriorityReactionOrder?.()
2495
+ : undefined;
2496
+ if (disposition.kind === "priority" && priorityLaneOrder === undefined) {
2497
+ return false;
2498
+ }
2499
+ const { changed, items } = applyTelegramQueuePromptReactionDisposition(
2002
2500
  deps.getQueuedItems(),
2003
2501
  messageId,
2004
- nextPriorityReactionOrder,
2005
- priorityEmoji,
2502
+ disposition,
2503
+ priorityLaneOrder,
2006
2504
  scope,
2007
2505
  );
2008
2506
  if (!changed) return false;
2009
- deps.setQueuedItems(items);
2010
- deps.incrementNextPriorityReactionOrder?.();
2011
- reorderTelegramQueueItemsRuntime(deps);
2507
+ if (disposition.kind === "priority") {
2508
+ deps.incrementNextPriorityReactionOrder?.();
2509
+ }
2510
+ commitReorderedTelegramQueueItemsRuntime(items, deps);
2012
2511
  return true;
2013
2512
  }
2014
2513
 
@@ -2018,18 +2517,22 @@ export async function enqueueTelegramPromptTurnRuntime<
2018
2517
  >(
2019
2518
  messages: TMessage[],
2020
2519
  deps: TelegramPromptEnqueueRuntimeDeps<TMessage, TContext>,
2021
- ): Promise<void> {
2520
+ ): Promise<PendingTelegramTurn> {
2022
2521
  const enqueuePlan = planTelegramPromptEnqueue(
2023
2522
  deps.getQueuedItems(),
2024
2523
  deps.getFoldQueuedPromptsIntoHistory(),
2025
2524
  );
2525
+ deps.assertExecutionCurrent?.();
2026
2526
  deps.setFoldQueuedPromptsIntoHistory(false);
2027
2527
  const turn = await deps.createTurn(messages, enqueuePlan.historyTurns);
2528
+ deps.assertExecutionCurrent?.();
2028
2529
  deps.setQueuedItems(
2029
2530
  appendTelegramQueueItem(enqueuePlan.remainingItems, turn),
2030
2531
  );
2532
+ deps.onQueued?.(turn);
2031
2533
  deps.updateStatus();
2032
2534
  deps.dispatchNextQueuedTelegramTurn();
2535
+ return turn;
2033
2536
  }
2034
2537
 
2035
2538
  export function createTelegramPromptEnqueueController<
@@ -2039,7 +2542,7 @@ export function createTelegramPromptEnqueueController<
2039
2542
  deps: TelegramPromptEnqueueControllerDeps<TMessage, TContext>,
2040
2543
  ): TelegramPromptEnqueueController<TMessage, TContext> {
2041
2544
  return {
2042
- enqueue: (messages, ctx) =>
2545
+ enqueue: (messages, ctx, onQueued) =>
2043
2546
  enqueueTelegramPromptTurnRuntime(messages, {
2044
2547
  ...deps,
2045
2548
  createTurn: (nextMessages, historyTurns) =>
@@ -2047,6 +2550,9 @@ export function createTelegramPromptEnqueueController<
2047
2550
  updateStatus: () => deps.updateStatus(ctx),
2048
2551
  dispatchNextQueuedTelegramTurn: () =>
2049
2552
  deps.dispatchNextQueuedTelegramTurn(ctx),
2553
+ assertExecutionCurrent: () =>
2554
+ deps.assertExecutionCurrent?.(messages),
2555
+ onQueued,
2050
2556
  }),
2051
2557
  };
2052
2558
  }
@@ -2055,8 +2561,9 @@ export function createTelegramControlQueueController<TContext>(
2055
2561
  deps: TelegramControlQueueControllerDeps<TContext>,
2056
2562
  ): TelegramControlQueueController<TContext> {
2057
2563
  return {
2058
- enqueue: (item, ctx) => {
2564
+ enqueue: (item, ctx, onQueued) => {
2059
2565
  deps.appendControlItem(item, ctx);
2566
+ onQueued?.(item);
2060
2567
  deps.dispatchNextQueuedTelegramTurn(ctx);
2061
2568
  },
2062
2569
  };
@@ -2086,7 +2593,7 @@ export interface TelegramControlRuntimeDeps<
2086
2593
  text: string,
2087
2594
  options?: { target?: TelegramQueueTarget },
2088
2595
  ) => Promise<number | undefined>;
2089
- onSettled: () => void;
2596
+ onSettled: (item: PendingTelegramControlItem<TContext>) => void;
2090
2597
  }
2091
2598
 
2092
2599
  export async function executeTelegramControlItemRuntime<TContext>(
@@ -2109,7 +2616,7 @@ export async function executeTelegramControlItemRuntime<TContext>(
2109
2616
  { target: item.target },
2110
2617
  );
2111
2618
  } finally {
2112
- deps.onSettled();
2619
+ deps.onSettled(item);
2113
2620
  }
2114
2621
  }
2115
2622
 
@@ -2179,7 +2686,18 @@ export function createTelegramDeferredQueueDispatchRuntime<TContext = unknown>(
2179
2686
  timers.delete(timer);
2180
2687
  if (generation !== scheduledGeneration || boundContext === undefined)
2181
2688
  return;
2182
- dispatchNextQueuedTelegramTurn(boundContext);
2689
+ try {
2690
+ dispatchNextQueuedTelegramTurn(boundContext);
2691
+ } catch (error) {
2692
+ try {
2693
+ deps.recordRuntimeEvent?.("dispatch", error, {
2694
+ phase: "deferred-queue-dispatch",
2695
+ generation: scheduledGeneration,
2696
+ });
2697
+ } catch {
2698
+ // Timer diagnostics cannot escape the deferred owner.
2699
+ }
2700
+ }
2183
2701
  }, delayMs);
2184
2702
  timer.unref?.();
2185
2703
  timers.add(timer);
@@ -2227,9 +2745,13 @@ export function createTelegramQueueDispatchWatchdogRuntime<TContext = unknown>(
2227
2745
  try {
2228
2746
  deps.dispatchNextQueuedTelegramTurn(ctx);
2229
2747
  } catch (error) {
2230
- deps.recordRuntimeEvent?.("dispatch", error, {
2231
- phase: "queue-watchdog",
2232
- });
2748
+ try {
2749
+ deps.recordRuntimeEvent?.("dispatch", error, {
2750
+ phase: "queue-watchdog",
2751
+ });
2752
+ } catch {
2753
+ // Watchdog diagnostics cannot escape the interval owner.
2754
+ }
2233
2755
  } finally {
2234
2756
  dispatchInFlight = false;
2235
2757
  }
@@ -2295,6 +2817,14 @@ export interface TelegramQueueDispatchControllerDeps<
2295
2817
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
2296
2818
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
2297
2819
  isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
2820
+ hasPendingInboundQueueMutationForItem?: (
2821
+ item: TelegramQueueItem<TContext>,
2822
+ ) => boolean;
2823
+ isQueueItemAdmissionReady?: (item: TelegramQueueItem<TContext>) => boolean;
2824
+ onControlSettled?: (
2825
+ item: PendingTelegramControlItem<TContext>,
2826
+ ctx: TContext,
2827
+ ) => void;
2298
2828
  }
2299
2829
 
2300
2830
  export interface TelegramQueueDispatchController<TContext = unknown> {
@@ -2350,6 +2880,10 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
2350
2880
  sendUserMessage: deps.sendUserMessage,
2351
2881
  onPromptDispatchFailure: deps.onPromptDispatchFailure,
2352
2882
  isQueueItemTransportActive: deps.isQueueItemTransportActive,
2883
+ hasPendingInboundQueueMutationForItem:
2884
+ deps.hasPendingInboundQueueMutationForItem,
2885
+ isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
2886
+ onControlSettled: deps.onControlSettled,
2353
2887
  recordRuntimeEvent: deps.recordRuntimeEvent,
2354
2888
  });
2355
2889
  }
@@ -2366,11 +2900,26 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2366
2900
  return;
2367
2901
  }
2368
2902
  const queuedItems = deps.getQueuedItems();
2369
- const activeItems = deps.isQueueItemTransportActive
2370
- ? queuedItems.filter(deps.isQueueItemTransportActive)
2371
- : queuedItems;
2372
- if (activeItems.length !== queuedItems.length) {
2373
- deps.setQueuedItems(activeItems);
2903
+ const activeItems: TelegramQueueItem<TContext>[] = [];
2904
+ const protectedInactiveItems: TelegramQueueItem<TContext>[] = [];
2905
+ const retainedItems: TelegramQueueItem<TContext>[] = [];
2906
+ let droppedInactiveItemCount = 0;
2907
+ for (const item of queuedItems) {
2908
+ if (
2909
+ !deps.isQueueItemTransportActive ||
2910
+ deps.isQueueItemTransportActive(item)
2911
+ ) {
2912
+ activeItems.push(item);
2913
+ retainedItems.push(item);
2914
+ } else if ((item.admissionReceipts?.length ?? 0) > 0) {
2915
+ protectedInactiveItems.push(item);
2916
+ retainedItems.push(item);
2917
+ } else {
2918
+ droppedInactiveItemCount += 1;
2919
+ }
2920
+ }
2921
+ if (droppedInactiveItemCount > 0) {
2922
+ deps.setQueuedItems(retainedItems);
2374
2923
  deps.recordRuntimeEvent?.(
2375
2924
  "dispatch",
2376
2925
  new Error(
@@ -2379,12 +2928,44 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2379
2928
  { phase: "transport-generation" },
2380
2929
  );
2381
2930
  }
2931
+ const dispatchableItems: TelegramQueueItem<TContext>[] = [];
2932
+ const suppressedActiveItems: TelegramQueueItem<TContext>[] = [];
2933
+ for (const item of activeItems) {
2934
+ if (
2935
+ item.kind === "prompt" &&
2936
+ item.reactionSuppressionEmoji !== undefined
2937
+ ) {
2938
+ suppressedActiveItems.push(item);
2939
+ } else {
2940
+ dispatchableItems.push(item);
2941
+ }
2942
+ }
2943
+ const nextItem = dispatchableItems[0];
2944
+ if (
2945
+ nextItem &&
2946
+ deps.hasPendingInboundQueueMutationForItem?.(nextItem)
2947
+ ) {
2948
+ deps.updateStatus(ctx);
2949
+ return;
2950
+ }
2951
+ if (
2952
+ nextItem &&
2953
+ deps.isQueueItemAdmissionReady &&
2954
+ !deps.isQueueItemAdmissionReady(nextItem)
2955
+ ) {
2956
+ deps.updateStatus(ctx);
2957
+ return;
2958
+ }
2382
2959
  const dispatchPlan = planNextTelegramQueueAction(
2383
- activeItems,
2960
+ dispatchableItems,
2384
2961
  deps.canDispatch(ctx),
2385
2962
  );
2386
2963
  if (dispatchPlan.kind !== "none") {
2387
- deps.setQueuedItems(dispatchPlan.remainingItems);
2964
+ deps.setQueuedItems([
2965
+ ...dispatchPlan.remainingItems,
2966
+ ...suppressedActiveItems,
2967
+ ...protectedInactiveItems,
2968
+ ]);
2388
2969
  }
2389
2970
  executeTelegramQueueDispatchPlan(dispatchPlan, {
2390
2971
  executeControlItem: (item) => {
@@ -2395,7 +2976,15 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2395
2976
  ctx,
2396
2977
  sendTextReply: deps.sendTextReply,
2397
2978
  recordRuntimeEvent: deps.recordRuntimeEvent,
2398
- onSettled: () => {
2979
+ onSettled: (settledItem) => {
2980
+ try {
2981
+ deps.onControlSettled?.(settledItem, ctx);
2982
+ } catch (error) {
2983
+ deps.recordRuntimeEvent?.("control", error, {
2984
+ phase: "receipt-settlement",
2985
+ controlType: settledItem.controlType,
2986
+ });
2987
+ }
2399
2988
  controlDispatchPending = false;
2400
2989
  if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
2401
2990
  if (