@llblab/pi-telegram 0.27.12 → 0.29.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,24 +10,33 @@ import { basename } from "node:path";
10
10
 
11
11
  import * as Sync from "./sync.ts";
12
12
  import * as Threads from "./threads.ts";
13
+ import { parseTelegramUpdateJournalQueueOwner } from "./journal.ts";
13
14
  import type { TelegramLockEntry, TelegramLockState } from "./locks.ts";
15
+ import type {
16
+ TelegramQueueHandoffPayload,
17
+ TelegramQueueHandoffStageResult,
18
+ } from "./queue.ts";
14
19
  import type { TelegramTarget } from "./target.ts";
15
20
  import {
16
21
  isTelegramApiMethodRetrySafe,
17
22
  TelegramApiCommitUnknownError,
18
23
  } from "./telegram-api.ts";
19
24
  import {
25
+ createTelegramBusFollowerDeliveryIdentity,
20
26
  createTelegramBusFollowerTargetController,
21
27
  createTelegramBusForeignOwnedUpdateForwarder,
22
28
  createTelegramBusLocalServer,
23
29
  createTelegramBusRequestIdFactory,
24
30
  createUnauthorizedBusAck,
31
+ getTelegramBusProtocolCompatibility,
25
32
  getTelegramBusSocketPath,
33
+ isTelegramBusEnvelopeAuthorized,
26
34
  resolveTelegramBusSocketPath,
27
35
  sendTelegramBusLocalEnvelope,
28
36
  type TelegramBusAgentMessage,
29
37
  type TelegramBusAgentTargetSelector,
30
38
  type TelegramBusEnvelope,
39
+ type TelegramBusProtocolIdentity,
31
40
  type TelegramBusSocketPathSource,
32
41
  } from "./bus.ts";
33
42
  import {
@@ -158,12 +167,18 @@ export interface TelegramBusFollowerRegistrationState {
158
167
  getSlot: () => string | undefined;
159
168
  getThreadName: () => string | undefined;
160
169
  getGeneration: () => string | undefined;
170
+ getLeaderProtocol: () => TelegramBusProtocolIdentity | undefined;
161
171
  getEligibleElectionSlots: () => readonly string[];
162
172
  setEligibleElectionSlots: (slots: readonly string[]) => void;
163
173
  setRegistered: (
164
174
  registered: boolean,
165
175
  target?: TelegramTarget,
166
- metadata?: { slot?: string; threadName?: string; generation?: string },
176
+ metadata?: {
177
+ slot?: string;
178
+ threadName?: string;
179
+ generation?: string;
180
+ leaderProtocol?: TelegramBusProtocolIdentity;
181
+ },
167
182
  ) => void;
168
183
  }
169
184
 
@@ -172,12 +187,33 @@ export interface TelegramBusForwardedUpdateReceiverRuntime {
172
187
  stop: () => Promise<void>;
173
188
  }
174
189
 
190
+ export interface TelegramBusFollowerDurableAdmissionResult {
191
+ deliveryId: string;
192
+ sourceUpdateId: number;
193
+ }
194
+
195
+ export interface TelegramBusFollowerDurableAdmissionPort<TContext> {
196
+ admit: (
197
+ envelope: Extract<
198
+ TelegramBusEnvelope,
199
+ {
200
+ kind:
201
+ | "leader.forwardCallback"
202
+ | "leader.forwardReaction"
203
+ | "leader.forwardMessage"
204
+ | "leader.forwardEditedMessage";
205
+ }
206
+ >,
207
+ ctx: TContext,
208
+ ) => Promise<TelegramBusFollowerDurableAdmissionResult>;
209
+ }
210
+
175
211
  export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
176
212
  socketPath: TelegramBusSocketPathSource;
177
213
  instanceId: string;
178
214
  getApiAuthSecret?: () => string | undefined;
179
215
  getForwardingAuthSecret?: () => string | undefined;
180
- getRegistrationGeneration?: () => string | undefined;
216
+ getRegistrationGeneration: () => string | undefined;
181
217
  getForwardCommentBatchPosition?: (
182
218
  message: TMessage,
183
219
  ) => "comment" | "forward" | undefined;
@@ -194,7 +230,7 @@ export interface TelegramBusFollowerApiCallerDeps {
194
230
  instanceId: string;
195
231
  createRequestId: () => string;
196
232
  getAuthSecret?: () => string | undefined;
197
- getRegistrationGeneration?: () => string | undefined;
233
+ getRegistrationGeneration: () => string | undefined;
198
234
  getNowMs?: () => number;
199
235
  timeoutMs?: number;
200
236
  }
@@ -204,6 +240,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
204
240
  > {
205
241
  instanceId: string;
206
242
  createRequestId: () => string;
243
+ protocolIdentity: TelegramBusProtocolIdentity;
207
244
  getLeaderAuthSecret?: (leader: { busSecret?: string }) => string | undefined;
208
245
  setActiveAuthSecret?: (secret: string | undefined) => void;
209
246
  followerBusSocketPath?: string;
@@ -217,6 +254,8 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
217
254
  getThreadName?: (ctx: TContext) => string | undefined;
218
255
  getNowMs?: () => number;
219
256
  getPid?: () => number;
257
+ getProcessBirthId?: () => string;
258
+ getSessionGeneration?: () => number;
220
259
  timeoutMs?: number;
221
260
  registrationTimeoutMs?: number;
222
261
  registrationRetryAttempts?: number;
@@ -228,6 +267,7 @@ export interface TelegramBusFollowerRegistrationRuntimeDeps<
228
267
  details?: Record<string, unknown>,
229
268
  ) => void;
230
269
  onHeartbeatFailure?: (error: unknown, ctx: TContext) => Promise<void> | void;
270
+ onRegistered?: (ctx: TContext) => Promise<void> | void;
231
271
  }
