@llblab/pi-telegram 0.22.1 → 0.23.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/AGENTS.md +11 -7
- package/BACKLOG.md +0 -50
- package/CHANGELOG.md +49 -2
- package/README.md +3 -1
- package/docs/README.md +1 -1
- package/docs/activity.md +8 -0
- package/docs/architecture.md +16 -14
- package/docs/locks.md +21 -15
- package/docs/multi-instance-bus.md +17 -16
- package/docs/outbound.md +16 -0
- package/docs/public-api.md +9 -4
- package/index.ts +74 -59
- package/lib/activity.ts +100 -3
- package/lib/bindings.ts +81 -6
- package/lib/bus-follower.ts +205 -17
- package/lib/bus-leader.ts +339 -133
- package/lib/bus.ts +82 -19
- package/lib/commands.ts +28 -4
- package/lib/config.ts +40 -28
- package/lib/media.ts +102 -1
- package/lib/menu-settings.ts +4 -1
- package/lib/outbound-attachments.ts +150 -1
- package/lib/outbound.ts +106 -1
- package/lib/polling.ts +5 -0
- package/lib/queue.ts +41 -48
- package/lib/routing.ts +88 -1
- package/lib/sync.ts +40 -3
- package/lib/telegram-api.ts +77 -13
- package/lib/text-groups.ts +183 -16
- package/lib/thread-reconciler.ts +66 -22
- package/lib/threads.ts +131 -23
- package/lib/turns.ts +102 -13
- package/lib/updates.ts +2 -0
- package/package.json +1 -1
package/lib/outbound.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram outbound surface helpers
|
|
3
3
|
* Zones: telegram outbound, command templates, voice delivery
|
|
4
|
-
* Owns configured outbound handler execution, text transforms, voice-file generation/delivery, runtime-event bridge, and compatibility re-exports; assistant markup parsing lives in outbound-markup and button callback actions live in outbound-buttons
|
|
4
|
+
* Owns configured outbound handler execution, text transforms, public assistant-output reply composition and mutation fencing, voice-file generation/delivery, runtime-event bridge, and compatibility re-exports; assistant markup parsing lives in outbound-markup and button callback actions live in outbound-buttons
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
|
|
11
|
+
import type { TelegramAssistantSegmentEvent } from "./activity.ts";
|
|
11
12
|
import { resolveTelegramTempDir } from "./paths.ts";
|
|
13
|
+
import * as Replies from "./replies.ts";
|
|
14
|
+
import type {
|
|
15
|
+
TelegramEditMessageTextBody,
|
|
16
|
+
TelegramSendMessageBody,
|
|
17
|
+
TelegramSendRichMessageBody,
|
|
18
|
+
TelegramSentMessage,
|
|
19
|
+
} from "./telegram-api.ts";
|
|
12
20
|
|
|
13
21
|
import {
|
|
14
22
|
planTelegramButtonReply,
|
|
@@ -905,3 +913,100 @@ export function createTelegramOutboundReplyArtifactSender(
|
|
|
905
913
|
}
|
|
906
914
|
};
|
|
907
915
|
}
|
|
916
|
+
|
|
917
|
+
// --- Public Assistant Output Delivery ---
|
|
918
|
+
|
|
919
|
+
export interface TelegramAssistantOutputMutationFence {
|
|
920
|
+
run: <TArgs extends unknown[], TResult>(
|
|
921
|
+
mutation: (...args: TArgs) => Promise<TResult>,
|
|
922
|
+
...args: TArgs
|
|
923
|
+
) => Promise<TResult>;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
export interface TelegramAssistantOutputDeliveryAuthority<TTransportStamp> {
|
|
927
|
+
transportStamp: TTransportStamp;
|
|
928
|
+
route: "direct" | "follower" | "none";
|
|
929
|
+
directEpoch?: number | string;
|
|
930
|
+
followerGeneration?: string;
|
|
931
|
+
target?: TelegramTarget;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
export function createTelegramAssistantOutputMutationFence(
|
|
935
|
+
isAuthorityActive: () => boolean,
|
|
936
|
+
): TelegramAssistantOutputMutationFence {
|
|
937
|
+
return {
|
|
938
|
+
run(mutation, ...args) {
|
|
939
|
+
if (!isAuthorityActive()) {
|
|
940
|
+
return Promise.reject(
|
|
941
|
+
new Error(
|
|
942
|
+
"Assistant output lost admission authority before transport mutation.",
|
|
943
|
+
),
|
|
944
|
+
);
|
|
945
|
+
}
|
|
946
|
+
return mutation(...args);
|
|
947
|
+
},
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function createTelegramAssistantOutputSender<
|
|
952
|
+
TTransportStamp,
|
|
953
|
+
TReplyMarkup = unknown,
|
|
954
|
+
>(deps: {
|
|
955
|
+
recordOwnership?: Replies.TelegramReplyOwnershipRecorder["record"];
|
|
956
|
+
sendMessage: (
|
|
957
|
+
body: TelegramSendMessageBody,
|
|
958
|
+
) => Promise<TelegramSentMessage>;
|
|
959
|
+
sendRichMessage: (
|
|
960
|
+
body: TelegramSendRichMessageBody,
|
|
961
|
+
) => Promise<TelegramSentMessage>;
|
|
962
|
+
editMessage: (
|
|
963
|
+
body: TelegramEditMessageTextBody,
|
|
964
|
+
) => Promise<unknown>;
|
|
965
|
+
getAssistantRenderingMode: () => "rich" | "html";
|
|
966
|
+
execCommand: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["execCommand"];
|
|
967
|
+
getHandlers?: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["getHandlers"];
|
|
968
|
+
recordRuntimeEvent?: TelegramOutboundTextReplyRuntimeDeps<TReplyMarkup>["recordRuntimeEvent"];
|
|
969
|
+
}): (
|
|
970
|
+
event: TelegramAssistantSegmentEvent,
|
|
971
|
+
authority: TelegramAssistantOutputDeliveryAuthority<TTransportStamp>,
|
|
972
|
+
isAuthorityActive: () => boolean,
|
|
973
|
+
) => Promise<void> {
|
|
974
|
+
return async function sendAssistantOutput(
|
|
975
|
+
event,
|
|
976
|
+
authority,
|
|
977
|
+
isAuthorityActive,
|
|
978
|
+
) {
|
|
979
|
+
const target = authority.target;
|
|
980
|
+
if (!target) {
|
|
981
|
+
throw new Error("Assistant output has no authorized Telegram target.");
|
|
982
|
+
}
|
|
983
|
+
const mutationFence =
|
|
984
|
+
createTelegramAssistantOutputMutationFence(isAuthorityActive);
|
|
985
|
+
const replyRuntime = Replies.createTelegramRenderedMessageDeliveryRuntime({
|
|
986
|
+
recordOwnership: deps.recordOwnership,
|
|
987
|
+
sendMessage(body) {
|
|
988
|
+
return mutationFence.run(deps.sendMessage, body);
|
|
989
|
+
},
|
|
990
|
+
sendRichMessage(body) {
|
|
991
|
+
return mutationFence.run(deps.sendRichMessage, body);
|
|
992
|
+
},
|
|
993
|
+
getAssistantRenderingMode: deps.getAssistantRenderingMode,
|
|
994
|
+
editMessage(body) {
|
|
995
|
+
return mutationFence.run(deps.editMessage, body);
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
const outboundRuntime = createTelegramOutboundTextReplyRuntime({
|
|
999
|
+
sendTextReply: replyRuntime.sendTextReply,
|
|
1000
|
+
sendMarkdownReply: replyRuntime.sendMarkdownReply,
|
|
1001
|
+
execCommand: deps.execCommand,
|
|
1002
|
+
getHandlers: deps.getHandlers,
|
|
1003
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1004
|
+
});
|
|
1005
|
+
await outboundRuntime.sendMarkdownReply(
|
|
1006
|
+
target.chatId,
|
|
1007
|
+
undefined,
|
|
1008
|
+
event.text,
|
|
1009
|
+
{ target },
|
|
1010
|
+
);
|
|
1011
|
+
};
|
|
1012
|
+
}
|
package/lib/polling.ts
CHANGED
|
@@ -160,6 +160,7 @@ export function createTelegramPollingControllerRuntime<
|
|
|
160
160
|
getUpdates: deps.getUpdates,
|
|
161
161
|
persistConfig: deps.persistConfig,
|
|
162
162
|
handleUpdate: deps.handleUpdate,
|
|
163
|
+
prepareUpdateBatch: deps.prepareUpdateBatch,
|
|
163
164
|
updateStatus: deps.updateStatus,
|
|
164
165
|
sleep: deps.sleep,
|
|
165
166
|
maxUpdateFailures: deps.maxUpdateFailures,
|
|
@@ -909,6 +910,7 @@ export interface TelegramPollLoopDeps<
|
|
|
909
910
|
) => Promise<TUpdate[]>;
|
|
910
911
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
911
912
|
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
913
|
+
prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
|
|
912
914
|
onErrorStatus: (message: string) => void;
|
|
913
915
|
onStatusReset: () => void;
|
|
914
916
|
sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
@@ -927,6 +929,7 @@ export interface TelegramPollLoopRunnerDeps<
|
|
|
927
929
|
) => Promise<TUpdate[]>;
|
|
928
930
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
929
931
|
handleUpdate: (update: TUpdate, ctx: TContext) => Promise<void>;
|
|
932
|
+
prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
|
|
930
933
|
updateStatus: (ctx: TContext, message?: string) => void;
|
|
931
934
|
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
932
935
|
maxUpdateFailures?: number;
|
|
@@ -973,6 +976,7 @@ export function createTelegramPollLoopRunner<
|
|
|
973
976
|
getUpdates: deps.getUpdates,
|
|
974
977
|
persistConfig: deps.persistConfig,
|
|
975
978
|
handleUpdate: deps.handleUpdate,
|
|
979
|
+
prepareUpdateBatch: deps.prepareUpdateBatch,
|
|
976
980
|
onErrorStatus: (message) => {
|
|
977
981
|
updateTelegramPollingStatusSafely(deps.updateStatus, ctx, {
|
|
978
982
|
message,
|
|
@@ -1039,6 +1043,7 @@ export async function runTelegramPollLoop<
|
|
|
1039
1043
|
buildTelegramLongPollRequest(deps.config.lastUpdateId),
|
|
1040
1044
|
deps.signal,
|
|
1041
1045
|
);
|
|
1046
|
+
deps.prepareUpdateBatch?.(updates);
|
|
1042
1047
|
consecutiveGetUpdatesConflicts = 0;
|
|
1043
1048
|
for (const update of updates) {
|
|
1044
1049
|
if (admittedUpdates.has(update.update_id)) {
|
package/lib/queue.ts
CHANGED
|
@@ -954,6 +954,11 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
954
954
|
options?: { target?: TelegramQueueTarget },
|
|
955
955
|
) => Promise<unknown>;
|
|
956
956
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
957
|
+
sendRichAttachmentReply?: (
|
|
958
|
+
turn: TTurn,
|
|
959
|
+
markdown: string,
|
|
960
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
961
|
+
) => Promise<boolean>;
|
|
957
962
|
answerGuestQuery?: (
|
|
958
963
|
guestQueryId: string,
|
|
959
964
|
text?: string,
|
|
@@ -978,10 +983,6 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
978
983
|
plan: TelegramAgentEndOutboundReplyPlan,
|
|
979
984
|
options?: { replyToPrompt?: boolean },
|
|
980
985
|
) => Promise<void>;
|
|
981
|
-
getDefaultChatId?: () => number | undefined;
|
|
982
|
-
getDefaultTarget?: () => TelegramQueueTarget | undefined;
|
|
983
|
-
isProactivePushEnabled?: () => boolean;
|
|
984
|
-
canSendProactivePush?: () => boolean;
|
|
985
986
|
recordRuntimeEvent?: (
|
|
986
987
|
category: string,
|
|
987
988
|
error: unknown,
|
|
@@ -1029,6 +1030,10 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1029
1030
|
>["sendMarkdownReply"];
|
|
1030
1031
|
sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
|
|
1031
1032
|
sendQueuedAttachments: (turn: TTurn) => Promise<void>;
|
|
1033
|
+
sendRichAttachmentReply?: TelegramAgentEndRuntimeDeps<
|
|
1034
|
+
TTurn,
|
|
1035
|
+
TReplyMarkup
|
|
1036
|
+
>["sendRichAttachmentReply"];
|
|
1032
1037
|
answerGuestQuery?: TelegramAgentEndRuntimeDeps<TTurn>["answerGuestQuery"];
|
|
1033
1038
|
sendGuestReply?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestReply"];
|
|
1034
1039
|
sendGuestAttachment?: TelegramAgentEndRuntimeDeps<TTurn>["sendGuestAttachment"];
|
|
@@ -1038,10 +1043,6 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1038
1043
|
TReplyMarkup
|
|
1039
1044
|
>["planOutboundReply"];
|
|
1040
1045
|
sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
|
|
1041
|
-
getDefaultChatId?: TelegramAgentEndRuntimeDeps<TTurn>["getDefaultChatId"];
|
|
1042
|
-
getDefaultTarget?: TelegramAgentEndRuntimeDeps<TTurn>["getDefaultTarget"];
|
|
1043
|
-
isProactivePushEnabled?: TelegramAgentEndRuntimeDeps<TTurn>["isProactivePushEnabled"];
|
|
1044
|
-
canSendProactivePush?: (ctx: TContext) => boolean;
|
|
1045
1046
|
recordRuntimeEvent?: TelegramAgentEndRuntimeDeps<TTurn>["recordRuntimeEvent"];
|
|
1046
1047
|
}
|
|
1047
1048
|
|
|
@@ -1135,12 +1136,9 @@ export function createTelegramAgentEndHook<
|
|
|
1135
1136
|
await deps.loadConfig?.();
|
|
1136
1137
|
if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
|
|
1137
1138
|
const turn = deps.getActiveTurn();
|
|
1138
|
-
const proactiveEnabled = deps.isProactivePushEnabled?.() ?? false;
|
|
1139
|
-
const canProactivePush = deps.canSendProactivePush?.(ctx) ?? false;
|
|
1140
1139
|
await handleTelegramAgentEndRuntime({
|
|
1141
1140
|
turn,
|
|
1142
|
-
assistant:
|
|
1143
|
-
turn || proactiveEnabled ? deps.extractAssistant(event.messages) : {},
|
|
1141
|
+
assistant: turn ? deps.extractAssistant(event.messages) : {},
|
|
1144
1142
|
foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
|
|
1145
1143
|
resetRuntimeState: deps.resetRuntimeState,
|
|
1146
1144
|
isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
|
|
@@ -1165,16 +1163,13 @@ export function createTelegramAgentEndHook<
|
|
|
1165
1163
|
sendMarkdownReply: deps.sendMarkdownReply,
|
|
1166
1164
|
sendTextReply: deps.sendTextReply,
|
|
1167
1165
|
sendQueuedAttachments: deps.sendQueuedAttachments,
|
|
1166
|
+
sendRichAttachmentReply: deps.sendRichAttachmentReply,
|
|
1168
1167
|
answerGuestQuery: deps.answerGuestQuery,
|
|
1169
1168
|
sendGuestReply: deps.sendGuestReply,
|
|
1170
1169
|
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1171
1170
|
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1172
1171
|
planOutboundReply: deps.planOutboundReply,
|
|
1173
1172
|
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
1174
|
-
getDefaultChatId: deps.getDefaultChatId,
|
|
1175
|
-
getDefaultTarget: deps.getDefaultTarget,
|
|
1176
|
-
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
1177
|
-
canSendProactivePush: () => canProactivePush,
|
|
1178
1173
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1179
1174
|
});
|
|
1180
1175
|
};
|
|
@@ -1241,35 +1236,6 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1241
1236
|
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1242
1237
|
});
|
|
1243
1238
|
if (!turn) {
|
|
1244
|
-
const proactiveEnabled = deps.isProactivePushEnabled?.() ?? false;
|
|
1245
|
-
const canProactivePush = deps.canSendProactivePush?.() ?? false;
|
|
1246
|
-
if (proactiveEnabled && finalText && !assistant.errorMessage) {
|
|
1247
|
-
if (canProactivePush) {
|
|
1248
|
-
const defaultTarget = deps.getDefaultTarget?.();
|
|
1249
|
-
const defaultChatId =
|
|
1250
|
-
defaultTarget?.chatId ?? deps.getDefaultChatId?.();
|
|
1251
|
-
if (defaultChatId !== undefined) {
|
|
1252
|
-
try {
|
|
1253
|
-
await deps.sendMarkdownReply(defaultChatId, undefined, finalText, {
|
|
1254
|
-
target: defaultTarget,
|
|
1255
|
-
});
|
|
1256
|
-
} catch (error) {
|
|
1257
|
-
deps.recordRuntimeEvent?.("proactive-push", error, {
|
|
1258
|
-
chatId: defaultChatId,
|
|
1259
|
-
threadId: defaultTarget?.threadId,
|
|
1260
|
-
});
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
} else {
|
|
1264
|
-
deps.recordRuntimeEvent?.(
|
|
1265
|
-
"proactive-push",
|
|
1266
|
-
new Error(
|
|
1267
|
-
"Proactive push skipped because this instance does not own Telegram polling.",
|
|
1268
|
-
),
|
|
1269
|
-
{ phase: "ownership" },
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
1239
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1274
1240
|
return;
|
|
1275
1241
|
}
|
|
@@ -1346,7 +1312,34 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1346
1312
|
if (!finalText && hasOutboundArtifacts)
|
|
1347
1313
|
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1348
1314
|
if (!isDeliveryActive()) return;
|
|
1349
|
-
|
|
1315
|
+
let richAttachmentDelivered = false;
|
|
1316
|
+
if (
|
|
1317
|
+
endPlan.kind === "text" &&
|
|
1318
|
+
finalText &&
|
|
1319
|
+
!hasOutboundArtifacts &&
|
|
1320
|
+
deps.sendRichAttachmentReply
|
|
1321
|
+
) {
|
|
1322
|
+
try {
|
|
1323
|
+
richAttachmentDelivered = await deps.sendRichAttachmentReply(
|
|
1324
|
+
turn,
|
|
1325
|
+
finalText,
|
|
1326
|
+
{ replyMarkup },
|
|
1327
|
+
);
|
|
1328
|
+
if (!isDeliveryActive()) return;
|
|
1329
|
+
if (richAttachmentDelivered) {
|
|
1330
|
+
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1331
|
+
}
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1334
|
+
phase: "rich-attachment-commit-unknown",
|
|
1335
|
+
chatId: turn.chatId,
|
|
1336
|
+
});
|
|
1337
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
if (!isDeliveryActive()) return;
|
|
1342
|
+
if (!richAttachmentDelivered && endPlan.kind === "text" && finalText) {
|
|
1350
1343
|
try {
|
|
1351
1344
|
const finalized = await deps.finalizeMarkdownPreview(
|
|
1352
1345
|
turn.chatId,
|
|
@@ -1414,7 +1407,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1414
1407
|
}
|
|
1415
1408
|
}
|
|
1416
1409
|
if (!isDeliveryActive()) return;
|
|
1417
|
-
if (endPlan.shouldSendAttachmentNotice) {
|
|
1410
|
+
if (!richAttachmentDelivered && endPlan.shouldSendAttachmentNotice) {
|
|
1418
1411
|
await deps.sendTextReply(
|
|
1419
1412
|
turn.chatId,
|
|
1420
1413
|
turn.replyToMessageId,
|
|
@@ -1423,7 +1416,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1423
1416
|
);
|
|
1424
1417
|
}
|
|
1425
1418
|
if (!isDeliveryActive()) return;
|
|
1426
|
-
await deps.sendQueuedAttachments(turn);
|
|
1419
|
+
if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn);
|
|
1427
1420
|
if (!isDeliveryActive()) return;
|
|
1428
1421
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1429
1422
|
};
|
package/lib/routing.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram inbound routing composition
|
|
3
3
|
* Zones: telegram inbound, orchestration, queue/menu/command composition
|
|
4
|
-
* Wires authorized updates into menus, commands, media grouping, and prompt queueing
|
|
4
|
+
* Wires authorized updates into menus, commands, media grouping, and prompt queueing, and owns exact assistant-output target/route authority capture
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
@@ -2471,3 +2471,90 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2471
2471
|
},
|
|
2472
2472
|
});
|
|
2473
2473
|
}
|
|
2474
|
+
|
|
2475
|
+
// --- Assistant Output Delivery Authority ---
|
|
2476
|
+
|
|
2477
|
+
export interface TelegramAssistantOutputAuthority<TTransportStamp> {
|
|
2478
|
+
transportStamp: TTransportStamp;
|
|
2479
|
+
route: "direct" | "follower" | "none";
|
|
2480
|
+
directEpoch?: number | string;
|
|
2481
|
+
followerGeneration?: string;
|
|
2482
|
+
target?: Queue.TelegramQueueTarget;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
export interface TelegramAssistantOutputAuthorityRuntime<TTransportStamp> {
|
|
2486
|
+
captureAuthority: () => TelegramAssistantOutputAuthority<TTransportStamp>;
|
|
2487
|
+
isAuthorityActive: (
|
|
2488
|
+
authority: TelegramAssistantOutputAuthority<TTransportStamp>,
|
|
2489
|
+
) => boolean;
|
|
2490
|
+
canDeliver: () => boolean;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
export function createTelegramAssistantOutputAuthorityRuntime<TTransportStamp>(deps: {
|
|
2494
|
+
getPreferredTarget: () => Queue.TelegramQueueTarget | undefined;
|
|
2495
|
+
getFallbackChatId: () => number | undefined;
|
|
2496
|
+
getTransportStamp: () => TTransportStamp;
|
|
2497
|
+
isTransportStampActive: (stamp: TTransportStamp) => boolean;
|
|
2498
|
+
ownsDirect: () => boolean;
|
|
2499
|
+
getDirectEpoch: () => number | string | undefined;
|
|
2500
|
+
isFollowerRegistered: () => boolean;
|
|
2501
|
+
getFollowerGeneration: () => string | undefined;
|
|
2502
|
+
}): TelegramAssistantOutputAuthorityRuntime<TTransportStamp> {
|
|
2503
|
+
const getCurrentTarget = (): Queue.TelegramQueueTarget | undefined => {
|
|
2504
|
+
const preferred = deps.getPreferredTarget();
|
|
2505
|
+
if (preferred) return { ...preferred };
|
|
2506
|
+
const chatId = deps.getFallbackChatId();
|
|
2507
|
+
return chatId === undefined ? undefined : { chatId };
|
|
2508
|
+
};
|
|
2509
|
+
return {
|
|
2510
|
+
captureAuthority() {
|
|
2511
|
+
const target = getCurrentTarget();
|
|
2512
|
+
const directEpoch = deps.ownsDirect()
|
|
2513
|
+
? deps.getDirectEpoch()
|
|
2514
|
+
: undefined;
|
|
2515
|
+
const followerGeneration = deps.isFollowerRegistered()
|
|
2516
|
+
? deps.getFollowerGeneration()
|
|
2517
|
+
: undefined;
|
|
2518
|
+
return {
|
|
2519
|
+
transportStamp: deps.getTransportStamp(),
|
|
2520
|
+
route:
|
|
2521
|
+
directEpoch !== undefined
|
|
2522
|
+
? "direct"
|
|
2523
|
+
: followerGeneration !== undefined
|
|
2524
|
+
? "follower"
|
|
2525
|
+
: "none",
|
|
2526
|
+
directEpoch,
|
|
2527
|
+
followerGeneration,
|
|
2528
|
+
target,
|
|
2529
|
+
};
|
|
2530
|
+
},
|
|
2531
|
+
isAuthorityActive(authority) {
|
|
2532
|
+
if (!deps.isTransportStampActive(authority.transportStamp)) return false;
|
|
2533
|
+
const target = getCurrentTarget();
|
|
2534
|
+
if (
|
|
2535
|
+
authority.target === undefined ||
|
|
2536
|
+
target?.chatId !== authority.target.chatId ||
|
|
2537
|
+
target?.threadId !== authority.target.threadId
|
|
2538
|
+
) {
|
|
2539
|
+
return false;
|
|
2540
|
+
}
|
|
2541
|
+
if (authority.route === "direct") {
|
|
2542
|
+
return (
|
|
2543
|
+
deps.ownsDirect() &&
|
|
2544
|
+
deps.getDirectEpoch() === authority.directEpoch
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
if (authority.route === "follower") {
|
|
2548
|
+
return (
|
|
2549
|
+
!deps.ownsDirect() &&
|
|
2550
|
+
deps.isFollowerRegistered() &&
|
|
2551
|
+
deps.getFollowerGeneration() === authority.followerGeneration
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
return false;
|
|
2555
|
+
},
|
|
2556
|
+
canDeliver() {
|
|
2557
|
+
return deps.ownsDirect() || deps.isFollowerRegistered();
|
|
2558
|
+
},
|
|
2559
|
+
};
|
|
2560
|
+
}
|
package/lib/sync.ts
CHANGED
|
@@ -123,6 +123,7 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
123
123
|
getLeaderTarget: () => TelegramTarget | undefined;
|
|
124
124
|
getCurrentLeaderEpoch?: () => number | string | undefined;
|
|
125
125
|
clearLeaderTarget: () => void;
|
|
126
|
+
disconnectFollowerThread?: () => Promise<boolean>;
|
|
126
127
|
getSyncState: () => TSyncState;
|
|
127
128
|
setSyncState: (state: TSyncState) => void;
|
|
128
129
|
stopPolling: () => Promise<string>;
|
|
@@ -160,13 +161,25 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
160
161
|
const currentRecord = deps.getCurrentThreadRecord();
|
|
161
162
|
if (currentRecord?.target.threadId) {
|
|
162
163
|
const isManualFollower = currentRecord.owner?.kind === "manual-follower";
|
|
163
|
-
|
|
164
|
-
|
|
164
|
+
const leaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
165
|
+
const ownsLeader = deps.getCurrentLeaderEpoch
|
|
166
|
+
? leaderEpoch !== undefined
|
|
167
|
+
: !isManualFollower;
|
|
168
|
+
if (isManualFollower && !ownsLeader) {
|
|
169
|
+
if (deps.disconnectFollowerThread) {
|
|
170
|
+
const disconnected = await deps.disconnectFollowerThread();
|
|
171
|
+
if (!disconnected) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
"Telegram follower thread deletion requires a live leader registration.",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
165
178
|
const stillOwnsLeaderEpoch = () =>
|
|
166
179
|
!deps.getCurrentLeaderEpoch ||
|
|
167
180
|
(leaderEpoch !== undefined &&
|
|
168
181
|
deps.getCurrentLeaderEpoch() === leaderEpoch);
|
|
169
|
-
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
182
|
+
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
170
183
|
ThreadReconciler.planDisconnectedInstanceThreadCleanup({
|
|
171
184
|
target: currentRecord.target as TelegramTarget & {
|
|
172
185
|
threadId: number;
|
|
@@ -185,6 +198,11 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
185
198
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
186
199
|
},
|
|
187
200
|
);
|
|
201
|
+
if (cleanup.incompleteActions?.length) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Telegram thread deletion was not confirmed; inspect /telegram-status --debug and retry /telegram-disconnect.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
188
206
|
if (!stillOwnsLeaderEpoch()) return deps.stopPolling();
|
|
189
207
|
const offlineChanged =
|
|
190
208
|
deps.topicTargetStore.markOfflineByInstanceId(deps.instanceId) > 0;
|
|
@@ -278,6 +296,15 @@ export interface TelegramStaleTopicApiErrorRecoveryDeps<TSyncState> {
|
|
|
278
296
|
getNowMs?: () => number;
|
|
279
297
|
}
|
|
280
298
|
|
|
299
|
+
export function createTelegramStaleTopicApiErrorRecoveryRuntime<
|
|
300
|
+
TSyncState extends TelegramSyncState,
|
|
301
|
+
>(
|
|
302
|
+
deps: TelegramStaleTopicApiErrorRecoveryDeps<TSyncState>,
|
|
303
|
+
): (apiBody: unknown, error: unknown) => Promise<boolean> {
|
|
304
|
+
return (apiBody, error) =>
|
|
305
|
+
recoverStaleTelegramTopicApiError(apiBody, error, deps);
|
|
306
|
+
}
|
|
307
|
+
|
|
281
308
|
export async function recoverStaleTelegramTopicApiError<
|
|
282
309
|
TSyncState extends TelegramSyncState,
|
|
283
310
|
>(
|
|
@@ -493,6 +520,16 @@ export interface TelegramSyncStateRuntime {
|
|
|
493
520
|
): void;
|
|
494
521
|
}
|
|
495
522
|
|
|
523
|
+
export function createTelegramConfigSyncPersister<TConfig>(deps: {
|
|
524
|
+
persist: (config?: TConfig) => Promise<void>;
|
|
525
|
+
markConfigChange: (action: string) => void;
|
|
526
|
+
}): (config?: TConfig) => Promise<void> {
|
|
527
|
+
return async (config) => {
|
|
528
|
+
await deps.persist(config);
|
|
529
|
+
deps.markConfigChange("config-persist");
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
496
533
|
export function createTelegramSyncStateRuntime(
|
|
497
534
|
initialState = createUnknownTelegramSyncState(),
|
|
498
535
|
): TelegramSyncStateRuntime {
|
package/lib/telegram-api.ts
CHANGED
|
@@ -212,19 +212,83 @@ export type TelegramSendMessageBody = Record<string, unknown> & {
|
|
|
212
212
|
reply_parameters?: TelegramReplyParameters;
|
|
213
213
|
};
|
|
214
214
|
|
|
215
|
-
export
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
215
|
+
export interface TelegramInputMediaPhoto extends Record<string, unknown> {
|
|
216
|
+
type: "photo";
|
|
217
|
+
media: string;
|
|
218
|
+
has_spoiler?: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface TelegramInputMediaVideo extends Record<string, unknown> {
|
|
222
|
+
type: "video";
|
|
223
|
+
media: string;
|
|
224
|
+
thumbnail?: string;
|
|
225
|
+
width?: number;
|
|
226
|
+
height?: number;
|
|
227
|
+
duration?: number;
|
|
228
|
+
supports_streaming?: boolean;
|
|
229
|
+
has_spoiler?: boolean;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface TelegramInputMediaAnimation extends Record<string, unknown> {
|
|
233
|
+
type: "animation";
|
|
234
|
+
media: string;
|
|
235
|
+
thumbnail?: string;
|
|
236
|
+
width?: number;
|
|
237
|
+
height?: number;
|
|
238
|
+
duration?: number;
|
|
239
|
+
has_spoiler?: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface TelegramInputMediaAudio extends Record<string, unknown> {
|
|
243
|
+
type: "audio";
|
|
244
|
+
media: string;
|
|
245
|
+
thumbnail?: string;
|
|
246
|
+
duration?: number;
|
|
247
|
+
performer?: string;
|
|
248
|
+
title?: string;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface TelegramInputMediaVoiceNote extends Record<string, unknown> {
|
|
252
|
+
type: "voice_note";
|
|
253
|
+
media: string;
|
|
254
|
+
caption?: string;
|
|
255
|
+
parse_mode?: string;
|
|
256
|
+
caption_entities?: unknown[];
|
|
257
|
+
duration?: number;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type TelegramInputRichMessageMediaValue =
|
|
261
|
+
| TelegramInputMediaAnimation
|
|
262
|
+
| TelegramInputMediaAudio
|
|
263
|
+
| TelegramInputMediaPhoto
|
|
264
|
+
| TelegramInputMediaVideo
|
|
265
|
+
| TelegramInputMediaVoiceNote;
|
|
266
|
+
|
|
267
|
+
export interface TelegramInputRichMessageMedia {
|
|
268
|
+
id: string;
|
|
269
|
+
media: TelegramInputRichMessageMediaValue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
type TelegramInputRichMessageCommon = {
|
|
273
|
+
is_rtl?: boolean;
|
|
274
|
+
skip_entity_detection?: boolean;
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
export type TelegramInputRichMessage = TelegramInputRichMessageCommon &
|
|
278
|
+
(
|
|
279
|
+
| {
|
|
280
|
+
markdown: string;
|
|
281
|
+
html?: never;
|
|
282
|
+
blocks?: never;
|
|
283
|
+
media?: TelegramInputRichMessageMedia[];
|
|
284
|
+
}
|
|
285
|
+
| {
|
|
286
|
+
html: string;
|
|
287
|
+
markdown?: never;
|
|
288
|
+
blocks?: never;
|
|
289
|
+
media?: TelegramInputRichMessageMedia[];
|
|
290
|
+
}
|
|
291
|
+
);
|
|
228
292
|
|
|
229
293
|
export type TelegramSendRichMessageBody = Record<string, unknown> & {
|
|
230
294
|
chat_id: number;
|