@llblab/pi-telegram 0.21.1 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/queue.ts CHANGED
@@ -25,15 +25,12 @@ export interface TelegramPromptImageContent {
25
25
  }
26
26
 
27
27
  export type TelegramPromptContent =
28
- | TelegramPromptTextContent
29
- | TelegramPromptImageContent;
28
+ TelegramPromptTextContent | TelegramPromptImageContent;
30
29
 
31
30
  export type TelegramQueueItemKind = "prompt" | "control";
32
31
  export type TelegramQueueLane = "control" | "priority" | "default";
33
32
  export type TelegramQueueAdmissionMode =
34
- | "control-queue"
35
- | "priority-queue"
36
- | "default-queue";
33
+ "control-queue" | "priority-queue" | "default-queue";
37
34
 
38
35
  export interface TelegramQueueLaneContract {
39
36
  lane: TelegramQueueLane;
@@ -72,10 +69,21 @@ export interface TelegramQueueTarget {
72
69
  threadId?: number;
73
70
  }
74
71
 
72
+ export interface TelegramTransportStamp {
73
+ profile: string;
74
+ generation: string;
75
+ }
76
+
77
+ export interface TelegramTransportStampRuntime {
78
+ getStamp(): TelegramTransportStamp;
79
+ isActive(stamp: TelegramTransportStamp | undefined): boolean;
80
+ }
81
+
75
82
  export interface TelegramQueueItemBase {
76
83
  kind: TelegramQueueItemKind;
77
84
  chatId: number;
78
85
  target?: TelegramQueueTarget;
86
+ transportStamp?: TelegramTransportStamp;
79
87
  replyToMessageId: number;
80
88
  guestQueryId?: string;
81
89
  queueOrder: number;
@@ -107,8 +115,7 @@ export interface PendingTelegramControlItem<
107
115
  }
108
116
 
109
117
  export type TelegramQueueItem<TContext = unknown> =
110
- | PendingTelegramTurn
111
- | PendingTelegramControlItem<TContext>;
118
+ PendingTelegramTurn | PendingTelegramControlItem<TContext>;
112
119
 
113
120
  export interface TelegramQueueStore<TContext = unknown> {
114
121
  getQueuedItems: () => TelegramQueueItem<TContext>[];
@@ -199,6 +206,54 @@ export function createTelegramQueueStore<TContext = unknown>(
199
206
  };
200
207
  }
201
208
 
209
+ export function createTelegramTransportStampRuntime(deps: {
210
+ getProfileName(): string | undefined;
211
+ getBotToken(): string | undefined;
212
+ }): TelegramTransportStampRuntime {
213
+ let profile: string | undefined;
214
+ let botToken: string | undefined;
215
+ let generation = 0;
216
+ const getStamp = function (): TelegramTransportStamp {
217
+ const nextProfile = deps.getProfileName() ?? "default";
218
+ const nextBotToken = deps.getBotToken();
219
+ if (nextProfile !== profile || nextBotToken !== botToken) {
220
+ profile = nextProfile;
221
+ botToken = nextBotToken;
222
+ generation += 1;
223
+ }
224
+ return { profile: nextProfile, generation: String(generation) };
225
+ };
226
+ return {
227
+ getStamp,
228
+ isActive(stamp) {
229
+ if (!stamp) return false;
230
+ const current = getStamp();
231
+ return (
232
+ stamp.profile === current.profile &&
233
+ stamp.generation === current.generation
234
+ );
235
+ },
236
+ };
237
+ }
238
+
239
+ export function createTelegramTransportStampedQueueStore<TContext>(
240
+ store: TelegramQueueStateStore<TContext>,
241
+ getTransportStamp: () => TelegramTransportStamp,
242
+ ): TelegramQueueStateStore<TContext> {
243
+ return {
244
+ getQueuedItems: store.getQueuedItems,
245
+ hasQueuedItems: store.hasQueuedItems,
246
+ setQueuedItems(items) {
247
+ const stamp = getTransportStamp();
248
+ store.setQueuedItems(
249
+ items.map((item) =>
250
+ item.transportStamp ? item : { ...item, transportStamp: stamp },
251
+ ),
252
+ );
253
+ },
254
+ };
255
+ }
256
+
202
257
  export function createTelegramQueueItemCountGetter<TContext = unknown>(
203
258
  store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
204
259
  ): () => number {
@@ -275,7 +330,9 @@ export function appendTelegramQueueItem<
275
330
 
276
331
  function getTelegramPromptTextSignature(item: PendingTelegramTurn): string {
277
332
  return item.content
278
- .filter((entry): entry is TelegramPromptTextContent => entry.type === "text")
333
+ .filter(
334
+ (entry): entry is TelegramPromptTextContent => entry.type === "text",
335
+ )
279
336
  .map((entry) => entry.text)
280
337
  .join("\n");
281
338
  }
@@ -288,7 +345,8 @@ function isDuplicateTelegramPromptTurn(
288
345
  left.chatId === right.chatId &&
289
346
  left.target?.threadId === right.target?.threadId &&
290
347
  left.replyToMessageId === right.replyToMessageId &&
291
- getTelegramPromptTextSignature(left) === getTelegramPromptTextSignature(right)
348
+ getTelegramPromptTextSignature(left) ===
349
+ getTelegramPromptTextSignature(right)
292
350
  );
293
351
  }
294
352
 
@@ -298,7 +356,8 @@ export function appendTelegramPromptTurnOnce<TContext = unknown>(
298
356
  ): { items: TelegramQueueItem<TContext>[]; appended: boolean } {
299
357
  assertTelegramQueueItemAdmissionValid(turn);
300
358
  const duplicate = items.some(
301
- (item) => isPendingTelegramTurn(item) && isDuplicateTelegramPromptTurn(item, turn),
359
+ (item) =>
360
+ isPendingTelegramTurn(item) && isDuplicateTelegramPromptTurn(item, turn),
302
361
  );
303
362
  if (duplicate) return { items, appended: false };
304
363
  return { items: [...items, turn], appended: true };
@@ -865,6 +924,8 @@ export interface TelegramAgentEndRuntimeDeps<
865
924
  assistant: TelegramAgentEndAssistantResult;
866
925
  foldQueuedPromptsIntoHistory: boolean;
867
926
  resetRuntimeState: () => void;
927
+ isSessionActive?: () => boolean;
928
+ isTurnTransportActive?: (turn: TTurn) => boolean;
868
929
  waitForTypingIdle?: () => Promise<void>;
869
930
  updateStatus: () => void;
870
931
  dispatchNextQueuedTelegramTurn: () => void;
@@ -941,6 +1002,8 @@ export interface TelegramAgentEndHookRuntimeDeps<
941
1002
  ) => TelegramAgentEndAssistantResult;
942
1003
  getFoldQueuedPromptsIntoHistory: () => boolean;
943
1004
  resetRuntimeState: () => void;
1005
+ isSessionActive?: (ctx: TContext) => boolean;
1006
+ isTurnTransportActive?: (turn: TTurn) => boolean;
944
1007
  waitForTypingIdle?: () => Promise<void>;
945
1008
  updateStatus: (ctx: TContext) => void;
946
1009
  dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
@@ -1070,6 +1133,7 @@ export function createTelegramAgentEndHook<
1070
1133
  ctx: TContext,
1071
1134
  ): Promise<void> => {
1072
1135
  await deps.loadConfig?.();
1136
+ if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
1073
1137
  const turn = deps.getActiveTurn();
1074
1138
  const proactiveEnabled = deps.isProactivePushEnabled?.() ?? false;
1075
1139
  const canProactivePush = deps.canSendProactivePush?.(ctx) ?? false;
@@ -1079,6 +1143,8 @@ export function createTelegramAgentEndHook<
1079
1143
  turn || proactiveEnabled ? deps.extractAssistant(event.messages) : {},
1080
1144
  foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
1081
1145
  resetRuntimeState: deps.resetRuntimeState,
1146
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
1147
+ isTurnTransportActive: deps.isTurnTransportActive,
1082
1148
  waitForTypingIdle: deps.waitForTypingIdle,
1083
1149
  updateStatus: () => deps.updateStatus(ctx),
1084
1150
  dispatchNextQueuedTelegramTurn: () => {
@@ -1086,7 +1152,13 @@ export function createTelegramAgentEndHook<
1086
1152
  deps.dispatchNextQueuedTelegramTurn,
1087
1153
  );
1088
1154
  },
