@llblab/pi-telegram 0.52.2 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -6,64 +6,102 @@
6
6
  * It must not spawn Pi processes or create hidden Telegram-originated instances.
7
7
  */
8
8
 
9
+ import { createHash } from "node:crypto";
9
10
  import { basename } from "node:path";
10
11
  import { isDeepStrictEqual } from "node:util";
11
- import type { TelegramWorkspaceRestoreExecutor, TelegramWorkspaceRestoreIntent } from "./threads.ts";
12
12
 
13
- import * as Threads from "./threads.ts";
14
- import * as WorkspaceIdentity from "./workspace-identity.ts";
15
- import { isWireRecord as isRecord } from "./wire.ts";
16
- import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
17
- import {
18
- TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
19
- type TelegramLockEntry,
20
- type TelegramLockState,
21
- } from "./locks.ts";
13
+ import type { TelegramPreparedHeldCommand } from "./commands.ts";
22
14
  import type {
23
- TelegramQueueHandoffPayload,
24
- TelegramQueueHandoffStageResult,
25
- } from "./queue.ts";
26
- import type { TelegramTarget } from "./target.ts";
15
+ TelegramWorkspaceRestoreExecutor,
16
+ TelegramWorkspaceRestoreIntent,
17
+ } from "./threads.ts";
18
+ import type {
19
+ TelegramLiveInputPreparation,
20
+ TelegramLiveSourceCompletionReadiness,
21
+ TelegramUpdateAdmissionLifecycleRuntime,
22
+ } from "./updates.ts";
23
+
27
24
  import {
28
- isTelegramApiMethodRetrySafe,
29
- TelegramApiCommitUnknownError,
30
- TelegramApiStaleTargetError,
31
- } from "./telegram-api.ts";
25
+ getTelegramBusTransportRetryPolicy,
26
+ TELEGRAM_BUS_REGISTRATION_RETRY,
27
+ } from "./bus-transport.ts";
32
28
  import {
33
29
  createTelegramBusFollowerDeliveryIdentity,
34
30
  createTelegramBusForeignOwnedUpdateForwarder,
35
31
  createTelegramBusLocalServer,
36
32
  createTelegramBusRequestIdFactory,
37
33
  createUnauthorizedBusAck,
34
+ rejectTelegramBusRequest,
38
35
  getTelegramBusProtocolCompatibility,
39
36
  getTelegramBusSocketPath,
40
37
  hasTelegramBusCapability,
41
38
  isTelegramBusEnvelopeAuthorized,
39
+ parseTelegramBusEnvelope,
42
40
  resolveTelegramBusSocketPath,
43
41
  sendTelegramBusLocalEnvelope,
42
+ TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
43
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
44
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
45
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
46
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
47
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
48
+ TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
49
+ TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
50
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
51
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
52
+ TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
53
+ TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
54
+ TelegramBusLocalAuthorityError,
44
55
  type TelegramBusAgentMessage,
45
56
  type TelegramBusAgentTargetSelector,
46
57
  type TelegramBusEnvelope,
47
- type TelegramBusWorkspaceRestoreObservation,
58
+ type TelegramBusFollowerQueueHandoffOffer,
48
59
  type TelegramBusForwardOwnership,
60
+ type TelegramBusLiveRebindApplyObservation,
61
+ type TelegramBusLiveRebindCommandObservation,
62
+ type TelegramBusLiveRebindSaveObservation,
63
+ type TelegramBusLiveRebindSettlementObservation,
64
+ type TelegramBusLiveRebindWorkObservation,
65
+ type TelegramBusLiveRebindWorkState,
49
66
  type TelegramBusProtocolIdentity,
67
+ type TelegramBusSelectedMenuDeliveryObservation,
68
+ type TelegramBusSelectedMenuTextEffect,
50
69
  type TelegramBusSocketPathSource,
51
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
52
- TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
53
- TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
54
- TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
55
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
70
+ type TelegramBusWorkspaceRestoreObservation,
56
71
  } from "./bus.ts";
57
- import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
72
+ import type {
73
+ TelegramConfigStore,
74
+ TelegramThreadDisplayMode,
75
+ } from "./config.ts";
58
76
  import {
59
- getTelegramBusTransportRetryPolicy,
60
- TELEGRAM_BUS_REGISTRATION_RETRY,
61
- } from "./bus-transport.ts";
77
+ parseTelegramUpdateJournalQueueOwner,
78
+ type TelegramUpdateJournalStoreOptions,
79
+ } from "./journal.ts";
80
+ import {
81
+ TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
82
+ type TelegramLockEntry,
83
+ type TelegramLockState,
84
+ } from "./locks.ts";
85
+ import { isPiStaleContextError } from "./pi.ts";
86
+ import type { TelegramQueueHandoffStageResult } from "./queue.ts";
87
+ import {
88
+ parseTelegramTarget as parseTarget,
89
+ type TelegramTarget,
90
+ } from "./target.ts";
91
+ import {
92
+ isTelegramApiMethodRetrySafe,
93
+ TelegramApiAuthorityError,
94
+ TelegramApiCommitUnknownError,
95
+ TelegramApiStaleTargetError,
96
+ } from "./telegram-api.ts";
97
+ import * as Threads from "./threads.ts";
98
+ import { isWireRecord as isRecord } from "./wire.ts";
62
99
  import {
63
100
  createTelegramWorkspaceAdmissionOperationId,
64
101
  runWithTelegramWorkspaceAdmissionsAsync,
65
102
  type TelegramWorkspaceAdmissionLedger,
66
103
  } from "./workspace-admission.ts";
104
+ import * as WorkspaceIdentity from "./workspace-identity.ts";
67
105
 
68
106
  const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
