@llblab/pi-telegram 0.27.12 → 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/routing.ts CHANGED
@@ -515,6 +515,8 @@ export interface TelegramInboundRouteRuntimeDeps<
515
515
  body: Record<string, unknown>,
516
516
  ) => Promise<TResponse>;
517
517
  getCurrentInstanceId?: () => string | undefined;
518
+ getAdmissionScope?: () => string | undefined;
519
+ getAdmissionJournalBinding?: () => string | undefined;
518
520
  getMessageOwnership?: Updates.TelegramMessageOwnershipLookup;
519
521
  getTargetOwnership?: Updates.TelegramTargetOwnershipLookup;
520
522
  recordMessageOwnership?: Updates.TelegramMessageOwnershipRecorder;
@@ -591,6 +593,7 @@ export interface TelegramInboundRouteRuntimeDeps<
591
593
  inboundHandlerRuntime: TelegramInboundHandlerRuntime<TContext>;
592
594
  threadStore?: Threads.TelegramTopicTargetStore;
593
595
  updateStatus: (ctx: TContext, error?: string) => void;
596
+ isContextActive?: (ctx: TContext) => boolean;
594
597
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
595
598
  requestDeferredDispatchNextQueuedTelegramTurn?: (
596
599
  dispatch: (ctx: TContext) => void,
@@ -743,6 +746,32 @@ export function createTelegramInboundRouteRuntime<
743
746
  );
744
747
  }
745
748
  };
749
+ const createAdmissionReceipts = (
750
+ queueKind: Queue.TelegramQueueItemKind,
751
+ sources: readonly unknown[],
752
+ ): Queue.TelegramQueueAdmissionReceipt[] => {
753
+ const sourceUpdateIds =
754
+ Updates.collectTelegramAdmissionSourceUpdateIds(sources);
755
+ if (sourceUpdateIds.length === 0) return [];
756
+ const receipt = Queue.createTelegramQueueAdmissionReceipt({
757
+ queueKind,
758
+ scope: deps.getAdmissionScope?.() ?? "",
759
+ sourceUpdateIds,
760
+ });
761
+ const journalBindingKey = deps.getAdmissionJournalBinding?.();
762
+ return receipt
763
+ ? [{
764
+ ...receipt,
765
+ ...(journalBindingKey ? { journalBindingKey } : {}),
766
+ }]
767
+ : [];
768
+ };
769
+ const reportQueueAdmission = (
770
+ sources: readonly unknown[],
771
+ receipts: readonly Queue.TelegramQueueAdmissionReceipt[],
772
+ ): void => {
773
+ Updates.reportTelegramQueueAdmission(sources, receipts);
774
+ };
746
775
  const prunePendingUnboundReroutes = () => {
747
776
  const nowMs = Date.now();
748
777
  for (const [id, entry] of pendingUnboundReroutes) {
@@ -811,10 +840,15 @@ export function createTelegramInboundRouteRuntime<
811
840
  prompt: string,
812
841
  ctx: TContext,
813
842
  target?: Queue.TelegramQueueTarget,
843
+ source?: unknown,
814
844
  ) => {
815
845
  const chatId = target?.chatId ?? deps.configStore.getAllowedUserId();
816
846
  if (typeof chatId !== "number") return;
817
847
  const order = deps.bridgeRuntime.queue.allocateItemOrder();
848
+ const admissionReceipts = createAdmissionReceipts(
849
+ "prompt",
850
+ source === undefined ? [] : [source],
851
+ );
818
852
  const turn: Queue.PendingTelegramTurn = {
819
853
  kind: "prompt",
820
854
  chatId,
@@ -833,8 +867,13 @@ export function createTelegramInboundRouteRuntime<
833
867
  ],
834
868
  historyText: Turns.truncateTelegramQueueSummary(prompt),
835
869
  statusSummary: Turns.truncateTelegramQueueSummary(prompt),
870
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
836
871
  };
837
872
  deps.queueMutationRuntime.append(turn, ctx);
873
+ reportQueueAdmission(
874
+ source === undefined ? [] : [source],
875
+ admissionReceipts,
876
+ );
838
877
  deps.updateStatus(ctx);
839
878
  requestDispatchNextQueuedTelegramTurn(ctx);
840
879
  },
@@ -845,17 +884,22 @@ export function createTelegramInboundRouteRuntime<
845
884
  ): TMessage[] => {
846
885
  return messages.map(
847
886
  (message) =>
848
- ({
849
- ...message,
850
- message_id: 0,
851
- message_thread_id: threadId,
852
- reply_to_message: undefined,
853
- }) as TMessage,
887
+ Updates.carryTelegramUpdateExecutionFence(
888
+ message,
889
+ {
890
+ ...message,
891
+ message_id: 0,
892
+ message_thread_id: threadId,
893
+ reply_to_message: undefined,
894
+ } as TMessage,
895
+ ),
854
896
  );
855
897
  };
856
898
  const applyThreadCleanupPlan = async (
857
899
  plan: ThreadReconciler.ThreadReconciliationPlan,
900
+ assertExecutionCurrent?: () => void,
858
901
  ): Promise<boolean> => {
902
+ assertExecutionCurrent?.();
859
903
  deps.recordThreadReconciliationPlan?.(plan);
860
904
  const result = await ThreadReconciler.applyThreadReconciliationPlan(plan, {
861
905
  callApi: deps.callApi,
@@ -871,10 +915,12 @@ export function createTelegramInboundRouteRuntime<
871
915
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
872
916
  recordRuntimeEvent: deps.recordRuntimeEvent,
873
917
  });
918
+ assertExecutionCurrent?.();
874
919
  return (result.incompleteActions?.length ?? 0) === 0;
875
920
  };
876
921
  const dismissRerouteChooserMessage = async (
877
922
  query: TCallbackQuery,
923
+ assertExecutionCurrent?: () => void,
878
924
  ): Promise<boolean> => {
879
925
  const chatId = query.message?.chat?.id;
880
926
  const messageId = query.message?.message_id;
@@ -886,7 +932,9 @@ export function createTelegramInboundRouteRuntime<
886
932
  return false;
887
933
  }
