@llblab/pi-telegram 0.21.0 → 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.
@@ -10,12 +10,7 @@ import type { TelegramTarget } from "./target.ts";
10
10
  type ThreadTarget = TelegramTarget & { threadId: number };
11
11
 
12
12
  type ThreadRecordStatus =
13
- | "active"
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
- | "stable"
161
- | "provisioning"
162
- | "sync-required"
163
- | "cleanup-required";
155
+ "stable" | "provisioning" | "sync-required" | "cleanup-required";
164
156
 
165
157
  export type ThreadReconciliationEvent =
166
- | "settled"
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 (currentLeaderEpoch !== undefined) {
414
- ports.recordRuntimeEvent?.(
415
- "telegram",
416
- "Thread reconciliation destructive action has no leader epoch",
417
- {
418
- phase: "thread-reconciler-missing-leader-epoch",
419
- action: action.kind,
420
- currentLeaderEpoch,
421
- chatId: action.target.chatId,
422
- threadId: action.target.threadId,
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
- shouldPersist =
551
- ports.markStaleByTarget?.(action.target, "closed") || shouldPersist;
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
- shouldPersist =
628
- ports.markStaleByTarget?.(action.target, "deleted") || shouldPersist;
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
- shouldPersist =
671
- ports.removePendingProvisionById?.(action.pendingProvisionId) ||
672
- shouldPersist;
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