@llblab/pi-telegram 0.24.3 → 0.24.5

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/lib/bindings.ts CHANGED
@@ -99,6 +99,7 @@ interface TelegramCommandsAndToolsBindingDeps {
99
99
  activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
100
100
  lockedPollingRuntime: Locks.TelegramLockedPollingRuntime<Pi.ExtensionContext>;
101
101
  stopPolling?: () => Promise<void | string>;
102
+ recoverPollingStart?: Commands.TelegramBridgeCommandRegistrationDeps["recoverPollingStart"];
102
103
  getDisconnectThreadName?: () => string | undefined;
103
104
  onTransportChanged?: () => Promise<void> | void;
104
105
  getStatusLines: (
@@ -127,6 +128,7 @@ export function registerTelegramCommandsAndTools({
127
128
  activeTurnRuntime,
128
129
  lockedPollingRuntime,
129
130
  stopPolling,
131
+ recoverPollingStart,
130
132
  getDisconnectThreadName,
131
133
  onTransportChanged,
132
134
  getStatusLines,
@@ -234,8 +236,16 @@ export function registerTelegramCommandsAndTools({
234
236
  getStatusLines,
235
237
  reloadConfig: configStore.load,
236
238
  hasBotToken: configStore.hasBotToken,
237
- startPolling: lockedPollingRuntime.start,
239
+ startPolling: async (ctx, options) => {
240
+ try {
241
+ return await lockedPollingRuntime.start(ctx, options);
242
+ } catch (error) {
243
+ recordRuntimeEvent("recovery", error, { phase: "polling-start" });
244
+ throw error;
245
+ }
246
+ },
238
247
  stopPolling: stopPolling ?? lockedPollingRuntime.stop,
248
+ recoverPollingStart,
239
249
  getDisconnectThreadName,
240
250
  updateStatus,
241
251
  getProfileNames: () =>
@@ -295,10 +305,9 @@ interface TelegramLifecycleBindingDeps {
295
305
  >;
296
306
  promptDispatchRuntime: Runtime.TelegramPromptDispatchRuntime<Pi.ExtensionContext>;
297
307
  deferredQueueDispatchRuntime: Queue.TelegramDeferredQueueDispatchRuntime<Pi.ExtensionContext>;
298
- lockOwnershipGuard: Pick<
299
- Locks.TelegramLockOwnershipGuard<Pi.ExtensionContext>,
300
- "ownsContext"
301
- >;
308
+ modelContextAvailabilityRuntime: Prompts.TelegramModelContextAvailabilityRuntime;
309
+ disconnectOnQuit?: () => Promise<unknown>;
310
+ resolveAutomaticThreadCleanupEnabled?: () => boolean | Promise<boolean>;
302
311
  buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
303
312
  callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
304
313
  sendChatAction: NonNullable<
@@ -338,7 +347,6 @@ interface TelegramLifecycleBindingDeps {
338
347
  Keyboard.TelegramInlineKeyboardMarkup
339
348
  >["finalizeMarkdownPreview"];
340
349
  proactivePushTargetGetter: () => Queue.TelegramQueueTarget | undefined;
341
- isProactivePushEnabled: () => boolean;
342
350
  getAssistantRenderingMode: () => "rich" | "html";
343
351
  recordMessageOwnership?: (input: {
344
352
  chatId: number;
@@ -367,7 +375,9 @@ export function registerTelegramLifecycleRuntimeHooks({
367
375
  previewRuntime,
368
376
  promptDispatchRuntime,
369
377
  deferredQueueDispatchRuntime,
370
- lockOwnershipGuard,
378
+ modelContextAvailabilityRuntime,
379
+ disconnectOnQuit,
380
+ resolveAutomaticThreadCleanupEnabled,
371
381
  buttonActionStore,
372
382
  callMultipart,
373
383
  sendChatAction,
@@ -380,7 +390,6 @@ export function registerTelegramLifecycleRuntimeHooks({
380
390
  sendGuestReply,
381
391
  finalizeMarkdownPreview,
382
392
  proactivePushTargetGetter,
383
- isProactivePushEnabled,
384
393
  getAssistantRenderingMode,
385
394
  recordMessageOwnership,
386
395
  canSendAgentActivity,
@@ -629,6 +638,7 @@ export function registerTelegramLifecycleRuntimeHooks({
629
638
  previewRuntime.invalidate();
630
639
  assistantOutputRuntime.start();
631
640
  activityRuntime.onSessionStart?.();
641
+ modelContextAvailabilityRuntime.reconcile();
632
642
  await sessionLifecycleRuntime.onSessionStart(event, ctx);
633
643
  },
634
644
  async onSessionShutdown(event, ctx) {
@@ -637,6 +647,17 @@ export function registerTelegramLifecycleRuntimeHooks({
637
647
  activityRuntime.onSessionShutdown();
638
648
  assistantOutputRuntime.stop();
639
649
  compactionObserver.onSessionShutdown();
650
+ if (event.reason === "quit" && disconnectOnQuit) {
651
+ try {
652
+ const automaticCleanupEnabled =
653
+ (await resolveAutomaticThreadCleanupEnabled?.()) ?? true;
654
+ if (automaticCleanupEnabled) await disconnectOnQuit();
655
+ } catch (error) {
656
+ recordRuntimeEvent("session", error, {
657
+ phase: "automatic-disconnect-on-quit",
658
+ });
659
+ }
660
+ }
640
661
  await sessionLifecycleRuntime.onSessionShutdown(event, ctx);
641
662
  },
642
663
  onSessionBeforeCompact(event, ctx) {
@@ -704,11 +725,11 @@ export function registerTelegramLifecycleRuntimeHooks({
704
725
  if (!isSessionContextActive(ctx)) return;
705
726
  await agentLifecycleHooks.onAgentSettled(event, ctx);
706
727
  activityRuntime.onAgentSettled();
728
+ modelContextAvailabilityRuntime.reconcile();
707
729
  },
708
730
  onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
709
- isConfigured: configStore.hasBotToken,
710
- isProactivePushEnabled,
711
- isCurrentOwner: lockOwnershipGuard.ownsContext,
731
+ reconcileAvailability: modelContextAvailabilityRuntime.reconcile,
732
+ isAvailable: canSendAgentActivity,
712
733
  }),
713
734
  });
714
735
  }
@@ -138,6 +138,17 @@ export interface TelegramBusFollowerSessionRefreshHookDeps<TContext> {
138
138
  ) => void;
139
139
  }
140
140
 
141
+ export type TelegramBusFollowerControlLifecyclePhase = "electing";
142
+
143
+ export interface TelegramBusFollowerControlState {
144
+ getActiveAuthSecret: () => string | undefined;
145
+ setActiveAuthSecret: (secret: string | undefined) => void;
146
+ getLifecyclePhase: () => TelegramBusFollowerControlLifecyclePhase | undefined;
147
+ setLifecyclePhase: (
148
+ phase: TelegramBusFollowerControlLifecyclePhase | undefined,
149
+ ) => void;
150
+ }
151
+
141
152
  export interface TelegramBusFollowerRegistrationState {
142
153
  isRegistered: () => boolean;
143
154
  getTarget: () => TelegramTarget | undefined;
@@ -841,7 +852,24 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
841
852
  };
842
853
  }
843
854
 
844
- export function createTelegramBusFollowerRegistrationState(): TelegramBusFollowerRegistrationState {
855
+ export function createTelegramBusFollowerControlState(): TelegramBusFollowerControlState {
856
+ let activeAuthSecret: string | undefined;
857
+ let lifecyclePhase: TelegramBusFollowerControlLifecyclePhase | undefined;
858
+ return {
859
+ getActiveAuthSecret: () => activeAuthSecret,
860
+ setActiveAuthSecret(secret) {
861
+ activeAuthSecret = secret;
862
+ },
863
+ getLifecyclePhase: () => lifecyclePhase,
864
+ setLifecyclePhase(phase) {
865
+ lifecyclePhase = phase;
866
+ },
867
+ };
868
+ }
869
+
870
+ export function createTelegramBusFollowerRegistrationState(
871
+ options: { onAvailabilityChanged?: () => void } = {},
872
+ ): TelegramBusFollowerRegistrationState {
845
873
  let registered = false;
846
874
  let target: TelegramTarget | undefined;
847
875
  let slot: string | undefined;
@@ -861,11 +889,13 @@ export function createTelegramBusFollowerRegistrationState(): TelegramBusFollowe
861
889
  ).sort();
862
890
  },
863
891
  setRegistered: (next, nextTarget, metadata) => {
892
+ const availabilityChanged = registered !== next;
864
893
  registered = next;
865
894
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
866
895
  slot = next ? metadata?.slot : undefined;
867
896
  threadName = next ? metadata?.threadName : undefined;
868
897
  generation = next ? metadata?.generation : undefined;
898
+ if (availabilityChanged) options.onAvailabilityChanged?.();
869
899
  },
870
900
  };
871
901
  }
package/lib/commands.ts CHANGED
@@ -297,6 +297,11 @@ export interface TelegramBridgeCommandStartPollingResult {
297
297
  owner?: string;
298
298
  }
299
299
 
300
+ export type TelegramPollingStartRecoveryResult =
301
+ | { kind: "unhandled" }
302
+ | { kind: "retry"; message: string }
303
+ | { kind: "blocked"; message: string };
304
+
300
305
  export interface TelegramBridgeCommandRegistrationDeps {
301
306
  promptForConfig: (ctx: ExtensionCommandContext, profileName?: string) => Promise<void>;
302
307
  getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
@@ -310,6 +315,9 @@ export interface TelegramBridgeCommandRegistrationDeps {
310
315
  | Promise<void | TelegramBridgeCommandStartPollingResult>
311
316
  | TelegramBridgeCommandStartPollingResult;
312
317
  stopPolling: () => Promise<void | string>;
318
+ recoverPollingStart?: (
319
+ error: unknown,
320
+ ) => Promise<TelegramPollingStartRecoveryResult>;
313
321
  getDisconnectThreadName?: () => string | undefined;
314
322
  updateStatus: (ctx: ExtensionCommandContext) => void;
315
323
  getProfileNames?: () => string[];
@@ -390,7 +398,41 @@ export function registerTelegramBridgeCommands(
390
398
  await deps.promptForConfig(ctx, profileName);
391
399
  return;
392
400
  }
393
- let result = await deps.startPolling(ctx, {
401
+ let recoveryUsed = false;
402
+ const startWithRecovery = async (
403
+ options: TelegramBridgeCommandStartPollingOptions,
404
+ ): Promise<void | TelegramBridgeCommandStartPollingResult> => {
405
+ try {
406
+ return await deps.startPolling(ctx, options);
407
+ } catch (error) {
408
+ if (!deps.recoverPollingStart || recoveryUsed) throw error;
409
+ const recovery = await deps.recoverPollingStart(error);
410
+ if (recovery.kind === "unhandled") throw error;
411
+ if (recovery.kind === "blocked") {
412
+ return { ok: false, message: recovery.message };
413
+ }
414
+ recoveryUsed = true;
415
+ try {
416
+ const retry = await deps.startPolling(ctx, options);
417
+ if (!retry) {
418
+ return { ok: true, message: recovery.message };
419
+ }
420
+ return {
421
+ ...retry,
422
+ message: retry.ok
423
+ ? `${recovery.message} ${retry.message ?? "Telegram bridge connected."}`
424
+ : retry.message,
425
+ };
426
+ } catch {
427
+ return {
428
+ ok: false,
429
+ message:
430
+ "Telegram temporary state was recovered, but the bridge could not restart. Restart this Pi instance and run /telegram-connect again.",
431
+ };
432
+ }
433
+ }
434
+ };
435
+ let result = await startWithRecovery({
394
436
  forceFreshLeaderThread: true,
395
437
  });
396
438
  if (result && !result.ok && result.canTakeover) {
@@ -403,7 +445,7 @@ export function registerTelegramBridgeCommands(
403
445
  deps.updateStatus(ctx);
404
446
  return;
405
447
  }
406
- result = await deps.startPolling(ctx, {
448
+ result = await startWithRecovery({
407
449
  force: true,
408
450
  forceFreshLeaderThread: true,
409
451
  });
package/lib/config.ts CHANGED
@@ -87,6 +87,10 @@ export interface TelegramConfig {
87
87
  sendTranscript?: boolean;
88
88
  };
89
89
  time?: TelegramTimeConfig;
90
+ threads?: {
91
+ /** Delete this instance's bound Telegram thread on graceful Pi quit. */
92
+ automaticCleanup?: boolean;
93
+ };
90
94
  /** Canonical bot/session profiles, including profiles.default. */
91
95
  profiles?: Record<string, TelegramBotProfile>;
92
96
  }
@@ -140,6 +144,7 @@ export interface TelegramConfigStore {
140
144
  getOutboundHandlers: () => TelegramOutboundHandlerConfig[] | undefined;
141
145
  setAllowedUserId: (userId: number) => void;
142
146
  load: () => Promise<void>;
147
+ didLastLoadRecoverInvalidConfig: () => boolean;
143
148
  persist: (config?: TelegramConfig) => Promise<void>;
144
149
  }
145
150
 
@@ -188,6 +193,7 @@ type TelegramMutableConfigStore = Pick<
188
193
  "get" | "set" | "persist"
189
194
  > & {
190
195
  load?: () => Promise<void>;
196
+ didLastLoadRecoverInvalidConfig?: () => boolean;
191
197
  };
192
198
 
193
199
  function isEmptyTelegramConfig(config: TelegramConfig): boolean {
@@ -498,6 +504,7 @@ export function createTelegramConfigStore(
498
504
  let mutationVersion = 0;
499
505
  let persistQueue: Promise<void> = Promise.resolve();
500
506
  let activeProfileName: string | undefined;
507
+ let lastLoadRecoveredInvalidConfig = false;
501
508
  const agentDir = options.agentDir ?? resolveAgentDir();
502
509
  const configPath = options.configPath ?? getConfigPath();
503
510
  const getEffectiveConfig = () =>
@@ -556,8 +563,10 @@ export function createTelegramConfigStore(
556
563
  setEffectiveConfig(nextConfig);
557
564
  },
558
565
  load: async () => {
566
+ lastLoadRecoveredInvalidConfig = false;
559
567
  const loadedConfig = await readTelegramConfig(configPath, {
560
568
  onInvalidConfig: (recovery) => {
569
+ lastLoadRecoveredInvalidConfig = true;
561
570
  options.recordRuntimeEvent?.("config", recovery.error, {
562
571
  phase: "load",
563
572
  configPath: recovery.configPath,
@@ -593,6 +602,7 @@ export function createTelegramConfigStore(
593
602
  persistedConfig = cloneTelegramConfig(config);
594
603
  mutationVersion += 1;
595
604
  },
605
+ didLastLoadRecoverInvalidConfig: () => lastLoadRecoveredInvalidConfig,
596
606
  persist: (nextConfig = getEffectiveConfig()) => {
597
607
  const profileName = activeProfileName;
598
608
  const desiredConfig = storeTelegramEffectiveConfig(
@@ -862,6 +872,41 @@ export function createTelegramProactivePushTargetGetter(deps: {
862
872
  };
863
873
  }
864
874
 
875
+ export function createTelegramAutomaticThreadCleanupChecker(
876
+ configStore: Pick<TelegramConfigStore, "get">,
877
+ ): () => boolean {
878
+ return () => configStore.get().threads?.automaticCleanup ?? true;
879
+ }
880
+
881
+ export function createTelegramAutomaticThreadCleanupResolver(
882
+ configStore: TelegramMutableConfigStore,
883
+ ): () => Promise<boolean> {
884
+ return async () => {
885
+ await loadLatestTelegramConfig(configStore);
886
+ if (configStore.didLastLoadRecoverInvalidConfig?.()) {
887
+ throw new Error(
888
+ "Automatic thread cleanup setting is unavailable after invalid Telegram config recovery.",
889
+ );
890
+ }
891
+ return createTelegramAutomaticThreadCleanupChecker(configStore)();
892
+ };
893
+ }
894
+
895
+ export function createTelegramAutomaticThreadCleanupSetter(
896
+ configStore: TelegramMutableConfigStore,
897
+ ): (enabled: boolean) => Promise<void> {
898
+ return async (enabled) => {
899
+ await loadLatestTelegramConfig(configStore);
900
+ const current = configStore.get();
901
+ const config = {
902
+ ...current,
903
+ threads: { ...current.threads, automaticCleanup: enabled },
904
+ };
905
+ configStore.set(config);
906
+ await configStore.persist(config);
907
+ };
908
+ }
909
+
865
910
  export function createTelegramConfigControls(
866
911
  configStore: TelegramMutableConfigStore,
867
912
  ) {
@@ -880,6 +925,12 @@ export function createTelegramConfigControls(
880
925
  setVoiceReplyMode: createTelegramVoiceReplyModeSetter(configStore),
881
926
  getTimeInjectionMode: createTelegramTimeInjectionModeGetter(configStore),
882
927
  setTimeInjectionMode: createTelegramTimeInjectionModeSetter(configStore),
928
+ isAutomaticThreadCleanupEnabled:
929
+ createTelegramAutomaticThreadCleanupChecker(configStore),
930
+ resolveAutomaticThreadCleanupEnabled:
931
+ createTelegramAutomaticThreadCleanupResolver(configStore),
932
+ setAutomaticThreadCleanupEnabled:
933
+ createTelegramAutomaticThreadCleanupSetter(configStore),
883
934
  };
884
935
  }
885
936
 
package/lib/locks.ts CHANGED
@@ -176,6 +176,42 @@ function sleepSync(ms: number): void {
176
176
  Atomics.wait(new Int32Array(buffer), 0, 0, ms);
177
177
  }
178
178
 
179
+ export interface TelegramRenameRetryOptions {
180
+ rename?: typeof renameSync;
181
+ attempts?: number;
182
+ retryDelayMs?: number;
183
+ }
184
+
185
+ /** Rename one Telegram runtime artifact with bounded Windows sharing retries. */
186
+ export function renameTelegramPathWithRetry(
187
+ sourcePath: string,
188
+ destinationPath: string,
189
+ options: TelegramRenameRetryOptions = {},
190
+ ): boolean {
191
+ const rename = options.rename ?? renameSync;
192
+ const attempts = Math.max(
193
+ 1,
194
+ options.attempts ?? TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS,
195
+ );
196
+ const retryDelayMs = Math.max(
197
+ 0,
198
+ options.retryDelayMs ?? TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS,
199
+ );
200
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
201
+ try {
202
+ rename(sourcePath, destinationPath);
203
+ return true;
204
+ } catch (error) {
205
+ if ((error as { code?: unknown })?.code === "ENOENT") return false;
206
+ if (!isRetryableLockWriteError(error) || attempt === attempts - 1) {
207
+ throw error;
208
+ }
209
+ sleepSync(retryDelayMs * (attempt + 1));
210
+ }
211
+ }
212
+ return false;
213
+ }
214
+
179
215
  interface TelegramLockTransactionOwner {
180
216
  pid: number;
181
217
  acquiredAtMs: number;
@@ -307,29 +343,11 @@ function releaseLockTransactionGuard(
307
343
  );
308
344
  }
309
345
  const releasedPath = `${path}.released.${randomUUID()}`;
310
- for (
311
- let attempt = 0;
312
- attempt < TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS;
313
- attempt += 1
314
- ) {
315
- try {
316
- renameSync(path, releasedPath);
317
- try {
318
- removeLockTransactionGuard(releasedPath);
319
- } catch {
320
- /* released debris cannot retain transaction authority */
321
- }
322
- return;
323
- } catch (error) {
324
- if ((error as { code?: unknown })?.code === "ENOENT") return;
325
- if (
326
- !isRetryableLockWriteError(error) ||
327
- attempt === TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS - 1
328
- ) {
329
- throw error;
330
- }
331
- sleepSync(TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS * (attempt + 1));
332
- }
346
+ if (!renameTelegramPathWithRetry(path, releasedPath)) return;
347
+ try {
348
+ removeLockTransactionGuard(releasedPath);
349
+ } catch {
350
+ /* released debris cannot retain transaction authority */
333
351
  }
334
352
  }
335
353
 
@@ -411,28 +429,8 @@ function reclaimAbandonedDirectoryGuard(
411
429
  throw error;
412
430
  }
413
431
 
414
- const renameWithRetry = (fromPath: string, toPath: string): boolean => {
415
- for (
416
- let attempt = 0;
417
- attempt < TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS;
418
- attempt += 1
419
- ) {
420
- try {
421
- renameRecovery(fromPath, toPath);
422
- return true;
423
- } catch (error) {
424
- if ((error as { code?: unknown })?.code === "ENOENT") return false;
425
- if (
426
- !isRetryableLockWriteError(error) ||
427
- attempt === TELEGRAM_LOCK_WRITE_RETRY_ATTEMPTS - 1
428
- ) {
429
- throw error;
430
- }
431
- sleepSync(TELEGRAM_LOCK_WRITE_RETRY_DELAY_MS * (attempt + 1));
432
- }
433
- }
434
- return false;
435
- };
432
+ const renameWithRetry = (fromPath: string, toPath: string): boolean =>
433
+ renameTelegramPathWithRetry(fromPath, toPath, { rename: renameRecovery });
436
434
 
437
435
  activeReclaims.add(reclaimGeneration);
438
436
  const stalePath = `${path}.stale.${process.pid}.${randomUUID()}`;
@@ -1139,6 +1137,7 @@ export interface TelegramLockedPollingRuntimeDeps<
1139
1137
  owner: TelegramLockEntry,
1140
1138
  ) => boolean | undefined | Promise<boolean | undefined>;
1141
1139
  stopFollowerRegistration?: () => void;
1140
+ onTransportAvailabilityChanged?: () => void;
1142
1141
  updateStatus: (ctx: TContext) => void;
1143
1142
  recordRuntimeEvent?: (
1144
1143
  category: string,
@@ -1189,6 +1188,7 @@ export function createTelegramLockedPollingRuntime<
1189
1188
  const stopAfterOwnershipLoss = () => {
1190
1189
  if (ownershipStop) return;
1191
1190
  stopOwnershipWatcher();
1191
+ deps.onTransportAvailabilityChanged?.();
1192
1192
  ownershipStop = deps
1193
1193
  .stopPolling()
1194
1194
  .catch((error) =>
@@ -1242,12 +1242,14 @@ export function createTelegramLockedPollingRuntime<
1242
1242
  });
1243
1243
  }
1244
1244
  deps.lock.release();
1245
+ deps.onTransportAvailabilityChanged?.();
1245
1246
  throw error;
1246
1247
  }
1247
1248
  if (deps.lock.owns(ctx)) return true;
1248
1249
  stopOwnershipWatcher();
1249
1250
  if (ownershipStop) await ownershipStop;
1250
1251
  await deps.stopPolling();
1252
+ deps.onTransportAvailabilityChanged?.();
1251
1253
  return false;
1252
1254
  };
1253
1255
  const canStartPolling = (ctx: TContext): boolean =>
@@ -1337,6 +1339,7 @@ export function createTelegramLockedPollingRuntime<
1337
1339
  message: "Telegram leadership changed during polling startup.",
1338
1340
  };
1339
1341
  }
1342
+ deps.onTransportAvailabilityChanged?.();
1340
1343
  deps.updateStatus(ctx);
1341
1344
  const staleSuffix = acquired.replacedStale ? " Replaced stale lock." : "";
1342
1345
  return { ok: true, message: `Telegram bridge connected.${staleSuffix}` };
@@ -1344,6 +1347,7 @@ export function createTelegramLockedPollingRuntime<
1344
1347
  stop: async () => {
1345
1348
  await suspendPolling();
1346
1349
  const state = deps.lock.release();
1350
+ deps.onTransportAvailabilityChanged?.();
1347
1351
  if (state.kind === "active-elsewhere") {
1348
1352
  return `Telegram bridge is active in another Pi instance (${formatTelegramLockEntry(state.lock)}).`;
1349
1353
  }
@@ -1391,6 +1395,7 @@ export function createTelegramLockedPollingRuntime<
1391
1395
  if (generation !== sessionAutoStartGeneration) return;
1392
1396
  if (!(await runOwnedPollingStart(ctx, {}))) return;
1393
1397
  if (generation !== sessionAutoStartGeneration) return;
1398
+ deps.onTransportAvailabilityChanged?.();
1394
1399
  deps.updateStatus(ctx);
1395
1400
  deps.recordRuntimeEvent?.("lock", "Telegram auto-start completed", {
1396
1401
  phase: "auto-start-complete",