@llblab/pi-kit 0.5.1 → 0.5.2
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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +16 -0
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +4 -0
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +19 -3
- package/node_modules/@llblab/pi-telegram/index.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +43 -25
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +60 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +101 -90
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +18 -7
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +11 -0
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +10 -3
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +19 -3
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +97 -66
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +192 -58
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +27 -35
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
|
|
9
9
|
import { isVoiceTurn } from "./voice.ts";
|
|
10
|
+
import { isTelegramApiCommitUnknownError } from "./telegram-api.ts";
|
|
10
11
|
|
|
11
12
|
// --- Queue Items ---
|
|
12
13
|
|
|
@@ -1497,6 +1498,10 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1497
1498
|
updateStatus: () => void;
|
|
1498
1499
|
dispatchNextQueuedTelegramTurn: () => void;
|
|
1499
1500
|
scheduleActiveTurnDelivery?: (task: () => Promise<void>) => void;
|
|
1501
|
+
preparePreviewClear?: (
|
|
1502
|
+
chatId: number,
|
|
1503
|
+
options?: { target?: TelegramQueueTarget; isDeliveryActive?: () => boolean },
|
|
1504
|
+
) => () => Promise<void>;
|
|
1500
1505
|
clearPreview: (
|
|
1501
1506
|
chatId: number,
|
|
1502
1507
|
options?: { target?: TelegramQueueTarget },
|
|
@@ -1520,11 +1525,14 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1520
1525
|
text: string,
|
|
1521
1526
|
options?: { target?: TelegramQueueTarget },
|
|
1522
1527
|
) => Promise<unknown>;
|
|
1523
|
-
sendQueuedAttachments: (
|
|
1528
|
+
sendQueuedAttachments: (
|
|
1529
|
+
turn: TTurn,
|
|
1530
|
+
options?: { isDeliveryActive?: () => boolean },
|
|
1531
|
+
) => Promise<void>;
|
|
1524
1532
|
sendRichAttachmentReply?: (
|
|
1525
1533
|
turn: TTurn,
|
|
1526
1534
|
markdown: string,
|
|
1527
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
1535
|
+
options?: { replyMarkup?: TReplyMarkup; isDeliveryActive?: () => boolean },
|
|
1528
1536
|
) => Promise<boolean>;
|
|
1529
1537
|
answerGuestQuery?: (
|
|
1530
1538
|
guestQueryId: string,
|
|
@@ -1548,7 +1556,7 @@ export interface TelegramAgentEndRuntimeDeps<
|
|
|
1548
1556
|
sendOutboundReplyArtifacts?: (
|
|
1549
1557
|
turn: TTurn,
|
|
1550
1558
|
plan: TelegramAgentEndOutboundReplyPlan,
|
|
1551
|
-
options?: { replyToPrompt?: boolean },
|
|
1559
|
+
options?: { replyToPrompt?: boolean; isDeliveryActive?: () => boolean },
|
|
1552
1560
|
) => Promise<void>;
|
|
1553
1561
|
recordRuntimeEvent?: (
|
|
1554
1562
|
category: string,
|
|
@@ -1583,6 +1591,11 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1583
1591
|
TTurn,
|
|
1584
1592
|
TReplyMarkup
|
|
1585
1593
|
>["scheduleActiveTurnDelivery"];
|
|
1594
|
+
reserveActiveTurnDelivery?: () => {
|
|
1595
|
+
schedule: (task: () => Promise<void>) => void;
|
|
1596
|
+
cancel: () => void;
|
|
1597
|
+
};
|
|
1598
|
+
preparePreviewClear?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["preparePreviewClear"];
|
|
1586
1599
|
clearPreview: TelegramAgentEndRuntimeDeps<
|
|
1587
1600
|
TTurn,
|
|
1588
1601
|
TReplyMarkup
|
|
@@ -1597,7 +1610,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1597
1610
|
TReplyMarkup
|
|
1598
1611
|
>["sendMarkdownReply"];
|
|
1599
1612
|
sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
|
|
1600
|
-
sendQueuedAttachments:
|
|
1613
|
+
sendQueuedAttachments: TelegramAgentEndRuntimeDeps<TTurn>["sendQueuedAttachments"];
|
|
1601
1614
|
sendRichAttachmentReply?: TelegramAgentEndRuntimeDeps<
|
|
1602
1615
|
TTurn,
|
|
1603
1616
|
TReplyMarkup
|
|
@@ -1702,48 +1715,56 @@ export function createTelegramAgentEndHook<
|
|
|
1702
1715
|
ctx: TContext,
|
|
1703
1716
|
assistantOverride?: TelegramAgentEndAssistantResult,
|
|
1704
1717
|
): Promise<void> => {
|
|
1705
|
-
|
|
1706
|
-
if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
|
|
1718
|
+
if (deps.isSessionActive?.(ctx) === false) return;
|
|
1707
1719
|
const turn = deps.getActiveTurn();
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
deps.
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
deps.
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1720
|
+
const assistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
|
|
1721
|
+
const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
|
|
1722
|
+
const reservation = turn && !turn.guestQueryId && hasPublication ? deps.reserveActiveTurnDelivery?.() : undefined;
|
|
1723
|
+
const scheduleDelivery = reservation?.schedule ?? deps.scheduleActiveTurnDelivery;
|
|
1724
|
+
try {
|
|
1725
|
+
await deps.loadConfig?.();
|
|
1726
|
+
if (deps.isSessionActive?.(ctx) === false || deps.getActiveTurn() !== turn) return;
|
|
1727
|
+
await handleTelegramAgentEndRuntime({
|
|
1728
|
+
turn,
|
|
1729
|
+
assistant,
|
|
1730
|
+
foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
|
|
1731
|
+
resetRuntimeState: deps.resetRuntimeState,
|
|
1732
|
+
isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
|
|
1733
|
+
isTurnTransportActive: deps.isTurnTransportActive,
|
|
1734
|
+
waitForTypingIdle: deps.waitForTypingIdle,
|
|
1735
|
+
waitForActivityIdle: deps.waitForActivityIdle,
|
|
1736
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
1737
|
+
dispatchNextQueuedTelegramTurn: () => {
|
|
1738
|
+
deps.requestDeferredDispatchNextQueuedTelegramTurn(
|
|
1739
|
+
deps.dispatchNextQueuedTelegramTurn,
|
|
1740
|
+
);
|
|
1741
|
+
},
|
|
1742
|
+
scheduleActiveTurnDelivery: scheduleDelivery
|
|
1743
|
+
? (task) =>
|
|
1744
|
+
scheduleDelivery(async () => {
|
|
1745
|
+
if (deps.isSessionActive?.(ctx) === false) return;
|
|
1746
|
+
await task();
|
|
1747
|
+
})
|
|
1748
|
+
: undefined,
|
|
1749
|
+
preparePreviewClear: deps.preparePreviewClear,
|
|
1750
|
+
clearPreview: deps.clearPreview,
|
|
1751
|
+
setPreviewPendingText: deps.setPreviewPendingText,
|
|
1752
|
+
finalizeMarkdownPreview: deps.finalizeMarkdownPreview,
|
|
1753
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
1754
|
+
sendTextReply: deps.sendTextReply,
|
|
1755
|
+
sendQueuedAttachments: deps.sendQueuedAttachments,
|
|
1756
|
+
sendRichAttachmentReply: deps.sendRichAttachmentReply,
|
|
1757
|
+
answerGuestQuery: deps.answerGuestQuery,
|
|
1758
|
+
sendGuestReply: deps.sendGuestReply,
|
|
1759
|
+
sendGuestAttachment: deps.sendGuestAttachment,
|
|
1760
|
+
sendGuestVoiceReply: deps.sendGuestVoiceReply,
|
|
1761
|
+
planOutboundReply: deps.planOutboundReply,
|
|
1762
|
+
sendOutboundReplyArtifacts: deps.sendOutboundReplyArtifacts,
|
|
1763
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1764
|
+
});
|
|
1765
|
+
} finally {
|
|
1766
|
+
reservation?.cancel();
|
|
1767
|
+
}
|
|
1747
1768
|
};
|
|
1748
1769
|
}
|
|
1749
1770
|
|
|
@@ -1786,6 +1807,10 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1786
1807
|
const isDeliveryActive = (): boolean =>
|
|
1787
1808
|
deps.isSessionActive?.() !== false &&
|
|
1788
1809
|
(!turn || deps.isTurnTransportActive?.(turn) !== false);
|
|
1810
|
+
const clearPreview = turn
|
|
1811
|
+
? deps.preparePreviewClear?.(turn.chatId, { target: turn.target, isDeliveryActive })
|
|
1812
|
+
?? (() => deps.clearPreview(turn.chatId, { target: turn.target }))
|
|
1813
|
+
: undefined;
|
|
1789
1814
|
const updateStatusIgnoringStaleContext = (): void => {
|
|
1790
1815
|
try {
|
|
1791
1816
|
deps.updateStatus();
|
|
@@ -1869,28 +1894,28 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1869
1894
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1870
1895
|
return;
|
|
1871
1896
|
}
|
|
1872
|
-
if (endPlan.shouldClearPreview) {
|
|
1873
|
-
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1874
|
-
}
|
|
1875
1897
|
if (!isDeliveryActive()) return;
|
|
1876
|
-
if (endPlan.shouldSendErrorMessage) {
|
|
1877
|
-
await deps.sendTextReply(
|
|
1878
|
-
turn.chatId,
|
|
1879
|
-
turn.replyToMessageId,
|
|
1880
|
-
assistant.errorMessage ||
|
|
1881
|
-
"Telegram bridge: Pi failed while processing the request.",
|
|
1882
|
-
{ target: turn.target },
|
|
1883
|
-
);
|
|
1884
|
-
if (!isDeliveryActive()) return;
|
|
1885
|
-
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1886
|
-
return;
|
|
1887
|
-
}
|
|
1888
1898
|
const deliverActiveTurn = async () => {
|
|
1889
1899
|
await deps.waitForActivityIdle?.();
|
|
1890
1900
|
if (!isDeliveryActive()) return;
|
|
1901
|
+
if (endPlan.shouldClearPreview || (!finalText && hasOutboundArtifacts)) {
|
|
1902
|
+
await clearPreview?.();
|
|
1903
|
+
if (!isDeliveryActive()) return;
|
|
1904
|
+
}
|
|
1905
|
+
if (endPlan.shouldSendErrorMessage) {
|
|
1906
|
+
await deps.sendTextReply(
|
|
1907
|
+
turn.chatId,
|
|
1908
|
+
turn.replyToMessageId,
|
|
1909
|
+
assistant.errorMessage ||
|
|
1910
|
+
"Telegram bridge: Pi failed while processing the request.",
|
|
1911
|
+
{ target: turn.target },
|
|
1912
|
+
);
|
|
1913
|
+
if (!isDeliveryActive()) return;
|
|
1914
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1891
1917
|
if (finalText) deps.setPreviewPendingText(finalText);
|
|
1892
|
-
|
|
1893
|
-
await deps.clearPreview(turn.chatId, { target: turn.target });
|
|
1918
|
+
|
|
1894
1919
|
if (!isDeliveryActive()) return;
|
|
1895
1920
|
let richAttachmentDelivered = false;
|
|
1896
1921
|
if (
|
|
@@ -1903,7 +1928,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1903
1928
|
richAttachmentDelivered = await deps.sendRichAttachmentReply(
|
|
1904
1929
|
turn,
|
|
1905
1930
|
finalText,
|
|
1906
|
-
{ replyMarkup },
|
|
1931
|
+
{ replyMarkup, isDeliveryActive },
|
|
1907
1932
|
);
|
|
1908
1933
|
if (!isDeliveryActive()) return;
|
|
1909
1934
|
if (richAttachmentDelivered) {
|
|
@@ -1912,6 +1937,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1912
1937
|
deps.setPreviewPendingText("");
|
|
1913
1938
|
}
|
|
1914
1939
|
} catch (error) {
|
|
1940
|
+
if (!isDeliveryActive()) return;
|
|
1915
1941
|
deps.recordRuntimeEvent?.("delivery", error, {
|
|
1916
1942
|
phase: "rich-attachment-commit-unknown",
|
|
1917
1943
|
chatId: turn.chatId,
|
|
@@ -1955,6 +1981,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1955
1981
|
try {
|
|
1956
1982
|
await deps.sendOutboundReplyArtifacts(turn, outboundReply, {
|
|
1957
1983
|
replyToPrompt: !finalText,
|
|
1984
|
+
isDeliveryActive,
|
|
1958
1985
|
});
|
|
1959
1986
|
if (!isDeliveryActive()) return;
|
|
1960
1987
|
} catch (error) {
|
|
@@ -1962,8 +1989,12 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1962
1989
|
phase: "voice-artifacts",
|
|
1963
1990
|
chatId: turn.chatId,
|
|
1964
1991
|
});
|
|
1965
|
-
// Fallback to planned text when voice delivery fails and text wasn't already delivered
|
|
1966
1992
|
if (!isDeliveryActive()) return;
|
|
1993
|
+
if (isTelegramApiCommitUnknownError(error)) {
|
|
1994
|
+
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
// Fallback only when voice delivery is not uncertain and text wasn't already delivered.
|
|
1967
1998
|
if (rawFinalText?.trim() && !finalText && hasOutboundArtifacts) {
|
|
1968
1999
|
try {
|
|
1969
2000
|
const fallbackMarkdown =
|
|
@@ -2000,13 +2031,13 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2000
2031
|
);
|
|
2001
2032
|
}
|
|
2002
2033
|
if (!isDeliveryActive()) return;
|
|
2003
|
-
if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn);
|
|
2034
|
+
if (!richAttachmentDelivered) await deps.sendQueuedAttachments(turn, { isDeliveryActive });
|
|
2004
2035
|
if (!isDeliveryActive()) return;
|
|
2005
2036
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
2006
2037
|
};
|
|
2007
2038
|
if (
|
|
2008
2039
|
deps.scheduleActiveTurnDelivery &&
|
|
2009
|
-
(endPlan.kind === "text" || endPlan.kind === "attachments-only")
|
|
2040
|
+
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
|
|
2010
2041
|
) {
|
|
2011
2042
|
deps.scheduleActiveTurnDelivery(deliverActiveTurn);
|
|
2012
2043
|
return;
|
|
@@ -448,7 +448,19 @@ async function deleteReservedTelegramTopicThroughReconciler(
|
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
export
|
|
451
|
+
export const TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS = 60 * 60_000;
|
|
452
|
+
|
|
453
|
+
export function isTelegramAllTabCommandExpired(
|
|
454
|
+
message: { date?: number; message_thread_id?: number },
|
|
455
|
+
nowMs = Date.now(),
|
|
456
|
+
): boolean {
|
|
457
|
+
return message.message_thread_id === undefined &&
|
|
458
|
+
typeof message.date === "number" && Number.isFinite(message.date) &&
|
|
459
|
+
message.date > 0 && Number.isFinite(nowMs) &&
|
|
460
|
+
nowMs - message.date * 1000 >= TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export type TelegramRoutedMessage = { date?: number } & Updates.TelegramUpdateMessage &
|
|
452
464
|
Media.TelegramMediaMessage &
|
|
453
465
|
Media.TelegramMediaGroupMessage &
|
|
454
466
|
Commands.TelegramCommandRuntimeMessage &
|
|
@@ -738,6 +750,12 @@ export function createTelegramInboundRouteRuntime<
|
|
|
738
750
|
messages: TMessage[];
|
|
739
751
|
createdAtMs: number;
|
|
740
752
|
dispatchKind: "prompt" | "command";
|
|
753
|
+
expiresAtMs?: number;
|
|
754
|
+
destinationSelected?: boolean;
|
|
755
|
+
selectionAttempted?: boolean;
|
|
756
|
+
dispatching?: boolean;
|
|
757
|
+
pauseExpiry?: () => void;
|
|
758
|
+
stopExpiry?: () => void;
|
|
741
759
|
cleanup?: PendingRerouteCleanup;
|
|
742
760
|
foreignRetry?: {
|
|
743
761
|
instanceId: string;
|
|
@@ -822,24 +840,71 @@ export function createTelegramInboundRouteRuntime<
|
|
|
822
840
|
): void => {
|
|
823
841
|
Updates.reportTelegramQueueAdmission(sources, receipts);
|
|
824
842
|
};
|
|
843
|
+
const removePendingReroute = (id: string): void => {
|
|
844
|
+
pendingUnboundReroutes.get(id)?.stopExpiry?.();
|
|
845
|
+
pendingUnboundReroutes.delete(id);
|
|
846
|
+
};
|
|
847
|
+
const expirePendingCommand = (id: string, pending: PendingUnboundReroute): boolean => {
|
|
848
|
+
if (pending.destinationSelected || pending.expiresAtMs === undefined ||
|
|
849
|
+
Date.now() < pending.expiresAtMs) return false;
|
|
850
|
+
if (pendingUnboundReroutes.get(id) !== pending) return true;
|
|
851
|
+
for (const message of pending.messages) Updates.reportTelegramUpdateCompleted(message);
|
|
852
|
+
removePendingReroute(id);
|
|
853
|
+
return true;
|
|
854
|
+
};
|
|
855
|
+
const armPendingCommandExpiry = (id: string, pending: PendingUnboundReroute): void => {
|
|
856
|
+
if (pending.dispatchKind !== "command" || pending.sourceTarget.threadId !== undefined) return;
|
|
857
|
+
pending.stopExpiry?.();
|
|
858
|
+
const execution = Updates.getTelegramUpdateExecutionFence(pending.messages[0]);
|
|
859
|
+
const onAbort = () => removePendingReroute(id);
|
|
860
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
861
|
+
let stopped = false;
|
|
862
|
+
pending.pauseExpiry = () => {
|
|
863
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
864
|
+
timer = undefined;
|
|
865
|
+
};
|
|
866
|
+
pending.stopExpiry = () => {
|
|
867
|
+
stopped = true;
|
|
868
|
+
pending.pauseExpiry?.();
|
|
869
|
+
execution?.signal.removeEventListener("abort", onAbort);
|
|
870
|
+
};
|
|
871
|
+
execution?.signal.addEventListener("abort", onAbort, { once: true });
|
|
872
|
+
if (execution?.signal.aborted) {
|
|
873
|
+
onAbort();
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
if (pending.expiresAtMs === undefined) {
|
|
877
|
+
const date = pending.messages[0]?.date;
|
|
878
|
+
if (typeof date !== "number" || !Number.isFinite(date) || date <= 0 || date * 1000 > Date.now()) return;
|
|
879
|
+
pending.expiresAtMs = date * 1000 + TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS;
|
|
880
|
+
}
|
|
881
|
+
const schedule = (): void => {
|
|
882
|
+
if (stopped || pending.destinationSelected || pendingUnboundReroutes.get(id) !== pending) return;
|
|
883
|
+
if (expirePendingCommand(id, pending)) return;
|
|
884
|
+
const delay = Math.min(TELEGRAM_ALL_TAB_COMMAND_MAX_AGE_MS, pending.expiresAtMs! - Date.now());
|
|
885
|
+
timer = setTimeout(schedule, Math.max(1, delay));
|
|
886
|
+
timer.unref?.();
|
|
887
|
+
};
|
|
888
|
+
schedule();
|
|
889
|
+
};
|
|
825
890
|
const prunePendingUnboundReroutes = () => {
|
|
826
891
|
const nowMs = Date.now();
|
|
827
892
|
for (const [id, entry] of pendingUnboundReroutes) {
|
|
828
|
-
if (
|
|
829
|
-
|
|
893
|
+
if (entry.dispatchKind === "command" && entry.sourceTarget.threadId === undefined) {
|
|
894
|
+
expirePendingCommand(id, entry);
|
|
895
|
+
} else if (nowMs - entry.createdAtMs > 30 * 60_000) {
|
|
896
|
+
removePendingReroute(id);
|
|
830
897
|
}
|
|
831
898
|
}
|
|
832
|
-
while (pendingUnboundReroutes.size > 100) {
|
|
833
|
-
const oldest = pendingUnboundReroutes.keys().next().value;
|
|
834
|
-
if (!oldest) break;
|
|
835
|
-
pendingUnboundReroutes.delete(oldest);
|
|
836
|
-
}
|
|
837
899
|
};
|
|
838
900
|
const storePendingUnboundReroute = (
|
|
839
901
|
messages: TMessage[],
|
|
840
902
|
dispatchKind: "prompt" | "command" = "prompt",
|
|
841
903
|
): string => {
|
|
842
904
|
prunePendingUnboundReroutes();
|
|
905
|
+
if (pendingUnboundReroutes.size >= 100) {
|
|
906
|
+
throw new Error("Telegram route chooser capacity reached; source remains retryable.");
|
|
907
|
+
}
|
|
843
908
|
nextUnboundRerouteId += 1;
|
|
844
909
|
const id = nextUnboundRerouteId.toString(36);
|
|
845
910
|
pendingUnboundReroutes.set(id, {
|
|
@@ -853,11 +918,33 @@ export function createTelegramInboundRouteRuntime<
|
|
|
853
918
|
createdAtMs: Date.now(),
|
|
854
919
|
dispatchKind,
|
|
855
920
|
});
|
|
921
|
+
const pending = pendingUnboundReroutes.get(id)!;
|
|
922
|
+
armPendingCommandExpiry(id, pending);
|
|
856
923
|
return id;
|
|
857
924
|
};
|
|
858
925
|
const rememberRerouteChooser = (id: string, messageId: number | undefined): void => {
|
|
859
926
|
const pending = pendingUnboundReroutes.get(id);
|
|
860
|
-
if (pending)
|
|
927
|
+
if (!pending) return;
|
|
928
|
+
pending.chooserMessageId = messageId;
|
|
929
|
+
if (messageId === undefined || pending.dispatchKind !== "command" ||
|
|
930
|
+
pending.sourceTarget.threadId !== undefined || pending.selectionAttempted) return;
|
|
931
|
+
const source = pending.messages[0];
|
|
932
|
+
const text = source?.text?.trim();
|
|
933
|
+
const execution = Updates.getTelegramUpdateExecutionFence(source);
|
|
934
|
+
const sourceIds = Updates.collectTelegramAdmissionSourceUpdateIds(pending.messages);
|
|
935
|
+
if (!text || Commands.parseTelegramCommand(text)?.name !== "start" ||
|
|
936
|
+
source?.from?.id === undefined || !execution?.isCurrent() || sourceIds.length !== 1 ||
|
|
937
|
+
expirePendingCommand(id, pending)) return;
|
|
938
|
+
for (const [oldId, old] of pendingUnboundReroutes) {
|
|
939
|
+
if (oldId === id || old.dispatchKind !== "command" || old.selectionAttempted || old.dispatching ||
|
|
940
|
+
old.sourceTarget.threadId !== undefined || old.sourceTarget.chatId !== pending.sourceTarget.chatId) continue;
|
|
941
|
+
const oldSource = old.messages[0];
|
|
942
|
+
const oldIds = Updates.collectTelegramAdmissionSourceUpdateIds(old.messages);
|
|
943
|
+
if (oldSource?.from?.id !== source.from.id || oldSource?.text?.trim() !== text ||
|
|
944
|
+
oldIds.length !== 1 || oldIds[0]! >= sourceIds[0]! ||
|
|
945
|
+
Updates.getTelegramUpdateExecutionFence(oldSource)?.signal !== execution.signal) continue;
|
|
946
|
+
if (Updates.reportTelegramUpdateCompleted(oldSource)) removePendingReroute(oldId);
|
|
947
|
+
}
|
|
861
948
|
};
|
|
862
949
|
const matchesRerouteChooser = (
|
|
863
950
|
pending: PendingUnboundReroute,
|
|
@@ -1123,12 +1210,15 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1123
1210
|
| ((messages: TMessage[], ctx: TContext) => Promise<void>)
|
|
1124
1211
|
| undefined;
|
|
1125
1212
|
const dispatchPendingRerouteMessages = async (
|
|
1126
|
-
pending:
|
|
1213
|
+
pending: Pick<PendingUnboundReroute, "dispatchKind" | "sourceTarget">,
|
|
1127
1214
|
messages: TMessage[],
|
|
1128
1215
|
ctx: TContext,
|
|
1129
1216
|
): Promise<void> => {
|
|
1130
1217
|
if (pending.dispatchKind === "command" && dispatchReroutedCommandMessages) {
|
|
1131
1218
|
await dispatchReroutedCommandMessages(messages, ctx);
|
|
1219
|
+
if (pending.sourceTarget.threadId === undefined) {
|
|
1220
|
+
for (const message of messages) Updates.reportTelegramUpdateCompleted(message);
|
|
1221
|
+
}
|
|
1132
1222
|
return;
|
|
1133
1223
|
}
|
|
1134
1224
|
await promptEnqueue(messages, ctx);
|
|
@@ -1147,7 +1237,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1147
1237
|
);
|
|
1148
1238
|
assertExecutionCurrent();
|
|
1149
1239
|
if (dismissed) {
|
|
1150
|
-
|
|
1240
|
+
removePendingReroute(rerouteId);
|
|
1151
1241
|
await deps.answerCallbackQuery(query.id, successMessage);
|
|
1152
1242
|
return;
|
|
1153
1243
|
}
|
|
@@ -1184,6 +1274,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1184
1274
|
outcome?.status === "fulfilled" &&
|
|
1185
1275
|
outcome.value.status === "accepted"
|
|
1186
1276
|
) {
|
|
1277
|
+
if (pending.dispatchKind === "command" && pending.sourceTarget.threadId === undefined) {
|
|
1278
|
+
Updates.reportTelegramUpdateCompleted(pending.messages[index]);
|
|
1279
|
+
}
|
|
1187
1280
|
return false;
|
|
1188
1281
|
}
|
|
1189
1282
|
if (outcome?.status === "rejected") {
|
|
@@ -1216,7 +1309,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1216
1309
|
typeof messageId !== "number" ||
|
|
1217
1310
|
!deps.threadStore ||
|
|
1218
1311
|
!pending ||
|
|
1219
|
-
!matchesRerouteChooser(pending, query)
|
|
1312
|
+
!matchesRerouteChooser(pending, query) ||
|
|
1313
|
+
expirePendingCommand(parsed.rerouteId, pending)
|
|
1220
1314
|
) {
|
|
1221
1315
|
await deps.answerCallbackQuery(query.id, "Message route expired.");
|
|
1222
1316
|
return true;
|
|
@@ -1258,7 +1352,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1258
1352
|
await deps.answerCallbackQuery(query.id, "Choose instance to restore.");
|
|
1259
1353
|
return true;
|
|
1260
1354
|
};
|
|
1261
|
-
const
|
|
1355
|
+
const executeUnboundRerouteCallback = async (
|
|
1262
1356
|
query: TCallbackQuery,
|
|
1263
1357
|
ctx: TContext,
|
|
1264
1358
|
): Promise<boolean> => {
|
|
@@ -1270,12 +1364,17 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1270
1364
|
const chatId = query.message?.chat?.id;
|
|
1271
1365
|
const pending = pendingUnboundReroutes.get(parsed.rerouteId);
|
|
1272
1366
|
if (typeof chatId !== "number" || !deps.threadStore || !pending ||
|
|
1273
|
-
!matchesRerouteChooser(pending, query)) {
|
|
1367
|
+
!matchesRerouteChooser(pending, query) || expirePendingCommand(parsed.rerouteId, pending)) {
|
|
1274
1368
|
await deps.answerCallbackQuery(query.id, "Message route expired.");
|
|
1275
1369
|
return true;
|
|
1276
1370
|
}
|
|
1277
1371
|
await deps.threadStore.load();
|
|
1278
1372
|
assertExecutionCurrent();
|
|
1373
|
+
if (pendingUnboundReroutes.get(parsed.rerouteId) !== pending ||
|
|
1374
|
+
expirePendingCommand(parsed.rerouteId, pending)) {
|
|
1375
|
+
await deps.answerCallbackQuery(query.id, "Message route expired.");
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1279
1378
|
if (pending.finalizeMessage) {
|
|
1280
1379
|
await finalizePendingReroute(
|
|
1281
1380
|
parsed.rerouteId,
|
|
@@ -1361,6 +1460,9 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1361
1460
|
await deps.answerCallbackQuery(query.id, "🚫 Thread restore source is already owned.");
|
|
1362
1461
|
return true;
|
|
1363
1462
|
}
|
|
1463
|
+
pending.destinationSelected = true;
|
|
1464
|
+
pending.selectionAttempted = true;
|
|
1465
|
+
pending.pauseExpiry?.();
|
|
1364
1466
|
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
1365
1467
|
const leaderProfileKey = getLeaderTopicProfileKey(ctx, currentInstanceId);
|
|
1366
1468
|
const isCurrentLeaderRecord = isCurrentLeaderTopicRecord(
|
|
@@ -1657,6 +1759,33 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1657
1759
|
);
|
|
1658
1760
|
return true;
|
|
1659
1761
|
};
|
|
1762
|
+
const handleUnboundRerouteCallback = async (
|
|
1763
|
+
query: TCallbackQuery,
|
|
1764
|
+
ctx: TContext,
|
|
1765
|
+
): Promise<boolean> => {
|
|
1766
|
+
const parsed = parseTelegramUnboundRerouteCallbackData(query.data);
|
|
1767
|
+
const pending = parsed && pendingUnboundReroutes.get(parsed.rerouteId);
|
|
1768
|
+
if (!parsed || !pending || pending.dispatchKind !== "command" ||
|
|
1769
|
+
pending.sourceTarget.threadId !== undefined || !matchesRerouteChooser(pending, query)) {
|
|
1770
|
+
return executeUnboundRerouteCallback(query, ctx);
|
|
1771
|
+
}
|
|
1772
|
+
if (pending.dispatching) {
|
|
1773
|
+
await deps.answerCallbackQuery(query.id, "Command routing is already in progress.");
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
pending.dispatching = true;
|
|
1777
|
+
try {
|
|
1778
|
+
return await executeUnboundRerouteCallback(query, ctx);
|
|
1779
|
+
} finally {
|
|
1780
|
+
pending.dispatching = false;
|
|
1781
|
+
if (pendingUnboundReroutes.get(parsed.rerouteId) === pending &&
|
|
1782
|
+
pending.destinationSelected && pending.messages.length > 0 &&
|
|
1783
|
+
!pending.cleanup && !pending.finalizeMessage) {
|
|
1784
|
+
pending.destinationSelected = false;
|
|
1785
|
+
armPendingCommandExpiry(parsed.rerouteId, pending);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
};
|
|
1660
1789
|
const callbackHandler = async (
|
|
1661
1790
|
query: TCallbackQuery,
|
|
1662
1791
|
ctx: TContext,
|
|
@@ -2122,11 +2251,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2122
2251
|
deps.getLiveThreadTargets?.(),
|
|
2123
2252
|
);
|
|
2124
2253
|
if (activeRecords.length === 0) return false;
|
|
2125
|
-
const commandMessage = {
|
|
2254
|
+
const commandMessage = Updates.carryTelegramUpdateExecutionFence(message, {
|
|
2126
2255
|
...message,
|
|
2127
2256
|
text: commandText,
|
|
2128
2257
|
caption: undefined,
|
|
2129
|
-
} as TMessage;
|
|
2258
|
+
} as TMessage);
|
|
2130
2259
|
const rerouteId = storePendingUnboundReroute([commandMessage], "command");
|
|
2131
2260
|
Updates.reportTelegramUpdateDeferred(commandMessage);
|
|
2132
2261
|
const text = formatTelegramAllTabMenuChooserText(command.name);
|
|
@@ -2135,49 +2264,52 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2135
2264
|
activeRecords,
|
|
2136
2265
|
{ canRestore: typeof message.message_thread_id === "number" },
|
|
2137
2266
|
);
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
? {
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
}
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2267
|
+
let chooserId: number | undefined;
|
|
2268
|
+
try {
|
|
2269
|
+
if (deps.sendInteractiveMessage) {
|
|
2270
|
+
chooserId = await deps.sendInteractiveMessage(
|
|
2271
|
+
message.chat.id,
|
|
2272
|
+
text,
|
|
2273
|
+
"html",
|
|
2274
|
+
replyMarkup,
|
|
2275
|
+
options.target || options.replyToSource
|
|
2276
|
+
? {
|
|
2277
|
+
...(options.target ? { target: options.target } : {}),
|
|
2278
|
+
...(options.replyToSource
|
|
2279
|
+
? { replyToMessageId: message.message_id }
|
|
2280
|
+
: {}),
|
|
2281
|
+
}
|
|
2282
|
+
: undefined,
|
|
2283
|
+
);
|
|
2284
|
+
} else if (deps.callApi) {
|
|
2285
|
+
const chooser = await deps.callApi<{ message_id?: number }>("sendMessage", {
|
|
2286
|
+
chat_id: message.chat.id,
|
|
2287
|
+
text,
|
|
2288
|
+
parse_mode: "HTML",
|
|
2289
|
+
reply_markup: replyMarkup,
|
|
2290
|
+
...(typeof options.target?.threadId === "number"
|
|
2291
|
+
? { message_thread_id: options.target.threadId }
|
|
2292
|
+
: {}),
|
|
2293
|
+
...(options.replyToSource
|
|
2294
|
+
? {
|
|
2295
|
+
reply_parameters: {
|
|
2296
|
+
message_id: message.message_id,
|
|
2297
|
+
allow_sending_without_reply: true,
|
|
2298
|
+
},
|
|
2299
|
+
}
|
|
2300
|
+
: {}),
|
|
2301
|
+
});
|
|
2302
|
+
chooserId = chooser?.message_id;
|
|
2303
|
+
} else {
|
|
2304
|
+
chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
|
|
2305
|
+
parseMode: "HTML",
|
|
2306
|
+
target: options.target,
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
removePendingReroute(rerouteId);
|
|
2311
|
+
throw error;
|
|
2176
2312
|
}
|
|
2177
|
-
const chooserId = await deps.sendTextReply(message.chat.id, message.message_id, text, {
|
|
2178
|
-
parseMode: "HTML",
|
|
2179
|
-
target: options.target,
|
|
2180
|
-
});
|
|
2181
2313
|
rememberRerouteChooser(rerouteId, chooserId);
|
|
2182
2314
|
return true;
|
|
2183
2315
|
};
|
|
@@ -2497,6 +2629,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2497
2629
|
deps.getLiveThreadTargets?.(),
|
|
2498
2630
|
);
|
|
2499
2631
|
const command = getKnownTelegramAllTabCommand(text);
|
|
2632
|
+
// Returning before deferral lets the admission worker terminally settle expired replay.
|
|
2633
|
+
if (command && command.name !== "thread" && isTelegramAllTabCommandExpired(message)) return;
|
|
2500
2634
|
if (bindings.length > 0 && command && command.name !== "thread") {
|
|
2501
2635
|
if (
|
|
2502
2636
|
await sendAllTabCommandChooser(command, text, message as TMessage, {
|