1089
- scheduleActiveTurnDelivery: deps.scheduleActiveTurnDelivery,
1155
+ scheduleActiveTurnDelivery: deps.scheduleActiveTurnDelivery
1156
+ ? (task) =>
1157
+ deps.scheduleActiveTurnDelivery?.(async () => {
1158
+ if (deps.isSessionActive?.(ctx) === false) return;
1159
+ await task();
1160
+ })
1161
+ : undefined,
1090
1162
  clearPreview: deps.clearPreview,
1091
1163
  setPreviewPendingText: deps.setPreviewPendingText,
1092
1164
  finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
@@ -1144,8 +1216,22 @@ export async function handleTelegramAgentEndRuntime<
1144
1216
  const hasOutboundArtifacts =
1145
1217
  !!outboundReply?.voiceText || !!outboundReply?.voiceReplies?.length;
1146
1218
  const replyMarkup = outboundReply?.replyMarkup;
1219
+ const isDeliveryActive = (): boolean =>
1220
+ deps.isSessionActive?.() !== false &&
1221
+ (!turn || deps.isTurnTransportActive?.(turn) !== false);
1222
+ if (!isDeliveryActive()) {
1223
+ deps.resetRuntimeState();
1224
+ deps.updateStatus();
1225
+ deps.dispatchNextQueuedTelegramTurn();
1226
+ return;
1227
+ }
1147
1228
  deps.resetRuntimeState();
