@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.
package/lib/bus.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { execFileSync } from "node:child_process";
9
- import { createHash, randomBytes } from "node:crypto";
9
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
10
10
  import {
11
11
  chmodSync,
12
12
  existsSync,
@@ -24,6 +24,7 @@ import {
24
24
  type Server,
25
25
  type Socket,
26
26
  } from "node:net";
27
+ import { createRequire } from "node:module";
27
28
  import { platform as getPlatform, tmpdir } from "node:os";
28
29
  import { basename, dirname, join } from "node:path";
29
30
 
@@ -42,11 +43,19 @@ import {
42
43
  type TelegramBusTransportEventRecorder,
43
44
  type TelegramBusTransportRetryPolicy,
44
45
  } from "./bus-transport.ts";
46
+ import {
47
+ TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS,
48
+ TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES,
49
+ type TelegramQueueHandoffPayload,
50
+ } from "./queue.ts";
45
51
  import type { TelegramTarget } from "./target.ts";
52
+ import { isProcessAlive } from "./locks.ts";
46
53
  import { resolveAgentDir } from "./paths.ts";
47
54
 
48
55
  export interface TelegramBusProcessRuntime {
49
56
  instanceId: string;
57
+ processId: number;
58
+ processBirthId: string;
50
59
  manualFollowerOwnerId: string;
51
60
  getLeaderSocketPath: () => string;
52
61
  getFollowerSocketPath: () => string;
@@ -58,6 +67,17 @@ export interface TelegramProcessBirthIdentityOptions {
58
67
  readDarwinProcessStart?: (pid: number) => string;
59
68
  }
60
69
 
70
+ export type TelegramProcessBirthProof =
71
+ | { status: "proven"; identity: string }
72
+ | { status: "unverifiable" };
73
+
74
+ export type TelegramProcessLiveness = "alive" | "dead" | "unverifiable";
75
+
76
+ export interface TelegramProcessLivenessOptions
77
+ extends TelegramProcessBirthIdentityOptions {
78
+ isProcessAlive?: (pid: number) => boolean;
79
+ }
80
+
61
81
  function readDarwinProcessStart(pid: number): string {
62
82
  return execFileSync(
63
83
  "/bin/ps",
@@ -69,45 +89,67 @@ function readDarwinProcessStart(pid: number): string {
69
89
  ).trim();
70
90
  }
71
91
 
72
- export function getTelegramProcessBirthIdentity(
92
+ export function getTelegramProcessBirthProof(
73
93
  pid: number,
74
- fallbackGeneration: number | string,
75
94
  options: TelegramProcessBirthIdentityOptions = {},
76
- ): string {
77
- if (pid > 0) {
78
- const platform = options.platform ?? getPlatform();
79
- if (platform === "linux") {
80
- try {
81
- const stat = (options.readProcStat ?? ((targetPid) =>
82
- readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
83
- const closeParen = stat.lastIndexOf(")");
84
- const fields = stat
85
- .slice(closeParen + 2)
86
- .trim()
87
- .split(/\s+/u);
88
- const startTicks = fields[19];
89
- if (startTicks) return `${pid}:start:${startTicks}`;
90
- } catch {
91
- /* inaccessible process metadata */
95
+ ): TelegramProcessBirthProof {
96
+ if (pid <= 0) return { status: "unverifiable" };
97
+ const platform = options.platform ?? getPlatform();
98
+ if (platform === "linux") {
99
+ try {
100
+ const stat = (options.readProcStat ?? ((targetPid) =>
101
+ readFileSync(`/proc/${targetPid}/stat`, "utf8")))(pid);
102
+ const closeParen = stat.lastIndexOf(")");
103
+ const fields = stat
104
+ .slice(closeParen + 2)
105
+ .trim()
106
+ .split(/\s+/u);
107
+ const startTicks = fields[19];
108
+ if (startTicks) {
109
+ return { status: "proven", identity: `${pid}:start:${startTicks}` };
92
110
  }
93
- } else if (platform === "darwin") {
94
- try {
95
- const startedAt = (
96
- options.readDarwinProcessStart ?? readDarwinProcessStart
97
- )(pid);
98
- if (startedAt) {
99
- const fingerprint = createHash("sha256")
100
- .update(startedAt)
101
- .digest("hex")
102
- .slice(0, 16);
103
- return `${pid}:start:${fingerprint}`;
104
- }
105
- } catch {
106
- /* inaccessible process metadata */
111
+ } catch {
112
+ /* inaccessible process metadata */
113
+ }
114
+ } else if (platform === "darwin") {
115
+ try {
116
+ const startedAt = (
117
+ options.readDarwinProcessStart ?? readDarwinProcessStart
118
+ )(pid);
119
+ if (startedAt) {
120
+ const fingerprint = createHash("sha256")
121
+ .update(startedAt)
122
+ .digest("hex")
123
+ .slice(0, 16);
124
+ return { status: "proven", identity: `${pid}:start:${fingerprint}` };
107
125
  }
126
+ } catch {
127
+ /* inaccessible process metadata */
108
128
  }
109
129
  }
110
- return `${pid}:generation:${fallbackGeneration}`;
130
+ return { status: "unverifiable" };
131
+ }
132
+
133
+ export function getTelegramProcessBirthIdentity(
134
+ pid: number,
135
+ fallbackGeneration: number | string,
136
+ options: TelegramProcessBirthIdentityOptions = {},
137
+ ): string {
138
+ const proof = getTelegramProcessBirthProof(pid, options);
139
+ return proof.status === "proven"
140
+ ? proof.identity
141
+ : `${pid}:generation:${fallbackGeneration}`;
142
+ }
143
+
144
+ export function getTelegramProcessLiveness(
145
+ owner: { processId: number; processBirthId: string },
146
+ options: TelegramProcessLivenessOptions = {},
147
+ ): TelegramProcessLiveness {
148
+ const processAlive = options.isProcessAlive ?? isProcessAlive;
149
+ if (!processAlive(owner.processId)) return "dead";
150
+ const proof = getTelegramProcessBirthProof(owner.processId, options);
151
+ if (proof.status === "unverifiable") return "unverifiable";
152
+ return proof.identity === owner.processBirthId ? "alive" : "dead";
111
153
  }
112
154
 
113
155
  export function createCurrentTelegramBusProcessRuntime(input: {
@@ -138,6 +180,8 @@ export function createTelegramBusProcessRuntime(input: {
138
180
  getTelegramProcessBirthIdentity(ownerPid, input.createdAtMs);
139
181
  return {
140
182
  instanceId,
183
+ processId: input.pid,
184
+ processBirthId: getTelegramProcessBirthIdentity(input.pid, instanceId),
141
185
  manualFollowerOwnerId,
142
186
  getLeaderSocketPath: () =>
143
187
  getTelegramBusSocketPath(
@@ -159,6 +203,106 @@ export function createTelegramBusAuthSecret(): string {
159
203
  return randomBytes(32).toString("base64url");
160
204
  }
161
205
 
206
+ export const TELEGRAM_BUS_PROTOCOL_VERSION = 1 as const;
207
+ export const TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION =
208
+ "durable-follower-admission-v1" as const;
209
+ export const TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF =
210
+ "queue-handoff-v1" as const;
211
+
212
+ export interface TelegramBusProtocolIdentity {
213
+ protocolVersion: number;
214
+ runtimeBuild: string;
215
+ capabilities: string[];
216
+ }
217
+
218
+ export interface TelegramBusProtocolCompatibility {
219
+ compatible: boolean;
220
+ reason?: "missing-identity" | "version-mismatch" | "missing-capability";
221
+ missingCapabilities: string[];
222
+ }
223
+
224
+ export function createTelegramBusProtocolIdentity(input: {
225
+ runtimeBuild: string;
226
+ capabilities?: readonly string[];
227
+ }): TelegramBusProtocolIdentity {
228
+ const runtimeBuild = input.runtimeBuild.trim();
229
+ if (!runtimeBuild || runtimeBuild.length > 128) {
230
+ throw new Error("Telegram bus runtime build identity is invalid.");
231
+ }
232
+ const capabilities = [...new Set(input.capabilities ?? [])].sort();
233
+ if (
234
+ capabilities.length > 32 ||
235
+ capabilities.some(
236
+ (capability) =>
237
+ capability.length > 128 ||
238
+ !/^[a-z0-9][a-z0-9._-]*$/u.test(capability),
239
+ )
240
+ ) {
241
+ throw new Error("Telegram bus capabilities must be canonical identifiers.");
242
+ }
243
+ return {
244
+ protocolVersion: TELEGRAM_BUS_PROTOCOL_VERSION,
245
+ runtimeBuild,
246
+ capabilities,
247
+ };
248
+ }
249
+
250
+ export function createTelegramCurrentBusProtocolIdentity(
251
+ capabilities: readonly string[] = [],
252
+ ): TelegramBusProtocolIdentity {
253
+ const packageMetadata = createRequire(import.meta.url)("../package.json") as {
254
+ version?: unknown;
255
+ };
256
+ if (typeof packageMetadata.version !== "string") {
257
+ throw new Error("Telegram package build identity is unavailable.");
258
+ }
259
+ return createTelegramBusProtocolIdentity({
260
+ runtimeBuild: packageMetadata.version,
261
+ capabilities,
262
+ });
263
+ }
264
+
265
+ export function hasTelegramBusCapability(
266
+ identity: TelegramBusProtocolIdentity | undefined,
267
+ capability: string,
268
+ ): boolean {
269
+ return identity?.capabilities.includes(capability) ?? false;
270
+ }
271
+
272
+ export function getTelegramBusProtocolCompatibility(input: {
273
+ local: TelegramBusProtocolIdentity;
274
+ remote?: TelegramBusProtocolIdentity;
275
+ }): TelegramBusProtocolCompatibility {
276
+ if (!input.remote) {
277
+ return {
278
+ compatible: false,
279
+ reason: "missing-identity",
280
+ missingCapabilities: [],
281
+ };
282
+ }
283
+ if (input.remote.protocolVersion !== input.local.protocolVersion) {
284
+ return {
285
+ compatible: false,
286
+ reason: "version-mismatch",
287
+ missingCapabilities: [],
288
+ };
289
+ }
290
+ const remoteCapabilities = new Set(input.remote.capabilities);
291
+ const missingCapabilities = input.local.capabilities
292
+ .filter(
293
+ (capability) =>
294
+ capability === TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
295
+ )
296
+ .filter((capability) => !remoteCapabilities.has(capability));
297
+ return missingCapabilities.length > 0
298
+ ? {
299
+ compatible: false,
300
+ reason: "missing-capability",
301
+ missingCapabilities,
302
+ }
303
+ : { compatible: true, missingCapabilities: [] };
304
+ }
305
+
162
306
  export function getTelegramBusSocketPath(
163
307
  agentDir = resolveAgentDir(),
164
308
  platform = getPlatform(),
@@ -192,6 +336,9 @@ export interface TelegramBusInstanceRegistration {
192
336
  target?: TelegramTarget;
193
337
  busSocketPath?: string;
194
338
  registrationGeneration?: string;
339
+ protocol?: TelegramBusProtocolIdentity;
340
+ sessionGeneration?: number;
341
+ processBirthId?: string;
195
342
  connectedAtMs: number;
196
343
  }
197
344
 
@@ -210,17 +357,28 @@ export function getTelegramFollowerTargetOwnership(input: {
210
357
  target: TelegramTarget;
211
358
  }[];
212
359
  currentInstanceId?: string;
213
- }): { instanceId: string; ownerGeneration?: string } | undefined {
360
+ }): {
361
+ instanceId: string;
362
+ ownerGeneration: string;
363
+ recipientBindingKey: string;
364
+ } | undefined {
214
365
  const liveFollower = input.followers.find((follower) => {
215
366
  return (
216
367
  follower.target?.chatId === input.target.chatId &&
217
368
  follower.target.threadId === input.target.threadId
218
369
  );
219
370
  });
220
- if (liveFollower) {
371
+ if (
372
+ liveFollower?.registrationGeneration &&
373
+ liveFollower.profileKey &&
374
+ liveFollower.protocol?.capabilities.includes(
375
+ TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
376
+ )
377
+ ) {
221
378
  return {
222
379
  instanceId: liveFollower.instanceId,
223
380
  ownerGeneration: liveFollower.registrationGeneration,
381
+ recipientBindingKey: liveFollower.profileKey,
224
382
  };
225
383
  }
226
384
  // Persisted records are restart hints, not live routing authority. Only an
@@ -460,6 +618,69 @@ export interface TelegramBusAgentMessage {
460
618
  text: string;
461
619
  }
462
620
 
621
+ export interface TelegramBusFollowerDeliveryIdentity {
622
+ deliveryId: string;
623
+ sourceUpdateId: number;
624
+ recipientBindingKey: string;
625
+ }
626
+
627
+ export type TelegramBusForeignUpdateFailureClass =
628
+ | "source-update-identity-missing"
629
+ | "recipient-binding-missing"
630
+ | "recipient-generation-missing"
631
+ | "transport-failed"
632
+ | "acknowledgement-missing"
633
+ | "acknowledgement-rejected"
634
+ | "acknowledgement-mismatched"
635
+ | "durable-receipt-missing"
636
+ | "durable-receipt-mismatched";
637
+
638
+ export type TelegramBusForeignUpdateSettlement =
639
+ | {
640
+ status: "accepted";
641
+ delivery: TelegramBusFollowerDeliveryIdentity;
642
+ }
643
+ | {
644
+ status: "retryable" | "terminal-rejected";
645
+ failureClass: TelegramBusForeignUpdateFailureClass;
646
+ message: string;
647
+ delivery?: TelegramBusFollowerDeliveryIdentity;
648
+ sourceUpdateId?: number;
649
+ };
650
+
651
+ export function createTelegramBusFollowerDeliveryIdentity(input: {
652
+ kind:
653
+ | "leader.forwardCallback"
654
+ | "leader.forwardReaction"
655
+ | "leader.forwardMessage"
656
+ | "leader.forwardEditedMessage";
657
+ recipientBindingKey: string;
658
+ sourceUpdateId: number;
659
+ }): TelegramBusFollowerDeliveryIdentity {
660
+ if (
661
+ !input.recipientBindingKey ||
662
+ !Number.isSafeInteger(input.sourceUpdateId) ||
663
+ input.sourceUpdateId < 0
664
+ ) {
665
+ throw new Error("Telegram follower delivery identity is incomplete.");
666
+ }
667
+ const deliveryId = createHash("sha256")
668
+ .update(
669
+ JSON.stringify({
670
+ version: 1,
671
+ kind: input.kind,
672
+ recipientBindingKey: input.recipientBindingKey,
673
+ sourceUpdateId: input.sourceUpdateId,
674
+ }),
675
+ )
676
+ .digest("hex");
677
+ return {
678
+ deliveryId: `telegram-follower-v1-${deliveryId}`,
679
+ sourceUpdateId: input.sourceUpdateId,
680
+ recipientBindingKey: input.recipientBindingKey,
681
+ };
682
+ }
683
+
463
684
  export type TelegramBusEnvelope = (
464
685
  | {
465
686
  kind: "follower.register";
@@ -484,7 +705,8 @@ export type TelegramBusEnvelope = (
484
705
  kind: "leader.forwardCallback";
485
706
  requestId: string;
486
707
  recipientInstanceId: string;
487
- recipientRegistrationGeneration?: string;
708
+ recipientRegistrationGeneration: string;
709
+ delivery: TelegramBusFollowerDeliveryIdentity;
488
710
  query: unknown;
489
711
  sentAtMs: number;
490
712
  }
@@ -492,7 +714,8 @@ export type TelegramBusEnvelope = (
492
714
  kind: "leader.forwardReaction";
493
715
  requestId: string;
494
716
  recipientInstanceId: string;
495
- recipientRegistrationGeneration?: string;
717
+ recipientRegistrationGeneration: string;
718
+ delivery: TelegramBusFollowerDeliveryIdentity;
496
719
  reactionUpdate: unknown;
497
720
  sentAtMs: number;
498
721
  }
@@ -500,7 +723,8 @@ export type TelegramBusEnvelope = (
500
723
  kind: "leader.forwardMessage";
501
724
  requestId: string;
502
725
  recipientInstanceId: string;
503
- recipientRegistrationGeneration?: string;
726
+ recipientRegistrationGeneration: string;
727
+ delivery: TelegramBusFollowerDeliveryIdentity;
504
728
  message: unknown;
505
729
  forwardCommentBatchPosition?: "comment" | "forward";
506
730
  sentAtMs: number;
@@ -509,7 +733,8 @@ export type TelegramBusEnvelope = (
509
733
  kind: "leader.forwardEditedMessage";
510
734
  requestId: string;
511
735
  recipientInstanceId: string;
512
- recipientRegistrationGeneration?: string;
736
+ recipientRegistrationGeneration: string;
737
+ delivery: TelegramBusFollowerDeliveryIdentity;
513
738
  message: unknown;
514
739
  sentAtMs: number;
515
740
  }
@@ -523,6 +748,37 @@ export type TelegramBusEnvelope = (
523
748
  reason: "thread-restore";
524
749
  sentAtMs: number;
525
750
  }
751
+ | {
752
+ kind: "leader.offerQueueHandoff";
753
+ requestId: string;
754
+ recipientInstanceId: string;
755
+ recipientRegistrationGeneration: string;
756
+ donorInstanceId: string;
757
+ donorProcessId: number;
758
+ donorProcessBirthId: string;
759
+ donorSessionGeneration: number;
760
+ donorAcquisitionId: string;
761
+ donorAcquiredAtMs: number;
762
+ handoffToken: string;
763
+ payload: TelegramQueueHandoffPayload;
764
+ sentAtMs: number;
765
+ }
766
+ | {
767
+ kind: "follower.offerQueueHandoff";
768
+ requestId: string;
769
+ instanceId: string;
770
+ registrationGeneration: string;
771
+ recipientInstanceId: string;
772
+ recipientRegistrationGeneration: string;
773
+ donorProcessId: number;
774
+ donorProcessBirthId: string;
775
+ donorSessionGeneration: number;
776
+ donorAcquisitionId: string;
777
+ donorAcquiredAtMs: number;
778
+ handoffToken: string;
779
+ payload: TelegramQueueHandoffPayload;
780
+ sentAtMs: number;
781
+ }
526
782
  | {
527
783
  kind: "follower.resolveAgentTarget";
528
784
  requestId: string;
@@ -554,13 +810,31 @@ export type TelegramBusEnvelope = (
554
810
  ok: boolean;
555
811
  message?: string;
556
812
  result?: unknown;
813
+ protocol?: TelegramBusProtocolIdentity;
557
814
  error?: {
558
- code: "commit-unknown" | "request-id-collision" | "ledger-overloaded";
815
+ code:
816
+ | "commit-unknown"
817
+ | "request-id-collision"
818
+ | "ledger-overloaded"
819
+ | "incompatible-protocol";
559
820
  method?: string;
560
821
  };
561
822
  }
562
823
  ) & { auth?: string };
563
824
 
825
+ export type TelegramBusEnvelopeTrafficClass =
826
+ | "bootstrap"
827
+ | "generation-fenced"
828
+ | "response";
829
+
830
+ export function getTelegramBusEnvelopeTrafficClass(
831
+ envelope: TelegramBusEnvelope,
832
+ ): TelegramBusEnvelopeTrafficClass {
833
+ if (envelope.kind === "follower.register") return "bootstrap";
834
+ if (envelope.kind === "bus.ack") return "response";
835
+ return "generation-fenced";
836
+ }
837
+
564
838
  export function createTelegramBusRequestId(input: {
565
839
  instanceId: string;
566
840
  sequence: number;
@@ -633,6 +907,20 @@ export function parseTelegramBusEnvelope(
633
907
  case "leader.replaceFollowerTarget":
634
908
  envelope = parseReplaceFollowerTargetEnvelope(value, requestId);
635
909
  break;
910
+ case "leader.offerQueueHandoff":
911
+ envelope = parseQueueHandoffEnvelope(
912
+ value,
913
+ requestId,
914
+ "leader.offerQueueHandoff",
915
+ );
916
+ break;
917
+ case "follower.offerQueueHandoff":
918
+ envelope = parseQueueHandoffEnvelope(
919
+ value,
920
+ requestId,
921
+ "follower.offerQueueHandoff",
922
+ );
923
+ break;
636
924
  case "follower.resolveAgentTarget":
637
925
  envelope = parseResolveAgentTargetEnvelope(value, requestId);
638
926
  break;
@@ -735,6 +1023,52 @@ export interface TelegramBusForeignOwnedForwarderDeps<TMessage = unknown> {
735
1023
  ) => void;
736
1024
  }
737
1025
 
1026
+ interface TelegramBusForwardOwnership {
1027
+ instanceId: string;
1028
+ ownerGeneration?: string;
1029
+ recipientBindingKey?: string;
1030
+ }
1031
+
1032
+ type TelegramBusDurableForwardEnvelope = Extract<
1033
+ TelegramBusEnvelope,
1034
+ {
1035
+ kind:
1036
+ | "leader.forwardCallback"
1037
+ | "leader.forwardReaction"
1038
+ | "leader.forwardMessage"
1039
+ | "leader.forwardEditedMessage";
1040
+ }
1041
+ > & {
1042
+ recipientRegistrationGeneration: string;
1043
+ delivery: TelegramBusFollowerDeliveryIdentity;
1044
+ };
1045
+
1046
+ function getTelegramBusForwardSourceUpdateId(
1047
+ value: unknown,
1048
+ ): number | undefined {
1049
+ if (!isRecord(value)) return undefined;
1050
+ const updateId = value.pi_telegram_source_update_id;
1051
+ return Number.isSafeInteger(updateId) && (updateId as number) >= 0
1052
+ ? (updateId as number)
1053
+ : undefined;
1054
+ }
1055
+
1056
+ function createTelegramBusForwardDelivery(
1057
+ kind:
1058
+ | "leader.forwardCallback"
1059
+ | "leader.forwardReaction"
1060
+ | "leader.forwardMessage"
1061
+ | "leader.forwardEditedMessage",
1062
+ sourceUpdateId: number,
1063
+ recipientBindingKey: string,
1064
+ ): TelegramBusFollowerDeliveryIdentity {
1065
+ return createTelegramBusFollowerDeliveryIdentity({
1066
+ kind,
1067
+ recipientBindingKey,
1068
+ sourceUpdateId,
1069
+ });
1070
+ }
1071
+
738
1072
  export function createTelegramBusForeignOwnedUpdateForwarder<
739
1073
  TContext,
740
1074
  TReactionUpdate,
@@ -745,108 +1079,286 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
745
1079
  ): {
746
1080
  forwardCallback: (input: {
747
1081
  query: TCallbackQuery;
748
- ownership: { instanceId: string; ownerGeneration?: string };
1082
+ ownership: TelegramBusForwardOwnership;
749
1083
  ctx: TContext;
750
- }) => Promise<boolean>;
1084
+ }) => Promise<TelegramBusForeignUpdateSettlement>;
751
1085
  forwardReaction: (input: {
752
1086
  reactionUpdate: TReactionUpdate;
753
- ownership: { instanceId: string; ownerGeneration?: string };
1087
+ ownership: TelegramBusForwardOwnership;
754
1088
  ctx: TContext;
755
- }) => Promise<boolean>;
1089
+ }) => Promise<TelegramBusForeignUpdateSettlement>;
756
1090
  forwardMessage: (input: {
757
1091
  message: TMessage;
758
- ownership: { instanceId: string; ownerGeneration?: string };
1092
+ ownership: TelegramBusForwardOwnership;
759
1093
  ctx: TContext;
760
- }) => Promise<boolean>;
1094
+ }) => Promise<TelegramBusForeignUpdateSettlement>;
761
1095
  forwardEditedMessage: (input: {
762
1096
  message: TMessage;
763
- ownership: { instanceId: string; ownerGeneration?: string };
1097
+ ownership: TelegramBusForwardOwnership;
764
1098
  ctx: TContext;
765
- }) => Promise<boolean>;
1099
+ }) => Promise<TelegramBusForeignUpdateSettlement>;
766
1100
  } {
767
1101
  const getNowMs = deps.getNowMs ?? Date.now;
768
- const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
1102
+ const reject = (input: {
1103
+ status: "retryable" | "terminal-rejected";
1104
+ failureClass: TelegramBusForeignUpdateFailureClass;
1105
+ message: string;
1106
+ envelopeKind: TelegramBusDurableForwardEnvelope["kind"];
1107
+ ownership: TelegramBusForwardOwnership;
1108
+ delivery?: TelegramBusFollowerDeliveryIdentity;
1109
+ sourceUpdateId?: number;
1110
+ }): TelegramBusForeignUpdateSettlement => {
1111
+ const settlement: TelegramBusForeignUpdateSettlement = {
1112
+ status: input.status,
1113
+ failureClass: input.failureClass,
1114
+ message: input.message,
1115
+ ...(input.delivery ? { delivery: input.delivery } : {}),
1116
+ ...(input.sourceUpdateId !== undefined
1117
+ ? { sourceUpdateId: input.sourceUpdateId }
1118
+ : {}),
1119
+ };
1120
+ deps.recordRuntimeEvent?.("bus", input.message, {
1121
+ phase: "foreign-update-forward-rejected",
1122
+ settlement: input.status,
1123
+ failureClass: input.failureClass,
1124
+ envelopeKind: input.envelopeKind,
1125
+ recipientInstanceId: input.ownership.instanceId,
1126
+ deliveryId: input.delivery?.deliveryId,
1127
+ sourceUpdateId:
1128
+ input.delivery?.sourceUpdateId ?? input.sourceUpdateId,
1129
+ });
1130
+ return settlement;
1131
+ };
1132
+ const prepare = (
1133
+ kind: TelegramBusDurableForwardEnvelope["kind"],
1134
+ value: unknown,
1135
+ ownership: TelegramBusForwardOwnership,
1136
+ ):
1137
+ | {
1138
+ delivery: TelegramBusFollowerDeliveryIdentity;
1139
+ recipientRegistrationGeneration: string;
1140
+ }
1141
+ | { settlement: TelegramBusForeignUpdateSettlement } => {
1142
+ const sourceUpdateId = getTelegramBusForwardSourceUpdateId(value);
1143
+ if (sourceUpdateId === undefined) {
1144
+ return {
1145
+ settlement: reject({
1146
+ status: "terminal-rejected",
1147
+ failureClass: "source-update-identity-missing",
1148
+ message: "Forwarded Telegram update has no durable source identity.",
1149
+ envelopeKind: kind,
1150
+ ownership,
1151
+ }),
1152
+ };
1153
+ }
1154
+ if (!ownership.recipientBindingKey) {
1155
+ return {
1156
+ settlement: reject({
1157
+ status: "terminal-rejected",
1158
+ failureClass: "recipient-binding-missing",
1159
+ message: "Forwarded Telegram update has no stable recipient binding.",
1160
+ envelopeKind: kind,
1161
+ ownership,
1162
+ sourceUpdateId,
1163
+ }),
1164
+ };
1165
+ }
1166
+ const delivery = createTelegramBusForwardDelivery(
1167
+ kind,
1168
+ sourceUpdateId,
1169
+ ownership.recipientBindingKey,
1170
+ );
1171
+ if (!ownership.ownerGeneration) {
1172
+ return {
1173
+ settlement: reject({
1174
+ status: "retryable",
1175
+ failureClass: "recipient-generation-missing",
1176
+ message: "Forwarded Telegram update has no live recipient generation.",
1177
+ envelopeKind: kind,
1178
+ ownership,
1179
+ delivery,
1180
+ }),
1181
+ };
1182
+ }
1183
+ return {
1184
+ delivery,
1185
+ recipientRegistrationGeneration: ownership.ownerGeneration,
1186
+ };
1187
+ };
1188
+ const send = async (
1189
+ envelope: TelegramBusDurableForwardEnvelope,
1190
+ ownership: TelegramBusForwardOwnership,
1191
+ ): Promise<TelegramBusForeignUpdateSettlement> => {
769
1192
  if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
770
1193
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
771
- const response = await sendTelegramBusLocalEnvelope({
772
- socketPath,
773
- envelope,
774
- timeoutMs: deps.timeoutMs,
775
- retry: getTelegramBusTransportRetryPolicy({
776
- endpoint: socketPath,
777
- operation: "operation",
778
- }),
779
- });
780
- const accepted = response?.kind === "bus.ack" && response.ok;
781
- if (!accepted) {
782
- deps.recordRuntimeEvent?.(
783
- "bus",
784
- response?.kind === "bus.ack"
785
- ? (response.message ?? "Follower rejected forwarded Telegram update.")
786
- : "Follower returned no forwarding acknowledgement.",
787
- {
788
- phase: "foreign-update-forward-rejected",
789
- envelopeKind: envelope.kind,
790
- recipientInstanceId:
791
- "recipientInstanceId" in envelope
792
- ? envelope.recipientInstanceId
793
- : undefined,
794
- },
795
- );
1194
+ let response: TelegramBusEnvelope | undefined;
1195
+ try {
1196
+ response = await sendTelegramBusLocalEnvelope({
1197
+ socketPath,
1198
+ envelope,
1199
+ timeoutMs: deps.timeoutMs,
1200
+ retry: getTelegramBusTransportRetryPolicy({
1201
+ endpoint: socketPath,
1202
+ operation: "operation",
1203
+ }),
1204
+ });
1205
+ } catch (error) {
1206
+ return reject({
1207
+ status: "retryable",
1208
+ failureClass: "transport-failed",
1209
+ message:
1210
+ error instanceof Error
1211
+ ? error.message
1212
+ : "Telegram follower forwarding transport failed.",
1213
+ envelopeKind: envelope.kind,
1214
+ ownership,
1215
+ delivery: envelope.delivery,
1216
+ });
1217
+ }
1218
+ if (response?.kind !== "bus.ack") {
1219
+ return reject({
1220
+ status: "retryable",
1221
+ failureClass: "acknowledgement-missing",
1222
+ message: "Follower returned no forwarding acknowledgement.",
1223
+ envelopeKind: envelope.kind,
1224
+ ownership,
1225
+ delivery: envelope.delivery,
1226
+ });
1227
+ }
1228
+ if (response.requestId !== envelope.requestId) {
1229
+ return reject({
1230
+ status: "terminal-rejected",
1231
+ failureClass: "acknowledgement-mismatched",
1232
+ message: "Follower returned a mismatched forwarding acknowledgement.",
1233
+ envelopeKind: envelope.kind,
1234
+ ownership,
1235
+ delivery: envelope.delivery,
1236
+ });
1237
+ }
1238
+ if (!response.ok) {
1239
+ return reject({
1240
+ status: "retryable",
1241
+ failureClass: "acknowledgement-rejected",
1242
+ message:
1243
+ response.message ?? "Follower rejected forwarded Telegram update.",
1244
+ envelopeKind: envelope.kind,
1245
+ ownership,
1246
+ delivery: envelope.delivery,
1247
+ });
1248
+ }
1249
+ const receipt = response.result;
1250
+ if (
1251
+ !isRecord(receipt) ||
1252
+ typeof receipt.deliveryId !== "string" ||
1253
+ !Number.isSafeInteger(receipt.sourceUpdateId)
1254
+ ) {
1255
+ return reject({
1256
+ status: "terminal-rejected",
1257
+ failureClass: "durable-receipt-missing",
1258
+ message: "Follower acknowledgement omitted the durable receipt.",
1259
+ envelopeKind: envelope.kind,
1260
+ ownership,
1261
+ delivery: envelope.delivery,
1262
+ });
1263
+ }
1264
+ if (
1265
+ receipt.deliveryId !== envelope.delivery.deliveryId ||
1266
+ receipt.sourceUpdateId !== envelope.delivery.sourceUpdateId
1267
+ ) {
1268
+ return reject({
1269
+ status: "terminal-rejected",
1270
+ failureClass: "durable-receipt-mismatched",
1271
+ message: "Follower acknowledgement returned a mismatched durable receipt.",
1272
+ envelopeKind: envelope.kind,
1273
+ ownership,
1274
+ delivery: envelope.delivery,
1275
+ });
796
1276
  }
797
- return accepted;
1277
+ return { status: "accepted", delivery: envelope.delivery };
798
1278
  };
799
1279
  return {
800
- forwardCallback: ({ query, ownership }) =>
801
- send({
802
- kind: "leader.forwardCallback",
803
- requestId: deps.createRequestId(),
804
- recipientInstanceId: ownership.instanceId,
805
- ...(ownership.ownerGeneration
806
- ? { recipientRegistrationGeneration: ownership.ownerGeneration }
807
- : {}),
808
- query,
809
- sentAtMs: getNowMs(),
810
- }),
811
- forwardReaction: ({ reactionUpdate, ownership }) =>
812
- send({
813
- kind: "leader.forwardReaction",
814
- requestId: deps.createRequestId(),
815
- recipientInstanceId: ownership.instanceId,
816
- ...(ownership.ownerGeneration
817
- ? { recipientRegistrationGeneration: ownership.ownerGeneration }
818
- : {}),
1280
+ forwardCallback: ({ query, ownership }) => {
1281
+ const prepared = prepare("leader.forwardCallback", query, ownership);
1282
+ if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1283
+ return send(
1284
+ {
1285
+ kind: "leader.forwardCallback",
1286
+ requestId: deps.createRequestId(),
1287
+ recipientInstanceId: ownership.instanceId,
1288
+ recipientRegistrationGeneration:
1289
+ prepared.recipientRegistrationGeneration,
1290
+ delivery: prepared.delivery,
1291
+ query,
1292
+ sentAtMs: getNowMs(),
1293
+ },
1294
+ ownership,
1295
+ );
1296
+ },
1297
+ forwardReaction: ({ reactionUpdate, ownership }) => {
1298
+ const prepared = prepare(
1299
+ "leader.forwardReaction",
819
1300
  reactionUpdate,
820
- sentAtMs: getNowMs(),
821
- }),
822
- forwardMessage: ({ message, ownership }) =>
823
- send({
824
- kind: "leader.forwardMessage",
825
- requestId: deps.createRequestId(),
826
- recipientInstanceId: ownership.instanceId,
827
- ...(ownership.ownerGeneration
828
- ? { recipientRegistrationGeneration: ownership.ownerGeneration }
829
- : {}),
830
- message,
831
- ...(deps.getForwardCommentBatchPosition?.(message) !== undefined
832
- ? {
833
- forwardCommentBatchPosition:
834
- deps.getForwardCommentBatchPosition(message),
835
- }
836
- : {}),
837
- sentAtMs: getNowMs(),
838
- }),
839
- forwardEditedMessage: ({ message, ownership }) =>
840
- send({
841
- kind: "leader.forwardEditedMessage",
842
- requestId: deps.createRequestId(),
843
- recipientInstanceId: ownership.instanceId,
844
- ...(ownership.ownerGeneration
845
- ? { recipientRegistrationGeneration: ownership.ownerGeneration }
846
- : {}),
1301
+ ownership,
1302
+ );
1303
+ if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1304
+ return send(
1305
+ {
1306
+ kind: "leader.forwardReaction",
1307
+ requestId: deps.createRequestId(),
1308
+ recipientInstanceId: ownership.instanceId,
1309
+ recipientRegistrationGeneration:
1310
+ prepared.recipientRegistrationGeneration,
1311
+ delivery: prepared.delivery,
1312
+ reactionUpdate,
1313
+ sentAtMs: getNowMs(),
1314
+ },
1315
+ ownership,
1316
+ );
1317
+ },
1318
+ forwardMessage: ({ message, ownership }) => {
1319
+ const prepared = prepare("leader.forwardMessage", message, ownership);
1320
+ if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1321
+ return send(
1322
+ {
1323
+ kind: "leader.forwardMessage",
1324
+ requestId: deps.createRequestId(),
1325
+ recipientInstanceId: ownership.instanceId,
1326
+ recipientRegistrationGeneration:
1327
+ prepared.recipientRegistrationGeneration,
1328
+ delivery: prepared.delivery,
1329
+ message,
1330
+ ...(deps.getForwardCommentBatchPosition?.(message) !== undefined
1331
+ ? {
1332
+ forwardCommentBatchPosition:
1333
+ deps.getForwardCommentBatchPosition(message),
1334
+ }
1335
+ : {}),
1336
+ sentAtMs: getNowMs(),
1337
+ },
1338
+ ownership,
1339
+ );
1340
+ },
1341
+ forwardEditedMessage: ({ message, ownership }) => {
1342
+ const prepared = prepare(
1343
+ "leader.forwardEditedMessage",
847
1344
  message,
848
- sentAtMs: getNowMs(),
849
- }),
1345
+ ownership,
1346
+ );
1347
+ if ("settlement" in prepared) return Promise.resolve(prepared.settlement);
1348
+ return send(
1349
+ {
1350
+ kind: "leader.forwardEditedMessage",
1351
+ requestId: deps.createRequestId(),
1352
+ recipientInstanceId: ownership.instanceId,
1353
+ recipientRegistrationGeneration:
1354
+ prepared.recipientRegistrationGeneration,
1355
+ delivery: prepared.delivery,
1356
+ message,
1357
+ sentAtMs: getNowMs(),
1358
+ },
1359
+ ownership,
1360
+ );
1361
+ },
850
1362
  };
851
1363
  }
852
1364
 
@@ -885,16 +1397,14 @@ export function createTelegramBusFollowerTargetController(
885
1397
  const getNowMs = deps.getNowMs ?? Date.now;
886
1398
  return {
887
1399
  async replaceTarget({ follower, target, oldTarget, reason }) {
888
- if (!follower.busSocketPath) return false;
1400
+ if (!follower.busSocketPath || !follower.registrationGeneration) {
1401
+ return false;
1402
+ }
889
1403
  const envelope: TelegramBusEnvelope = {
890
1404
  kind: "leader.replaceFollowerTarget",
891
1405
  requestId: deps.createRequestId(),
892
1406
  recipientInstanceId: follower.instanceId,
893
- ...(follower.registrationGeneration
894
- ? {
895
- recipientRegistrationGeneration: follower.registrationGeneration,
896
- }
897
- : {}),
1407
+ recipientRegistrationGeneration: follower.registrationGeneration,
898
1408
  target,
899
1409
  ...(oldTarget ? { oldTarget } : {}),
900
1410
  reason,
@@ -947,7 +1457,11 @@ export function isTelegramBusEnvelopeAuthorized(
947
1457
  envelope: TelegramBusEnvelope,
948
1458
  secret: string | undefined,
949
1459
  ): boolean {
950
- return !secret || envelope.auth === secret;
1460
+ if (!secret) return true;
1461
+ if (typeof envelope.auth !== "string") return false;
1462
+ const auth = Buffer.from(envelope.auth);
1463
+ const expected = Buffer.from(secret);
1464
+ return auth.length === expected.length && timingSafeEqual(auth, expected);
951
1465
  }
952
1466
 
953
1467
  export function createUnauthorizedBusAck(
@@ -1354,6 +1868,14 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
1354
1868
  ): TelegramBusFollowerView => ({
1355
1869
  ...follower,
1356
1870
  target: follower.target ? { ...follower.target } : undefined,
1871
+ ...(follower.protocol
1872
+ ? {
1873
+ protocol: {
1874
+ ...follower.protocol,
1875
+ capabilities: [...follower.protocol.capabilities],
1876
+ },
1877
+ }
1878
+ : {}),
1357
1879
  });
1358
1880
  return {
1359
1881
  register: (registration) => {
@@ -1372,6 +1894,14 @@ export function createTelegramBusFollowerRegistry(): TelegramBusFollowerRegistry
1372
1894
  const next: TelegramBusFollowerView = {
1373
1895
  ...registration,
1374
1896
  target: registration.target ? { ...registration.target } : undefined,
1897
+ ...(registration.protocol
1898
+ ? {
1899
+ protocol: {
1900
+ ...registration.protocol,
1901
+ capabilities: [...registration.protocol.capabilities],
1902
+ },
1903
+ }
1904
+ : {}),
1375
1905
  lastHeartbeatMs:
1376
1906
  existing?.lastHeartbeatMs ?? registration.connectedAtMs,
1377
1907
  };
@@ -1510,22 +2040,42 @@ function parseDisconnectEnvelope(
1510
2040
  : undefined;
1511
2041
  }
1512
2042
 
2043
+ function parseTelegramBusFollowerDeliveryIdentity(
2044
+ value: unknown,
2045
+ ): TelegramBusFollowerDeliveryIdentity | undefined {
2046
+ if (!isRecord(value)) return undefined;
2047
+ if (
2048
+ typeof value.deliveryId !== "string" ||
2049
+ !/^telegram-follower-v1-[a-f0-9]{64}$/u.test(value.deliveryId) ||
2050
+ !Number.isSafeInteger(value.sourceUpdateId) ||
2051
+ (value.sourceUpdateId as number) < 0 ||
2052
+ typeof value.recipientBindingKey !== "string" ||
2053
+ !value.recipientBindingKey
2054
+ ) {
2055
+ return undefined;
2056
+ }
2057
+ return {
2058
+ deliveryId: value.deliveryId,
2059
+ sourceUpdateId: value.sourceUpdateId as number,
2060
+ recipientBindingKey: value.recipientBindingKey,
2061
+ };
2062
+ }
2063
+
1513
2064
  function parseForwardCallbackEnvelope(
1514
2065
  value: Record<string, unknown>,
1515
2066
  requestId: string,
1516
2067
  ): TelegramBusEnvelope | undefined {
1517
- return typeof value.recipientInstanceId === "string" &&
2068
+ const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
2069
+ return delivery &&
2070
+ typeof value.recipientInstanceId === "string" &&
2071
+ typeof value.recipientRegistrationGeneration === "string" &&
1518
2072
  typeof value.sentAtMs === "number"
1519
2073
  ? {
1520
2074
  kind: "leader.forwardCallback",
1521
2075
  requestId,
1522
2076
  recipientInstanceId: value.recipientInstanceId,
1523
- ...(typeof value.recipientRegistrationGeneration === "string"
1524
- ? {
1525
- recipientRegistrationGeneration:
1526
- value.recipientRegistrationGeneration,
1527
- }
1528
- : {}),
2077
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2078
+ delivery,
1529
2079
  query: value.query,
1530
2080
  sentAtMs: value.sentAtMs,
1531
2081
  }
@@ -1536,18 +2086,17 @@ function parseForwardReactionEnvelope(
1536
2086
  value: Record<string, unknown>,
1537
2087
  requestId: string,
1538
2088
  ): TelegramBusEnvelope | undefined {
1539
- return typeof value.recipientInstanceId === "string" &&
2089
+ const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
2090
+ return delivery &&
2091
+ typeof value.recipientInstanceId === "string" &&
2092
+ typeof value.recipientRegistrationGeneration === "string" &&
1540
2093
  typeof value.sentAtMs === "number"
1541
2094
  ? {
1542
2095
  kind: "leader.forwardReaction",
1543
2096
  requestId,
1544
2097
  recipientInstanceId: value.recipientInstanceId,
1545
- ...(typeof value.recipientRegistrationGeneration === "string"
1546
- ? {
1547
- recipientRegistrationGeneration:
1548
- value.recipientRegistrationGeneration,
1549
- }
1550
- : {}),
2098
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2099
+ delivery,
1551
2100
  reactionUpdate: value.reactionUpdate,
1552
2101
  sentAtMs: value.sentAtMs,
1553
2102
  }
@@ -1559,18 +2108,17 @@ function parseForwardMessageEnvelope(
1559
2108
  requestId: string,
1560
2109
  kind: "leader.forwardMessage" | "leader.forwardEditedMessage",
1561
2110
  ): TelegramBusEnvelope | undefined {
1562
- return typeof value.recipientInstanceId === "string" &&
2111
+ const delivery = parseTelegramBusFollowerDeliveryIdentity(value.delivery);
2112
+ return delivery &&
2113
+ typeof value.recipientInstanceId === "string" &&
2114
+ typeof value.recipientRegistrationGeneration === "string" &&
1563
2115
  typeof value.sentAtMs === "number"
1564
2116
  ? {
1565
2117
  kind,
1566
2118
  requestId,
1567
2119
  recipientInstanceId: value.recipientInstanceId,
1568
- ...(typeof value.recipientRegistrationGeneration === "string"
1569
- ? {
1570
- recipientRegistrationGeneration:
1571
- value.recipientRegistrationGeneration,
1572
- }
1573
- : {}),
2120
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2121
+ delivery,
1574
2122
  message: value.message,
1575
2123
  ...(kind === "leader.forwardMessage" &&
1576
2124
  (value.forwardCommentBatchPosition === "comment" ||
@@ -1616,6 +2164,247 @@ function parseReplaceFollowerTargetEnvelope(
1616
2164
  };
1617
2165
  }
1618
2166
 
2167
+ function parseQueueAdmissionReceipt(
2168
+ value: unknown,
2169
+ queueKind: "prompt" | "control",
2170
+ ):
2171
+ | {
2172
+ queueKind: "prompt" | "control";
2173
+ receiptId: string;
2174
+ sourceUpdateIds: number[];
2175
+ }
2176
+ | undefined {
2177
+ if (!isRecord(value) || value.queueKind !== queueKind) return undefined;
2178
+ const sourceUpdateIds = Array.isArray(value.sourceUpdateIds)
2179
+ ? value.sourceUpdateIds
2180
+ : undefined;
2181
+ if (
2182
+ typeof value.receiptId !== "string" ||
2183
+ !value.receiptId ||
2184
+ (value.journalBindingKey !== undefined &&
2185
+ (typeof value.journalBindingKey !== "string" ||
2186
+ !value.journalBindingKey.trim())) ||
2187
+ !sourceUpdateIds ||
2188
+ sourceUpdateIds.length === 0 ||
2189
+ sourceUpdateIds.some(
2190
+ (updateId, index) =>
2191
+ !Number.isSafeInteger(updateId) ||
2192
+ (updateId as number) < 0 ||
2193
+ (index > 0 &&
2194
+ (updateId as number) <=
2195
+ (sourceUpdateIds[index - 1] as number)),
2196
+ )
2197
+ ) {
2198
+ return undefined;
2199
+ }
2200
+ return {
2201
+ queueKind,
2202
+ receiptId: value.receiptId,
2203
+ sourceUpdateIds: sourceUpdateIds as number[],
2204
+ ...(typeof value.journalBindingKey === "string"
2205
+ ? { journalBindingKey: value.journalBindingKey }
2206
+ : {}),
2207
+ };
2208
+ }
2209
+
2210
+ function parseQueueHandoffPayload(
2211
+ value: unknown,
2212
+ ): TelegramQueueHandoffPayload | undefined {
2213
+ if (!isRecord(value) || (value.kind !== "prompt" && value.kind !== "control")) {
2214
+ return undefined;
2215
+ }
2216
+ const queueKind = value.kind;
2217
+ const target = parseTarget(value.target);
2218
+ const transportStamp = isRecord(value.transportStamp) &&
2219
+ typeof value.transportStamp.profile === "string" &&
2220
+ typeof value.transportStamp.generation === "string"
2221
+ ? {
2222
+ profile: value.transportStamp.profile,
2223
+ generation: value.transportStamp.generation,
2224
+ }
2225
+ : undefined;
2226
+ if (
2227
+ !Number.isSafeInteger(value.chatId) ||
2228
+ (value.target !== undefined && !target) ||
2229
+ (value.transportStamp !== undefined && !transportStamp) ||
2230
+ !Number.isSafeInteger(value.replyToMessageId) ||
2231
+ (value.guestQueryId !== undefined &&
2232
+ typeof value.guestQueryId !== "string") ||
2233
+ !Number.isSafeInteger(value.queueOrder) ||
2234
+ (value.queueLane !== "control" &&
2235
+ value.queueLane !== "priority" &&
2236
+ value.queueLane !== "default") ||
2237
+ !Number.isSafeInteger(value.laneOrder) ||
2238
+ typeof value.statusSummary !== "string" ||
2239
+ !Array.isArray(value.admissionReceipts)
2240
+ ) {
2241
+ return undefined;
2242
+ }
2243
+ const admissionReceipts = value.admissionReceipts.map((receipt) =>
2244
+ parseQueueAdmissionReceipt(receipt, queueKind),
2245
+ );
2246
+ if (
2247
+ admissionReceipts.length === 0 ||
2248
+ admissionReceipts.length > TELEGRAM_QUEUE_HANDOFF_MAX_RECEIPTS ||
2249
+ admissionReceipts.some((receipt) => receipt === undefined)
2250
+ ) {
2251
+ return undefined;
2252
+ }
2253
+ const queueLane = value.queueLane as "control" | "priority" | "default";
2254
+ const base = {
2255
+ chatId: value.chatId as number,
2256
+ ...(target ? { target } : {}),
2257
+ ...(transportStamp ? { transportStamp } : {}),
2258
+ replyToMessageId: value.replyToMessageId as number,
2259
+ ...(typeof value.guestQueryId === "string"
2260
+ ? { guestQueryId: value.guestQueryId }
2261
+ : {}),
2262
+ queueOrder: value.queueOrder as number,
2263
+ queueLane,
2264
+ laneOrder: value.laneOrder as number,
2265
+ statusSummary: value.statusSummary,
2266
+ admissionReceipts: admissionReceipts as NonNullable<
2267
+ (typeof admissionReceipts)[number]
2268
+ >[],
2269
+ };
2270
+ if (queueKind === "control") {
2271
+ if (
2272
+ value.queueLane !== "control" ||
2273
+ (value.controlType !== "status" && value.controlType !== "model")
2274
+ ) {
2275
+ return undefined;
2276
+ }
2277
+ return { kind: "control", controlType: value.controlType, ...base };
2278
+ }
2279
+ if (
2280
+ value.queueLane === "control" ||
2281
+ !Array.isArray(value.sourceMessageIds) ||
2282
+ value.sourceMessageIds.some((id) => !Number.isSafeInteger(id)) ||
2283
+ !Array.isArray(value.queuedAttachments) ||
2284
+ value.queuedAttachments.some(
2285
+ (attachment) =>
2286
+ !isRecord(attachment) ||
2287
+ typeof attachment.path !== "string" ||
2288
+ typeof attachment.fileName !== "string",
2289
+ ) ||
2290
+ !Array.isArray(value.content) ||
2291
+ value.content.some(
2292
+ (content) =>
2293
+ !isRecord(content) ||
2294
+ (content.type === "text"
2295
+ ? typeof content.text !== "string"
2296
+ : content.type === "image"
2297
+ ? typeof content.data !== "string" ||
2298
+ typeof content.mimeType !== "string"
2299
+ : true),
2300
+ ) ||
2301
+ typeof value.historyText !== "string" ||
2302
+ (value.priorityEmoji !== undefined &&
2303
+ typeof value.priorityEmoji !== "string") ||
2304
+ (value.reactionSuppressionEmoji !== undefined &&
2305
+ typeof value.reactionSuppressionEmoji !== "string") ||
2306
+ (value.voiceReplyPreferred !== undefined &&
2307
+ typeof value.voiceReplyPreferred !== "boolean") ||
2308
+ (value.voiceReplyRequired !== undefined &&
2309
+ typeof value.voiceReplyRequired !== "boolean")
2310
+ ) {
2311
+ return undefined;
2312
+ }
2313
+ return {
2314
+ kind: "prompt",
2315
+ ...base,
2316
+ sourceMessageIds: value.sourceMessageIds as number[],
2317
+ queuedAttachments: value.queuedAttachments as Array<{
2318
+ path: string;
2319
+ fileName: string;
2320
+ }>,
2321
+ content: value.content as Array<
2322
+ | { type: "text"; text: string }
2323
+ | { type: "image"; data: string; mimeType: string }
2324
+ >,
2325
+ historyText: value.historyText,
2326
+ ...(typeof value.priorityEmoji === "string"
2327
+ ? { priorityEmoji: value.priorityEmoji }
2328
+ : {}),
2329
+ ...(typeof value.reactionSuppressionEmoji === "string"
2330
+ ? { reactionSuppressionEmoji: value.reactionSuppressionEmoji }
2331
+ : {}),
2332
+ ...(typeof value.voiceReplyPreferred === "boolean"
2333
+ ? { voiceReplyPreferred: value.voiceReplyPreferred }
2334
+ : {}),
2335
+ ...(typeof value.voiceReplyRequired === "boolean"
2336
+ ? { voiceReplyRequired: value.voiceReplyRequired }
2337
+ : {}),
2338
+ };
2339
+ }
2340
+
2341
+ function parseQueueHandoffEnvelope(
2342
+ value: Record<string, unknown>,
2343
+ requestId: string,
2344
+ kind: "leader.offerQueueHandoff" | "follower.offerQueueHandoff",
2345
+ ): TelegramBusEnvelope | undefined {
2346
+ let serializedPayloadBytes: number;
2347
+ try {
2348
+ serializedPayloadBytes = Buffer.byteLength(JSON.stringify(value.payload));
2349
+ } catch {
2350
+ return undefined;
2351
+ }
2352
+ if (serializedPayloadBytes > TELEGRAM_QUEUE_HANDOFF_PAYLOAD_MAX_BYTES) {
2353
+ return undefined;
2354
+ }
2355
+ const payload = parseQueueHandoffPayload(value.payload);
2356
+ if (
2357
+ !payload ||
2358
+ typeof value.recipientInstanceId !== "string" ||
2359
+ typeof value.recipientRegistrationGeneration !== "string" ||
2360
+ !Number.isSafeInteger(value.donorProcessId) ||
2361
+ (value.donorProcessId as number) <= 0 ||
2362
+ typeof value.donorProcessBirthId !== "string" ||
2363
+ !value.donorProcessBirthId ||
2364
+ !Number.isSafeInteger(value.donorSessionGeneration) ||
2365
+ (value.donorSessionGeneration as number) <= 0 ||
2366
+ typeof value.donorAcquisitionId !== "string" ||
2367
+ !value.donorAcquisitionId ||
2368
+ !Number.isSafeInteger(value.donorAcquiredAtMs) ||
2369
+ (value.donorAcquiredAtMs as number) < 0 ||
2370
+ typeof value.handoffToken !== "string" ||
2371
+ value.handoffToken.length < 32 ||
2372
+ value.handoffToken.length > 256 ||
2373
+ value.donorProcessBirthId.length > 256 ||
2374
+ value.donorAcquisitionId.length > 256 ||
2375
+ typeof value.sentAtMs !== "number"
2376
+ ) {
2377
+ return undefined;
2378
+ }
2379
+ const fields = {
2380
+ requestId,
2381
+ recipientInstanceId: value.recipientInstanceId,
2382
+ recipientRegistrationGeneration: value.recipientRegistrationGeneration,
2383
+ donorProcessId: value.donorProcessId as number,
2384
+ donorProcessBirthId: value.donorProcessBirthId,
2385
+ donorSessionGeneration: value.donorSessionGeneration as number,
2386
+ donorAcquisitionId: value.donorAcquisitionId,
2387
+ donorAcquiredAtMs: value.donorAcquiredAtMs as number,
2388
+ handoffToken: value.handoffToken,
2389
+ payload,
2390
+ sentAtMs: value.sentAtMs,
2391
+ };
2392
+ if (kind === "leader.offerQueueHandoff") {
2393
+ return typeof value.donorInstanceId === "string"
2394
+ ? { kind, donorInstanceId: value.donorInstanceId, ...fields }
2395
+ : undefined;
2396
+ }
2397
+ return typeof value.instanceId === "string" &&
2398
+ typeof value.registrationGeneration === "string"
2399
+ ? {
2400
+ kind,
2401
+ instanceId: value.instanceId,
2402
+ registrationGeneration: value.registrationGeneration,
2403
+ ...fields,
2404
+ }
2405
+ : undefined;
2406
+ }
2407
+
1619
2408
  function parseResolveAgentTargetEnvelope(
1620
2409
  value: Record<string, unknown>,
1621
2410
  requestId: string,
@@ -1730,12 +2519,15 @@ function parseAckEnvelope(
1730
2519
  message: typeof value.message === "string" ? value.message : undefined,
1731
2520
  };
1732
2521
  if (Object.hasOwn(value, "result")) envelope.result = value.result;
2522
+ const protocol = parseTelegramBusProtocolIdentity(value.protocol);
2523
+ if (protocol) envelope.protocol = protocol;
1733
2524
  if (isRecord(value.error)) {
1734
2525
  const code = value.error.code;
1735
2526
  if (
1736
2527
  code === "commit-unknown" ||
1737
2528
  code === "request-id-collision" ||
1738
- code === "ledger-overloaded"
2529
+ code === "ledger-overloaded" ||
2530
+ code === "incompatible-protocol"
1739
2531
  ) {
1740
2532
  envelope.error = {
1741
2533
  code,
@@ -1748,6 +2540,44 @@ function parseAckEnvelope(
1748
2540
  return envelope;
1749
2541
  }
1750
2542
 
2543
+ function parseTelegramBusProtocolIdentity(
2544
+ value: unknown,
2545
+ ): TelegramBusProtocolIdentity | undefined {
2546
+ if (!isRecord(value)) return undefined;
2547
+ if (
2548
+ !Number.isSafeInteger(value.protocolVersion) ||
2549
+ (value.protocolVersion as number) <= 0 ||
2550
+ typeof value.runtimeBuild !== "string" ||
2551
+ !value.runtimeBuild.trim() ||
2552
+ value.runtimeBuild !== value.runtimeBuild.trim() ||
2553
+ value.runtimeBuild.length > 128 ||
2554
+ !Array.isArray(value.capabilities) ||
2555
+ value.capabilities.length > 32 ||
2556
+ value.capabilities.some(
2557
+ (capability) =>
2558
+ typeof capability !== "string" ||
2559
+ capability.length > 128 ||
2560
+ !/^[a-z0-9][a-z0-9._-]*$/u.test(capability),
2561
+ )
2562
+ ) {
2563
+ return undefined;
2564
+ }
2565
+ const capabilities = value.capabilities as string[];
2566
+ if (
2567
+ capabilities.some(
2568
+ (capability, index) =>
2569
+ index > 0 && capability <= capabilities[index - 1]!,
2570
+ )
2571
+ ) {
2572
+ return undefined;
2573
+ }
2574
+ return {
2575
+ protocolVersion: value.protocolVersion as number,
2576
+ runtimeBuild: value.runtimeBuild,
2577
+ capabilities: [...capabilities],
2578
+ };
2579
+ }
2580
+
1751
2581
  function parseRegistration(
1752
2582
  value: unknown,
1753
2583
  ): TelegramBusInstanceRegistration | undefined {
@@ -1778,6 +2608,17 @@ function parseRegistration(
1778
2608
  if (typeof value.registrationGeneration === "string") {
1779
2609
  registration.registrationGeneration = value.registrationGeneration;
1780
2610
  }
2611
+ if (
2612
+ Number.isSafeInteger(value.sessionGeneration) &&
2613
+ (value.sessionGeneration as number) > 0
2614
+ ) {
2615
+ registration.sessionGeneration = value.sessionGeneration as number;
2616
+ }
2617
+ if (typeof value.processBirthId === "string" && value.processBirthId) {
2618
+ registration.processBirthId = value.processBirthId;
2619
+ }
2620
+ const protocol = parseTelegramBusProtocolIdentity(value.protocol);
2621
+ if (protocol) registration.protocol = protocol;
1781
2622
  if (target) registration.target = target;
1782
2623
  return registration;
1783
2624
  }