@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/commands.ts CHANGED
@@ -13,7 +13,9 @@ import type { TelegramBridgeStatusLineOptions } from "./status.ts";
13
13
  import {
14
14
  createTelegramControlItemBuilder,
15
15
  createTelegramControlQueueController,
16
+ createTelegramQueueAdmissionReceipt,
16
17
  type PendingTelegramControlItem,
18
+ type TelegramQueueAdmissionReceipt,
17
19
  } from "./queue.ts";
18
20
 
19
21
  export interface ParsedTelegramCommand {
@@ -282,7 +284,16 @@ export async function registerTelegramBotCommands(
282
284
  export function createTelegramBotCommandRegistrar(
283
285
  deps: TelegramBotCommandRegistrationDeps,
284
286
  ): () => Promise<void> {
285
- return () => registerTelegramBotCommands(deps);
287
+ let pending: Promise<void> | undefined;
288
+ return () => {
289
+ if (pending) return pending;
290
+ let request: Promise<void>;
291
+ request = registerTelegramBotCommands(deps).finally(() => {
292
+ if (pending === request) pending = undefined;
293
+ });
294
+ pending = request;
295
+ return request;
296
+ };
286
297
  }
287
298
 
288
299
  export interface TelegramBridgeCommandStartPollingOptions {
@@ -652,6 +663,7 @@ export interface TelegramCommandRuntimeMessage {
652
663
  message_id: number;
653
664
  message_thread_id?: number;
654
665
  from?: { id?: number };
666
+ pi_telegram_source_update_id?: number;
655
667
  }
656
668
 
657
669
  export interface TelegramCommandMessageTarget {
@@ -673,6 +685,14 @@ export interface TelegramCommandTargetRuntimeDeps<TContext> {
673
685
  controlType: TelegramControlCommandType,
674
686
  statusSummary: string,
675
687
  execute: (ctx: TContext) => Promise<void>,
688
+ admissionReceipts?: TelegramQueueAdmissionReceipt[],
689
+ onQueued?: (item: PendingTelegramControlItem<TContext>) => void,
690
+ ) => void;
691
+ getAdmissionScope?: () => string | undefined;
692
+ getAdmissionJournalBinding?: () => string | undefined;
693
+ onControlQueued?: (
694
+ message: TelegramCommandRuntimeMessage,
695
+ receipt: TelegramQueueAdmissionReceipt,
676
696
  ) => void;
677
697
  showStatus: (
678
698
  chatId: number,
@@ -737,6 +757,7 @@ export interface TelegramCommandControlQueueRuntimeDeps<TContext> {
737
757
  replyToMessageId: number;
738
758
  controlType: TelegramControlCommandType;
739
759
  statusSummary: string;
760
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
740
761
  execute: (ctx: TContext) => Promise<void>;
741
762
  }) => PendingTelegramControlItem<TContext>;
742
763
  appendControlItem: (
@@ -766,22 +787,34 @@ export function createTelegramCommandControlEnqueueAdapter<TContext>(deps: {
766
787
  replyToMessageId: number;
767
788
  controlType: TelegramControlCommandType;
768
789
  statusSummary: string;
790
+ admissionReceipts?: TelegramQueueAdmissionReceipt[];
769
791
  execute: (ctx: TContext) => Promise<void>;
770
792
  }) => PendingTelegramControlItem<TContext>;
771
793
  enqueueControlItem: (
772
794
  item: PendingTelegramControlItem<TContext>,
773
795
  ctx: TContext,
796
+ onQueued?: (item: PendingTelegramControlItem<TContext>) => void,
774
797
  ) => void;
775
798
  }): TelegramCommandTargetRuntimeDeps<TContext>["enqueueControlItem"] {
776
- return (target, ctx, controlType, statusSummary, execute) => {
799
+ return (
800
+ target,
801
+ ctx,
802
+ controlType,
803
+ statusSummary,
804
+ execute,
805
+ admissionReceipts,
806
+ onQueued,
807
+ ) => {
777
808
  deps.enqueueControlItem(
778
809
  deps.createControlItem({
779
810
  ...target,
780
811
  controlType,
781
812
  statusSummary,
813
+ ...(admissionReceipts?.length ? { admissionReceipts } : {}),
782
814
  execute,
783
815
  }),
784
816
  ctx,
817
+ onQueued,
785
818
  );
786
819
  };
787
820
  }
@@ -802,6 +835,9 @@ export function createTelegramCommandTargetQueueRuntime<
802
835
  appendControlItem: deps.appendControlItem,
803
836
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
804
837
  }),
838
+ getAdmissionScope: deps.getAdmissionScope,
839
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
840
+ onControlQueued: deps.onControlQueued,
805
841
  showStatus: deps.showStatus,
806
842
  openModelMenu: deps.openModelMenu,
807
843
  openSettingsMenu: deps.openSettingsMenu,
@@ -817,12 +853,32 @@ export function createTelegramCommandTargetRuntime<
817
853
  ): TelegramCommandTargetRuntime<TMessage, TContext> {
818
854
  return {
819
855
  enqueueControlItem: (message, ctx, controlType, statusSummary, execute) => {
856
+ const sourceUpdateId = message.pi_telegram_source_update_id;
857
+ const baseReceipt =
858
+ typeof sourceUpdateId === "number"
859
+ ? createTelegramQueueAdmissionReceipt({
860
+ queueKind: "control",
861
+ scope: deps.getAdmissionScope?.() ?? "",
862
+ sourceUpdateIds: [sourceUpdateId],
863
+ })
864
+ : undefined;
865
+ const journalBindingKey = deps.getAdmissionJournalBinding?.();
866
+ const receipt = baseReceipt
867
+ ? {
868
+ ...baseReceipt,
869
+ ...(journalBindingKey ? { journalBindingKey } : {}),
870
+ }
871
+ : undefined;
820
872
  deps.enqueueControlItem(
821
873
  getTelegramCommandMessageTarget(message),
822
874
  ctx,
823
875
  controlType,
824
876
  statusSummary,
825
877
  execute,
878
+ receipt ? [receipt] : undefined,
879
+ receipt
880
+ ? () => deps.onControlQueued?.(message, receipt)
881
+ : undefined,
826
882
  );
827
883
  },
828
884
  showStatus: (message, ctx) => {
@@ -889,6 +945,7 @@ export interface TelegramCommandOrPromptRuntimeDeps<TMessage, TContext> {
889
945
  ) => string | undefined;
890
946
  replaceMessageText: (message: TMessage, text: string) => TMessage;
891
947
  enqueueTurn: (messages: TMessage[], ctx: TContext) => Promise<void>;
948
+ assertExecutionCurrent?: (message: TMessage) => void;
892
949
  }
893
950
 
894
951
  export interface TelegramCommandRuntimeDeps<
@@ -908,6 +965,7 @@ export interface TelegramCommandRuntimeDeps<
908
965
  isCompactionInProgress: () => boolean;
909
966
  setCompactionInProgress: (inProgress: boolean) => void;
910
967
  updateStatus: (ctx: TContext) => void;
968
+ isContextActive?: (ctx: TContext) => boolean;
911
969
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
912
970
  requestDeferredDispatchNextQueuedTelegramTurn?: (
913
971
  dispatch: (ctx: TContext) => void,
@@ -946,6 +1004,7 @@ export interface TelegramCommandRuntimeDeps<
946
1004
  persistConfig: () => Promise<void>;
947
1005
  sendTextReply: (message: TMessage, text: string) => Promise<void>;
948
1006
  sendInteractiveMessage?: TelegramCompactConfirmationDeps["sendInteractiveMessage"];
1007
+ assertExecutionCurrent?: (message: TMessage) => void;
949
1008
  }
950
1009
 
951
1010
  export const TELEGRAM_APP_MENU_INTRO_HTML = [
@@ -1420,6 +1479,9 @@ export function createTelegramCommandHandlerTargetRuntime<
1420
1479
  }),
1421
1480
  appendControlItem: deps.appendControlItem,
1422
1481
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
1482
+ getAdmissionScope: deps.getAdmissionScope,
1483
+ getAdmissionJournalBinding: deps.getAdmissionJournalBinding,
1484
+ onControlQueued: deps.onControlQueued,
1423
1485
  showStatus: deps.showStatus,
1424
1486
  openModelMenu: deps.openModelMenu,
1425
1487
  openSettingsMenu: deps.openSettingsMenu,
@@ -1439,6 +1501,7 @@ export function createTelegramCommandHandlerTargetRuntime<
1439
1501
  isCompactionInProgress: deps.isCompactionInProgress,
1440
1502
  setCompactionInProgress: deps.setCompactionInProgress,
1441
1503
  updateStatus: deps.updateStatus,
1504
+ isContextActive: deps.isContextActive,
1442
1505
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
1443
1506
  startTypingLoop: deps.startTypingLoop,
1444
1507
  stopTypingLoop: deps.stopTypingLoop,
@@ -1487,12 +1550,14 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
1487
1550
  const firstMessage = messages[0];
1488
1551
  if (!firstMessage) return;
1489
1552
  if (deps.shouldIgnoreMessages?.(messages)) return;
1553
+ deps.assertExecutionCurrent?.(firstMessage);
1490
1554
  const command = parseTelegramCommand(deps.extractRawText(messages));
1491
1555
  const handled = await deps.handleCommand(
1492
1556
  command?.name,
1493
1557
  firstMessage,
1494
1558
  ctx,
1495
1559
  );
1560
+ deps.assertExecutionCurrent?.(firstMessage);
1496
1561
  if (handled) return;
1497
1562
  if (command && deps.executeExtensionCommand) {
1498
1563
  const handledByExtension = await deps.executeExtensionCommand(
@@ -1500,6 +1565,7 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
1500
1565
  messages[0]!,
1501
1566
  ctx,
1502
1567
  );
1568
+ deps.assertExecutionCurrent?.(firstMessage);
1503
1569
  if (handledByExtension) return;
1504
1570
  }
1505
1571
  if (command?.name && deps.expandPromptTemplateCommand) {
@@ -1508,6 +1574,7 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
1508
1574
  command.args,
1509
1575
  );
1510
1576
  if (expanded !== undefined) {
1577
+ deps.assertExecutionCurrent?.(firstMessage);
1511
1578
  await deps.enqueueTurn(
1512
1579
  [
1513
1580
  deps.replaceMessageText(firstMessage, expanded),
@@ -1518,11 +1585,41 @@ export function createTelegramCommandOrPromptRuntime<TMessage, TContext>(
1518
1585
  return;
1519
1586
  }
1520
1587
  }
1588
+ deps.assertExecutionCurrent?.(firstMessage);
1521
1589
  await deps.enqueueTurn(messages, ctx);
1522
1590
  },
1523
1591
  };
1524
1592
  }
1525
1593
 
1594
+ function scheduleTelegramCommandEffect<TContext>(
1595
+ ctx: TContext,
1596
+ command: string,
1597
+ phase: string,
1598
+ deps: TelegramRuntimeEventRecorderPort & {
1599
+ isContextActive?: (ctx: TContext) => boolean;
1600
+ },
1601
+ effect: () => Promise<void>,
1602
+ assertExecutionCurrent?: () => void,
1603
+ ): void {
1604
+ void Promise.resolve()
1605
+ .then(async () => {
1606
+ if (deps.isContextActive?.(ctx) === false) return;
1607
+ assertExecutionCurrent?.();
1608
+ await effect();
1609
+ assertExecutionCurrent?.();
1610
+ })
1611
+ .catch((error) => {
1612
+ try {
1613
+ deps.recordRuntimeEvent?.("telegram-command", error, {
1614
+ command,
1615
+ phase,
1616
+ });
1617
+ } catch {
1618
+ // Effect diagnostics cannot create an unhandled detached Promise.
1619
+ }
1620
+ });
1621
+ }
1622
+
1526
1623
  async function handleTelegramCommandRuntime<
1527
1624
  TMessage extends TelegramCommandRuntimeMessage,
1528
1625
  TContext,
@@ -1532,8 +1629,13 @@ async function handleTelegramCommandRuntime<
1532
1629
  ctx: TContext,
1533
1630
  deps: TelegramCommandRuntimeDeps<TMessage, TContext>,
1534
1631
  ): Promise<boolean> {
1535
- const sendReplyFor = (nextMessage: TMessage) => (text: string) =>
1536
- deps.sendTextReply(nextMessage, text);
1632
+ const assertExecutionCurrentFor = (nextMessage: TMessage) => (): void =>
1633
+ deps.assertExecutionCurrent?.(nextMessage);
1634
+ const sendReplyFor = (nextMessage: TMessage) => async (text: string) => {
1635
+ deps.assertExecutionCurrent?.(nextMessage);
1636
+ await deps.sendTextReply(nextMessage, text);
1637
+ deps.assertExecutionCurrent?.(nextMessage);
1638
+ };
1537
1639
  const updateStatusFor = (commandCtx: TContext) => () =>
1538
1640
  deps.updateStatus(commandCtx);
1539
1641
  return executeTelegramCommandAction(
@@ -1585,7 +1687,14 @@ async function handleTelegramCommandRuntime<
1585
1687
  });
