@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/AGENTS.md +7 -4
- package/BACKLOG.md +29 -1
- package/CHANGELOG.md +479 -452
- package/docs/architecture.md +12 -10
- package/docs/delivery.md +2 -1
- package/docs/locks.md +13 -5
- package/docs/multi-instance-bus.md +5 -5
- package/index.ts +311 -633
- package/lib/bindings.ts +59 -21
- package/lib/bus-api.ts +12 -2
- package/lib/bus-follower.ts +327 -83
- package/lib/bus-leader.ts +201 -121
- package/lib/bus.ts +345 -37
- package/lib/config.ts +168 -28
- package/lib/delivery.ts +148 -61
- package/lib/lifecycle.ts +199 -8
- package/lib/locks.ts +854 -57
- package/lib/logs.ts +185 -22
- package/lib/media.ts +79 -24
- package/lib/model.ts +5 -10
- package/lib/ownership.ts +150 -6
- package/lib/polling.ts +156 -7
- package/lib/preview.ts +15 -3
- package/lib/queue.ts +167 -20
- package/lib/routing.ts +68 -12
- package/lib/sync.ts +105 -32
- package/lib/telegram-api.ts +86 -10
- package/lib/text-groups.ts +71 -15
- package/lib/thread-reconciler.ts +49 -36
- package/lib/threads.ts +505 -118
- package/package.json +1 -1
package/lib/queue.ts
CHANGED
|
@@ -25,15 +25,12 @@ export interface TelegramPromptImageContent {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export type TelegramPromptContent =
|
|
28
|
-
|
|
|
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
|
-
| "
|
|
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
|
-
|
|
|
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(
|
|
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) ===
|
|
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) =>
|
|
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 =
|
|
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 ||
|
|
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
|
-
? {
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
1893
|
-
| undefined;
|
|
1948
|
+
Record<string, unknown> | undefined;
|
|
1894
1949
|
const guestBotCallerChat = gm.guest_bot_caller_chat as
|
|
1895
|
-
|
|
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
|
|
2004
|
+
const replyBlock = replyText
|
|
2005
|
+
? `${replyHeader} ${replyText}`
|
|
2006
|
+
: replyHeader;
|
|
1951
2007
|
sourceContext = appendTelegramSourceAttachmentSection(
|
|
1952
2008
|
replyBlock,
|
|
1953
2009
|
replyPeer,
|