@llblab/pi-telegram 0.22.1 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +11 -7
- package/BACKLOG.md +0 -50
- package/CHANGELOG.md +49 -2
- package/README.md +3 -1
- package/docs/README.md +1 -1
- package/docs/activity.md +8 -0
- package/docs/architecture.md +16 -14
- package/docs/locks.md +21 -15
- package/docs/multi-instance-bus.md +17 -16
- package/docs/outbound.md +16 -0
- package/docs/public-api.md +9 -4
- package/index.ts +74 -59
- package/lib/activity.ts +100 -3
- package/lib/bindings.ts +81 -6
- package/lib/bus-follower.ts +205 -17
- package/lib/bus-leader.ts +339 -133
- package/lib/bus.ts +82 -19
- package/lib/commands.ts +28 -4
- package/lib/config.ts +40 -28
- package/lib/media.ts +102 -1
- package/lib/menu-settings.ts +4 -1
- package/lib/outbound-attachments.ts +150 -1
- package/lib/outbound.ts +106 -1
- package/lib/polling.ts +5 -0
- package/lib/queue.ts +41 -48
- package/lib/routing.ts +88 -1
- package/lib/sync.ts +40 -3
- package/lib/telegram-api.ts +77 -13
- package/lib/text-groups.ts +183 -16
- package/lib/thread-reconciler.ts +66 -22
- package/lib/threads.ts +131 -23
- package/lib/turns.ts +102 -13
- package/lib/updates.ts +2 -0
- package/package.json +1 -1
package/lib/text-groups.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* Owns conservative delayed grouping for Telegram text messages that look like automatic long-message splits
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { setTimeout as waitForTimeout } from "node:timers/promises";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
extractTelegramMessageText,
|
|
11
|
+
type TelegramMessageForwardOrigin,
|
|
12
|
+
type TelegramMessageUser,
|
|
13
|
+
type TelegramRichMessage,
|
|
14
|
+
} from "./media.ts";
|
|
15
|
+
|
|
7
16
|
const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
|
|
8
17
|
const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
|
|
9
18
|
const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
|
|
@@ -13,9 +22,13 @@ export interface TelegramTextGroupMessage {
|
|
|
13
22
|
media_group_id?: string;
|
|
14
23
|
chat: { id: number };
|
|
15
24
|
message_thread_id?: number;
|
|
16
|
-
from?: { id
|
|
25
|
+
from?: { id?: number; is_bot?: boolean };
|
|
17
26
|
text?: string;
|
|
18
27
|
caption?: string;
|
|
28
|
+
rich_message?: TelegramRichMessage;
|
|
29
|
+
forward_origin?: TelegramMessageForwardOrigin;
|
|
30
|
+
forward_from?: TelegramMessageUser;
|
|
31
|
+
forward_sender_name?: string;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
export interface TelegramTextGroupState<TMessage, TContext = unknown> {
|
|
@@ -24,10 +37,24 @@ export interface TelegramTextGroupState<TMessage, TContext = unknown> {
|
|
|
24
37
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
25
38
|
dispatching?: boolean;
|
|
26
39
|
suspended?: boolean;
|
|
27
|
-
reschedule?: () => void;
|
|
40
|
+
reschedule?: (delayMs?: number) => void;
|
|
41
|
+
dispatchLimit?: number;
|
|
42
|
+
forwardCommentCandidate?: boolean;
|
|
28
43
|
}
|
|
29
44
|
|
|
45
|
+
export type TelegramForwardCommentBatchPosition = "comment" | "forward";
|
|
46
|
+
|
|
30
47
|
export interface TelegramTextGroupController<TMessage, TContext = unknown> {
|
|
48
|
+
prepareUpdateBatch: (
|
|
49
|
+
updates: readonly { message?: TMessage }[],
|
|
50
|
+
) => void;
|
|
51
|
+
getPreparedForwardingPosition: (
|
|
52
|
+
message: TelegramTextGroupMessage,
|
|
53
|
+
) => TelegramForwardCommentBatchPosition | undefined;
|
|
54
|
+
prepareForwardedMessage: (
|
|
55
|
+
message: TelegramTextGroupMessage,
|
|
56
|
+
position: TelegramForwardCommentBatchPosition,
|
|
57
|
+
) => void;
|
|
31
58
|
queueMessage: (options: {
|
|
32
59
|
message: TMessage;
|
|
33
60
|
context: TContext;
|
|
@@ -43,6 +70,7 @@ export interface TelegramTextGroupController<TMessage, TContext = unknown> {
|
|
|
43
70
|
|
|
44
71
|
export interface TelegramTextGroupControllerOptions {
|
|
45
72
|
debounceMs?: number;
|
|
73
|
+
forwardCommentWaitMs?: number | false;
|
|
46
74
|
minSplitLength?: number;
|
|
47
75
|
setTimer?: (
|
|
48
76
|
callback: () => void,
|
|
@@ -66,19 +94,44 @@ export interface TelegramGroupedInputClearerDeps {
|
|
|
66
94
|
function extractTelegramTextGroupText(
|
|
67
95
|
message: TelegramTextGroupMessage,
|
|
68
96
|
): string {
|
|
69
|
-
return
|
|
97
|
+
return extractTelegramMessageText(message);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isTelegramForwardedMessage(
|
|
101
|
+
message: TelegramTextGroupMessage,
|
|
102
|
+
): boolean {
|
|
103
|
+
return (
|
|
104
|
+
message.forward_origin !== undefined ||
|
|
105
|
+
message.forward_from !== undefined ||
|
|
106
|
+
typeof message.forward_sender_name === "string"
|
|
107
|
+
);
|
|
70
108
|
}
|
|
71
109
|
|
|
72
110
|
function isTelegramTextGroupCommand(text: string): boolean {
|
|
73
111
|
return text.trimStart().startsWith("/");
|
|
74
112
|
}
|
|
75
113
|
|
|
114
|
+
function isTelegramTextGroupClearingCommand(text: string): boolean {
|
|
115
|
+
const command = text.trimStart().split(/\s+/, 1)[0]?.split("@", 1)[0];
|
|
116
|
+
return command === "/stop";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getTelegramTextGroupMessageIdentity(
|
|
120
|
+
message: TelegramTextGroupMessage,
|
|
121
|
+
): string {
|
|
122
|
+
const threadKey =
|
|
123
|
+
typeof message.message_thread_id === "number"
|
|
124
|
+
? `thread:${message.message_thread_id}`
|
|
125
|
+
: "private";
|
|
126
|
+
return `${message.chat.id}:${threadKey}:${message.message_id}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
76
129
|
function getTelegramTextGroupKey(
|
|
77
130
|
message: TelegramTextGroupMessage,
|
|
78
131
|
): string | undefined {
|
|
79
132
|
if (message.media_group_id) return undefined;
|
|
80
133
|
if (!message.from || message.from.is_bot) return undefined;
|
|
81
|
-
if (
|
|
134
|
+
if (!extractTelegramTextGroupText(message)) return undefined;
|
|
82
135
|
const threadKey =
|
|
83
136
|
typeof message.message_thread_id === "number"
|
|
84
137
|
? `thread:${message.message_thread_id}`
|
|
@@ -127,12 +180,20 @@ export function queueTelegramTextGroupMessage<
|
|
|
127
180
|
messages: TMessage[],
|
|
128
181
|
ctx: TContext,
|
|
129
182
|
) => unknown | Promise<unknown>;
|
|
183
|
+
forceStart?: boolean;
|
|
184
|
+
dispatchImmediately?: boolean;
|
|
185
|
+
forwardCommentCandidate?: boolean;
|
|
186
|
+
delayMs?: number;
|
|
130
187
|
}): boolean {
|
|
131
188
|
const key = getTelegramTextGroupKey(options.message);
|
|
132
189
|
if (!key) return false;
|
|
133
190
|
const existing = options.groups.get(key);
|
|
191
|
+
if (existing?.messages.some(
|
|
192
|
+
(message) => message.message_id === options.message.message_id,
|
|
193
|
+
)) return true;
|
|
134
194
|
if (
|
|
135
195
|
!existing &&
|
|
196
|
+
!options.forceStart &&
|
|
136
197
|
!canStartTelegramTextGroup(options.message, options.minSplitLength)
|
|
137
198
|
)
|
|
138
199
|
return false;
|
|
@@ -141,9 +202,8 @@ export function queueTelegramTextGroupMessage<
|
|
|
141
202
|
const state = existing ?? { messages: [] };
|
|
142
203
|
state.messages.push(options.message);
|
|
143
204
|
state.context = options.context;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
state.flushTimer = options.setTimer(() => {
|
|
205
|
+
state.forwardCommentCandidate = options.forwardCommentCandidate;
|
|
206
|
+
const dispatchQueued = (): void => {
|
|
147
207
|
state.flushTimer = undefined;
|
|
148
208
|
const queued = options.groups.get(key);
|
|
149
209
|
if (!queued || queued.context === undefined) return;
|
|
@@ -151,7 +211,9 @@ export function queueTelegramTextGroupMessage<
|
|
|
151
211
|
scheduleDispatch();
|
|
152
212
|
return;
|
|
153
213
|
}
|
|
154
|
-
const
|
|
214
|
+
const dispatchCount = queued.dispatchLimit ?? queued.messages.length;
|
|
215
|
+
queued.dispatchLimit = undefined;
|
|
216
|
+
const dispatchedMessages = queued.messages.slice(0, dispatchCount);
|
|
155
217
|
const dispatchedIds = new Set(
|
|
156
218
|
dispatchedMessages.map((message) => message.message_id),
|
|
157
219
|
);
|
|
@@ -174,12 +236,17 @@ export function queueTelegramTextGroupMessage<
|
|
|
174
236
|
if (!queued.flushTimer) scheduleDispatch();
|
|
175
237
|
},
|
|
176
238
|
);
|
|
177
|
-
|
|
239
|
+
};
|
|
240
|
+
const scheduleDispatch = (delayMs = options.debounceMs): void => {
|
|
241
|
+
if (state.suspended) return;
|
|
242
|
+
state.flushTimer = options.setTimer(dispatchQueued, delayMs);
|
|
178
243
|
state.flushTimer.unref?.();
|
|
179
244
|
};
|
|
180
245
|
state.reschedule = scheduleDispatch;
|
|
181
246
|
if (state.flushTimer) options.clearTimer(state.flushTimer);
|
|
182
|
-
scheduleDispatch(
|
|
247
|
+
scheduleDispatch(
|
|
248
|
+
options.dispatchImmediately ? 0 : (options.delayMs ?? options.debounceMs),
|
|
249
|
+
);
|
|
183
250
|
options.groups.set(key, state);
|
|
184
251
|
return true;
|
|
185
252
|
}
|
|
@@ -191,17 +258,102 @@ export function createTelegramTextGroupController<
|
|
|
191
258
|
options: TelegramTextGroupControllerOptions = {},
|
|
192
259
|
): TelegramTextGroupController<TMessage, TContext> {
|
|
193
260
|
const groups = new Map<string, TelegramTextGroupState<TMessage, TContext>>();
|
|
261
|
+
const plannedForwardCommentStarts = new Set<string>();
|
|
262
|
+
const plannedForwardCommentEnds = new Set<string>();
|
|
194
263
|
const debounceMs = options.debounceMs ?? TELEGRAM_TEXT_GROUP_DEBOUNCE_MS;
|
|
195
264
|
const minSplitLength =
|
|
196
265
|
options.minSplitLength ?? TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH;
|
|
266
|
+
const forwardCommentWaitMs =
|
|
267
|
+
options.forwardCommentWaitMs === undefined
|
|
268
|
+
? debounceMs
|
|
269
|
+
: options.forwardCommentWaitMs;
|
|
197
270
|
const setTimer =
|
|
198
271
|
options.setTimer ??
|
|
199
|
-
((callback: () => void, ms: number): ReturnType<typeof setTimeout> =>
|
|
200
|
-
|
|
201
|
-
|
|
272
|
+
((callback: () => void, ms: number): ReturnType<typeof setTimeout> => {
|
|
273
|
+
const controller = new AbortController();
|
|
274
|
+
void waitForTimeout(ms, undefined, {
|
|
275
|
+
signal: controller.signal,
|
|
276
|
+
}).then(callback, () => undefined);
|
|
277
|
+
return controller as unknown as ReturnType<typeof setTimeout>;
|
|
278
|
+
});
|
|
279
|
+
const clearTimer =
|
|
280
|
+
options.clearTimer ??
|
|
281
|
+
(options.setTimer
|
|
282
|
+
? clearTimeout
|
|
283
|
+
: (timer: ReturnType<typeof setTimeout>): void => {
|
|
284
|
+
(timer as unknown as AbortController).abort();
|
|
285
|
+
});
|
|
202
286
|
return {
|
|
203
|
-
|
|
204
|
-
|
|
287
|
+
prepareUpdateBatch(updates) {
|
|
288
|
+
for (let index = 0; index + 1 < updates.length; index += 1) {
|
|
289
|
+
const comment = updates[index]?.message;
|
|
290
|
+
const forwarded = updates[index + 1]?.message;
|
|
291
|
+
if (!comment || !forwarded) continue;
|
|
292
|
+
const commentText = extractTelegramTextGroupText(comment);
|
|
293
|
+
const commentKey = getTelegramTextGroupKey(comment);
|
|
294
|
+
const forwardedKey = getTelegramTextGroupKey(forwarded);
|
|
295
|
+
if (
|
|
296
|
+
!commentKey ||
|
|
297
|
+
commentKey !== forwardedKey ||
|
|
298
|
+
!commentText ||
|
|
299
|
+
isTelegramTextGroupCommand(commentText) ||
|
|
300
|
+
isTelegramForwardedMessage(comment) ||
|
|
301
|
+
!isTelegramForwardedMessage(forwarded) ||
|
|
302
|
+
forwarded.message_id <= comment.message_id ||
|
|
303
|
+
forwarded.message_id >
|
|
304
|
+
comment.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP
|
|
305
|
+
) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
plannedForwardCommentStarts.add(
|
|
309
|
+
getTelegramTextGroupMessageIdentity(comment),
|
|
310
|
+
);
|
|
311
|
+
plannedForwardCommentEnds.add(
|
|
312
|
+
getTelegramTextGroupMessageIdentity(forwarded),
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
getPreparedForwardingPosition(message) {
|
|
317
|
+
const identity = getTelegramTextGroupMessageIdentity(message);
|
|
318
|
+
if (plannedForwardCommentStarts.has(identity)) return "comment";
|
|
319
|
+
if (plannedForwardCommentEnds.has(identity)) return "forward";
|
|
320
|
+
return undefined;
|
|
321
|
+
},
|
|
322
|
+
prepareForwardedMessage(message, position) {
|
|
323
|
+
const identity = getTelegramTextGroupMessageIdentity(message);
|
|
324
|
+
if (position === "comment") plannedForwardCommentStarts.add(identity);
|
|
325
|
+
else plannedForwardCommentEnds.add(identity);
|
|
326
|
+
},
|
|
327
|
+
queueMessage: ({ message, context, dispatchMessages }) => {
|
|
328
|
+
const identity = getTelegramTextGroupMessageIdentity(message);
|
|
329
|
+
const key = getTelegramTextGroupKey(message);
|
|
330
|
+
const plannedStart = plannedForwardCommentStarts.delete(identity);
|
|
331
|
+
const forwarded = isTelegramForwardedMessage(message);
|
|
332
|
+
const existing = key ? groups.get(key) : undefined;
|
|
333
|
+
const text = extractTelegramTextGroupText(message);
|
|
334
|
+
if (existing && isTelegramTextGroupClearingCommand(text)) {
|
|
335
|
+
if (existing.flushTimer) clearTimer(existing.flushTimer);
|
|
336
|
+
groups.delete(key!);
|
|
337
|
+
}
|
|
338
|
+
const separateFromCandidate =
|
|
339
|
+
!!existing?.forwardCommentCandidate &&
|
|
340
|
+
!forwarded &&
|
|
341
|
+
!isTelegramTextGroupCommand(text);
|
|
342
|
+
if (separateFromCandidate) {
|
|
343
|
+
existing.dispatchLimit = existing.messages.length;
|
|
344
|
+
}
|
|
345
|
+
const forceStart =
|
|
346
|
+
plannedStart ||
|
|
347
|
+
(forwardCommentWaitMs !== false &&
|
|
348
|
+
!forwarded &&
|
|
349
|
+
!!key &&
|
|
350
|
+
typeof message.text === "string" &&
|
|
351
|
+
!isTelegramTextGroupCommand(extractTelegramTextGroupText(message)));
|
|
352
|
+
const dispatchImmediately =
|
|
353
|
+
separateFromCandidate ||
|
|
354
|
+
plannedForwardCommentEnds.delete(identity) ||
|
|
355
|
+
(forwarded && !!key && groups.has(key));
|
|
356
|
+
return queueTelegramTextGroupMessage({
|
|
205
357
|
message,
|
|
206
358
|
context,
|
|
207
359
|
groups,
|
|
@@ -210,7 +362,20 @@ export function createTelegramTextGroupController<
|
|
|
210
362
|
setTimer,
|
|
211
363
|
clearTimer,
|
|
212
364
|
dispatchMessages,
|
|
213
|
-
|
|
365
|
+
forceStart,
|
|
366
|
+
dispatchImmediately,
|
|
367
|
+
forwardCommentCandidate:
|
|
368
|
+
forceStart &&
|
|
369
|
+
!forwarded &&
|
|
370
|
+
!canStartTelegramTextGroup(message, minSplitLength),
|
|
371
|
+
delayMs:
|
|
372
|
+
forceStart && !canStartTelegramTextGroup(message, minSplitLength)
|
|
373
|
+
? forwardCommentWaitMs === false
|
|
374
|
+
? undefined
|
|
375
|
+
: forwardCommentWaitMs
|
|
376
|
+
: undefined,
|
|
377
|
+
});
|
|
378
|
+
},
|
|
214
379
|
suspend: () => {
|
|
215
380
|
for (const state of groups.values()) {
|
|
216
381
|
state.suspended = true;
|
|
@@ -230,6 +395,8 @@ export function createTelegramTextGroupController<
|
|
|
230
395
|
if (state.flushTimer) clearTimer(state.flushTimer);
|
|
231
396
|
}
|
|
232
397
|
groups.clear();
|
|
398
|
+
plannedForwardCommentStarts.clear();
|
|
399
|
+
plannedForwardCommentEnds.clear();
|
|
233
400
|
},
|
|
234
401
|
};
|
|
235
402
|
}
|
package/lib/thread-reconciler.ts
CHANGED
|
@@ -10,7 +10,13 @@ import type { TelegramTarget } from "./target.ts";
|
|
|
10
10
|
type ThreadTarget = TelegramTarget & { threadId: number };
|
|
11
11
|
|
|
12
12
|
type ThreadRecordStatus =
|
|
13
|
-
|
|
13
|
+
| "active"
|
|
14
|
+
| "offline"
|
|
15
|
+
| "stale"
|
|
16
|
+
| "pending"
|
|
17
|
+
| "starting"
|
|
18
|
+
| "probe-required"
|
|
19
|
+
| "failed";
|
|
14
20
|
|
|
15
21
|
export interface ThreadReconciliationRecord {
|
|
16
22
|
target: ThreadTarget;
|
|
@@ -182,6 +188,7 @@ export interface ThreadReconciliationPlan {
|
|
|
182
188
|
|
|
183
189
|
export interface ThreadReconciliationApplyResult {
|
|
184
190
|
changed: boolean;
|
|
191
|
+
incompleteActions?: ThreadReconciliationAction[];
|
|
185
192
|
}
|
|
186
193
|
|
|
187
194
|
export interface ThreadReconciliationApplyPorts {
|
|
@@ -376,7 +383,7 @@ function getActionLeaderEpoch(
|
|
|
376
383
|
return "leaderEpoch" in action ? action.leaderEpoch : undefined;
|
|
377
384
|
}
|
|
378
385
|
|
|
379
|
-
function
|
|
386
|
+
function isTelegramTopicTargetDeletedOrMissingError(error: unknown): boolean {
|
|
380
387
|
if (!(error instanceof Error)) return false;
|
|
381
388
|
const message = error.message.toLowerCase();
|
|
382
389
|
return (
|
|
@@ -384,7 +391,15 @@ function isTelegramTopicTargetGoneError(error: unknown): boolean {
|
|
|
384
391
|
message.includes("message thread not found") ||
|
|
385
392
|
message.includes("thread not found") ||
|
|
386
393
|
message.includes("topic not found") ||
|
|
387
|
-
message.includes("topic deleted")
|
|
394
|
+
message.includes("topic deleted")
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isTelegramTopicTargetGoneError(error: unknown): boolean {
|
|
399
|
+
if (isTelegramTopicTargetDeletedOrMissingError(error)) return true;
|
|
400
|
+
if (!(error instanceof Error)) return false;
|
|
401
|
+
const message = error.message.toLowerCase();
|
|
402
|
+
return (
|
|
388
403
|
message.includes("topic closed") ||
|
|
389
404
|
message.includes("thread closed") ||
|
|
390
405
|
message.includes("forum topic closed") ||
|
|
@@ -503,6 +518,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
503
518
|
): Promise<ThreadReconciliationApplyResult> {
|
|
504
519
|
let shouldPersist = false;
|
|
505
520
|
const persistFences: ThreadReconciliationAction[] = [];
|
|
521
|
+
const incompleteActions: ThreadReconciliationAction[] = [];
|
|
506
522
|
for (const action of plan.actions) {
|
|
507
523
|
if (action.kind === "remove-reservation") {
|
|
508
524
|
shouldPersist =
|
|
@@ -521,7 +537,10 @@ export async function applyThreadReconciliationPlan(
|
|
|
521
537
|
continue;
|
|
522
538
|
}
|
|
523
539
|
if (action.kind === "close-stale-replaced-topic") {
|
|
524
|
-
if (shouldSkipForStaleLeaderEpoch(action, ports))
|
|
540
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
541
|
+
incompleteActions.push(action);
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
525
544
|
if (!ports.callApi) {
|
|
526
545
|
ports.recordRuntimeEvent?.(
|
|
527
546
|
"telegram",
|
|
@@ -533,28 +552,35 @@ export async function applyThreadReconciliationPlan(
|
|
|
533
552
|
threadId: action.target.threadId,
|
|
534
553
|
},
|
|
535
554
|
);
|
|
555
|
+
incompleteActions.push(action);
|
|
536
556
|
continue;
|
|
537
557
|
}
|
|
558
|
+
let closeConfirmed = false;
|
|
538
559
|
try {
|
|
539
|
-
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
540
560
|
await ports.callApi("closeForumTopic", {
|
|
541
561
|
chat_id: action.target.chatId,
|
|
542
562
|
message_thread_id: action.target.threadId,
|
|
543
563
|
});
|
|
544
|
-
|
|
545
|
-
const changed =
|
|
546
|
-
ports.markStaleByTarget?.(action.target, "closed") ?? false;
|
|
547
|
-
if (changed) persistFences.push(action);
|
|
548
|
-
shouldPersist = changed || shouldPersist;
|
|
549
|
-
}
|
|
564
|
+
closeConfirmed = true;
|
|
550
565
|
} catch (error) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
566
|
+
closeConfirmed = isTelegramTopicTargetGoneError(error);
|
|
567
|
+
if (!closeConfirmed) {
|
|
568
|
+
ports.recordRuntimeEvent?.("telegram", error, {
|
|
569
|
+
phase: `thread-reconciler-${action.reason}-closeForumTopic`,
|
|
570
|
+
instanceId: action.instanceId,
|
|
571
|
+
chatId: action.target.chatId,
|
|
572
|
+
threadId: action.target.threadId,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
557
575
|
}
|
|
576
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports) || !closeConfirmed) {
|
|
577
|
+
incompleteActions.push(action);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const changed =
|
|
581
|
+
ports.markStaleByTarget?.(action.target, "closed") ?? false;
|
|
582
|
+
if (changed) persistFences.push(action);
|
|
583
|
+
shouldPersist = changed || shouldPersist;
|
|
558
584
|
continue;
|
|
559
585
|
}
|
|
560
586
|
if (
|
|
@@ -566,7 +592,10 @@ export async function applyThreadReconciliationPlan(
|
|
|
566
592
|
action.kind === "close-delete-disconnected-instance-topic" ||
|
|
567
593
|
action.kind === "close-delete-expired-pending-provision-topic"
|
|
568
594
|
) {
|
|
569
|
-
if (shouldSkipForStaleLeaderEpoch(action, ports))
|
|
595
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
596
|
+
incompleteActions.push(action);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
570
599
|
if (!ports.callApi) {
|
|
571
600
|
ports.recordRuntimeEvent?.(
|
|
572
601
|
"telegram",
|
|
@@ -578,6 +607,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
578
607
|
threadId: action.target.threadId,
|
|
579
608
|
},
|
|
580
609
|
);
|
|
610
|
+
incompleteActions.push(action);
|
|
581
611
|
continue;
|
|
582
612
|
}
|
|
583
613
|
let deleteConfirmed = false;
|
|
@@ -590,7 +620,11 @@ export async function applyThreadReconciliationPlan(
|
|
|
590
620
|
});
|
|
591
621
|
if (method === "deleteForumTopic") deleteConfirmed = true;
|
|
592
622
|
} catch (error) {
|
|
593
|
-
|
|
623
|
+
const confirmsMethodOutcome =
|
|
624
|
+
method === "deleteForumTopic"
|
|
625
|
+
? isTelegramTopicTargetDeletedOrMissingError(error)
|
|
626
|
+
: isTelegramTopicTargetGoneError(error);
|
|
627
|
+
if (confirmsMethodOutcome) {
|
|
594
628
|
if (method === "deleteForumTopic") deleteConfirmed = true;
|
|
595
629
|
} else {
|
|
596
630
|
ports.recordRuntimeEvent?.("telegram", error, {
|
|
@@ -601,7 +635,10 @@ export async function applyThreadReconciliationPlan(
|
|
|
601
635
|
}
|
|
602
636
|
}
|
|
603
637
|
}
|
|
604
|
-
if (shouldSkipForStaleLeaderEpoch(action, ports))
|
|
638
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
639
|
+
incompleteActions.push(action);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
605
642
|
if (!deleteConfirmed) {
|
|
606
643
|
ports.recordRuntimeEvent?.(
|
|
607
644
|
"telegram",
|
|
@@ -615,6 +652,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
615
652
|
...("messageId" in action ? { messageId: action.messageId } : {}),
|
|
616
653
|
},
|
|
617
654
|
);
|
|
655
|
+
incompleteActions.push(action);
|
|
618
656
|
continue;
|
|
619
657
|
}
|
|
620
658
|
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
@@ -682,12 +720,18 @@ export async function applyThreadReconciliationPlan(
|
|
|
682
720
|
if (shouldPersist) {
|
|
683
721
|
for (const action of persistFences) {
|
|
684
722
|
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
685
|
-
return {
|
|
723
|
+
return {
|
|
724
|
+
changed: true,
|
|
725
|
+
...(incompleteActions.length > 0 ? { incompleteActions } : {}),
|
|
726
|
+
};
|
|
686
727
|
}
|
|
687
728
|
}
|
|
688
729
|
await ports.persist?.();
|
|
689
730
|
}
|
|
690
|
-
return {
|
|
731
|
+
return {
|
|
732
|
+
changed: shouldPersist,
|
|
733
|
+
...(incompleteActions.length > 0 ? { incompleteActions } : {}),
|
|
734
|
+
};
|
|
691
735
|
}
|
|
692
736
|
|
|
693
737
|
export function planThreadReconciliation(
|