1586
1688
  },
1587
1689
  handleQueue: async (nextMessage, commandCtx) => {
1588
- await deps.openQueueMenu(nextMessage, commandCtx);
1690
+ scheduleTelegramCommandEffect(
1691
+ commandCtx,
1692
+ "queue",
1693
+ "menu-render",
1694
+ deps,
1695
+ () => deps.openQueueMenu(nextMessage, commandCtx),
1696
+ assertExecutionCurrentFor(nextMessage),
1697
+ );
1589
1698
  },
1590
1699
  handleCompact: async (nextMessage, commandCtx) => {
1591
1700
  if (deps.sendInteractiveMessage) {
@@ -1626,33 +1735,53 @@ async function handleTelegramCommandRuntime<
1626
1735
  });
1627
1736
  },
1628
1737
  handleStatus: async (nextMessage, commandCtx) => {
1629
- await deps.showStatus(nextMessage, commandCtx);
1738
+ scheduleTelegramCommandEffect(
1739
+ commandCtx,
1740
+ "status",
1741
+ "menu-render",
1742
+ deps,
1743
+ () => deps.showStatus(nextMessage, commandCtx),
1744
+ assertExecutionCurrentFor(nextMessage),
1745
+ );
1630
1746
  },
1631
1747
  handleModel: async (nextMessage, commandCtx) => {
1632
- await handleTelegramModelCommand<TContext>({
1633
- ctx: commandCtx,
1634
- openModelMenu: (controlCtx) =>
1635
- deps.openModelMenu(nextMessage, controlCtx),
1636
- });
1748
+ scheduleTelegramCommandEffect(
1749
+ commandCtx,
1750
+ "model",
1751
+ "menu-render",
1752
+ deps,
1753
+ () =>
1754
+ handleTelegramModelCommand<TContext>({
1755
+ ctx: commandCtx,
1756
+ openModelMenu: (controlCtx) =>
1757
+ deps.openModelMenu(nextMessage, controlCtx),
1758
+ }),
1759
+ assertExecutionCurrentFor(nextMessage),
1760
+ );
1637
1761
  },
