@llblab/pi-kit 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
  4. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +21 -0
  5. package/node_modules/@llblab/pi-telegram/README.md +2 -0
  6. package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -4
  7. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +5 -1
  8. package/node_modules/@llblab/pi-telegram/docs/outbound.md +19 -3
  9. package/node_modules/@llblab/pi-telegram/index.ts +10 -0
  10. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
  11. package/node_modules/@llblab/pi-telegram/lib/activity.ts +60 -1
  12. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +101 -90
  13. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -2
  14. package/node_modules/@llblab/pi-telegram/lib/locks.ts +99 -16
  15. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +18 -7
  16. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +11 -0
  17. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +10 -3
  18. package/node_modules/@llblab/pi-telegram/lib/polling.ts +142 -30
  19. package/node_modules/@llblab/pi-telegram/lib/preview.ts +19 -3
  20. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +8 -6
  21. package/node_modules/@llblab/pi-telegram/lib/queue.ts +101 -66
  22. package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
  23. package/node_modules/@llblab/pi-telegram/lib/status.ts +4 -0
  24. package/node_modules/@llblab/pi-telegram/lib/updates.ts +33 -35
  25. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  26. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +2 -0
  27. package/package.json +2 -2
@@ -7,6 +7,7 @@
7
7
  import { createHash } from "node:crypto";
8
8
 
9
9
  import { isVoiceTurn } from "./voice.ts";
10
+ import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
10
11
 
11
12
  // --- Queue Items ---
12
13
 
@@ -1497,6 +1498,10 @@ export interface TelegramAgentEndRuntimeDeps<
1497
1498
  updateStatus: () => void;
1498
1499
  dispatchNextQueuedTelegramTurn: () => void;
1499
1500
  scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
1501
+ preparePreviewClear?: (
1502
+ chatId: number,
1503
+ options?: { target?: TelegramQueueTarget; isDeliveryActive?: () => boolean },
1504
+ ) => () => Promise<void>;
1500
1505
  clearPreview: (
1501
1506
  chatId: number,
1502
1507
  options?: { target?: TelegramQueueTarget },
@@ -1520,11 +1525,14 @@ export interface TelegramAgentEndRuntimeDeps<
1520
1525
  text: string,
1521
1526
  options?: { target?: TelegramQueueTarget },
1522
1527
  ) => Promise<unknown>;
1523
- sendQueuedAttachments: (turn: TTurn) => Promise<void>;
1528
+ sendQueuedAttachments: (
1529
+ turn: TTurn,
1530
+ options?: { isDeliveryActive?: () => boolean },
1531
+ ) => Promise<void>;
1524
1532
  sendRichAttachmentReply?: (
1525
1533
  turn: TTurn,
1526
1534
  markdown: string,
1527
- options?: { replyMarkup?: TReplyMarkup },
1535
+ options?: { replyMarkup?: TReplyMarkup; isDeliveryActive?: () => boolean },
1528
1536
  ) => Promise<boolean>;