232
272
 
233
273
  export function createTelegramManualFollowerProfileKeyResolver(input: {
@@ -399,37 +439,21 @@ export interface TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext> {
399
439
  ) => void;
400
440
  }
401
441
 
402
- export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
403
- TContext,
404
- TReactionUpdate,
405
- TCallbackQuery,
406
- TMessage = unknown,
407
- > {
442
+ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
408
443
  socketPath: TelegramBusSocketPathSource;
409
444
  instanceId: string;
410
445
  getAuthSecret?: () => string | undefined;
411
- getRegistrationGeneration?: () => string | undefined;
446
+ getRegistrationGeneration: () => string | undefined;
447
+ getRecipientBindingKey: () => string | undefined;
448
+ durableAdmission: TelegramBusFollowerDurableAdmissionPort<TContext>;
412
449
  getContext: () => TContext | undefined;
413
- handleForwardedCallback: (
414
- query: TCallbackQuery,
415
- ctx: TContext,
416
- ) => Promise<void> | void;
417
- handleForwardedReaction: (
418
- reactionUpdate: TReactionUpdate,
419
- ctx: TContext,
420
- ) => Promise<void> | void;
421
- prepareForwardedMessage?: (
422
- message: TMessage,
423
- position: "comment" | "forward",
424
- ) => void;
425
- handleForwardedMessage?: (
426
- message: TMessage,
427
- ctx: TContext,
428
- ) => Promise<void> | void;
429
- handleForwardedEditedMessage?: (
430
- message: TMessage,
450
+ handleQueueHandoff?: (
451
+ envelope: Extract<
452
+ TelegramBusEnvelope,
453
+ { kind: "leader.offerQueueHandoff" }
454
+ >,
431
455
  ctx: TContext,
432
- ) => Promise<void> | void;
456
+ ) => Promise<TelegramQueueHandoffStageResult> | TelegramQueueHandoffStageResult;
433
457
  handleReplaceTarget?: (
434
458
  input: {
435
459
  target: TelegramTarget & { threadId: number };
@@ -445,83 +469,13 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<
445
469
  ) => void;
446
470
  }
447
471
 
448
- export interface TelegramBusFollowerRuntimeAssemblyDeps<
449
- TContext extends { cwd?: string },
450
- TReactionUpdate,
451
- TCallbackQuery,
452
- TMessage = unknown,
453
- > {
454
- receiver: Omit<
455
- TelegramBusForwardedUpdateReceiverRuntimeDeps<
456
- TContext,
457
- TReactionUpdate,
458
- TCallbackQuery,
459
- TMessage
460
- >,
461
- "handleReplaceTarget"
462
- >;
463
- targetReplacement: TelegramBusFollowerTargetReplacementHandlerDeps<TContext>;
464
- recovery: Omit<
465
- TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
466
- "getRegistrationRuntime"
467
- >;
468
- registration: Omit<
469
- TelegramBusFollowerRegistrationRuntimeDeps<TContext>,
470
- "startReceiving" | "stopReceiving" | "onHeartbeatFailure"
471
- >;
472
- }
473
-
474
472
  export interface TelegramBusFollowerRuntimeAssembly<TContext> {
475
473
  receiver: TelegramBusForwardedUpdateReceiverRuntime;
476
474
  registration: TelegramBusFollowerRegistrationRuntime<TContext>;
477
475
  }
478
476
 
479
- export function createTelegramBusForwardedRouteHandlers<
480
- TContext,
481
- TReactionUpdate,
482
- TCallbackQuery,
483
- TMessage,
484
- >(route: {
485
- handleUpdate(
486
- update: {
487
- callback_query?: TCallbackQuery;
488
- message?: TMessage;
489
- edited_message?: TMessage;
490
- },
491
- ctx: TContext,
492
- ): Promise<void> | void;
493
- handleAuthorizedReactionUpdate(
494
- reactionUpdate: TReactionUpdate,
495
- ctx: TContext,
496
- ): Promise<void> | void;
497
- }): Pick<
498
- TelegramBusForwardedUpdateReceiverRuntimeDeps<
499
- TContext,
500
- TReactionUpdate,
501
- TCallbackQuery,
502
- TMessage
503
- >,
504
- | "handleForwardedCallback"
505
- | "handleForwardedReaction"
506
- | "handleForwardedMessage"
507
- | "handleForwardedEditedMessage"
508
- > {
509
- return {
510
- handleForwardedCallback: (query, ctx) =>
511
- route.handleUpdate({ callback_query: query }, ctx),
512
- handleForwardedReaction: route.handleAuthorizedReactionUpdate,
513
- handleForwardedMessage: (message, ctx) =>
514
- route.handleUpdate({ message }, ctx),
515
- handleForwardedEditedMessage: (message, ctx) =>
516
- route.handleUpdate({ edited_message: message }, ctx),
517
- };
518
- }
519
-
520
477
  export interface TelegramBusFollowerRuntimeAssemblyPorts<
521
478
  TContext extends { cwd?: string },
522
- TReactionUpdate,
523
- TCallbackQuery,
524
- TMessage = unknown,
525
479
  > {
526
480
  instanceId: string;
527
481
  registrationState: TelegramBusFollowerRegistrationState;
@@ -531,13 +485,11 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
531
485
  details?: Record<string, unknown>,
532
486
  ) => void;
533
487
  receiver: Omit<
534
- TelegramBusForwardedUpdateReceiverRuntimeDeps<
535
- TContext,
536
- TReactionUpdate,
537
- TCallbackQuery,
538
- TMessage
539
- >,
540
- "handleReplaceTarget" | "instanceId" | "recordRuntimeEvent"
488
+ TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
489
+ | "handleReplaceTarget"
490
+ | "instanceId"
491
+ | "recordRuntimeEvent"
492
+ | "getRegistrationGeneration"
541
493
  >;
542
494
  targetReplacement: Omit<
543
495
  TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
@@ -555,81 +507,42 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
555
507
  | "instanceId"
556
508
  | "registrationState"
557
509
  | "recordRuntimeEvent"
558
- >;
559
- }
560
-
561
- export function createTelegramBusFollowerRuntimeAssemblyDeps<
562
- TContext extends { cwd?: string },
563
- TReactionUpdate,
564
- TCallbackQuery,
565
- TMessage = unknown,
566
- >(
567
- ports: TelegramBusFollowerRuntimeAssemblyPorts<
568
- TContext,
569
- TReactionUpdate,
570
- TCallbackQuery,
571
- TMessage
572
- >,
573
- ): TelegramBusFollowerRuntimeAssemblyDeps<
574
- TContext,
575
- TReactionUpdate,
576
- TCallbackQuery,
577
- TMessage
578
- > {
579
- return {
580
- receiver: {
581
- ...ports.receiver,
582
- instanceId: ports.instanceId,
583
- recordRuntimeEvent: ports.recordRuntimeEvent,
584
- },
585
- targetReplacement: {
586
- ...ports.targetReplacement,
587
- instanceId: ports.instanceId,
588
- registrationState: ports.registrationState,
589
- recordRuntimeEvent: ports.recordRuntimeEvent,
590
- },
591
- recovery: {
592
- ...ports.recovery,
593
- registrationState: ports.registrationState,
594
- recordRuntimeEvent: ports.recordRuntimeEvent,
595
- },
596
- registration: {
597
- ...ports.registration,
598
- instanceId: ports.instanceId,
599
- registrationState: ports.registrationState,
600
- recordRuntimeEvent: ports.recordRuntimeEvent,
601
- },
510
+ | "protocolIdentity"
511
+ > & {
512
+ protocolIdentity: TelegramBusProtocolIdentity;
602
513
  };
603
514
  }
604
515
 
605
516
  export function createTelegramBusFollowerRuntimeAssembly<
606
517
  TContext extends { cwd?: string },
607
- TReactionUpdate,
608
- TCallbackQuery,
609
- TMessage = unknown,
610
518
  >(
611
- deps: TelegramBusFollowerRuntimeAssemblyDeps<
612
- TContext,
613
- TReactionUpdate,
614
- TCallbackQuery,
615
- TMessage
616
- >,
519
+ ports: TelegramBusFollowerRuntimeAssemblyPorts<TContext>,
617
520
  ): TelegramBusFollowerRuntimeAssembly<TContext> {
521
+ const sharedRuntimeDeps = {
522
+ instanceId: ports.instanceId,
523
+ registrationState: ports.registrationState,
524
+ recordRuntimeEvent: ports.recordRuntimeEvent,
525
+ };
618
526
  const receiver = createTelegramBusForwardedUpdateReceiverRuntime({
619
- ...deps.receiver,
620
- getRegistrationGeneration:
621
- deps.targetReplacement.registrationState.getGeneration,
622
- handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler(
623
- deps.targetReplacement,
624
- ),
527
+ ...ports.receiver,
528
+ instanceId: ports.instanceId,
529
+ recordRuntimeEvent: ports.recordRuntimeEvent,
530
+ getRegistrationGeneration: ports.registrationState.getGeneration,
531
+ handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
532
+ ...ports.targetReplacement,
533
+ ...sharedRuntimeDeps,
534
+ }),
625
535
  });
626
536
  let registration: TelegramBusFollowerRegistrationRuntime<TContext>;
627
537
  const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
628
- ...deps.recovery,
538
+ ...ports.recovery,
539
+ registrationState: ports.registrationState,
540
+ recordRuntimeEvent: ports.recordRuntimeEvent,
629
541
  getRegistrationRuntime: () => registration,
630
542
  });