1638
1762
  handleThinking: async (nextMessage, commandCtx) => {
1639
- await deps.openThinkingMenu(nextMessage, commandCtx);
1763
+ scheduleTelegramCommandEffect(
1764
+ commandCtx,
1765
+ "thinking",
1766
+ "menu-render",
1767
+ deps,
1768
+ () => deps.openThinkingMenu(nextMessage, commandCtx),
1769
+ assertExecutionCurrentFor(nextMessage),
1770
+ );
1640
1771
  },
1641
1772
  handleSettings: deps.openSettingsMenu
1642
1773
  ? async (nextMessage, commandCtx) => {
1643
- await deps.openSettingsMenu?.(nextMessage, commandCtx);
1774
+ scheduleTelegramCommandEffect(
1775
+ commandCtx,
1776
+ "settings",
1777
+ "menu-render",
1778
+ deps,
1779
+ () => deps.openSettingsMenu!(nextMessage, commandCtx),
1780
+ assertExecutionCurrentFor(nextMessage),
1781
+ );
1644
1782
  }
1645
1783
  : undefined,
1646
1784
  handleHelp: async (nextMessage, nextCommandName, commandCtx) => {
1647
- try {
1648
- await deps.registerBotCommands();
1649
- } catch (error) {
1650
- const errorMessage = getTelegramCommandErrorMessage(error);
1651
- await deps.sendTextReply(
1652
- nextMessage,
1653
- `Warning: failed to register bot commands menu: ${errorMessage}`,
1654
- );
1655
- }
1656
1785
  if (
1657
1786
  nextMessage.from?.id !== undefined &&
1658
1787
  canPairTelegramUserFromCommandMessage(nextMessage)
@@ -1663,16 +1792,41 @@ async function handleTelegramCommandRuntime<
1663
1792
  setAllowedUserId: deps.setAllowedUserId,
1664
1793
  persistConfig: deps.persistConfig,
1665
1794
  updateStatus: updateStatusFor(commandCtx),
1795
+ assertExecutionCurrent: assertExecutionCurrentFor(nextMessage),
1666
1796
  });