1529
1537
  answerGuestQuery?: (
1530
1538
  guestQueryId: string,
@@ -1548,7 +1556,7 @@ export interface TelegramAgentEndRuntimeDeps<
1548
1556
  sendOutboundReplyArtifacts?: (
1549
1557
  turn: TTurn,
1550
1558
  plan: TelegramAgentEndOutboundReplyPlan,
1551
- options?: { replyToPrompt?: boolean },
1559
+ options?: { replyToPrompt?: boolean; isDeliveryActive?: () => boolean },
1552
1560
  ) => Promise<void>;
1553
1561
  recordRuntimeEvent?: (
1554
1562
  category: string,
@@ -1583,6 +1591,11 @@ export interface TelegramAgentEndHookRuntimeDeps<
1583
1591
  TTurn,
1584
1592
  TReplyMarkup
1585
1593
  >["scheduleActiveTurnDelivery"];
1594
+ reserveActiveTurnDelivery?: () => {
1595
+ schedule: (task: () => Promise<void>) => void;
1596
+ cancel: () => void;
1597
+ };
1598
+ preparePreviewClear?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewClear"];
1586
1599
  clearPreview: TelegramAgentEndRuntimeDeps<
1587
1600
  TTurn,
1588
1601
  TReplyMarkup
@@ -1597,7 +1610,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
1597
1610
  TReplyMarkup
1598
1611
  >["sendMarkdownReply"];
1599
1612
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
1600
- sendQueuedAttachments: (turn: TTurn) => Promise<void>;
1613
+ sendQueuedAttachments: TelegramAgentEndRuntimeDeps<TTurn>["sendQueuedAttachments"];
1601
1614
  sendRichAttachmentReply?: TelegramAgentEndRuntimeDeps<
1602
1615
  TTurn,
1603
1616
  TReplyMarkup
@@ -1702,48 +1715,56 @@ export function createTelegramAgentEndHook<
1702
1715
  ctx: TContext,
1703
1716
  assistantOverride?: TelegramAgentEndAssistantResult,
1704
1717
  ): Promise<void> => {
1705
- await deps.loadConfig?.();
1706
- if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
1718
+ if (deps.isSessionActive?.(ctx) === false) return;
1707
1719
  const turn = deps.getActiveTurn();
1708
- await handleTelegramAgentEndRuntime({
1709
- turn,
1710
- assistant:
1711
- assistantOverride ??
1712
- (turn ? deps.extractAssistant(event.messages) : {}),
1713
- foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
1714
- resetRuntimeState: deps.resetRuntimeState,
1715
- isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
1716
- isTurnTransportActive: deps.isTurnTransportActive,
1717
- waitForTypingIdle: deps.waitForTypingIdle,
1718
- waitForActivityIdle: deps.waitForActivityIdle,
1719
- updateStatus: () => deps.updateStatus(ctx),
1720
- dispatchNextQueuedTelegramTurn: () => {
1721
- deps.requestDeferredDispatchNextQueuedTelegramTurn(
1722
- deps.dispatchNextQueuedTelegramTurn,
1723
- );
1724
- },
1725
- scheduleActiveTurnDelivery: deps.scheduleActiveTurnDelivery
1726
- ? (task) =>
1727
- deps.scheduleActiveTurnDelivery?.(async () => {
1728
- if (deps.isSessionActive?.(ctx) === false) return;
1729
- await task();
1730
- })
1731
- : undefined,
1732
- clearPreview: deps.clearPreview,
1733
- setPreviewPendingText: deps.setPreviewPendingText,
1734
- finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
1735
- sendMarkdownReply: deps.sendMarkdownReply,
1736
- sendTextReply: deps.sendTextReply,
1737
- sendQueuedAttachments: deps.sendQueuedAttachments,
1738
- sendRichAttachmentReply: deps.sendRichAttachmentReply,
1739
- answerGuestQuery: deps.answerGuestQuery,
1740
- sendGuestReply: deps.sendGuestReply,
1741
- sendGuestAttachment: deps.sendGuestAttachment,
1742
- sendGuestVoiceReply: deps.sendGuestVoiceReply,
1743
- planOutboundReply: deps.planOutboundReply,
1744
- sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
1745
- recordRuntimeEvent: deps.recordRuntimeEvent,
1746
- });
1720
+ const assistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
1721
+ const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
1722
+ const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
1723
+ const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
1724
+ try {
1725
+ await deps.loadConfig?.();
1726
+ if (deps.isSessionActive?.(ctx) === false || deps.getActiveTurn() !== turn) return;
1727
+ await handleTelegramAgentEndRuntime({
1728
+ turn,
1729
+ assistant,
1730
+ foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
1731
+ resetRuntimeState: deps.resetRuntimeState,
1732
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
1733
+ isTurnTransportActive: deps.isTurnTransportActive,
1734
+ waitForTypingIdle: deps.waitForTypingIdle,
1735
+ waitForActivityIdle: deps.waitForActivityIdle,
1736
+ updateStatus: () => deps.updateStatus(ctx),
1737
+ dispatchNextQueuedTelegramTurn: () => {
1738
+ deps.requestDeferredDispatchNextQueuedTelegramTurn(
1739
+ deps.dispatchNextQueuedTelegramTurn,
1740
+ );
1741
+ },
1742
+ scheduleActiveTurnDelivery: scheduleDelivery
1743
+ ? (task) =>
1744
+ scheduleDelivery(async () => {
1745
+ if (deps.isSessionActive?.(ctx) === false) return;
1746
+ await task();
1747
+ })
1748
+ : undefined,
1749
+ preparePreviewClear: deps.preparePreviewClear,
1750
+ clearPreview: deps.clearPreview,
1751
+ setPreviewPendingText: deps.setPreviewPendingText,
1752
+ finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
1753
+ sendMarkdownReply: deps.sendMarkdownReply,
1754
+ sendTextReply: deps.sendTextReply,
1755
+ sendQueuedAttachments: deps.sendQueuedAttachments,
1756
+ sendRichAttachmentReply: deps.sendRichAttachmentReply,
1757
+ answerGuestQuery: deps.answerGuestQuery,
1758
+ sendGuestReply: deps.sendGuestReply,
1759
+ sendGuestAttachment: deps.sendGuestAttachment,
1760
+ sendGuestVoiceReply: deps.sendGuestVoiceReply,
1761
+ planOutboundReply: deps.planOutboundReply,
1762
+ sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
1763
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1764
+ });
1765
+ } finally {
1766
+ reservation?.cancel();
1767
+ }
1747
1768
  };
1748
1769
  }