1148
1229
  await deps.waitForTypingIdle?.();
1230
+ if (!isDeliveryActive()) {
1231
+ deps.updateStatus();
1232
+ deps.dispatchNextQueuedTelegramTurn();
1233
+ return;
1234
+ }
1149
1235
  deps.updateStatus();
1150
1236
  const endPlan = buildTelegramAgentEndPlan({
1151
1237
  hasTurn: !!turn,
@@ -1160,7 +1246,8 @@ export async function handleTelegramAgentEndRuntime<
1160
1246
  if (proactiveEnabled && finalText && !assistant.errorMessage) {
1161
1247
  if (canProactivePush) {
1162
1248
  const defaultTarget = deps.getDefaultTarget?.();
1163
- const defaultChatId = defaultTarget?.chatId ?? deps.getDefaultChatId?.();
1249
+ const defaultChatId =
1250
+ defaultTarget?.chatId ?? deps.getDefaultChatId?.();
1164
1251
  if (defaultChatId !== undefined) {
1165
1252
  try {
1166
1253
  await deps.sendMarkdownReply(defaultChatId, undefined, finalText, {
@@ -1233,12 +1320,14 @@ export async function handleTelegramAgentEndRuntime<
1233
1320
  await deps.answerGuestQuery?.(turn.guestQueryId, finalText);
1234
1321
  }
1235
1322
  }
1323
+ if (!isDeliveryActive()) return;
1236
1324
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1237
1325
  return;
1238
1326
  }
1239
1327
  if (endPlan.shouldClearPreview) {
1240
1328
  await deps.clearPreview(turn.chatId, { target: turn.target });
1241
1329
  }
1330
+ if (!isDeliveryActive()) return;
1242
1331
  if (endPlan.shouldSendErrorMessage) {
1243
1332
  await deps.sendTextReply(
1244
1333
  turn.chatId,
@@ -1247,13 +1336,16 @@ export async function handleTelegramAgentEndRuntime<
1247
1336
  "Telegram bridge: Pi failed while processing the request.",
1248
1337
  { target: turn.target },
1249
1338
  );
1339
+ if (!isDeliveryActive()) return;
1250
1340
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1251
1341
  return;
1252
1342
  }
1253
1343
  const deliverActiveTurn = async () => {
1344
+ if (!isDeliveryActive()) return;
1254
1345
  if (finalText) deps.setPreviewPendingText(finalText);
1255
1346
  if (!finalText && hasOutboundArtifacts)
1256
1347
  await deps.clearPreview(turn.chatId, { target: turn.target });
1348
+ if (!isDeliveryActive()) return;
1257
1349
  if (endPlan.kind === "text" && finalText) {
1258
1350
  try {
1259
1351
  const finalized = await deps.finalizeMarkdownPreview(
@@ -1262,8 +1354,10 @@ export async function handleTelegramAgentEndRuntime<
1262
1354
  turn.replyToMessageId,
1263
1355
  { replyMarkup, target: turn.target },
1264
1356
  );
1357
+ if (!isDeliveryActive()) return;
1265
1358
  if (!finalized) {
1266
1359
  await deps.clearPreview(turn.chatId, { target: turn.target });
1360
+ if (!isDeliveryActive()) return;
1267
1361
  await deps.sendMarkdownReply(
1268
1362
  turn.chatId,
1269
1363
  turn.replyToMessageId,
@@ -1279,27 +1373,35 @@ export async function handleTelegramAgentEndRuntime<
1279
1373
  });
1280
1374
  }
1281
1375
  }
1376
+ if (!isDeliveryActive()) return;
1282
1377
  if (outboundReply && deps.sendOutboundReplyArtifacts) {
1283
1378
  try {
1284
1379
  await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
1285
1380
  replyToPrompt: !finalText,
1286
1381
  });
1382
+ if (!isDeliveryActive()) return;
1287
1383
  } catch (error) {
1288
1384
  deps.recordRuntimeEvent?.("delivery", error, {
1289
1385
  phase: "voice-artifacts",
1290
1386
  chatId: turn.chatId,
1291
1387
  });
1292
1388
  // Fallback to planned text when voice delivery fails and text wasn't already delivered
1389
+ if (!isDeliveryActive()) return;
1293
1390
  if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
1294
1391
  try {
1295
1392
  const fallbackMarkdown =
1296
- plannedReply?.markdown || outboundReply?.voiceText || rawFinalText;
1393
+ plannedReply?.markdown ||
1394
+ outboundReply?.voiceText ||
1395
+ rawFinalText;
1297
1396
  await deps.sendMarkdownReply(
1298
1397
  turn.chatId,
1299
1398
  turn.replyToMessageId,
1300
1399
  fallbackMarkdown,
1301
1400
  plannedReply?.replyMarkup || turn.target
1302
- ? { replyMarkup: plannedReply?.replyMarkup, target: turn.target }
1401
+ ? {
1402
+ replyMarkup: plannedReply?.replyMarkup,
1403
+ target: turn.target,
1404
+ }
1303
1405
  : undefined,
1304
1406
  );
1305
1407
  } catch (fallbackError) {
@@ -1311,6 +1413,7 @@ export async function handleTelegramAgentEndRuntime<
1311
1413
  }
1312
1414
  }
1313
1415
  }
1416
+ if (!isDeliveryActive()) return;
1314
1417
  if (endPlan.shouldSendAttachmentNotice) {
1315
1418
  await deps.sendTextReply(
1316
1419
  turn.chatId,
@@ -1319,7 +1422,9 @@ export async function handleTelegramAgentEndRuntime<
1319
1422
  { target: turn.target },
1320
1423
  );
1321
1424
  }
1425
+ if (!isDeliveryActive()) return;
1322
1426
  await deps.sendQueuedAttachments(turn);
1427
+ if (!isDeliveryActive()) return;
1323
1428
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
1324
1429
  };
1325
1430
  if (
@@ -1387,6 +1492,7 @@ export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
1387
1492
  ctx: TContext;
1388
1493
  currentModel: TModel | undefined;
1389
1494
  loadConfig: () => Promise<void>;
1495
+ isSessionActive?: () => boolean;
1390
1496
  applyState: (state: TelegramSessionStartState<TModel>) => void;
1391
1497
  bindDeferredDispatchContext?: (ctx: TContext) => void;
1392
1498
  prepareTempDir: () => Promise<unknown>;
@@ -1394,6 +1500,7 @@ export interface TelegramSessionStartRuntimeDeps<TContext, TModel = unknown> {
1394
1500
  }
1395
1501
 
1396
1502
  export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
1503
+ isSessionActive?: () => boolean;
1397
1504
  unbindDeferredDispatchContext?: () => void;
1398
1505
  applyState: (state: TelegramSessionShutdownState<TQueueItem>) => void;
1399
1506
  clearPendingMediaGroups: () => void;
@@ -1404,6 +1511,7 @@ export interface TelegramSessionShutdownRuntimeDeps<TQueueItem> {
1404
1511
  chatId: number,
1405
1512
  options?: { target?: TelegramQueueTarget },
1406
1513
  ) => Promise<void>;
1514
+ previewShutdownTimeoutMs?: number;
1407
1515
  clearActiveTurn: () => void;
1408
1516
  clearAbort: () => void;
1409
1517
  stopPolling: () => Promise<void>;
@@ -1420,6 +1528,7 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
1420
1528
  bindDeferredDispatchContext?: (ctx: TContext) => void;
1421
1529
  prepareTempDir: () => Promise<unknown>;
1422
1530
  updateStatus: (ctx: TContext) => void;
1531
+ isSessionActive?: (ctx: TContext) => boolean;
1423
1532
  unbindDeferredDispatchContext?: () => void;
1424
1533
  applySessionShutdownState: (
1425
1534
  state: TelegramSessionShutdownState<TQueueItem>,
@@ -1432,6 +1541,7 @@ export interface TelegramSessionLifecycleHookRuntimeDeps<
1432
1541
  chatId: number,
1433
1542
  options?: { target?: TelegramQueueTarget },
1434
1543
  ) => Promise<void>;
1544
+ previewShutdownTimeoutMs?: number;
1435
1545
  clearActiveTurn: () => void;
1436
1546
  clearAbort: () => void;
1437
1547
  stopPolling: () => Promise<void>;
@@ -1586,8 +1696,10 @@ export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
1586
1696
  deps: TelegramSessionStartRuntimeDeps<TContext, TModel>,
1587
1697
  ): Promise<void> {
1588
1698
  await deps.loadConfig();
1699
+ if (deps.isSessionActive?.() === false) return;
1589
1700
  deps.applyState(buildTelegramSessionStartState(deps.currentModel));
1590
1701
  await deps.prepareTempDir();
1702
+ if (deps.isSessionActive?.() === false) return;
1591
1703
  try {
1592
1704
  deps.bindDeferredDispatchContext?.(deps.ctx);
1593
1705
  } catch (error) {
@@ -1599,15 +1711,28 @@ export async function startTelegramSessionRuntime<TContext, TModel = unknown>(
1599
1711
  export async function shutdownTelegramSessionRuntime<TQueueItem>(
1600
1712
  deps: TelegramSessionShutdownRuntimeDeps<TQueueItem>,
1601
1713
  ): Promise<void> {
1714
+ if (deps.isSessionActive?.() === false) return;
1602
1715
  deps.unbindDeferredDispatchContext?.();
1603
1716
  await deps.stopPolling();
1717
+ if (deps.isSessionActive?.() === false) return;
1604
1718
  deps.applyState(buildTelegramSessionShutdownState<TQueueItem>());
1605
1719
  deps.clearPendingMediaGroups();
1606
1720
  deps.clearModelMenuState();
1607
1721
  const activeTurnChatId = deps.getActiveTurnChatId();
1608
1722
  if (activeTurnChatId !== undefined) {
1609
1723
  const target = deps.getActiveTurnTarget?.();
1610
- await deps.clearPreview(activeTurnChatId, target ? { target } : undefined);
1724
+ const previewTimeoutMs = deps.previewShutdownTimeoutMs ?? 1000;
1725
+ let timeout: ReturnType<typeof setTimeout> | undefined;
1726
+ await Promise.race([
1727
+ deps.clearPreview(activeTurnChatId, target ? { target } : undefined),
1728
+ new Promise<void>((resolve) => {
1729
+ timeout = setTimeout(resolve, previewTimeoutMs);
1730
+ timeout.unref?.();
1731
+ }),
1732
+ ]).finally(() => {
1733
+ if (timeout) clearTimeout(timeout);
1734
+ });
1735
+ if (deps.isSessionActive?.() === false) return;
1611
1736
  }
1612
1737
  deps.clearActiveTurn();
1613
1738
  deps.clearAbort();
@@ -1642,6 +1767,7 @@ export function createTelegramSessionLifecycleRuntime<
1642
1767
  bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
1643
1768
  prepareTempDir: deps.prepareTempDir,
1644
1769
  updateStatus: deps.updateStatus,
1770
+ isSessionActive: deps.isSessionActive,
1645
1771
  unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
1646
1772
  applySessionShutdownState: stateApplier.applyShutdownState,
1647
1773
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
@@ -1671,6 +1797,7 @@ export function createTelegramSessionLifecycleHooks<
1671
1797
  ctx,
1672
1798
  currentModel: deps.getCurrentModel(ctx),
1673
1799
  loadConfig: deps.loadConfig,
1800
+ isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
1674
1801
  applyState: deps.applySessionStartState,
1675
1802
  bindDeferredDispatchContext: deps.bindDeferredDispatchContext,
1676
1803
  prepareTempDir: deps.prepareTempDir,
@@ -1681,9 +1808,14 @@ export function createTelegramSessionLifecycleHooks<
1681
1808
  throw error;
1682
1809
  }
1683
1810
  },
1684
- onSessionShutdown: async (): Promise<void> => {
1811
+ onSessionShutdown: async (
1812
+ _event?: TelegramSessionLifecycleHookEvent,
1813
+ ctx?: TContext,
1814
+ ): Promise<void> => {
1685
1815
  try {
1686
1816
  await shutdownTelegramSessionRuntime<TQueueItem>({
1817
+ isSessionActive: () =>
1818
+ ctx === undefined ? true : (deps.isSessionActive?.(ctx) ?? true),
1687
1819
  unbindDeferredDispatchContext: deps.unbindDeferredDispatchContext,
1688
1820
  applyState: deps.applySessionShutdownState,
1689
1821
  clearPendingMediaGroups: deps.clearPendingMediaGroups,
@@ -1691,6 +1823,7 @@ export function createTelegramSessionLifecycleHooks<
1691
1823
  getActiveTurnChatId: deps.getActiveTurnChatId,
1692
1824
  getActiveTurnTarget: deps.getActiveTurnTarget,
1693
1825
  clearPreview: deps.clearPreview,
1826
+ previewShutdownTimeoutMs: deps.previewShutdownTimeoutMs,
1694
1827
  clearActiveTurn: deps.clearActiveTurn,
1695
1828
  clearAbort: deps.clearAbort,
1696
1829
  stopPolling: deps.stopPolling,
@@ -2017,9 +2150,7 @@ export interface TelegramQueueDispatchWatchdogRuntimeDeps<
2017
2150
  clearInterval?: (timer: ReturnType<typeof setInterval>) => void;
2018
2151
  }
2019
2152
 
2020
- export function createTelegramQueueDispatchWatchdogRuntime<
2021
- TContext = unknown,
2022
- >(
2153
+ export function createTelegramQueueDispatchWatchdogRuntime<TContext = unknown>(
2023
2154
  deps: TelegramQueueDispatchWatchdogRuntimeDeps<TContext>,
2024
2155
  ): TelegramQueueDispatchWatchdogRuntime<TContext> {
2025
2156
  const intervalMs = deps.intervalMs ?? 1000;
@@ -2107,6 +2238,7 @@ export interface TelegramQueueDispatchControllerDeps<
2107
2238
  onPromptDispatchStart: (ctx: TContext, chatId: number) => void;
2108
2239
  sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
2109
2240
  onPromptDispatchFailure: (ctx: TContext, message: string) => void;
2241
+ isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
2110
2242
  }
2111
2243
 
2112
2244
  export interface TelegramQueueDispatchController<TContext = unknown> {
@@ -2159,6 +2291,7 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
2159
2291
  onPromptDispatchStart: deps.onPromptDispatchStart,
2160
2292
  sendUserMessage: deps.sendUserMessage,
2161
2293
  onPromptDispatchFailure: deps.onPromptDispatchFailure,
2294
+ isQueueItemTransportActive: deps.isQueueItemTransportActive,
2162
2295
  recordRuntimeEvent: deps.recordRuntimeEvent,
2163
2296
  });
2164
2297
  }
@@ -2174,8 +2307,22 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
2174
2307
  deps.updateStatus(ctx);
2175
2308
  return;
2176
2309
  }
2310
+ const queuedItems = deps.getQueuedItems();
2311
+ const activeItems = deps.isQueueItemTransportActive
2312
+ ? queuedItems.filter(deps.isQueueItemTransportActive)
2313
+ : queuedItems;
2314
+ if (activeItems.length !== queuedItems.length) {
2315
+ deps.setQueuedItems(activeItems);
2316
+ deps.recordRuntimeEvent?.(
2317
+ "dispatch",
2318
+ new Error(
2319
+ "Dropped queue work from an inactive Telegram transport generation.",
2320
+ ),
2321
+ { phase: "transport-generation" },
2322
+ );
2323
+ }
2177
2324
  const dispatchPlan = planNextTelegramQueueAction(
2178
- deps.getQueuedItems(),
2325
+ activeItems,
2179
2326
  deps.canDispatch(ctx),
2180
2327
  );
2181
2328
  if (dispatchPlan.kind !== "none") {
package/lib/routing.ts CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { readFile } from "node:fs/promises";
8
8
  import { basename, dirname } from "node:path";
9
+ import * as Bus from "./bus.ts";
9
10
  import * as Commands from "./commands.ts";
10
11
  import type { TelegramConfigStore } from "./config.ts";
11
12
  import type { TelegramSectionRegistry } from "./sections.ts";
@@ -18,6 +19,10 @@ import * as PromptTemplates from "./prompt-templates.ts";
18
19
  import * as Queue from "./queue.ts";
19
20
  import type { TelegramBridgeRuntime } from "./runtime.ts";
20
21
  import * as TextGroups from "./text-groups.ts";
22
+ import type {
23
+ TelegramInstanceThreadIdentityCandidate,
24
+ TelegramTopicTargetRecord,
25
+ } from "./threads.ts";
21
26
  import * as ThreadReconciler from "./thread-reconciler.ts";
22
27
  import * as Turns from "./turns.ts";
23
28
 
@@ -39,8 +44,7 @@ function formatTelegramPromptPeer(
39
44
  }
40
45
  const displayName = [peer.first_name, peer.last_name]
41
46
  .filter(
42
- (part): part is string =>
43
- typeof part === "string" && part.length > 0,
47
+ (part): part is string => typeof part === "string" && part.length > 0,
44
48
  )
45
49
  .join(" ");
46
50
  if (displayName) return displayName;
@@ -425,8 +429,7 @@ async function deleteReservedTelegramTopicThroughReconciler(
425
429
  >;
426
430
  getCurrentLeaderEpoch?: () => number | string | undefined;
427
431
  getThreadReconciliationMachineState?: () =>
428
- | ThreadReconciler.ThreadReconciliationMachineState
429
- | undefined;
432
+ ThreadReconciler.ThreadReconciliationMachineState | undefined;
430
433
  recordThreadReconciliationPlan?: (
431
434
  plan: ThreadReconciler.ThreadReconciliationPlan,
432
435
  ) => void;
@@ -487,6 +490,56 @@ export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
487
490
  export type TelegramRoutedCallbackQuery = Updates.TelegramCallbackQuery &
488
491
  Menu.MenuCallbackQuery;
489
492
 
493
+ export interface TelegramInboundBusProjectionRuntime {
494
+ getTargetOwnership: Updates.TelegramTargetOwnershipLookup;
495
+ getLiveThreadTargets(): Queue.TelegramQueueTarget[];
496
+ getLocalThreadLabelForTarget(
497
+ target: Queue.TelegramQueueTarget,
498
+ ): string | undefined;
499
+ }
500
+
501
+ export function createTelegramInboundBusProjectionRuntime(deps: {
502
+ instanceId: string;
503
+ listFollowers(): readonly Bus.TelegramBusFollowerView[];
504
+ listThreadRecords(): readonly TelegramTopicTargetRecord[];
505
+ getLeaderTarget(): Queue.TelegramQueueTarget | undefined;
506
+ isFollowerRegistered(): boolean;
507
+ getFollowerTarget(): Queue.TelegramQueueTarget | undefined;
508
+ getCurrentIdentity(
509
+ target?: Queue.TelegramQueueTarget,
510
+ ): TelegramInstanceThreadIdentityCandidate;
511
+ }): TelegramInboundBusProjectionRuntime {
512
+ return {
513
+ getTargetOwnership(target) {
514
+ return Bus.getTelegramFollowerTargetOwnership({
515
+ target,
516
+ followers: deps.listFollowers(),
517
+ activeThreadRecords: deps.listThreadRecords(),
518
+ currentInstanceId: deps.instanceId,
519
+ });
520
+ },
521
+ getLiveThreadTargets() {
522
+ return Bus.listTelegramBusLiveThreadTargets({
523
+ leaderTarget: deps.getLeaderTarget(),
524
+ followers: deps.listFollowers(),
525
+ });
526
+ },
527
+ getLocalThreadLabelForTarget(target) {
528
+ const followerTarget = deps.getFollowerTarget();
529
+ const leaderTarget = deps.getLeaderTarget();
530
+ const isLocalFollowerTarget =
531
+ deps.isFollowerRegistered() &&
532
+ followerTarget?.chatId === target.chatId &&
533
+ followerTarget.threadId === target.threadId;
534
+ const isLocalLeaderTarget =
535
+ leaderTarget?.chatId === target.chatId &&
536
+ leaderTarget.threadId === target.threadId;
537
+ if (!isLocalFollowerTarget && !isLocalLeaderTarget) return undefined;
538
+ return deps.getCurrentIdentity(target).threadName;
539
+ },
540
+ };
541
+ }
542
+
490
543
  export interface TelegramInboundRouteRuntimeDeps<
491
544
  TMessage extends TelegramRoutedMessage,
492
545
  TCallbackQuery extends TelegramRoutedCallbackQuery,
@@ -511,8 +564,7 @@ export interface TelegramInboundRouteRuntimeDeps<
511
564
  ) => string | undefined;
512
565
  getCurrentLeaderEpoch?: () => number | string | undefined;
513
566
  getThreadReconciliationMachineState?: () =>
514
- | ThreadReconciler.ThreadReconciliationMachineState
515
- | undefined;
567
+ ThreadReconciler.ThreadReconciliationMachineState | undefined;
516
568
  recordThreadReconciliationPlan?: (
517
569
  plan: ThreadReconciler.ThreadReconciliationPlan,
518
570
  ) => void;
@@ -1494,7 +1546,11 @@ export function createTelegramInboundRouteRuntime<
1494
1546
  for (const r of records) {
1495
1547
  if (r.target.chatId !== chatId || r.target.threadId !== threadId)
1496
1548
  continue;
1497
- if (currentInstanceId && r.instanceId && r.instanceId !== currentInstanceId)
1549
+ if (
1550
+ currentInstanceId &&
1551
+ r.instanceId &&
1552
+ r.instanceId !== currentInstanceId
1553
+ )
1498
1554
  continue;
1499
1555
  return r.threadName &&
1500
1556
  Threads.isTelegramTopicThreadNameValidForSlot(r.threadName, r.slot)
@@ -1889,11 +1945,9 @@ export function createTelegramInboundRouteRuntime<
1889
1945
  const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
1890
1946
  const replyFromRaw = replyMsg?.from as Record<string, unknown> | undefined;
1891
1947
  const guestBotCallerUser = gm.guest_bot_caller_user as
1892
- | Record<string, unknown>
1893
- | undefined;
1948
+ Record<string, unknown> | undefined;
1894
1949
  const guestBotCallerChat = gm.guest_bot_caller_chat as
1895
- | Record<string, unknown>
1896
- | undefined;
1950
+ Record<string, unknown> | undefined;
1897
1951
  const ownerUserId = deps.configStore.getAllowedUserId();
1898
1952
  const replyPeer = formatTelegramPromptPeer(replyFromRaw);
1899
1953
  const guestPeer = resolveTelegramGuestPromptPeer({
@@ -1947,7 +2001,9 @@ export function createTelegramInboundRouteRuntime<
1947
2001
  let sourceContext = "";
1948
2002
  if (replyMsg) {
1949
2003
  const replyHeader = replyPeer ? `[reply|from:${replyPeer}]` : "[reply]";
1950
- const replyBlock = replyText ? `${replyHeader} ${replyText}` : replyHeader;
2004
+ const replyBlock = replyText
2005
+ ? `${replyHeader} ${replyText}`
2006
+ : replyHeader;
1951
2007
  sourceContext = appendTelegramSourceAttachmentSection(
1952
2008
  replyBlock,
1953
2009
  replyPeer,