@llblab/pi-telegram 0.27.12 → 0.29.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/AGENTS.md +152 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +398 -443
- package/README.md +10 -7
- package/api/updates.ts +5 -0
- package/docs/architecture.md +73 -28
- package/docs/multi-instance-bus.md +23 -5
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +8 -7
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +358 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -5
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +4 -2
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +20 -89
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/skills.ts +21 -0
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +7 -3
- package/scripts/check-downgrade.mjs +435 -0
- package/skills/button-console/SKILL.md +139 -0
- package/skills/telegram-bridge/SKILL.md +138 -0
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<
|
|
313
|
+
item: Pick<
|
|
314
|
+
TelegramQueueItem,
|
|
315
|
+
"kind" | "queueLane" | "admissionReceipts"
|
|
316
|
+
>,
|
|
179
317
|
): void {
|
|
180
|
-
if (isTelegramQueueItemAdmissionValid(item))
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
): {
|
|
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 =
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
888
|
+
removedItems,
|
|
889
|
+
removedCount: removedItems.length,
|
|
423
890
|
};
|
|
424
891
|
}
|
|
425
892
|
|
|
426
|
-
export function
|
|
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
|
|
432
|
-
const
|
|
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
|
-
|
|
909
|
+
continue;
|
|
440
910
|
}
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
923
|
+
item.queueLane === queueLane &&
|
|
924
|
+
item.laneOrder === laneOrder &&
|
|
925
|
+
item.priorityEmoji === priorityEmoji &&
|
|
926
|
+
item.reactionSuppressionEmoji === reactionSuppressionEmoji
|
|
465
927
|
) {
|
|
466
|
-
|
|
928
|
+
continue;
|
|
467
929
|
}
|
|
468
|
-
|
|
469
|
-
|
|
930
|
+
if (nextItems === items) nextItems = [...items];
|
|
931
|
+
nextItems[index] = {
|
|
470
932
|
...item,
|
|
471
|
-
queueLane
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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: (
|
|
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
|
-
|
|
1907
|
-
|
|
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.
|
|
1927
|
-
|
|
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.
|
|
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
|
|
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.
|
|
2452
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
1951
2453
|
} catch (error) {
|
|
1952
|
-
|
|
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 } =
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
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.
|
|
2476
|
+
deps.onItemsDiscarded?.(removedItems, deps.ctx);
|
|
1971
2477
|
} catch (error) {
|
|
1972
|
-
|
|
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
|
|
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
|
|
2000
|
-
|
|
2001
|
-
|
|
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
|
-
|
|
2005
|
-
|
|
2502
|
+
disposition,
|
|
2503
|
+
priorityLaneOrder,
|
|
2006
2504
|
scope,
|
|
2007
2505
|
);
|
|
2008
2506
|
if (!changed) return false;
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
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<
|
|
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
|
-
|
|
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
|
-
|
|
2231
|
-
|
|
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 =
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
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
|
-
|
|
2960
|
+
dispatchableItems,
|
|
2384
2961
|
deps.canDispatch(ctx),
|
|
2385
2962
|
);
|
|
2386
2963
|
if (dispatchPlan.kind !== "none") {
|
|
2387
|
-
deps.setQueuedItems(
|
|
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 (
|