1749
1770
 
@@ -1786,6 +1807,10 @@ export async function handleTelegramAgentEndRuntime<
1786
1807
  const isDeliveryActive = (): boolean =>
1787
1808
  deps.isSessionActive?.() !== false &&
1788
1809
  (!turn || deps.isTurnTransportActive?.(turn) !== false);
1810
+ const clearPreview = turn
1811
+ ? deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
1812
+ ?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
1813
+ : undefined;
1789
1814
  const updateStatusIgnoringStaleContext = (): void => {
1790
1815
  try {
1791
1816
  deps.updateStatus();
@@ -1869,28 +1894,28 @@ export async function handleTelegramAgentEndRuntime<
1869
1894
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1870
1895
  return;
1871
1896
  }
1872
- if (endPlan.shouldClearPreview) {
1873
- await deps.clearPreview(turn.chatId, { target: turn.target });
1874
- }
1875
1897
  if (!isDeliveryActive()) return;
1876
- if (endPlan.shouldSendErrorMessage) {
1877
- await deps.sendTextReply(
1878
- turn.chatId,
1879
- turn.replyToMessageId,
1880
- assistant.errorMessage ||
1881
- "Telegram bridge: Pi failed while processing the request.",
1882
- { target: turn.target },
1883
- );
1884
- if (!isDeliveryActive()) return;
1885
- if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1886
- return;
1887
- }
1888
1898
  const deliverActiveTurn = async () => {
1889
1899
  await deps.waitForActivityIdle?.();
1890
1900
  if (!isDeliveryActive()) return;
1901
+ if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
1902
+ await clearPreview?.();
1903
+ if (!isDeliveryActive()) return;
1904
+ }
1905
+ if (endPlan.shouldSendErrorMessage) {
1906
+ await deps.sendTextReply(
1907
+ turn.chatId,
1908
+ turn.replyToMessageId,
1909
+ assistant.errorMessage ||
1910
+ "Telegram bridge: Pi failed while processing the request.",
1911
+ { target: turn.target },
1912
+ );
1913
+ if (!isDeliveryActive()) return;
1914
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1915
+ return;
1916
+ }
1891
1917
  if (finalText) deps.setPreviewPendingText(finalText);
1892
- if (!finalText && hasOutboundArtifacts)
1893
- await deps.clearPreview(turn.chatId, { target: turn.target });
1918
+
1894
1919
  if (!isDeliveryActive()) return;
1895
1920
  let richAttachmentDelivered = false;
1896
1921
  if (
@@ -1903,13 +1928,16 @@ export async function handleTelegramAgentEndRuntime<
1903
1928
  richAttachmentDelivered = await deps.sendRichAttachmentReply(
1904
1929
  turn,
1905
1930
  finalText,
1906
- { replyMarkup },
1931
+ { replyMarkup, isDeliveryActive },
1907
1932
  );
1908
1933
  if (!isDeliveryActive()) return;
1909
1934
  if (richAttachmentDelivered) {
1910
1935
  await deps.clearPreview(turn.chatId, { target: turn.target });
1936
+ if (!isDeliveryActive()) return;
1937
+ deps.setPreviewPendingText("");
1911
1938
  }
1912
1939
  } catch (error) {
1940
+ if (!isDeliveryActive()) return;
1913
1941
  deps.recordRuntimeEvent?.("delivery", error, {
1914
1942
  phase: "rich-attachment-commit-unknown",
1915
1943
  chatId: turn.chatId,
@@ -1938,6 +1966,8 @@ export async function handleTelegramAgentEndRuntime<
1938
1966
  { replyMarkup, target: turn.target },
1939
1967
  );
1940
1968
  }
1969
+ if (!isDeliveryActive()) return;
1970
+ deps.setPreviewPendingText("");
1941
1971
  } catch (error) {
1942
1972
  deps.recordRuntimeEvent?.("delivery", error, {
1943
1973
  phase: "final-text",
@@ -1951,6 +1981,7 @@ export async function handleTelegramAgentEndRuntime<
1951
1981
  try {
1952
1982
  await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1953
1983
  replyToPrompt: !finalText,
1984
+ isDeliveryActive,
1954
1985
  });
1955
1986
  if (!isDeliveryActive()) return;
1956
1987
  } catch (error) {
@@ -1958,8 +1989,12 @@ export async function handleTelegramAgentEndRuntime<
1958
1989
  phase: "voice-artifacts",
1959
1990
  chatId: turn.chatId,
1960
1991
  });
1961
- // Fallback to planned text when voice delivery fails and text wasn't already delivered
1962
1992
  if (!isDeliveryActive()) return;
1993
+ if (isTelegramApiCommitUnknownError(error)) {
1994
+ if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1995
+ return;
1996
+ }
1997
+ // Fallback only when voice delivery is not uncertain and text wasn't already delivered.
1963
1998
  if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
1964
1999
  try {
1965
2000
  const fallbackMarkdown =
@@ -1996,13 +2031,13 @@ export async function handleTelegramAgentEndRuntime<
1996
2031
  );
1997
2032
  }