1667
1797
  }
1668
- const forumBootstrapMessage =
1669
- nextCommandName === "start"
1670
- ? await deps.handleForumBootstrap?.(nextMessage, commandCtx)
1671
- : undefined;
1672
- if (forumBootstrapMessage) {
1673
- await deps.sendTextReply(nextMessage, forumBootstrapMessage);
1674
- }
1675
- await deps.showStatus(nextMessage, commandCtx);
1798
+ const isContextActive = () =>
1799
+ deps.isContextActive?.(commandCtx) !== false;
1800
+ scheduleTelegramCommandEffect(
1801
+ commandCtx,
1802
+ nextCommandName,
1803
+ "menu-render",
1804
+ deps,
1805
+ async () => {
1806
+ let forumBootstrapMessage: string | undefined;
1807
+ if (nextCommandName === "start" && deps.handleForumBootstrap) {
1808
+ forumBootstrapMessage = await deps.handleForumBootstrap(
1809
+ nextMessage,
1810
+ commandCtx,
1811
+ );
1812
+ }
1813
+ if (!isContextActive()) return;
1814
+ if (forumBootstrapMessage) {
1815
+ await deps.sendTextReply(nextMessage, forumBootstrapMessage);
1816
+ }
1817
+ if (!isContextActive()) return;
1818
+ await deps.showStatus(nextMessage, commandCtx);
1819
+ },
1820
+ assertExecutionCurrentFor(nextMessage),
1821
+ );
1822
+ scheduleTelegramCommandEffect(
1823
+ commandCtx,
1824
+ nextCommandName,
1825
+ "bot-command-sync",
1826
+ deps,
1827
+ deps.registerBotCommands,
1828
+ assertExecutionCurrentFor(nextMessage),
1829
+ );
1676
1830
  },