888
934
  try {
935
+ assertExecutionCurrent?.();
889
936
  await deps.deleteMessage(chatId, messageId);
937
+ assertExecutionCurrent?.();
890
938
  return true;
891
939
  } catch (error) {
892
940
  deps.recordRuntimeEvent?.("telegram", error, {
@@ -901,6 +949,7 @@ export function createTelegramInboundRouteRuntime<
901
949
  const closeReroutedUnboundTopic = async (
902
950
  target: { chatId: number; threadId: number } | undefined,
903
951
  messageId: number | undefined,
952
+ assertExecutionCurrent?: () => void,
904
953
  ): Promise<boolean> => {
905
954
  if (!target || !deps.threadStore) return true;
906
955
  const nowMs = Date.now();
@@ -923,10 +972,11 @@ export function createTelegramInboundRouteRuntime<
923
972
  },
924
973
  ],
925
974
  });
926
- return applyThreadCleanupPlan(plan);
975
+ return applyThreadCleanupPlan(plan, assertExecutionCurrent);
927
976
  };
928
977
  const closePreviousLeaderThread = async (
929
978
  target: { chatId: number; threadId: number } | undefined,
979
+ assertExecutionCurrent?: () => void,
930
980
  ): Promise<boolean> => {
931
981
  if (!target || !deps.threadStore) return true;
932
982
  const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
@@ -942,11 +992,12 @@ export function createTelegramInboundRouteRuntime<
942
992
  : {}),
943
993
  },
944
994
  ],
945
- });
995
+ }, assertExecutionCurrent);
946
996
  };
947
997
  const closeReplacedFollowerThread = async (
948
998
  target: { chatId: number; threadId: number } | undefined,
949
999
  instanceId: string | undefined,
1000
+ assertExecutionCurrent?: () => void,
950
1001
  ): Promise<boolean> => {
951
1002
  if (!target || !deps.threadStore) return true;
952
1003
  const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
@@ -962,18 +1013,27 @@ export function createTelegramInboundRouteRuntime<
962
1013
  : {}),
963
1014
  },
964
1015
  ],
965
- });
1016
+ }, assertExecutionCurrent);
966
1017
  };
967
1018
  const retryPendingRerouteCleanup = async (
968
1019
  cleanup: PendingRerouteCleanup,
1020
+ assertExecutionCurrent?: () => void,
969
1021
  ): Promise<boolean> => {
970
1022
  if (cleanup.kind === "unbound") {
971
- return closeReroutedUnboundTopic(cleanup.target, cleanup.messageId);
1023
+ return closeReroutedUnboundTopic(
1024
+ cleanup.target,
1025
+ cleanup.messageId,
1026
+ assertExecutionCurrent,
1027
+ );
972
1028
  }
973
1029
  if (cleanup.kind === "previous-leader") {
974
- return closePreviousLeaderThread(cleanup.target);
1030
+ return closePreviousLeaderThread(cleanup.target, assertExecutionCurrent);
975
1031
  }
976
- return closeReplacedFollowerThread(cleanup.target, cleanup.instanceId);
1032
+ return closeReplacedFollowerThread(
1033
+ cleanup.target,
1034
+ cleanup.instanceId,
1035
+ assertExecutionCurrent,
1036
+ );
977
1037
  };
978
1038
  let dispatchReroutedCommandMessages:
979
1039
  | ((messages: TMessage[], ctx: TContext) => Promise<void>)
@@ -994,8 +1054,12 @@ export function createTelegramInboundRouteRuntime<
994
1054
  pending: PendingUnboundReroute,
995
1055
  query: TCallbackQuery,
996
1056
  successMessage: string,
1057
+ assertExecutionCurrent?: () => void,
997
1058
  ): Promise<void> => {
998
- const dismissed = await dismissRerouteChooserMessage(query);
1059
+ const dismissed = await dismissRerouteChooserMessage(
1060
+ query,
1061
+ assertExecutionCurrent,
1062
+ );
999
1063
  if (dismissed) {
1000
1064
  pendingUnboundReroutes.delete(rerouteId);
1001
1065
  await deps.answerCallbackQuery(query.id, successMessage);
@@ -1012,7 +1076,9 @@ export function createTelegramInboundRouteRuntime<
1012
1076
  instanceId: string,
1013
1077
  threadId: number,
1014
1078
  ctx: TContext,
1079
+ assertExecutionCurrent?: () => void,
1015
1080
  ): Promise<boolean> => {
1081
+ assertExecutionCurrent?.();
1016
1082
  const forwardMessage = deps.foreignOwnedUpdateForwarder?.forwardMessage;
1017
1083
  if (!forwardMessage) return false;
1018
1084
  const messages = cloneTelegramMessagesForThread(pending.messages, threadId);
@@ -1025,9 +1091,15 @@ export function createTelegramInboundRouteRuntime<
1025
1091
  }),
1026
1092
  ),
1027
1093
  );