1998
2033
  if (!isDeliveryActive()) return;
1999
- if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn);
2034
+ if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn, { isDeliveryActive });
2000
2035
  if (!isDeliveryActive()) return;
2001
2036
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
2002
2037
  };
2003
2038
  if (
2004
2039
  deps.scheduleActiveTurnDelivery &&
2005
- (endPlan.kind === "text" || endPlan.kind === "attachments-only")
2040
+ (endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
2006
2041
  ) {
2007
2042
  deps.scheduleActiveTurnDelivery(deliverActiveTurn);
2008
2043
  return;
@@ -448,7 +448,19 @@ async function deleteReservedTelegramTopicThroughReconciler(
448
448
  );
449
449
  }
450
450
 
451
- export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
451
+ export const TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS = 60 * 60_000;
452
+
453
+ export function isTelegramAllTabCommandExpired(
454
+ message: { date?: number; message_thread_id?: number },
455
+ nowMs = Date.now(),
456
+ ): boolean {
457
+ return message.message_thread_id === undefined &&
458
+ typeof message.date === "number" && Number.isFinite(message.date) &&
459
+ message.date > 0 && Number.isFinite(nowMs) &&
460
+ nowMs - message.date * 1000 >= TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS;
461
+ }
462
+
463
+ export type TelegramRoutedMessage = { date?: number } & Updates.TelegramUpdateMessage &
452
464
  Media.TelegramMediaMessage &
453
465
  Media.TelegramMediaGroupMessage &
454
466
  Commands.TelegramCommandRuntimeMessage &
@@ -738,6 +750,12 @@ export function createTelegramInboundRouteRuntime<
738
750
  messages: TMessage[];
739
751
  createdAtMs: number;
740
752
  dispatchKind: "prompt" | "command";
753
+ expiresAtMs?: number;
754
+ destinationSelected?: boolean;
755
+ selectionAttempted?: boolean;
756
+ dispatching?: boolean;
757
+ pauseExpiry?: () => void;
758
+ stopExpiry?: () => void;
741
759
  cleanup?: PendingRerouteCleanup;
742
760
  foreignRetry?: {
743
761
  instanceId: string;
@@ -822,24 +840,71 @@ export function createTelegramInboundRouteRuntime<
822
840
  ): void => {
823
841
  Updates.reportTelegramQueueAdmission(sources, receipts);
824
842
  };
843
+ const removePendingReroute = (id: string): void => {
844
+ pendingUnboundReroutes.get(id)?.stopExpiry?.();
845
+ pendingUnboundReroutes.delete(id);
846
+ };
847
+ const expirePendingCommand = (id: string, pending: PendingUnboundReroute): boolean => {
848
+ if (pending.destinationSelected || pending.expiresAtMs === undefined ||
849
+ Date.now() < pending.expiresAtMs) return false;
850
+ if (pendingUnboundReroutes.get(id) !== pending) return true;
851
+ for (const message of pending.messages) Updates.reportTelegramUpdateCompleted(message);
852
+ removePendingReroute(id);
853
+ return true;
854
+ };
855
+ const armPendingCommandExpiry = (id: string, pending: PendingUnboundReroute): void => {
856
+ if (pending.dispatchKind !== "command" || pending.sourceTarget.threadId !== undefined) return;
857
+ pending.stopExpiry?.();
858
+ const execution = Updates.getTelegramUpdateExecutionFence(pending.messages[0]);
859
+ const onAbort = () => removePendingReroute(id);
860
+ let timer: ReturnType<typeof setTimeout> | undefined;
861
+ let stopped = false;
862
+ pending.pauseExpiry = () => {
863
+ if (timer !== undefined) clearTimeout(timer);
864
+ timer = undefined;
865
+ };
866
+ pending.stopExpiry = () => {
867
+ stopped = true;
868
+ pending.pauseExpiry?.();
869
+ execution?.signal.removeEventListener("abort", onAbort);
870
+ };
871
+ execution?.signal.addEventListener("abort", onAbort, { once: true });
872
+ if (execution?.signal.aborted) {
873
+ onAbort();
874
+ return;
875
+ }
876
+ if (pending.expiresAtMs === undefined) {
877
+ const date = pending.messages[0]?.date;
878
+ if (typeof date !== "number" || !Number.isFinite(date) || date <= 0 || date * 1000 > Date.now()) return;
879
+ pending.expiresAtMs = date * 1000 + TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS;
880
+ }
881
+ const schedule = (): void => {
882
+ if (stopped || pending.destinationSelected || pendingUnboundReroutes.get(id) !== pending) return;
883
+ if (expirePendingCommand(id, pending)) return;
884
+ const delay = Math.min(TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS, pending.expiresAtMs! - Date.now());
885
+ timer = setTimeout(schedule, Math.max(1, delay));
886
+ timer.unref?.();
887
+ };
888
+ schedule();
889
+ };
825
890
  const prunePendingUnboundReroutes = () => {
826
891
  const nowMs = Date.now();
827
892
  for (const [id, entry] of pendingUnboundReroutes) {
828
- if (nowMs - entry.createdAtMs > 30 * 60_000) {
829
- pendingUnboundReroutes.delete(id);
893
+ if (entry.dispatchKind === "command" && entry.sourceTarget.threadId === undefined) {
894
+ expirePendingCommand(id, entry);
895
+ } else if (nowMs - entry.createdAtMs > 30 * 60_000) {
896
+ removePendingReroute(id);
830
897
  }
831
898
  }
832
- while (pendingUnboundReroutes.size > 100) {
833
- const oldest = pendingUnboundReroutes.keys().next().value;
834
- if (!oldest) break;
835
- pendingUnboundReroutes.delete(oldest);
836
- }
837
899
  };
838
900
  const storePendingUnboundReroute = (
839
901
  messages: TMessage[],
840
902
  dispatchKind: "prompt" | "command" = "prompt",
841
903
  ): string => {
842
904
  prunePendingUnboundReroutes();
905
+ if (pendingUnboundReroutes.size >= 100) {
906
+ throw new Error("Telegram route chooser capacity reached; source remains retryable.");
907
+ }
843
908
  nextUnboundRerouteId += 1;
844
909
  const id = nextUnboundRerouteId.toString(36);
845
910
  pendingUnboundReroutes.set(id, {
@@ -853,11 +918,33 @@ export function createTelegramInboundRouteRuntime<
853
918
  createdAtMs: Date.now(),
854
919
  dispatchKind,
855
920
  });
921
+ const pending = pendingUnboundReroutes.get(id)!;
922
+ armPendingCommandExpiry(id, pending);
856
923
  return id;
857
924
  };
858
925
  const rememberRerouteChooser = (id: string, messageId: number | undefined): void => {
859
926
  const pending = pendingUnboundReroutes.get(id);
860
- if (pending) pending.chooserMessageId = messageId;
927
+ if (!pending) return;
928
+ pending.chooserMessageId = messageId;
929
+ if (messageId === undefined || pending.dispatchKind !== "command" ||
930
+ pending.sourceTarget.threadId !== undefined || pending.selectionAttempted) return;
931
+ const source = pending.messages[0];
932
+ const text = source?.text?.trim();
933
+ const execution = Updates.getTelegramUpdateExecutionFence(source);
934
+ const sourceIds = Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages);
935
+ if (!text || Commands.parseTelegramCommand(text)?.name !== "start" ||
936
+ source?.from?.id === undefined || !execution?.isCurrent() || sourceIds.length !== 1 ||
937
+ expirePendingCommand(id, pending)) return;
938
+ for (const [oldId, old] of pendingUnboundReroutes) {
939
+ if (oldId === id || old.dispatchKind !== "command" || old.selectionAttempted || old.dispatching ||
940
+ old.sourceTarget.threadId !== undefined || old.sourceTarget.chatId !== pending.sourceTarget.chatId) continue;
941
+ const oldSource = old.messages[0];
942
+ const oldIds = Updates.collectTelegramAdmissionSourceUpdateIds(old.messages);
943
+ if (oldSource?.from?.id !== source.from.id || oldSource?.text?.trim() !== text ||
944
+ oldIds.length !== 1 || oldIds[0]! >= sourceIds[0]! ||
945
+ Updates.getTelegramUpdateExecutionFence(oldSource)?.signal !== execution.signal) continue;
946
+ if (Updates.reportTelegramUpdateCompleted(oldSource)) removePendingReroute(oldId);
947
+ }
861
948
  };
862
949
  const matchesRerouteChooser = (
863
950
  pending: PendingUnboundReroute,
@@ -1123,12 +1210,15 @@ export function createTelegramInboundRouteRuntime<
1123
1210
  | ((messages: TMessage[], ctx: TContext) => Promise<void>)
1124
1211
  | undefined;
1125
1212
  const dispatchPendingRerouteMessages = async (
1126
- pending: { dispatchKind: "prompt" | "command" },
1213
+ pending: Pick<PendingUnboundReroute, "dispatchKind" | "sourceTarget">,
1127
1214
  messages: TMessage[],
1128
1215
  ctx: TContext,
1129
1216
  ): Promise<void> => {
1130
1217
  if (pending.dispatchKind === "command" && dispatchReroutedCommandMessages) {
1131
1218
  await dispatchReroutedCommandMessages(messages, ctx);
1219
+ if (pending.sourceTarget.threadId === undefined) {
1220
+ for (const message of messages) Updates.reportTelegramUpdateCompleted(message);
1221
+ }
1132
1222
  return;
1133
1223
  }
1134
1224
  await promptEnqueue(messages, ctx);
@@ -1147,7 +1237,7 @@ export function createTelegramInboundRouteRuntime<
1147
1237
  );
1148
1238
  assertExecutionCurrent();
1149
1239
  if (dismissed) {
1150
- pendingUnboundReroutes.delete(rerouteId);
1240
+ removePendingReroute(rerouteId);
1151
1241
  await deps.answerCallbackQuery(query.id, successMessage);
1152
1242
  return;
1153
1243
  }
@@ -1184,6 +1274,9 @@ export function createTelegramInboundRouteRuntime<
1184
1274
  outcome?.status === "fulfilled" &&
1185
1275
  outcome.value.status === "accepted"
1186
1276
  ) {
1277
+ if (pending.dispatchKind === "command" && pending.sourceTarget.threadId === undefined) {
1278
+ Updates.reportTelegramUpdateCompleted(pending.messages[index]);
1279
+ }
1187
1280
  return false;
1188
1281
  }
1189
1282
  if (outcome?.status === "rejected") {
@@ -1216,7 +1309,8 @@ export function createTelegramInboundRouteRuntime<
1216
1309
  typeof messageId !== "number" ||
1217
1310
  !deps.threadStore ||
1218
1311
  !pending ||
1219
- !matchesRerouteChooser(pending, query)
1312
+ !matchesRerouteChooser(pending, query) ||
1313
+ expirePendingCommand(parsed.rerouteId, pending)
1220
1314
  ) {
1221
1315
  await deps.answerCallbackQuery(query.id, "Message route expired.");
1222
1316
  return true;
@@ -1258,7 +1352,7 @@ export function createTelegramInboundRouteRuntime<
1258
1352
  await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
1259
1353
  return true;
1260
1354
  };
1261
- const handleUnboundRerouteCallback = async (
1355
+ const executeUnboundRerouteCallback = async (
1262
1356
  query: TCallbackQuery,
1263
1357
  ctx: TContext,
1264
1358
  ): Promise<boolean> => {
@@ -1270,12 +1364,17 @@ export function createTelegramInboundRouteRuntime<
1270
1364
  const chatId = query.message?.chat?.id;
1271
1365
  const pending = pendingUnboundReroutes.get(parsed.rerouteId);
1272
1366
  if (typeof chatId !== "number" || !deps.threadStore || !pending ||
1273
- !matchesRerouteChooser(pending, query)) {
1367
+ !matchesRerouteChooser(pending, query) || expirePendingCommand(parsed.rerouteId, pending)) {
1274
1368
  await deps.answerCallbackQuery(query.id, "Message route expired.");
1275
1369
  return true;
1276
1370
  }
1277
1371
  await deps.threadStore.load();
1278
1372
  assertExecutionCurrent();
1373
+ if (pendingUnboundReroutes.get(parsed.rerouteId) !== pending ||
1374
+ expirePendingCommand(parsed.rerouteId, pending)) {
1375
+ await deps.answerCallbackQuery(query.id, "Message route expired.");
1376
+ return true;
1377
+ }
1279
1378
  if (pending.finalizeMessage) {
1280
1379
  await finalizePendingReroute(
1281
1380
  parsed.rerouteId,
@@ -1361,6 +1460,9 @@ export function createTelegramInboundRouteRuntime<
1361
1460
  await deps.answerCallbackQuery(query.id, "🚫 Thread restore source is already owned.");
1362
1461
  return true;
1363
1462
  }
1463
+ pending.destinationSelected = true;
1464
+ pending.selectionAttempted = true;
1465
+ pending.pauseExpiry?.();
1364
1466
  const currentInstanceId = deps.getCurrentInstanceId?.();
1365
1467
  const leaderProfileKey = getLeaderTopicProfileKey(ctx, currentInstanceId);
1366
1468
  const isCurrentLeaderRecord = isCurrentLeaderTopicRecord(
@@ -1657,6 +1759,33 @@ export function createTelegramInboundRouteRuntime<
1657
1759
  );
1658
1760
  return true;
1659
1761
  };
1762
+ const handleUnboundRerouteCallback = async (
1763
+ query: TCallbackQuery,
1764
+ ctx: TContext,
1765
+ ): Promise<boolean> => {
1766
+ const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
1767
+ const pending = parsed && pendingUnboundReroutes.get(parsed.rerouteId);
1768
+ if (!parsed || !pending || pending.dispatchKind !== "command" ||
1769
+ pending.sourceTarget.threadId !== undefined || !matchesRerouteChooser(pending, query)) {
1770
+ return executeUnboundRerouteCallback(query, ctx);
1771
+ }
1772
+ if (pending.dispatching) {
1773
+ await deps.answerCallbackQuery(query.id, "Command routing is already in progress.");
1774
+ return true;
1775
+ }
1776
+ pending.dispatching = true;
1777
+ try {
1778
+ return await executeUnboundRerouteCallback(query, ctx);
1779
+ } finally {
1780
+ pending.dispatching = false;
1781
+ if (pendingUnboundReroutes.get(parsed.rerouteId) === pending &&
1782
+ pending.destinationSelected && pending.messages.length > 0 &&
1783
+ !pending.cleanup && !pending.finalizeMessage) {
1784
+ pending.destinationSelected = false;
1785
+ armPendingCommandExpiry(parsed.rerouteId, pending);
1786
+ }
1787
+ }
1788
+ };
1660
1789
  const callbackHandler = async (
1661
1790
  query: TCallbackQuery,
1662
1791
  ctx: TContext,
@@ -2122,11 +2251,11 @@ export function createTelegramInboundRouteRuntime<
2122
2251
  deps.getLiveThreadTargets?.(),
2123
2252
  );
2124
2253
  if (activeRecords.length === 0) return false;
2125
- const commandMessage = {
2254
+ const commandMessage = Updates.carryTelegramUpdateExecutionFence(message, {
2126
2255
  ...message,
2127
2256
  text: commandText,
2128
2257
  caption: undefined,
2129
- } as TMessage;
2258
+ } as TMessage);
2130
2259
  const rerouteId = storePendingUnboundReroute([commandMessage], "command");
2131
2260
  Updates.reportTelegramUpdateDeferred(commandMessage);
2132
2261
  const text = formatTelegramAllTabMenuChooserText(command.name);
@@ -2135,49 +2264,52 @@ export function createTelegramInboundRouteRuntime<
2135
2264
  activeRecords,
2136
2265
  { canRestore: typeof message.message_thread_id === "number" },
2137
2266
  );
2138
- if (deps.sendInteractiveMessage) {
2139
- const chooserId = await deps.sendInteractiveMessage(
2140
- message.chat.id,
2141
- text,
2142
- "html",
2143
- replyMarkup,
2144
- options.target || options.replyToSource
2145
- ? {
2146
- ...(options.target ? { target: options.target } : {}),
2147
- ...(options.replyToSource
2148
- ? { replyToMessageId: message.message_id }
2149
- : {}),
2150
- }
2151
- : undefined,
2152
- );
2153
- rememberRerouteChooser(rerouteId, chooserId);
2154
- return true;
2155
- }
2156
- if (deps.callApi) {
2157
- const chooser = await deps.callApi<{ message_id?: number }>("sendMessage", {
2158
- chat_id: message.chat.id,
2159
- text,
2160
- parse_mode: "HTML",
2161
- reply_markup: replyMarkup,
2162
- ...(typeof options.target?.threadId === "number"
2163
- ? { message_thread_id: options.target.threadId }
2164
- : {}),
2165
- ...(options.replyToSource
2166
- ? {
2167
- reply_parameters: {
2168
- message_id: message.message_id,
2169
- allow_sending_without_reply: true,
2170
- },
2171
- }
2172
- : {}),
2173
- });
2174
- rememberRerouteChooser(rerouteId, chooser?.message_id);
2175
- return true;
2267
+ let chooserId: number | undefined;
2268
+ try {
2269
+ if (deps.sendInteractiveMessage) {
2270
+ chooserId = await deps.sendInteractiveMessage(
2271
+ message.chat.id,
2272
+ text,
2273
+ "html",
2274
+ replyMarkup,
2275
+ options.target || options.replyToSource
2276
+ ? {
2277
+ ...(options.target ? { target: options.target } : {}),
2278
+ ...(options.replyToSource
2279
+ ? { replyToMessageId: message.message_id }
2280
+ : {}),
2281
+ }
2282
+ : undefined,
2283
+ );
2284
+ } else if (deps.callApi) {
2285
+ const chooser = await deps.callApi<{ message_id?: number }>("sendMessage", {
2286
+ chat_id: message.chat.id,
2287
+ text,
2288
+ parse_mode: "HTML",
2289
+ reply_markup: replyMarkup,
2290
+ ...(typeof options.target?.threadId === "number"
2291
+ ? { message_thread_id: options.target.threadId }
2292
+ : {}),
2293
+ ...(options.replyToSource
2294
+ ? {
2295
+ reply_parameters: {
2296
+ message_id: message.message_id,
2297
+ allow_sending_without_reply: true,
2298
+ },
2299
+ }
2300
+ : {}),
2301
+ });
2302
+ chooserId = chooser?.message_id;
2303
+ } else {
2304
+ chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
2305
+ parseMode: "HTML",
2306
+ target: options.target,
2307
+ });
2308
+ }
2309
+ } catch (error) {
2310
+ removePendingReroute(rerouteId);
2311
+ throw error;
2176
2312
  }
2177
- const chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
2178
- parseMode: "HTML",
2179
- target: options.target,
2180
- });
2181
2313
  rememberRerouteChooser(rerouteId, chooserId);
2182
2314
  return true;
2183
2315
  };
@@ -2497,6 +2629,8 @@ export function createTelegramInboundRouteRuntime<
2497
2629
  deps.getLiveThreadTargets?.(),
2498
2630
  );
2499
2631
  const command = getKnownTelegramAllTabCommand(text);
2632
+ // Returning before deferral lets the admission worker terminally settle expired replay.
2633
+ if (command && command.name !== "thread" && isTelegramAllTabCommandExpired(message)) return;
2500
2634
  if (bindings.length > 0 && command && command.name !== "thread") {
2501
2635
  if (
2502
2636
  await sendAllTabCommandChooser(command, text, message as TMessage, {