@llblab/pi-telegram 0.52.2 → 0.53.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.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
package/lib/polling.ts CHANGED
@@ -74,7 +74,9 @@ export function getLatestTelegramUpdateId(
74
74
  export class TelegramPersistentGetUpdatesConflictError extends Error {
75
75
  readonly count: number;
76
76
  constructor(count: number) {
77
- super(`Telegram polling stopped after ${count} consecutive getUpdates conflicts.`);
77
+ super(
78
+ `Telegram polling stopped after ${count} consecutive getUpdates conflicts.`,
79
+ );
78
80
  this.name = "TelegramPersistentGetUpdatesConflictError";
79
81
  this.count = count;
80
82
  }
@@ -134,22 +136,13 @@ export interface TelegramPollingStartState {
134
136
  }
135
137
 
136
138
  export type TelegramPollingWorkPhase =
137
- | "long-poll"
138
- | "persisting-journal"
139
- | "persisting-offset"
140
- | "retrying";
139
+ "long-poll" | "persisting-journal" | "persisting-offset" | "retrying";
141
140
 
142
141
  export type TelegramPollingPhase =
143
- | "stopped"
144
- | "starting"
145
- | TelegramPollingWorkPhase;
142
+ "stopped" | "starting" | TelegramPollingWorkPhase;
146
143
 
147
144
  export type TelegramPollingStopReason =
148
- | "not-started"
149
- | "requested"
150
- | "completed"
151
- | "failed"
152
- | "persistent-conflict";
145
+ "not-started" | "requested" | "completed" | "failed" | "persistent-conflict";
153
146
 
154
147
  export interface TelegramPollingStateSnapshot {
155
148
  phase: TelegramPollingPhase;
@@ -162,8 +155,7 @@ export interface TelegramPollingStateSnapshot {
162
155
  stopReason?: TelegramPollingStopReason;
163
156
  }
164
157
 
165
- export interface TelegramPollingControllerState
166
- extends TelegramPollingStateSnapshot {
158
+ export interface TelegramPollingControllerState extends TelegramPollingStateSnapshot {
167
159
  pollingPromise?: Promise<void>;
168
160
  pollingController?: AbortController;
169
161
  }
@@ -185,8 +177,7 @@ function getTelegramPollingStateSnapshot(
185
177
  startedAtMs: state.startedAtMs,
186
178
  stoppedAtMs: state.stoppedAtMs,
187
179
  lastSuccessfulResponseAtMs: state.lastSuccessfulResponseAtMs,
188
- lastSuccessfulResponseUpdateCount:
189
- state.lastSuccessfulResponseUpdateCount,
180
+ lastSuccessfulResponseUpdateCount: state.lastSuccessfulResponseUpdateCount,
190
181
  stopReason: state.stopReason,
191
182
  };
192
183
  }
@@ -263,7 +254,8 @@ export function createTelegramPollingAdmissionRuntime<TContext>(deps: {
263
254
  async start(ctx) {
264
255
  if (!(deps.canStart?.(ctx) ?? true)) return;
265
256
  const expectedGeneration = ++generation;
266
- const isCurrent = () => expectedGeneration === generation && (deps.canStart?.(ctx) ?? true);
257
+ const isCurrent = () =>
258
+ expectedGeneration === generation && (deps.canStart?.(ctx) ?? true);
267
259
  if (!isCurrent()) return;
268
260
  await deps.prepareStart?.();
269
261
  if (!isCurrent()) return;
@@ -333,7 +325,10 @@ export function createTelegramDurablePollingRuntimeAssembly<
333
325
  if (batch.length > 0) deps.prepareUpdateBatch(batch);
334
326
  } catch (error) {
335
327
  try {
336
- deps.recordRuntimeEvent?.("polling", error, { phase: "batch-preparation", updateCount: updates.length });
328
+ deps.recordRuntimeEvent?.("polling", error, {
329
+ phase: "batch-preparation",
330
+ updateCount: updates.length,
331
+ });
337
332
  } catch {
338
333
  // Already-published input must still reach the worker if diagnostics fail.
339
334
  }
@@ -592,8 +587,13 @@ export function startTelegramPollingRuntime<TContext>(
592
587
  if (ownsController) deps.setPollingController(undefined);
593
588
  if (!ownsPromise && !ownsController) return;
594
589
  deps.onPollingStopped?.(
595
- controller.signal.aborted ? "requested" : persistentConflict
596
- ? "persistent-conflict" : failed ? "failed" : "completed",
590
+ controller.signal.aborted
591
+ ? "requested"
592
+ : persistentConflict
593
+ ? "persistent-conflict"
594
+ : failed
595
+ ? "failed"
596
+ : "completed",
597
597
  );
598
598
  // Detach the inner promise before outer teardown calls polling.stop().
599
599
  if (persistentConflict && !controller.signal.aborted) {
@@ -603,11 +603,14 @@ export function startTelegramPollingRuntime<TContext>(
603
603
  } else {
604
604
  deps.stopTypingLoop();
605
605
  deps.recordRuntimeEvent?.("polling", persistentConflict, {
606
- phase: "persistent-conflict", count: persistentConflict.count,
606
+ phase: "persistent-conflict",
607
+ count: persistentConflict.count,
607
608
  });
608
609
  }
609
610
  } catch (error) {
610
- deps.recordRuntimeEvent?.("polling", error, { phase: "conflict-stand-down" });
611
+ deps.recordRuntimeEvent?.("polling", error, {
612
+ phase: "conflict-stand-down",
613
+ });
611
614
  }
612
615
  if (deps.getPollingController() || deps.getPollingPromise()) return;
613
616
  }
@@ -731,7 +734,9 @@ export interface TelegramThreadTargetObservationBinding<TContext> {
731
734
  set(handler: TelegramThreadTargetObservationHandler<TContext>): void;
732
735
  }
733
736
 
734
- export function createTelegramThreadTargetObservationBinding<TContext>(): TelegramThreadTargetObservationBinding<TContext> {
737
+ export function createTelegramThreadTargetObservationBinding<
738
+ TContext,
739
+ >(): TelegramThreadTargetObservationBinding<TContext> {
735
740
  let handler: TelegramThreadTargetObservationHandler<TContext> | undefined;
736
741
  return {
737
742
  async handle(ctx) {
@@ -760,16 +765,8 @@ export interface TelegramThreadAwarePollingPorts<TContext, TOwner> {
760
765
  stopFollowerRegistration: () => void;
761
766
  }
762
767
 
763
- export interface TelegramThreadAwarePollingDeps<
764
- TContext,
765
- TOwner,
766
- > extends TelegramStartupThreadCapabilityProbeDeps {
767
- lifecycle?: TelegramThreadCapabilityLifecycle;
768
- isBusRuntimeEnabled: () => boolean;
769
- isTopicModeUnavailableError: (error: unknown) => boolean;
770
- getPollingStartedWithTelegramBus: () => boolean;
771
- setPollingStartedWithTelegramBus: (started: boolean) => void;
772
- setForceFreshLeaderThreadOnNextStart: (forceFresh: boolean) => void;
768
+ /** Classic/leader polling and follower registration controls that thread-aware polling switches between. */
769
+ export interface TelegramThreadAwarePollingControls<TContext, TOwner> {
773
770
  startClassicPolling: (ctx: TContext) => MaybePromise<void>;
774
771
  stopClassicPolling: () => Promise<void>;
775
772
  startBusLeaderPolling: (ctx: TContext) => Promise<void>;
@@ -786,35 +783,29 @@ export interface TelegramThreadAwarePollingDeps<
786
783
  ) => Promise<boolean | undefined>;
787
784
  hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
788
785
  stopFollowerRegistration: () => void;
786
+ isBusRuntimeEnabled: () => boolean;
787
+ isTopicModeUnavailableError: (error: unknown) => boolean;
789
788
  }
790
789
 
791
- export interface TelegramThreadCapabilityOrchestrationDeps<
792
- TContext,
793
- TOwner,
794
- > extends TelegramThreadCapabilityReaderDeps {
790
+ export interface TelegramThreadAwarePollingDeps<TContext, TOwner>
791
+ extends
792
+ TelegramStartupThreadCapabilityProbeDeps,
793
+ TelegramThreadAwarePollingControls<TContext, TOwner> {
794
+ lifecycle?: TelegramThreadCapabilityLifecycle;
795
+ getPollingStartedWithTelegramBus: () => boolean;
796
+ setPollingStartedWithTelegramBus: (started: boolean) => void;
797
+ setForceFreshLeaderThreadOnNextStart: (forceFresh: boolean) => void;
798
+ }
799
+
800
+ export interface TelegramThreadCapabilityOrchestrationDeps<TContext, TOwner>
801
+ extends
802
+ TelegramThreadCapabilityReaderDeps,
803
+ TelegramThreadAwarePollingControls<TContext, TOwner> {
795
804
  state: TelegramThreadCapabilityStateRuntime;
796
805
  topicTargetStore: TelegramThreadCapabilityStore;
797
- isBusRuntimeEnabled: () => boolean;
798
806
  ownsLock: (ctx: TContext) => boolean;
799
807
  isFollowerRegistered?: () => boolean;
800
- startClassicPolling: (ctx: TContext) => MaybePromise<void>;
801
- stopClassicPolling: () => Promise<void>;
802
- startBusLeaderPolling: (ctx: TContext) => Promise<void>;
803
- stopBusLeaderPolling: () => Promise<void>;
804
- startLeaderHealth: () => void;
805
- stopLeaderHealth: () => void;
806
- registerFollowerWithLeader: (
807
- ctx: TContext,
808
- owner: TOwner,
809
- ) => Promise<boolean | undefined>;
810
- restoreFollowerWithLeader?: (
811
- ctx: TContext,
812
- owner: TOwner,
813
- ) => Promise<boolean | undefined>;
814
- hasRememberedWorkspaceBinding?: (ctx: TContext) => boolean;
815
808
  suspendLiveThreadTarget?: () => void;
816
- stopFollowerRegistration: () => void;
817
- isTopicModeUnavailableError: (error: unknown) => boolean;
818
809
  updateStatus: (ctx: TContext) => void;
819
810
  recordEvent: (
820
811
  category: string,
@@ -864,7 +855,9 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
864
855
  const expected = generation;
865
856
  return () => expected === generation;
866
857
  },
867
- invalidate() { generation++; },
858
+ invalidate() {
859
+ generation++;
860
+ },
868
861
  };
869
862
  const capabilityDeps: TelegramThreadCapabilityRuntimeDeps<TContext> = {
870
863
  lifecycle,
@@ -987,6 +980,32 @@ function hasTelegramThreadCapabilityBindings(
987
980
  );
988
981
  }
989
982
 
983
+ /** Falls back to classic polling, recording a failed restore durably; false once the attempt is stale. */
984
+ async function restoreTelegramClassicPolling<TContext>(
985
+ ctx: TContext,
986
+ phase: string,
987
+ deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
988
+ isCurrent: () => boolean,
989
+ ): Promise<boolean> {
990
+ try {
991
+ await deps.startClassicPolling(ctx);
992
+ return isCurrent();
993
+ } catch (classicError) {
994
+ if (!isCurrent()) return false;
995
+ deps.topicTargetStore.setBotState({
996
+ threadMode: "disabled",
997
+ updatedAtMs: (deps.getNowMs ?? Date.now)(),
998
+ lastReconcileAction: `${phase}-classic-restore-failed`,
999
+ });
1000
+ await deps.topicTargetStore.persist();
1001
+ if (!isCurrent()) return false;
1002
+ deps.recordEvent("bus", classicError, {
1003
+ phase: `${phase}-classic-restore`,
1004
+ });
1005
+ return true;
1006
+ }
1007
+ }
1008
+
990
1009
  export async function applyTelegramThreadCapability<TContext>(
991
1010
  ctx: TContext,
992
1011
  threadModeEnabled: boolean,
@@ -1029,22 +1048,8 @@ export async function applyTelegramThreadCapability<TContext>(
1029
1048
  if (!isCurrent()) return;
1030
1049
  deps.setPollingStartedWithTelegramBus(false);
1031
1050
  if (hadLiveThreadTransport) deps.suspendLiveThreadTarget?.();
1032
- try {
1033
- await deps.startClassicPolling(ctx);
1034
- if (!isCurrent()) return;
1035
- } catch (classicError) {
1036
- if (!isCurrent()) return;
1037
- deps.topicTargetStore.setBotState({
1038
- threadMode: "disabled",
1039
- updatedAtMs: (deps.getNowMs ?? Date.now)(),
1040
- lastReconcileAction: `${phase}-classic-restore-failed`,
1041
- });
1042
- await deps.topicTargetStore.persist();
1043
- if (!isCurrent()) return;
1044
- deps.recordEvent("bus", classicError, {
1045
- phase: `${phase}-classic-restore`,
1046
- });
1047
- }
1051
+ if (!(await restoreTelegramClassicPolling(ctx, phase, deps, isCurrent)))
1052
+ return;
1048
1053
  }
1049
1054
  deps.updateStatus(ctx);
1050
1055
  return;
@@ -1081,22 +1086,8 @@ export async function applyTelegramThreadCapability<TContext>(
1081
1086
  if (!isCurrent()) return;
1082
1087
  deps.setTopicModeUnavailable(true);
1083
1088
  }
1084
- try {
1085
- await deps.startClassicPolling(ctx);
1086
- if (!isCurrent()) return;
1087
- } catch (classicError) {
1088
- if (!isCurrent()) return;
1089
- deps.topicTargetStore.setBotState({
1090
- threadMode: "disabled",
1091
- updatedAtMs: (deps.getNowMs ?? Date.now)(),
1092
- lastReconcileAction: `${phase}-classic-restore-failed`,
1093
- });
1094
- await deps.topicTargetStore.persist();
1095
- if (!isCurrent()) return;
1096
- deps.recordEvent("bus", classicError, {
1097
- phase: `${phase}-classic-restore`,
1098
- });
1099
- }
1089
+ if (!(await restoreTelegramClassicPolling(ctx, phase, deps, isCurrent)))
1090
+ return;
1100
1091
  deps.updateStatus(ctx);
1101
1092
  if (threadModeUnavailable) return;
1102
1093
  throw error;
@@ -1116,12 +1107,16 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1116
1107
  const expectedGeneration = ++generation;
1117
1108
  deps.lifecycle?.invalidate();
1118
1109
  const isLifecycleCurrent = deps.lifecycle?.capture() ?? (() => true);
1119
- const isCurrent = () => expectedGeneration === generation && isLifecycleCurrent();
1110
+ const isCurrent = () =>
1111
+ expectedGeneration === generation && isLifecycleCurrent();
1120
1112
  await deps.topicTargetStore.load();
1121
1113
  if (!isCurrent()) return;
1122
1114
  let startupThreadCapability: boolean | undefined;
1123
1115
  try {
1124
- startupThreadCapability = await probeTelegramStartupThreadCapability(deps, isCurrent);
1116
+ startupThreadCapability = await probeTelegramStartupThreadCapability(
1117
+ deps,
1118
+ isCurrent,
1119
+ );
1125
1120
  } catch (error) {
1126
1121
  if (!isCurrent()) return;
1127
1122
  deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
@@ -1168,7 +1163,8 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1168
1163
  deps.stopLeaderHealth();
1169
1164
  if (deps.getPollingStartedWithTelegramBus()) {
1170
1165
  await deps.stopBusLeaderPolling();
1171
- if (expectedGeneration === generation) deps.setPollingStartedWithTelegramBus(false);
1166
+ if (expectedGeneration === generation)
1167
+ deps.setPollingStartedWithTelegramBus(false);
1172
1168
  return;
1173
1169
  }
1174
1170
  await deps.stopClassicPolling();
@@ -1193,13 +1189,25 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
1193
1189
  owner: TOwner,
1194
1190
  ): Promise<boolean | undefined> => {
1195
1191
  if (!(await refreshFollowerState())) {
1196
- deps.recordEvent("bus", "Telegram follower auto-connect skipped: Threaded Mode is not enabled.",
1197
- { phase: "follower-auto-connect-skip", reason: "thread-mode-unavailable" });
1192
+ deps.recordEvent(
1193
+ "bus",
1194
+ "Telegram follower auto-connect skipped: Threaded Mode is not enabled.",
1195
+ {
1196
+ phase: "follower-auto-connect-skip",
1197
+ reason: "thread-mode-unavailable",
1198
+ },
1199
+ );
1198
1200
  return undefined;
1199
1201
  }
1200
1202
  if (!deps.hasRememberedWorkspaceBinding?.(ctx)) {
1201
- deps.recordEvent("bus", "Telegram follower auto-connect skipped: no binding for this session.",
1202
- { phase: "follower-auto-connect-skip", reason: "session-binding-unavailable" });
1203
+ deps.recordEvent(
1204
+ "bus",
1205
+ "Telegram follower auto-connect skipped: no binding for this session.",
1206
+ {
1207
+ phase: "follower-auto-connect-skip",
1208
+ reason: "session-binding-unavailable",
1209
+ },
1210
+ );
1203
1211
  return undefined;
1204
1212
  }
1205
1213
  return deps.restoreFollowerWithLeader?.(ctx, owner);
@@ -1270,7 +1278,8 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1270
1278
  }
1271
1279
  const expectedGeneration = generation;
1272
1280
  const isLifecycleCurrent = deps.lifecycle?.capture() ?? (() => true);
1273
- const isCurrent = (): boolean => generation === expectedGeneration && isLifecycleCurrent();
1281
+ const isCurrent = (): boolean =>
1282
+ generation === expectedGeneration && isLifecycleCurrent();
1274
1283
  let tracked: Promise<void>;
1275
1284
  tracked = readTelegramThreadCapability(deps)
1276
1285
  .then(async (threadModeEnabled) => {
@@ -1363,7 +1372,11 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
1363
1372
  try {
1364
1373
  check(ctx);
1365
1374
  } catch (error) {
1366
- try { stop(); } catch { /* Timer shutdown must not escape the callback. */ }
1375
+ try {
1376
+ stop();
1377
+ } catch {
1378
+ /* Timer shutdown must not escape the callback. */
1379
+ }
1367
1380
  try {
1368
1381
  deps.recordEvent("bus", error, { phase: "capability-monitor" });
1369
1382
  } catch {
@@ -1487,13 +1500,10 @@ export async function admitTelegramPollingUpdateBatch<
1487
1500
  return { updateCount: deps.updates.length, latestUpdateId };
1488
1501
  }
1489
1502
 
1490
- export interface TelegramPollLoopDeps<
1503
+ /** Bot API, journal and progress ports shared by one poll loop and its restartable runner. */
1504
+ export interface TelegramPollLoopTransportPorts<
1491
1505
  TUpdate extends TelegramUpdate,
1492
- TContext = unknown,
1493
1506
  > extends TelegramRuntimeEventRecorderPort {
1494
- ctx: TContext;
1495
- signal: AbortSignal;
1496
- config: TelegramPollingConfig;
1497
1507
  deleteWebhook: (signal: AbortSignal) => Promise<unknown>;
1498
1508
  getUpdates: (
1499
1509
  body: Record<string, unknown>,
@@ -1508,9 +1518,6 @@ export interface TelegramPollLoopDeps<
1508
1518
  getAcceptedThroughUpdateId?: () => number | undefined;
1509
1519
  getJournalEntryCount: () => number;
1510
1520
  signalUpdateWorker: () => void;
1511
- onErrorStatus: (message: string) => void;
1512
- onStatusReset: () => void;
1513
- sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
1514
1521
  onPhaseChange?: (
1515
1522
  phase: TelegramPollingWorkPhase,
1516
1523
  currentUpdateId?: number,
@@ -1518,32 +1525,25 @@ export interface TelegramPollLoopDeps<
1518
1525
  onSuccessfulResponse?: (updateCount: number) => void;
1519
1526
  }
1520
1527
 
1528
+ export interface TelegramPollLoopDeps<
1529
+ TUpdate extends TelegramUpdate,
1530
+ TContext = unknown,
1531
+ > extends TelegramPollLoopTransportPorts<TUpdate> {
1532
+ ctx: TContext;
1533
+ signal: AbortSignal;
1534
+ config: TelegramPollingConfig;
1535
+ onErrorStatus: (message: string) => void;
1536
+ onStatusReset: () => void;
1537
+ sleep: (ms: number, signal?: AbortSignal) => Promise<void>;
1538
+ }
1539
+
1521
1540
  export interface TelegramPollLoopRunnerDeps<
1522
1541
  TUpdate extends TelegramUpdate,
1523
1542
  TContext = unknown,
1524
- > extends TelegramRuntimeEventRecorderPort {
1543
+ > extends TelegramPollLoopTransportPorts<TUpdate> {
1525
1544
  getConfig: () => TelegramPollingConfig;
1526
- deleteWebhook: (signal: AbortSignal) => Promise<unknown>;
1527
- getUpdates: (
1528
- body: Record<string, unknown>,
1529
- signal: AbortSignal,
1530
- ) => Promise<TUpdate[]>;
1531
- getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
1532
- persistConfig: (config: TelegramPollingConfig) => Promise<void>;
1533
- appendUpdateBatch: (
1534
- updates: readonly TUpdate[],
1535
- acceptedThroughUpdateId?: number,
1536
- ) => MaybePromise<unknown>;
1537
- getAcceptedThroughUpdateId?: () => number | undefined;
1538
- getJournalEntryCount: () => number;
1539
- signalUpdateWorker: () => void;
1540
1545
  updateStatus: (ctx: TContext, message?: string) => void;
1541
1546
  sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
1542
- onPhaseChange?: (
1543
- phase: TelegramPollingWorkPhase,
1544
- currentUpdateId?: number,
1545
- ) => void;
1546
- onSuccessfulResponse?: (updateCount: number) => void;
1547
1547
  }
1548
1548
 
1549
1549
  export function sleepTelegramPollingRetry(
@@ -1641,10 +1641,7 @@ function reportTelegramPollingPhase(
1641
1641
  function reportTelegramPollingResponse<
1642
1642
  TUpdate extends TelegramUpdate,
1643
1643
  TContext,
1644
- >(
1645
- deps: TelegramPollLoopDeps<TUpdate, TContext>,
1646
- updateCount: number,
1647
- ): void {
1644
+ >(deps: TelegramPollLoopDeps<TUpdate, TContext>, updateCount: number): void {
1648
1645
  try {
1649
1646
  deps.onSuccessfulResponse?.(updateCount);
1650
1647
  } catch (error) {
@@ -1724,11 +1721,16 @@ export async function runTelegramPollLoop<
1724
1721
  let consecutiveFailures = 0;
1725
1722
  const retryConflict = async () => {
1726
1723
  consecutiveGetUpdatesConflicts += 1;
1727
- if (consecutiveGetUpdatesConflicts >= TELEGRAM_GET_UPDATES_CONFLICT_STOP_LIMIT) {
1728
- throw new TelegramPersistentGetUpdatesConflictError(consecutiveGetUpdatesConflicts);
1724
+ if (
1725
+ consecutiveGetUpdatesConflicts >= TELEGRAM_GET_UPDATES_CONFLICT_STOP_LIMIT
1726
+ ) {
1727
+ throw new TelegramPersistentGetUpdatesConflictError(
1728
+ consecutiveGetUpdatesConflicts,
1729
+ );
1729
1730
  }
1730
1731
  await deps.sleep(
1731
- consecutiveGetUpdatesConflicts < TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT
1732
+ consecutiveGetUpdatesConflicts <
1733
+ TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_LIMIT
1732
1734
  ? TELEGRAM_GET_UPDATES_CONFLICT_FAST_RETRY_MS
1733
1735
  : TELEGRAM_GET_UPDATES_CONFLICT_SLOW_RETRY_MS,
1734
1736
  deps.signal,
@@ -1747,9 +1749,7 @@ export async function runTelegramPollLoop<
1747
1749
  "Telegram polling cursor is missing while the durable update journal is non-empty.",
1748
1750
  );
1749
1751
  }
1750
- if (
1751
- deps.getAcceptedThroughUpdateId?.() === undefined
1752
- ) {
1752
+ if (deps.getAcceptedThroughUpdateId?.() === undefined) {
1753
1753
  try {
1754
1754
  const request = buildTelegramInitialSyncRequest();
1755
1755
  reportTelegramPollingPhase(deps, "long-poll");
@@ -1757,11 +1757,7 @@ export async function runTelegramPollLoop<
1757
1757
  reportTelegramPollingResponse(deps, updates.length);
1758
1758
  const lastUpdateId = getLatestTelegramUpdateId(updates);
1759
1759
  if (lastUpdateId !== undefined) {
1760
- reportTelegramPollingPhase(
1761
- deps,
1762
- "persisting-offset",
1763
- lastUpdateId,
1764
- );
1760
+ reportTelegramPollingPhase(deps, "persisting-offset", lastUpdateId);
1765
1761
  await deps.appendUpdateBatch([], lastUpdateId);
1766
1762
  deps.recordRuntimeEvent?.(
1767
1763
  "polling",
@@ -1774,12 +1770,13 @@ export async function runTelegramPollLoop<
1774
1770
  reportTelegramPollingPhase(deps, "retrying");
1775
1771
  if (isTelegramGetUpdatesConflictError(error)) {
1776
1772
  await retryConflict();
1777
- } else deps.recordRuntimeEvent?.("polling", error, {
1778
- phase: "initial-sync",
1779
- ...(error instanceof TelegramGetUpdatesTimeoutError
1780
- ? { timeoutMs: error.timeoutMs }
1781
- : {}),
1782
- });
1773
+ } else
1774
+ deps.recordRuntimeEvent?.("polling", error, {
1775
+ phase: "initial-sync",
1776
+ ...(error instanceof TelegramGetUpdatesTimeoutError
1777
+ ? { timeoutMs: error.timeoutMs }
1778
+ : {}),
1779
+ });
1783
1780
  }
1784
1781
  }
1785
1782
  let currentUpdateId: number | undefined;