@llblab/pi-telegram 0.24.2 → 0.24.4
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 +6 -6
- package/BACKLOG.md +12 -0
- package/CHANGELOG.md +12 -0
- package/README.md +8 -4
- package/docs/architecture.md +6 -6
- package/docs/multi-instance-bus.md +5 -5
- package/docs/public-api.md +7 -3
- package/index.ts +40 -4
- package/lib/bindings.ts +21 -10
- package/lib/bus-follower.ts +5 -1
- package/lib/config.ts +51 -0
- package/lib/locks.ts +7 -0
- package/lib/menu-settings.ts +94 -6
- package/lib/outbound-attachments.ts +10 -15
- package/lib/pi.ts +6 -0
- package/lib/prompts.ts +126 -8
- package/lib/queue.ts +27 -3
- package/lib/routing.ts +301 -256
- package/lib/runtime.ts +6 -1
- package/lib/telegram-api.ts +100 -1
- package/package.json +10 -7
- package/scripts/audit-dependencies.ts +78 -0
- package/scripts/dependency-audit-policy.ts +300 -0
package/lib/routing.ts
CHANGED
|
@@ -164,34 +164,10 @@ function escapeHtml(text: string): string {
|
|
|
164
164
|
.replace(/>/g, ">");
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
interface TelegramAllTabPendingCommand {
|
|
168
|
-
command: Commands.ParsedTelegramCommand;
|
|
169
|
-
text: string;
|
|
170
|
-
createdAtMs: number;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const TELEGRAM_ALL_TAB_MENU_CALLBACK_PREFIX = "allmenu:";
|
|
174
167
|
const TELEGRAM_UNBOUND_REROUTE_CALLBACK_PREFIX = "reroute:";
|
|
175
168
|
const TELEGRAM_UNBOUND_REROUTE_RESTORE_MENU_CALLBACK_PREFIX = "rerouterestore:";
|
|
176
169
|
const TELEGRAM_UNBOUND_REROUTE_NEW_SLOT_CALLBACK_PREFIX = "reroutenew:";
|
|
177
170
|
|
|
178
|
-
function formatTelegramAllTabMenuCallbackData(
|
|
179
|
-
commandId: string,
|
|
180
|
-
threadId: number,
|
|
181
|
-
): string {
|
|
182
|
-
return `${TELEGRAM_ALL_TAB_MENU_CALLBACK_PREFIX}${commandId}:${threadId}`;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function parseTelegramAllTabMenuCallbackData(
|
|
186
|
-
data: string | undefined,
|
|
187
|
-
): { commandId: string; threadId: number } | undefined {
|
|
188
|
-
const match = data?.match(/^allmenu:([a-z0-9]+):(\d+)$/);
|
|
189
|
-
const commandId = match?.[1];
|
|
190
|
-
const threadId = Number(match?.[2]);
|
|
191
|
-
if (!commandId || !Number.isSafeInteger(threadId)) return undefined;
|
|
192
|
-
return { commandId, threadId };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
171
|
function formatTelegramUnboundRerouteCallbackData(
|
|
196
172
|
rerouteId: string,
|
|
197
173
|
threadId: number,
|
|
@@ -291,22 +267,6 @@ function getTelegramRoutableThreadRecords(
|
|
|
291
267
|
);
|
|
292
268
|
}
|
|
293
269
|
|
|
294
|
-
function buildTelegramAllTabMenuChooserMarkup(
|
|
295
|
-
commandId: string,
|
|
296
|
-
records: readonly Threads.TelegramTopicTargetRecord[],
|
|
297
|
-
): Menu.TelegramReplyMarkup {
|
|
298
|
-
const rows = records.map((record) => [
|
|
299
|
-
{
|
|
300
|
-
text: getTelegramRouteThreadButtonLabel(record),
|
|
301
|
-
callback_data: formatTelegramAllTabMenuCallbackData(
|
|
302
|
-
commandId,
|
|
303
|
-
record.target.threadId,
|
|
304
|
-
),
|
|
305
|
-
},
|
|
306
|
-
]);
|
|
307
|
-
return { inline_keyboard: rows };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
270
|
function formatTelegramAllTabMenuChooserText(command: string): string {
|
|
311
271
|
return [
|
|
312
272
|
"<b>🧵 Choose target thread</b>",
|
|
@@ -695,7 +655,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
695
655
|
}
|
|
696
656
|
|
|
697
657
|
const TELEGRAM_OWNED_CALLBACK_PREFIXES = [
|
|
698
|
-
|
|
658
|
+
"allmenu:",
|
|
699
659
|
TELEGRAM_UNBOUND_REROUTE_CALLBACK_PREFIX,
|
|
700
660
|
"compact:",
|
|
701
661
|
"menu:",
|
|
@@ -732,14 +692,36 @@ export function createTelegramInboundRouteRuntime<
|
|
|
732
692
|
TModel
|
|
733
693
|
>,
|
|
734
694
|
): Updates.TelegramUpdateRuntimeController<TContext, TUpdate> {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
695
|
+
type PendingRerouteCleanup =
|
|
696
|
+
| {
|
|
697
|
+
kind: "unbound";
|
|
698
|
+
target: { chatId: number; threadId: number };
|
|
699
|
+
messageId?: number;
|
|
700
|
+
}
|
|
701
|
+
| {
|
|
702
|
+
kind: "previous-leader";
|
|
703
|
+
target: { chatId: number; threadId: number };
|
|
704
|
+
}
|
|
705
|
+
| {
|
|
706
|
+
kind: "replaced-follower";
|
|
707
|
+
target: { chatId: number; threadId: number };
|
|
708
|
+
instanceId?: string;
|
|
709
|
+
};
|
|
710
|
+
type PendingUnboundReroute = {
|
|
711
|
+
messages: TMessage[];
|
|
712
|
+
createdAtMs: number;
|
|
713
|
+
dispatchKind: "prompt" | "command";
|
|
714
|
+
cleanup?: PendingRerouteCleanup;
|
|
715
|
+
foreignRetry?: {
|
|
716
|
+
instanceId: string;
|
|
717
|
+
threadId: number;
|
|
718
|
+
cleanup: PendingRerouteCleanup;
|
|
719
|
+
};
|
|
720
|
+
finalizeMessage?: string;
|
|
721
|
+
};
|
|
722
|
+
const pendingUnboundReroutes = new Map<string, PendingUnboundReroute>();
|
|
740
723
|
const guidedUnboundTopicKeys = new Set<string>();
|
|
741
724
|
let nextUnboundRerouteId = 0;
|
|
742
|
-
let nextAllTabCommandId = 0;
|
|
743
725
|
const requestDispatchNextQueuedTelegramTurn = (ctx: TContext): void => {
|
|
744
726
|
deps.dispatchNextQueuedTelegramTurn(ctx);
|
|
745
727
|
if (
|
|
@@ -764,11 +746,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
764
746
|
pendingUnboundReroutes.delete(oldest);
|
|
765
747
|
}
|
|
766
748
|
};
|
|
767
|
-
const storePendingUnboundReroute = (
|
|
749
|
+
const storePendingUnboundReroute = (
|
|
750
|
+
messages: TMessage[],
|
|
751
|
+
dispatchKind: "prompt" | "command" = "prompt",
|
|
752
|
+
): string => {
|
|
768
753
|
prunePendingUnboundReroutes();
|
|
769
754
|
nextUnboundRerouteId += 1;
|
|
770
755
|
const id = nextUnboundRerouteId.toString(36);
|
|
771
|
-
pendingUnboundReroutes.set(id, {
|
|
756
|
+
pendingUnboundReroutes.set(id, {
|
|
757
|
+
messages,
|
|
758
|
+
createdAtMs: Date.now(),
|
|
759
|
+
dispatchKind,
|
|
760
|
+
});
|
|
772
761
|
return id;
|
|
773
762
|
};
|
|
774
763
|
const pendingUnboundRerouteMediaGroups = new Map<
|
|
@@ -778,39 +767,6 @@ export function createTelegramInboundRouteRuntime<
|
|
|
778
767
|
timer: ReturnType<typeof setTimeout>;
|
|
779
768
|
}
|
|
780
769
|
>();
|
|
781
|
-
const prunePendingAllTabCommands = () => {
|
|
782
|
-
const nowMs = Date.now();
|
|
783
|
-
for (const [id, entry] of pendingAllTabCommands) {
|
|
784
|
-
if (nowMs - entry.createdAtMs > 30 * 60_000) {
|
|
785
|
-
pendingAllTabCommands.delete(id);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
while (pendingAllTabCommands.size > 100) {
|
|
789
|
-
const oldest = pendingAllTabCommands.keys().next().value;
|
|
790
|
-
if (!oldest) break;
|
|
791
|
-
pendingAllTabCommands.delete(oldest);
|
|
792
|
-
}
|
|
793
|
-
};
|
|
794
|
-
const storePendingAllTabCommand = (
|
|
795
|
-
command: Commands.ParsedTelegramCommand,
|
|
796
|
-
text: string,
|
|
797
|
-
): string => {
|
|
798
|
-
prunePendingAllTabCommands();
|
|
799
|
-
const bareCommandText = `/${command.name}`;
|
|
800
|
-
const id = text.trim() === bareCommandText ? command.name : undefined;
|
|
801
|
-
if (id) {
|
|
802
|
-
pendingAllTabCommands.set(id, { command, text, createdAtMs: Date.now() });
|
|
803
|
-
return id;
|
|
804
|
-
}
|
|
805
|
-
nextAllTabCommandId += 1;
|
|
806
|
-
const generatedId = nextAllTabCommandId.toString(36);
|
|
807
|
-
pendingAllTabCommands.set(generatedId, {
|
|
808
|
-
command,
|
|
809
|
-
text,
|
|
810
|
-
createdAtMs: Date.now(),
|
|
811
|
-
});
|
|
812
|
-
return generatedId;
|
|
813
|
-
};
|
|
814
770
|
const menuCallbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
|
|
815
771
|
TCallbackQuery,
|
|
816
772
|
TContext,
|
|
@@ -889,9 +845,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
889
845
|
};
|
|
890
846
|
const applyThreadCleanupPlan = async (
|
|
891
847
|
plan: ThreadReconciler.ThreadReconciliationPlan,
|
|
892
|
-
): Promise<
|
|
848
|
+
): Promise<boolean> => {
|
|
893
849
|
deps.recordThreadReconciliationPlan?.(plan);
|
|
894
|
-
await ThreadReconciler.applyThreadReconciliationPlan(plan, {
|
|
850
|
+
const result = await ThreadReconciler.applyThreadReconciliationPlan(plan, {
|
|
895
851
|
callApi: deps.callApi,
|
|
896
852
|
markStaleByTarget: (staleTarget, syncStatus, lastSyncError) =>
|
|
897
853
|
deps.threadStore?.markStaleByTarget(
|
|
@@ -905,10 +861,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
905
861
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
906
862
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
907
863
|
});
|
|
864
|
+
return (result.incompleteActions?.length ?? 0) === 0;
|
|
908
865
|
};
|
|
909
866
|
const dismissRerouteChooserMessage = async (
|
|
910
867
|
query: TCallbackQuery,
|
|
911
|
-
): Promise<
|
|
868
|
+
): Promise<boolean> => {
|
|
912
869
|
const chatId = query.message?.chat?.id;
|
|
913
870
|
const messageId = query.message?.message_id;
|
|
914
871
|
if (
|
|
@@ -916,10 +873,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
916
873
|
typeof messageId !== "number" ||
|
|
917
874
|
!deps.deleteMessage
|
|
918
875
|
) {
|
|
919
|
-
return;
|
|
876
|
+
return false;
|
|
920
877
|
}
|
|
921
878
|
try {
|
|
922
879
|
await deps.deleteMessage(chatId, messageId);
|
|
880
|
+
return true;
|
|
923
881
|
} catch (error) {
|
|
924
882
|
deps.recordRuntimeEvent?.("telegram", error, {
|
|
925
883
|
phase: "reroute-chooser-delete",
|
|
@@ -927,13 +885,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
927
885
|
messageId,
|
|
928
886
|
threadId: query.message?.message_thread_id,
|
|
929
887
|
});
|
|
888
|
+
return false;
|
|
930
889
|
}
|
|
931
890
|
};
|
|
932
891
|
const closeReroutedUnboundTopic = async (
|
|
933
892
|
target: { chatId: number; threadId: number } | undefined,
|
|
934
893
|
messageId: number | undefined,
|
|
935
|
-
): Promise<
|
|
936
|
-
if (!target || !deps.threadStore) return;
|
|
894
|
+
): Promise<boolean> => {
|
|
895
|
+
if (!target || !deps.threadStore) return true;
|
|
937
896
|
const nowMs = Date.now();
|
|
938
897
|
const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
939
898
|
const plan = ThreadReconciler.planThreadReconciliation({
|
|
@@ -954,14 +913,14 @@ export function createTelegramInboundRouteRuntime<
|
|
|
954
913
|
},
|
|
955
914
|
],
|
|
956
915
|
});
|
|
957
|
-
|
|
916
|
+
return applyThreadCleanupPlan(plan);
|
|
958
917
|
};
|
|
959
918
|
const closePreviousLeaderThread = async (
|
|
960
919
|
target: { chatId: number; threadId: number } | undefined,
|
|
961
|
-
): Promise<
|
|
962
|
-
if (!target || !deps.threadStore) return;
|
|
920
|
+
): Promise<boolean> => {
|
|
921
|
+
if (!target || !deps.threadStore) return true;
|
|
963
922
|
const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
964
|
-
|
|
923
|
+
return applyThreadCleanupPlan({
|
|
965
924
|
actions: [
|
|
966
925
|
{
|
|
967
926
|
kind: "close-delete-previous-leader-topic",
|
|
@@ -978,10 +937,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
978
937
|
const closeReplacedFollowerThread = async (
|
|
979
938
|
target: { chatId: number; threadId: number } | undefined,
|
|
980
939
|
instanceId: string | undefined,
|
|
981
|
-
): Promise<
|
|
982
|
-
if (!target || !deps.threadStore) return;
|
|
940
|
+
): Promise<boolean> => {
|
|
941
|
+
if (!target || !deps.threadStore) return true;
|
|
983
942
|
const currentLeaderEpoch = deps.getCurrentLeaderEpoch?.();
|
|
984
|
-
|
|
943
|
+
return applyThreadCleanupPlan({
|
|
985
944
|
actions: [
|
|
986
945
|
{
|
|
987
946
|
kind: "close-delete-replaced-follower-topic",
|
|
@@ -995,6 +954,82 @@ export function createTelegramInboundRouteRuntime<
|
|
|
995
954
|
],
|
|
996
955
|
});
|
|
997
956
|
};
|
|
957
|
+
const retryPendingRerouteCleanup = async (
|
|
958
|
+
cleanup: PendingRerouteCleanup,
|
|
959
|
+
): Promise<boolean> => {
|
|
960
|
+
if (cleanup.kind === "unbound") {
|
|
961
|
+
return closeReroutedUnboundTopic(cleanup.target, cleanup.messageId);
|
|
962
|
+
}
|
|
963
|
+
if (cleanup.kind === "previous-leader") {
|
|
964
|
+
return closePreviousLeaderThread(cleanup.target);
|
|
965
|
+
}
|
|
966
|
+
return closeReplacedFollowerThread(cleanup.target, cleanup.instanceId);
|
|
967
|
+
};
|
|
968
|
+
let dispatchReroutedCommandMessages:
|
|
969
|
+
| ((messages: TMessage[], ctx: TContext) => Promise<void>)
|
|
970
|
+
| undefined;
|
|
971
|
+
const dispatchPendingRerouteMessages = async (
|
|
972
|
+
pending: { dispatchKind: "prompt" | "command" },
|
|
973
|
+
messages: TMessage[],
|
|
974
|
+
ctx: TContext,
|
|
975
|
+
): Promise<void> => {
|
|
976
|
+
if (pending.dispatchKind === "command" && dispatchReroutedCommandMessages) {
|
|
977
|
+
await dispatchReroutedCommandMessages(messages, ctx);
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
await promptEnqueue(messages, ctx);
|
|
981
|
+
};
|
|
982
|
+
const finalizePendingReroute = async (
|
|
983
|
+
rerouteId: string,
|
|
984
|
+
pending: PendingUnboundReroute,
|
|
985
|
+
query: TCallbackQuery,
|
|
986
|
+
successMessage: string,
|
|
987
|
+
): Promise<void> => {
|
|
988
|
+
const dismissed = await dismissRerouteChooserMessage(query);
|
|
989
|
+
if (dismissed) {
|
|
990
|
+
pendingUnboundReroutes.delete(rerouteId);
|
|
991
|
+
await deps.answerCallbackQuery(query.id, successMessage);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
pending.finalizeMessage = successMessage;
|
|
995
|
+
await deps.answerCallbackQuery(
|
|
996
|
+
query.id,
|
|
997
|
+
`${successMessage} Chooser cleanup is still pending. Try again.`,
|
|
998
|
+
);
|
|
999
|
+
};
|
|
1000
|
+
const forwardPendingRerouteMessages = async (
|
|
1001
|
+
pending: PendingUnboundReroute,
|
|
1002
|
+
instanceId: string,
|
|
1003
|
+
threadId: number,
|
|
1004
|
+
ctx: TContext,
|
|
1005
|
+
): Promise<boolean> => {
|
|
1006
|
+
const forwardMessage = deps.foreignOwnedUpdateForwarder?.forwardMessage;
|
|
1007
|
+
if (!forwardMessage) return false;
|
|
1008
|
+
const messages = cloneTelegramMessagesForThread(pending.messages, threadId);
|
|
1009
|
+
const outcomes = await Promise.allSettled(
|
|
1010
|
+
messages.map((message) =>
|
|
1011
|
+
forwardMessage({
|
|
1012
|
+
message,
|
|
1013
|
+
ownership: { instanceId },
|
|
1014
|
+
ctx,
|
|
1015
|
+
}),
|
|
1016
|
+
),
|
|
1017
|
+
);
|
|
1018
|
+
pending.messages = pending.messages.filter((_, index) => {
|
|
1019
|
+
const outcome = outcomes[index];
|
|
1020
|
+
if (outcome?.status === "fulfilled" && outcome.value) return false;
|
|
1021
|
+
if (outcome?.status === "rejected") {
|
|
1022
|
+
deps.recordRuntimeEvent?.("bus", outcome.reason, {
|
|
1023
|
+
phase: "reroute-foreign-forward",
|
|
1024
|
+
instanceId,
|
|
1025
|
+
threadId,
|
|
1026
|
+
messageIndex: index,
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
return true;
|
|
1030
|
+
});
|
|
1031
|
+
return pending.messages.length === 0;
|
|
1032
|
+
};
|
|
998
1033
|
const handleUnboundRerouteRestoreMenuCallback = async (
|
|
999
1034
|
query: TCallbackQuery,
|
|
1000
1035
|
_ctx: TContext,
|
|
@@ -1062,6 +1097,51 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1062
1097
|
return true;
|
|
1063
1098
|
}
|
|
1064
1099
|
await deps.threadStore.load();
|
|
1100
|
+
if (pending.finalizeMessage) {
|
|
1101
|
+
await finalizePendingReroute(
|
|
1102
|
+
parsed.rerouteId,
|
|
1103
|
+
pending,
|
|
1104
|
+
query,
|
|
1105
|
+
pending.finalizeMessage,
|
|
1106
|
+
);
|
|
1107
|
+
return true;
|
|
1108
|
+
}
|
|
1109
|
+
if (pending.foreignRetry) {
|
|
1110
|
+
const retry = pending.foreignRetry;
|
|
1111
|
+
const allForwarded = await forwardPendingRerouteMessages(
|
|
1112
|
+
pending,
|
|
1113
|
+
retry.instanceId,
|
|
1114
|
+
retry.threadId,
|
|
1115
|
+
ctx,
|
|
1116
|
+
);
|
|
1117
|
+
if (!allForwarded) {
|
|
1118
|
+
await deps.answerCallbackQuery(
|
|
1119
|
+
query.id,
|
|
1120
|
+
"Target thread is unavailable; retrying will send only remaining messages.",
|
|
1121
|
+
);
|
|
1122
|
+
return true;
|
|
1123
|
+
}
|
|
1124
|
+
pending.foreignRetry = undefined;
|
|
1125
|
+
if (retry.cleanup) pending.cleanup = retry.cleanup;
|
|
1126
|
+
}
|
|
1127
|
+
if (pending.cleanup) {
|
|
1128
|
+
const cleanupComplete = await retryPendingRerouteCleanup(pending.cleanup);
|
|
1129
|
+
if (!cleanupComplete) {
|
|
1130
|
+
await deps.answerCallbackQuery(
|
|
1131
|
+
query.id,
|
|
1132
|
+
"Message routed, but thread cleanup is still pending. Try again.",
|
|
1133
|
+
);
|
|
1134
|
+
return true;
|
|
1135
|
+
}
|
|
1136
|
+
pending.cleanup = undefined;
|
|
1137
|
+
await finalizePendingReroute(
|
|
1138
|
+
parsed.rerouteId,
|
|
1139
|
+
pending,
|
|
1140
|
+
query,
|
|
1141
|
+
"Thread cleanup completed.",
|
|
1142
|
+
);
|
|
1143
|
+
return true;
|
|
1144
|
+
}
|
|
1065
1145
|
const record = getTelegramRoutableThreadRecords(
|
|
1066
1146
|
deps.threadStore.list(),
|
|
1067
1147
|
deps.getLiveThreadTargets?.(),
|
|
@@ -1150,27 +1230,43 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1150
1230
|
});
|
|
1151
1231
|
}
|
|
1152
1232
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
}),
|
|
1164
|
-
),
|
|
1233
|
+
const cleanup: PendingRerouteCleanup = {
|
|
1234
|
+
kind: "replaced-follower",
|
|
1235
|
+
target: record.target,
|
|
1236
|
+
instanceId: record.instanceId,
|
|
1237
|
+
};
|
|
1238
|
+
const allForwarded = await forwardPendingRerouteMessages(
|
|
1239
|
+
pending,
|
|
1240
|
+
record.instanceId!,
|
|
1241
|
+
sourceTarget.threadId,
|
|
1242
|
+
ctx,
|
|
1165
1243
|
);
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1244
|
+
if (!allForwarded) {
|
|
1245
|
+
pending.foreignRetry = {
|
|
1246
|
+
instanceId: record.instanceId!,
|
|
1247
|
+
threadId: sourceTarget.threadId,
|
|
1248
|
+
cleanup,
|
|
1249
|
+
};
|
|
1250
|
+
await deps.answerCallbackQuery(
|
|
1251
|
+
query.id,
|
|
1252
|
+
"Thread restored; retrying will send only remaining messages before old-thread cleanup.",
|
|
1253
|
+
);
|
|
1254
|
+
return true;
|
|
1170
1255
|
}
|
|
1171
|
-
await
|
|
1172
|
-
|
|
1173
|
-
|
|
1256
|
+
const cleanupComplete = await retryPendingRerouteCleanup(cleanup);
|
|
1257
|
+
if (!cleanupComplete) {
|
|
1258
|
+
pending.cleanup = cleanup;
|
|
1259
|
+
await deps.answerCallbackQuery(
|
|
1260
|
+
query.id,
|
|
1261
|
+
"Thread restored, but old-thread cleanup is still pending. Try again.",
|
|
1262
|
+
);
|
|
1263
|
+
return true;
|
|
1264
|
+
}
|
|
1265
|
+
await finalizePendingReroute(
|
|
1266
|
+
parsed.rerouteId,
|
|
1267
|
+
pending,
|
|
1268
|
+
query,
|
|
1269
|
+
"Message routed.",
|
|
1174
1270
|
);
|
|
1175
1271
|
return true;
|
|
1176
1272
|
}
|
|
@@ -1186,49 +1282,64 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1186
1282
|
);
|
|
1187
1283
|
return true;
|
|
1188
1284
|
}
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
ctx,
|
|
1195
|
-
}),
|
|
1196
|
-
),
|
|
1285
|
+
const allForwarded = await forwardPendingRerouteMessages(
|
|
1286
|
+
pending,
|
|
1287
|
+
record.instanceId,
|
|
1288
|
+
parsed.threadId,
|
|
1289
|
+
ctx,
|
|
1197
1290
|
);
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1291
|
+
if (!allForwarded) {
|
|
1292
|
+
await deps.answerCallbackQuery(
|
|
1293
|
+
query.id,
|
|
1294
|
+
"Target thread is unavailable; retrying will send only remaining messages.",
|
|
1295
|
+
);
|
|
1296
|
+
return true;
|
|
1203
1297
|
}
|
|
1204
|
-
await
|
|
1205
|
-
|
|
1206
|
-
|
|
1298
|
+
const cleanupComplete = await closeReroutedUnboundTopic(
|
|
1299
|
+
sourceTarget,
|
|
1300
|
+
sourceMessageId,
|
|
1301
|
+
);
|
|
1302
|
+
if (!cleanupComplete && sourceTarget) {
|
|
1303
|
+
pending.cleanup = {
|
|
1304
|
+
kind: "unbound",
|
|
1305
|
+
target: sourceTarget,
|
|
1306
|
+
...(typeof sourceMessageId === "number"
|
|
1307
|
+
? { messageId: sourceMessageId }
|
|
1308
|
+
: {}),
|
|
1309
|
+
};
|
|
1310
|
+
await deps.answerCallbackQuery(
|
|
1311
|
+
query.id,
|
|
1312
|
+
"Message routed, but thread cleanup is still pending. Try again.",
|
|
1313
|
+
);
|
|
1314
|
+
return true;
|
|
1315
|
+
}
|
|
1316
|
+
await finalizePendingReroute(
|
|
1317
|
+
parsed.rerouteId,
|
|
1318
|
+
pending,
|
|
1319
|
+
query,
|
|
1320
|
+
"Message routed.",
|
|
1207
1321
|
);
|
|
1208
1322
|
return true;
|
|
1209
1323
|
}
|
|
1210
1324
|
if (
|
|
1211
1325
|
sourceTarget &&
|
|
1212
1326
|
isCurrentLeaderRecord &&
|
|
1213
|
-
|
|
1327
|
+
parsed.useNewSlot &&
|
|
1214
1328
|
(record.target.chatId !== sourceTarget.chatId ||
|
|
1215
1329
|
record.target.threadId !== sourceTarget.threadId)
|
|
1216
1330
|
) {
|
|
1217
1331
|
deps.threadStore.markStaleByTarget(
|
|
1218
1332
|
record.target,
|
|
1219
1333
|
"deleted",
|
|
1220
|
-
|
|
1221
|
-
? "Current leader thread was replaced by a new-slot reroute source."
|
|
1222
|
-
: "Current leader thread was replaced by reroute source.",
|
|
1334
|
+
"Current leader thread was replaced by a new-slot reroute source.",
|
|
1223
1335
|
);
|
|
1224
|
-
const slot =
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
: (record.slot ?? "?");
|
|
1336
|
+
const slot =
|
|
1337
|
+
deps.threadStore.allocateSlot(
|
|
1338
|
+
leaderProfileKey ?? record.profileKey,
|
|
1339
|
+
getNextTelegramSlotPreference(record.slot),
|
|
1340
|
+
) ??
|
|
1341
|
+
record.slot ??
|
|
1342
|
+
"?";
|
|
1232
1343
|
const nowMs = Date.now();
|
|
1233
1344
|
const threadName = getRestoredThreadName(record, slot);
|
|
1234
1345
|
deps.threadStore.upsert({
|
|
@@ -1239,9 +1350,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1239
1350
|
threadName,
|
|
1240
1351
|
instanceId: currentInstanceId,
|
|
1241
1352
|
slot,
|
|
1242
|
-
lastReconcileAction:
|
|
1243
|
-
? "reroute-new-slot"
|
|
1244
|
-
: "reroute-reclaim",
|
|
1353
|
+
lastReconcileAction: "reroute-new-slot",
|
|
1245
1354
|
rerouteConfirmedAtMs: nowMs,
|
|
1246
1355
|
});
|
|
1247
1356
|
await deps.threadStore.persist();
|
|
@@ -1272,110 +1381,58 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1272
1381
|
slot,
|
|
1273
1382
|
},
|
|
1274
1383
|
);
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
await promptEnqueue(
|
|
1384
|
+
await dispatchPendingRerouteMessages(
|
|
1385
|
+
pending,
|
|
1279
1386
|
cloneTelegramMessagesForThread(pending.messages, sourceTarget.threadId),
|
|
1280
1387
|
ctx,
|
|
1281
1388
|
);
|
|
1282
|
-
|
|
1283
|
-
await
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
await dismissRerouteChooserMessage(query);
|
|
1290
|
-
await closeReroutedUnboundTopic(sourceTarget, sourceMessageId);
|
|
1291
|
-
await deps.answerCallbackQuery(query.id, "Message routed.");
|
|
1292
|
-
return true;
|
|
1293
|
-
};
|
|
1294
|
-
let dispatchAllTabCommandToTarget:
|
|
1295
|
-
| ((
|
|
1296
|
-
commandText: string,
|
|
1297
|
-
query: TCallbackQuery,
|
|
1298
|
-
threadId: number,
|
|
1299
|
-
ctx: TContext,
|
|
1300
|
-
) => Promise<void>)
|
|
1301
|
-
| undefined;
|
|
1302
|
-
const handleAllTabMenuCallback = async (
|
|
1303
|
-
query: TCallbackQuery,
|
|
1304
|
-
ctx: TContext,
|
|
1305
|
-
): Promise<boolean> => {
|
|
1306
|
-
const parsed = parseTelegramAllTabMenuCallbackData(query.data);
|
|
1307
|
-
if (!parsed) return false;
|
|
1308
|
-
const chatId = query.message?.chat?.id;
|
|
1309
|
-
const pending = pendingAllTabCommands.get(parsed.commandId);
|
|
1310
|
-
if (typeof chatId !== "number" || !deps.threadStore || !pending) {
|
|
1311
|
-
await deps.answerCallbackQuery(query.id, "Thread menu expired.");
|
|
1312
|
-
return true;
|
|
1313
|
-
}
|
|
1314
|
-
await deps.threadStore.load();
|
|
1315
|
-
const record = getTelegramRoutableThreadRecords(
|
|
1316
|
-
deps.threadStore.list(),
|
|
1317
|
-
deps.getLiveThreadTargets?.(),
|
|
1318
|
-
).find(
|
|
1319
|
-
(candidate) =>
|
|
1320
|
-
candidate.target.chatId === chatId &&
|
|
1321
|
-
candidate.target.threadId === parsed.threadId,
|
|
1322
|
-
);
|
|
1323
|
-
if (!record) {
|
|
1324
|
-
await deps.answerCallbackQuery(query.id, "Thread is not active yet.");
|
|
1325
|
-
return true;
|
|
1326
|
-
}
|
|
1327
|
-
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
1328
|
-
const leaderProfileKey = getLeaderTopicProfileKey(ctx, currentInstanceId);
|
|
1329
|
-
const isCurrentLeaderRecord = isCurrentLeaderTopicRecord(
|
|
1330
|
-
record,
|
|
1331
|
-
leaderProfileKey,
|
|
1332
|
-
currentInstanceId,
|
|
1333
|
-
);
|
|
1334
|
-
if (
|
|
1335
|
-
record.instanceId &&
|
|
1336
|
-
record.instanceId !== currentInstanceId &&
|
|
1337
|
-
!isCurrentLeaderRecord
|
|
1338
|
-
) {
|
|
1339
|
-
if (!deps.foreignOwnedUpdateForwarder?.forwardMessage) {
|
|
1389
|
+
pending.messages = [];
|
|
1390
|
+
const cleanupComplete = await closePreviousLeaderThread(record.target);
|
|
1391
|
+
if (!cleanupComplete) {
|
|
1392
|
+
pending.cleanup = {
|
|
1393
|
+
kind: "previous-leader",
|
|
1394
|
+
target: record.target,
|
|
1395
|
+
};
|
|
1340
1396
|
await deps.answerCallbackQuery(
|
|
1341
1397
|
query.id,
|
|
1342
|
-
"
|
|
1398
|
+
"Thread restored, but old-thread cleanup is still pending. Try again.",
|
|
1343
1399
|
);
|
|
1344
1400
|
return true;
|
|
1345
1401
|
}
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
from: query.from,
|
|
1352
|
-
message_thread_id: parsed.threadId,
|
|
1353
|
-
text: pending.text,
|
|
1354
|
-
} as TMessage,
|
|
1355
|
-
ownership: { instanceId: record.instanceId },
|
|
1356
|
-
ctx,
|
|
1357
|
-
});
|
|
1358
|
-
if (forwarded) pendingAllTabCommands.delete(parsed.commandId);
|
|
1359
|
-
await deps.answerCallbackQuery(
|
|
1360
|
-
query.id,
|
|
1361
|
-
forwarded
|
|
1362
|
-
? "Opening in target thread."
|
|
1363
|
-
: "Target thread is unavailable.",
|
|
1402
|
+
await finalizePendingReroute(
|
|
1403
|
+
parsed.rerouteId,
|
|
1404
|
+
pending,
|
|
1405
|
+
query,
|
|
1406
|
+
"Message routed.",
|
|
1364
1407
|
);
|
|
1365
1408
|
return true;
|
|
1366
1409
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1410
|
+
await dispatchPendingRerouteMessages(pending, reroutedMessages, ctx);
|
|
1411
|
+
pending.messages = [];
|
|
1412
|
+
const cleanupComplete = await closeReroutedUnboundTopic(
|
|
1413
|
+
sourceTarget,
|
|
1414
|
+
sourceMessageId,
|
|
1415
|
+
);
|
|
1416
|
+
if (!cleanupComplete && sourceTarget) {
|
|
1417
|
+
pending.cleanup = {
|
|
1418
|
+
kind: "unbound",
|
|
1419
|
+
target: sourceTarget,
|
|
1420
|
+
...(typeof sourceMessageId === "number"
|
|
1421
|
+
? { messageId: sourceMessageId }
|
|
1422
|
+
: {}),
|
|
1423
|
+
};
|
|
1424
|
+
await deps.answerCallbackQuery(
|
|
1425
|
+
query.id,
|
|
1426
|
+
"Message routed, but thread cleanup is still pending. Try again.",
|
|
1427
|
+
);
|
|
1369
1428
|
return true;
|
|
1370
1429
|
}
|
|
1371
|
-
await
|
|
1372
|
-
|
|
1430
|
+
await finalizePendingReroute(
|
|
1431
|
+
parsed.rerouteId,
|
|
1432
|
+
pending,
|
|
1373
1433
|
query,
|
|
1374
|
-
|
|
1375
|
-
ctx,
|
|
1434
|
+
"Message routed.",
|
|
1376
1435
|
);
|
|
1377
|
-
pendingAllTabCommands.delete(parsed.commandId);
|
|
1378
|
-
await deps.answerCallbackQuery(query.id, "Opening in target thread.");
|
|
1379
1436
|
return true;
|
|
1380
1437
|
};
|
|
1381
1438
|
const callbackHandler = async (
|
|
@@ -1384,7 +1441,6 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1384
1441
|
): Promise<void> => {
|
|
1385
1442
|
if (await handleUnboundRerouteRestoreMenuCallback(query, ctx)) return;
|
|
1386
1443
|
if (await handleUnboundRerouteCallback(query, ctx)) return;
|
|
1387
|
-
if (await handleAllTabMenuCallback(query, ctx)) return;
|
|
1388
1444
|
if (deps.buttonActionStore) {
|
|
1389
1445
|
const handled = await OutboundHandlers.handleTelegramButtonCallbackQuery(
|
|
1390
1446
|
query,
|
|
@@ -1769,10 +1825,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1769
1825
|
deps.getLiveThreadTargets?.(),
|
|
1770
1826
|
);
|
|
1771
1827
|
if (activeRecords.length === 0) return false;
|
|
1772
|
-
const
|
|
1828
|
+
const commandMessage = {
|
|
1829
|
+
...message,
|
|
1830
|
+
text: commandText,
|
|
1831
|
+
caption: undefined,
|
|
1832
|
+
} as TMessage;
|
|
1833
|
+
const rerouteId = storePendingUnboundReroute([commandMessage], "command");
|
|
1773
1834
|
const text = formatTelegramAllTabMenuChooserText(command.name);
|
|
1774
|
-
const replyMarkup =
|
|
1775
|
-
|
|
1835
|
+
const replyMarkup = buildTelegramUnboundRerouteChooserMarkup(
|
|
1836
|
+
rerouteId,
|
|
1776
1837
|
activeRecords,
|
|
1777
1838
|
);
|
|
1778
1839
|
if (deps.sendInteractiveMessage) {
|
|
@@ -1877,24 +1938,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1877
1938
|
({ ...message, text, caption: undefined }) as TMessage,
|
|
1878
1939
|
enqueueTurn: promptEnqueue,
|
|
1879
1940
|
});
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
if (typeof chatId !== "number") return;
|
|
1883
|
-
await commandOrPrompt.dispatchMessages(
|
|
1884
|
-
[
|
|
1885
|
-
{
|
|
1886
|
-
...(query.message ?? {}),
|
|
1887
|
-
message_id: 0,
|
|
1888
|
-
chat: { id: chatId, type: "private" },
|
|
1889
|
-
from: query.from,
|
|
1890
|
-
message_thread_id: threadId,
|
|
1891
|
-
text: commandText,
|
|
1892
|
-
caption: undefined,
|
|
1893
|
-
} as TMessage,
|
|
1894
|
-
],
|
|
1895
|
-
ctx,
|
|
1896
|
-
);
|
|
1897
|
-
};
|
|
1941
|
+
dispatchReroutedCommandMessages = (messages, ctx) =>
|
|
1942
|
+
commandOrPrompt.dispatchMessages(messages, ctx);
|
|
1898
1943
|
const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
|
|
1899
1944
|
TMessage,
|
|
1900
1945
|
TContext
|