69
107
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
@@ -271,7 +309,9 @@ export interface TelegramBusFollowerClientRuntimeDeps<TMessage = unknown> {
271
309
  getForwardCommentBatchPosition?: (
272
310
  message: TMessage,
273
311
  ) => "comment" | "forward" | undefined;
274
- validateForwardOwnership?: (ownership: TelegramBusForwardOwnership) => boolean;
312
+ validateForwardOwnership?: (
313
+ ownership: TelegramBusForwardOwnership,
314
+ ) => boolean;
275
315
  recordRuntimeEvent?: (
276
316
  category: string,
277
317
  error: unknown,
@@ -354,10 +394,12 @@ export type TelegramBusFollowerPromotionHandler<TContext> = (
354
394
  ) => Promise<boolean>;
355
395
 
356
396
  type TelegramBusFollowerWorkspaceAdmissionDeps = {
357
- getWorkspaceAdmission?: () => Pick<
358
- TelegramWorkspaceAdmissionLedger,
359
- "acquireAdmission" | "releaseAdmission"
360
- > | undefined;
397
+ getWorkspaceAdmission?: () =>
398
+ | Pick<
399
+ TelegramWorkspaceAdmissionLedger,
400
+ "acquireAdmission" | "releaseAdmission"
401
+ >
402
+ | undefined;
361
403
  recordRuntimeEvent?: (
362
404
  category: string,
363
405
  error: unknown,
@@ -409,83 +451,84 @@ export function createTelegramBusFollowerPromotionHandler<
409
451
  ) => void;
410
452
  getNowMs?: () => number;
411
453
  getPid?: () => number;
412
- getWorkspaceAdmission?:
413
- TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
454
+ getWorkspaceAdmission?: TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
414
455
  }): TelegramBusFollowerPromotionHandler<TContext> {
415
- return (ctx, binding, election) => runTelegramBusFollowerWorkspaceMutation(
416
- input,
417
- "workspace.promote-follower",
418
- async () => {
419
- let promotedRecord: Threads.TelegramTopicTargetRecord | undefined;
420
- const promoted = await input.startLeader(ctx, election, async () => {
421
- promotedRecord =
422
- await Threads.promoteTelegramFollowerBindingToLeader({
423
- store: input.topicTargetStore,
424
- instanceId: input.instanceId,
456
+ return (ctx, binding, election) =>
457
+ runTelegramBusFollowerWorkspaceMutation(
458
+ input,
459
+ "workspace.promote-follower",
460
+ async () => {
461
+ let promotedRecord: Threads.TelegramTopicTargetRecord | undefined;
462
+ const promoted = await input.startLeader(ctx, election, async () => {
463
+ promotedRecord = await Threads.promoteTelegramFollowerBindingToLeader(
464
+ {
465
+ store: input.topicTargetStore,
466
+ instanceId: input.instanceId,
467
+ cwd: ctx.cwd,
468
+ sessionId: input.getSessionId?.(ctx),
469
+ telegramProfile: input.getActiveProfileName(),
470
+ target: binding.target,
471
+ slot: binding.slot,
472
+ threadName: binding.threadName,
473
+ },
474
+ );
475
+ if (!promotedRecord && typeof binding.target?.threadId === "number") {
476
+ throw new Error(
477
+ "Telegram follower promotion slot authority is unavailable.",
478
+ );
479
+ }
480
+ if (promotedRecord) {
481
+ input.recordRuntimeEvent(
482
+ "bus",
483
+ "Follower thread binding promoted to leader",
484
+ {
485
+ phase: "follower-promoted-binding",
486
+ chatId: promotedRecord.target.chatId,
487
+ threadId: promotedRecord.target.threadId,
488
+ slot: promotedRecord.slot,
489
+ threadName: promotedRecord.threadName,
490
+ },
491
+ );
492
+ }
493
+ });
494
+ if (
495
+ promoted &&
496
+ promotedRecord &&
497
+ typeof binding.target?.threadId === "number"
498
+ ) {
499
+ const profileKey = Threads.getTelegramThreadOwnerKey({
500
+ kind: "leader",
425
501
  cwd: ctx.cwd,
426
- sessionId: input.getSessionId?.(ctx),
502
+ instanceId: input.instanceId,
427
503
  telegramProfile: input.getActiveProfileName(),
428
- target: binding.target,
429
- slot: binding.slot,
430
- threadName: binding.threadName,
431
504
  });
432
- if (!promotedRecord && typeof binding.target?.threadId === "number") {
433
- throw new Error(
434
- "Telegram follower promotion slot authority is unavailable.",
435
- );
436
- }
437
- if (promotedRecord) {
505
+ Threads.setTelegramLeaderSessionHandoff({
506
+ pid: input.getPid?.() ?? process.pid,
507
+ instanceId: input.instanceId,
508
+ createdAtMs: input.getNowMs?.() ?? Date.now(),
509
+ profileKey,
510
+ target: {
511
+ chatId: binding.target.chatId,
512
+ threadId: binding.target.threadId,
513
+ },
514
+ slot: promotedRecord.slot,
515
+ threadName: promotedRecord.threadName,
516
+ });
438
517
  input.recordRuntimeEvent(
439
518
  "bus",
440
- "Follower thread binding promoted to leader",
519
+ "Promoted leader binding retained for session replacement",
441
520
  {
442
- phase: "follower-promoted-binding",
443
- chatId: promotedRecord.target.chatId,
444
- threadId: promotedRecord.target.threadId,
521
+ phase: "follower-promoted-session-handoff",
522
+ chatId: binding.target.chatId,
523
+ threadId: binding.target.threadId,
445
524
  slot: promotedRecord.slot,
446
525
  threadName: promotedRecord.threadName,
447
526
  },
448
527
  );
449
528
  }
450
- });
451
- if (
452
- promoted &&
453
- promotedRecord &&
454
- typeof binding.target?.threadId === "number"
455
- ) {
456
- const profileKey = Threads.getTelegramThreadOwnerKey({
457
- kind: "leader",
458
- cwd: ctx.cwd,
459
- instanceId: input.instanceId,
460
- telegramProfile: input.getActiveProfileName(),
461
- });
462
- Threads.setTelegramLeaderSessionHandoff({
463
- pid: input.getPid?.() ?? process.pid,
464
- instanceId: input.instanceId,
465
- createdAtMs: input.getNowMs?.() ?? Date.now(),
466
- profileKey,
467
- target: {
468
- chatId: binding.target.chatId,
469
- threadId: binding.target.threadId,
470
- },
471
- slot: promotedRecord.slot,
472
- threadName: promotedRecord.threadName,
473
- });
474
- input.recordRuntimeEvent(
475
- "bus",
476
- "Promoted leader binding retained for session replacement",
477
- {
478
- phase: "follower-promoted-session-handoff",
479
- chatId: binding.target.chatId,
480
- threadId: binding.target.threadId,
481
- slot: promotedRecord.slot,
482
- threadName: promotedRecord.threadName,
483
- },
484
- );
485
- }
486
- return promoted;
487
- },
488
- );
529
+ return promoted;
530
+ },
531
+ );
489
532
  }
490
533
 
491
534
  export type TelegramBusFollowerLeaderState =
@@ -538,13 +581,18 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
538
581
  getAuthSecret?: () => string | undefined;
539
582
  getRegistrationGeneration: () => string | undefined;
540
583
  getRecipientBindingKey: () => string | undefined;
584
+ /** Live preparation names the current journal, not the stable forwarding/profile identity. */
585
+ getLiveRebindJournalBindingKey?: () => string | undefined;
541
586
  durableAdmission: TelegramBusFollowerDurableAdmissionPort<TContext>;
542
587
  sourceReferenceAdmission?: TelegramBusFollowerDurableAdmissionPort<TContext>;
543
588
  isSourceReferenceAdmissionEnabled?: () => boolean;
544
589
  hasAuthenticatedSourceReferenceTransport?: () => boolean;
545
590
  getContext: () => TContext | undefined;
546
591
  handleInputCustodyHandoff?: (
547
- envelope: Extract<TelegramBusEnvelope, { kind: "leader.offerInputCustodyHandoff" }>,
592
+ envelope: Extract<
593
+ TelegramBusEnvelope,
594
+ { kind: "leader.offerInputCustodyHandoff" }
595
+ >,
548
596
  ctx: TContext,
549
597
  ) => Promise<unknown> | unknown;
550
598
  handleQueueHandoff?: (
@@ -553,10 +601,49 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
553
601
  { kind: "leader.offerQueueHandoff" }
554
602
  >,
555
603
  ctx: TContext,
556
- ) => Promise<TelegramQueueHandoffStageResult> | TelegramQueueHandoffStageResult;
604
+ ) =>
605
+ Promise<TelegramQueueHandoffStageResult> | TelegramQueueHandoffStageResult;
606
+ getSessionId?: () => string | undefined;
607
+ getLeaderProtocol?: () => TelegramBusProtocolIdentity | undefined;
608
+ getLocalProtocol?: () => TelegramBusProtocolIdentity | undefined;
609
+ isLiveRebindSaveEnabled?: () => boolean;
610
+ isLiveRebindApplyEnabled?: () => boolean;
611
+ isLiveRebindSettleEnabled?: () => boolean;
612
+ handleLiveRebindSettle?: (
613
+ envelope: Extract<TelegramBusEnvelope, { kind: "leader.settleLiveRebind" }>,
614
+ ctx: TContext,
615
+ isCurrent: () => boolean,
616
+ ) => Promise<
617
+ | TelegramBusLiveRebindSettlementObservation
618
+ | TelegramBusLiveRebindWorkObservation
619
+ | TelegramBusLiveRebindCommandObservation
620
+ >;
621
+ handleLiveRebindApply?: (
622
+ envelope: Extract<TelegramBusEnvelope, { kind: "leader.applyLiveRebind" }>,
623
+ ctx: TContext,
624
+ isCurrent: () => boolean,
625
+ ) => Promise<TelegramBusLiveRebindApplyObservation>;
626
+ /** Availability only; never future recipient activation or a command execution grant. */
627
+ isLiveRebindCommandSetEnabled?: () => boolean;
628
+ handleLiveRebindSave?: (
629
+ envelope: Extract<
630
+ TelegramBusEnvelope,
631
+ { kind: "leader.prepareLiveRebind" }
632
+ >,
633
+ ctx: TContext,
634
+ isCurrent: () => boolean,
635
+ ) =>
636
+ | TelegramBusLiveRebindSaveObservation
637
+ | Promise<TelegramBusLiveRebindSaveObservation>;
557
638
  isWorkspaceRestoreEnabled?: () => boolean;
558
- handleWorkspaceRestore?: (input: { operationId: string; registrationGeneration: string; mode: "apply" | "inspect" },
559
- ctx: TContext) => Promise<TelegramBusWorkspaceRestoreObservation>;
639
+ handleWorkspaceRestore?: (
640
+ input: {
641
+ operationId: string;
642
+ registrationGeneration: string;
643
+ mode: "apply" | "inspect";
644
+ },
645
+ ctx: TContext,
646
+ ) => Promise<TelegramBusWorkspaceRestoreObservation>;
560
647
  recordRuntimeEvent?: (
561
648
  category: string,
562
649
  error: unknown,
@@ -582,9 +669,7 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
582
669
  ) => void;
583
670
  receiver: Omit<
584
671
  TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
585
- | "instanceId"
586
- | "recordRuntimeEvent"
587
- | "getRegistrationGeneration"
672
+ "instanceId" | "recordRuntimeEvent" | "getRegistrationGeneration"
588
673
  >;
589
674
  recovery: Omit<
590
675
  TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
@@ -610,25 +695,47 @@ export function createTelegramBusFollowerRuntimeAssembly<
610
695
  ports: TelegramBusFollowerRuntimeAssemblyPorts<TContext>,
611
696
  ): TelegramBusFollowerRuntimeAssembly<TContext> {
612
697
  // Binding startup needs registration identity before it can grant inbound authority.
613
- let readyContext: { generation: string; ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
614
- const isReadyContext = (ctx?: TContext): boolean => Boolean(readyContext &&
615
- (ctx === undefined || readyContext.ctx === ctx) &&
616
- ports.registrationState.getGeneration() === readyContext.generation &&
617
- ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
618
- ports.registration.getSessionId?.(readyContext.ctx) === readyContext.sessionId &&
619
- ports.registration.isContextActive?.(readyContext.ctx) !== false);
698
+ let readyContext:
699
+ | {
700
+ generation: string;
701
+ ctx: TContext;
702
+ sessionGeneration: number | undefined;
703
+ sessionId: string | undefined;
704
+ }
705
+ | undefined;
706
+ const isReadyContext = (ctx?: TContext): boolean =>
707
+ Boolean(
708
+ readyContext &&
709
+ (ctx === undefined || readyContext.ctx === ctx) &&
710
+ ports.registrationState.getGeneration() === readyContext.generation &&
711
+ ports.registration.getSessionGeneration?.() ===
712
+ readyContext.sessionGeneration &&
713
+ ports.registration.getSessionId?.(readyContext.ctx) ===
714
+ readyContext.sessionId &&
715
+ ports.registration.isContextActive?.(readyContext.ctx) !== false,
716
+ );
620
717
  const prepareContext = async (ctx: TContext): Promise<void> => {
621
718
  const generation = ports.registrationState.getGeneration();
622
719
  const sessionGeneration = ports.registration.getSessionGeneration?.();
623
720
  const sessionId = ports.registration.getSessionId?.(ctx);
624
721
  readyContext = undefined;
625
- if (!generation || (sessionId !== undefined && ports.registrationState.getSessionId(sessionId) !== sessionId))
626
- throw new Error("Telegram follower session change requires acknowledged leader registration.");
722
+ if (
723
+ !generation ||
724
+ (sessionId !== undefined &&
725
+ ports.registrationState.getSessionId(sessionId) !== sessionId)
726
+ )
727
+ throw new Error(
728
+ "Telegram follower session change requires acknowledged leader registration.",
729
+ );
627
730
  await ports.registration.onRegistered?.(ctx);
628
- if (generation && ports.registrationState.getGeneration() === generation &&
629
- ports.registration.getSessionGeneration?.() === sessionGeneration &&
630
- ports.registration.getSessionId?.(ctx) === sessionId &&
631
- ports.registration.isContextActive?.(ctx) !== false) readyContext = { generation, ctx, sessionGeneration, sessionId };
731
+ if (
732
+ generation &&
733
+ ports.registrationState.getGeneration() === generation &&
734
+ ports.registration.getSessionGeneration?.() === sessionGeneration &&
735
+ ports.registration.getSessionId?.(ctx) === sessionId &&
736
+ ports.registration.isContextActive?.(ctx) !== false
737
+ )
738
+ readyContext = { generation, ctx, sessionGeneration, sessionId };
632
739
  };
633
740
  const sharedRuntimeDeps = {
634
741
  instanceId: ports.instanceId,
@@ -665,12 +772,20 @@ export function createTelegramBusFollowerRuntimeAssembly<
665
772
  const sessionGeneration = ports.registration.getSessionGeneration?.();
666
773
  if (ports.registration.isContextActive?.(ctx) === false) return;
667
774
  await baseRegistration.setContext(ctx);
668
- if (ports.registration.getSessionGeneration?.() !== sessionGeneration ||
669
- ports.registration.isContextActive?.(ctx) === false) return;
775
+ if (
776
+ ports.registration.getSessionGeneration?.() !== sessionGeneration ||
777
+ ports.registration.isContextActive?.(ctx) === false
778
+ )
779
+ return;
670
780
  if (!isReadyContext(ctx)) await prepareContext(ctx);
671
781
  },
672
782
  };
673
- return { receiver, registration, getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined };
783
+ return {
784
+ receiver,
785
+ registration,
786
+ getReadySessionId: () =>
787
+ isReadyContext() ? readyContext?.sessionId : undefined,
788
+ };
674
789
  }
675
790
 
676
791
  export interface TelegramBusFollowerRestoreContext {
@@ -694,122 +809,622 @@ export function createTelegramBusFollowerRestoreContextGetter<TContext>(deps: {
694
809
  getLeaderState: () => TelegramLockState;
695
810
  getAuthenticatedSecret: () => string | undefined;
696
811
  getLeaderProtocol: () => TelegramBusProtocolIdentity | undefined;
812
+ capability?:
813
+ | typeof TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE
814
+ | typeof TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY
815
+ | typeof TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY;
697
816
  }) {
698
817
  return (ctx: TContext): TelegramBusFollowerRestoreContext | undefined => {
699
818
  if (!deps.isContextCurrent(ctx)) return undefined;
700
819
  const leader = deps.getLeaderState();
701
820
  const secret = deps.getAuthenticatedSecret();
702
821
  const leaderProtocol = deps.getLeaderProtocol();
703
- if (!leaderProtocol || !hasTelegramBusCapability(leaderProtocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE)) return undefined;
704
- if (leader.kind !== "active-elsewhere" || !secret || leader.lock.busSecret !== secret ||
705
- !leader.lock.instanceId || leader.lock.leaderEpoch === undefined) return undefined;
822
+ if (
823
+ !leaderProtocol ||
824
+ !hasTelegramBusCapability(
825
+ leaderProtocol,
826
+ deps.capability ?? TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
827
+ )
828
+ )
829
+ return undefined;
830
+ if (
831
+ leader.kind !== "active-elsewhere" ||
832
+ !secret ||
833
+ leader.lock.busSecret !== secret ||
834
+ !leader.lock.instanceId ||
835
+ leader.lock.leaderEpoch === undefined
836
+ )
837
+ return undefined;
706
838
  const profileBindingKey = deps.getProfileBindingKey();
707
839
  const operatorUserId = deps.getOperatorUserId();
708
840
  const sessionId = deps.getSessionId(ctx);
709
841
  const cwd = deps.getCwd(ctx);
710
842
  const generation = deps.getGeneration();
711
- if (!profileBindingKey || !sessionId || !cwd || typeof operatorUserId !== "number" ||
712
- !Number.isSafeInteger(operatorUserId) || operatorUserId <= 0 ||
713
- !Number.isSafeInteger(generation) || generation < 0 || !deps.isContextCurrent(ctx)) return undefined;
714
- return { executor: { instanceId: leader.lock.instanceId, leaderEpoch: String(leader.lock.leaderEpoch) },
715
- profileBindingKey, operatorUserId, sessionId, cwd, generation, leaderProtocol: structuredClone(leaderProtocol) };
843
+ if (
844
+ !profileBindingKey ||
845
+ !sessionId ||
846
+ !cwd ||
847
+ typeof operatorUserId !== "number" ||
848
+ !Number.isSafeInteger(operatorUserId) ||
849
+ operatorUserId <= 0 ||
850
+ !Number.isSafeInteger(generation) ||
851
+ generation < 0 ||
852
+ !deps.isContextCurrent(ctx)
853
+ )
854
+ return undefined;
855
+ return {
856
+ executor: {
857
+ instanceId: leader.lock.instanceId,
858
+ leaderEpoch: String(leader.lock.leaderEpoch),
859
+ },
860
+ profileBindingKey,
861
+ operatorUserId,
862
+ sessionId,
863
+ cwd,
864
+ generation,
865
+ leaderProtocol: structuredClone(leaderProtocol),
866
+ };
716
867
  };
717
868
  }
718
869
 
719
- /** Prepared receiver: its caller authenticates the envelope; no canonical writes, dispatch or cleanup. */
720
- export function createTelegramBusFollowerWorkspaceRestoreHandler<TContext>(deps: {
870
+ /** Shared canonical/local target owner and scoped live-carrier hooks; caller authenticates transport, no canonical writes or cleanup. */
871
+ export function createTelegramBusFollowerWorkspaceRestoreHandler<
872
+ TContext,
873
+ >(deps: {
721
874
  instanceId: string;
722
875
  /** Captures actual Pi lifetime and authenticated leader/profile authority, never request-derived values. */
723
- getContextAuthority: (ctx: TContext) => TelegramBusFollowerRestoreContext | undefined;
724
- readRestoreIntent: (operationId: string, profileBindingKey: string) => TelegramWorkspaceRestoreIntent | undefined;
725
- topicTargetStore: Pick<Threads.TelegramTopicTargetStore, "load" | "withWorkspaceRestoreSnapshot">;
726
- registrationState: Pick<TelegramBusFollowerRegistrationState, "getTarget" | "getSlot" | "getGeneration" | "setRegistered" | "getLeaderProtocol">;
727
- getWorkspaceAdmission: NonNullable<TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"]>;
876
+ getContextAuthority: (
877
+ ctx: TContext,
878
+ ) => TelegramBusFollowerRestoreContext | undefined;
879
+ readRestoreIntent: (
880
+ operationId: string,
881
+ profileBindingKey: string,
882
+ ) => TelegramWorkspaceRestoreIntent | undefined;
883
+ readLiveRebindIntent?: (
884
+ operationId: string,
885
+ profileBindingKey: string,
886
+ ) => Threads.TelegramWorkspaceLiveRebindIntent | undefined;
887
+ topicTargetStore: Pick<
888
+ Threads.TelegramTopicTargetStore,
889
+ "load" | "withWorkspaceRestoreSnapshot"
890
+ > &
891
+ Partial<
892
+ Pick<Threads.TelegramTopicTargetStore, "withWorkspaceLiveRebindSnapshot">
893
+ >;
894
+ registrationState: Pick<
895
+ TelegramBusFollowerRegistrationState,
896
+ | "getTarget"
897
+ | "getSlot"
898
+ | "getGeneration"
899
+ | "setRegistered"
900
+ | "getLeaderProtocol"
901
+ >;
902
+ getWorkspaceAdmission: NonNullable<
903
+ TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"]
904
+ >;
728
905
  recordRuntimeEvent?: TelegramBusFollowerWorkspaceAdmissionDeps["recordRuntimeEvent"];
729
906
  }) {
730
- return async (input: { operationId: string; registrationGeneration: string; mode: "apply" | "inspect" }, ctx: TContext) => {
907
+ const handler = async (
908
+ input: {
909
+ operationId: string;
910
+ registrationGeneration: string;
911
+ mode: "apply" | "inspect";
912
+ liveRebind?: {
913
+ sourceUpdateIds: readonly number[];
914
+ isCurrent(): boolean;
915
+ /** Captured prepared-branch target; equality must precede local apply or release grants. */
916
+ expectedTarget?: TelegramTarget & { threadId: number };
917
+ release?: (canRelease: () => boolean) => void;
918
+ observeWork?: (
919
+ oldTarget: TelegramTarget & { threadId: number },
920
+ ) => void;
921
+ };
922
+ },
923
+ ctx: TContext,
924
+ ) => {
925
+ const scope = input.liveRebind,
926
+ ports = scope && {
927
+ isCurrent: scope.isCurrent,
928
+ release: scope.release,
929
+ observeWork: scope.observeWork,
930
+ };
931
+ const suppliedTarget = scope?.expectedTarget,
932
+ expectedTarget = suppliedTarget && { ...suppliedTarget };
933
+ const sourceUpdateIds =
934
+ scope && [...scope.sourceUpdateIds].sort((a, b) => a - b);
935
+ const liveCurrent = scope && ports!.isCurrent.bind(scope),
936
+ release = scope && ports!.release?.bind(scope),
937
+ workObserver = scope && ports!.observeWork?.bind(scope);
731
938
  const captured = deps.getContextAuthority(ctx);
732
939
  if (!captured) throw new Error("Stale Telegram follower Restore context.");
733
940
  const authority = structuredClone(captured);
734
- const request = structuredClone(input);
735
- if (!authority.profileBindingKey || !authority.sessionId || !authority.cwd ||
736
- !authority.executor?.instanceId || !authority.executor.leaderEpoch ||
737
- !(typeof authority.generation === "string" ? authority.generation.length > 0 :
738
- Number.isSafeInteger(authority.generation) && authority.generation >= 0) ||
739
- !Number.isSafeInteger(authority.operatorUserId) || authority.operatorUserId <= 0 ||
740
- !request.operationId || !request.registrationGeneration || !["apply", "inspect"].includes(request.mode))
941
+ const request = structuredClone({
942
+ operationId: input.operationId,
943
+ registrationGeneration: input.registrationGeneration,
944
+ mode: input.mode,
945
+ });
946
+ const readIntent = () =>
947
+ liveCurrent
948
+ ? deps.readLiveRebindIntent?.(
949
+ request.operationId,
950
+ authority.profileBindingKey,
951
+ )
952
+ : deps.readRestoreIntent(
953
+ request.operationId,
954
+ authority.profileBindingKey,
955
+ );
956
+ let sampleWork: (() => void) | undefined;
957
+ if (suppliedTarget !== undefined && !expectedTarget)
958
+ throw new Error("Invalid prepared live-rebind target.");
959
+ if (
960
+ expectedTarget &&
961
+ (!Number.isSafeInteger(expectedTarget.chatId) ||
962
+ expectedTarget.chatId !== authority.operatorUserId ||
963
+ !Number.isSafeInteger(expectedTarget.threadId) ||
964
+ expectedTarget.threadId <= 0)
965
+ )
966
+ throw new Error("Invalid prepared live-rebind target.");
967
+ if (
968
+ (release || workObserver) &&
969
+ !hasTelegramBusCapability(
970
+ authority.leaderProtocol,
971
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
972
+ )
973
+ )
974
+ throw new Error("Live-rebind settlement capability is unavailable.");
975
+ if (
976
+ liveCurrent &&
977
+ !hasTelegramBusCapability(
978
+ authority.leaderProtocol,
979
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
980
+ )
981
+ )
982
+ throw new Error("Live-rebind apply capability is unavailable.");
983
+ if (
984
+ !authority.profileBindingKey ||
985
+ !authority.sessionId ||
986
+ !authority.cwd ||
987
+ !authority.executor?.instanceId ||
988
+ !authority.executor.leaderEpoch ||
989
+ !(typeof authority.generation === "string"
990
+ ? authority.generation.length > 0
991
+ : Number.isSafeInteger(authority.generation) &&
992
+ authority.generation >= 0) ||
993
+ !Number.isSafeInteger(authority.operatorUserId) ||
994
+ authority.operatorUserId <= 0 ||
995
+ !request.operationId ||
996
+ !request.registrationGeneration ||
997
+ !["apply", "inspect"].includes(request.mode)
998
+ )
741
999
  throw new Error("Invalid Telegram follower Restore authority.");
742
1000
  const assertCurrent = (): void => {
743
- if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
744
- deps.registrationState.getGeneration() !== request.registrationGeneration)
1001
+ if (
1002
+ !isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
1003
+ liveCurrent?.() === false ||
1004
+ (scope &&
1005
+ (input.liveRebind !== scope ||
1006
+ Object.entries(ports!).some(
1007
+ ([key, value]) => Reflect.get(scope, key) !== value,
1008
+ ))) ||
1009
+ deps.registrationState.getGeneration() !==
1010
+ request.registrationGeneration
1011
+ )
745
1012
  throw new Error("Stale Telegram follower Restore authority.");
746
1013
  };
747
1014
  assertCurrent();
748
- if (!deps.getWorkspaceAdmission) throw new Error("Telegram Workspace admission authority is unavailable.");
749
- const result = await runTelegramBusFollowerWorkspaceMutation(deps, "workspace.restore-follower-target", async () => {
750
- assertCurrent();
751
- const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
752
- assertCurrent();
753
- if (!intent || intent.request.operationId !== request.operationId ||
754
- (intent.phase !== "recipient-issued" && intent.phase !== "ready") ||
1015
+ if (!deps.getWorkspaceAdmission)
1016
+ throw new Error("Telegram Workspace admission authority is unavailable.");
1017
+ const result = await runTelegramBusFollowerWorkspaceMutation(
1018
+ deps,
1019
+ "workspace.restore-follower-target",
1020
+ async () => {
1021
+ assertCurrent();
1022
+ const intent = readIntent();
1023
+ assertCurrent();
1024
+ if (
1025
+ !intent ||
1026
+ intent.request.operationId !== request.operationId ||
1027
+ (liveCurrent
1028
+ ? !("kind" in intent) ||
1029
+ intent.kind !== "live-rebind" ||
1030
+ !isDeepStrictEqual(
1031
+ [...intent.request.source.updateIds].sort((a, b) => a - b),
1032
+ sourceUpdateIds,
1033
+ ) ||
1034
+ (request.mode === "apply"
1035
+ ? intent.phase !== "rebound"
1036
+ : !["rebound", "released", "finished"].includes(intent.phase))
1037
+ : "kind" in intent ||
1038
+ (intent.phase !== "recipient-issued" &&
1039
+ intent.phase !== "ready")) ||
1040
+ (expectedTarget &&
1041
+ !isDeepStrictEqual(intent.request.target, expectedTarget)) ||
755
1042
  !isDeepStrictEqual(intent.executor, authority.executor) ||
756
- intent.operatorUserId !== authority.operatorUserId || !intent.recipient ||
757
- (request.mode === "apply" && (intent.recipient.kind !== "follower" || intent.recipient.instanceId !== deps.instanceId ||
758
- intent.recipient.generation !== request.registrationGeneration)) ||
759
- intent.recipient.sessionId !== authority.sessionId || intent.request.binding.sessionId !== authority.sessionId ||
760
- intent.request.binding.cwd !== WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
761
- throw new Error("Telegram follower Restore intent does not match this recipient.");
762
- const expected = structuredClone(intent);
763
- await deps.topicTargetStore.load();
764
- assertCurrent();
765
- const observe = (canonical: Readonly<Pick<Threads.TelegramTopicTargetFile, "threads" | "workspaceBindings">>): TelegramBusWorkspaceRestoreObservation => {
766
- if (!isDeepStrictEqual(deps.readRestoreIntent(request.operationId, authority.profileBindingKey), expected))
767
- throw new Error("Telegram follower Restore intent changed.");
1043
+ intent.operatorUserId !== authority.operatorUserId ||
1044
+ !intent.recipient ||
1045
+ ((request.mode === "apply" || liveCurrent) &&
1046
+ (intent.recipient.kind !== "follower" ||
1047
+ intent.recipient.instanceId !== deps.instanceId ||
1048
+ intent.recipient.generation !==
1049
+ request.registrationGeneration)) ||
1050
+ intent.recipient.sessionId !== authority.sessionId ||
1051
+ intent.request.binding.sessionId !== authority.sessionId ||
1052
+ intent.request.binding.cwd !==
1053
+ WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd)
1054
+ )
1055
+ throw new Error(
1056
+ "Telegram follower Restore intent does not match this recipient.",
1057
+ );
1058
+ const expected = structuredClone(intent);
1059
+ await deps.topicTargetStore.load();
768
1060
  assertCurrent();
769
- const { binding, target } = expected.request;
770
- const bindings = (canonical.workspaceBindings ?? []).filter(value => value.bindingKey === binding.bindingKey);
771
- const snapshot = canonical.threads;
772
- const records = snapshot.filter(value => value.instanceId === deps.instanceId);
773
- const owners = snapshot.filter(value => value.status === "active" &&
774
- (value.slot === binding.slot || isDeepStrictEqual(value.target, target)));
775
- if (bindings.length !== 1 || records.length !== 1 || owners.length !== 1 || bindings[0]?.cwd !== binding.cwd ||
776
- bindings[0]?.sessionId !== binding.sessionId || bindings[0]?.slot !== binding.slot || bindings[0]?.inactiveSinceMs !== undefined ||
777
- !isDeepStrictEqual(bindings[0]?.target, target) || records[0]?.status !== "active" ||
1061
+ const observe = (
1062
+ canonical: Readonly<
1063
+ Pick<
1064
+ Threads.TelegramTopicTargetFile,
1065
+ "threads" | "workspaceBindings"
1066
+ >
1067
+ >,
1068
+ ): TelegramBusWorkspaceRestoreObservation => {
1069
+ if (!isDeepStrictEqual(readIntent(), expected))
1070
+ throw new Error("Telegram follower Restore intent changed.");
1071
+ assertCurrent();
1072
+ const { binding, target } = expected.request;
1073
+ const bindings = (canonical.workspaceBindings ?? []).filter(
1074
+ (value) => value.bindingKey === binding.bindingKey,
1075
+ );
1076
+ const snapshot = canonical.threads;
1077
+ const records = snapshot.filter(
1078
+ (value) => value.instanceId === deps.instanceId,
1079
+ );
1080
+ const owners = snapshot.filter(
1081
+ (value) =>
1082
+ value.status === "active" &&
1083
+ (value.slot === binding.slot ||
1084
+ isDeepStrictEqual(value.target, target)),
1085
+ );
1086
+ if (
1087
+ bindings.length !== 1 ||
1088
+ records.length !== 1 ||
1089
+ owners.length !== 1 ||
1090
+ bindings[0]?.cwd !== binding.cwd ||
1091
+ bindings[0]?.sessionId !== binding.sessionId ||
1092
+ bindings[0]?.slot !== binding.slot ||
1093
+ bindings[0]?.inactiveSinceMs !== undefined ||
1094
+ !isDeepStrictEqual(bindings[0]?.target, target) ||
1095
+ records[0]?.status !== "active" ||
778
1096
  records[0]?.owner?.kind !== "manual-follower" ||
779
- records[0]?.slot !== binding.slot || !isDeepStrictEqual(records[0]?.target, target) ||
780
- deps.registrationState.getSlot() !== binding.slot)
781
- throw new Error("Telegram follower Restore canonical binding is not committed.");
782
- const localTarget = deps.registrationState.getTarget();
783
- const alreadyReady = isDeepStrictEqual(localTarget, target);
784
- if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
785
- throw new Error("Telegram follower Restore local target changed.");
786
- assertCurrent();
787
- if (!alreadyReady && request.mode === "apply") {
788
- if (expected.phase !== "recipient-issued") throw new Error("Telegram follower Restore readiness regressed.");
789
- deps.registrationState.setRegistered(true, target, { slot: binding.slot,
790
- threadName: records[0]?.threadName, generation: request.registrationGeneration,
791
- leaderProtocol: deps.registrationState.getLeaderProtocol() });
1097
+ records[0]?.slot !== binding.slot ||
1098
+ !isDeepStrictEqual(records[0]?.target, target) ||
1099
+ deps.registrationState.getSlot() !== binding.slot
1100
+ )
1101
+ throw new Error(
1102
+ "Telegram follower Restore canonical binding is not committed.",
1103
+ );
1104
+ const localTarget = deps.registrationState.getTarget();
1105
+ const alreadyReady = isDeepStrictEqual(localTarget, target);
1106
+ if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
1107
+ throw new Error("Telegram follower Restore local target changed.");
1108
+ assertCurrent();
1109
+ if (!alreadyReady && request.mode === "apply") {
1110
+ if (
1111
+ expected.phase !== (liveCurrent ? "rebound" : "recipient-issued")
1112
+ )
1113
+ throw new Error("Telegram follower Restore readiness regressed.");
1114
+ deps.registrationState.setRegistered(true, target, {
1115
+ slot: binding.slot,
1116
+ threadName: records[0]?.threadName,
1117
+ generation: request.registrationGeneration,
1118
+ leaderProtocol: deps.registrationState.getLeaderProtocol(),
1119
+ });
1120
+ }
1121
+ assertCurrent();
1122
+ const observedTarget = deps.registrationState.getTarget();
1123
+ if (
1124
+ !observedTarget ||
1125
+ (!isDeepStrictEqual(observedTarget, target) &&
1126
+ !isDeepStrictEqual(observedTarget, binding.target))
1127
+ )
1128
+ throw new Error("Telegram follower Restore local target changed.");
1129
+ const ready = isDeepStrictEqual(observedTarget, target);
1130
+ assertCurrent();
1131
+ return {
1132
+ operationId: request.operationId,
1133
+ recipient: {
1134
+ kind: "follower" as const,
1135
+ instanceId: deps.instanceId,
1136
+ sessionId: authority.sessionId,
1137
+ generation: request.registrationGeneration,
1138
+ },
1139
+ target: {
1140
+ chatId: observedTarget.chatId,
1141
+ threadId: observedTarget.threadId!,
1142
+ },
1143
+ slot: binding.slot!,
1144
+ ready,
1145
+ };
1146
+ };
1147
+ let result: TelegramBusWorkspaceRestoreObservation | undefined;
1148
+ deps.topicTargetStore.withWorkspaceRestoreSnapshot(
1149
+ expected,
1150
+ (snapshot) => {
1151
+ result = observe(snapshot);
1152
+ },
1153
+ );
1154
+ if (!result)
1155
+ throw new Error(
1156
+ "Telegram follower Restore observation was not completed.",
1157
+ );
1158
+ if (release || workObserver) {
1159
+ if (
1160
+ !("kind" in expected) ||
1161
+ expected.kind !== "live-rebind" ||
1162
+ expected.phase !== "released" ||
1163
+ !result.ready ||
1164
+ request.mode !== "inspect"
1165
+ )
1166
+ throw new Error(
1167
+ "Live-rebind dispatch has no canonical released/local-applied grant.",
1168
+ );
1169
+ // Journal reads stay outside the canonical transaction; each barrier check reobserves only Workspace authority.
1170
+ if (workObserver)
1171
+ sampleWork = () => {
1172
+ deps.topicTargetStore.withWorkspaceRestoreSnapshot(
1173
+ expected,
1174
+ (snapshot) => {
1175
+ if (!observe(snapshot).ready)
1176
+ throw new Error(
1177
+ "Live-rebind work observation lost local target application.",
1178
+ );
1179
+ workObserver(expected.request.binding.target);
1180
+ assertCurrent();
1181
+ },
1182
+ );
1183
+ };
1184
+ release?.(() => {
1185
+ assertCurrent();
1186
+ let ready = false;
1187
+ deps.topicTargetStore.withWorkspaceRestoreSnapshot(
1188
+ expected,
1189
+ (snapshot) => {
1190
+ ready = observe(snapshot).ready;
1191
+ },
1192
+ );
1193
+ assertCurrent();
1194
+ return ready;
1195
+ });
792
1196
  }
793
- assertCurrent();
794
- const observedTarget = deps.registrationState.getTarget();
795
- if (!observedTarget || (!isDeepStrictEqual(observedTarget, target) && !isDeepStrictEqual(observedTarget, binding.target)))
796
- throw new Error("Telegram follower Restore local target changed.");
797
- const ready = isDeepStrictEqual(observedTarget, target);
798
- assertCurrent();
799
- return { operationId: request.operationId, recipient: { kind: "follower" as const, instanceId: deps.instanceId,
800
- sessionId: authority.sessionId, generation: request.registrationGeneration },
801
- target: { chatId: observedTarget.chatId, threadId: observedTarget.threadId! }, slot: binding.slot!, ready };
802
- };
803
- let result: TelegramBusWorkspaceRestoreObservation | undefined;
804
- deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, snapshot => { result = observe(snapshot); });
805
- if (!result) throw new Error("Telegram follower Restore observation was not completed.");
806
- return result;
807
- });
1197
+ return result;
1198
+ },
1199
+ );
1200
+ assertCurrent();
1201
+ if (
1202
+ !isDeepStrictEqual(deps.registrationState.getTarget(), result.target) ||
1203
+ deps.registrationState.getSlot() !== result.slot
1204
+ )
1205
+ throw new Error(
1206
+ "Telegram follower Restore local registration changed after admission release.",
1207
+ );
1208
+ // Work can change while admission releases; sample only after that await under a fresh canonical snapshot.
1209
+ sampleWork?.();
808
1210
  assertCurrent();
809
- if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) || deps.registrationState.getSlot() !== result.slot)
810
- throw new Error("Telegram follower Restore local registration changed after admission release.");
811
1211
  return result;
812
1212
  };
1213
+ return Object.assign(handler, {
1214
+ /** Capture availability only; asserting current recipient requires canonical released/local-applied ownership. */
1215
+ prepareLiveCommandRecipient(
1216
+ input: {
1217
+ operationId: string;
1218
+ registrationGeneration: string;
1219
+ sessionId: string;
1220
+ sourceUpdateIds: readonly number[];
1221
+ target: TelegramTarget & { threadId: number };
1222
+ },
1223
+ ctx: TContext,
1224
+ ):
1225
+ | Pick<
1226
+ TelegramBusFollowerCommandOwner<TContext>,
1227
+ "isCurrent" | "assertRecipientCurrent"
1228
+ >
1229
+ | undefined {
1230
+ const expected = structuredClone(input),
1231
+ getAuthority = deps.getContextAuthority,
1232
+ read = deps.readLiveRebindIntent,
1233
+ store = deps.topicTargetStore;
1234
+ const snapshot = store.withWorkspaceLiveRebindSnapshot,
1235
+ state = deps.registrationState;
1236
+ const statePorts = {
1237
+ getTarget: state.getTarget,
1238
+ getSlot: state.getSlot,
1239
+ getGeneration: state.getGeneration,
1240
+ getLeaderProtocol: state.getLeaderProtocol,
1241
+ };
1242
+ const instanceId = deps.instanceId,
1243
+ authority = structuredClone(getAuthority.call(deps, ctx)),
1244
+ oldTarget = structuredClone(state.getTarget()),
1245
+ slot = state.getSlot();
1246
+ if (
1247
+ !authority ||
1248
+ !read ||
1249
+ !snapshot ||
1250
+ !instanceId ||
1251
+ !slot ||
1252
+ !expected.operationId?.trim() ||
1253
+ !expected.registrationGeneration?.trim() ||
1254
+ expected.sessionId !== authority.sessionId ||
1255
+ expected.sourceUpdateIds.length !== 1 ||
1256
+ !Number.isSafeInteger(expected.sourceUpdateIds[0]) ||
1257
+ expected.sourceUpdateIds[0]! < 0 ||
1258
+ typeof authority.generation !== "number" ||
1259
+ !Number.isSafeInteger(authority.generation) ||
1260
+ authority.generation < 0 ||
1261
+ !authority.profileBindingKey?.trim() ||
1262
+ !authority.cwd?.trim() ||
1263
+ !authority.executor?.instanceId?.trim() ||
1264
+ !authority.executor.leaderEpoch?.trim() ||
1265
+ !Number.isSafeInteger(authority.operatorUserId) ||
1266
+ authority.operatorUserId <= 0 ||
1267
+ expected.target.chatId !== authority.operatorUserId ||
1268
+ !Number.isSafeInteger(expected.target.threadId) ||
1269
+ expected.target.threadId <= 0 ||
1270
+ !oldTarget ||
1271
+ oldTarget.chatId !== expected.target.chatId ||
1272
+ !Number.isSafeInteger(oldTarget.threadId) ||
1273
+ oldTarget.threadId! <= 0
1274
+ )
1275
+ return undefined;
1276
+ const portsCurrent = () =>
1277
+ deps.instanceId === instanceId &&
1278
+ deps.getContextAuthority === getAuthority &&
1279
+ deps.readLiveRebindIntent === read &&
1280
+ deps.topicTargetStore === store &&
1281
+ store.withWorkspaceLiveRebindSnapshot === snapshot &&
1282
+ deps.registrationState === state &&
1283
+ Object.entries(statePorts).every(
1284
+ ([key, value]) => Reflect.get(state, key) === value,
1285
+ );
1286
+ const isCurrent = () => {
1287
+ try {
1288
+ if (
1289
+ !portsCurrent() ||
1290
+ !isDeepStrictEqual(getAuthority.call(deps, ctx), authority) ||
1291
+ state.getGeneration() !== expected.registrationGeneration ||
1292
+ state.getSlot() !== slot ||
1293
+ !isDeepStrictEqual(
1294
+ state.getLeaderProtocol(),
1295
+ authority.leaderProtocol,
1296
+ ) ||
1297
+ ![
1298
+ ...TELEGRAM_BUS_LIVE_REBIND_CAPABILITIES,
1299
+ ...TELEGRAM_BUS_HELD_COMMAND_CAPABILITIES,
1300
+ ].every((cap) =>
1301
+ hasTelegramBusCapability(authority.leaderProtocol, cap),
1302
+ ) ||
1303
+ ![oldTarget, expected.target].some((target) =>
1304
+ isDeepStrictEqual(state.getTarget(), target),
1305
+ )
1306
+ )
1307
+ return false;
1308
+ return (
1309
+ portsCurrent() &&
1310
+ isDeepStrictEqual(getAuthority.call(deps, ctx), authority) &&
1311
+ state.getGeneration() === expected.registrationGeneration &&
1312
+ state.getSlot() === slot &&
1313
+ isDeepStrictEqual(
1314
+ state.getLeaderProtocol(),
1315
+ authority.leaderProtocol,
1316
+ ) &&
1317
+ [oldTarget, expected.target].some((target) =>
1318
+ isDeepStrictEqual(state.getTarget(), target),
1319
+ ) &&
1320
+ portsCurrent()
1321
+ );
1322
+ } catch {
1323
+ return false;
1324
+ }
1325
+ };
1326
+ if (!isCurrent()) return undefined;
1327
+ return {
1328
+ isCurrent,
1329
+ assertRecipientCurrent() {
1330
+ if (!isCurrent())
1331
+ throw new Error("Selected command recipient authority changed.");
1332
+ const value = read.call(
1333
+ deps,
1334
+ expected.operationId,
1335
+ authority.profileBindingKey,
1336
+ );
1337
+ if (
1338
+ !value ||
1339
+ value.kind !== "live-rebind" ||
1340
+ value.phase !== "released" ||
1341
+ value.cleanup ||
1342
+ value.request.operationId !== expected.operationId ||
1343
+ value.recipient.kind !== "follower" ||
1344
+ value.recipient.instanceId !== instanceId ||
1345
+ value.recipient.sessionId !== expected.sessionId ||
1346
+ value.recipient.generation !== expected.registrationGeneration ||
1347
+ !isDeepStrictEqual(value.executor, authority.executor) ||
1348
+ value.operatorUserId !== authority.operatorUserId ||
1349
+ !isDeepStrictEqual(
1350
+ value.request.source.updateIds,
1351
+ expected.sourceUpdateIds,
1352
+ ) ||
1353
+ !isDeepStrictEqual(value.request.target, expected.target) ||
1354
+ !isDeepStrictEqual(value.request.binding.target, oldTarget) ||
1355
+ value.request.owner.instanceId !== instanceId ||
1356
+ value.request.owner.owner?.kind !== "manual-follower" ||
1357
+ value.request.owner.slot !== slot ||
1358
+ !isDeepStrictEqual(value.request.owner.target, oldTarget) ||
1359
+ value.request.binding.cwd !==
1360
+ WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd) ||
1361
+ value.request.binding.sessionId !== expected.sessionId ||
1362
+ value.request.binding.slot !== slot ||
1363
+ !isCurrent()
1364
+ )
1365
+ throw new Error(
1366
+ "Selected command recipient has no exact released intent.",
1367
+ );
1368
+ const intent = structuredClone(value);
1369
+ let confirmed = false;
1370
+ snapshot.call(store, intent, (canonical) => {
1371
+ if (
1372
+ !isCurrent() ||
1373
+ !isDeepStrictEqual(
1374
+ read.call(
1375
+ deps,
1376
+ expected.operationId,
1377
+ authority.profileBindingKey,
1378
+ ),
1379
+ intent,
1380
+ )
1381
+ )
1382
+ return;
1383
+ const bindings = (canonical.workspaceBindings ?? []).filter(
1384
+ (row) => row.bindingKey === intent.request.binding.bindingKey,
1385
+ );
1386
+ const owners = canonical.threads.filter(
1387
+ (row) =>
1388
+ row.status === "active" &&
1389
+ (row.slot === slot ||
1390
+ isDeepStrictEqual(row.target, expected.target)),
1391
+ );
1392
+ const row = bindings[0],
1393
+ owner = owners[0];
1394
+ confirmed =
1395
+ bindings.length === 1 &&
1396
+ row?.sessionId === expected.sessionId &&
1397
+ row.cwd === intent.request.binding.cwd &&
1398
+ row.slot === slot &&
1399
+ row.workspaceKey === intent.request.binding.workspaceKey &&
1400
+ row.inactiveSinceMs === undefined &&
1401
+ isDeepStrictEqual(row.target, expected.target) &&
1402
+ owners.length === 1 &&
1403
+ owner?.instanceId === instanceId &&
1404
+ owner.owner?.kind === "manual-follower" &&
1405
+ owner.slot === slot &&
1406
+ owner.profileKey === intent.request.owner.profileKey &&
1407
+ isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
1408
+ isDeepStrictEqual(owner.target, expected.target) &&
1409
+ isDeepStrictEqual(state.getTarget(), expected.target) &&
1410
+ isCurrent() &&
1411
+ isDeepStrictEqual(
1412
+ read.call(
1413
+ deps,
1414
+ expected.operationId,
1415
+ authority.profileBindingKey,
1416
+ ),
1417
+ intent,
1418
+ );
1419
+ });
1420
+ if (!confirmed || !isCurrent())
1421
+ throw new Error(
1422
+ "Selected command canonical/local recipient changed.",
1423
+ );
1424
+ },
1425
+ };
1426
+ },
1427
+ });
813
1428
  }
814
1429
 
815
1430
  export function createTelegramBusFollowerClientRuntime<
@@ -819,8 +1434,7 @@ export function createTelegramBusFollowerClientRuntime<
819
1434
  TMessage = unknown,
820
1435
  >(deps: TelegramBusFollowerClientRuntimeDeps<TMessage>) {
821
1436
  const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
822
- const timeoutMs =
823
- deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1437
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
824
1438
  const sharedClientDeps = {
825
1439
  socketPath: deps.socketPath,
826
1440
  createRequestId,
@@ -849,8 +1463,7 @@ export function createTelegramBusFollowerClientRuntime<
849
1463
  >({
850
1464
  ...sharedClientDeps,
851
1465
  getAuthSecret: deps.getForwardingAuthSecret,
852
- getForwardCommentBatchPosition:
853
- deps.getForwardCommentBatchPosition,
1466
+ getForwardCommentBatchPosition: deps.getForwardCommentBatchPosition,
854
1467
  validateForwardOwnership: deps.validateForwardOwnership,
855
1468
  recordRuntimeEvent: deps.recordRuntimeEvent,
856
1469
  }),
@@ -865,34 +1478,21 @@ export function createTelegramBusFollowerClientRuntime<
865
1478
 
866
1479
  export function createTelegramBusFollowerQueueHandoffClient(
867
1480
  deps: TelegramBusFollowerApiCallerDeps,
868
- ): (input: {
869
- recipientInstanceId: string;
870
- recipientRegistrationGeneration: string;
871
- donorProcessId: number;
872
- donorProcessBirthId: string;
873
- donorSessionGeneration: number;
874
- donorAcquisitionId: string;
875
- donorAcquiredAtMs: number;
876
- handoffToken: string;
877
- payload: TelegramQueueHandoffPayload;
878
- }) => Promise<TelegramQueueHandoffStageResult> {
1481
+ ): (
1482
+ input: TelegramBusFollowerQueueHandoffOffer,
1483
+ ) => Promise<TelegramQueueHandoffStageResult> {
879
1484
  const getNowMs = deps.getNowMs ?? Date.now;
880
- const timeoutMs =
881
- deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1485
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
882
1486
  return async (input) => {
883
1487
  const registration = await resolveTelegramBusFollowerRegistration(
884
1488
  deps,
885
1489
  timeoutMs,
886
1490
  );
887
1491
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
888
- const response = await sendTelegramBusLocalEnvelope({
1492
+ const response = await sendTelegramBusOperation(
889
1493
  socketPath,
890
- timeoutMs: registration.remainingTimeoutMs,
891
- retry: getTelegramBusTransportRetryPolicy({
892
- endpoint: socketPath,
893
- operation: "operation",
894
- }),
895
- envelope: {
1494
+ registration.remainingTimeoutMs,
1495
+ {
896
1496
  kind: "follower.offerQueueHandoff",
897
1497
  requestId: deps.createRequestId(),
898
1498
  auth: deps.getAuthSecret?.(),
@@ -901,7 +1501,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
901
1501
  ...input,
902
1502
  sentAtMs: getNowMs(),
903
1503
  },
904
- });
1504
+ );
905
1505
  const queueOwner =
906
1506
  response?.kind === "bus.ack" && isRecord(response.result)
907
1507
  ? parseTelegramUpdateJournalQueueOwner(response.result.queueOwner)
@@ -921,7 +1521,8 @@ export function createTelegramBusFollowerQueueHandoffClient(
921
1521
  input.payload.admissionReceipts[0].sourceUpdateIds.length &&
922
1522
  response.result.sourceUpdateIds.every(
923
1523
  (updateId, index) =>
924
- updateId === input.payload.admissionReceipts[0]!.sourceUpdateIds[index],
1524
+ updateId ===
1525
+ input.payload.admissionReceipts[0]!.sourceUpdateIds[index],
925
1526
  ) &&
926
1527
  queueOwner
927
1528
  ) {
@@ -934,7 +1535,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
934
1535
  }
935
1536
  throw new Error(
936
1537
  response?.kind === "bus.ack"
937
- ? response.message ?? "Telegram queue handoff was rejected."
1538
+ ? (response.message ?? "Telegram queue handoff was rejected.")
938
1539
  : "Telegram queue handoff did not return an acknowledgement.",
939
1540
  );
940
1541
  };
@@ -949,8 +1550,7 @@ function createTelegramBusAgentMessageClient(
949
1550
  routeMessage: (message: TelegramBusAgentMessage) => Promise<void>;
950
1551
  } {
951
1552
  const getNowMs = deps.getNowMs ?? Date.now;
952
- const timeoutMs =
953
- deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1553
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
954
1554
  const request = async (
955
1555
  envelope:
956
1556
  | Extract<TelegramBusEnvelope, { kind: "follower.resolveAgentTarget" }>
@@ -958,19 +1558,15 @@ function createTelegramBusAgentMessageClient(
958
1558
  requestTimeoutMs = timeoutMs,
959
1559
  ): Promise<unknown> => {
960
1560
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
961
- const response = await sendTelegramBusLocalEnvelope({
1561
+ const response = await sendTelegramBusOperation(
962
1562
  socketPath,
963
- timeoutMs: requestTimeoutMs,
964
- retry: getTelegramBusTransportRetryPolicy({
965
- endpoint: socketPath,
966
- operation: "operation",
967
- }),
1563
+ requestTimeoutMs,
968
1564
  envelope,
969
- });
1565
+ );
970
1566
  if (response?.kind === "bus.ack" && response.ok) return response.result;
971
1567
  throw new Error(
972
1568
  response?.kind === "bus.ack"
973
- ? response.message ?? "Telegram bus agent message failed."
1569
+ ? (response.message ?? "Telegram bus agent message failed.")
974
1570
  : "Telegram bus agent message did not return an acknowledgement.",
975
1571
  );
976
1572
  };
@@ -991,13 +1587,16 @@ function createTelegramBusAgentMessageClient(
991
1587
  return {
992
1588
  async resolveTarget(selector) {
993
1589
  const registration = await registrationFields();
994
- const result = await request({
995
- kind: "follower.resolveAgentTarget",
996
- requestId: deps.createRequestId(),
997
- ...registration.fields,
998
- selector,
999
- sentAtMs: getNowMs(),
1000
- }, registration.remainingTimeoutMs);
1590
+ const result = await request(
1591
+ {
1592
+ kind: "follower.resolveAgentTarget",
1593
+ requestId: deps.createRequestId(),
1594
+ ...registration.fields,
1595
+ selector,
1596
+ sentAtMs: getNowMs(),
1597
+ },
1598
+ registration.remainingTimeoutMs,
1599
+ );
1001
1600
  if (!result || typeof result !== "object" || Array.isArray(result)) {
1002
1601
  throw new Error("Telegram bus returned an invalid agent target.");
1003
1602
  }
@@ -1012,39 +1611,241 @@ function createTelegramBusAgentMessageClient(
1012
1611
  },
1013
1612
  async routeMessage(message) {
1014
1613
  const registration = await registrationFields();
1015
- await request({
1016
- kind: "follower.routeAgentMessage",
1017
- requestId: deps.createRequestId(),
1018
- ...registration.fields,
1019
- message,
1020
- sentAtMs: getNowMs(),
1021
- }, registration.remainingTimeoutMs);
1614
+ await request(
1615
+ {
1616
+ kind: "follower.routeAgentMessage",
1617
+ requestId: deps.createRequestId(),
1618
+ ...registration.fields,
1619
+ message,
1620
+ sentAtMs: getNowMs(),
1621
+ },
1622
+ registration.remainingTimeoutMs,
1623
+ );
1022
1624
  },
1023
1625
  };
1024
1626
  }
1025
1627
 
1026
- export function createTelegramBusFollowerApiCaller(
1027
- deps: TelegramBusFollowerApiCallerDeps,
1028
- ): (method: string, args: unknown[]) => Promise<unknown> {
1029
- const getNowMs = deps.getNowMs ?? Date.now;
1030
- const timeoutMs =
1031
- deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1032
- return async (method, args) => {
1033
- const registration = await resolveTelegramBusFollowerRegistration(
1034
- deps,
1035
- timeoutMs,
1628
+ /** Private selected text-effect caller; not a guarded ordinary API adapter or follower command admission grant. */
1629
+ export function createTelegramBusFollowerSelectedMenuCaller<TContext>(deps: {
1630
+ client: TelegramBusFollowerApiCallerDeps;
1631
+ protocolIdentity: TelegramBusProtocolIdentity;
1632
+ recipient: {
1633
+ getContextAuthority(
1634
+ ctx: TContext,
1635
+ ): TelegramBusFollowerRestoreContext | undefined;
1636
+ getJournalBindingKey(ctx: TContext): string | undefined;
1637
+ getProcessIdentity(): { processId: number; processBirthId: string };
1638
+ registrationState: Pick<
1639
+ TelegramBusFollowerRegistrationState,
1640
+ "getTarget" | "getSlot" | "getGeneration" | "getLeaderProtocol"
1641
+ >;
1642
+ };
1643
+ }) {
1644
+ return async (input: {
1645
+ ctx: TContext;
1646
+ operationId: string;
1647
+ registrationGeneration: string;
1648
+ effect: TelegramBusSelectedMenuTextEffect;
1649
+ assertAuthority: () => void;
1650
+ }): Promise<TelegramBusSelectedMenuDeliveryObservation> => {
1651
+ let dispatched = false,
1652
+ method = "sendMessage";
1653
+ try {
1654
+ const client = { ...deps.client },
1655
+ recipient = deps.recipient,
1656
+ ctx = input.ctx,
1657
+ assertAuthority = input.assertAuthority;
1658
+ const getAuthority = recipient.getContextAuthority,
1659
+ getJournal = recipient.getJournalBindingKey,
1660
+ getProcess = recipient.getProcessIdentity;
1661
+ const state = recipient.registrationState,
1662
+ statePorts = {
1663
+ getTarget: state.getTarget,
1664
+ getSlot: state.getSlot,
1665
+ getGeneration: state.getGeneration,
1666
+ getLeaderProtocol: state.getLeaderProtocol,
1667
+ };
1668
+ const authority = structuredClone(getAuthority.call(recipient, ctx));
1669
+ const journalBindingKey = getJournal.call(recipient, ctx),
1670
+ process = structuredClone(getProcess.call(recipient)),
1671
+ target = structuredClone(state.getTarget()),
1672
+ slot = state.getSlot();
1673
+ const protocol = structuredClone(deps.protocolIdentity),
1674
+ leaderProtocol = structuredClone(state.getLeaderProtocol()),
1675
+ auth = client.getAuthSecret?.();
1676
+ const endpoint = resolveTelegramBusSocketPath(client.socketPath),
1677
+ timeoutMs = client.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1678
+ if (
1679
+ !authority ||
1680
+ typeof authority.generation !== "number" ||
1681
+ typeof assertAuthority !== "function" ||
1682
+ !slot ||
1683
+ !auth ||
1684
+ !Number.isFinite(timeoutMs) ||
1685
+ timeoutMs <= 0
1686
+ )
1687
+ throw new Error("Selected-menu local owner is unavailable.");
1688
+ const decoded = parseTelegramBusEnvelope(
1689
+ JSON.stringify({
1690
+ kind: "follower.deliverSelectedMenu",
1691
+ requestId: client.createRequestId(),
1692
+ auth,
1693
+ instanceId: client.instanceId,
1694
+ operationId: input.operationId,
1695
+ registrationGeneration: input.registrationGeneration,
1696
+ recipient: {
1697
+ sessionId: authority.sessionId,
1698
+ sessionGeneration: authority.generation,
1699
+ ...process,
1700
+ profileKey: authority.profileBindingKey,
1701
+ journalBindingKey,
1702
+ target,
1703
+ },
1704
+ executor: authority.executor,
1705
+ operatorUserId: authority.operatorUserId,
1706
+ effect: structuredClone(input.effect),
1707
+ sentAtMs: (client.getNowMs ?? Date.now)(),
1708
+ }),
1709
+ );
1710
+ if (decoded?.kind !== "follower.deliverSelectedMenu")
1711
+ throw new Error("Selected-menu local description is invalid.");
1712
+ const expected = decoded;
1713
+ method =
1714
+ expected.effect.kind === "send-text"
1715
+ ? "sendMessage"
1716
+ : "editMessageText";
1717
+ const assertCurrent = (allowPending = false) => {
1718
+ assertAuthority();
1719
+ const local = getAuthority.call(recipient, ctx),
1720
+ currentGeneration = client.getRegistrationGeneration();
1721
+ if (
1722
+ !isDeepStrictEqual(deps.client, client) ||
1723
+ deps.recipient !== recipient ||
1724
+ recipient.getContextAuthority !== getAuthority ||
1725
+ recipient.getJournalBindingKey !== getJournal ||
1726
+ recipient.getProcessIdentity !== getProcess ||
1727
+ recipient.registrationState !== state ||
1728
+ !Object.entries(statePorts).every(
1729
+ ([key, value]) => state[key as keyof typeof statePorts] === value,
1730
+ ) ||
1731
+ !isDeepStrictEqual(local, authority) ||
1732
+ getJournal.call(recipient, ctx) !== journalBindingKey ||
1733
+ !isDeepStrictEqual(getProcess.call(recipient), process) ||
1734
+ !isDeepStrictEqual(state.getTarget(), target) ||
1735
+ state.getSlot() !== slot ||
1736
+ state.getGeneration() !== expected.registrationGeneration ||
1737
+ (!allowPending &&
1738
+ currentGeneration !== expected.registrationGeneration) ||
1739
+ (currentGeneration !== undefined &&
1740
+ currentGeneration !== expected.registrationGeneration) ||
1741
+ client.getAuthSecret?.() !== auth ||
1742
+ resolveTelegramBusSocketPath(client.socketPath) !== endpoint ||
1743
+ !isDeepStrictEqual(deps.protocolIdentity, protocol) ||
1744
+ !isDeepStrictEqual(state.getLeaderProtocol(), leaderProtocol) ||
1745
+ !getTelegramBusProtocolCompatibility({
1746
+ local: protocol,
1747
+ remote: leaderProtocol,
1748
+ }).compatible ||
1749
+ !hasTelegramBusCapability(
1750
+ protocol,
1751
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
1752
+ ) ||
1753
+ !hasTelegramBusCapability(
1754
+ leaderProtocol,
1755
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
1756
+ )
1757
+ )
1758
+ throw new Error("Selected-menu local authority changed.");
1759
+ if (!isDeepStrictEqual(getAuthority.call(recipient, ctx), authority))
1760
+ throw new Error(
1761
+ "Selected-menu context changed after local observations.",
1762
+ );
1763
+ assertAuthority();
1764
+ };
1765
+ assertCurrent(true);
1766
+ const registration = await resolveTelegramBusFollowerRegistration(
1767
+ client,
1768
+ timeoutMs,
1769
+ );
1770
+ if (registration.generation !== expected.registrationGeneration)
1771
+ throw new Error("Selected-menu registration changed.");
1772
+ assertCurrent();
1773
+ dispatched = true;
1774
+ const response = await sendTelegramBusLocalEnvelope({
1775
+ socketPath: endpoint,
1776
+ envelope: expected,
1777
+ timeoutMs: registration.remainingTimeoutMs,
1778
+ retry: { attempts: 1, delayMs: 0 },
1779
+ assertAuthority: () => assertCurrent(),
1780
+ });
1781
+ assertCurrent();
1782
+ if (
1783
+ response?.kind !== "bus.ack" ||
1784
+ response.requestId !== expected.requestId ||
1785
+ !response.ok ||
1786
+ response.error !== undefined ||
1787
+ (response.protocol !== undefined &&
1788
+ !isDeepStrictEqual(response.protocol, leaderProtocol)) ||
1789
+ !isRecord(response.result)
1790
+ )
1791
+ throw new Error("Selected-menu acknowledgement is unconfirmed.");
1792
+ const result = response.result;
1793
+ if (
1794
+ !isDeepStrictEqual(Object.keys(result).sort(), [
1795
+ "effect",
1796
+ "messageId",
1797
+ "operationId",
1798
+ "recipient",
1799
+ "registrationGeneration",
1800
+ ]) ||
1801
+ result.operationId !== expected.operationId ||
1802
+ result.registrationGeneration !== expected.registrationGeneration ||
1803
+ result.effect !== expected.effect.kind ||
1804
+ !isDeepStrictEqual(result.recipient, expected.recipient) ||
1805
+ typeof result.messageId !== "number" ||
1806
+ !Number.isSafeInteger(result.messageId) ||
1807
+ result.messageId <= 0 ||
1808
+ (expected.effect.kind === "edit-text" &&
1809
+ result.messageId !== expected.effect.messageId)
1810
+ )
1811
+ throw new Error("Selected-menu recipient/result is unconfirmed.");
1812
+ assertCurrent();
1813
+ return {
1814
+ operationId: expected.operationId,
1815
+ recipient: structuredClone(expected.recipient),
1816
+ registrationGeneration: expected.registrationGeneration,
1817
+ effect: expected.effect.kind,
1818
+ messageId: result.messageId,
1819
+ };
1820
+ } catch (error) {
1821
+ if (
1822
+ !dispatched ||
1823
+ (error instanceof TelegramBusLocalAuthorityError &&
1824
+ !error.requestIssued)
1825
+ )
1826
+ throw new TelegramApiAuthorityError(false);
1827
+ throw new TelegramApiCommitUnknownError(method, error);
1828
+ }
1829
+ };
1830
+ }
1831
+
1832
+ export function createTelegramBusFollowerApiCaller(
1833
+ deps: TelegramBusFollowerApiCallerDeps,
1834
+ ): (method: string, args: unknown[]) => Promise<unknown> {
1835
+ const getNowMs = deps.getNowMs ?? Date.now;
1836
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
1837
+ return async (method, args) => {
1838
+ const registration = await resolveTelegramBusFollowerRegistration(
1839
+ deps,
1840
+ timeoutMs,
1036
1841
  );
1037
1842
  const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
1038
1843
  let response: TelegramBusEnvelope | undefined;
1039
1844
  try {
1040
- response = await sendTelegramBusLocalEnvelope({
1845
+ response = await sendTelegramBusOperation(
1041
1846
  socketPath,
1042
- timeoutMs: registration.remainingTimeoutMs,
1043
- retry: getTelegramBusTransportRetryPolicy({
1044
- endpoint: socketPath,
1045
- operation: "operation",
1046
- }),
1047
- envelope: {
1847
+ registration.remainingTimeoutMs,
1848
+ {
1048
1849
  kind: "follower.callApi",
1049
1850
  requestId: deps.createRequestId(),
1050
1851
  auth: deps.getAuthSecret?.(),
@@ -1054,7 +1855,7 @@ export function createTelegramBusFollowerApiCaller(
1054
1855
  args,
1055
1856
  sentAtMs: getNowMs(),
1056
1857
  },
1057
- });
1858
+ );
1058
1859
  } catch (error) {
1059
1860
  const apiMethod =
1060
1861
  (method === "call" || method === "callMultipart") &&
@@ -1101,9 +1902,7 @@ export function createTelegramBusFollowerApiCaller(
1101
1902
  async function resolveTelegramBusFollowerRegistration(
1102
1903
  deps: Pick<
1103
1904
  TelegramBusFollowerApiCallerDeps,
1104
- | "getRegistrationGeneration"
1105
- | "waitForRegistrationGeneration"
1106
- | "getNowMs"
1905
+ "getRegistrationGeneration" | "waitForRegistrationGeneration" | "getNowMs"
1107
1906
  >,
1108
1907
  timeoutMs: number,
1109
1908
  ): Promise<{ generation: string; remainingTimeoutMs: number }> {
@@ -1111,24 +1910,17 @@ async function resolveTelegramBusFollowerRegistration(
1111
1910
  if (current) return { generation: current, remainingTimeoutMs: timeoutMs };
1112
1911
  const getNowMs = deps.getNowMs ?? Date.now;
1113
1912
  const startedAtMs = getNowMs();
1114
- const restored = await deps.waitForRegistrationGeneration?.(
1115
- timeoutMs,
1913
+ const restored = await deps.waitForRegistrationGeneration?.(timeoutMs);
1914
+ const remainingTimeoutMs = Math.max(
1915
+ 0,
1916
+ timeoutMs - (getNowMs() - startedAtMs),
1116
1917
  );
1117
- const remainingTimeoutMs = Math.max(0, timeoutMs - (getNowMs() - startedAtMs));
1118
1918
  if (restored && remainingTimeoutMs > 0) {
1119
1919
  return { generation: restored, remainingTimeoutMs };
1120
1920
  }
1121
1921
  throw new Error("Telegram bus follower is not registered.");
1122
1922
  }
1123
1923
 
1124
- function isTelegramStaleContextError(error: unknown): boolean {
1125
- return (
1126
- error instanceof Error &&
1127
- (error.message.includes("stale after session") ||
1128
- error.message.includes("stale ctx"))
1129
- );
1130
- }
1131
-
1132
1924
  export function createTelegramBusFollowerSessionReplacementSuspender(
1133
1925
  deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
1134
1926
  ): (preserveTarget?: boolean) => Promise<void> {
@@ -1248,7 +2040,9 @@ export function createTelegramBusFollowerSessionRefreshHook<TContext>(
1248
2040
  try {
1249
2041
  await deps.registrationRuntime.setContext(ctx);
1250
2042
  } catch (error) {
1251
- deps.recordRuntimeEvent("bus", error, { phase: "follower-session-refresh" });
2043
+ deps.recordRuntimeEvent("bus", error, {
2044
+ phase: "follower-session-refresh",
2045
+ });
1252
2046
  return;
1253
2047
  }
1254
2048
  if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
@@ -1309,13 +2103,23 @@ export function createTelegramBusFollowerRegistrationState(
1309
2103
  getThreadName: () => threadName,
1310
2104
  getDisplayTitle: () => displayTitle,
1311
2105
  setDisplayTitle(title, expectedGeneration) {
1312
- if (!registered || !generation || expectedGeneration !== generation ||
1313
- !title.trim() || title.length > 128 || displayTitle === title) return false;
2106
+ if (
2107
+ !registered ||
2108
+ !generation ||
2109
+ expectedGeneration !== generation ||
2110
+ !title.trim() ||
2111
+ title.length > 128 ||
2112
+ displayTitle === title
2113
+ )
2114
+ return false;
1314
2115
  displayTitle = title;
1315
2116
  return true;
1316
2117
  },
1317
2118
  getGeneration: () => generation,
1318
- getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
2119
+ getSessionId: (currentSessionId) =>
2120
+ registered && generation && currentSessionId === sessionId
2121
+ ? sessionId
2122
+ : undefined,
1319
2123
  beginRecovery: () => {
1320
2124
  if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
1321
2125
  activeRecoveryEpoch = ++recoveryEpoch;
@@ -1326,7 +2130,9 @@ export function createTelegramBusFollowerRegistrationState(
1326
2130
  activeRecoveryEpoch = undefined;
1327
2131
  if (epoch !== undefined) settleGenerationWaiters(undefined, epoch);
1328
2132
  },
1329
- waitForGeneration: (timeoutMs = TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS) => {
2133
+ waitForGeneration: (
2134
+ timeoutMs = TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS,
2135
+ ) => {
1330
2136
  if (generation) return Promise.resolve(generation);
1331
2137
  const epoch = activeRecoveryEpoch;
1332
2138
  if (epoch === undefined) return Promise.resolve(undefined);
@@ -1353,21 +2159,33 @@ export function createTelegramBusFollowerRegistrationState(
1353
2159
  ).sort();
1354
2160
  },
1355
2161
  setRegistered: (next, nextTarget, metadata) => {
1356
- const retainedDisplayTitle = next && registered &&
2162
+ const retainedDisplayTitle =
2163
+ next &&
2164
+ registered &&
1357
2165
  generation === metadata?.generation &&
1358
- target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
1359
- ? displayTitle : undefined;
1360
- const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
2166
+ target?.chatId === nextTarget?.chatId &&
2167
+ target?.threadId === nextTarget?.threadId
2168
+ ? displayTitle
2169
+ : undefined;
2170
+ const retainedSessionId =
2171
+ next && registered && generation === metadata?.generation
2172
+ ? sessionId
2173
+ : undefined;
1361
2174
  const availabilityChanged = registered !== next;
1362
2175
  registered = next;
1363
2176
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
1364
2177
  slot = next ? metadata?.slot : undefined;
1365
2178
  threadName = next ? metadata?.threadName : undefined;
1366
- displayTitle = next && nextTarget && metadata?.generation &&
1367
- metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
1368
- ? metadata.displayTitle : retainedDisplayTitle;
2179
+ displayTitle =
2180
+ next &&
2181
+ nextTarget &&
2182
+ metadata?.generation &&
2183
+ metadata.displayTitle?.trim() &&
2184
+ metadata.displayTitle.length <= 128
2185
+ ? metadata.displayTitle
2186
+ : retainedDisplayTitle;
1369
2187
  generation = next ? metadata?.generation : undefined;
1370
- sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
2188
+ sessionId = next ? (metadata?.sessionId ?? retainedSessionId) : undefined;
1371
2189
  leaderProtocol =
1372
2190
  next && metadata?.leaderProtocol
1373
2191
  ? {
@@ -1403,7 +2221,7 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
1403
2221
  try {
1404
2222
  deps.updateStatus(ctx);
1405
2223
  } catch (error) {
1406
- if (!isTelegramStaleContextError(error)) throw error;
2224
+ if (!isPiStaleContextError(error)) throw error;
1407
2225
  deps.recordRuntimeEvent("bus", error, {
1408
2226
  phase: "follower-stale-context-status",
1409
2227
  });
@@ -1606,19 +2424,14 @@ export function createTelegramBusFollowerHeartbeatRecoveryHandler<TContext>(
1606
2424
  scheduleRecovery(error, ctx, initialBinding);
1607
2425
  return;
1608
2426
  }
1609
- await attemptPreferredPromotion(
1610
- error,
1611
- ctx,
1612
- initialBinding,
1613
- graceState,
1614
- );
2427
+ await attemptPreferredPromotion(error, ctx, initialBinding, graceState);
1615
2428
  return;
1616
2429
  }
1617
2430
  await attemptPreferredPromotion(error, ctx, initialBinding, state);
1618
2431
  } catch (promotionError) {
1619
2432
  deps.setLifecyclePhase(undefined);
1620
2433
  safeUpdateStatus(ctx);
1621
- if (isTelegramStaleContextError(promotionError)) {
2434
+ if (isPiStaleContextError(promotionError)) {
1622
2435
  deps.recordRuntimeEvent("bus", promotionError, {
1623
2436
  phase: "follower-heartbeat-stale-context",
1624
2437
  });
@@ -1661,11 +2474,26 @@ export function createTelegramBusFollowerRegistrationRuntime<
1661
2474
  let activeLeaderSocketPath: string | undefined;
1662
2475
  let activeAuthSecret: string | undefined;
1663
2476
  let activeRegistrationGeneration: string | undefined;
1664
- let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
2477
+ let registrationAttempt:
2478
+ | {
2479
+ ctx: TContext;
2480
+ sessionGeneration: number | undefined;
2481
+ sessionId: string | undefined;
2482
+ }
2483
+ | undefined;
1665
2484
  let activeContext: TContext | undefined;
1666
2485
  let lastKnownTarget: TelegramTarget | undefined;
1667
2486
  let lastKnownSlot: string | undefined;
1668
2487
  let lastKnownThreadName: string | undefined;
2488
+ // A leader result is accepted only for the same endpoint and registration generation that issued the request.
2489
+ const isLeaderSessionCurrent = (
2490
+ socketPath: string | undefined,
2491
+ generation: string | undefined,
2492
+ ): boolean =>
2493
+ activeLeaderSocketPath === socketPath &&
2494
+ activeRegistrationGeneration === generation &&
2495
+ (!deps.registrationState ||
2496
+ deps.registrationState.getGeneration() === generation);
1669
2497
  const stopHeartbeat = () => {
1670
2498
  if (!heartbeatInterval) return;
1671
2499
  clearInterval(heartbeatInterval);
@@ -1705,14 +2533,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
1705
2533
  activeRegistrationGeneration === registrationGeneration &&
1706
2534
  activeContext === heartbeatContext;
1707
2535
  try {
1708
- const response = await sendTelegramBusLocalEnvelope({
1709
- socketPath: leaderSocketPath,
1710
- timeoutMs: heartbeatTimeoutMs,
1711
- retry: getTelegramBusTransportRetryPolicy({
1712
- endpoint: leaderSocketPath,
1713
- operation: "operation",
1714
- }),
1715
- envelope: {
2536
+ const response = await sendTelegramBusOperation(
2537
+ leaderSocketPath,
2538
+ heartbeatTimeoutMs,
2539
+ {
1716
2540
  kind: "follower.heartbeat",
1717
2541
  requestId: deps.createRequestId(),
1718
2542
  auth: activeAuthSecret,
@@ -1720,7 +2544,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1720
2544
  registrationGeneration,
1721
2545
  sentAtMs: getNowMs(),
1722
2546
  },
1723
- });
2547
+ );
1724
2548
  if (!isCurrentHeartbeat()) return;
1725
2549
  if (response?.kind === "bus.ack" && response.ok) {
1726
2550
  const heartbeatResult = isRecord(response.result)
@@ -1732,9 +2556,14 @@ export function createTelegramBusFollowerRegistrationRuntime<
1732
2556
  )
1733
2557
  : [];
1734
2558
  deps.registrationState?.setEligibleElectionSlots(slots);
1735
- if (typeof heartbeatResult?.displayTitle === "string" &&
1736
- deps.registrationState?.setDisplayTitle(heartbeatResult.displayTitle, registrationGeneration) &&
1737
- heartbeatContext) {
2559
+ if (
2560
+ typeof heartbeatResult?.displayTitle === "string" &&
2561
+ deps.registrationState?.setDisplayTitle(
2562
+ heartbeatResult.displayTitle,
2563
+ registrationGeneration,
2564
+ ) &&
2565
+ heartbeatContext
2566
+ ) {
1738
2567
  try {
1739
2568
  deps.onDisplayTitleChanged?.(heartbeatContext);
1740
2569
  } catch {
@@ -1801,8 +2630,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
1801
2630
  const sessionId = deps.getSessionId?.(ctx);
1802
2631
  const attempt = { ctx, sessionGeneration, sessionId };
1803
2632
  registrationAttempt = attempt;
1804
- const isCurrentRequest = () => registrationAttempt === attempt &&
1805
- deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
2633
+ const isCurrentRequest = () =>
2634
+ registrationAttempt === attempt &&
2635
+ deps.getSessionGeneration?.() === sessionGeneration &&
2636
+ deps.getSessionId?.(ctx) === sessionId &&
1806
2637
  deps.isContextActive?.(ctx) !== false;
1807
2638
  const abandonOwnedRequest = async (): Promise<void> => {
1808
2639
  if (registrationAttempt !== attempt) return;
@@ -1816,6 +2647,18 @@ export function createTelegramBusFollowerRegistrationRuntime<
1816
2647
  deps.setActiveAuthSecret?.(undefined);
1817
2648
  await deps.stopReceiving?.();
1818
2649
  };
2650
+ // Failed registration releases its leader session but keeps the attempt owner for caller-visible errors.
2651
+ const failRegistration = async (
2652
+ clearGeneration = false,
2653
+ ): Promise<void> => {
2654
+ stopHeartbeat();
2655
+ activeLeaderSocketPath = undefined;
2656
+ activeAuthSecret = undefined;
2657
+ if (clearGeneration) activeRegistrationGeneration = undefined;
2658
+ deps.registrationState?.setRegistered(false);
2659
+ deps.setActiveAuthSecret?.(undefined);
2660
+ await deps.stopReceiving?.();
2661
+ };
1819
2662
  const pendingHandoff = options
1820
2663
  ? undefined
1821
2664
  : getTelegramFollowerSessionHandoff();
@@ -1827,17 +2670,22 @@ export function createTelegramBusFollowerRegistrationRuntime<
1827
2670
  previousInstanceId: pendingHandoff.instanceId,
1828
2671
  }
1829
2672
  : undefined;
1830
- const registrationOptions: {
1831
- target?: TelegramTarget;
1832
- previousInstanceId?: string;
1833
- restoreWorkspace?: boolean;
1834
- } | undefined = options ?? pendingHandoffOptions;
2673
+ const registrationOptions:
2674
+ | {
2675
+ target?: TelegramTarget;
2676
+ previousInstanceId?: string;
2677
+ restoreWorkspace?: boolean;
2678
+ }
2679
+ | undefined = options ?? pendingHandoffOptions;
1835
2680
  const leaderSocketPath =
1836
2681
  leader.busSocketPath ??
1837
2682
  deps.getLeaderSocketPath?.() ??
1838
2683
  getTelegramBusSocketPath();
1839
2684
  await deps.startReceiving?.();
1840
- if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
2685
+ if (!isCurrentRequest()) {
2686
+ await abandonOwnedRequest();
2687
+ return false;
2688
+ }
1841
2689
  activeAuthSecret = deps.getLeaderAuthSecret
1842
2690
  ? deps.getLeaderAuthSecret(leader)
1843
2691
  : leader?.busSecret;
@@ -1903,48 +2751,46 @@ export function createTelegramBusFollowerRegistrationRuntime<
1903
2751
  },
1904
2752
  });
1905
2753
  } catch (error) {
1906
- if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1907
- stopHeartbeat();
1908
- activeLeaderSocketPath = undefined;
1909
- activeAuthSecret = undefined;
1910
- deps.registrationState?.setRegistered(false);
1911
- deps.setActiveAuthSecret?.(undefined);
1912
- await deps.stopReceiving?.();
2754
+ if (!isCurrentRequest()) {
2755
+ await abandonOwnedRequest();
2756
+ return false;
2757
+ }
2758
+ await failRegistration();
1913
2759
  throw error;
1914
2760
  }
1915
- if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
2761
+ if (!isCurrentRequest()) {
2762
+ await abandonOwnedRequest();
2763
+ return false;
2764
+ }
1916
2765
  const compatibility = getTelegramBusProtocolCompatibility({
1917
2766
  local: deps.protocolIdentity,
1918
2767
  remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1919
2768
  });
1920
2769
  // An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
1921
- const unidentifiedRejection = response?.kind === "bus.ack" && !response.ok && response.protocol === undefined;
2770
+ const unidentifiedRejection =
2771
+ response?.kind === "bus.ack" &&
2772
+ !response.ok &&
2773
+ response.protocol === undefined;
1922
2774
  if (!compatibility.compatible && !unidentifiedRejection) {
1923
- stopHeartbeat();
1924
- activeLeaderSocketPath = undefined;
1925
- activeAuthSecret = undefined;
1926
- deps.registrationState?.setRegistered(false);
1927
- deps.setActiveAuthSecret?.(undefined);
1928
- await deps.stopReceiving?.();
2775
+ await failRegistration();
1929
2776
  throw new Error(
1930
2777
  `Incompatible Telegram bus leader protocol: ${compatibility.reason}.`,
1931
2778
  );
1932
2779
  }
1933
2780
  if (response?.kind === "bus.ack" && !response.ok) {
1934
- stopHeartbeat();
1935
- activeLeaderSocketPath = undefined;
1936
- activeAuthSecret = undefined;
1937
- deps.registrationState?.setRegistered(false);
1938
- deps.setActiveAuthSecret?.(undefined);
1939
- await deps.stopReceiving?.();
2781
+ await failRegistration();
1940
2782
  if (
1941
2783
  registrationOptions?.restoreWorkspace &&
1942
2784
  response.error?.code === "workspace-binding-unavailable"
1943
2785
  ) {
1944
- deps.recordRuntimeEvent?.("bus", "Telegram follower auto-connect refused by leader: Workspace binding unavailable.", {
1945
- phase: "follower-auto-connect-skip",
1946
- reason: "leader-binding-unavailable",
1947
- });
2786
+ deps.recordRuntimeEvent?.(
2787
+ "bus",
2788
+ "Telegram follower auto-connect refused by leader: Workspace binding unavailable.",
2789
+ {
2790
+ phase: "follower-auto-connect-skip",
2791
+ reason: "leader-binding-unavailable",
2792
+ },
2793
+ );
1948
2794
  return false;
1949
2795
  }
1950
2796
  throw new Error(
@@ -1958,9 +2804,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1958
2804
  ...registrationResult,
1959
2805
  generation: registrationGeneration,
1960
2806
  sessionId,
1961
- ...(response.protocol
1962
- ? { leaderProtocol: response.protocol }
1963
- : {}),
2807
+ ...(response.protocol ? { leaderProtocol: response.protocol } : {}),
1964
2808
  });
1965
2809
  lastKnownTarget = registrationResult.target;
1966
2810
  lastKnownSlot = registrationResult.slot;
@@ -1971,19 +2815,22 @@ export function createTelegramBusFollowerRegistrationRuntime<
1971
2815
  try {
1972
2816
  await deps.onRegistered?.(ctx);
1973
2817
  } catch (error) {
1974
- if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
1975
- stopHeartbeat();
1976
- activeLeaderSocketPath = undefined;
1977
- activeAuthSecret = undefined;
1978
- activeRegistrationGeneration = undefined;
1979
- deps.registrationState?.setRegistered(false);
1980
- deps.setActiveAuthSecret?.(undefined);
1981
- await deps.stopReceiving?.();
2818
+ if (!isCurrentRequest()) {
2819
+ await abandonOwnedRequest();
2820
+ return false;
2821
+ }
2822
+ await failRegistration(true);
1982
2823
  throw error;
1983
2824
  }
1984
- if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
2825
+ if (!isCurrentRequest()) {
2826
+ await abandonOwnedRequest();
2827
+ return false;
2828
+ }
1985
2829
  await requestHeartbeat();
1986
- if (!isCurrentRequest()) { await abandonOwnedRequest(); return false; }
2830
+ if (!isCurrentRequest()) {
2831
+ await abandonOwnedRequest();
2832
+ return false;
2833
+ }
1987
2834
  startHeartbeat(leaderSocketPath);
1988
2835
  if (
1989
2836
  pendingHandoffOptions &&
@@ -1994,51 +2841,82 @@ export function createTelegramBusFollowerRegistrationRuntime<
1994
2841
  }
1995
2842
  return true;
1996
2843
  }
1997
- stopHeartbeat();
1998
- activeLeaderSocketPath = undefined;
1999
- activeAuthSecret = undefined;
2000
- deps.registrationState?.setRegistered(false);
2001
- deps.setActiveAuthSecret?.(undefined);
2002
- await deps.stopReceiving?.();
2844
+ await failRegistration();
2003
2845
  return false;
2004
2846
  },
2005
2847
  setContext(ctx) {
2006
- if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
2007
- registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
2008
- registrationAttempt.sessionId !== deps.getSessionId?.(ctx))) registrationAttempt = undefined;
2848
+ if (
2849
+ registrationAttempt &&
2850
+ (registrationAttempt.ctx !== ctx ||
2851
+ registrationAttempt.sessionGeneration !==
2852
+ deps.getSessionGeneration?.() ||
2853
+ registrationAttempt.sessionId !== deps.getSessionId?.(ctx))
2854
+ )
2855
+ registrationAttempt = undefined;
2009
2856
  activeContext = ctx;
2010
2857
  },
2011
2858
  async setThreadDisplayMode(mode) {
2012
2859
  const socketPath = activeLeaderSocketPath;
2013
2860
  const generation = activeRegistrationGeneration;
2014
2861
  const auth = activeAuthSecret;
2015
- if (!socketPath || !generation || !deps.registrationState?.isRegistered()) {
2862
+ if (
2863
+ !socketPath ||
2864
+ !generation ||
2865
+ !deps.registrationState?.isRegistered()
2866
+ ) {
2016
2867
  throw new Error("Telegram follower is not registered with the leader.");
2017
2868
  }
2018
- const requiredCapability = mode === "directory-snake" || mode === "directory-title"
2019
- ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
2020
- : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
2021
- if (!hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
2022
- !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), requiredCapability)) {
2023
- throw new Error("The Telegram peers do not support this Thread display setting. Update or restart both instances.");
2869
+ const requiredCapability =
2870
+ mode === "directory-snake" || mode === "directory-title"
2871
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
2872
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
2873
+ if (
2874
+ !hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
2875
+ !hasTelegramBusCapability(
2876
+ deps.registrationState.getLeaderProtocol(),
2877
+ requiredCapability,
2878
+ )
2879
+ ) {
2880
+ throw new Error(
2881
+ "The Telegram peers do not support this Thread display setting. Update or restart both instances.",
2882
+ );
2024
2883
  }
2025
2884
  const requestId = deps.createRequestId();
2026
2885
  const response = await sendTelegramBusLocalEnvelope({
2027
- socketPath, timeoutMs: registrationTimeoutMs,
2886
+ socketPath,
2887
+ timeoutMs: registrationTimeoutMs,
2028
2888
  envelope: {
2029
- kind: "follower.setThreadDisplayMode", requestId, auth,
2030
- instanceId: deps.instanceId, registrationGeneration: generation, mode,
2889
+ kind: "follower.setThreadDisplayMode",
2890
+ requestId,
2891
+ auth,
2892
+ instanceId: deps.instanceId,
2893
+ registrationGeneration: generation,
2894
+ mode,
2031
2895
  },
2032
2896
  });
2033
- if (activeLeaderSocketPath !== socketPath || activeRegistrationGeneration !== generation ||
2034
- activeAuthSecret !== auth || deps.registrationState.getGeneration() !== generation) {
2035
- throw new Error("Telegram Thread display setting completed for a stale registration.");
2897
+ if (
2898
+ activeLeaderSocketPath !== socketPath ||
2899
+ activeRegistrationGeneration !== generation ||
2900
+ activeAuthSecret !== auth ||
2901
+ deps.registrationState.getGeneration() !== generation
2902
+ ) {
2903
+ throw new Error(
2904
+ "Telegram Thread display setting completed for a stale registration.",
2905
+ );
2036
2906
  }
2037
- if (response?.kind !== "bus.ack" || !response.ok || response.requestId !== requestId ||
2038
- !isRecord(response.result) || response.result.mode !== mode) {
2039
- throw new Error(response?.kind === "bus.ack"
2040
- ? response.message ?? "Telegram Thread display setting was rejected."
2041
- : "Telegram Thread display setting was not acknowledged.");
2907
+ if (
2908
+ response?.kind !== "bus.ack" ||
2909
+ !response.ok ||
2910
+ response.requestId !== requestId ||
2911
+ !isRecord(response.result) ||
2912
+ response.result.mode !== mode
2913
+ ) {
2914
+ throw new Error(
2915
+ response?.kind === "bus.ack"
2916
+ ? (response.message ??
2917
+ "Telegram Thread display setting was rejected.")
2918
+ : "Telegram Thread display setting was not acknowledged.",
2919
+ );
2042
2920
  }
2043
2921
  },
2044
2922
  async renameThread(target, threadName) {
@@ -2059,14 +2937,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
2059
2937
  const expectedLeaderSocketPath = activeLeaderSocketPath;
2060
2938
  const expectedRegistrationGeneration = activeRegistrationGeneration;
2061
2939
  const expectedAuthSecret = activeAuthSecret;
2062
- const response = await sendTelegramBusLocalEnvelope({
2063
- socketPath: expectedLeaderSocketPath,
2064
- timeoutMs: registrationTimeoutMs,
2065
- retry: getTelegramBusTransportRetryPolicy({
2066
- endpoint: expectedLeaderSocketPath,
2067
- operation: "operation",
2068
- }),
2069
- envelope: {
2940
+ const response = await sendTelegramBusOperation(
2941
+ expectedLeaderSocketPath,
2942
+ registrationTimeoutMs,
2943
+ {
2070
2944
  kind: "follower.renameThread",
2071
2945
  requestId: deps.createRequestId(),
2072
2946
  auth: expectedAuthSecret,
@@ -2076,20 +2950,20 @@ export function createTelegramBusFollowerRegistrationRuntime<
2076
2950
  threadName,
2077
2951
  sentAtMs: getNowMs(),
2078
2952
  },
2079
- });
2953
+ );
2080
2954
  if (response?.kind !== "bus.ack" || !response.ok) {
2081
2955
  throw new Error(
2082
2956
  response?.kind === "bus.ack"
2083
- ? (response.message ?? "Telegram Workspace Thread rename was rejected.")
2957
+ ? (response.message ??
2958
+ "Telegram Workspace Thread rename was rejected.")
2084
2959
  : "Telegram Workspace Thread rename was not acknowledged.",
2085
2960
  );
2086
2961
  }
2087
2962
  if (
2088
- activeLeaderSocketPath !== expectedLeaderSocketPath ||
2089
- activeRegistrationGeneration !== expectedRegistrationGeneration ||
2090
- (deps.registrationState &&
2091
- deps.registrationState.getGeneration() !==
2092
- expectedRegistrationGeneration)
2963
+ !isLeaderSessionCurrent(
2964
+ expectedLeaderSocketPath,
2965
+ expectedRegistrationGeneration,
2966
+ )
2093
2967
  ) {
2094
2968
  throw new Error(
2095
2969
  "Telegram Workspace Thread rename completed for a stale follower registration.",
@@ -2117,24 +2991,23 @@ export function createTelegramBusFollowerRegistrationRuntime<
2117
2991
  if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
2118
2992
  throw new Error("Telegram follower is not registered with the leader.");
2119
2993
  }
2120
- if (deps.registrationState && !hasTelegramBusCapability(
2121
- deps.registrationState.getLeaderProtocol(),
2122
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2123
- )) {
2994
+ if (
2995
+ deps.registrationState &&
2996
+ !hasTelegramBusCapability(
2997
+ deps.registrationState.getLeaderProtocol(),
2998
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2999
+ )
3000
+ ) {
2124
3001
  throw new Error(
2125
3002
  "The active Telegram leader does not support Workspace Thread reset. Update or restart that Pi instance.",
2126
3003
  );
2127
3004
  }
2128
3005
  const expectedLeaderSocketPath = activeLeaderSocketPath;
2129
3006
  const expectedRegistrationGeneration = activeRegistrationGeneration;
2130
- const response = await sendTelegramBusLocalEnvelope({
2131
- socketPath: expectedLeaderSocketPath,
2132
- timeoutMs: registrationTimeoutMs,
2133
- retry: getTelegramBusTransportRetryPolicy({
2134
- endpoint: expectedLeaderSocketPath,
2135
- operation: "operation",
2136
- }),
2137
- envelope: {
3007
+ const response = await sendTelegramBusOperation(
3008
+ expectedLeaderSocketPath,
3009
+ registrationTimeoutMs,
3010
+ {
2138
3011
  kind: "follower.resetThreadName",
2139
3012
  requestId: deps.createRequestId(),
2140
3013
  auth: activeAuthSecret,
@@ -2143,21 +3016,30 @@ export function createTelegramBusFollowerRegistrationRuntime<
2143
3016
  target,
2144
3017
  sentAtMs: getNowMs(),
2145
3018
  },
2146
- });
2147
- const resetName = response?.kind === "bus.ack" && response.ok &&
2148
- response.result && typeof response.result === "object" &&
3019
+ );
3020
+ const resetName =
3021
+ response?.kind === "bus.ack" &&
3022
+ response.ok &&
3023
+ response.result &&
3024
+ typeof response.result === "object" &&
2149
3025
  "threadName" in response.result &&
2150
3026
  typeof response.result.threadName === "string"
2151
- ? response.result.threadName : undefined;
3027
+ ? response.result.threadName
3028
+ : undefined;
2152
3029
  if (!resetName) {
2153
- throw new Error(response?.kind === "bus.ack"
2154
- ? response.message ?? "Telegram Workspace Thread reset was rejected."
2155
- : "Telegram Workspace Thread reset was not acknowledged.");
2156
- }
2157
- if (activeLeaderSocketPath !== expectedLeaderSocketPath ||
2158
- activeRegistrationGeneration !== expectedRegistrationGeneration ||
2159
- (deps.registrationState && deps.registrationState.getGeneration() !==
2160
- expectedRegistrationGeneration)) {
3030
+ throw new Error(
3031
+ response?.kind === "bus.ack"
3032
+ ? (response.message ??
3033
+ "Telegram Workspace Thread reset was rejected.")
3034
+ : "Telegram Workspace Thread reset was not acknowledged.",
3035
+ );
3036
+ }
3037
+ if (
3038
+ !isLeaderSessionCurrent(
3039
+ expectedLeaderSocketPath,
3040
+ expectedRegistrationGeneration,
3041
+ )
3042
+ ) {
2161
3043
  throw new Error(
2162
3044
  "Telegram Workspace Thread reset completed for a stale follower registration.",
2163
3045
  );
@@ -2195,17 +3077,14 @@ export function createTelegramBusFollowerRegistrationRuntime<
2195
3077
  const expectedRegistrationGeneration = activeRegistrationGeneration;
2196
3078
  const expectedAuthSecret = activeAuthSecret;
2197
3079
  const requestId = deps.createRequestId();
2198
- const response = await sendTelegramBusLocalEnvelope({
2199
- socketPath: expectedLeaderSocketPath,
2200
- timeoutMs: registrationTimeoutMs,
2201
- retry: getTelegramBusTransportRetryPolicy({
2202
- endpoint: expectedLeaderSocketPath,
2203
- operation: "operation",
2204
- }),
2205
- envelope: {
2206
- kind: operation === "publish"
2207
- ? "follower.publishSessionReplacement"
2208
- : "follower.settleSessionReplacement",
3080
+ const response = await sendTelegramBusOperation(
3081
+ expectedLeaderSocketPath,
3082
+ registrationTimeoutMs,
3083
+ {
3084
+ kind:
3085
+ operation === "publish"
3086
+ ? "follower.publishSessionReplacement"
3087
+ : "follower.settleSessionReplacement",
2209
3088
  requestId,
2210
3089
  auth: expectedAuthSecret,
2211
3090
  instanceId: deps.instanceId,
@@ -2213,11 +3092,13 @@ export function createTelegramBusFollowerRegistrationRuntime<
2213
3092
  intent,
2214
3093
  sentAtMs: getNowMs(),
2215
3094
  },
2216
- });
3095
+ );
2217
3096
  if (
2218
- response?.kind !== "bus.ack" || !response.ok ||
3097
+ response?.kind !== "bus.ack" ||
3098
+ !response.ok ||
2219
3099
  response.requestId !== requestId ||
2220
- !isRecord(response.result) || response.result.committed !== true
3100
+ !isRecord(response.result) ||
3101
+ response.result.committed !== true
2221
3102
  ) {
2222
3103
  throw new Error(
2223
3104
  response?.kind === "bus.ack"
@@ -2226,12 +3107,11 @@ export function createTelegramBusFollowerRegistrationRuntime<
2226
3107
  );
2227
3108
  }
2228
3109
  if (
2229
- activeLeaderSocketPath !== expectedLeaderSocketPath ||
2230
- activeRegistrationGeneration !== expectedRegistrationGeneration ||
2231
- activeAuthSecret !== expectedAuthSecret ||
2232
- (deps.registrationState &&
2233
- deps.registrationState.getGeneration() !==
2234
- expectedRegistrationGeneration)
3110
+ !isLeaderSessionCurrent(
3111
+ expectedLeaderSocketPath,
3112
+ expectedRegistrationGeneration,
3113
+ ) ||
3114
+ activeAuthSecret !== expectedAuthSecret
2235
3115
  ) {
2236
3116
  throw new Error(
2237
3117
  "Telegram session replacement completed for a stale follower registration.",
@@ -2243,14 +3123,10 @@ export function createTelegramBusFollowerRegistrationRuntime<
2243
3123
  if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
2244
3124
  return false;
2245
3125
  }
2246
- const response = await sendTelegramBusLocalEnvelope({
2247
- socketPath: activeLeaderSocketPath,
2248
- timeoutMs: registrationTimeoutMs,
2249
- retry: getTelegramBusTransportRetryPolicy({
2250
- endpoint: activeLeaderSocketPath,
2251
- operation: "operation",
2252
- }),
2253
- envelope: {
3126
+ const response = await sendTelegramBusOperation(
3127
+ activeLeaderSocketPath,
3128
+ registrationTimeoutMs,
3129
+ {
2254
3130
  kind: "follower.disconnect",
2255
3131
  requestId: deps.createRequestId(),
2256
3132
  auth: activeAuthSecret,
@@ -2258,7 +3134,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
2258
3134
  registrationGeneration: activeRegistrationGeneration,
2259
3135
  sentAtMs: getNowMs(),
2260
3136
  },
2261
- });
3137
+ );
2262
3138
  if (response?.kind === "bus.ack" && response.ok) return true;
2263
3139
  throw new Error(
2264
3140
  response?.kind === "bus.ack"
@@ -2283,28 +3159,72 @@ export function createTelegramBusFollowerPairedAdmission(deps: {
2283
3159
  return (updates, publish) => {
2284
3160
  if (updates.length !== 1) return { admitted: false };
2285
3161
  const update = updates[0]!;
2286
- const keys = Object.keys(update).filter((key) =>
2287
- key !== "update_id" && key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD);
3162
+ const keys = Object.keys(update).filter(
3163
+ (key) =>
3164
+ key !== "update_id" &&
3165
+ key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD,
3166
+ );
2288
3167
  if (keys.length !== 1) return { admitted: false };
2289
3168
  const kind = keys[0];
2290
- if (kind !== "message" && kind !== "edited_message" &&
2291
- kind !== "callback_query" && kind !== "message_reaction") return { admitted: false };
3169
+ if (
3170
+ kind !== "message" &&
3171
+ kind !== "edited_message" &&
3172
+ kind !== "callback_query" &&
3173
+ kind !== "message_reaction"
3174
+ )
3175
+ return { admitted: false };
2292
3176
  const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
2293
- if (position !== undefined && (kind !== "message" ||
2294
- (position !== "comment" && position !== "forward"))) return { admitted: false };
3177
+ if (
3178
+ position !== undefined &&
3179
+ (kind !== "message" || (position !== "comment" && position !== "forward"))
3180
+ )
3181
+ return { admitted: false };
2295
3182
  const carrier = update[kind];
2296
- if (!Number.isSafeInteger(update.update_id) || update.update_id < 0 ||
2297
- !isRecord(carrier) || carrier.pi_telegram_source_update_id !== update.update_id ||
2298
- carrier.sender_chat !== undefined || carrier.actor_chat !== undefined) return { admitted: false };
3183
+ if (
3184
+ !Number.isSafeInteger(update.update_id) ||
3185
+ update.update_id < 0 ||
3186
+ !isRecord(carrier) ||
3187
+ carrier.pi_telegram_source_update_id !== update.update_id ||
3188
+ carrier.sender_chat !== undefined ||
3189
+ carrier.actor_chat !== undefined
3190
+ )
3191
+ return { admitted: false };
2299
3192
  const sender = kind === "message_reaction" ? carrier.user : carrier.from;
2300
- if (!isRecord(sender) || typeof sender.id !== "number" || !Number.isSafeInteger(sender.id) ||
2301
- sender.id <= 0 || sender.is_bot !== false) return { admitted: false };
3193
+ if (
3194
+ !isRecord(sender) ||
3195
+ typeof sender.id !== "number" ||
3196
+ !Number.isSafeInteger(sender.id) ||
3197
+ sender.id <= 0 ||
3198
+ sender.is_bot !== false
3199
+ )
3200
+ return { admitted: false };
2302
3201
  return deps.configStore.withPairedUserAdmission(
2303
- deps.profileName, deps.tokenSha256, sender.id, publish, deps.assertExecutionCurrent,
3202
+ deps.profileName,
3203
+ deps.tokenSha256,
3204
+ sender.id,
3205
+ publish,
3206
+ deps.assertExecutionCurrent,
2304
3207
  );
2305
3208
  };
2306
3209
  }
2307
3210
 
3211
+ /** One authenticated local operation request with the shared operation retry policy; no replay semantics. */
3212
+ function sendTelegramBusOperation(
3213
+ socketPath: string,
3214
+ timeoutMs: number,
3215
+ envelope: TelegramBusEnvelope,
3216
+ ) {
3217
+ return sendTelegramBusLocalEnvelope({
3218
+ socketPath,
3219
+ timeoutMs,
3220
+ envelope,
3221
+ retry: getTelegramBusTransportRetryPolicy({
3222
+ endpoint: socketPath,
3223
+ operation: "operation",
3224
+ }),
3225
+ });
3226
+ }
3227
+
2308
3228
  export function prepareTelegramBusFollowerJournaledUpdateForExecution<
2309
3229
  TUpdate extends { message?: unknown } & Record<string, unknown>,
2310
3230
  >(
@@ -2330,7 +3250,713 @@ export function prepareTelegramBusFollowerJournaledUpdateForExecution<
2330
3250
  return prepared;
2331
3251
  }
2332
3252
 
2333
- export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps: {
3253
+ type LiveRebindCarrierEnvelope = Pick<
3254
+ Extract<TelegramBusEnvelope, { kind: "leader.settleLiveRebind" }>,
3255
+ | "operationId"
3256
+ | "recipientInstanceId"
3257
+ | "recipientSessionId"
3258
+ | "recipientRegistrationGeneration"
3259
+ | "recipientBindingKey"
3260
+ | "sourceUpdateIds"
3261
+ | "selectedCommand"
3262
+ | "preparedSource"
3263
+ >;
3264
+
3265
+ /** Copy the captured command target for the native owner; ordinary input supplies no target constraint. */
3266
+ function copyLiveRebindCommandTarget(
3267
+ saved: TelegramBusLiveRebindSaveObservation,
3268
+ ): { expectedTarget?: TelegramTarget & { threadId: number } } {
3269
+ return saved.selectedCommand
3270
+ ? { expectedTarget: { ...saved.selectedCommand.target } }
3271
+ : {};
3272
+ }
3273
+
3274
+ /** Exact operation/recipient/source agreement between a retained save and a later request; identity only, never currency. */
3275
+ function matchesLiveRebindCarrier(
3276
+ saved: TelegramBusLiveRebindSaveObservation,
3277
+ envelope: LiveRebindCarrierEnvelope,
3278
+ ): boolean {
3279
+ return (
3280
+ saved.operationId === envelope.operationId &&
3281
+ saved.recipient.instanceId === envelope.recipientInstanceId &&
3282
+ saved.recipient.sessionId === envelope.recipientSessionId &&
3283
+ saved.recipient.generation === envelope.recipientRegistrationGeneration &&
3284
+ saved.recipient.bindingKey === envelope.recipientBindingKey &&
3285
+ isDeepStrictEqual(
3286
+ saved.sourceUpdateIds,
3287
+ [...envelope.sourceUpdateIds].sort((a, b) => a - b),
3288
+ )
3289
+ );
3290
+ }
3291
+
3292
+ /** Exact branch agreement: a command record requires its saved proof; ordinary input refuses any proof. */
3293
+ function matchesLiveRebindBranch(
3294
+ saved: TelegramBusLiveRebindSaveObservation,
3295
+ status: boolean,
3296
+ envelope: LiveRebindCarrierEnvelope,
3297
+ ): boolean {
3298
+ return (
3299
+ isDeepStrictEqual(saved.selectedCommand, envelope.selectedCommand) &&
3300
+ (status
3301
+ ? isDeepStrictEqual(saved.preparedSource, envelope.preparedSource)
3302
+ : envelope.preparedSource === undefined)
3303
+ );
3304
+ }
3305
+
3306
+ /** Existing Commands/menu assembly preflights the copied input and captures independent recipient/transport owners. */
3307
+ export interface TelegramBusFollowerCommandOwner<TContext> {
3308
+ isCurrent(): boolean;
3309
+ assertRecipientCurrent(): void;
3310
+ prepare(
3311
+ readiness: TelegramLiveSourceCompletionReadiness,
3312
+ ctx: TContext,
3313
+ input: {
3314
+ target: TelegramTarget & { threadId: number };
3315
+ assertSourceCurrent(): void;
3316
+ assertRecipientCurrent(): void;
3317
+ },
3318
+ ): TelegramPreparedHeldCommand | undefined;
3319
+ }
3320
+
3321
+ /** One current warm preparation, not a prompt queue or restart recovery record. */
3322
+ export function createTelegramBusFollowerLiveRebindRuntime<TContext>(deps: {
3323
+ getAdmission(
3324
+ ctx: TContext,
3325
+ ):
3326
+ | Pick<
3327
+ TelegramUpdateAdmissionLifecycleRuntime<TContext>,
3328
+ "prepareLiveInput" | "appendBatch" | "getJournalBindingKey"
3329
+ >
3330
+ | undefined;
3331
+ applyTarget?: (
3332
+ ...args: Parameters<
3333
+ ReturnType<
3334
+ typeof createTelegramBusFollowerWorkspaceRestoreHandler<TContext>
3335
+ >
3336
+ >
3337
+ ) => ReturnType<
3338
+ ReturnType<
3339
+ typeof createTelegramBusFollowerWorkspaceRestoreHandler<TContext>
3340
+ >
3341
+ >;
3342
+ observeWork?: (
3343
+ oldTarget: TelegramTarget & { threadId: number },
3344
+ ctx: TContext,
3345
+ ) => TelegramBusLiveRebindWorkState;
3346
+ /** Pure preflight: undefined refuses before hold/append; this port never activates future recipient authority. */
3347
+ getCommandOwner?: (
3348
+ input: Readonly<
3349
+ Omit<
3350
+ Extract<TelegramBusEnvelope, { kind: "leader.prepareLiveRebind" }>,
3351
+ "kind" | "auth" | "requestId" | "sentAtMs"
3352
+ >
3353
+ >,
3354
+ ctx: TContext,
3355
+ ) => TelegramBusFollowerCommandOwner<TContext> | undefined;
3356
+ }) {
3357
+ type Status = {
3358
+ owner: TelegramBusFollowerCommandOwner<TContext>;
3359
+ current(): boolean;
3360
+ prepare: TelegramBusFollowerCommandOwner<TContext>["prepare"];
3361
+ recipient: () => void;
3362
+ attempted?: true;
3363
+ plan?: TelegramPreparedHeldCommand;
3364
+ ports?: Pick<
3365
+ TelegramPreparedHeldCommand,
3366
+ "bindCarrier" | "execute" | "inspectCompletion"
3367
+ >;
3368
+ result?: "completed" | "unknown";
3369
+ };
3370
+ let retained:
3371
+ | {
3372
+ operationId: string;
3373
+ digest: string;
3374
+ current(): boolean;
3375
+ held: TelegramLiveInputPreparation;
3376
+ ctx: TContext;
3377
+ observation: TelegramBusLiveRebindSaveObservation;
3378
+ applyIssued?: true;
3379
+ applying?: true;
3380
+ settling?: true;
3381
+ outcome?: TelegramBusLiveRebindSettlementObservation;
3382
+ status?: Status;
3383
+ }
3384
+ | undefined;
3385
+ const runtime = {
3386
+ save(
3387
+ envelope: Extract<
3388
+ TelegramBusEnvelope,
3389
+ { kind: "leader.prepareLiveRebind" }
3390
+ >,
3391
+ ctx: TContext,
3392
+ isCurrent: () => boolean,
3393
+ ): TelegramBusLiveRebindSaveObservation {
3394
+ const captured = structuredClone(envelope),
3395
+ selectedCommand = captured.selectedCommand;
3396
+ const heldMarker = selectedCommand,
3397
+ getCommandOwner = heldMarker ? deps.getCommandOwner : undefined,
3398
+ applyOwner = deps.applyTarget;
3399
+ if (
3400
+ heldMarker !== undefined &&
3401
+ (!getCommandOwner || !applyOwner || captured.updates.length !== 1)
3402
+ )
3403
+ throw new Error("Selected command native consumption is unavailable.");
3404
+ const admission = deps.getAdmission(ctx),
3405
+ prepareInput = admission?.prepareLiveInput,
3406
+ append = admission?.appendBatch;
3407
+ let status =
3408
+ retained?.operationId === captured.operationId
3409
+ ? retained.status
3410
+ : undefined;
3411
+ const current = () =>
3412
+ isCurrent() &&
3413
+ deps.getAdmission(ctx) === admission &&
3414
+ admission?.getJournalBindingKey() === captured.recipientBindingKey &&
3415
+ (!heldMarker ||
3416
+ (deps.getCommandOwner === getCommandOwner &&
3417
+ deps.applyTarget === applyOwner &&
3418
+ admission?.prepareLiveInput === prepareInput &&
3419
+ admission?.appendBatch === append &&
3420
+ !!status?.current()));
3421
+ if (
3422
+ retained?.status &&
3423
+ retained.operationId !== captured.operationId &&
3424
+ retained.outcome?.status === "released" &&
3425
+ (retained.status.result !== "completed" ||
3426
+ !retained.status.ports?.inspectCompletion.call(retained.status.plan))
3427
+ )
3428
+ throw new Error(
3429
+ "Released held command is unconfirmed; no carrier replacement.",
3430
+ );
3431
+ if (heldMarker && !status) {
3432
+ const {
3433
+ recipientInstanceId,
3434
+ recipientRegistrationGeneration,
3435
+ recipientSessionId,
3436
+ recipientBindingKey,
3437
+ operationId,
3438
+ updates,
3439
+ } = captured;
3440
+ const owner = getCommandOwner!(
3441
+ structuredClone({
3442
+ recipientInstanceId,
3443
+ recipientRegistrationGeneration,
3444
+ recipientSessionId,
3445
+ recipientBindingKey,
3446
+ operationId,
3447
+ updates,
3448
+ selectedCommand,
3449
+ }),
3450
+ ctx,
3451
+ );
3452
+ if (
3453
+ !owner ||
3454
+ typeof owner.isCurrent !== "function" ||
3455
+ typeof owner.prepare !== "function" ||
3456
+ typeof owner.assertRecipientCurrent !== "function"
3457
+ )
3458
+ throw new Error("Selected command preparation owner is unavailable.");
3459
+ const ports = {
3460
+ isCurrent: owner.isCurrent,
3461
+ prepare: owner.prepare,
3462
+ assertRecipientCurrent: owner.assertRecipientCurrent,
3463
+ };
3464
+ status = {
3465
+ owner,
3466
+ current: () =>
3467
+ Object.entries(ports).every(
3468
+ ([key, value]) => Reflect.get(owner, key) === value,
3469
+ ) &&
3470
+ (!status?.plan ||
3471
+ Object.entries(status.ports!).every(
3472
+ ([key, value]) => Reflect.get(status!.plan!, key) === value,
3473
+ )) &&
3474
+ ports.isCurrent.call(owner),
3475
+ prepare: ports.prepare,
3476
+ recipient: ports.assertRecipientCurrent,
3477
+ };
3478
+ }
3479
+ if (!admission?.prepareLiveInput || !current())
3480
+ throw new Error("Live-rebind recipient admission is unavailable.");
3481
+ const digest = createHash("sha256")
3482
+ .update(JSON.stringify(captured.updates))
3483
+ .digest("hex");
3484
+ if (retained?.status && !retained.current())
3485
+ throw new Error(
3486
+ "Prepared command authority changed; no replacement adoption.",
3487
+ );
3488
+ if (
3489
+ retained &&
3490
+ (!retained.current() ||
3491
+ (retained.operationId !== envelope.operationId &&
3492
+ (retained.outcome?.status === "released" ||
3493
+ retained.outcome?.status === "discarded")))
3494
+ )
3495
+ retained = undefined;
3496
+ if (
3497
+ heldMarker &&
3498
+ retained &&
3499
+ (retained.ctx !== ctx ||
3500
+ retained.observation.recipient.instanceId !==
3501
+ captured.recipientInstanceId ||
3502
+ retained.observation.recipient.sessionId !==
3503
+ captured.recipientSessionId ||
3504
+ retained.observation.recipient.generation !==
3505
+ captured.recipientRegistrationGeneration ||
3506
+ retained.observation.recipient.bindingKey !==
3507
+ captured.recipientBindingKey)
3508
+ )
3509
+ throw new Error("Selected command saved recipient changed.");
3510
+ if (
3511
+ retained &&
3512
+ (retained.operationId !== captured.operationId ||
3513
+ retained.digest !== digest ||
3514
+ !isDeepStrictEqual(
3515
+ retained.observation.selectedCommand,
3516
+ selectedCommand,
3517
+ ))
3518
+ )
3519
+ throw new Error(
3520
+ "Another live-rebind input or branch is already prepared.",
3521
+ );
3522
+ if (!retained) {
3523
+ const held = admission.prepareLiveInput(
3524
+ ctx,
3525
+ captured.updates.map((update) => update.update_id),
3526
+ current,
3527
+ );
3528
+ if (!held)
3529
+ throw new Error("Live-rebind recipient input cannot be prepared.");
3530
+ if (heldMarker && !held.canPrepareStatusConsumption?.()) {
3531
+ held.cancelEmpty();
3532
+ throw new Error(
3533
+ "Selected command native consumption is unavailable.",
3534
+ );
3535
+ }
3536
+ retained = {
3537
+ operationId: captured.operationId,
3538
+ digest,
3539
+ current,
3540
+ held,
3541
+ ctx,
3542
+ ...(status ? { status } : {}),
3543
+ observation: {
3544
+ operationId: captured.operationId,
3545
+ recipient: {
3546
+ instanceId: captured.recipientInstanceId,
3547
+ sessionId: captured.recipientSessionId,
3548
+ generation: captured.recipientRegistrationGeneration,
3549
+ bindingKey: captured.recipientBindingKey,
3550
+ },
3551
+ sourceUpdateIds: [...held.sourceUpdateIds].sort((a, b) => a - b),
3552
+ ...(selectedCommand
3553
+ ? { selectedCommand: structuredClone(selectedCommand) }
3554
+ : {}),
3555
+ status: "saved",
3556
+ },
3557
+ };
3558
+ try {
3559
+ admission.appendBatch(captured.updates);
3560
+ } catch (error) {
3561
+ if (!held.confirmSaved()) throw error;
3562
+ }
3563
+ }
3564
+ if (!current() || !retained.held.confirmSaved())
3565
+ throw new Error("Live-rebind recipient save is unconfirmed.");
3566
+ if (status && !status.plan) {
3567
+ if (status.attempted)
3568
+ throw new Error(
3569
+ "Selected command preparation is unconfirmed; no replay.",
3570
+ );
3571
+ status.attempted = true;
3572
+ const record = retained,
3573
+ readiness = record.held.prepareSourceCompletion?.();
3574
+ const assertSourceCurrent = () => {
3575
+ if (
3576
+ retained !== record ||
3577
+ !record.current() ||
3578
+ !record.held.isOwnerCurrent()
3579
+ )
3580
+ throw new Error("Selected command source owner changed.");
3581
+ };
3582
+ const plan =
3583
+ readiness &&
3584
+ status.prepare.call(status.owner, readiness, ctx, {
3585
+ target: { ...heldMarker!.target },
3586
+ assertSourceCurrent,
3587
+ assertRecipientCurrent: status.recipient,
3588
+ });
3589
+ if (
3590
+ !plan ||
3591
+ !current() ||
3592
+ !record.held.confirmSaved() ||
3593
+ !isDeepStrictEqual(plan.source, readiness!.snapshot.source) ||
3594
+ [plan.bindCarrier, plan.execute, plan.inspectCompletion].some(
3595
+ (port) => typeof port !== "function",
3596
+ )
3597
+ )
3598
+ throw new Error("Selected command plan is unavailable.");
3599
+ status.plan = plan;
3600
+ status.ports = {
3601
+ bindCarrier: plan.bindCarrier,
3602
+ execute: plan.execute,
3603
+ inspectCompletion: plan.inspectCompletion,
3604
+ };
3605
+ const continuation = {
3606
+ source: plan.source,
3607
+ assertCurrent() {
3608
+ assertSourceCurrent();
3609
+ if (record.outcome?.status !== "released")
3610
+ throw new Error(
3611
+ "Selected command has no confirmed peer release.",
3612
+ );
3613
+ status!.recipient.call(status!.owner);
3614
+ },
3615
+ bindCarrier: status.ports.bindCarrier.bind(plan),
3616
+ async execute() {
3617
+ status!.result = "unknown";
3618
+ const completed = await status!.ports!.execute.call(plan);
3619
+ if (completed && record.current() && record.held.isOwnerCurrent())
3620
+ status!.result = "completed";
3621
+ return completed;
3622
+ },
3623
+ };
3624
+ if (
3625
+ !record.held.prepareStatusConsumption?.(continuation) ||
3626
+ !current()
3627
+ ) {
3628
+ delete status.plan;
3629
+ delete status.ports;
3630
+ throw new Error("Selected command consumption is unconfirmed.");
3631
+ }
3632
+ record.observation.preparedSource = { ...plan.source };
3633
+ }
3634
+ return structuredClone(retained.observation);
3635
+ },
3636
+ async apply(
3637
+ envelope: Extract<
3638
+ TelegramBusEnvelope,
3639
+ { kind: "leader.applyLiveRebind" }
3640
+ >,
3641
+ ctx: TContext,
3642
+ isCurrent: () => boolean,
3643
+ ): Promise<TelegramBusLiveRebindApplyObservation> {
3644
+ const record = retained;
3645
+ if (envelope.selectedCommand !== undefined && !record?.status?.plan)
3646
+ throw new Error("Selected command native consumption is unavailable.");
3647
+ const current = () =>
3648
+ retained === record &&
3649
+ !!record &&
3650
+ record.ctx === ctx &&
3651
+ record.current() &&
3652
+ isCurrent() &&
3653
+ record.held.isCurrent() &&
3654
+ matchesLiveRebindBranch(record.observation, !!record.status, envelope);
3655
+ if (
3656
+ !record ||
3657
+ !current() ||
3658
+ !record.held.confirmSaved() ||
3659
+ !deps.applyTarget ||
3660
+ record.applying ||
3661
+ record.settling ||
3662
+ !matchesLiveRebindCarrier(record.observation, envelope)
3663
+ )
3664
+ throw new Error(
3665
+ "Live-rebind apply has no exact current saved carrier.",
3666
+ );
3667
+ const mode =
3668
+ envelope.mode === "apply" && !record.applyIssued ? "apply" : "inspect";
3669
+ if (mode === "apply") record.applyIssued = true;
3670
+ record.applying = true;
3671
+ try {
3672
+ const observation = await deps.applyTarget(
3673
+ {
3674
+ operationId: record.operationId,
3675
+ registrationGeneration: envelope.recipientRegistrationGeneration,
3676
+ mode,
3677
+ liveRebind: {
3678
+ sourceUpdateIds: record.observation.sourceUpdateIds,
3679
+ isCurrent: current,
3680
+ ...copyLiveRebindCommandTarget(record.observation),
3681
+ },
3682
+ },
3683
+ ctx,
3684
+ );
3685
+ if (
3686
+ !current() ||
3687
+ !record.held.confirmSaved() ||
3688
+ observation.operationId !== record.operationId ||
3689
+ observation.recipient.instanceId !== envelope.recipientInstanceId ||
3690
+ observation.recipient.sessionId !== envelope.recipientSessionId ||
3691
+ observation.recipient.generation !==
3692
+ envelope.recipientRegistrationGeneration
3693
+ )
3694
+ throw new Error("Live-rebind apply observation changed.");
3695
+ return {
3696
+ ...structuredClone(record.observation),
3697
+ target: { ...observation.target },
3698
+ slot: observation.slot,
3699
+ status: observation.ready ? "applied" : "saved",
3700
+ };
3701
+ } finally {
3702
+ delete record.applying;
3703
+ }
3704
+ },
3705
+ async settle(
3706
+ envelope: Extract<
3707
+ TelegramBusEnvelope,
3708
+ { kind: "leader.settleLiveRebind" }
3709
+ >,
3710
+ ctx: TContext,
3711
+ isCurrent: () => boolean,
3712
+ ): Promise<
3713
+ | TelegramBusLiveRebindSettlementObservation
3714
+ | TelegramBusLiveRebindWorkObservation
3715
+ | TelegramBusLiveRebindCommandObservation
3716
+ > {
3717
+ if (envelope.mode === "observe-command")
3718
+ return inspectCommandCompletion(envelope, ctx, isCurrent);
3719
+ const record = retained,
3720
+ status = record?.status,
3721
+ plan = status?.plan;
3722
+ if (envelope.selectedCommand !== undefined && !plan)
3723
+ throw new Error("Selected command native consumption is unavailable.");
3724
+ // Only the marked read-only held work sample may outlive the source worker, through the captured recipient owner.
3725
+ const commandWork =
3726
+ envelope.mode === "observe" && envelope.selectedCommand !== undefined;
3727
+ const recipientCurrent = () => {
3728
+ if (
3729
+ record?.status !== status ||
3730
+ status?.plan !== plan ||
3731
+ !status?.current()
3732
+ )
3733
+ return false;
3734
+ try {
3735
+ status.recipient.call(status.owner);
3736
+ return true;
3737
+ } catch {
3738
+ return false;
3739
+ }
3740
+ };
3741
+ const current = () =>
3742
+ retained === record &&
3743
+ !!record &&
3744
+ record.ctx === ctx &&
3745
+ record.current() &&
3746
+ isCurrent() &&
3747
+ (commandWork ? recipientCurrent() : record.held.isOwnerCurrent()) &&
3748
+ matchesLiveRebindBranch(record.observation, !!record.status, envelope);
3749
+ if (
3750
+ !record ||
3751
+ !current() ||
3752
+ record.applying ||
3753
+ record.settling ||
3754
+ !matchesLiveRebindCarrier(record.observation, envelope)
3755
+ )
3756
+ throw new Error("Live-rebind settlement has no exact current carrier.");
3757
+ if (envelope.mode === "observe") {
3758
+ if (
3759
+ record.outcome?.status !== "released" ||
3760
+ !deps.applyTarget ||
3761
+ !deps.observeWork ||
3762
+ !envelope.oldTarget
3763
+ )
3764
+ throw new Error(
3765
+ "Live-rebind work observation has no released carrier or observer.",
3766
+ );
3767
+ const oldTarget = { ...envelope.oldTarget };
3768
+ let work: TelegramBusLiveRebindWorkState | undefined;
3769
+ record.settling = true;
3770
+ try {
3771
+ await deps.applyTarget(
3772
+ {
3773
+ operationId: record.operationId,
3774
+ registrationGeneration: envelope.recipientRegistrationGeneration,
3775
+ mode: "inspect",
3776
+ liveRebind: {
3777
+ sourceUpdateIds: record.observation.sourceUpdateIds,
3778
+ isCurrent: current,
3779
+ ...copyLiveRebindCommandTarget(record.observation),
3780
+ observeWork(canonicalOldTarget) {
3781
+ if (
3782
+ !current() ||
3783
+ !isDeepStrictEqual(canonicalOldTarget, oldTarget)
3784
+ )
3785
+ throw new Error("Live-rebind old work target changed.");
3786
+ work = structuredClone(
3787
+ deps.observeWork!(canonicalOldTarget, ctx),
3788
+ );
3789
+ if (
3790
+ !current() ||
3791
+ !isDeepStrictEqual(canonicalOldTarget, oldTarget)
3792
+ )
3793
+ throw new Error(
3794
+ "Live-rebind work observer authority or target changed.",
3795
+ );
3796
+ },
3797
+ },
3798
+ },
3799
+ ctx,
3800
+ );
3801
+ if (!current() || !work)
3802
+ throw new Error("Live-rebind work observation is unconfirmed.");
3803
+ // Unfinished or unconfirmed selected execution stays unknown; later settled delivery cannot clear it.
3804
+ if (commandWork && status!.result !== "completed")
3805
+ work.unknown = true;
3806
+ if (!current())
3807
+ throw new Error("Live-rebind work observer authority changed.");
3808
+ return {
3809
+ ...structuredClone(record.observation),
3810
+ status: "observed",
3811
+ oldTarget,
3812
+ work,
3813
+ };
3814
+ } finally {
3815
+ delete record.settling;
3816
+ }
3817
+ }
3818
+ const action = envelope.mode;
3819
+ if (record.outcome) {
3820
+ if (record.outcome.action !== envelope.mode)
3821
+ throw new Error("Live-rebind input already has another disposition.");
3822
+ return structuredClone(record.outcome);
3823
+ }
3824
+ record.settling = true;
3825
+ const observation = (
3826
+ status: TelegramBusLiveRebindSettlementObservation["status"],
3827
+ ): TelegramBusLiveRebindSettlementObservation => ({
3828
+ ...structuredClone(record.observation),
3829
+ action,
3830
+ status,
3831
+ });
3832
+ try {
3833
+ if (envelope.mode === "discard") {
3834
+ record.outcome = observation("unknown");
3835
+ const status = record.held.discardSaved(current);
3836
+ record.outcome =
3837
+ status === "protected" ? undefined : observation(status);
3838
+ if (!current())
3839
+ throw new Error("Live-rebind disposal authority changed.");
3840
+ return observation(status);
3841
+ }
3842
+ if (
3843
+ !record.held.isCurrent() ||
3844
+ !record.held.confirmSaved() ||
3845
+ !deps.applyTarget
3846
+ )
3847
+ throw new Error("Live-rebind input cannot be released.");
3848
+ await deps.applyTarget(
3849
+ {
3850
+ operationId: record.operationId,
3851
+ registrationGeneration: envelope.recipientRegistrationGeneration,
3852
+ mode: "inspect",
3853
+ liveRebind: {
3854
+ sourceUpdateIds: record.observation.sourceUpdateIds,
3855
+ isCurrent: current,
3856
+ ...copyLiveRebindCommandTarget(record.observation),
3857
+ release(canRelease) {
3858
+ record.outcome = observation("unknown");
3859
+ record.outcome = record.held.release(
3860
+ () => current() && canRelease(),
3861
+ )
3862
+ ? observation("released")
3863
+ : undefined;
3864
+ },
3865
+ },
3866
+ },
3867
+ ctx,
3868
+ );
3869
+ if (!current())
3870
+ throw new Error("Live-rebind release authority changed.");
3871
+ return structuredClone(record.outcome ?? observation("protected"));
3872
+ } finally {
3873
+ delete record.settling;
3874
+ }
3875
+ },
3876
+ };
3877
+ async function inspectCommandCompletion(
3878
+ envelope: Extract<TelegramBusEnvelope, { kind: "leader.settleLiveRebind" }>,
3879
+ ctx: TContext,
3880
+ isCurrent: () => boolean,
3881
+ ): Promise<TelegramBusLiveRebindCommandObservation> {
3882
+ const record = retained;
3883
+ const current = () =>
3884
+ retained === record &&
3885
+ !!record?.status?.plan &&
3886
+ record.ctx === ctx &&
3887
+ record.current() &&
3888
+ record.held.isOwnerCurrent() &&
3889
+ isCurrent() &&
3890
+ matchesLiveRebindBranch(record.observation, true, envelope);
3891
+ if (
3892
+ !record?.status?.plan ||
3893
+ !deps.applyTarget ||
3894
+ !current() ||
3895
+ record.outcome?.status !== "released" ||
3896
+ record.applying ||
3897
+ record.settling ||
3898
+ !matchesLiveRebindCarrier(record.observation, envelope)
3899
+ )
3900
+ throw new Error(
3901
+ "Selected command completion has no exact released carrier.",
3902
+ );
3903
+ record.settling = true;
3904
+ try {
3905
+ const assertRecipient = () => {
3906
+ record.status!.recipient.call(record.status!.owner);
3907
+ if (!current())
3908
+ throw new Error("Selected command completion recipient changed.");
3909
+ };
3910
+ assertRecipient();
3911
+ const observed = await deps.applyTarget(
3912
+ {
3913
+ operationId: record.operationId,
3914
+ registrationGeneration: record.observation.recipient.generation,
3915
+ mode: "inspect",
3916
+ liveRebind: {
3917
+ sourceUpdateIds: record.observation.sourceUpdateIds,
3918
+ expectedTarget: { ...record.observation.selectedCommand!.target },
3919
+ isCurrent: current,
3920
+ },
3921
+ },
3922
+ ctx,
3923
+ );
3924
+ if (!observed.ready || !current())
3925
+ throw new Error("Selected command completion target is unconfirmed.");
3926
+ assertRecipient();
3927
+ const sourceAck = record.status.ports!.inspectCompletion.call(
3928
+ record.status.plan,
3929
+ );
3930
+ assertRecipient();
3931
+ if (
3932
+ sourceAck &&
3933
+ !isDeepStrictEqual(sourceAck, record.observation.preparedSource)
3934
+ )
3935
+ throw new Error("Selected command completion exact ACK changed.");
3936
+ const settled = {
3937
+ preparedSource: { ...record.observation.preparedSource! },
3938
+ command: record.status.result ?? ("not-issued" as const),
3939
+ ...(sourceAck ? { sourceAck: { ...sourceAck } } : {}),
3940
+ };
3941
+ const { selectedCommand } = record.observation;
3942
+ if (envelope.mode !== "observe-command" || !selectedCommand)
3943
+ throw new Error("Held completion mode does not match its marker.");
3944
+ return {
3945
+ ...structuredClone(record.observation),
3946
+ status: "command-observed",
3947
+ selectedCommand: structuredClone(selectedCommand),
3948
+ ...settled,
3949
+ };
3950
+ } finally {
3951
+ delete record.settling;
3952
+ }
3953
+ }
3954
+ return runtime;
3955
+ }
3956
+
3957
+ export function createTelegramBusFollowerDurableAdmissionRuntime<
3958
+ TContext,
3959
+ >(deps: {
2334
3960
  journal: {
2335
3961
  appendBatch(
2336
3962
  updates: readonly ({ update_id: number } & Record<string, unknown>)[],
@@ -2344,10 +3970,17 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2344
3970
  // The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
2345
3971
  const recentDeliveries = new Map<string, number>();
2346
3972
  const getNowMs = deps.getNowMs ?? Date.now;
2347
- const limit = deps.recentDeliveryLimit ?? { maxAgeMs: 24 * 60 * 60 * 1000, maxEntries: 4096 };
3973
+ const limit = deps.recentDeliveryLimit ?? {
3974
+ maxAgeMs: 24 * 60 * 60 * 1000,
3975
+ maxEntries: 4096,
3976
+ };
2348
3977
  const pruneRecentDeliveries = (nowMs: number): void => {
2349
3978
  for (const [deliveryId, admittedAtMs] of recentDeliveries) {
2350
- if (recentDeliveries.size <= limit.maxEntries && nowMs - admittedAtMs < limit.maxAgeMs) break;
3979
+ if (
3980
+ recentDeliveries.size <= limit.maxEntries &&
3981
+ nowMs - admittedAtMs < limit.maxAgeMs
3982
+ )
3983
+ break;
2351
3984
  recentDeliveries.delete(deliveryId);
2352
3985
  }
2353
3986
  };
@@ -2355,11 +3988,14 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2355
3988
  async admit(envelope, ctx) {
2356
3989
  const delivery = envelope.delivery;
2357
3990
  if (!delivery) {
2358
- throw new Error("Telegram follower durable admission requires delivery identity.");
3991
+ throw new Error(
3992
+ "Telegram follower durable admission requires delivery identity.",
3993
+ );
2359
3994
  }
2360
- if (envelope.kind === "leader.wakeInputCustody") throw new Error(
2361
- "Telegram follower custody wake cannot use executable-copy admission.",
2362
- );
3995
+ if (envelope.kind === "leader.wakeInputCustody")
3996
+ throw new Error(
3997
+ "Telegram follower custody wake cannot use executable-copy admission.",
3998
+ );
2363
3999
  const expected = createTelegramBusFollowerDeliveryIdentity({
2364
4000
  kind: envelope.kind,
2365
4001
  recipientBindingKey: delivery.recipientBindingKey,
@@ -2378,7 +4014,9 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2378
4014
  !isRecord(carrier) ||
2379
4015
  carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId
2380
4016
  ) {
2381
- throw new Error("Telegram follower delivery source update id mismatch.");
4017
+ throw new Error(
4018
+ "Telegram follower delivery source update id mismatch.",
4019
+ );
2382
4020
  }
2383
4021
  const update = {
2384
4022
  update_id: delivery.sourceUpdateId,
@@ -2413,89 +4051,154 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2413
4051
  }
2414
4052
 
2415
4053
  export interface TelegramBusFollowerInputCustodyBundle<TContext> {
2416
- acceptHandoff(input: {
2417
- sourceRecoveryKey: string;
2418
- recipientBindingKey: string;
2419
- source: { journalBindingKey: string; tokenSha256: string; updateId: number };
2420
- handoffId: string;
2421
- }, ctx: TContext): unknown;
2422
- wakeSource(input: TelegramBusFollowerDurableAdmissionResult & {
2423
- recipientBindingKey: string;
2424
- sourceRecoveryKey: string;
2425
- sourceClaim: { acquisitionId: string; handoffId: string };
2426
- }, ctx: TContext): void;
4054
+ acceptHandoff(
4055
+ input: {
4056
+ sourceRecoveryKey: string;
4057
+ recipientBindingKey: string;
4058
+ source: {
4059
+ journalBindingKey: string;
4060
+ tokenSha256: string;
4061
+ updateId: number;
4062
+ };
4063
+ handoffId: string;
4064
+ },
4065
+ ctx: TContext,
4066
+ ): unknown;
4067
+ wakeSource(
4068
+ input: TelegramBusFollowerDurableAdmissionResult & {
4069
+ recipientBindingKey: string;
4070
+ sourceRecoveryKey: string;
4071
+ sourceClaim: { acquisitionId: string; handoffId: string };
4072
+ },
4073
+ ctx: TContext,
4074
+ ): void;
2427
4075
  resolveForwardReference(input: {
2428
- sourceUpdateId: number; recipientBindingKey: string;
2429
- }): { sourceRecoveryKey: string; source: { updateId: number; owner: {
2430
- acquisitionId: string; handoffId: string } } } | undefined;
4076
+ sourceUpdateId: number;
4077
+ recipientBindingKey: string;
4078
+ }):
4079
+ | {
4080
+ sourceRecoveryKey: string;
4081
+ source: {
4082
+ updateId: number;
4083
+ owner: {
4084
+ acquisitionId: string;
4085
+ handoffId: string;
4086
+ };
4087
+ };
4088
+ }
4089
+ | undefined;
2431
4090
  }
2432
4091
 
2433
4092
  export function createTelegramBusFollowerInputCustodyPorts<TContext>(deps: {
2434
- getInputCustodyBus(): TelegramBusFollowerInputCustodyBundle<TContext> | undefined;
4093
+ getInputCustodyBus():
4094
+ TelegramBusFollowerInputCustodyBundle<TContext> | undefined;
2435
4095
  }) {
2436
4096
  const getRequired = (): TelegramBusFollowerInputCustodyBundle<TContext> => {
2437
4097
  const bundle = deps.getInputCustodyBus();
2438
- if (!bundle) throw new Error("Telegram input custody bus binding is unavailable.");
4098
+ if (!bundle)
4099
+ throw new Error("Telegram input custody bus binding is unavailable.");
2439
4100
  return bundle;
2440
4101
  };
2441
4102
  return {
2442
4103
  isSourceReferenceAdmissionEnabled: () => Boolean(deps.getInputCustodyBus()),
2443
4104
  handleInputCustodyHandoff(
2444
- envelope: Extract<TelegramBusEnvelope, { kind: "leader.offerInputCustodyHandoff" }>,
4105
+ envelope: Extract<
4106
+ TelegramBusEnvelope,
4107
+ { kind: "leader.offerInputCustodyHandoff" }
4108
+ >,
2445
4109
  ctx: TContext,
2446
4110
  ) {
2447
- return getRequired().acceptHandoff({ sourceRecoveryKey: envelope.sourceRecoveryKey,
2448
- recipientBindingKey: envelope.recipientBindingKey,
2449
- source: envelope.source, handoffId: envelope.handoffId }, ctx);
4111
+ return getRequired().acceptHandoff(
4112
+ {
4113
+ sourceRecoveryKey: envelope.sourceRecoveryKey,
4114
+ recipientBindingKey: envelope.recipientBindingKey,
4115
+ source: envelope.source,
4116
+ handoffId: envelope.handoffId,
4117
+ },
4118
+ ctx,
4119
+ );
2450
4120
  },
2451
- sourceReferenceAdmission: createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>({
2452
- wakeSource(input, ctx) { getRequired().wakeSource(input, ctx); },
2453
- }),
4121
+ sourceReferenceAdmission:
4122
+ createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>({
4123
+ wakeSource(input, ctx) {
4124
+ getRequired().wakeSource(input, ctx);
4125
+ },
4126
+ }),
2454
4127
  resolveInputCustodyReference(input: {
2455
- sourceUpdateId: number; recipientBindingKey: string;
4128
+ sourceUpdateId: number;
4129
+ recipientBindingKey: string;
2456
4130
  }) {
2457
4131
  return deps.getInputCustodyBus()?.resolveForwardReference(input);
2458
4132
  },
2459
4133
  };
2460
4134
  }
2461
4135
 
2462
- export function createTelegramBusFollowerSourceReferenceAdmissionRuntime<TContext>(deps: {
2463
- wakeSource: (input: TelegramBusFollowerDurableAdmissionResult & {
2464
- recipientBindingKey: string;
2465
- sourceRecoveryKey: string;
2466
- sourceClaim: { acquisitionId: string; handoffId: string };
2467
- }, ctx: TContext) => Promise<void> | void;
4136
+ export function createTelegramBusFollowerSourceReferenceAdmissionRuntime<
4137
+ TContext,
4138
+ >(deps: {
4139
+ wakeSource: (
4140
+ input: TelegramBusFollowerDurableAdmissionResult & {
4141
+ recipientBindingKey: string;
4142
+ sourceRecoveryKey: string;
4143
+ sourceClaim: { acquisitionId: string; handoffId: string };
4144
+ },
4145
+ ctx: TContext,
4146
+ ) => Promise<void> | void;
2468
4147
  }): TelegramBusFollowerDurableAdmissionPort<TContext> {
2469
4148
  return {
2470
4149
  async admit(envelope, ctx) {
2471
4150
  const delivery = envelope.delivery;
2472
- if (!delivery) throw new Error(
2473
- "Telegram follower source-reference admission requires delivery identity.",
2474
- );
2475
- if (!delivery.sourceRecoveryKey) throw new Error(
2476
- "Telegram follower source-reference admission requires a recovery key.",
2477
- );
2478
- if (!delivery.sourceClaim) throw new Error(
2479
- "Telegram follower source-reference admission requires exact claim evidence.",
2480
- );
2481
- const expected = createTelegramBusFollowerDeliveryIdentity({ kind: envelope.kind,
4151
+ if (!delivery)
4152
+ throw new Error(
4153
+ "Telegram follower source-reference admission requires delivery identity.",
4154
+ );
4155
+ if (!delivery.sourceRecoveryKey)
4156
+ throw new Error(
4157
+ "Telegram follower source-reference admission requires a recovery key.",
4158
+ );
4159
+ if (!delivery.sourceClaim)
4160
+ throw new Error(
4161
+ "Telegram follower source-reference admission requires exact claim evidence.",
4162
+ );
4163
+ const expected = createTelegramBusFollowerDeliveryIdentity({
4164
+ kind: envelope.kind,
2482
4165
  recipientBindingKey: delivery.recipientBindingKey,
2483
- sourceUpdateId: delivery.sourceUpdateId });
2484
- if (expected.deliveryId !== delivery.deliveryId) throw new Error(
2485
- "Invalid Telegram follower source-reference delivery id.",
4166
+ sourceUpdateId: delivery.sourceUpdateId,
4167
+ });
4168
+ if (expected.deliveryId !== delivery.deliveryId)
4169
+ throw new Error(
4170
+ "Invalid Telegram follower source-reference delivery id.",
4171
+ );
4172
+ const carrier =
4173
+ envelope.kind === "leader.wakeInputCustody"
4174
+ ? undefined
4175
+ : envelope.kind === "leader.forwardCallback"
4176
+ ? envelope.query
4177
+ : envelope.kind === "leader.forwardReaction"
4178
+ ? envelope.reactionUpdate
4179
+ : envelope.message;
4180
+ if (
4181
+ envelope.kind !== "leader.wakeInputCustody" &&
4182
+ (!isRecord(carrier) ||
4183
+ carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId)
4184
+ )
4185
+ throw new Error(
4186
+ "Telegram follower source-reference update id mismatch.",
4187
+ );
4188
+ await deps.wakeSource(
4189
+ {
4190
+ deliveryId: delivery.deliveryId,
4191
+ sourceUpdateId: delivery.sourceUpdateId,
4192
+ recipientBindingKey: delivery.recipientBindingKey,
4193
+ sourceRecoveryKey: delivery.sourceRecoveryKey,
4194
+ sourceClaim: { ...delivery.sourceClaim },
4195
+ },
4196
+ ctx,
2486
4197
  );
2487
- const carrier = envelope.kind === "leader.wakeInputCustody" ? undefined
2488
- : envelope.kind === "leader.forwardCallback" ? envelope.query
2489
- : envelope.kind === "leader.forwardReaction" ? envelope.reactionUpdate : envelope.message;
2490
- if (envelope.kind !== "leader.wakeInputCustody" &&
2491
- (!isRecord(carrier) || carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId))
2492
- throw new Error("Telegram follower source-reference update id mismatch.");
2493
- await deps.wakeSource({ deliveryId: delivery.deliveryId,
4198
+ return {
4199
+ deliveryId: delivery.deliveryId,
2494
4200
  sourceUpdateId: delivery.sourceUpdateId,
2495
- recipientBindingKey: delivery.recipientBindingKey,
2496
- sourceRecoveryKey: delivery.sourceRecoveryKey,
2497
- sourceClaim: { ...delivery.sourceClaim } }, ctx);
2498
- return { deliveryId: delivery.deliveryId, sourceUpdateId: delivery.sourceUpdateId };
4201
+ };
2499
4202
  },
2500
4203
  };
2501
4204
  }
@@ -2527,50 +4230,184 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2527
4230
  envelope.kind !== "leader.wakeInputCustody" &&
2528
4231
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
2529
4232
  envelope.kind !== "leader.workspaceRestore" &&
4233
+ envelope.kind !== "leader.prepareLiveRebind" &&
4234
+ envelope.kind !== "leader.applyLiveRebind" &&
4235
+ envelope.kind !== "leader.settleLiveRebind" &&
2530
4236
  envelope.kind !== "leader.offerQueueHandoff") ||
2531
4237
  envelope.recipientInstanceId !== deps.instanceId
2532
4238
  ) {
2533
- return {
2534
- kind: "bus.ack",
2535
- requestId: envelope.requestId,
2536
- ok: false,
2537
- message: "Telegram bus receiver cannot handle this envelope.",
2538
- };
4239
+ return rejectTelegramBusRequest(
4240
+ envelope.requestId,
4241
+ "Telegram bus receiver cannot handle this envelope.",
4242
+ );
2539
4243
  }
2540
4244
  const registrationGeneration = deps.getRegistrationGeneration();
2541
4245
  if (
2542
4246
  !registrationGeneration ||
2543
4247
  envelope.recipientRegistrationGeneration !== registrationGeneration
2544
4248
  ) {
2545
- return {
2546
- kind: "bus.ack",
2547
- requestId: envelope.requestId,
2548
- ok: false,
2549
- message: "Stale Telegram bus follower registration generation.",
2550
- };
4249
+ return rejectTelegramBusRequest(
4250
+ envelope.requestId,
4251
+ "Stale Telegram bus follower registration generation.",
4252
+ );
4253
+ }
4254
+ if (
4255
+ envelope.kind === "leader.prepareLiveRebind" ||
4256
+ envelope.kind === "leader.applyLiveRebind" ||
4257
+ envelope.kind === "leader.settleLiveRebind"
4258
+ ) {
4259
+ const apply = envelope.kind !== "leader.prepareLiveRebind",
4260
+ settle = envelope.kind === "leader.settleLiveRebind";
4261
+ const commandInput = envelope.selectedCommand,
4262
+ commandEnabled = deps.isLiveRebindCommandSetEnabled;
4263
+ const required = [
4264
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SAVE,
4265
+ ...(apply ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY] : []),
4266
+ ...(settle ? [TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE] : []),
4267
+ ...(commandInput
4268
+ ? [
4269
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_APPLY,
4270
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_SETTLE,
4271
+ TELEGRAM_BUS_CAPABILITY_LIVE_REBIND_COMMAND_SET,
4272
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
4273
+ ]
4274
+ : []),
4275
+ ];
4276
+ const ctx = deps.getContext(),
4277
+ sessionId = deps.getSessionId?.(),
4278
+ bindingKey = deps.getLiveRebindJournalBindingKey?.();
4279
+ const protocol = structuredClone(deps.getLeaderProtocol?.()),
4280
+ localProtocol = structuredClone(deps.getLocalProtocol?.());
4281
+ const current = () =>
4282
+ !!authSecret &&
4283
+ deps.getAuthSecret?.() === authSecret &&
4284
+ deps.getContext() === ctx &&
4285
+ deps.getRegistrationGeneration() === registrationGeneration &&
4286
+ deps.getSessionId?.() === sessionId &&
4287
+ deps.getLiveRebindJournalBindingKey?.() === bindingKey &&
4288
+ deps.isLiveRebindSaveEnabled?.() === true &&
4289
+ (!apply || deps.isLiveRebindApplyEnabled?.() === true) &&
4290
+ (!settle || deps.isLiveRebindSettleEnabled?.() === true) &&
4291
+ (!commandInput ||
4292
+ (typeof commandEnabled === "function" &&
4293
+ deps.isLiveRebindCommandSetEnabled === commandEnabled &&
4294
+ commandEnabled())) &&
4295
+ isDeepStrictEqual(deps.getLeaderProtocol?.(), protocol) &&
4296
+ isDeepStrictEqual(deps.getLocalProtocol?.(), localProtocol);
4297
+ if (
4298
+ !authSecret ||
4299
+ !ctx ||
4300
+ !bindingKey ||
4301
+ sessionId !== envelope.recipientSessionId ||
4302
+ bindingKey !== envelope.recipientBindingKey ||
4303
+ !required.every(
4304
+ (cap) =>
4305
+ hasTelegramBusCapability(protocol, cap) &&
4306
+ hasTelegramBusCapability(localProtocol, cap),
4307
+ ) ||
4308
+ !localProtocol ||
4309
+ !getTelegramBusProtocolCompatibility({
4310
+ local: localProtocol,
4311
+ remote: protocol,
4312
+ }).compatible ||
4313
+ !current() ||
4314
+ (settle
4315
+ ? !deps.handleLiveRebindSettle
4316
+ : apply
4317
+ ? !deps.handleLiveRebindApply
4318
+ : !deps.handleLiveRebindSave)
4319
+ )
4320
+ return rejectTelegramBusRequest(
4321
+ envelope.requestId,
4322
+ "Live-rebind capability or recipient authority is unavailable.",
4323
+ );
4324
+ try {
4325
+ const result =
4326
+ envelope.kind === "leader.settleLiveRebind"
4327
+ ? await deps.handleLiveRebindSettle!(envelope, ctx, current)
4328
+ : envelope.kind === "leader.applyLiveRebind"
4329
+ ? await deps.handleLiveRebindApply!(envelope, ctx, current)
4330
+ : await deps.handleLiveRebindSave!(envelope, ctx, current);
4331
+ if (!current())
4332
+ throw new Error("Live-rebind receiver authority changed.");
4333
+ return {
4334
+ kind: "bus.ack",
4335
+ requestId: envelope.requestId,
4336
+ ok: true,
4337
+ result,
4338
+ };
4339
+ } catch (error) {
4340
+ deps.recordRuntimeEvent?.("bus", error, {
4341
+ phase: "follower-live-rebind-control",
4342
+ });
4343
+ return {
4344
+ kind: "bus.ack",
4345
+ requestId: envelope.requestId,
4346
+ ok: false,
4347
+ message:
4348
+ error instanceof Error
4349
+ ? error.message
4350
+ : "Live-rebind request failed.",
4351
+ };
4352
+ }
2551
4353
  }
2552
4354
  if (envelope.kind === "leader.workspaceRestore") {
2553
4355
  const ctx = deps.getContext();
2554
- if (!authSecret || !ctx || !deps.isWorkspaceRestoreEnabled?.() || !deps.handleWorkspaceRestore) return {
2555
- kind: "bus.ack", requestId: envelope.requestId, ok: false, message: "Workspace Restore capability is unavailable.",
2556
- };
4356
+ if (
4357
+ !authSecret ||
4358
+ !ctx ||
4359
+ !deps.isWorkspaceRestoreEnabled?.() ||
4360
+ !deps.handleWorkspaceRestore
4361
+ )
4362
+ return rejectTelegramBusRequest(
4363
+ envelope.requestId,
4364
+ "Workspace Restore capability is unavailable.",
4365
+ );
2557
4366
  try {
2558
- const result = await deps.handleWorkspaceRestore({ operationId: envelope.operationId,
2559
- registrationGeneration, mode: envelope.mode }, ctx);
2560
- if (deps.getAuthSecret?.() !== authSecret || deps.getRegistrationGeneration() !== registrationGeneration ||
2561
- deps.getContext() !== ctx || !deps.isWorkspaceRestoreEnabled()) throw new Error("Workspace Restore receiver authority changed.");
2562
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
4367
+ const result = await deps.handleWorkspaceRestore(
4368
+ {
4369
+ operationId: envelope.operationId,
4370
+ registrationGeneration,
4371
+ mode: envelope.mode,
4372
+ },
4373
+ ctx,
4374
+ );
4375
+ if (
4376
+ deps.getAuthSecret?.() !== authSecret ||
4377
+ deps.getRegistrationGeneration() !== registrationGeneration ||
4378
+ deps.getContext() !== ctx ||
4379
+ !deps.isWorkspaceRestoreEnabled()
4380
+ )
4381
+ throw new Error("Workspace Restore receiver authority changed.");
4382
+ return {
4383
+ kind: "bus.ack",
4384
+ requestId: envelope.requestId,
4385
+ ok: true,
4386
+ result,
4387
+ };
2563
4388
  } catch (error) {
2564
- deps.recordRuntimeEvent?.("bus", error, { phase: "follower-workspace-restore" });
2565
- return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
2566
- message: error instanceof Error ? error.message : "Workspace Restore failed." };
4389
+ deps.recordRuntimeEvent?.("bus", error, {
4390
+ phase: "follower-workspace-restore",
4391
+ });
4392
+ return {
4393
+ kind: "bus.ack",
4394
+ requestId: envelope.requestId,
4395
+ ok: false,
4396
+ message:
4397
+ error instanceof Error
4398
+ ? error.message
4399
+ : "Workspace Restore failed.",
4400
+ };
2567
4401
  }
2568
4402
  }
2569
- if (envelope.kind === "leader.offerInputCustodyHandoff" &&
2570
- envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
2571
- kind: "bus.ack", requestId: envelope.requestId, ok: false,
2572
- message: "Mismatched Telegram follower handoff recipient identity.",
2573
- };
4403
+ if (
4404
+ envelope.kind === "leader.offerInputCustodyHandoff" &&
4405
+ envelope.recipientBindingKey !== deps.getRecipientBindingKey()
4406
+ )
4407
+ return rejectTelegramBusRequest(
4408
+ envelope.requestId,
4409
+ "Mismatched Telegram follower handoff recipient identity.",
4410
+ );
2574
4411
  if (
2575
4412
  envelope.kind !== "leader.offerQueueHandoff" &&
2576
4413
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
@@ -2578,48 +4415,58 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2578
4415
  envelope.delivery.recipientBindingKey !==
2579
4416
  deps.getRecipientBindingKey())
2580
4417
  ) {
2581
- return {
2582
- kind: "bus.ack",
2583
- requestId: envelope.requestId,
2584
- ok: false,
2585
- message: "Mismatched Telegram follower delivery identity.",
2586
- };
4418
+ return rejectTelegramBusRequest(
4419
+ envelope.requestId,
4420
+ "Mismatched Telegram follower delivery identity.",
4421
+ );
2587
4422
  }
2588
4423
  const ctx = deps.getContext();
2589
4424
  if (!ctx) {
2590
- return {
2591
- kind: "bus.ack",
2592
- requestId: envelope.requestId,
2593
- ok: false,
2594
- message: "Telegram bus follower has no active context.",
2595
- };
4425
+ return rejectTelegramBusRequest(
4426
+ envelope.requestId,
4427
+ "Telegram bus follower has no active context.",
4428
+ );
2596
4429
  }
2597
4430
  try {
2598
4431
  if (envelope.kind === "leader.offerInputCustodyHandoff") {
2599
- if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false)) throw new Error(
2600
- "Telegram input custody handoff capability is not enabled.",
2601
- );
2602
- if (!deps.hasAuthenticatedSourceReferenceTransport?.()) throw new Error(
2603
- "Telegram input custody handoff requires authenticated transport.",
2604
- );
2605
- if (!deps.handleInputCustodyHandoff) throw new Error(
2606
- "Telegram input custody handoff acceptance is unavailable.",
2607
- );
4432
+ if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false))
4433
+ throw new Error(
4434
+ "Telegram input custody handoff capability is not enabled.",
4435
+ );
4436
+ if (!deps.hasAuthenticatedSourceReferenceTransport?.())
4437
+ throw new Error(
4438
+ "Telegram input custody handoff requires authenticated transport.",
4439
+ );
4440
+ if (!deps.handleInputCustodyHandoff)
4441
+ throw new Error(
4442
+ "Telegram input custody handoff acceptance is unavailable.",
4443
+ );
2608
4444
  const result = await deps.handleInputCustodyHandoff(envelope, ctx);
2609
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
4445
+ return {
4446
+ kind: "bus.ack",
4447
+ requestId: envelope.requestId,
4448
+ ok: true,
4449
+ result,
4450
+ };
2610
4451
  }
2611
4452
  if (envelope.kind !== "leader.offerQueueHandoff") {
2612
- const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
4453
+ const sourceReferenceEnabled =
4454
+ envelope.kind === "leader.wakeInputCustody" ||
2613
4455
  (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
2614
- if (sourceReferenceEnabled &&
2615
- !deps.hasAuthenticatedSourceReferenceTransport?.()) throw new Error(
4456
+ if (
4457
+ sourceReferenceEnabled &&
4458
+ !deps.hasAuthenticatedSourceReferenceTransport?.()
4459
+ )
4460
+ throw new Error(
2616
4461
  "Telegram follower source-reference admission requires authenticated transport.",
2617
4462
  );
2618
4463
  const admission = sourceReferenceEnabled
2619
- ? deps.sourceReferenceAdmission : deps.durableAdmission;
2620
- if (!admission) throw new Error(
2621
- "Telegram follower source-reference admission is enabled without a wake authority.",
2622
- );
4464
+ ? deps.sourceReferenceAdmission
4465
+ : deps.durableAdmission;
4466
+ if (!admission)
4467
+ throw new Error(
4468
+ "Telegram follower source-reference admission is enabled without a wake authority.",
4469
+ );
2623
4470
  const receipt = await admission.admit(envelope, ctx);
2624
4471
  return {
2625
4472
  kind: "bus.ack",
@@ -2691,11 +4538,3 @@ function parseRegistrationResult(value: unknown): {
2691
4538
  : {}),
2692
4539
  };
2693
4540
  }
2694
-
2695
- function parseTarget(value: unknown): TelegramTarget | undefined {
2696
- if (value === undefined) return undefined;
2697
- if (!isRecord(value) || typeof value.chatId !== "number") return undefined;
2698
- const target: TelegramTarget = { chatId: value.chatId };
2699
- if (typeof value.threadId === "number") target.threadId = value.threadId;
2700
- return target;
2701
- }