1094
+ assertExecutionCurrent?.();
1028
1095
  pending.messages = pending.messages.filter((_, index) => {
1029
1096
  const outcome = outcomes[index];
1030
- if (outcome?.status === "fulfilled" && outcome.value) return false;
1097
+ if (
1098
+ outcome?.status === "fulfilled" &&
1099
+ outcome.value.status === "accepted"
1100
+ ) {
1101
+ return false;
1102
+ }
1031
1103
  if (outcome?.status === "rejected") {
1032
1104
  deps.recordRuntimeEvent?.("bus", outcome.reason, {
1033
1105
  phase: "reroute-foreign-forward",
@@ -1100,6 +1172,9 @@ export function createTelegramInboundRouteRuntime<
1100
1172
  ): Promise<boolean> => {
1101
1173
  const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
1102
1174
  if (!parsed) return false;
1175
+ const assertExecutionCurrent =
1176
+ Updates.createTelegramUpdateExecutionFenceGuard(query);
1177
+ assertExecutionCurrent();
1103
1178
  const chatId = query.message?.chat?.id;
1104
1179
  const pending = pendingUnboundReroutes.get(parsed.rerouteId);
1105
1180
  if (typeof chatId !== "number" || !deps.threadStore || !pending) {
@@ -1107,12 +1182,14 @@ export function createTelegramInboundRouteRuntime<
1107
1182
  return true;
1108
1183
  }
1109
1184
  await deps.threadStore.load();
1185
+ assertExecutionCurrent();
1110
1186
  if (pending.finalizeMessage) {
1111
1187
  await finalizePendingReroute(
1112
1188
  parsed.rerouteId,
1113
1189
  pending,
1114
1190
  query,
1115
1191
  pending.finalizeMessage,
1192
+ assertExecutionCurrent,
1116
1193
  );
1117
1194
  return true;
1118
1195
  }
@@ -1123,6 +1200,7 @@ export function createTelegramInboundRouteRuntime<
1123
1200
  retry.instanceId,
1124
1201
  retry.threadId,
1125
1202
  ctx,
1203
+ assertExecutionCurrent,
1126
1204
  );
1127
1205
  if (!allForwarded) {
1128
1206
  await deps.answerCallbackQuery(
@@ -1135,7 +1213,10 @@ export function createTelegramInboundRouteRuntime<
1135
1213
  if (retry.cleanup) pending.cleanup = retry.cleanup;
1136
1214
  }
1137
1215
  if (pending.cleanup) {
1138
- const cleanupComplete = await retryPendingRerouteCleanup(pending.cleanup);
1216
+ const cleanupComplete = await retryPendingRerouteCleanup(
1217
+ pending.cleanup,
1218
+ assertExecutionCurrent,
1219
+ );
1139
1220
  if (!cleanupComplete) {
1140
1221
  await deps.answerCallbackQuery(
1141
1222
  query.id,
@@ -1149,6 +1230,7 @@ export function createTelegramInboundRouteRuntime<
1149
1230
  pending,
1150
1231
  query,
1151
1232
  "Thread cleanup completed.",
1233
+ assertExecutionCurrent,
1152
1234
  );
1153
1235
  return true;
1154
1236
  }
@@ -1192,11 +1274,13 @@ export function createTelegramInboundRouteRuntime<
1192
1274
  );
1193
1275
  return true;
1194
1276
  }
1277
+ assertExecutionCurrent();
1195
1278
  const replaced = await deps.replaceFollowerThreadTarget({
1196
1279
  record,
1197
1280
  target: sourceTarget,
1198
1281
  oldTarget: record.target,
1199
1282
  });
1283
+ assertExecutionCurrent();
1200
1284
  if (!replaced) {
1201
1285
  await deps.answerCallbackQuery(
1202
1286
  query.id,
@@ -1207,6 +1291,7 @@ export function createTelegramInboundRouteRuntime<
1207
1291
  const nowMs = Date.now();
1208
1292
  const slot = record.slot ?? "?";
1209
1293
  const threadName = getRestoredThreadName(record, slot);
1294
+ assertExecutionCurrent();
1210
1295
  deps.threadStore.markStaleByTarget(
1211
1296
  record.target,
1212
1297
  "deleted",
@@ -1224,13 +1309,16 @@ export function createTelegramInboundRouteRuntime<
1224
1309
  rerouteConfirmedAtMs: nowMs,
1225
1310
  });
1226
1311
  await deps.threadStore.persist();
1312
+ assertExecutionCurrent();
1227
1313
  if (deps.callApi) {
1228
1314
  try {
1315
+ assertExecutionCurrent();
1229
1316
  await deps.callApi("editForumTopic", {
1230
1317
  chat_id: sourceTarget.chatId,
1231
1318
  message_thread_id: sourceTarget.threadId,
1232
1319
  name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1233
1320
  });
1321
+ assertExecutionCurrent();
1234
1322
  } catch (renameError) {
1235
1323
  deps.recordRuntimeEvent?.("telegram", renameError, {
1236
1324
  phase: "follower-topic-reroute-restore-rename",
@@ -1250,6 +1338,7 @@ export function createTelegramInboundRouteRuntime<
1250
1338
  record.instanceId!,
1251
1339
  sourceTarget.threadId,
1252
1340
  ctx,
1341
+ assertExecutionCurrent,
1253
1342
  );
1254
1343
  if (!allForwarded) {
1255
1344
  pending.foreignRetry = {
@@ -1263,7 +1352,10 @@ export function createTelegramInboundRouteRuntime<
1263
1352
  );
1264
1353
  return true;
1265
1354
  }
1266
- const cleanupComplete = await retryPendingRerouteCleanup(cleanup);
1355
+ const cleanupComplete = await retryPendingRerouteCleanup(
1356
+ cleanup,
1357
+ assertExecutionCurrent,
1358
+ );
1267
1359
  if (!cleanupComplete) {
1268
1360
  pending.cleanup = cleanup;
1269
1361
  await deps.answerCallbackQuery(
@@ -1297,6 +1389,7 @@ export function createTelegramInboundRouteRuntime<
1297
1389
  record.instanceId,
1298
1390
  parsed.threadId,
1299
1391
  ctx,
1392
+ assertExecutionCurrent,
1300
1393
  );
1301
1394
  if (!allForwarded) {
1302
1395
  await deps.answerCallbackQuery(
@@ -1308,6 +1401,7 @@ export function createTelegramInboundRouteRuntime<
1308
1401
  const cleanupComplete = await closeReroutedUnboundTopic(
1309
1402
  sourceTarget,
1310
1403
  sourceMessageId,
1404
+ assertExecutionCurrent,
1311
1405
  );
1312
1406
  if (!cleanupComplete && sourceTarget) {
1313
1407
  pending.cleanup = {
@@ -1371,11 +1465,13 @@ export function createTelegramInboundRouteRuntime<
1371
1465
  });
1372
1466
  if (deps.callApi) {
1373
1467
  try {
1468
+ assertExecutionCurrent();
1374
1469
  await deps.callApi("editForumTopic", {
1375
1470
  chat_id: sourceTarget.chatId,
1376
1471
  message_thread_id: sourceTarget.threadId,
1377
1472
  name: Threads.getTelegramTopicTitleForThreadName(threadName, slot),
1378
1473
  });
1474
+ assertExecutionCurrent();
1379
1475
  } catch (renameError) {
1380
1476
  deps.recordRuntimeEvent?.("telegram", renameError, {
1381
1477
  phase: "leader-topic-reroute-reclaim-rename",
@@ -1402,7 +1498,10 @@ export function createTelegramInboundRouteRuntime<
1402
1498
  ctx,
1403
1499
  );
1404
1500
  pending.messages = [];
1405
- const cleanupComplete = await closePreviousLeaderThread(record.target);
1501
+ const cleanupComplete = await closePreviousLeaderThread(
1502
+ record.target,
1503
+ assertExecutionCurrent,
1504
+ );
1406
1505
  if (!cleanupComplete) {
1407
1506
  pending.cleanup = {
1408
1507
  kind: "previous-leader",
@@ -1427,6 +1526,7 @@ export function createTelegramInboundRouteRuntime<
1427
1526
  const cleanupComplete = await closeReroutedUnboundTopic(
1428
1527
  sourceTarget,
1429
1528
  sourceMessageId,
1529
+ assertExecutionCurrent,
1430
1530
  );
1431
1531
  if (!cleanupComplete && sourceTarget) {
1432
1532
  pending.cleanup = {
@@ -1454,6 +1554,9 @@ export function createTelegramInboundRouteRuntime<
1454
1554
  query: TCallbackQuery,
1455
1555
  ctx: TContext,
1456
1556
  ): Promise<void> => {
1557
+ const assertExecutionCurrent =
1558
+ Updates.createTelegramUpdateExecutionFenceGuard(query);
1559
+ assertExecutionCurrent();
1457
1560
  if (await handleUnboundRerouteRestoreMenuCallback(query, ctx)) return;
1458
1561
  if (await handleUnboundRerouteCallback(query, ctx)) return;
1459
1562
  if (deps.buttonActionStore) {
@@ -1486,31 +1589,43 @@ export function createTelegramInboundRouteRuntime<
1486
1589
  if (typeof chatId !== "number" || typeof messageId !== "number")
1487
1590
  return false;
1488
1591
  const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
1489
- const turn = OutboundHandlers.createTelegramButtonPromptTurn({
1490
- chatId,
1491
- target:
1492
- typeof buttonQuery.message?.message_thread_id === "number"
1493
- ? {
1494
- chatId,
1495
- threadId: buttonQuery.message.message_thread_id,
1496
- }
1497
- : { chatId },
1498
- replyToMessageId: messageId,
1499
- queueOrder,
1500
- action,
1501
- });
1592
+ const admissionReceipts = createAdmissionReceipts("prompt", [
1593
+ buttonQuery,
1594
+ ]);
1595
+ const turn: Queue.PendingTelegramTurn = {
1596
+ ...OutboundHandlers.createTelegramButtonPromptTurn({
1597
+ chatId,
1598
+ target:
1599
+ typeof buttonQuery.message?.message_thread_id === "number"
1600
+ ? {
1601
+ chatId,
1602
+ threadId: buttonQuery.message.message_thread_id,
1603
+ }
1604
+ : { chatId },
1605
+ replyToMessageId: messageId,
1606
+ queueOrder,
1607
+ action,
1608
+ }),
1609
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
1610
+ };
1502
1611
  const result = Queue.appendTelegramPromptTurnOnce(
1503
1612
  deps.telegramQueueStore.getQueuedItems(),
1504
1613
  turn,
1505
1614
  );
1506
- if (!result.appended) return false;
1615
+ if (!result.appended) {
1616
+ reportQueueAdmission([buttonQuery], admissionReceipts);
1617
+ return false;
1618
+ }
1619
+ Updates.assertTelegramUpdateExecutionCurrent(buttonQuery);
1507
1620
  deps.telegramQueueStore.setQueuedItems(result.items);
1621
+ reportQueueAdmission([buttonQuery], admissionReceipts);
1508
1622
  deps.updateStatus(context);
1509
1623
  requestDispatchNextQueuedTelegramTurn(context);
1510
1624
  return true;
1511
1625
  },
1512
1626
  },
1513
1627
  );
1628
+ assertExecutionCurrent();
1514
1629
  if (handled) return;
1515
1630
  }
1516
1631
  const handledByCompact =
@@ -1556,13 +1671,16 @@ export function createTelegramInboundRouteRuntime<
1556
1671
  });
1557
1672
  },
1558
1673
  });
1674
+ assertExecutionCurrent();
1559
1675
  if (handledByCompact) return;
1560
1676
  const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
1677
+ assertExecutionCurrent();
1561
1678
  if (handledByQueue) return;
1562
1679
  const handledBySettings = await deps.settingsMenuCallbackHandler?.(
1563
1680
  query,
1564
1681
  ctx,
1565
1682
  );
1683
+ assertExecutionCurrent();
1566
1684
  if (handledBySettings) return;
1567
1685
  const callbackData = query.data;
1568
1686
  if (callbackData && !isTelegramOwnedCallbackData(callbackData)) {
@@ -1574,6 +1692,7 @@ export function createTelegramInboundRouteRuntime<
1574
1692
  typeof query.message?.message_thread_id === "number"
1575
1693
  ? { chatId, threadId: query.message.message_thread_id }
1576
1694
  : { chatId };
1695
+ const admissionReceipts = createAdmissionReceipts("prompt", [query]);
1577
1696
  const turn: Queue.PendingTelegramTurn = {
1578
1697
  kind: "prompt",
1579
1698
  chatId,
@@ -1587,15 +1706,20 @@ export function createTelegramInboundRouteRuntime<
1587
1706
  content: [{ type: "text", text: `[callback] ${callbackData}` }],
1588
1707
  historyText: callbackData,
1589
1708
  statusSummary: callbackData,
1709
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
1590
1710
  };
1591
1711
  const result = Queue.appendTelegramPromptTurnOnce(
1592
1712
  deps.telegramQueueStore.getQueuedItems(),
1593
1713
  turn,
1594
1714
  );
1595
1715
  if (result.appended) {
1716
+ Updates.assertTelegramUpdateExecutionCurrent(query);
1596
1717
  deps.telegramQueueStore.setQueuedItems(result.items);
1718
+ reportQueueAdmission([query], admissionReceipts);
1597
1719
  deps.updateStatus(ctx);
1598
1720
  requestDispatchNextQueuedTelegramTurn(ctx);
1721
+ } else {
1722
+ reportQueueAdmission([query], admissionReceipts);
1599
1723
  }
1600
1724
  }
1601
1725
  await deps.answerCallbackQuery(query.id);
@@ -1612,6 +1736,11 @@ export function createTelegramInboundRouteRuntime<
1612
1736
  processAttachments: deps.inboundHandlerRuntime.process,
1613
1737
  resolveTimeLine: deps.resolveTimeLine,
1614
1738
  getAllowedUserId: deps.configStore.getAllowedUserId,
1739
+ getAdmissionScope: deps.getAdmissionScope,
1740
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
1741
+ assertExecutionCurrent(message) {
1742
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1743
+ },
1615
1744
 
1616
1745
  // Voice policy resolves missing, invalid, and legacy manual config to hidden.
1617
1746
  getVoiceReplyMode: () => getTelegramVoiceReplyMode(deps.configStore.get()),
@@ -1661,7 +1790,12 @@ export function createTelegramInboundRouteRuntime<
1661
1790
  laneOrder: deps.bridgeRuntime.queue.allocateControlOrder(),
1662
1791
  statusSummary: "continue",
1663
1792
  };
1793
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1664
1794
  deps.queueMutationRuntime.append(continueTurn, ctx);
1795
+ reportQueueAdmission(
1796
+ [continueMessage],
1797
+ continueTurn.admissionReceipts ?? [],
1798
+ );
1665
1799
  requestDispatchNextQueuedTelegramTurn(ctx);
1666
1800
  };
1667
1801
  const reservedCommandNames = () =>
@@ -1675,6 +1809,9 @@ export function createTelegramInboundRouteRuntime<
1675
1809
  TMessage,
1676
1810
  TContext
1677
1811
  >({
1812
+ assertExecutionCurrent(message) {
1813
+ Updates.assertTelegramUpdateExecutionCurrent(message);
1814
+ },
1678
1815
  hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
1679
1816
  clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
1680
1817
  hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
@@ -1690,6 +1827,7 @@ export function createTelegramInboundRouteRuntime<
1690
1827
  setCompactionInProgress:
1691
1828
  deps.bridgeRuntime.lifecycle.setCompactionInProgress,
1692
1829
  updateStatus: deps.updateStatus,
1830
+ isContextActive: deps.isContextActive,
1693
1831
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
1694
1832
  requestDeferredDispatchNextQueuedTelegramTurn:
1695
1833
  deps.requestDeferredDispatchNextQueuedTelegramTurn,
@@ -1700,6 +1838,10 @@ export function createTelegramInboundRouteRuntime<
1700
1838
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
1701
1839
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
1702
1840
  appendControlItem: deps.queueMutationRuntime.append,
1841
+ getAdmissionScope: deps.getAdmissionScope,
1842
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
1843
+ onControlQueued: (message, receipt) =>
1844
+ reportQueueAdmission([message], [receipt]),
1703
1845
  showStatus: deps.menuActions.sendStatusMessage,
1704
1846
  openModelMenu: deps.menuActions.openModelMenu,
1705
1847
  openThinkingMenu: (message, ctx) => {
@@ -1720,27 +1862,36 @@ export function createTelegramInboundRouteRuntime<
1720
1862
  sendInteractiveMessage: deps.sendInteractiveMessage,
1721
1863
  recordRuntimeEvent: deps.recordRuntimeEvent,
1722
1864
  });
1723
- const promptEnqueue = Queue.createTelegramPromptEnqueueController<
1724
- TMessage,
1725
- TContext
1726
- >({
1727
- ...deps.telegramQueueStore,
1728
- getFoldQueuedPromptsIntoHistory:
1729
- deps.bridgeRuntime.lifecycle.shouldFoldQueuedPromptsIntoHistory,
1730
- setFoldQueuedPromptsIntoHistory:
1731
- deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
1732
- createTurn: async (messages, historyTurns, turnCtx) => {
1733
- const turn = await promptTurnBuilder(messages, historyTurns, turnCtx);
1734
- return turn.replyToMessageId > 0
1735
- ? turn
1736
- : { ...turn, replyToMessageId: 0 };
1737
- },
1738
- updateStatus: deps.updateStatus,
1739
- dispatchNextQueuedTelegramTurn: requestDispatchNextQueuedTelegramTurn,
1740
- }).enqueue;
1865
+ const promptEnqueueController =
1866
+ Queue.createTelegramPromptEnqueueController<TMessage, TContext>({
1867
+ ...deps.telegramQueueStore,
1868
+ getFoldQueuedPromptsIntoHistory:
1869
+ deps.bridgeRuntime.lifecycle.shouldFoldQueuedPromptsIntoHistory,
1870
+ setFoldQueuedPromptsIntoHistory:
1871
+ deps.bridgeRuntime.lifecycle.setFoldQueuedPromptsIntoHistory,
1872
+ createTurn: async (messages, historyTurns, turnCtx) => {
1873
+ const turn = await promptTurnBuilder(messages, historyTurns, turnCtx);
1874
+ return turn.replyToMessageId > 0
1875
+ ? turn
1876
+ : { ...turn, replyToMessageId: 0 };
1877
+ },
1878
+ updateStatus: deps.updateStatus,
1879
+ dispatchNextQueuedTelegramTurn: requestDispatchNextQueuedTelegramTurn,
1880
+ assertExecutionCurrent: (messages) =>
1881
+ Updates.assertTelegramUpdateExecutionCurrent(messages[0]),
1882
+ });
1883
+ const promptEnqueue = async (
1884
+ messages: TMessage[],
1885
+ ctx: TContext,
1886
+ ): Promise<Queue.PendingTelegramTurn> => {
1887
+ return promptEnqueueController.enqueue(messages, ctx, (turn) => {
1888
+ reportQueueAdmission(messages, turn.admissionReceipts ?? []);
1889
+ });
1890
+ };
1741
1891
  const sendUnboundRerouteChooserNow = async (
1742
1892
  messages: TMessage[],
1743
1893
  ctx: TContext,
1894
+ reportDeferred = true,
1744
1895
  ): Promise<void> => {
1745
1896
  const message = messages[0];
1746
1897
  if (!message || !deps.threadStore) return;
@@ -1775,6 +1926,11 @@ export function createTelegramInboundRouteRuntime<
1775
1926
  return;
1776
1927
  }
1777
1928
  const rerouteId = storePendingUnboundReroute(messages);
1929
+ if (reportDeferred) {
1930
+ for (const source of messages) {
1931
+ Updates.reportTelegramUpdateDeferred(source);
1932
+ }
1933
+ }
1778
1934
  const text = formatTelegramUnboundRerouteChooserText(activeRecords, {
1779
1935
  includeGuidance,
1780
1936
  });
@@ -1821,10 +1977,11 @@ export function createTelegramInboundRouteRuntime<
1821
1977
  const messages = [...(existing?.messages ?? []), message];
1822
1978
  const timer = setTimeout(() => {
1823
1979
  pendingUnboundRerouteMediaGroups.delete(groupKey);
1824
- void sendUnboundRerouteChooserNow(messages, ctx);
1980
+ void sendUnboundRerouteChooserNow(messages, ctx, false);
1825
1981
  }, 1200);
1826
1982
  timer.unref?.();
1827
1983
  pendingUnboundRerouteMediaGroups.set(groupKey, { messages, timer });
1984
+ Updates.reportTelegramUpdateDeferred(message);
1828
1985
  };
1829
1986
  const getKnownTelegramAllTabCommand = (
1830
1987
  text: string,
@@ -1864,6 +2021,7 @@ export function createTelegramInboundRouteRuntime<
1864
2021
  caption: undefined,
1865
2022
  } as TMessage;
1866
2023
  const rerouteId = storePendingUnboundReroute([commandMessage], "command");
2024
+ Updates.reportTelegramUpdateDeferred(commandMessage);
1867
2025
  const text = formatTelegramAllTabMenuChooserText(command.name);
1868
2026
  const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
1869
2027
  rerouteId,
@@ -1917,6 +2075,9 @@ export function createTelegramInboundRouteRuntime<
1917
2075
  TContext
1918
2076
  >({
1919
2077
  extractRawText: Media.extractFirstTelegramMessageText,
2078
+ assertExecutionCurrent(message) {
2079
+ Updates.assertTelegramUpdateExecutionCurrent(message);
2080
+ },
1920
2081
  shouldIgnoreMessages: (messages) =>
1921
2082
  !Media.hasTelegramMessagesPromptContent(messages),
1922
2083
  handleCommand: commandHandler,
@@ -1926,18 +2087,26 @@ export function createTelegramInboundRouteRuntime<
1926
2087
  );
1927
2088
  if (!extensionCommand) return false;
1928
2089
  const sourceTarget = Updates.getTelegramMessageTarget(message);
2090
+ const assertExecutionCurrent =
2091
+ Updates.createTelegramUpdateExecutionFenceGuard(message);
1929
2092
  try {
2093
+ assertExecutionCurrent();
1930
2094
  await extensionCommand.handler({
1931
2095
  name: command.name,
1932
2096
  args: command.args,
1933
- reply: (text) =>
1934
- deps
1935
- .sendTextReply(message.chat.id, message.message_id, text, {
1936
- target: sourceTarget,
1937
- })
1938
- .then(() => {}),
1939
- enqueuePrompt: (prompt) =>
1940
- promptEnqueue(
2097
+ reply: async (text) => {
2098
+ assertExecutionCurrent();
2099
+ await deps.sendTextReply(
2100
+ message.chat.id,
2101
+ message.message_id,
2102
+ text,
2103
+ { target: sourceTarget },
2104
+ );
2105
+ assertExecutionCurrent();
2106
+ },
2107
+ enqueuePrompt: async (prompt) => {
2108
+ assertExecutionCurrent();
2109
+ await promptEnqueue(
1941
2110
  [
1942
2111
  {
1943
2112
  ...message,
@@ -1946,12 +2115,15 @@ export function createTelegramInboundRouteRuntime<
1946
2115
  } as TMessage,
1947
2116
  ],
1948
2117
  ctx,
1949
- ),
2118
+ );
2119
+ },
1950
2120
  });
2121
+ assertExecutionCurrent();
1951
2122
  } catch (error) {
1952
2123
  deps.recordRuntimeEvent?.("telegram-command", error, {
1953
2124
  command: command.name,
1954
2125
  });
2126
+ assertExecutionCurrent();
1955
2127
  await deps.sendTextReply(
1956
2128
  message.chat.id,
1957
2129
  message.message_id,
@@ -1969,7 +2141,9 @@ export function createTelegramInboundRouteRuntime<
1969
2141
  ),
1970
2142
  replaceMessageText: (message, text) =>
1971
2143
  ({ ...message, text, caption: undefined }) as TMessage,
1972
- enqueueTurn: promptEnqueue,
2144
+ enqueueTurn: async (messages, ctx) => {
2145
+ await promptEnqueue(messages, ctx);
2146
+ },
1973
2147
  });
1974
2148
  dispatchReroutedCommandMessages = (messages, ctx) =>
1975
2149
  commandOrPrompt.dispatchMessages(messages, ctx);
@@ -1979,6 +2153,7 @@ export function createTelegramInboundRouteRuntime<
1979
2153
  >({
1980
2154
  mediaGroups: deps.mediaGroupRuntime,
1981
2155
  dispatchMessages: commandOrPrompt.dispatchMessages,
2156
+ onDeferredMessage: Updates.reportTelegramUpdateDeferred,
1982
2157
  });
1983
2158
  const textDispatch = TextGroups.createTelegramTextGroupDispatchRuntime<
1984
2159
  TMessage,
@@ -1987,6 +2162,7 @@ export function createTelegramInboundRouteRuntime<
1987
2162
  textGroups: deps.textGroupRuntime,
1988
2163
  dispatchMessages: commandOrPrompt.dispatchMessages,
1989
2164
  dispatchSingleMessage: mediaDispatch.handleMessage,
2165
+ onDeferredMessage: Updates.reportTelegramUpdateDeferred,
1990
2166
  });
1991
2167
  const editRuntime = Turns.createTelegramQueuedPromptEditRuntime<
1992
2168
  TMessage,
@@ -1999,16 +2175,24 @@ export function createTelegramInboundRouteRuntime<
1999
2175
  lifecycle: Updates.TelegramTopicLifecycleUpdate<TMessage>,
2000
2176
  ctx: TContext,
2001
2177
  ): Promise<void> => {
2178
+ const assertExecutionCurrent =
2179
+ Updates.createTelegramUpdateExecutionFenceGuard(lifecycle.message);
2180
+ assertExecutionCurrent();
2002
2181
  await deps.handleTelegramTopicLifecycleUpdate?.(lifecycle, ctx);
2182
+ assertExecutionCurrent();
2003
2183
  if (lifecycle.kind !== "created" || !deps.threadStore) {
2004
2184
  return;
2005
2185
  }
2006
2186
  await deps.threadStore.load();
2187
+ assertExecutionCurrent();
2007
2188
  };
2008
2189
  const handleAuthorizedTelegramGuestMessage = async (
2009
2190
  guestMessage: Updates.TelegramGuestMessage & { from: TelegramUser },
2010
2191
  ctx: TContext,
2011
2192
  ): Promise<void> => {
2193
+ const assertExecutionCurrent =
2194
+ Updates.createTelegramUpdateExecutionFenceGuard(guestMessage);
2195
+ assertExecutionCurrent();
2012
2196
  const text = guestMessage.text ?? "";
2013
2197
  const gm = guestMessage as unknown as Record<string, unknown>;
2014
2198
  // Build telegram prefix with guest context
@@ -2062,14 +2246,17 @@ export function createTelegramInboundRouteRuntime<
2062
2246
  { downloadFile: deps.downloadFile },
2063
2247
  )
2064
2248
  : [];
2249
+ assertExecutionCurrent();
2065
2250
  const files = await Media.downloadTelegramMessageFiles([guestMsg], {
2066
2251
  downloadFile: deps.downloadFile,
2067
2252
  });
2253
+ assertExecutionCurrent();
2068
2254
  const processed = await deps.inboundHandlerRuntime.process(
2069
2255
  files,
2070
2256
  text,
2071
2257
  ctx,
2072
2258
  );
2259
+ assertExecutionCurrent();
2073
2260
  const rawText = processed.rawText || text;
2074
2261
  let sourceContext = "";
2075
2262
  if (replyMsg) {
@@ -2099,6 +2286,7 @@ export function createTelegramInboundRouteRuntime<
2099
2286
  if (file.isImage && file.mimeType) {
2100
2287
  try {
2101
2288
  const buffer = await readFile(file.path);
2289
+ assertExecutionCurrent();
2102
2290
  content.push({
2103
2291
  type: "image",
2104
2292
  data: Buffer.from(buffer).toString("base64"),
@@ -2109,6 +2297,7 @@ export function createTelegramInboundRouteRuntime<
2109
2297
  }
2110
2298
  }
2111
2299
  }
2300
+ const admissionReceipts = createAdmissionReceipts("prompt", [guestMessage]);
2112
2301
  const guestTurn: Queue.PendingTelegramTurn = {
2113
2302
  kind: "prompt",
2114
2303
  chatId: 0,
@@ -2128,11 +2317,14 @@ export function createTelegramInboundRouteRuntime<
2128
2317
  statusSummary: Turns.truncateTelegramQueueSummary(
2129
2318
  processed.rawText || text,
2130
2319
  ),
2320
+ ...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
2131
2321
  };
2132
2322
  const items = deps.telegramQueueStore.getQueuedItems();
2323
+ Updates.assertTelegramUpdateExecutionCurrent(guestMessage);
2133
2324
  deps.telegramQueueStore.setQueuedItems(
2134
2325
  Queue.appendTelegramQueueItem(items, guestTurn),
2135
2326
  );
2327
+ reportQueueAdmission([guestMessage], admissionReceipts);
2136
2328
  deps.updateStatus(ctx);
2137
2329
  requestDispatchNextQueuedTelegramTurn(ctx);
2138
2330
  };
@@ -2148,17 +2340,20 @@ export function createTelegramInboundRouteRuntime<
2148
2340
  persistConfig: deps.configStore.persist,
2149
2341
  updateStatus: deps.updateStatus,
2150
2342
  removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
2343
+ flushPendingMediaGroupMessage: deps.mediaGroupRuntime.flushMessage,
2344
+ flushPendingTextGroupMessage: deps.textGroupRuntime.flushMessage,
2151
2345
  removeQueuedTelegramTurnsByMessageIds:
2152
2346
  deps.queueMutationRuntime.removeByMessageIds,
2153
- clearQueuedTelegramTurnPriorityByMessageId:
2154
- deps.queueMutationRuntime.clearPriorityByMessageId,
2155
- prioritizeQueuedTelegramTurnByMessageId:
2156
- deps.queueMutationRuntime.prioritizeByMessageId,
2347
+ applyQueuedTelegramTurnReactionByMessageId:
2348
+ deps.queueMutationRuntime.applyReactionByMessageId,
2157
2349
  answerCallbackQuery: deps.answerCallbackQuery,
2158
2350
  answerGuestQuery: deps.answerGuestQuery,
2159
2351
  handleAuthorizedTelegramCallbackQuery: callbackHandler,
2160
2352
  sendTextReply: deps.sendTextReply,
2161
2353
  handleAuthorizedTelegramMessage: async (message, ctx) => {
2354
+ const assertExecutionCurrent =
2355
+ Updates.createTelegramUpdateExecutionFenceGuard(message);
2356
+ assertExecutionCurrent();
2162
2357
  if (typeof message.message_thread_id === "number") {
2163
2358
  await deps.handleTelegramThreadTargetObserved?.(
2164
2359
  {
@@ -2167,12 +2362,14 @@ export function createTelegramInboundRouteRuntime<
2167
2362
  },
2168
2363
  ctx,
2169
2364
  );
2365
+ assertExecutionCurrent();
2170
2366
  }
2171
2367
  const text = Media.extractFirstTelegramMessageText([
2172
2368
  message as TMessage,
2173
2369
  ]).trim();
2174
2370
  if (deps.threadStore && typeof message.message_thread_id !== "number") {
2175
2371
  await deps.threadStore.load();
2372
+ assertExecutionCurrent();
2176
2373
  if (deps.threadStore.getBotState().threadMode === "disabled") {
2177
2374
  await textDispatch.handleMessage(message as TMessage, ctx);
2178
2375
  return;
@@ -2213,6 +2410,7 @@ export function createTelegramInboundRouteRuntime<
2213
2410
  "thread-mode-unavailable-threadless-prompt",
2214
2411
  });
2215
2412
  await deps.threadStore.persist();
2413
+ assertExecutionCurrent();
2216
2414
  await textDispatch.handleMessage(message as TMessage, ctx);
2217
2415
  return;
2218
2416
  }
@@ -2236,11 +2434,15 @@ export function createTelegramInboundRouteRuntime<
2236
2434
  handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
2237
2435
  handleAuthorizedTelegramGuestMessage,
2238
2436
  handleUnboundTelegramTopicMessage: async (message, ctx) => {
2437
+ const assertExecutionCurrent =
2438
+ Updates.createTelegramUpdateExecutionFenceGuard(message);
2439
+ assertExecutionCurrent();
2239
2440
  if (!deps.threadStore) {
2240
2441
  await textDispatch.handleMessage(message as TMessage, ctx);
2241
2442
  return;
2242
2443
  }
2243
2444
  await deps.threadStore.load();
2445
+ assertExecutionCurrent();
2244
2446
  if (deps.threadStore.getBotState().threadMode === "disabled") {
2245
2447
  await textDispatch.handleMessage(message as TMessage, ctx);
2246
2448
  return;
@@ -2280,6 +2482,7 @@ export function createTelegramInboundRouteRuntime<
2280
2482
  rerouteConfirmedAtMs: nowMs,
2281
2483
  });
2282
2484
  await deps.threadStore.persist();
2485
+ assertExecutionCurrent();
2283
2486
  }
2284
2487
  await textDispatch.handleMessage(message as TMessage, ctx);
2285
2488
  return;
@@ -2350,6 +2553,7 @@ export function createTelegramInboundRouteRuntime<
2350
2553
  slot,
2351
2554
  });
2352
2555
  await deps.threadStore.persist();
2556
+ assertExecutionCurrent();
2353
2557
  deps.setCurrentLeaderIdentity?.({
2354
2558
  target: { chatId: target.chatId, threadId: target.threadId },
2355
2559
  slot,
@@ -2381,6 +2585,27 @@ export function createTelegramInboundRouteRuntime<
2381
2585
  );
2382
2586
  return;
2383
2587
  }
2588
+ const deletedObservation = deps.threadStore
2589
+ .listSyncObservations()
2590
+ .find(
2591
+ (observation) =>
2592
+ observation.syncStatus === "deleted" &&
2593
+ observation.target.chatId === target.chatId &&
2594
+ observation.target.threadId === target.threadId,
2595
+ );
2596
+ if (deletedObservation) {
2597
+ deps.recordRuntimeEvent?.(
2598
+ "inbound-worker",
2599
+ "Discarded update from a confirmed deleted Telegram thread",
2600
+ {
2601
+ phase: "discard-deleted-thread",
2602
+ chatId: target.chatId,
2603
+ threadId: target.threadId,
2604
+ messageId: message.message_id,
2605
+ },
2606
+ );
2607
+ return;
2608
+ }
2384
2609
  const reservations = deps.threadStore.listReservations();
2385
2610
  const reservation = reservations.find(
2386
2611
  (reservation) =>
@@ -2464,6 +2689,7 @@ export function createTelegramInboundRouteRuntime<
2464
2689
  slot,
2465
2690
  });
2466
2691
  await deps.threadStore.persist();
2692
+ assertExecutionCurrent();
2467
2693
  deps.setCurrentLeaderIdentity?.({
2468
2694
  target: { chatId: target.chatId, threadId: target.threadId },
2469
2695
  slot,
@@ -2535,6 +2761,7 @@ export function createTelegramInboundRouteRuntime<
2535
2761
  slot,
2536
2762
  });
2537
2763
  await deps.threadStore.persist();
2764
+ assertExecutionCurrent();
2538
2765
  deps.setCurrentLeaderIdentity?.({
2539
2766
  target: { chatId: target.chatId, threadId: target.threadId },
2540
2767
  slot,