@llblab/pi-telegram 0.18.4 → 0.18.6

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.
@@ -19,11 +19,17 @@ import {
19
19
  sendTelegramBusLocalEnvelope,
20
20
  type TelegramBusEnvelope,
21
21
  } from "./bus.ts";
22
+ import {
23
+ getTelegramBusTransportRetryPolicy,
24
+ TELEGRAM_BUS_REGISTRATION_RETRY,
25
+ } from "./bus-transport.ts";
22
26
 
23
27
  export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
24
28
  export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
25
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = 10;
26
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = 150;
29
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
30
+ TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
31
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
32
+ TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
27
33
 
28
34
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
29
35
  "__piTelegramFollowerSessionHandoff";
@@ -197,16 +203,25 @@ export interface TelegramBusFollowerLeaderLock {
197
203
  busSecret?: string;
198
204
  }
199
205
 
206
+ export interface TelegramBusFollowerPromotedBinding {
207
+ target?: TelegramTarget;
208
+ slot?: string;
209
+ threadName?: string;
210
+ }
211
+
200
212
  export interface TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext> {
201
213
  registrationState: Pick<
202
214
  TelegramBusFollowerRegistrationState,
203
- "setRegistered"
215
+ "getTarget" | "getSlot" | "getThreadName" | "setRegistered"
204
216
  >;
205
217
  getRegistrationRuntime: () => TelegramBusFollowerRegistrationRuntime<TContext>;
206
218
  getLeaderState: () => TelegramBusFollowerLeaderState;
207
219
  setLifecyclePhase: (phase: "electing" | undefined) => void;
208
220
  updateStatus: (ctx: TContext) => void;
209
- promoteToLeader: (ctx: TContext) => Promise<void> | void;
221
+ promoteToLeader: (
222
+ ctx: TContext,
223
+ binding: TelegramBusFollowerPromotedBinding,
224
+ ) => Promise<void> | void;
210
225
  sleep: (ms: number) => Promise<void>;
211
226
  promotionGraceMs: number;
212
227
  recordRuntimeEvent: (
@@ -341,6 +356,10 @@ export function createTelegramBusFollowerApiCaller(
341
356
  const response = await sendTelegramBusLocalEnvelope({
342
357
  socketPath: deps.socketPath,
343
358
  timeoutMs,
359
+ retry: getTelegramBusTransportRetryPolicy({
360
+ endpoint: deps.socketPath,
361
+ operation: "operation",
362
+ }),
344
363
  envelope: {
345
364
  kind: "follower.callApi",
346
365
  requestId: deps.createRequestId(),
@@ -368,19 +387,6 @@ function isTelegramStaleContextError(error: unknown): boolean {
368
387
  );
369
388
  }
370
389
 
371
- function isRetryableTelegramBusRegistrationError(error: unknown): boolean {
372
- if (!(error instanceof Error)) return false;
373
- const code = (error as NodeJS.ErrnoException).code;
374
- return code === "ENOENT" || code === "ECONNREFUSED" || code === "EPIPE";
375
- }
376
-
377
- function delayTelegramBusFollowerRegistration(ms: number): Promise<void> {
378
- return new Promise((resolve) => {
379
- const timer = setTimeout(resolve, ms);
380
- timer.unref?.();
381
- });
382
- }
383
-
384
390
  export function createTelegramBusFollowerSessionReplacementSuspender(
385
391
  deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
386
392
  ): () => Promise<void> {
@@ -514,7 +520,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
514
520
  return false;
515
521
  }
516
522
  };
517
- const promoteToLeader = async (reason: unknown, ctx: TContext) => {
523
+ const snapshotBinding = (): TelegramBusFollowerPromotedBinding => ({
524
+ target: deps.registrationState.getTarget(),
525
+ slot: deps.registrationState.getSlot(),
526
+ threadName: deps.registrationState.getThreadName(),
527
+ });
528
+ const promoteToLeader = async (
529
+ reason: unknown,
530
+ ctx: TContext,
531
+ binding = snapshotBinding(),
532
+ ) => {
518
533
  deps.setLifecyclePhase("electing");
519
534
  safeUpdateStatus(ctx);
520
535
  deps.recordRuntimeEvent("bus", reason, {
@@ -526,7 +541,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
526
541
  deps.recordRuntimeEvent("bus", "Telegram follower elected for promotion", {
527
542
  phase: "follower-promotion-electing",
528
543
  });
529
- await deps.promoteToLeader(ctx);
544
+ await deps.promoteToLeader(ctx, binding);
530
545
  deps.setLifecyclePhase(undefined);
531
546
  safeUpdateStatus(ctx);
532
547
  deps.recordRuntimeEvent("bus", "Telegram follower promotion completed", {
@@ -537,6 +552,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
537
552
  if (promotionPending) return;
538
553
  promotionPending = true;
539
554
  try {
555
+ const initialBinding = snapshotBinding();
540
556
  const state = deps.getLeaderState();
541
557
  if (state.kind === "active-elsewhere") {
542
558
  clearRegisteredState(ctx);
@@ -568,16 +584,16 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
568
584
  ) {
569
585
  return;
570
586
  }
571
- await promoteToLeader(error, ctx);
587
+ await promoteToLeader(error, ctx, initialBinding);
572
588
  return;
573
589
  }
574
590
  if (graceState.kind === "stale" || graceState.kind === "inactive") {
575
- await promoteToLeader(error, ctx);
591
+ await promoteToLeader(error, ctx, initialBinding);
576
592
  }
577
593
  return;
578
594
  }
579
595
  if (state.kind === "stale" || state.kind === "inactive") {
580
- await promoteToLeader(error, ctx);
596
+ await promoteToLeader(error, ctx, initialBinding);
581
597
  }
582
598
  } catch (promotionError) {
583
599
  deps.setLifecyclePhase(undefined);
@@ -634,6 +650,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
634
650
  const response = await sendTelegramBusLocalEnvelope({
635
651
  socketPath: activeLeaderSocketPath,
636
652
  timeoutMs: deps.timeoutMs,
653
+ retry: getTelegramBusTransportRetryPolicy({
654
+ endpoint: activeLeaderSocketPath,
655
+ operation: "operation",
656
+ }),
637
657
  envelope: {
638
658
  kind: "follower.heartbeat",
639
659
  requestId: deps.createRequestId(),
@@ -692,31 +712,25 @@ export function createTelegramBusFollowerRegistrationRuntime<
692
712
  });
693
713
  let response: TelegramBusEnvelope | undefined;
694
714
  try {
695
- for (let attempt = 1; ; attempt += 1) {
696
- try {
697
- response = await sendTelegramBusLocalEnvelope({
698
- socketPath: leaderSocketPath,
699
- timeoutMs: registrationTimeoutMs,
700
- envelope: createRegistrationEnvelope(),
701
- });
702
- break;
703
- } catch (error) {
704
- if (
705
- attempt >= registrationRetryAttempts ||
706
- !isRetryableTelegramBusRegistrationError(error)
707
- ) {
708
- throw error;
709
- }
710
- deps.recordRuntimeEvent?.("bus", error, {
711
- phase: "follower-register-retry",
712
- attempt,
713
- socketPath: leaderSocketPath,
715
+ response = await sendTelegramBusLocalEnvelope({
716
+ socketPath: leaderSocketPath,
717
+ timeoutMs: registrationTimeoutMs,
718
+ envelope: createRegistrationEnvelope(),
719
+ retry: getTelegramBusTransportRetryPolicy({
720
+ endpoint: leaderSocketPath,
721
+ operation: "registration",
722
+ overrides: {
723
+ attempts: registrationRetryAttempts,
724
+ delayMs: registrationRetryDelayMs,
725
+ },
726
+ }),
727
+ recordTransportEvent(phase, details) {
728
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
729
+ phase: `follower-register-${phase}`,
730
+ ...details,
714
731
  });
715
- await delayTelegramBusFollowerRegistration(
716
- registrationRetryDelayMs,
717
- );
718
- }
719
- }
732
+ },
733
+ });
720
734
  } catch (error) {
721
735
  stopHeartbeat();
722
736
  activeLeaderSocketPath = undefined;
@@ -781,6 +795,12 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
781
795
  ): TelegramBusForwardedUpdateReceiverRuntime {
782
796
  const server = createTelegramBusLocalServer({
783
797
  socketPath: deps.socketPath,
798
+ recordTransportEvent(phase, details) {
799
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
800
+ phase: `follower-receiver-${phase}`,
801
+ ...details,
802
+ });
803
+ },
784
804
  async handleEnvelope(envelope) {
785
805
  const authSecret = deps.getAuthSecret?.();
786
806
  if (deps.getAuthSecret && (!authSecret || envelope.auth !== authSecret)) {
package/lib/bus-leader.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  type TelegramBusFollowerView,
22
22
  type TelegramBusInstanceRegistration,
23
23
  } from "./bus.ts";
24
+ import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
24
25
 
25
26
  export interface TelegramBusLeaderRuntime<TContext> {
26
27
  startPolling: (ctx: TContext) => Promise<void>;
@@ -162,6 +163,17 @@ export interface TelegramBusFollowerRegistryRestoreDeps {
162
163
  getNowMs?: () => number;
163
164
  }
164
165
 
166
+ export interface TelegramBusFollowerMessageOwnershipRecord {
167
+ follower: TelegramBusFollowerView;
168
+ chatId: number;
169
+ messageId: number;
170
+ target?: TelegramTarget;
171
+ }
172
+
173
+ export type TelegramBusFollowerMessageOwnershipRecorder = (
174
+ record: TelegramBusFollowerMessageOwnershipRecord,
175
+ ) => void;
176
+
165
177
  export interface TelegramBusLeaderRuntimeDeps<TContext> {
166
178
  socketPath: string;
167
179
  followerRegistry: TelegramBusFollowerRegistry;
@@ -174,6 +186,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
174
186
  method: string;
175
187
  args: unknown[];
176
188
  }) => boolean;
189
+ recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
177
190
  provisionFollowerTarget?: (
178
191
  registration: TelegramBusInstanceRegistration,
179
192
  ) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
@@ -324,30 +337,74 @@ export function createTelegramBusFollowerTargetProvisioner(
324
337
  registration.profileKey ?? `manual:${registration.instanceId}`;
325
338
  const followerOwner =
326
339
  Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
327
- deps.onProvisioningStart?.();
328
- let result: Threads.TelegramTopicTargetProvisionResult;
329
- try {
330
- result = await provision({
331
- instanceId: registration.instanceId,
332
- owner:
333
- followerOwner.kind === "manual-follower"
334
- ? followerOwner
335
- : {
336
- kind: "manual-follower",
337
- instanceId: registration.instanceId,
338
- },
339
- profileKey: followerProfileKey,
340
- threadName: registration.threadName,
341
- });
342
- } finally {
343
- deps.onProvisioningEnd?.();
344
- }
340
+ const provisionTarget = async () => {
341
+ deps.onProvisioningStart?.();
342
+ try {
343
+ return await provision({
344
+ instanceId: registration.instanceId,
345
+ owner:
346
+ followerOwner.kind === "manual-follower"
347
+ ? followerOwner
348
+ : {
349
+ kind: "manual-follower",
350
+ instanceId: registration.instanceId,
351
+ },
352
+ profileKey: followerProfileKey,
353
+ threadName: registration.threadName,
354
+ });
355
+ } finally {
356
+ deps.onProvisioningEnd?.();
357
+ }
358
+ };
359
+ let result = await provisionTarget();
345
360
  deps.setSyncState(
346
361
  Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
347
362
  nowMs: getNowMs(),
348
363
  action: "follower-register",
349
364
  }),
350
365
  );
366
+ let connectedAnnouncement =
367
+ createTelegramBusInstanceLifecycleAnnouncement({
368
+ target: result.target,
369
+ threadName: result.record.threadName,
370
+ slot: result.record.slot,
371
+ state: "connected",
372
+ });
373
+ let connectedAnnouncementSent = false;
374
+ if (result.reused && connectedAnnouncement) {
375
+ try {
376
+ await deps.callApi("sendMessage", {
377
+ chat_id: connectedAnnouncement.target.chatId,
378
+ message_thread_id: connectedAnnouncement.target.threadId,
379
+ text: connectedAnnouncement.text,
380
+ parse_mode: connectedAnnouncement.parseMode,
381
+ });
382
+ connectedAnnouncementSent = true;
383
+ } catch (error) {
384
+ deps.recordRuntimeEvent("telegram", error, {
385
+ phase: "follower-topic-reuse-probe",
386
+ instanceId: registration.instanceId,
387
+ chatId: result.target.chatId,
388
+ threadId: result.target.threadId,
389
+ });
390
+ if (Threads.isTelegramTopicTargetStaleError(error)) {
391
+ deps.topicTargetStore.markStaleByTarget(
392
+ result.target,
393
+ "deleted",
394
+ "Follower registration found the reusable thread target stale.",
395
+ );
396
+ await deps.topicTargetStore.persist();
397
+ result = await provisionTarget();
398
+ connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
399
+ target: result.target,
400
+ threadName: result.record.threadName,
401
+ slot: result.record.slot,
402
+ state: "connected",
403
+ });
404
+ connectedAnnouncementSent = false;
405
+ }
406
+ }
407
+ }
351
408
  recordSlowTelegramBusFollowerRegistrationStep(deps, {
352
409
  phase: "follower-register-critical",
353
410
  elapsedMs: Date.now() - registrationStartedAtMs,
@@ -355,16 +412,9 @@ export function createTelegramBusFollowerTargetProvisioner(
355
412
  target: result.target,
356
413
  reused: result.reused,
357
414
  });
358
- const connectedAnnouncement =
359
- createTelegramBusInstanceLifecycleAnnouncement({
360
- target: result.target,
361
- threadName: result.record.threadName,
362
- slot: result.record.slot,
363
- state: "connected",
364
- });
365
415
  scheduleTelegramBusLeaderBackgroundTask(async () => {
366
416
  const backgroundStartedAtMs = Date.now();
367
- if (connectedAnnouncement) {
417
+ if (connectedAnnouncement && !connectedAnnouncementSent) {
368
418
  try {
369
419
  await deps.callApi("sendMessage", {
370
420
  chat_id: connectedAnnouncement.target.chatId,
@@ -578,6 +628,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
578
628
  method: string;
579
629
  args: unknown[];
580
630
  }) => boolean;
631
+ recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
581
632
  provisionFollowerTarget?: (
582
633
  registration: TelegramBusInstanceRegistration,
583
634
  ) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
@@ -598,31 +649,22 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
598
649
  >,
599
650
  ): Promise<TelegramBusEnvelope> => {
600
651
  const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
601
- if (!follower) {
602
- return {
603
- kind: "bus.ack",
604
- requestId: envelope.requestId,
605
- ok: false,
606
- message: "Unknown Telegram bus follower instance.",
607
- };
608
- }
609
- if (!follower.busSocketPath) {
610
- return {
611
- kind: "bus.ack",
612
- requestId: envelope.requestId,
613
- ok: false,
614
- message: "Telegram bus follower does not expose a receiver socket.",
615
- };
616
- }
617
- deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
652
+ const followerSocketPath =
653
+ follower?.busSocketPath ??
654
+ getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
655
+ if (follower) deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
618
656
  try {
619
657
  const response = await sendTelegramBusLocalEnvelope({
620
- socketPath: follower.busSocketPath,
658
+ socketPath: followerSocketPath,
621
659
  envelope,
622
660
  timeoutMs: deps.timeoutMs,
661
+ retry: getTelegramBusTransportRetryPolicy({
662
+ endpoint: followerSocketPath,
663
+ operation: "operation",
664
+ }),
623
665
  });
624
666
  if (response?.kind === "bus.ack" && response.ok) {
625
- deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
667
+ if (follower) deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
626
668
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
627
669
  }
628
670
  const message =
@@ -714,6 +756,72 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
714
756
  };
715
757
  }
716
758
 
759
+ function asRecord(value: unknown): Record<string, unknown> | undefined {
760
+ return value && typeof value === "object" && !Array.isArray(value)
761
+ ? (value as Record<string, unknown>)
762
+ : undefined;
763
+ }
764
+
765
+ function asInteger(value: unknown): number | undefined {
766
+ if (typeof value === "number" && Number.isInteger(value)) return value;
767
+ if (typeof value !== "string" || value.trim() === "") return undefined;
768
+ const parsed = Number(value);
769
+ return Number.isInteger(parsed) ? parsed : undefined;
770
+ }
771
+
772
+ function getFollowerApiMethodAndBody(envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>): {
773
+ apiMethod: string;
774
+ body?: Record<string, unknown>;
775
+ } {
776
+ if (envelope.method === "call" || envelope.method === "callMultipart") {
777
+ return {
778
+ apiMethod:
779
+ typeof envelope.args[0] === "string" ? envelope.args[0] : "",
780
+ body: asRecord(envelope.args[1]),
781
+ };
782
+ }
783
+ return { apiMethod: envelope.method, body: asRecord(envelope.args[0]) };
784
+ }
785
+
786
+ function getSentMessageIds(result: unknown): number[] {
787
+ const values = Array.isArray(result) ? result : [result];
788
+ return values
789
+ .map((value) => asInteger(asRecord(value)?.message_id))
790
+ .filter((messageId): messageId is number => messageId !== undefined);
791
+ }
792
+
793
+ function recordFollowerApiMessageOwnership(input: {
794
+ envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>;
795
+ follower: TelegramBusFollowerView;
796
+ result: unknown;
797
+ record?: TelegramBusFollowerMessageOwnershipRecorder;
798
+ }): void {
799
+ if (!input.record) return;
800
+ const { apiMethod, body } = getFollowerApiMethodAndBody(input.envelope);
801
+ if (
802
+ apiMethod !== "sendMessage" &&
803
+ apiMethod !== "sendRichMessage" &&
804
+ apiMethod !== "sendPhoto" &&
805
+ apiMethod !== "sendDocument" &&
806
+ apiMethod !== "sendVoice" &&
807
+ apiMethod !== "sendMediaGroup"
808
+ ) {
809
+ return;
810
+ }
811
+ const chatId = asInteger(body?.chat_id) ?? input.follower.target?.chatId;
812
+ if (chatId === undefined) return;
813
+ const threadId = asInteger(body?.message_thread_id) ?? input.follower.target?.threadId;
814
+ const target = threadId !== undefined ? { chatId, threadId } : { chatId };
815
+ for (const messageId of getSentMessageIds(input.result)) {
816
+ input.record({
817
+ follower: input.follower,
818
+ chatId,
819
+ messageId,
820
+ target,
821
+ });
822
+ }
823
+ }
824
+
717
825
  async function handleFollowerApiCall(
718
826
  envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>,
719
827
  deps: {
@@ -725,6 +833,7 @@ async function handleFollowerApiCall(
725
833
  method: string;
726
834
  args: unknown[];
727
835
  }) => boolean;
836
+ recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
728
837
  },
729
838
  ): Promise<TelegramBusEnvelope> {
730
839
  const follower = deps.followerRegistry.get(envelope.instanceId);
@@ -761,11 +870,18 @@ async function handleFollowerApiCall(
761
870
  };
762
871
  }
763
872
  try {
873
+ const result = await deps.callApi(envelope.method, envelope.args);
874
+ recordFollowerApiMessageOwnership({
875
+ envelope,
876
+ follower,
877
+ result,
878
+ record: deps.recordFollowerMessageOwnership,
879
+ });
764
880
  return {
765
881
  kind: "bus.ack",
766
882
  requestId: envelope.requestId,
767
883
  ok: true,
768
- result: await deps.callApi(envelope.method, envelope.args),
884
+ result,
769
885
  };
770
886
  } catch (error) {
771
887
  return {
@@ -842,7 +958,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
842
958
  ): TelegramBusLeaderRuntime<TContext> {
843
959
  const getNowMs = deps.getNowMs ?? Date.now;
844
960
  const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
845
- const followerStaleAfterMs = deps.followerStaleAfterMs ?? 2000;
961
+ const followerStaleAfterMs = deps.followerStaleAfterMs ?? 5000;
846
962
  let pruneInterval: ReturnType<typeof setInterval> | undefined;
847
963
  const stopPruning = () => {
848
964
  if (!pruneInterval) return;
@@ -878,12 +994,19 @@ export function createTelegramBusLeaderRuntime<TContext>(
878
994
  };
879
995
  const localServer = createTelegramBusLocalServer({
880
996
  socketPath: deps.socketPath,
997
+ recordTransportEvent(phase, details) {
998
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
999
+ phase: `leader-${phase}`,
1000
+ ...details,
1001
+ });
1002
+ },
881
1003
  handleEnvelope: createTelegramBusLeaderEnvelopeHandler({
882
1004
  followerRegistry: deps.followerRegistry,
883
1005
  authSecret: deps.authSecret,
884
1006
  getNowMs,
885
1007
  callApi: deps.callApi,
886
1008
  authorizeFollowerApiCall: deps.authorizeFollowerApiCall,
1009
+ recordFollowerMessageOwnership: deps.recordFollowerMessageOwnership,
887
1010
  provisionFollowerTarget: deps.provisionFollowerTarget,
888
1011
  }),
889
1012
  });
@@ -917,6 +1040,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
917
1040
  .catch((error) =>
918
1041
  deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }),
919
1042
  );
1043
+ deps.followerRegistry.clear();
920
1044
  }
921
1045
  },
922
1046
  };