@llblab/pi-telegram 0.50.1 → 0.51.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 +10 -8
- package/BACKLOG.md +13 -32
- package/CHANGELOG.md +16 -0
- package/README.md +4 -3
- package/dist/lib/bindings.d.ts +4 -2
- package/dist/lib/bindings.js +5 -5
- package/dist/lib/bus-follower.d.ts +4 -2
- package/dist/lib/bus-follower.js +99 -10
- package/dist/lib/bus-leader.d.ts +9 -3
- package/dist/lib/bus-leader.js +231 -71
- package/dist/lib/bus.d.ts +4 -0
- package/dist/lib/bus.js +34 -7
- package/dist/lib/commands.d.ts +5 -0
- package/dist/lib/commands.js +9 -0
- package/dist/lib/extension.js +68 -3
- package/dist/lib/journal.d.ts +3 -0
- package/dist/lib/journal.js +13 -0
- package/dist/lib/lifecycle.d.ts +2 -0
- package/dist/lib/lifecycle.js +25 -4
- package/dist/lib/locks.d.ts +1 -0
- package/dist/lib/locks.js +32 -12
- package/dist/lib/paths.d.ts +10 -0
- package/dist/lib/paths.js +22 -1
- package/dist/lib/polling.js +2 -2
- package/dist/lib/queue.d.ts +9 -1
- package/dist/lib/queue.js +64 -13
- package/dist/lib/replies.d.ts +4 -0
- package/dist/lib/replies.js +14 -0
- package/dist/lib/routing.d.ts +1 -0
- package/dist/lib/routing.js +5 -0
- package/dist/lib/sync.d.ts +9 -0
- package/dist/lib/sync.js +41 -1
- package/dist/lib/telegram-api.d.ts +9 -1
- package/dist/lib/telegram-api.js +23 -3
- package/dist/lib/thread-cleanup-manager.js +7 -2
- package/dist/lib/threads.d.ts +2 -0
- package/dist/lib/threads.js +68 -37
- package/dist/lib/updates.d.ts +6 -4
- package/dist/lib/updates.js +134 -61
- package/dist/lib/workspace-admission.d.ts +9 -0
- package/dist/lib/workspace-admission.js +49 -3
- package/dist/lib/workspace-retirement.d.ts +77 -6
- package/dist/lib/workspace-retirement.js +384 -8
- package/dist/lib/workspace-slots.d.ts +3 -0
- package/dist/lib/workspace-slots.js +6 -0
- package/dist/package.json +1 -1
- package/docs/architecture.md +22 -15
- package/docs/multi-instance-bus.md +32 -11
- package/docs/public-api.md +1 -1
- package/docs/updates.md +2 -0
- package/lib/bindings.ts +12 -7
- package/lib/bus-follower.ts +82 -14
- package/lib/bus-leader.ts +236 -94
- package/lib/bus.ts +39 -9
- package/lib/commands.ts +19 -0
- package/lib/extension.ts +78 -2
- package/lib/journal.ts +14 -0
- package/lib/lifecycle.ts +23 -4
- package/lib/locks.ts +32 -13
- package/lib/paths.ts +29 -1
- package/lib/polling.ts +2 -2
- package/lib/queue.ts +77 -19
- package/lib/replies.ts +18 -0
- package/lib/routing.ts +7 -0
- package/lib/sync.ts +45 -1
- package/lib/telegram-api.ts +30 -2
- package/lib/thread-cleanup-manager.ts +11 -3
- package/lib/threads.ts +73 -36
- package/lib/updates.ts +145 -78
- package/lib/workspace-admission.ts +67 -4
- package/lib/workspace-retirement.ts +465 -7
- package/lib/workspace-slots.ts +7 -0
- package/package.json +1 -1
package/dist/lib/queue.js
CHANGED
|
@@ -168,6 +168,18 @@ export function createTelegramActiveTurnStore() {
|
|
|
168
168
|
clear: () => {
|
|
169
169
|
activeTurn = undefined;
|
|
170
170
|
},
|
|
171
|
+
markNextAbortAnnouncement: () => {
|
|
172
|
+
if (!activeTurn)
|
|
173
|
+
return false;
|
|
174
|
+
activeTurn.announceNextAbortOnEnd = true;
|
|
175
|
+
return true;
|
|
176
|
+
},
|
|
177
|
+
clearNextAbortAnnouncement: () => {
|
|
178
|
+
if (!activeTurn?.announceNextAbortOnEnd)
|
|
179
|
+
return false;
|
|
180
|
+
delete activeTurn.announceNextAbortOnEnd;
|
|
181
|
+
return true;
|
|
182
|
+
},
|
|
171
183
|
getChatId: () => activeTurn?.chatId,
|
|
172
184
|
getTarget: () => activeTurn?.target ? { ...activeTurn.target } : undefined,
|
|
173
185
|
getReplyToMessageId: () => activeTurn?.replyToMessageId,
|
|
@@ -815,6 +827,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
815
827
|
kind: "no-turn",
|
|
816
828
|
shouldClearPreview: false,
|
|
817
829
|
shouldDispatchNext,
|
|
830
|
+
shouldSendAbortMessage: false,
|
|
818
831
|
shouldSendErrorMessage: false,
|
|
819
832
|
shouldSendAttachmentNotice: false,
|
|
820
833
|
};
|
|
@@ -824,6 +837,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
824
837
|
kind: "aborted",
|
|
825
838
|
shouldClearPreview: true,
|
|
826
839
|
shouldDispatchNext,
|
|
840
|
+
shouldSendAbortMessage: options.announceNextAbortOnEnd === true,
|
|
827
841
|
shouldSendErrorMessage: false,
|
|
828
842
|
shouldSendAttachmentNotice: false,
|
|
829
843
|
};
|
|
@@ -833,6 +847,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
833
847
|
kind: "error",
|
|
834
848
|
shouldClearPreview: true,
|
|
835
849
|
shouldDispatchNext,
|
|
850
|
+
shouldSendAbortMessage: false,
|
|
836
851
|
shouldSendErrorMessage: true,
|
|
837
852
|
shouldSendAttachmentNotice: false,
|
|
838
853
|
};
|
|
@@ -842,6 +857,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
842
857
|
kind: "text",
|
|
843
858
|
shouldClearPreview: false,
|
|
844
859
|
shouldDispatchNext,
|
|
860
|
+
shouldSendAbortMessage: false,
|
|
845
861
|
shouldSendErrorMessage: false,
|
|
846
862
|
shouldSendAttachmentNotice: false,
|
|
847
863
|
};
|
|
@@ -851,6 +867,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
851
867
|
kind: "attachments-only",
|
|
852
868
|
shouldClearPreview: true,
|
|
853
869
|
shouldDispatchNext,
|
|
870
|
+
shouldSendAbortMessage: false,
|
|
854
871
|
shouldSendErrorMessage: false,
|
|
855
872
|
shouldSendAttachmentNotice: true,
|
|
856
873
|
};
|
|
@@ -859,6 +876,7 @@ export function buildTelegramAgentEndPlan(options) {
|
|
|
859
876
|
kind: "empty",
|
|
860
877
|
shouldClearPreview: true,
|
|
861
878
|
shouldDispatchNext,
|
|
879
|
+
shouldSendAbortMessage: false,
|
|
862
880
|
shouldSendErrorMessage: false,
|
|
863
881
|
shouldSendAttachmentNotice: false,
|
|
864
882
|
};
|
|
@@ -869,8 +887,7 @@ export function createTelegramAgentEndHook(deps) {
|
|
|
869
887
|
return;
|
|
870
888
|
const turn = deps.getActiveTurn();
|
|
871
889
|
const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
|
|
872
|
-
const assistant = extractedAssistant
|
|
873
|
-
deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
|
|
890
|
+
const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
|
|
874
891
|
? { stopReason: extractedAssistant.stopReason }
|
|
875
892
|
: extractedAssistant;
|
|
876
893
|
const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
|
|
@@ -992,6 +1009,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
992
1009
|
hasFinalText: !!finalText || hasOutboundArtifacts,
|
|
993
1010
|
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
994
1011
|
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1012
|
+
announceNextAbortOnEnd: turn?.announceNextAbortOnEnd === true,
|
|
995
1013
|
});
|
|
996
1014
|
if (!turn) {
|
|
997
1015
|
if (endPlan.shouldDispatchNext)
|
|
@@ -1117,17 +1135,26 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1117
1135
|
if (!isDeliveryActive())
|
|
1118
1136
|
return;
|
|
1119
1137
|
}
|
|
1120
|
-
if (endPlan.shouldSendErrorMessage) {
|
|
1138
|
+
if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
|
|
1121
1139
|
const errorMessage = assistant.errorMessage ||
|
|
1122
1140
|
"Telegram bridge: Pi failed while processing the request.";
|
|
1123
|
-
const isOperationAborted =
|
|
1124
|
-
"This operation was aborted";
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1141
|
+
const isOperationAborted = endPlan.shouldSendAbortMessage ||
|
|
1142
|
+
errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
|
|
1143
|
+
try {
|
|
1144
|
+
await deps.sendTextReply(turn.chatId, turn.replyToMessageId, isOperationAborted
|
|
1145
|
+
? "<b>⏹️ This operation was aborted.</b>"
|
|
1146
|
+
: errorMessage, {
|
|
1147
|
+
target: turn.target,
|
|
1148
|
+
...(isOperationAborted ? { parseMode: "HTML" } : {}),
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
if (!isOperationAborted)
|
|
1153
|
+
throw error;
|
|
1154
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
1155
|
+
phase: "next-abort-announcement",
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1131
1158
|
if (!isDeliveryActive())
|
|
1132
1159
|
return;
|
|
1133
1160
|
if (endPlan.shouldDispatchNext)
|
|
@@ -1268,7 +1295,7 @@ export async function handleTelegramAgentEndRuntime(deps) {
|
|
|
1268
1295
|
deps.dispatchNextQueuedTelegramTurn();
|
|
1269
1296
|
};
|
|
1270
1297
|
if (deps.scheduleActiveTurnDelivery &&
|
|
1271
|
-
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
|
|
1298
|
+
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)) {
|
|
1272
1299
|
deps.scheduleActiveTurnDelivery(deliverActiveTurn);
|
|
1273
1300
|
return;
|
|
1274
1301
|
}
|
|
@@ -1766,6 +1793,7 @@ export function createTelegramQueueDispatchRuntime(deps) {
|
|
|
1766
1793
|
commitPromptDispatch: deps.commitPromptDispatch,
|
|
1767
1794
|
sendUserMessage: deps.sendUserMessage,
|
|
1768
1795
|
onPromptDispatchFailure: deps.onPromptDispatchFailure,
|
|
1796
|
+
reconcileNextDispatchAnnouncementReplyOwnership: deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
1769
1797
|
isQueueItemTransportActive: deps.isQueueItemTransportActive,
|
|
1770
1798
|
hasPendingInboundQueueMutationForItem: deps.hasPendingInboundQueueMutationForItem,
|
|
1771
1799
|
isQueueItemAdmissionReady: deps.isQueueItemAdmissionReady,
|
|
@@ -1777,12 +1805,19 @@ export function createTelegramQueueDispatchRuntime(deps) {
|
|
|
1777
1805
|
export function createTelegramQueueDispatchController(deps) {
|
|
1778
1806
|
let controlDispatchPending = false;
|
|
1779
1807
|
let nextDispatchAnnouncementRequested = false;
|
|
1808
|
+
let nextDispatchAnnouncementGeneration = 0;
|
|
1780
1809
|
let nextDispatchAnnouncementAnchor;
|
|
1781
1810
|
const controller = {
|
|
1782
1811
|
requestNextDispatchAnnouncement: () => {
|
|
1812
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
1783
1813
|
nextDispatchAnnouncementRequested = true;
|
|
1784
1814
|
nextDispatchAnnouncementAnchor = undefined;
|
|
1785
1815
|
},
|
|
1816
|
+
cancelNextDispatchAnnouncement: () => {
|
|
1817
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
1818
|
+
nextDispatchAnnouncementRequested = false;
|
|
1819
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
1820
|
+
},
|
|
1786
1821
|
dispatchNext: (ctx) => {
|
|
1787
1822
|
if (deps.hasDispatchContext && !deps.hasDispatchContext())
|
|
1788
1823
|
return;
|
|
@@ -1973,11 +2008,22 @@ export function createTelegramQueueDispatchController(deps) {
|
|
|
1973
2008
|
if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
|
|
1974
2009
|
nextDispatchAnnouncementRequested = false;
|
|
1975
2010
|
controlDispatchPending = true;
|
|
2011
|
+
const announcementGeneration = nextDispatchAnnouncementGeneration;
|
|
1976
2012
|
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
1977
2013
|
deps.updateStatus(ctx);
|
|
1978
2014
|
void deps.sendTextReply(dispatchPlan.item.chatId, dispatchPlan.item.replyToMessageId, "<b>⏩ Dispatching next queued turn.</b>", { target: dispatchPlan.item.target }).catch((error) => {
|
|
1979
|
-
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2015
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2016
|
+
phase: "next-announcement",
|
|
2017
|
+
});
|
|
1980
2018
|
}).finally(() => {
|
|
2019
|
+
try {
|
|
2020
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership?.(dispatchPlan.item);
|
|
2021
|
+
}
|
|
2022
|
+
catch (error) {
|
|
2023
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2024
|
+
phase: "next-announcement-reply-ownership",
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
1981
2027
|
controlDispatchPending = false;
|
|
1982
2028
|
if (deps.hasDispatchContext && !deps.hasDispatchContext())
|
|
1983
2029
|
return;
|
|
@@ -1985,6 +2031,11 @@ export function createTelegramQueueDispatchController(deps) {
|
|
|
1985
2031
|
deps.isDispatchGenerationActive &&
|
|
1986
2032
|
!deps.isDispatchGenerationActive(dispatchGeneration))
|
|
1987
2033
|
return;
|
|
2034
|
+
if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
|
|
2035
|
+
if (nextDispatchAnnouncementRequested)
|
|
2036
|
+
controller.dispatchNext(ctx);
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
1988
2039
|
executePlan();
|
|
1989
2040
|
});
|
|
1990
2041
|
return;
|
package/dist/lib/replies.d.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface ReplyDedupRuntime {
|
|
|
23
23
|
}
|
|
24
24
|
export declare function createReplyDedupRuntime(): ReplyDedupRuntime;
|
|
25
25
|
export declare function resetTransportReplyDedup(): void;
|
|
26
|
+
/** Keeps a successfully published transition notice as the first reply of the
|
|
27
|
+
* next agent turn. The following agent-start reset consumes this one-shot
|
|
28
|
+
* preservation, so later messages in that turn do not repeat the reply header. */
|
|
29
|
+
export declare function preserveTransportReplyDedupOnNextReset(chatId: number, messageId: number, target?: TelegramTarget): void;
|
|
26
30
|
export declare function buildTelegramReplyParameters(chatId: number, messageId: number | undefined, target?: TelegramTarget): TelegramReplyParameters | undefined;
|
|
27
31
|
export declare function withTelegramReplyParameters<T>(chatId: number, messageId: number | undefined, target: TelegramTarget | undefined, send: (parameters: TelegramReplyParameters | undefined) => Promise<T>): Promise<T>;
|
|
28
32
|
export declare function isAssistantAgentMessage(message: unknown): boolean;
|
package/dist/lib/replies.js
CHANGED
|
@@ -31,6 +31,7 @@ export function createReplyDedupRuntime() {
|
|
|
31
31
|
}
|
|
32
32
|
// --- Transport-level dedup ---
|
|
33
33
|
const lastRepliedToMessageIdByTarget = new Map();
|
|
34
|
+
const replyDedupPreservedOnNextReset = new Map();
|
|
34
35
|
let replyDedupGeneration = 0;
|
|
35
36
|
function getReplyDedupTargetKey(chatId, target) {
|
|
36
37
|
const threadId = target?.threadId;
|
|
@@ -41,6 +42,19 @@ function getReplyDedupTargetKey(chatId, target) {
|
|
|
41
42
|
export function resetTransportReplyDedup() {
|
|
42
43
|
replyDedupGeneration += 1;
|
|
43
44
|
lastRepliedToMessageIdByTarget.clear();
|
|
45
|
+
for (const [key, messageId] of replyDedupPreservedOnNextReset) {
|
|
46
|
+
lastRepliedToMessageIdByTarget.set(key, messageId);
|
|
47
|
+
}
|
|
48
|
+
replyDedupPreservedOnNextReset.clear();
|
|
49
|
+
}
|
|
50
|
+
/** Keeps a successfully published transition notice as the first reply of the
|
|
51
|
+
* next agent turn. The following agent-start reset consumes this one-shot
|
|
52
|
+
* preservation, so later messages in that turn do not repeat the reply header. */
|
|
53
|
+
export function preserveTransportReplyDedupOnNextReset(chatId, messageId, target) {
|
|
54
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
55
|
+
if (lastRepliedToMessageIdByTarget.get(key) !== messageId)
|
|
56
|
+
return;
|
|
57
|
+
replyDedupPreservedOnNextReset.set(key, messageId);
|
|
44
58
|
}
|
|
45
59
|
export function buildTelegramReplyParameters(chatId, messageId, target) {
|
|
46
60
|
if (messageId === undefined || messageId <= 0)
|
package/dist/lib/routing.d.ts
CHANGED
|
@@ -122,6 +122,7 @@ export interface TelegramInboundRouteRuntimeDeps<TMessage extends TelegramRouted
|
|
|
122
122
|
isContextActive?: (ctx: TContext) => boolean;
|
|
123
123
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
124
124
|
requestNextDispatchAnnouncement?: () => void;
|
|
125
|
+
cancelNextDispatchAnnouncement?: () => void;
|
|
125
126
|
requestDeferredDispatchNextQueuedTelegramTurn?: (dispatch: (ctx: TContext) => void) => void;
|
|
126
127
|
hasDeferredDispatchContext?: () => boolean;
|
|
127
128
|
startTypingLoop?: (ctx: TContext, chatId?: number, options?: {
|
package/dist/lib/routing.js
CHANGED
|
@@ -1418,6 +1418,10 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1418
1418
|
isContextActive: deps.isContextActive,
|
|
1419
1419
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
1420
1420
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
1421
|
+
cancelNextTransitionAnnouncements: () => {
|
|
1422
|
+
deps.activeTurnRuntime.clearNextAbortAnnouncement();
|
|
1423
|
+
deps.cancelNextDispatchAnnouncement?.();
|
|
1424
|
+
},
|
|
1421
1425
|
requestDeferredDispatchNextQueuedTelegramTurn: deps.requestDeferredDispatchNextQueuedTelegramTurn,
|
|
1422
1426
|
startTypingLoop: deps.startTypingLoop,
|
|
1423
1427
|
stopTypingLoop: deps.stopTypingLoop,
|
|
@@ -1476,6 +1480,7 @@ export function createTelegramInboundRouteRuntime(deps) {
|
|
|
1476
1480
|
},
|
|
1477
1481
|
getPromptTemplateCommands,
|
|
1478
1482
|
sendTextReply: deps.sendTextReply,
|
|
1483
|
+
markActiveTurnNextAbortAnnouncement: deps.activeTurnRuntime.markNextAbortAnnouncement,
|
|
1479
1484
|
getActiveTurnReply: () => {
|
|
1480
1485
|
const activeTurn = deps.activeTurnRuntime.get();
|
|
1481
1486
|
if (!activeTurn)
|
package/dist/lib/sync.d.ts
CHANGED
|
@@ -97,6 +97,15 @@ export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
|
97
97
|
export declare function markTelegramConfigSyncChange<TSyncState extends TelegramSyncState>(state: TSyncState, action: string, options?: {
|
|
98
98
|
nowMs?: number;
|
|
99
99
|
}): TSyncState;
|
|
100
|
+
export declare function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
101
|
+
instanceId: string;
|
|
102
|
+
topicTargetStore: Pick<TelegramTopicTargetStore, "load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
|
|
103
|
+
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
104
|
+
getProfileName: () => string | undefined;
|
|
105
|
+
isPollingSuspended: () => boolean;
|
|
106
|
+
resolveAutomaticThreadCleanupEnabled: () => boolean | Promise<boolean>;
|
|
107
|
+
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
108
|
+
}): (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined;
|
|
100
109
|
export interface TelegramSessionRestartThreadCleanupDeps<TSyncState extends TelegramSyncState> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
|
|
101
110
|
suspendPolling: () => Promise<void>;
|
|
102
111
|
}
|
package/dist/lib/sync.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.js";
|
|
7
7
|
import { getTelegramTargetKey } from "./target.js";
|
|
8
8
|
import * as ThreadReconciler from "./thread-reconciler.js";
|
|
9
|
+
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
|
|
9
10
|
import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
|
|
10
11
|
import { createTelegramCleanupTargetProtection, commitTelegramWorkspaceProvisionBinding, getTelegramTargetFromApiBody, getTelegramThreadOwnerKey, isSameTelegramProcessInstance, isTelegramTopicTargetStaleError, normalizeTelegramWorkspacePath, provisionOwnBusTopic, } from "./threads.js";
|
|
11
12
|
export function markTelegramConfigSyncChange(state, action, options) {
|
|
@@ -24,6 +25,42 @@ export function markTelegramConfigSyncChange(state, action, options) {
|
|
|
24
25
|
});
|
|
25
26
|
return nextState;
|
|
26
27
|
}
|
|
28
|
+
export function createTelegramPreservedLeaderQuitHandler(deps) {
|
|
29
|
+
return (isSessionCurrent) => {
|
|
30
|
+
const epoch = deps.getCurrentLeaderEpoch();
|
|
31
|
+
const profile = deps.getProfileName();
|
|
32
|
+
if (epoch === undefined || !isSessionCurrent())
|
|
33
|
+
return undefined;
|
|
34
|
+
const isCurrent = () => isSessionCurrent() && deps.getCurrentLeaderEpoch() === epoch &&
|
|
35
|
+
deps.getProfileName() === profile && deps.isPollingSuspended();
|
|
36
|
+
return async () => {
|
|
37
|
+
if (!isCurrent() || await deps.resolveAutomaticThreadCleanupEnabled() !== false)
|
|
38
|
+
return;
|
|
39
|
+
if (!isCurrent())
|
|
40
|
+
return;
|
|
41
|
+
await deps.runWorkspaceOperation({
|
|
42
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
43
|
+
operationKind: "workspace.preserve-leader-quit",
|
|
44
|
+
scopes: [{ kind: "profile" }],
|
|
45
|
+
}, async () => {
|
|
46
|
+
if (!isCurrent())
|
|
47
|
+
return;
|
|
48
|
+
await deps.topicTargetStore.load();
|
|
49
|
+
if (!isCurrent())
|
|
50
|
+
return;
|
|
51
|
+
const records = deps.topicTargetStore.list().filter((record) => record.instanceId === deps.instanceId && (record.status === "active" || record.status === "starting"));
|
|
52
|
+
if (records.length !== 1)
|
|
53
|
+
return;
|
|
54
|
+
const record = records[0];
|
|
55
|
+
if (deps.topicTargetStore.listPendingCleanups().some((intent) => intent.target.chatId === record.target.chatId && intent.target.threadId === record.target.threadId))
|
|
56
|
+
return;
|
|
57
|
+
if (!await deps.topicTargetStore.detachTargetOwner(record, isCurrent) && isCurrent()) {
|
|
58
|
+
throw new Error("Telegram preserved leader detachment was not committed.");
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|
|
27
64
|
export function createTelegramSessionRestartThreadCleanupHandler(deps) {
|
|
28
65
|
return createTelegramManualThreadDisconnectHandler({
|
|
29
66
|
...deps,
|
|
@@ -353,10 +390,13 @@ export async function ensureTelegramLeaderThreadBinding(deps) {
|
|
|
353
390
|
};
|
|
354
391
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
355
392
|
const legacyLeaderRecord = deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
393
|
+
let capacityUnavailable = false;
|
|
356
394
|
const workspaceIdentity = normalizedLeaderCwd
|
|
357
|
-
? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId })
|
|
395
|
+
? deps.topicTargetStore.claimWorkspaceIdentity(normalizedLeaderCwd, deps.instanceId, legacyLeaderRecord?.instanceId, { sessionId: deps.sessionId, onCapacityUnavailable() { capacityUnavailable = true; } })
|
|
358
396
|
: undefined;
|
|
359
397
|
if (deps.cwd && !workspaceIdentity) {
|
|
398
|
+
if (capacityUnavailable)
|
|
399
|
+
throw new TelegramWorkspaceSlotUnavailableError();
|
|
360
400
|
throw new Error("Telegram Workspace identity is already claimed.");
|
|
361
401
|
}
|
|
362
402
|
const commitWorkspaceBinding = async (result) => {
|
|
@@ -484,6 +484,8 @@ export declare function getTelegramApiErrorRequestTarget(error: unknown): {
|
|
|
484
484
|
threadId: number;
|
|
485
485
|
} | undefined;
|
|
486
486
|
export declare function isTelegramStaleTargetHttpError(error: unknown): boolean;
|
|
487
|
+
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
488
|
+
export declare function isTelegramApiRequestRejected(error: unknown, method: string): boolean;
|
|
487
489
|
export declare function isTelegramMessageNotModifiedError(error: unknown): boolean;
|
|
488
490
|
export declare function isTelegramApiMethodRetrySafe(method: string): boolean;
|
|
489
491
|
export declare function isRetryableTelegramApiError(error: unknown): boolean;
|
|
@@ -522,13 +524,19 @@ export declare function createTelegramAssistantDraftSender(deps: {
|
|
|
522
524
|
sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
|
|
523
525
|
}): TelegramBridgeApiRuntime["sendMessageDraft"];
|
|
524
526
|
export declare function buildTelegramAnswerGuestQueryBody(guestQueryId: string, text?: string, options?: TelegramAnswerGuestQueryOptions): Record<string, unknown>;
|
|
527
|
+
export type TelegramWorkspaceThreadDeletionTransport = (authorize: () => {
|
|
528
|
+
chatId: number;
|
|
529
|
+
threadId: number;
|
|
530
|
+
}) => Promise<void>;
|
|
525
531
|
export declare function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
526
532
|
getBotToken: () => string | undefined;
|
|
527
533
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
528
534
|
captureRequestErrorHandler?: TelegramBridgeApiRuntimeDeps["captureRequestErrorHandler"];
|
|
529
535
|
targetActivity?: TelegramApiTargetActivityRuntime;
|
|
530
536
|
workspaceAdmission?: TelegramApiWorkspaceAdmissionPort | (() => TelegramApiWorkspaceAdmissionPort | undefined);
|
|
531
|
-
}): TelegramBridgeApiRuntime
|
|
537
|
+
}): TelegramBridgeApiRuntime & {
|
|
538
|
+
deleteWorkspaceThread: TelegramWorkspaceThreadDeletionTransport;
|
|
539
|
+
};
|
|
532
540
|
export declare function createTelegramBridgeApiRuntime(deps: TelegramBridgeApiRuntimeDeps): TelegramBridgeApiRuntime;
|
|
533
541
|
/**
|
|
534
542
|
* Creates a low-level Telegram Bot API client.
|
package/dist/lib/telegram-api.js
CHANGED
|
@@ -231,11 +231,13 @@ export function isTelegramApiCommitUnknownError(error) {
|
|
|
231
231
|
class TelegramApiHttpError extends Error {
|
|
232
232
|
status;
|
|
233
233
|
retryAfterSeconds;
|
|
234
|
+
rejectedRequestMethod;
|
|
234
235
|
requestTarget;
|
|
235
|
-
constructor(message, status, retryAfterSeconds) {
|
|
236
|
+
constructor(message, status, retryAfterSeconds, rejectedRequestMethod) {
|
|
236
237
|
super(message);
|
|
237
238
|
this.status = status;
|
|
238
239
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
240
|
+
this.rejectedRequestMethod = rejectedRequestMethod;
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
function attachTelegramApiRequestTarget(error, body) {
|
|
@@ -267,6 +269,11 @@ export function isTelegramStaleTargetHttpError(error) {
|
|
|
267
269
|
return false;
|
|
268
270
|
return /^Telegram API \w+ failed: HTTP 400: Bad Request: (message thread not found|thread not found|topic not found|topic deleted|topic closed|thread closed|forum topic closed|message thread closed|topic_id_invalid|topic_closed)$/i.test(error.message);
|
|
269
271
|
}
|
|
272
|
+
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
273
|
+
export function isTelegramApiRequestRejected(error, method) {
|
|
274
|
+
return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
|
|
275
|
+
error.rejectedRequestMethod === method;
|
|
276
|
+
}
|
|
270
277
|
export function isTelegramMessageNotModifiedError(error) {
|
|
271
278
|
return (error instanceof Error && error.message.includes("message is not modified"));
|
|
272
279
|
}
|
|
@@ -398,7 +405,8 @@ async function parseTelegramApiResponse(response, method) {
|
|
|
398
405
|
const retryAfterHeader = response.headers?.get("retry-after");
|
|
399
406
|
const retryAfterSeconds = data?.parameters?.retry_after ??
|
|
400
407
|
(retryAfterHeader ? Number.parseInt(retryAfterHeader, 10) : undefined);
|
|
401
|
-
throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined
|
|
408
|
+
throw new TelegramApiHttpError(`Telegram API ${method} failed: ${status}${description}`, response.status, Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined, data?.ok === false && data.error_code === response.status &&
|
|
409
|
+
[400, 401, 403, 404, 429].includes(response.status) ? method : undefined);
|
|
402
410
|
}
|
|
403
411
|
if (!data) {
|
|
404
412
|
throw new TelegramApiMalformedSuccessError(method, "returned invalid JSON");
|
|
@@ -856,7 +864,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
|
|
|
856
864
|
},
|
|
857
865
|
})
|
|
858
866
|
: client;
|
|
859
|
-
|
|
867
|
+
const runtime = createTelegramBridgeApiRuntime({
|
|
860
868
|
client: deps.targetActivity
|
|
861
869
|
? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
|
|
862
870
|
: admittedClient,
|
|
@@ -866,6 +874,18 @@ export function createDefaultTelegramBridgeApiRuntime(deps) {
|
|
|
866
874
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
867
875
|
captureRequestErrorHandler: deps.captureRequestErrorHandler,
|
|
868
876
|
});
|
|
877
|
+
return {
|
|
878
|
+
...runtime,
|
|
879
|
+
async deleteWorkspaceThread(authorize) {
|
|
880
|
+
// The exclusive deletion permit replaces ordinary target admission.
|
|
881
|
+
const target = authorize();
|
|
882
|
+
const deleted = await client.call("deleteForumTopic", {
|
|
883
|
+
chat_id: target.chatId, message_thread_id: target.threadId,
|
|
884
|
+
}, { maxAttempts: 1, retrySafety: "non-idempotent" });
|
|
885
|
+
if (deleted !== true)
|
|
886
|
+
throw new Error("Telegram Workspace Thread deletion was not confirmed.");
|
|
887
|
+
},
|
|
888
|
+
};
|
|
869
889
|
}
|
|
870
890
|
export function createTelegramBridgeApiRuntime(deps) {
|
|
871
891
|
const recoverRequestError = async (handler, error) => {
|
|
@@ -342,7 +342,9 @@ export function createTelegramThreadCleanupWorkStore(options) {
|
|
|
342
342
|
return { confirmed: true, entry: structuredClone(entry) };
|
|
343
343
|
});
|
|
344
344
|
},
|
|
345
|
-
list() {
|
|
345
|
+
list() {
|
|
346
|
+
return structuredClone(withTelegramFileTransaction(`${options.path}.transaction`, () => read().workSets));
|
|
347
|
+
},
|
|
346
348
|
};
|
|
347
349
|
}
|
|
348
350
|
export async function commitTelegramInactiveThreadCleanup(input) {
|
|
@@ -401,7 +403,10 @@ export function createTelegramThreadCleanupPermitRuntime(deps) {
|
|
|
401
403
|
if (!deps.canAdoptFence(fence))
|
|
402
404
|
return undefined;
|
|
403
405
|
try {
|
|
404
|
-
|
|
406
|
+
const adopted = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
|
|
407
|
+
if (adopted.destructiveKind !== "manual-thread-cleanup")
|
|
408
|
+
return undefined;
|
|
409
|
+
fence = adopted;
|
|
405
410
|
}
|
|
406
411
|
catch {
|
|
407
412
|
return undefined;
|
package/dist/lib/threads.d.ts
CHANGED
|
@@ -212,6 +212,8 @@ export interface TelegramTopicTargetStore {
|
|
|
212
212
|
refresh?: () => Promise<void>;
|
|
213
213
|
persist: () => Promise<void>;
|
|
214
214
|
invalidateTarget: (target: TelegramTarget, isCurrent: () => boolean, lastSyncError: string) => Promise<boolean>;
|
|
215
|
+
/** Caller proves owner detachment; this does not assert Telegram Thread absence. */
|
|
216
|
+
detachTargetOwner: (expected: TelegramTopicTargetRecord, isCurrent: () => boolean) => Promise<boolean>;
|
|
215
217
|
list: () => TelegramTopicTargetRecord[];
|
|
216
218
|
getFollowerRecoveryHintByTarget?: (target: TelegramTarget) => {
|
|
217
219
|
slot?: string;
|