1677
1831
  },
1678
1832
  );
package/lib/config.ts CHANGED
@@ -186,6 +186,18 @@ export interface TelegramConfigStore {
186
186
  persist: (config?: TelegramConfig) => Promise<void>;
187
187
  }
188
188
 
189
+ export function createTelegramConfigBotIdGetter(
190
+ store: Pick<TelegramConfigStore, "get">,
191
+ ): () => number | undefined {
192
+ return () => store.get().botId;
193
+ }
194
+
195
+ export function createTelegramActiveProfileKeyGetter(
196
+ store: Pick<TelegramConfigStore, "getActiveProfileName">,
197
+ ): () => string {
198
+ return () => store.getActiveProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
199
+ }
200
+
189
201
  export interface TelegramConfigStoreOptions {
190
202
  initialConfig?: TelegramConfig;
191
203
  agentDir?: string;
@@ -1053,6 +1065,7 @@ export interface TelegramUserPairingDeps<TContext> {
1053
1065
  setAllowedUserId: (userId: number) => void;
1054
1066
  persistConfig: () => Promise<void>;
1055
1067
  updateStatus: (ctx: TContext) => void;
1068
+ assertExecutionCurrent?: () => void;
1056
1069
  }
1057
1070
 
1058
1071
  export interface TelegramUserPairingRuntimeDeps<TContext> {
@@ -1063,7 +1076,11 @@ export interface TelegramUserPairingRuntimeDeps<TContext> {
1063
1076
  }
1064
1077
 
1065
1078
  export interface TelegramUserPairingRuntime<TContext> {
1066
- pairIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
1079
+ pairIfNeeded: (
1080
+ userId: number,
1081
+ ctx: TContext,
1082
+ assertExecutionCurrent?: () => void,
1083
+ ) => Promise<boolean>;
1067
1084
  }
1068
1085
 
1069
1086
  export function getTelegramAuthorizationState(
@@ -1096,8 +1113,11 @@ export async function pairTelegramUserIfNeeded<TContext>(
1096
1113
  deps.allowedUserId,
1097
1114
  );
1098
1115
  if (authorization.kind !== "pair") return false;
1116
+ deps.assertExecutionCurrent?.();
1099
1117
  deps.setAllowedUserId(authorization.userId);
1118
+ deps.assertExecutionCurrent?.();
1100
1119
  await deps.persistConfig();
1120
+ deps.assertExecutionCurrent?.();
1101
1121
  try {
1102
1122
  deps.updateStatus(deps.ctx);
1103
1123
  } catch (error) {
@@ -1110,13 +1130,14 @@ export function createTelegramUserPairingRuntime<TContext>(
1110
1130
  deps: TelegramUserPairingRuntimeDeps<TContext>,
1111
1131
  ): TelegramUserPairingRuntime<TContext> {
1112
1132
  return {
1113
- pairIfNeeded: (userId, ctx) =>
1133
+ pairIfNeeded: (userId, ctx, assertExecutionCurrent) =>
1114
1134
  pairTelegramUserIfNeeded(userId, {
1115
1135
  allowedUserId: deps.getAllowedUserId(),
1116
1136
  ctx,
1117
1137
  setAllowedUserId: deps.setAllowedUserId,
1118
1138
  persistConfig: deps.persistConfig,
1119
1139
  updateStatus: deps.updateStatus,
1140
+ assertExecutionCurrent,
1120
1141
  }),
1121
1142
  };
1122
1143
  }