@llblab/pi-telegram 0.21.1 → 0.22.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 +6 -4
- package/CHANGELOG.md +474 -452
- package/docs/architecture.md +12 -10
- package/docs/delivery.md +2 -1
- package/docs/locks.md +9 -5
- package/docs/multi-instance-bus.md +5 -5
- package/index.ts +311 -633
- package/lib/bindings.ts +59 -21
- package/lib/bus-api.ts +12 -2
- package/lib/bus-follower.ts +327 -83
- package/lib/bus-leader.ts +201 -121
- package/lib/bus.ts +345 -37
- package/lib/config.ts +168 -28
- package/lib/delivery.ts +148 -61
- package/lib/lifecycle.ts +199 -8
- package/lib/locks.ts +569 -56
- package/lib/logs.ts +178 -19
- package/lib/media.ts +79 -24
- package/lib/model.ts +5 -10
- package/lib/ownership.ts +150 -6
- package/lib/polling.ts +156 -7
- package/lib/preview.ts +15 -3
- package/lib/queue.ts +167 -20
- package/lib/routing.ts +68 -12
- package/lib/sync.ts +105 -32
- package/lib/telegram-api.ts +86 -10
- package/lib/text-groups.ts +71 -15
- package/lib/thread-reconciler.ts +49 -36
- package/lib/threads.ts +505 -118
- package/package.json +1 -1
package/lib/thread-reconciler.ts
CHANGED
|
@@ -10,12 +10,7 @@ import type { TelegramTarget } from "./target.ts";
|
|
|
10
10
|
type ThreadTarget = TelegramTarget & { threadId: number };
|
|
11
11
|
|
|
12
12
|
type ThreadRecordStatus =
|
|
13
|
-
| "
|
|
14
|
-
| "offline"
|
|
15
|
-
| "stale"
|
|
16
|
-
| "pending"
|
|
17
|
-
| "starting"
|
|
18
|
-
| "failed";
|
|
13
|
+
"active" | "offline" | "stale" | "pending" | "starting" | "failed";
|
|
19
14
|
|
|
20
15
|
export interface ThreadReconciliationRecord {
|
|
21
16
|
target: ThreadTarget;
|
|
@@ -157,16 +152,10 @@ export type ThreadReconciliationAction =
|
|
|
157
152
|
};
|
|
158
153
|
|
|
159
154
|
export type ThreadReconciliationPhase =
|
|
160
|
-
| "
|
|
161
|
-
| "provisioning"
|
|
162
|
-
| "sync-required"
|
|
163
|
-
| "cleanup-required";
|
|
155
|
+
"stable" | "provisioning" | "sync-required" | "cleanup-required";
|
|
164
156
|
|
|
165
157
|
export type ThreadReconciliationEvent =
|
|
166
|
-
| "
|
|
167
|
-
| "pending-provision"
|
|
168
|
-
| "sync-required"
|
|
169
|
-
| "cleanup-required";
|
|
158
|
+
"settled" | "pending-provision" | "sync-required" | "cleanup-required";
|
|
170
159
|
|
|
171
160
|
export interface ThreadReconciliationMachineState {
|
|
172
161
|
phase: ThreadReconciliationPhase;
|
|
@@ -410,20 +399,19 @@ function shouldSkipForStaleLeaderEpoch(
|
|
|
410
399
|
const actionLeaderEpoch = getActionLeaderEpoch(action);
|
|
411
400
|
const currentLeaderEpoch = ports.getCurrentLeaderEpoch?.();
|
|
412
401
|
if (actionLeaderEpoch === undefined) {
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return false;
|
|
402
|
+
if (!ports.getCurrentLeaderEpoch) return false;
|
|
403
|
+
ports.recordRuntimeEvent?.(
|
|
404
|
+
"telegram",
|
|
405
|
+
"Thread reconciliation destructive action has no leader epoch",
|
|
406
|
+
{
|
|
407
|
+
phase: "thread-reconciler-missing-leader-epoch",
|
|
408
|
+
currentLeaderEpoch,
|
|
409
|
+
action: action.kind,
|
|
410
|
+
chatId: action.target.chatId,
|
|
411
|
+
threadId: action.target.threadId,
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
return true;
|
|
427
415
|
}
|
|
428
416
|
if (currentLeaderEpoch === actionLeaderEpoch) return false;
|
|
429
417
|
ports.recordRuntimeEvent?.(
|
|
@@ -492,6 +480,7 @@ export function createThreadReconciliationRuntime(deps: {
|
|
|
492
480
|
export function planDisconnectedInstanceThreadCleanup(input: {
|
|
493
481
|
target: TelegramTarget & { threadId: number };
|
|
494
482
|
instanceId?: string;
|
|
483
|
+
leaderEpoch?: number | string;
|
|
495
484
|
}): ThreadReconciliationPlan {
|
|
496
485
|
return {
|
|
497
486
|
actions: [
|
|
@@ -500,6 +489,9 @@ export function planDisconnectedInstanceThreadCleanup(input: {
|
|
|
500
489
|
target: input.target,
|
|
501
490
|
reason: "manual-disconnect",
|
|
502
491
|
instanceId: input.instanceId,
|
|
492
|
+
...(input.leaderEpoch !== undefined
|
|
493
|
+
? { leaderEpoch: input.leaderEpoch }
|
|
494
|
+
: {}),
|
|
503
495
|
},
|
|
504
496
|
],
|
|
505
497
|
};
|
|
@@ -510,6 +502,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
510
502
|
ports: ThreadReconciliationApplyPorts,
|
|
511
503
|
): Promise<ThreadReconciliationApplyResult> {
|
|
512
504
|
let shouldPersist = false;
|
|
505
|
+
const persistFences: ThreadReconciliationAction[] = [];
|
|
513
506
|
for (const action of plan.actions) {
|
|
514
507
|
if (action.kind === "remove-reservation") {
|
|
515
508
|
shouldPersist =
|
|
@@ -543,12 +536,17 @@ export async function applyThreadReconciliationPlan(
|
|
|
543
536
|
continue;
|
|
544
537
|
}
|
|
545
538
|
try {
|
|
539
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
546
540
|
await ports.callApi("closeForumTopic", {
|
|
547
541
|
chat_id: action.target.chatId,
|
|
548
542
|
message_thread_id: action.target.threadId,
|
|
549
543
|
});
|
|
550
|
-
|
|
551
|
-
|
|
544
|
+
if (!shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
545
|
+
const changed =
|
|
546
|
+
ports.markStaleByTarget?.(action.target, "closed") ?? false;
|
|
547
|
+
if (changed) persistFences.push(action);
|
|
548
|
+
shouldPersist = changed || shouldPersist;
|
|
549
|
+
}
|
|
552
550
|
} catch (error) {
|
|
553
551
|
ports.recordRuntimeEvent?.("telegram", error, {
|
|
554
552
|
phase: `thread-reconciler-${action.reason}-closeForumTopic`,
|
|
@@ -584,6 +582,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
584
582
|
}
|
|
585
583
|
let deleteConfirmed = false;
|
|
586
584
|
for (const method of ["closeForumTopic", "deleteForumTopic"]) {
|
|
585
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) break;
|
|
587
586
|
try {
|
|
588
587
|
await ports.callApi(method, {
|
|
589
588
|
chat_id: action.target.chatId,
|
|
@@ -602,6 +601,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
602
601
|
}
|
|
603
602
|
}
|
|
604
603
|
}
|
|
604
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
605
605
|
if (!deleteConfirmed) {
|
|
606
606
|
ports.recordRuntimeEvent?.(
|
|
607
607
|
"telegram",
|
|
@@ -617,6 +617,7 @@ export async function applyThreadReconciliationPlan(
|
|
|
617
617
|
);
|
|
618
618
|
continue;
|
|
619
619
|
}
|
|
620
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
620
621
|
if (
|
|
621
622
|
action.kind !== "close-delete-previous-leader-topic" &&
|
|
622
623
|
action.kind !== "close-delete-pruned-follower-topic" &&
|
|
@@ -624,8 +625,10 @@ export async function applyThreadReconciliationPlan(
|
|
|
624
625
|
action.kind !== "close-delete-disconnected-instance-topic" &&
|
|
625
626
|
action.kind !== "close-delete-expired-pending-provision-topic"
|
|
626
627
|
) {
|
|
627
|
-
|
|
628
|
-
ports.markStaleByTarget?.(action.target, "deleted")
|
|
628
|
+
const changed =
|
|
629
|
+
ports.markStaleByTarget?.(action.target, "deleted") ?? false;
|
|
630
|
+
if (changed) persistFences.push(action);
|
|
631
|
+
shouldPersist = changed || shouldPersist;
|
|
629
632
|
}
|
|
630
633
|
ports.recordRuntimeEvent?.(
|
|
631
634
|
"telegram",
|
|
@@ -667,13 +670,23 @@ export async function applyThreadReconciliationPlan(
|
|
|
667
670
|
action.kind === "close-delete-expired-pending-provision-topic" &&
|
|
668
671
|
deleteConfirmed
|
|
669
672
|
) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) continue;
|
|
674
|
+
const changed =
|
|
675
|
+
ports.removePendingProvisionById?.(action.pendingProvisionId) ??
|
|
676
|
+
false;
|
|
677
|
+
if (changed) persistFences.push(action);
|
|
678
|
+
shouldPersist = changed || shouldPersist;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (shouldPersist) {
|
|
683
|
+
for (const action of persistFences) {
|
|
684
|
+
if (shouldSkipForStaleLeaderEpoch(action, ports)) {
|
|
685
|
+
return { changed: true };
|
|
673
686
|
}
|
|
674
687
|
}
|
|
688
|
+
await ports.persist?.();
|
|
675
689
|
}
|
|
676
|
-
if (shouldPersist) await ports.persist?.();
|
|
677
690
|
return { changed: shouldPersist };
|
|
678
691
|
}
|
|
679
692
|
|