@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/lib/queue.ts
CHANGED
|
@@ -126,6 +126,8 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
126
126
|
historyText: string;
|
|
127
127
|
priorityEmoji?: string;
|
|
128
128
|
reactionSuppressionEmoji?: string;
|
|
129
|
+
/** Emit the explicit aborted-turn notice when /next settles this active turn. */
|
|
130
|
+
announceNextAbortOnEnd?: boolean;
|
|
129
131
|
|
|
130
132
|
/** Turn should preferably be delivered as voice (mirror mode + user sent voice) */
|
|
131
133
|
voiceReplyPreferred?: boolean;
|
|
@@ -235,6 +237,8 @@ export interface TelegramActiveTurnStore<
|
|
|
235
237
|
has: () => boolean;
|
|
236
238
|
set: (turn: TTurn) => void;
|
|
237
239
|
clear: () => void;
|
|
240
|
+
markNextAbortAnnouncement: () => boolean;
|
|
241
|
+
clearNextAbortAnnouncement: () => boolean;
|
|
238
242
|
getChatId: () => number | undefined;
|
|
239
243
|
getTarget: () => TelegramQueueTarget | undefined;
|
|
240
244
|
getReplyToMessageId: () => number | undefined;
|
|
@@ -461,6 +465,16 @@ export function createTelegramActiveTurnStore<
|
|
|
461
465
|
clear: () => {
|
|
462
466
|
activeTurn = undefined;
|
|
463
467
|
},
|
|
468
|
+
markNextAbortAnnouncement: () => {
|
|
469
|
+
if (!activeTurn) return false;
|
|
470
|
+
activeTurn.announceNextAbortOnEnd = true;
|
|
471
|
+
return true;
|
|
472
|
+
},
|
|
473
|
+
clearNextAbortAnnouncement: () => {
|
|
474
|
+
if (!activeTurn?.announceNextAbortOnEnd) return false;
|
|
475
|
+
delete activeTurn.announceNextAbortOnEnd;
|
|
476
|
+
return true;
|
|
477
|
+
},
|
|
464
478
|
getChatId: () => activeTurn?.chatId,
|
|
465
479
|
getTarget: () =>
|
|
466
480
|
activeTurn?.target ? { ...activeTurn.target } : undefined,
|
|
@@ -1488,6 +1502,7 @@ export interface TelegramAgentEndPlan {
|
|
|
1488
1502
|
kind: "no-turn" | "aborted" | "error" | "text" | "attachments-only" | "empty";
|
|
1489
1503
|
shouldClearPreview: boolean;
|
|
1490
1504
|
shouldDispatchNext: boolean;
|
|
1505
|
+
shouldSendAbortMessage: boolean;
|
|
1491
1506
|
shouldSendErrorMessage: boolean;
|
|
1492
1507
|
shouldSendAttachmentNotice: boolean;
|
|
1493
1508
|
}
|
|
@@ -1615,7 +1630,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
|
|
|
1615
1630
|
extractAssistant: (
|
|
1616
1631
|
messages: readonly TMessage[],
|
|
1617
1632
|
) => TelegramAgentEndAssistantResult;
|
|
1618
|
-
|
|
1633
|
+
isAssistantAlreadyPublished?: (assistant: TelegramAgentEndAssistantResult) => boolean;
|
|
1619
1634
|
getFoldQueuedPromptsIntoHistory: () => boolean;
|
|
1620
1635
|
resetRuntimeState: () => void;
|
|
1621
1636
|
isSessionActive?: (ctx: TContext) => boolean;
|
|
@@ -1680,6 +1695,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1680
1695
|
hasFinalText: boolean;
|
|
1681
1696
|
hasQueuedAttachments: boolean;
|
|
1682
1697
|
foldQueuedPromptsIntoHistory: boolean;
|
|
1698
|
+
announceNextAbortOnEnd?: boolean;
|
|
1683
1699
|
}): TelegramAgentEndPlan {
|
|
1684
1700
|
const shouldDispatchNext = shouldDispatchAfterTelegramAgentEnd({
|
|
1685
1701
|
hasTurn: options.hasTurn,
|
|
@@ -1691,6 +1707,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1691
1707
|
kind: "no-turn",
|
|
1692
1708
|
shouldClearPreview: false,
|
|
1693
1709
|
shouldDispatchNext,
|
|
1710
|
+
shouldSendAbortMessage: false,
|
|
1694
1711
|
shouldSendErrorMessage: false,
|
|
1695
1712
|
shouldSendAttachmentNotice: false,
|
|
1696
1713
|
};
|
|
@@ -1700,6 +1717,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1700
1717
|
kind: "aborted",
|
|
1701
1718
|
shouldClearPreview: true,
|
|
1702
1719
|
shouldDispatchNext,
|
|
1720
|
+
shouldSendAbortMessage: options.announceNextAbortOnEnd === true,
|
|
1703
1721
|
shouldSendErrorMessage: false,
|
|
1704
1722
|
shouldSendAttachmentNotice: false,
|
|
1705
1723
|
};
|
|
@@ -1709,6 +1727,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1709
1727
|
kind: "error",
|
|
1710
1728
|
shouldClearPreview: true,
|
|
1711
1729
|
shouldDispatchNext,
|
|
1730
|
+
shouldSendAbortMessage: false,
|
|
1712
1731
|
shouldSendErrorMessage: true,
|
|
1713
1732
|
shouldSendAttachmentNotice: false,
|
|
1714
1733
|
};
|
|
@@ -1718,6 +1737,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1718
1737
|
kind: "text",
|
|
1719
1738
|
shouldClearPreview: false,
|
|
1720
1739
|
shouldDispatchNext,
|
|
1740
|
+
shouldSendAbortMessage: false,
|
|
1721
1741
|
shouldSendErrorMessage: false,
|
|
1722
1742
|
shouldSendAttachmentNotice: false,
|
|
1723
1743
|
};
|
|
@@ -1727,6 +1747,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1727
1747
|
kind: "attachments-only",
|
|
1728
1748
|
shouldClearPreview: true,
|
|
1729
1749
|
shouldDispatchNext,
|
|
1750
|
+
shouldSendAbortMessage: false,
|
|
1730
1751
|
shouldSendErrorMessage: false,
|
|
1731
1752
|
shouldSendAttachmentNotice: true,
|
|
1732
1753
|
};
|
|
@@ -1735,6 +1756,7 @@ export function buildTelegramAgentEndPlan(options: {
|
|
|
1735
1756
|
kind: "empty",
|
|
1736
1757
|
shouldClearPreview: true,
|
|
1737
1758
|
shouldDispatchNext,
|
|
1759
|
+
shouldSendAbortMessage: false,
|
|
1738
1760
|
shouldSendErrorMessage: false,
|
|
1739
1761
|
shouldSendAttachmentNotice: false,
|
|
1740
1762
|
};
|
|
@@ -1761,8 +1783,7 @@ export function createTelegramAgentEndHook<
|
|
|
1761
1783
|
if (deps.isSessionActive?.(ctx) === false) return;
|
|
1762
1784
|
const turn = deps.getActiveTurn();
|
|
1763
1785
|
const extractedAssistant = assistantOverride ?? (turn ? deps.extractAssistant(event.messages) : {});
|
|
1764
|
-
const assistant = extractedAssistant
|
|
1765
|
-
deps.isRecoveredAssistantAlreadyPublished?.(extractedAssistant)
|
|
1786
|
+
const assistant = deps.isAssistantAlreadyPublished?.(extractedAssistant)
|
|
1766
1787
|
? { stopReason: extractedAssistant.stopReason }
|
|
1767
1788
|
: extractedAssistant;
|
|
1768
1789
|
const hasPublication = !!assistant.text || assistant.stopReason === "error" || !!turn?.queuedAttachments.length;
|
|
@@ -1892,6 +1913,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
1892
1913
|
hasFinalText: !!finalText || hasOutboundArtifacts,
|
|
1893
1914
|
hasQueuedAttachments: (turn?.queuedAttachments.length ?? 0) > 0,
|
|
1894
1915
|
foldQueuedPromptsIntoHistory: deps.foldQueuedPromptsIntoHistory,
|
|
1916
|
+
announceNextAbortOnEnd: turn?.announceNextAbortOnEnd === true,
|
|
1895
1917
|
});
|
|
1896
1918
|
if (!turn) {
|
|
1897
1919
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
@@ -2018,22 +2040,29 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2018
2040
|
await clearTurnPreview();
|
|
2019
2041
|
if (!isDeliveryActive()) return;
|
|
2020
2042
|
}
|
|
2021
|
-
if (endPlan.shouldSendErrorMessage) {
|
|
2043
|
+
if (endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage) {
|
|
2022
2044
|
const errorMessage = assistant.errorMessage ||
|
|
2023
2045
|
"Telegram bridge: Pi failed while processing the request.";
|
|
2024
|
-
const isOperationAborted =
|
|
2025
|
-
"This operation was aborted";
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2046
|
+
const isOperationAborted = endPlan.shouldSendAbortMessage ||
|
|
2047
|
+
errorMessage.trim().replace(/\.$/, "") === "This operation was aborted";
|
|
2048
|
+
try {
|
|
2049
|
+
await deps.sendTextReply(
|
|
2050
|
+
turn.chatId,
|
|
2051
|
+
turn.replyToMessageId,
|
|
2052
|
+
isOperationAborted
|
|
2053
|
+
? "<b>⏹️ This operation was aborted.</b>"
|
|
2054
|
+
: errorMessage,
|
|
2055
|
+
{
|
|
2056
|
+
target: turn.target,
|
|
2057
|
+
...(isOperationAborted ? { parseMode: "HTML" as const } : {}),
|
|
2058
|
+
},
|
|
2059
|
+
);
|
|
2060
|
+
} catch (error) {
|
|
2061
|
+
if (!isOperationAborted) throw error;
|
|
2062
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
2063
|
+
phase: "next-abort-announcement",
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2037
2066
|
if (!isDeliveryActive()) return;
|
|
2038
2067
|
if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
|
|
2039
2068
|
return;
|
|
@@ -2181,7 +2210,7 @@ export async function handleTelegramAgentEndRuntime<
|
|
|
2181
2210
|
};
|
|
2182
2211
|
if (
|
|
2183
2212
|
deps.scheduleActiveTurnDelivery &&
|
|
2184
|
-
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
|
|
2213
|
+
(endPlan.kind === "text" || endPlan.kind === "attachments-only" || endPlan.shouldSendAbortMessage || endPlan.shouldSendErrorMessage || endPlan.shouldClearPreview)
|
|
2185
2214
|
) {
|
|
2186
2215
|
deps.scheduleActiveTurnDelivery(deliverActiveTurn);
|
|
2187
2216
|
return;
|
|
@@ -3093,6 +3122,9 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
3093
3122
|
) => boolean;
|
|
3094
3123
|
sendUserMessage: TelegramDispatchRuntimeDeps<TContext>["sendUserMessage"];
|
|
3095
3124
|
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
3125
|
+
reconcileNextDispatchAnnouncementReplyOwnership?: (
|
|
3126
|
+
item: PendingTelegramTurn,
|
|
3127
|
+
) => void;
|
|
3096
3128
|
isQueueItemTransportActive?: (item: TelegramQueueItem<TContext>) => boolean;
|
|
3097
3129
|
hasPendingInboundQueueMutationForItem?: (
|
|
3098
3130
|
item: TelegramQueueItem<TContext>,
|
|
@@ -3108,6 +3140,7 @@ export interface TelegramQueueDispatchControllerDeps<
|
|
|
3108
3140
|
export interface TelegramQueueDispatchController<TContext = unknown> {
|
|
3109
3141
|
dispatchNext: (ctx: TContext) => void;
|
|
3110
3142
|
requestNextDispatchAnnouncement: () => void;
|
|
3143
|
+
cancelNextDispatchAnnouncement: () => void;
|
|
3111
3144
|
}
|
|
3112
3145
|
|
|
3113
3146
|
export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
@@ -3162,6 +3195,8 @@ export function createTelegramQueueDispatchRuntime<TContext = unknown>(
|
|
|
3162
3195
|
commitPromptDispatch: deps.commitPromptDispatch,
|
|
3163
3196
|
sendUserMessage: deps.sendUserMessage,
|
|
3164
3197
|
onPromptDispatchFailure: deps.onPromptDispatchFailure,
|
|
3198
|
+
reconcileNextDispatchAnnouncementReplyOwnership:
|
|
3199
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership,
|
|
3165
3200
|
isQueueItemTransportActive: deps.isQueueItemTransportActive,
|
|
3166
3201
|
hasPendingInboundQueueMutationForItem:
|
|
3167
3202
|
deps.hasPendingInboundQueueMutationForItem,
|
|
@@ -3177,12 +3212,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3177
3212
|
): TelegramQueueDispatchController<TContext> {
|
|
3178
3213
|
let controlDispatchPending = false;
|
|
3179
3214
|
let nextDispatchAnnouncementRequested = false;
|
|
3215
|
+
let nextDispatchAnnouncementGeneration = 0;
|
|
3180
3216
|
let nextDispatchAnnouncementAnchor: TelegramQueueItem<TContext> | undefined;
|
|
3181
3217
|
const controller: TelegramQueueDispatchController<TContext> = {
|
|
3182
3218
|
requestNextDispatchAnnouncement: () => {
|
|
3219
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
3183
3220
|
nextDispatchAnnouncementRequested = true;
|
|
3184
3221
|
nextDispatchAnnouncementAnchor = undefined;
|
|
3185
3222
|
},
|
|
3223
|
+
cancelNextDispatchAnnouncement: () => {
|
|
3224
|
+
nextDispatchAnnouncementGeneration += 1;
|
|
3225
|
+
nextDispatchAnnouncementRequested = false;
|
|
3226
|
+
nextDispatchAnnouncementAnchor = undefined;
|
|
3227
|
+
},
|
|
3186
3228
|
dispatchNext: (ctx) => {
|
|
3187
3229
|
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3188
3230
|
if (controlDispatchPending) {
|
|
@@ -3394,6 +3436,7 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3394
3436
|
if (dispatchPlan.kind === "prompt" && nextDispatchAnnouncementRequested) {
|
|
3395
3437
|
nextDispatchAnnouncementRequested = false;
|
|
3396
3438
|
controlDispatchPending = true;
|
|
3439
|
+
const announcementGeneration = nextDispatchAnnouncementGeneration;
|
|
3397
3440
|
const dispatchGeneration = deps.getDispatchGeneration?.();
|
|
3398
3441
|
deps.updateStatus(ctx);
|
|
3399
3442
|
void deps.sendTextReply(
|
|
@@ -3402,8 +3445,19 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3402
3445
|
"<b>⏩ Dispatching next queued turn.</b>",
|
|
3403
3446
|
{ target: dispatchPlan.item.target },
|
|
3404
3447
|
).catch((error) => {
|
|
3405
|
-
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3448
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3449
|
+
phase: "next-announcement",
|
|
3450
|
+
});
|
|
3406
3451
|
}).finally(() => {
|
|
3452
|
+
try {
|
|
3453
|
+
deps.reconcileNextDispatchAnnouncementReplyOwnership?.(
|
|
3454
|
+
dispatchPlan.item,
|
|
3455
|
+
);
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
deps.recordRuntimeEvent?.("dispatch", error, {
|
|
3458
|
+
phase: "next-announcement-reply-ownership",
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3407
3461
|
controlDispatchPending = false;
|
|
3408
3462
|
if (deps.hasDispatchContext && !deps.hasDispatchContext()) return;
|
|
3409
3463
|
if (
|
|
@@ -3411,6 +3465,10 @@ export function createTelegramQueueDispatchController<TContext = unknown>(
|
|
|
3411
3465
|
deps.isDispatchGenerationActive &&
|
|
3412
3466
|
!deps.isDispatchGenerationActive(dispatchGeneration)
|
|
3413
3467
|
) return;
|
|
3468
|
+
if (announcementGeneration !== nextDispatchAnnouncementGeneration) {
|
|
3469
|
+
if (nextDispatchAnnouncementRequested) controller.dispatchNext(ctx);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3414
3472
|
executePlan();
|
|
3415
3473
|
});
|
|
3416
3474
|
return;
|
package/lib/replies.ts
CHANGED
|
@@ -68,6 +68,7 @@ export function createReplyDedupRuntime(): ReplyDedupRuntime {
|
|
|
68
68
|
// --- Transport-level dedup ---
|
|
69
69
|
|
|
70
70
|
const lastRepliedToMessageIdByTarget = new Map<string, number>();
|
|
71
|
+
const replyDedupPreservedOnNextReset = new Map<string, number>();
|
|
71
72
|
let replyDedupGeneration = 0;
|
|
72
73
|
|
|
73
74
|
function getReplyDedupTargetKey(
|
|
@@ -83,6 +84,23 @@ function getReplyDedupTargetKey(
|
|
|
83
84
|
export function resetTransportReplyDedup(): void {
|
|
84
85
|
replyDedupGeneration += 1;
|
|
85
86
|
lastRepliedToMessageIdByTarget.clear();
|
|
87
|
+
for (const [key, messageId] of replyDedupPreservedOnNextReset) {
|
|
88
|
+
lastRepliedToMessageIdByTarget.set(key, messageId);
|
|
89
|
+
}
|
|
90
|
+
replyDedupPreservedOnNextReset.clear();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Keeps a successfully published transition notice as the first reply of the
|
|
94
|
+
* next agent turn. The following agent-start reset consumes this one-shot
|
|
95
|
+
* preservation, so later messages in that turn do not repeat the reply header. */
|
|
96
|
+
export function preserveTransportReplyDedupOnNextReset(
|
|
97
|
+
chatId: number,
|
|
98
|
+
messageId: number,
|
|
99
|
+
target?: TelegramTarget,
|
|
100
|
+
): void {
|
|
101
|
+
const key = getReplyDedupTargetKey(chatId, target);
|
|
102
|
+
if (lastRepliedToMessageIdByTarget.get(key) !== messageId) return;
|
|
103
|
+
replyDedupPreservedOnNextReset.set(key, messageId);
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
export function buildTelegramReplyParameters(
|
package/lib/routing.ts
CHANGED
|
@@ -627,6 +627,7 @@ export interface TelegramInboundRouteRuntimeDeps<
|
|
|
627
627
|
isContextActive?: (ctx: TContext) => boolean;
|
|
628
628
|
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
629
629
|
requestNextDispatchAnnouncement?: () => void;
|
|
630
|
+
cancelNextDispatchAnnouncement?: () => void;
|
|
630
631
|
requestDeferredDispatchNextQueuedTelegramTurn?: (
|
|
631
632
|
dispatch: (ctx: TContext) => void,
|
|
632
633
|
) => void;
|
|
@@ -2222,6 +2223,10 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2222
2223
|
isContextActive: deps.isContextActive,
|
|
2223
2224
|
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
2224
2225
|
requestNextDispatchAnnouncement: deps.requestNextDispatchAnnouncement,
|
|
2226
|
+
cancelNextTransitionAnnouncements: () => {
|
|
2227
|
+
deps.activeTurnRuntime.clearNextAbortAnnouncement();
|
|
2228
|
+
deps.cancelNextDispatchAnnouncement?.();
|
|
2229
|
+
},
|
|
2225
2230
|
requestDeferredDispatchNextQueuedTelegramTurn:
|
|
2226
2231
|
deps.requestDeferredDispatchNextQueuedTelegramTurn,
|
|
2227
2232
|
startTypingLoop: deps.startTypingLoop,
|
|
@@ -2297,6 +2302,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2297
2302
|
},
|
|
2298
2303
|
getPromptTemplateCommands,
|
|
2299
2304
|
sendTextReply: deps.sendTextReply,
|
|
2305
|
+
markActiveTurnNextAbortAnnouncement:
|
|
2306
|
+
deps.activeTurnRuntime.markNextAbortAnnouncement,
|
|
2300
2307
|
getActiveTurnReply: () => {
|
|
2301
2308
|
const activeTurn = deps.activeTurnRuntime.get();
|
|
2302
2309
|
if (!activeTurn) return undefined;
|
package/lib/sync.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { getTelegramApiErrorRequestTarget, isTelegramStaleTargetHttpError } from "./telegram-api.ts";
|
|
8
8
|
import { getTelegramTargetKey, type TelegramTarget } from "./target.ts";
|
|
9
9
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
10
|
+
import { TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
|
|
10
11
|
import {
|
|
11
12
|
createTelegramWorkspaceAdmissionOperationId,
|
|
12
13
|
runWithTelegramWorkspaceAdmissionsAsync,
|
|
@@ -197,6 +198,47 @@ export function markTelegramConfigSyncChange<
|
|
|
197
198
|
return nextState;
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
export function createTelegramPreservedLeaderQuitHandler(deps: {
|
|
202
|
+
instanceId: string;
|
|
203
|
+
topicTargetStore: Pick<TelegramTopicTargetStore,
|
|
204
|
+
"load" | "list" | "listPendingCleanups" | "detachTargetOwner">;
|
|
205
|
+
getCurrentLeaderEpoch: () => number | string | undefined;
|
|
206
|
+
getProfileName: () => string | undefined;
|
|
207
|
+
isPollingSuspended: () => boolean;
|
|
208
|
+
resolveAutomaticThreadCleanupEnabled: () => boolean | Promise<boolean>;
|
|
209
|
+
runWorkspaceOperation: TelegramSyncWorkspaceOperationRunner;
|
|
210
|
+
}): (isSessionCurrent: () => boolean) => (() => Promise<void>) | undefined {
|
|
211
|
+
return (isSessionCurrent) => {
|
|
212
|
+
const epoch = deps.getCurrentLeaderEpoch();
|
|
213
|
+
const profile = deps.getProfileName();
|
|
214
|
+
if (epoch === undefined || !isSessionCurrent()) return undefined;
|
|
215
|
+
const isCurrent = () => isSessionCurrent() && deps.getCurrentLeaderEpoch() === epoch &&
|
|
216
|
+
deps.getProfileName() === profile && deps.isPollingSuspended();
|
|
217
|
+
return async () => {
|
|
218
|
+
if (!isCurrent() || await deps.resolveAutomaticThreadCleanupEnabled() !== false) return;
|
|
219
|
+
if (!isCurrent()) return;
|
|
220
|
+
await deps.runWorkspaceOperation({
|
|
221
|
+
operationId: createTelegramWorkspaceAdmissionOperationId(),
|
|
222
|
+
operationKind: "workspace.preserve-leader-quit",
|
|
223
|
+
scopes: [{ kind: "profile" }],
|
|
224
|
+
}, async () => {
|
|
225
|
+
if (!isCurrent()) return;
|
|
226
|
+
await deps.topicTargetStore.load();
|
|
227
|
+
if (!isCurrent()) return;
|
|
228
|
+
const records = deps.topicTargetStore.list().filter((record) =>
|
|
229
|
+
record.instanceId === deps.instanceId && (record.status === "active" || record.status === "starting"));
|
|
230
|
+
if (records.length !== 1) return;
|
|
231
|
+
const record = records[0]!;
|
|
232
|
+
if (deps.topicTargetStore.listPendingCleanups().some((intent) =>
|
|
233
|
+
intent.target.chatId === record.target.chatId && intent.target.threadId === record.target.threadId)) return;
|
|
234
|
+
if (!await deps.topicTargetStore.detachTargetOwner(record, isCurrent) && isCurrent()) {
|
|
235
|
+
throw new Error("Telegram preserved leader detachment was not committed.");
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
200
242
|
export interface TelegramSessionRestartThreadCleanupDeps<
|
|
201
243
|
TSyncState extends TelegramSyncState,
|
|
202
244
|
> extends Omit<TelegramManualThreadDisconnectDeps<TSyncState>, "stopPolling"> {
|
|
@@ -650,15 +692,17 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
650
692
|
const leaderProfileKey = getTelegramThreadOwnerKey(leaderOwner);
|
|
651
693
|
const legacyLeaderRecord =
|
|
652
694
|
deps.topicTargetStore.getByProfileKey(leaderProfileKey);
|
|
695
|
+
let capacityUnavailable = false;
|
|
653
696
|
const workspaceIdentity = normalizedLeaderCwd
|
|
654
697
|
? deps.topicTargetStore.claimWorkspaceIdentity(
|
|
655
698
|
normalizedLeaderCwd,
|
|
656
699
|
deps.instanceId,
|
|
657
700
|
legacyLeaderRecord?.instanceId,
|
|
658
|
-
{ sessionId: deps.sessionId },
|
|
701
|
+
{ sessionId: deps.sessionId, onCapacityUnavailable() { capacityUnavailable = true; } },
|
|
659
702
|
)
|
|
660
703
|
: undefined;
|
|
661
704
|
if (deps.cwd && !workspaceIdentity) {
|
|
705
|
+
if (capacityUnavailable) throw new TelegramWorkspaceSlotUnavailableError();
|
|
662
706
|
throw new Error("Telegram Workspace identity is already claimed.");
|
|
663
707
|
}
|
|
664
708
|
const commitWorkspaceBinding = async (
|
package/lib/telegram-api.ts
CHANGED
|
@@ -882,15 +882,18 @@ export function isTelegramApiCommitUnknownError(
|
|
|
882
882
|
class TelegramApiHttpError extends Error {
|
|
883
883
|
readonly status: number | undefined;
|
|
884
884
|
readonly retryAfterSeconds: number | undefined;
|
|
885
|
+
readonly rejectedRequestMethod: string | undefined;
|
|
885
886
|
requestTarget?: { chatId: number; threadId: number };
|
|
886
887
|
constructor(
|
|
887
888
|
message: string,
|
|
888
889
|
status: number | undefined,
|
|
889
890
|
retryAfterSeconds: number | undefined,
|
|
891
|
+
rejectedRequestMethod?: string,
|
|
890
892
|
) {
|
|
891
893
|
super(message);
|
|
892
894
|
this.status = status;
|
|
893
895
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
896
|
+
this.rejectedRequestMethod = rejectedRequestMethod;
|
|
894
897
|
}
|
|
895
898
|
}
|
|
896
899
|
|
|
@@ -934,6 +937,12 @@ export function isTelegramStaleTargetHttpError(error: unknown): boolean {
|
|
|
934
937
|
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);
|
|
935
938
|
}
|
|
936
939
|
|
|
940
|
+
/** Only a parsed Telegram rejection of this method proves a request had no effect. */
|
|
941
|
+
export function isTelegramApiRequestRejected(error: unknown, method: string): boolean {
|
|
942
|
+
return error instanceof TelegramApiHttpError && error.rejectedRequestMethod !== undefined &&
|
|
943
|
+
error.rejectedRequestMethod === method;
|
|
944
|
+
}
|
|
945
|
+
|
|
937
946
|
export function isTelegramMessageNotModifiedError(error: unknown): boolean {
|
|
938
947
|
return (
|
|
939
948
|
error instanceof Error && error.message.includes("message is not modified")
|
|
@@ -1106,6 +1115,8 @@ async function parseTelegramApiResponse<TResponse>(
|
|
|
1106
1115
|
`Telegram API ${method} failed: ${status}${description}`,
|
|
1107
1116
|
response.status,
|
|
1108
1117
|
Number.isFinite(retryAfterSeconds) ? retryAfterSeconds : undefined,
|
|
1118
|
+
data?.ok === false && data.error_code === response.status &&
|
|
1119
|
+
[400, 401, 403, 404, 429].includes(response.status) ? method : undefined,
|
|
1109
1120
|
);
|
|
1110
1121
|
}
|
|
1111
1122
|
if (!data) {
|
|
@@ -1785,6 +1796,10 @@ export function buildTelegramAnswerGuestQueryBody(
|
|
|
1785
1796
|
return body;
|
|
1786
1797
|
}
|
|
1787
1798
|
|
|
1799
|
+
export type TelegramWorkspaceThreadDeletionTransport = (
|
|
1800
|
+
authorize: () => { chatId: number; threadId: number },
|
|
1801
|
+
) => Promise<void>;
|
|
1802
|
+
|
|
1788
1803
|
export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
1789
1804
|
getBotToken: () => string | undefined;
|
|
1790
1805
|
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
|
|
@@ -1793,7 +1808,9 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1793
1808
|
workspaceAdmission?:
|
|
1794
1809
|
| TelegramApiWorkspaceAdmissionPort
|
|
1795
1810
|
| (() => TelegramApiWorkspaceAdmissionPort | undefined);
|
|
1796
|
-
}): TelegramBridgeApiRuntime {
|
|
1811
|
+
}): TelegramBridgeApiRuntime & {
|
|
1812
|
+
deleteWorkspaceThread: TelegramWorkspaceThreadDeletionTransport;
|
|
1813
|
+
} {
|
|
1797
1814
|
const client = createTelegramApiClient(deps.getBotToken, {
|
|
1798
1815
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1799
1816
|
});
|
|
@@ -1807,7 +1824,7 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1807
1824
|
},
|
|
1808
1825
|
})
|
|
1809
1826
|
: client;
|
|
1810
|
-
|
|
1827
|
+
const runtime = createTelegramBridgeApiRuntime({
|
|
1811
1828
|
client: deps.targetActivity
|
|
1812
1829
|
? createTelegramApiTargetTrackingClient(admittedClient, deps.targetActivity)
|
|
1813
1830
|
: admittedClient,
|
|
@@ -1817,6 +1834,17 @@ export function createDefaultTelegramBridgeApiRuntime(deps: {
|
|
|
1817
1834
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1818
1835
|
captureRequestErrorHandler: deps.captureRequestErrorHandler,
|
|
1819
1836
|
});
|
|
1837
|
+
return {
|
|
1838
|
+
...runtime,
|
|
1839
|
+
async deleteWorkspaceThread(authorize) {
|
|
1840
|
+
// The exclusive deletion permit replaces ordinary target admission.
|
|
1841
|
+
const target = authorize();
|
|
1842
|
+
const deleted = await client.call<boolean>("deleteForumTopic", {
|
|
1843
|
+
chat_id: target.chatId, message_thread_id: target.threadId,
|
|
1844
|
+
}, { maxAttempts: 1, retrySafety: "non-idempotent" });
|
|
1845
|
+
if (deleted !== true) throw new Error("Telegram Workspace Thread deletion was not confirmed.");
|
|
1846
|
+
},
|
|
1847
|
+
};
|
|
1820
1848
|
}
|
|
1821
1849
|
|
|
1822
1850
|
export function createTelegramBridgeApiRuntime(
|
|
@@ -433,7 +433,12 @@ export function createTelegramThreadCleanupWorkStore(options: {
|
|
|
433
433
|
publish(file); return { confirmed: true, entry: structuredClone(entry) };
|
|
434
434
|
});
|
|
435
435
|
},
|
|
436
|
-
list() {
|
|
436
|
+
list() {
|
|
437
|
+
return structuredClone(withTelegramFileTransaction(
|
|
438
|
+
`${options.path}.transaction`,
|
|
439
|
+
() => read().workSets,
|
|
440
|
+
));
|
|
441
|
+
},
|
|
437
442
|
};
|
|
438
443
|
}
|
|
439
444
|
|
|
@@ -542,8 +547,11 @@ export function createTelegramThreadCleanupPermitRuntime(deps: {
|
|
|
542
547
|
if (fence.leaderEpoch !== leaderEpoch || fence.owner.processId !== owner.processId ||
|
|
543
548
|
fence.owner.processBirthId !== owner.processBirthId) {
|
|
544
549
|
if (!deps.canAdoptFence(fence)) return undefined;
|
|
545
|
-
try {
|
|
546
|
-
{ owner, leaderEpoch });
|
|
550
|
+
try {
|
|
551
|
+
const adopted = deps.ledger.adoptThreadCleanupFence(fence, { owner, leaderEpoch });
|
|
552
|
+
if (adopted.destructiveKind !== "manual-thread-cleanup") return undefined;
|
|
553
|
+
fence = adopted;
|
|
554
|
+
} catch { return undefined; }
|
|
547
555
|
}
|
|
548
556
|
return deps.getLeaderEpoch() === leaderEpoch ? fence : undefined;
|
|
549
557
|
};
|