631
543
  registration = createTelegramBusFollowerRegistrationRuntime({
632
- ...deps.registration,
544
+ ...ports.registration,
545
+ ...sharedRuntimeDeps,
633
546
  startReceiving: receiver.start,
634
547
  stopReceiving: receiver.stop,
635
548
  onHeartbeatFailure: recovery,
@@ -640,11 +553,7 @@ export function createTelegramBusFollowerRuntimeAssembly<
640
553
  export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
641
554
  deps: TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
642
555
  ): NonNullable<
643
- TelegramBusForwardedUpdateReceiverRuntimeDeps<
644
- TContext,
645
- unknown,
646
- unknown
647
- >["handleReplaceTarget"]
556
+ TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>["handleReplaceTarget"]
648
557
  > {
649
558
  const getNowMs = deps.getNowMs ?? Date.now;
650
559
  return async (input, ctx) => {
@@ -756,6 +665,12 @@ export function createTelegramBusFollowerClientRuntime<
756
665
  deps.getForwardCommentBatchPosition,
757
666
  recordRuntimeEvent: deps.recordRuntimeEvent,
758
667
  }),
668
+ queueHandoff: createTelegramBusFollowerQueueHandoffClient({
669
+ ...sharedClientDeps,
670
+ instanceId: deps.instanceId,
671
+ getAuthSecret: deps.getApiAuthSecret,
672
+ getRegistrationGeneration: deps.getRegistrationGeneration,
673
+ }),
759
674
  targetController: createTelegramBusFollowerTargetController({
760
675
  ...sharedClientDeps,
761
676
  getAuthSecret: deps.getForwardingAuthSecret,
@@ -763,6 +678,83 @@ export function createTelegramBusFollowerClientRuntime<
763
678
  };
764
679
  }
765
680
 
681
+ export function createTelegramBusFollowerQueueHandoffClient(
682
+ deps: TelegramBusFollowerApiCallerDeps,
683
+ ): (input: {
684
+ recipientInstanceId: string;
685
+ recipientRegistrationGeneration: string;
686
+ donorProcessId: number;
687
+ donorProcessBirthId: string;
688
+ donorSessionGeneration: number;
689
+ donorAcquisitionId: string;
690
+ donorAcquiredAtMs: number;
691
+ handoffToken: string;
692
+ payload: TelegramQueueHandoffPayload;
693
+ }) => Promise<TelegramQueueHandoffStageResult> {
694
+ const getNowMs = deps.getNowMs ?? Date.now;
695
+ const timeoutMs =
696
+ deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
697
+ return async (input) => {
698
+ const registrationGeneration = deps.getRegistrationGeneration();
699
+ if (!registrationGeneration) {
700
+ throw new Error("Telegram bus follower is not registered.");
701
+ }
702
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
703
+ const response = await sendTelegramBusLocalEnvelope({
704
+ socketPath,
705
+ timeoutMs,
706
+ retry: getTelegramBusTransportRetryPolicy({
707
+ endpoint: socketPath,
708
+ operation: "operation",
709
+ }),
710
+ envelope: {
711
+ kind: "follower.offerQueueHandoff",
712
+ requestId: deps.createRequestId(),
713
+ auth: deps.getAuthSecret?.(),
714
+ instanceId: deps.instanceId,
715
+ registrationGeneration,
716
+ ...input,
717
+ sentAtMs: getNowMs(),
718
+ },
719
+ });
720
+ const queueOwner =
721
+ response?.kind === "bus.ack" && isRecord(response.result)
722
+ ? parseTelegramUpdateJournalQueueOwner(response.result.queueOwner)
723
+ : undefined;
724
+ if (
725
+ response?.kind === "bus.ack" &&
726
+ response.ok &&
727
+ isRecord(response.result) &&
728
+ response.result.status === "staged" &&
729
+ typeof response.result.receiptId === "string" &&
730
+ Array.isArray(response.result.sourceUpdateIds) &&
731
+ response.result.sourceUpdateIds.every(Number.isSafeInteger) &&
732
+ input.payload.admissionReceipts.length === 1 &&
733
+ response.result.receiptId ===
734
+ input.payload.admissionReceipts[0]?.receiptId &&
735
+ response.result.sourceUpdateIds.length ===
736
+ input.payload.admissionReceipts[0].sourceUpdateIds.length &&
737
+ response.result.sourceUpdateIds.every(
738
+ (updateId, index) =>
739
+ updateId === input.payload.admissionReceipts[0]!.sourceUpdateIds[index],
740
+ ) &&
741
+ queueOwner
742
+ ) {
743
+ return {
744
+ status: "staged",
745
+ receiptId: response.result.receiptId,
746
+ sourceUpdateIds: response.result.sourceUpdateIds as number[],
747
+ queueOwner,
748
+ };
749
+ }
750
+ throw new Error(
751
+ response?.kind === "bus.ack"
752
+ ? response.message ?? "Telegram queue handoff was rejected."
753
+ : "Telegram queue handoff did not return an acknowledgement.",
754
+ );
755
+ };
756
+ }
757
+
766
758
  export function createTelegramBusAgentMessageClient(
767
759
  deps: TelegramBusFollowerApiCallerDeps,
768
760
  ): {
@@ -796,13 +788,17 @@ export function createTelegramBusAgentMessageClient(
796
788
  : "Telegram bus agent message did not return an acknowledgement.",
797
789
  );
798
790
  };
799
- const registrationFields = () => ({
800
- auth: deps.getAuthSecret?.(),
801
- instanceId: deps.instanceId,
802
- ...(deps.getRegistrationGeneration?.()
803
- ? { registrationGeneration: deps.getRegistrationGeneration?.() }
804
- : {}),
805
- });
791
+ const registrationFields = () => {
792
+ const registrationGeneration = deps.getRegistrationGeneration();
793
+ if (!registrationGeneration) {
794
+ throw new Error("Telegram bus follower is not registered.");
795
+ }
796
+ return {
797
+ auth: deps.getAuthSecret?.(),
798
+ instanceId: deps.instanceId,
799
+ registrationGeneration,
800
+ };
801
+ };
806
802
  return {
807
803
  async resolveTarget(selector) {
808
804
  const result = await request({
@@ -844,6 +840,10 @@ export function createTelegramBusFollowerApiCaller(
844
840
  deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
845
841
  return async (method, args) => {
846
842
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
843
+ const registrationGeneration = deps.getRegistrationGeneration();
844
+ if (!registrationGeneration) {
845
+ throw new Error("Telegram bus follower is not registered.");
846
+ }
847
847
  let response: TelegramBusEnvelope | undefined;
848
848
  try {
849
849
  response = await sendTelegramBusLocalEnvelope({
@@ -858,11 +858,7 @@ export function createTelegramBusFollowerApiCaller(
858
858
  requestId: deps.createRequestId(),
859
859
  auth: deps.getAuthSecret?.(),
860
860
  instanceId: deps.instanceId,
861
- ...(deps.getRegistrationGeneration?.()
862
- ? {
863
- registrationGeneration: deps.getRegistrationGeneration?.(),
864
- }
865
- : {}),
861
+ registrationGeneration,
866
862
  method,
867
863
  args,
868
864
  sentAtMs: getNowMs(),
@@ -1048,6 +1044,7 @@ export function createTelegramBusFollowerRegistrationState(
1048
1044
  let slot: string | undefined;
1049
1045
  let threadName: string | undefined;
1050
1046
  let generation: string | undefined;
1047
+ let leaderProtocol: TelegramBusProtocolIdentity | undefined;
1051
1048
  let eligibleElectionSlots: string[] = [];
1052
1049
  return {
1053
1050
  isRegistered: () => registered,
@@ -1055,6 +1052,10 @@ export function createTelegramBusFollowerRegistrationState(
1055
1052
  getSlot: () => slot,
1056
1053
  getThreadName: () => threadName,
1057
1054
  getGeneration: () => generation,
1055
+ getLeaderProtocol: () =>
1056
+ leaderProtocol
1057
+ ? { ...leaderProtocol, capabilities: [...leaderProtocol.capabilities] }
1058
+ : undefined,
1058
1059
  getEligibleElectionSlots: () => [...eligibleElectionSlots],
1059
1060
  setEligibleElectionSlots: (slots) => {
1060
1061
  eligibleElectionSlots = Array.from(
@@ -1068,6 +1069,13 @@ export function createTelegramBusFollowerRegistrationState(
1068
1069
  slot = next ? metadata?.slot : undefined;
1069
1070
  threadName = next ? metadata?.threadName : undefined;
1070
1071
  generation = next ? metadata?.generation : undefined;
1072
+ leaderProtocol =
1073
+ next && metadata?.leaderProtocol
1074
+ ? {
1075
+ ...metadata.leaderProtocol,
1076
+ capabilities: [...metadata.leaderProtocol.capabilities],
1077
+ }
1078
+ : undefined;
1071
1079
  if (availabilityChanged) options.onAvailabilityChanged?.();
1072
1080
  },
1073
1081
  };
@@ -1337,6 +1345,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
1337
1345
  deps.registrationRetryDelayMs ??
1338
1346
  TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS;
1339
1347
  let heartbeatInterval: ReturnType<typeof setInterval> | undefined;
1348
+ let heartbeatPromise: Promise<void> | undefined;
1349
+ let heartbeatPromiseGeneration: string | undefined;
1340
1350
  let activeLeaderSocketPath: string | undefined;
1341
1351
  let activeAuthSecret: string | undefined;
1342
1352
  let activeRegistrationGeneration: string | undefined;
@@ -1353,22 +1363,39 @@ export function createTelegramBusFollowerRegistrationRuntime<
1353
1363
  stopHeartbeat();
1354
1364
  activeAuthSecret = undefined;
1355
1365
  activeRegistrationGeneration = undefined;
1366
+ heartbeatPromise = undefined;
1367
+ heartbeatPromiseGeneration = undefined;
1356
1368
  deps.setActiveAuthSecret?.(undefined);
1357
1369
  deps.registrationState?.setRegistered(false);
1358
1370
  lastKnownTarget = undefined;
1359
1371
  lastKnownSlot = undefined;
1360
1372
  lastKnownThreadName = undefined;
1361
1373
  activeContext = undefined;
1362
- void deps.stopReceiving?.();
1374
+ void Promise.resolve(deps.stopReceiving?.()).catch((error) => {
1375
+ try {
1376
+ deps.recordRuntimeEvent?.("bus", error, {
1377
+ phase: "follower-receiver-stop",
1378
+ });
1379
+ } catch {
1380
+ // Stop diagnostics cannot create an unhandled Promise.
1381
+ }
1382
+ });
1363
1383
  };
1364
1384
  const sendHeartbeat = async () => {
1365
- if (!activeLeaderSocketPath) return;
1385
+ const leaderSocketPath = activeLeaderSocketPath;
1386
+ const registrationGeneration = activeRegistrationGeneration;
1387
+ const heartbeatContext = activeContext;
1388
+ if (!leaderSocketPath || !registrationGeneration) return;
1389
+ const isCurrentHeartbeat = (): boolean =>
1390
+ activeLeaderSocketPath === leaderSocketPath &&
1391
+ activeRegistrationGeneration === registrationGeneration &&
1392
+ activeContext === heartbeatContext;
1366
1393
  try {
1367
1394
  const response = await sendTelegramBusLocalEnvelope({
1368
- socketPath: activeLeaderSocketPath,
1395
+ socketPath: leaderSocketPath,
1369
1396
  timeoutMs: deps.timeoutMs,
1370
1397
  retry: getTelegramBusTransportRetryPolicy({
1371
- endpoint: activeLeaderSocketPath,
1398
+ endpoint: leaderSocketPath,
1372
1399
  operation: "operation",
1373
1400
  }),
1374
1401
  envelope: {
@@ -1376,12 +1403,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
1376
1403
  requestId: deps.createRequestId(),
1377
1404
  auth: activeAuthSecret,
1378
1405
  instanceId: deps.instanceId,
1379
- ...(activeRegistrationGeneration
1380
- ? { registrationGeneration: activeRegistrationGeneration }
1381
- : {}),
1406
+ registrationGeneration,
1382
1407
  sentAtMs: getNowMs(),
1383
1408
  },
1384
1409
  });
1410
+ if (!isCurrentHeartbeat()) return;
1385
1411
  if (response?.kind === "bus.ack" && response.ok) {
1386
1412
  const heartbeatResult = isRecord(response.result)
1387
1413
  ? response.result
@@ -1399,15 +1425,49 @@ export function createTelegramBusFollowerRegistrationRuntime<
1399
1425
  );
1400
1426
  }
1401
1427
  } catch (error) {
1402
- deps.recordRuntimeEvent?.("bus", error, { phase: "follower-heartbeat" });
1403
- if (activeContext) await deps.onHeartbeatFailure?.(error, activeContext);
1428
+ if (!isCurrentHeartbeat()) return;
1429
+ try {
1430
+ deps.recordRuntimeEvent?.("bus", error, {
1431
+ phase: "follower-heartbeat",
1432
+ });
1433
+ } catch {
1434
+ // Diagnostics cannot replace heartbeat recovery.
1435
+ }
1436
+ if (!heartbeatContext) return;
1437
+ try {
1438
+ await deps.onHeartbeatFailure?.(error, heartbeatContext);
1439
+ } catch (recoveryError) {
1440
+ try {
1441
+ deps.recordRuntimeEvent?.("bus", recoveryError, {
1442
+ phase: "follower-heartbeat-recovery",
1443
+ });
1444
+ } catch {
1445
+ // A diagnostic sink cannot create an unhandled interval rejection.
1446
+ }
1447
+ }
1448
+ }
1449
+ };
1450
+ const requestHeartbeat = (): Promise<void> => {
1451
+ const generation = activeRegistrationGeneration;
1452
+ if (heartbeatPromise && heartbeatPromiseGeneration === generation) {
1453
+ return heartbeatPromise;
1404
1454
  }
1455
+ let tracked: Promise<void>;
1456
+ tracked = sendHeartbeat().finally(() => {
1457
+ if (heartbeatPromise === tracked) {
1458
+ heartbeatPromise = undefined;
1459
+ heartbeatPromiseGeneration = undefined;
1460
+ }
1461
+ });
1462
+ heartbeatPromise = tracked;
1463
+ heartbeatPromiseGeneration = generation;
1464
+ return tracked;
1405
1465
  };
1406
1466
  const startHeartbeat = (socketPath: string) => {
1407
1467
  stopHeartbeat();
1408
1468
  activeLeaderSocketPath = socketPath;
1409
1469
  heartbeatInterval = setInterval(() => {
1410
- void sendHeartbeat();
1470
+ void requestHeartbeat();
1411
1471
  }, heartbeatMs);
1412
1472
  heartbeatInterval.unref?.();
1413
1473
  };
@@ -1460,6 +1520,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
1460
1520
  : {}),
1461
1521
  cwd: ctx.cwd,
1462
1522
  pid: getPid(),
1523
+ processBirthId: deps.getProcessBirthId?.(),
1524
+ sessionGeneration: deps.getSessionGeneration?.(),
1463
1525
  target:
1464
1526
  registrationOptions?.target ??
1465
1527
  deps.registrationState?.getTarget() ??
@@ -1467,6 +1529,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1467
1529
  busSocketPath:
1468
1530
  deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
1469
1531
  registrationGeneration,
1532
+ protocol: deps.protocolIdentity,
1470
1533
  connectedAtMs: getNowMs(),
1471
1534
  },
1472
1535
  };
@@ -1508,6 +1571,21 @@ export function createTelegramBusFollowerRegistrationRuntime<
1508
1571
  await deps.stopReceiving?.();
1509
1572
  return false;
1510
1573
  }
1574
+ const compatibility = getTelegramBusProtocolCompatibility({
1575
+ local: deps.protocolIdentity,
1576
+ remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1577
+ });
1578
+ if (!compatibility.compatible) {
1579
+ stopHeartbeat();
1580
+ activeLeaderSocketPath = undefined;
1581
+ activeAuthSecret = undefined;
1582
+ deps.registrationState?.setRegistered(false);
1583
+ deps.setActiveAuthSecret?.(undefined);
1584
+ await deps.stopReceiving?.();
1585
+ throw new Error(
1586
+ `Incompatible Telegram bus leader protocol: ${compatibility.reason}.`,
1587
+ );
1588
+ }
1511
1589
  if (response?.kind === "bus.ack" && !response.ok) {
1512
1590
  stopHeartbeat();
1513
1591
  activeLeaderSocketPath = undefined;
@@ -1525,6 +1603,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
1525
1603
  deps.registrationState?.setRegistered(true, registrationResult.target, {
1526
1604
  ...registrationResult,
1527
1605
  generation: registrationGeneration,
1606
+ ...(response.protocol
1607
+ ? { leaderProtocol: response.protocol }
1608
+ : {}),
1528
1609
  });
1529
1610
  lastKnownTarget = registrationResult.target;
1530
1611
  lastKnownSlot = registrationResult.slot;
@@ -1532,7 +1613,19 @@ export function createTelegramBusFollowerRegistrationRuntime<
1532
1613
  activeLeaderSocketPath = leaderSocketPath;
1533
1614
  activeRegistrationGeneration = registrationGeneration;
1534
1615
  activeContext = ctx;
1535
- await sendHeartbeat();
1616
+ try {
1617
+ await deps.onRegistered?.(ctx);
1618
+ } catch (error) {
1619
+ stopHeartbeat();
1620
+ activeLeaderSocketPath = undefined;
1621
+ activeAuthSecret = undefined;
1622
+ activeRegistrationGeneration = undefined;
1623
+ deps.registrationState?.setRegistered(false);
1624
+ deps.setActiveAuthSecret?.(undefined);
1625
+ await deps.stopReceiving?.();
1626
+ throw error;
1627
+ }
1628
+ await requestHeartbeat();
1536
1629
  startHeartbeat(leaderSocketPath);
1537
1630
  if (
1538
1631
  pendingHandoffOptions &&
@@ -1585,18 +1678,97 @@ export function createTelegramBusFollowerRegistrationRuntime<
1585
1678
  };
1586
1679
  }
1587
1680
 
1588
- export function createTelegramBusForwardedUpdateReceiverRuntime<
1589
- TContext,
1590
- TReactionUpdate,
1591
- TCallbackQuery,
1592
- TMessage = unknown,
1681
+ const TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD =
1682
+ "pi_telegram_forward_comment_batch_position";
1683
+
1684
+ export function prepareTelegramBusFollowerJournaledUpdateForExecution<
1685
+ TUpdate extends { message?: unknown } & Record<string, unknown>,
1593
1686
  >(
1594
- deps: TelegramBusForwardedUpdateReceiverRuntimeDeps<
1595
- TContext,
1596
- TReactionUpdate,
1597
- TCallbackQuery,
1598
- TMessage
1599
- >,
1687
+ update: TUpdate,
1688
+ prepareForwardedMessage: (
1689
+ message: NonNullable<TUpdate["message"]>,
1690
+ position: "comment" | "forward",
1691
+ ) => void,
1692
+ ): TUpdate {
1693
+ const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
1694
+ if (
1695
+ update.message !== undefined &&
1696
+ (position === "comment" || position === "forward")
1697
+ ) {
1698
+ prepareForwardedMessage(
1699
+ update.message as NonNullable<TUpdate["message"]>,
1700
+ position,
1701
+ );
1702
+ }
1703
+ if (position === undefined) return update;
1704
+ const prepared = { ...update };
1705
+ delete prepared[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
1706
+ return prepared;
1707
+ }
1708
+
1709
+ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps: {
1710
+ journal: {
1711
+ appendBatch(
1712
+ updates: readonly ({ update_id: number } & Record<string, unknown>)[],
1713
+ ): unknown;
1714
+ };
1715
+ signalWorker: (ctx: TContext) => void;
1716
+ }): TelegramBusFollowerDurableAdmissionPort<TContext> {
1717
+ return {
1718
+ async admit(envelope, ctx) {
1719
+ const delivery = envelope.delivery;
1720
+ if (!delivery) {
1721
+ throw new Error("Telegram follower durable admission requires delivery identity.");
1722
+ }
1723
+ const expected = createTelegramBusFollowerDeliveryIdentity({
1724
+ kind: envelope.kind,
1725
+ recipientBindingKey: delivery.recipientBindingKey,
1726
+ sourceUpdateId: delivery.sourceUpdateId,
1727
+ });
1728
+ if (expected.deliveryId !== delivery.deliveryId) {
1729
+ throw new Error("Invalid Telegram follower delivery id.");
1730
+ }
1731
+ const carrier =
1732
+ envelope.kind === "leader.forwardCallback"
1733
+ ? envelope.query
1734
+ : envelope.kind === "leader.forwardReaction"
1735
+ ? envelope.reactionUpdate
1736
+ : envelope.message;
1737
+ if (
1738
+ !isRecord(carrier) ||
1739
+ carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId
1740
+ ) {
1741
+ throw new Error("Telegram follower delivery source update id mismatch.");
1742
+ }
1743
+ const update = {
1744
+ update_id: delivery.sourceUpdateId,
1745
+ ...(envelope.kind === "leader.forwardMessage" &&
1746
+ envelope.forwardCommentBatchPosition
1747
+ ? {
1748
+ [TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD]:
1749
+ envelope.forwardCommentBatchPosition,
1750
+ }
1751
+ : {}),
1752
+ ...(envelope.kind === "leader.forwardCallback"
1753
+ ? { callback_query: carrier }
1754
+ : envelope.kind === "leader.forwardReaction"
1755
+ ? { message_reaction: carrier }
1756
+ : envelope.kind === "leader.forwardMessage"
1757
+ ? { message: carrier }
1758
+ : { edited_message: carrier }),
1759
+ };
1760
+ deps.journal.appendBatch([update]);
1761
+ deps.signalWorker(ctx);
1762
+ return {
1763
+ deliveryId: delivery.deliveryId,
1764
+ sourceUpdateId: delivery.sourceUpdateId,
1765
+ };
1766
+ },
1767
+ };
1768
+ }
1769
+
1770
+ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
1771
+ deps: TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
1600
1772
  ): TelegramBusForwardedUpdateReceiverRuntime {
1601
1773
  const server = createTelegramBusLocalServer({
1602
1774
  socketPath: deps.socketPath,
@@ -1608,7 +1780,10 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
1608
1780
  },
1609
1781
  async handleEnvelope(envelope) {
1610
1782
  const authSecret = deps.getAuthSecret?.();
1611
- if (deps.getAuthSecret && (!authSecret || envelope.auth !== authSecret)) {
1783
+ if (
1784
+ deps.getAuthSecret &&
1785
+ (!authSecret || !isTelegramBusEnvelopeAuthorized(envelope, authSecret))
1786
+ ) {
1612
1787
  return createUnauthorizedBusAck(envelope.requestId);
1613
1788
  }
1614
1789
  if (
@@ -1616,7 +1791,8 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
1616
1791
  envelope.kind !== "leader.forwardReaction" &&
1617
1792
  envelope.kind !== "leader.forwardMessage" &&
1618
1793
  envelope.kind !== "leader.forwardEditedMessage" &&
1619
- envelope.kind !== "leader.replaceFollowerTarget") ||
1794
+ envelope.kind !== "leader.replaceFollowerTarget" &&
1795
+ envelope.kind !== "leader.offerQueueHandoff") ||
1620
1796
  envelope.recipientInstanceId !== deps.instanceId
1621
1797
  ) {
1622
1798
  return {
@@ -1626,9 +1802,9 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
1626
1802
  message: "Telegram bus receiver cannot handle this envelope.",
1627
1803
  };
1628
1804
  }
1629
- const registrationGeneration = deps.getRegistrationGeneration?.();
1805
+ const registrationGeneration = deps.getRegistrationGeneration();
1630
1806
  if (
1631
- registrationGeneration &&
1807
+ !registrationGeneration ||
1632
1808
  envelope.recipientRegistrationGeneration !== registrationGeneration
1633
1809
  ) {
1634
1810
  return {
@@ -1638,6 +1814,20 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
1638
1814
  message: "Stale Telegram bus follower registration generation.",
1639
1815
  };
1640
1816
  }
1817
+ if (
1818
+ envelope.kind !== "leader.replaceFollowerTarget" &&
1819
+ envelope.kind !== "leader.offerQueueHandoff" &&
1820
+ (!envelope.delivery ||
1821
+ envelope.delivery.recipientBindingKey !==
1822
+ deps.getRecipientBindingKey())
1823
+ ) {
1824
+ return {
1825
+ kind: "bus.ack",
1826
+ requestId: envelope.requestId,
1827
+ ok: false,
1828
+ message: "Mismatched Telegram follower delivery identity.",
1829
+ };
1830
+ }
1641
1831
  const ctx = deps.getContext();
1642
1832
  if (!ctx) {
1643
1833
  return {
@@ -1648,40 +1838,48 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<
1648
1838
  };
1649
1839
  }
1650
1840
  try {
1651
- if (envelope.kind === "leader.forwardCallback") {
1652
- await deps.handleForwardedCallback(
1653
- envelope.query as TCallbackQuery,
1654
- ctx,
1655
- );
1656
- } else if (envelope.kind === "leader.forwardReaction") {
1657
- await deps.handleForwardedReaction(
1658
- envelope.reactionUpdate as TReactionUpdate,
1659
- ctx,
1660
- );
1661
- } else if (envelope.kind === "leader.forwardMessage") {
1662
- if (envelope.forwardCommentBatchPosition) {
1663
- deps.prepareForwardedMessage?.(
1664
- envelope.message as TMessage,
1665
- envelope.forwardCommentBatchPosition,
1666
- );
1667
- }
1668
- if (!deps.handleForwardedMessage) {
1841
+ if (
1842
+ envelope.kind !== "leader.replaceFollowerTarget" &&
1843
+ envelope.kind !== "leader.offerQueueHandoff"
1844
+ ) {
1845
+ const receipt = await deps.durableAdmission.admit(envelope, ctx);
1846
+ return {
1847
+ kind: "bus.ack",
1848
+ requestId: envelope.requestId,
1849
+ ok: true,
1850
+ result: receipt,
1851
+ };
1852
+ }
1853
+ if (envelope.kind === "leader.offerQueueHandoff") {
1854
+ if (!deps.handleQueueHandoff) {
1669
1855
  throw new Error(
1670
- "Telegram bus receiver cannot handle this envelope.",
1856
+ "Telegram bus receiver cannot accept queue handoff payloads.",
1671
1857
  );
1672
1858
  }
1673
- await deps.handleForwardedMessage(envelope.message as TMessage, ctx);
1674
- } else if (envelope.kind === "leader.forwardEditedMessage") {
1675
- if (!deps.handleForwardedEditedMessage) {
1859
+ const result = await deps.handleQueueHandoff(envelope, ctx);
1860
+ const receipt = envelope.payload.admissionReceipts[0];
1861
+ if (
1862
+ envelope.payload.admissionReceipts.length !== 1 ||
1863
+ !receipt ||
1864
+ result.status !== "staged" ||
1865
+ result.receiptId !== receipt.receiptId ||
1866
+ result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
1867
+ result.sourceUpdateIds.some(
1868
+ (updateId, index) => updateId !== receipt.sourceUpdateIds[index],
1869
+ )
1870
+ ) {
1676
1871
  throw new Error(
1677
- "Telegram bus receiver cannot handle this envelope.",
1872
+ "Telegram queue handoff staging returned a mismatched receipt.",
1678
1873
  );
1679
1874
  }
1680
- await deps.handleForwardedEditedMessage(
1681
- envelope.message as TMessage,
1682
- ctx,
1683
- );
1684
- } else {
1875
+ return {
1876
+ kind: "bus.ack",
1877
+ requestId: envelope.requestId,
1878
+ ok: true,
1879
+ result,
1880
+ };
1881
+ }
1882
+ {
1685
1883
  if (!deps.handleReplaceTarget) {
1686
1884
  throw new Error(
1687
1885
  "Telegram bus receiver cannot replace follower target.",