@llblab/pi-telegram 0.51.5 → 0.52.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 (181) hide show
  1. package/AGENTS.md +10 -8
  2. package/BACKLOG.md +1 -8
  3. package/CHANGELOG.md +23 -0
  4. package/LICENSE +21 -0
  5. package/README.md +16 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +9 -2
  9. package/dist/lib/bindings.js +22 -13
  10. package/dist/lib/bus-api.js +4 -2
  11. package/dist/lib/bus-follower.d.ts +55 -57
  12. package/dist/lib/bus-follower.js +213 -140
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +12 -26
  24. package/dist/lib/commands.js +152 -98
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +281 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +177 -16
  34. package/dist/lib/journal.js +1115 -249
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.d.ts +57 -2
  38. package/dist/lib/lifecycle.js +110 -5
  39. package/dist/lib/locks.d.ts +100 -12
  40. package/dist/lib/locks.js +380 -14
  41. package/dist/lib/logging.d.ts +30 -12
  42. package/dist/lib/logging.js +129 -72
  43. package/dist/lib/media.d.ts +28 -5
  44. package/dist/lib/media.js +26 -8
  45. package/dist/lib/menu-model.d.ts +0 -11
  46. package/dist/lib/menu-model.js +8 -8
  47. package/dist/lib/menu-settings.d.ts +0 -18
  48. package/dist/lib/menu-settings.js +18 -18
  49. package/dist/lib/menu-status.d.ts +4 -4
  50. package/dist/lib/menu-status.js +12 -7
  51. package/dist/lib/menu-thinking.d.ts +1 -1
  52. package/dist/lib/menu-thinking.js +1 -1
  53. package/dist/lib/menu.d.ts +1 -0
  54. package/dist/lib/menu.js +4 -3
  55. package/dist/lib/model.js +2 -4
  56. package/dist/lib/outbound-attachments.d.ts +0 -1
  57. package/dist/lib/outbound-attachments.js +1 -1
  58. package/dist/lib/outbound-buttons.js +1 -8
  59. package/dist/lib/outbound-markup.d.ts +1 -6
  60. package/dist/lib/outbound-markup.js +4 -21
  61. package/dist/lib/outbound.d.ts +0 -8
  62. package/dist/lib/outbound.js +5 -5
  63. package/dist/lib/paths.d.ts +50 -7
  64. package/dist/lib/paths.js +155 -17
  65. package/dist/lib/pi.d.ts +1 -0
  66. package/dist/lib/pi.js +2 -1
  67. package/dist/lib/polling.d.ts +0 -5
  68. package/dist/lib/polling.js +5 -5
  69. package/dist/lib/preview.d.ts +0 -3
  70. package/dist/lib/preview.js +3 -3
  71. package/dist/lib/prompt-templates.d.ts +0 -1
  72. package/dist/lib/prompt-templates.js +1 -1
  73. package/dist/lib/queue.d.ts +3 -15
  74. package/dist/lib/queue.js +69 -8
  75. package/dist/lib/recovery.d.ts +29 -9
  76. package/dist/lib/recovery.js +104 -33
  77. package/dist/lib/replies.d.ts +0 -4
  78. package/dist/lib/replies.js +1 -1
  79. package/dist/lib/routing.d.ts +73 -8
  80. package/dist/lib/routing.js +2206 -307
  81. package/dist/lib/sections.d.ts +0 -2
  82. package/dist/lib/sections.js +1 -1
  83. package/dist/lib/status.d.ts +46 -9
  84. package/dist/lib/status.js +179 -27
  85. package/dist/lib/sync.d.ts +0 -5
  86. package/dist/lib/sync.js +9 -8
  87. package/dist/lib/telegram-api.d.ts +11 -3
  88. package/dist/lib/telegram-api.js +34 -23
  89. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  90. package/dist/lib/thread-cleanup-manager.js +18 -21
  91. package/dist/lib/thread-naming.d.ts +32 -5
  92. package/dist/lib/thread-naming.js +191 -7
  93. package/dist/lib/thread-reconciler.d.ts +2 -0
  94. package/dist/lib/thread-reconciler.js +1 -1
  95. package/dist/lib/threads.d.ts +292 -59
  96. package/dist/lib/threads.js +1664 -373
  97. package/dist/lib/turns.d.ts +0 -1
  98. package/dist/lib/turns.js +1 -1
  99. package/dist/lib/updates.d.ts +184 -28
  100. package/dist/lib/updates.js +1032 -114
  101. package/dist/lib/wire.d.ts +12 -0
  102. package/dist/lib/wire.js +21 -0
  103. package/dist/lib/workspace-admission.d.ts +25 -17
  104. package/dist/lib/workspace-admission.js +95 -20
  105. package/dist/lib/workspace-identity.d.ts +20 -0
  106. package/dist/lib/workspace-identity.js +103 -0
  107. package/dist/lib/workspace-retirement.d.ts +45 -10
  108. package/dist/lib/workspace-retirement.js +196 -64
  109. package/dist/package.json +6 -6
  110. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  111. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  112. package/docs/README.md +1 -1
  113. package/docs/activity.md +1 -1
  114. package/docs/architecture.md +158 -39
  115. package/docs/callback-namespaces.md +1 -1
  116. package/docs/delivery.md +1 -1
  117. package/docs/generative-apps.md +2 -2
  118. package/docs/inbound.md +1 -1
  119. package/docs/multi-instance-bus.md +127 -19
  120. package/docs/public-api.md +5 -3
  121. package/docs/ui-style.md +26 -8
  122. package/docs/updates.md +1 -1
  123. package/lib/activity-verbosity.ts +5 -9
  124. package/lib/bindings.ts +23 -10
  125. package/lib/bus-api.ts +5 -2
  126. package/lib/bus-follower.ts +251 -226
  127. package/lib/bus-leader.ts +178 -31
  128. package/lib/bus-transport.ts +30 -8
  129. package/lib/bus.ts +110 -334
  130. package/lib/channel-posts.ts +29 -29
  131. package/lib/command-templates.ts +5 -5
  132. package/lib/commands.ts +150 -112
  133. package/lib/config.ts +12 -17
  134. package/lib/delivery.ts +2 -8
  135. package/lib/extension.ts +282 -72
  136. package/lib/generative-apps.ts +0 -22
  137. package/lib/inbound.ts +2 -2
  138. package/lib/journal.ts +1182 -326
  139. package/lib/keyboard.ts +2 -2
  140. package/lib/lifecycle.ts +141 -5
  141. package/lib/locks.ts +402 -23
  142. package/lib/logging.ts +151 -101
  143. package/lib/media.ts +51 -9
  144. package/lib/menu-model.ts +8 -8
  145. package/lib/menu-settings.ts +18 -18
  146. package/lib/menu-status.ts +11 -0
  147. package/lib/menu-thinking.ts +2 -2
  148. package/lib/menu.ts +5 -1
  149. package/lib/model.ts +2 -4
  150. package/lib/outbound-attachments.ts +1 -1
  151. package/lib/outbound-buttons.ts +1 -10
  152. package/lib/outbound-markup.ts +5 -24
  153. package/lib/outbound.ts +5 -5
  154. package/lib/paths.ts +177 -17
  155. package/lib/pi.ts +5 -2
  156. package/lib/polling.ts +5 -5
  157. package/lib/preview.ts +3 -3
  158. package/lib/prompt-templates.ts +1 -1
  159. package/lib/queue.ts +67 -9
  160. package/lib/recovery.ts +104 -44
  161. package/lib/replies.ts +1 -1
  162. package/lib/routing.ts +1955 -375
  163. package/lib/sections.ts +1 -1
  164. package/lib/status.ts +185 -36
  165. package/lib/sync.ts +8 -8
  166. package/lib/telegram-api.ts +48 -33
  167. package/lib/thread-cleanup-manager.ts +24 -21
  168. package/lib/thread-naming.ts +267 -9
  169. package/lib/thread-reconciler.ts +3 -1
  170. package/lib/threads.ts +1718 -491
  171. package/lib/turns.ts +1 -1
  172. package/lib/updates.ts +1072 -151
  173. package/lib/wire.ts +28 -0
  174. package/lib/workspace-admission.ts +97 -47
  175. package/lib/workspace-identity.ts +147 -0
  176. package/lib/workspace-retirement.ts +234 -102
  177. package/package.json +6 -6
  178. package/scripts/audit-exports.mjs +100 -0
  179. package/scripts/check-downgrade.mjs +80 -43
  180. package/skills/generated-control-surface/SKILL.md +3 -3
  181. package/skills/telegram-bridge/references/diagnosis.md +3 -3
@@ -7,9 +7,12 @@
7
7
  */
8
8
 
9
9
  import { basename } from "node:path";
10
+ import { isDeepStrictEqual } from "node:util";
11
+ import type { TelegramWorkspaceRestoreExecutor, TelegramWorkspaceRestoreIntent } from "./threads.ts";
10
12
 
11
- import * as Sync from "./sync.ts";
12
13
  import * as Threads from "./threads.ts";
14
+ import * as WorkspaceIdentity from "./workspace-identity.ts";
15
+ import { isWireRecord as isRecord } from "./wire.ts";
13
16
  import { parseTelegramUpdateJournalQueueOwner, type TelegramUpdateJournalStoreOptions } from "./journal.ts";
14
17
  import {
15
18
  TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS,
@@ -28,7 +31,6 @@ import {
28
31
  } from "./telegram-api.ts";
29
32
  import {
30
33
  createTelegramBusFollowerDeliveryIdentity,
31
- createTelegramBusFollowerTargetController,
32
34
  createTelegramBusForeignOwnedUpdateForwarder,
33
35
  createTelegramBusLocalServer,
34
36
  createTelegramBusRequestIdFactory,
@@ -42,6 +44,7 @@ import {
42
44
  type TelegramBusAgentMessage,
43
45
  type TelegramBusAgentTargetSelector,
44
46
  type TelegramBusEnvelope,
47
+ type TelegramBusWorkspaceRestoreObservation,
45
48
  type TelegramBusForwardOwnership,
46
49
  type TelegramBusProtocolIdentity,
47
50
  type TelegramBusSocketPathSource,
@@ -49,6 +52,7 @@ import {
49
52
  TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
50
53
  TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
51
54
  TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
55
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
52
56
  } from "./bus.ts";
53
57
  import type { TelegramConfigStore, TelegramThreadDisplayMode } from "./config.ts";
54
58
  import {
@@ -61,15 +65,15 @@ import {
61
65
  type TelegramWorkspaceAdmissionLedger,
62
66
  } from "./workspace-admission.ts";
63
67
 
64
- export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
65
- export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
66
- export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
67
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
68
+ const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
69
+ const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
70
+ const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
71
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
68
72
  export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS =
69
73
  TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
70
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
74
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS =
71
75
  TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
72
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
76
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS =
73
77
  TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
74
78
 
75
79
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY =
@@ -112,7 +116,7 @@ export function setTelegramFollowerSessionHandoff(
112
116
  else store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY] = handoff;
113
117
  }
114
118
 
115
- export function isTelegramFollowerSessionHandoffFresh(
119
+ function isTelegramFollowerSessionHandoffFresh(
116
120
  handoff: TelegramFollowerSessionHandoff | undefined,
117
121
  options: { pid?: number; nowMs?: number; ttlMs?: number } = {},
118
122
  ): handoff is TelegramFollowerSessionHandoff {
@@ -206,6 +210,8 @@ export interface TelegramBusFollowerRegistrationState {
206
210
  getDisplayTitle: () => string | undefined;
207
211
  setDisplayTitle: (title: string, generation: string) => boolean;
208
212
  getGeneration: () => string | undefined;
213
+ /** Return the acknowledged session only when it matches the caller's current context. */
214
+ getSessionId: (currentSessionId: string | undefined) => string | undefined;
209
215
  beginRecovery: () => number;
210
216
  cancelRecovery: () => void;
211
217
  waitForGeneration: (timeoutMs?: number) => Promise<string | undefined>;
@@ -220,6 +226,7 @@ export interface TelegramBusFollowerRegistrationState {
220
226
  threadName?: string;
221
227
  displayTitle?: string;
222
228
  generation?: string;
229
+ sessionId?: string;
223
230
  leaderProtocol?: TelegramBusProtocolIdentity;
224
231
  },
225
232
  ) => void;
@@ -481,30 +488,6 @@ export function createTelegramBusFollowerPromotionHandler<
481
488
  );
482
489
  }
483
490
 
484
- export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
485
- topicTargetStore: Pick<
486
- Threads.TelegramTopicTargetStore,
487
- "load" | "list" | "markStaleByTarget" | "upsert" | "persist"
488
- >;
489
- registrationState: Pick<
490
- TelegramBusFollowerRegistrationState,
491
- "getTarget" | "getSlot" | "setRegistered" | "getGeneration"
492
- >;
493
- instanceId: string;
494
- getManualFollowerProfileKey: () => string;
495
- manualFollowerOwnerId: string;
496
- getSyncState: () => Sync.TelegramSyncState;
497
- setSyncState: (state: Sync.TelegramSyncState) => void;
498
- getNowMs?: () => number;
499
- updateStatus: (ctx: TContext) => void;
500
- recordRuntimeEvent?: (
501
- category: string,
502
- error: unknown,
503
- details?: Record<string, unknown>,
504
- ) => void;
505
- getWorkspaceAdmission?: TelegramBusFollowerWorkspaceAdmissionDeps["getWorkspaceAdmission"];
506
- }
507
-
508
491
  export type TelegramBusFollowerLeaderState =
509
492
  | { kind: "inactive" }
510
493
  | { kind: "active-here"; lock: TelegramBusFollowerLeaderLock }
@@ -571,15 +554,9 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
571
554
  >,
572
555
  ctx: TContext,
573
556
  ) => Promise<TelegramQueueHandoffStageResult> | TelegramQueueHandoffStageResult;
574
- handleReplaceTarget?: (
575
- input: {
576
- target: TelegramTarget & { threadId: number };
577
- oldTarget?: TelegramTarget & { threadId: number };
578
- reason: "thread-restore";
579
- registrationGeneration: string;
580
- },
581
- ctx: TContext,
582
- ) => Promise<void> | void;
557
+ isWorkspaceRestoreEnabled?: () => boolean;
558
+ handleWorkspaceRestore?: (input: { operationId: string; registrationGeneration: string; mode: "apply" | "inspect" },
559
+ ctx: TContext) => Promise<TelegramBusWorkspaceRestoreObservation>;
583
560
  recordRuntimeEvent?: (
584
561
  category: string,
585
562
  error: unknown,
@@ -590,6 +567,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
590
567
  export interface TelegramBusFollowerRuntimeAssembly<TContext> {
591
568
  receiver: TelegramBusForwardedUpdateReceiverRuntime;
592
569
  registration: TelegramBusFollowerRegistrationRuntime<TContext>;
570
+ getReadySessionId: () => string | undefined;
593
571
  }
594
572
 
595
573
  export interface TelegramBusFollowerRuntimeAssemblyPorts<
@@ -604,15 +582,10 @@ export interface TelegramBusFollowerRuntimeAssemblyPorts<
604
582
  ) => void;
605
583
  receiver: Omit<
606
584
  TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>,
607
- | "handleReplaceTarget"
608
585
  | "instanceId"
609
586
  | "recordRuntimeEvent"
610
587
  | "getRegistrationGeneration"
611
588
  >;
612
- targetReplacement: Omit<
613
- TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
614
- "registrationState" | "instanceId" | "recordRuntimeEvent"
615
- >;
616
589
  recovery: Omit<
617
590
  TelegramBusFollowerHeartbeatRecoveryHandlerDeps<TContext>,
618
591
  "getRegistrationRuntime" | "registrationState" | "recordRuntimeEvent"
@@ -637,20 +610,25 @@ export function createTelegramBusFollowerRuntimeAssembly<
637
610
  ports: TelegramBusFollowerRuntimeAssemblyPorts<TContext>,
638
611
  ): TelegramBusFollowerRuntimeAssembly<TContext> {
639
612
  // Binding startup needs registration identity before it can grant inbound authority.
640
- let readyContext: { generation: string; ctx: TContext; sessionGeneration: number | undefined } | undefined;
613
+ let readyContext: { generation: string; ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
641
614
  const isReadyContext = (ctx?: TContext): boolean => Boolean(readyContext &&
642
615
  (ctx === undefined || readyContext.ctx === ctx) &&
643
616
  ports.registrationState.getGeneration() === readyContext.generation &&
644
617
  ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
618
+ ports.registration.getSessionId?.(readyContext.ctx) === readyContext.sessionId &&
645
619
  ports.registration.isContextActive?.(readyContext.ctx) !== false);
646
620
  const prepareContext = async (ctx: TContext): Promise<void> => {
647
621
  const generation = ports.registrationState.getGeneration();
648
622
  const sessionGeneration = ports.registration.getSessionGeneration?.();
623
+ const sessionId = ports.registration.getSessionId?.(ctx);
649
624
  readyContext = undefined;
625
+ if (!generation || (sessionId !== undefined && ports.registrationState.getSessionId(sessionId) !== sessionId))
626
+ throw new Error("Telegram follower session change requires acknowledged leader registration.");
650
627
  await ports.registration.onRegistered?.(ctx);
651
628
  if (generation && ports.registrationState.getGeneration() === generation &&
652
629
  ports.registration.getSessionGeneration?.() === sessionGeneration &&
653
- ports.registration.isContextActive?.(ctx) !== false) readyContext = { generation, ctx, sessionGeneration };
630
+ ports.registration.getSessionId?.(ctx) === sessionId &&
631
+ ports.registration.isContextActive?.(ctx) !== false) readyContext = { generation, ctx, sessionGeneration, sessionId };
654
632
  };
655
633
  const sharedRuntimeDeps = {
656
634
  instanceId: ports.instanceId,
@@ -664,10 +642,6 @@ export function createTelegramBusFollowerRuntimeAssembly<
664
642
  getRegistrationGeneration() {
665
643
  return isReadyContext() ? readyContext?.generation : undefined;
666
644
  },
667
- handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
668
- ...ports.targetReplacement,
669
- ...sharedRuntimeDeps,
670
- }),
671
645
  });
672
646
  let registration: TelegramBusFollowerRegistrationRuntime<TContext>;
673
647
  const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
@@ -696,115 +670,146 @@ export function createTelegramBusFollowerRuntimeAssembly<
696
670
  if (!isReadyContext(ctx)) await prepareContext(ctx);
697
671
  },
698
672
  };
699
- return { receiver, registration };
673
+ return { receiver, registration, getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined };
700
674
  }
701
675
 
702
- export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
703
- deps: TelegramBusFollowerTargetReplacementHandlerDeps<TContext>,
704
- ): NonNullable<
705
- TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext>["handleReplaceTarget"]
706
- > {
707
- const getNowMs = deps.getNowMs ?? Date.now;
708
- return (input, ctx) => runTelegramBusFollowerWorkspaceMutation(
709
- deps,
710
- "workspace.replace-follower-target",
711
- async () => {
712
- const assertCurrent = (expectedTarget = input.oldTarget): void => {
713
- const target = deps.registrationState.getTarget();
714
- if (
715
- !input.registrationGeneration ||
716
- deps.registrationState.getGeneration() !==
717
- input.registrationGeneration ||
718
- !input.oldTarget ||
719
- !expectedTarget ||
720
- target?.chatId !== expectedTarget.chatId ||
721
- target.threadId !== expectedTarget.threadId ||
722
- input.target.chatId !== input.oldTarget.chatId ||
723
- input.target.threadId === input.oldTarget.threadId
724
- ) {
725
- throw new Error(
726
- "Stale Telegram follower target replacement authority.",
727
- );
728
- }
729
- };
676
+ export interface TelegramBusFollowerRestoreContext {
677
+ executor: TelegramWorkspaceRestoreExecutor;
678
+ profileBindingKey: string;
679
+ operatorUserId: number;
680
+ cwd: string;
681
+ sessionId: string;
682
+ generation: string | number;
683
+ leaderProtocol?: TelegramBusProtocolIdentity;
684
+ }
685
+
686
+ /** Binds Restore to real Pi lifetime and the current authenticated transport owner, not request fields. */
687
+ export function createTelegramBusFollowerRestoreContextGetter<TContext>(deps: {
688
+ isContextCurrent: (ctx: TContext) => boolean;
689
+ getSessionId: (ctx: TContext) => string | undefined;
690
+ getCwd: (ctx: TContext) => string | undefined;
691
+ getGeneration: () => number;
692
+ getProfileBindingKey: () => string | undefined;
693
+ getOperatorUserId: () => number | undefined;
694
+ getLeaderState: () => TelegramLockState;
695
+ getAuthenticatedSecret: () => string | undefined;
696
+ getLeaderProtocol: () => TelegramBusProtocolIdentity | undefined;
697
+ }) {
698
+ return (ctx: TContext): TelegramBusFollowerRestoreContext | undefined => {
699
+ if (!deps.isContextCurrent(ctx)) return undefined;
700
+ const leader = deps.getLeaderState();
701
+ const secret = deps.getAuthenticatedSecret();
702
+ 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;
706
+ const profileBindingKey = deps.getProfileBindingKey();
707
+ const operatorUserId = deps.getOperatorUserId();
708
+ const sessionId = deps.getSessionId(ctx);
709
+ const cwd = deps.getCwd(ctx);
710
+ 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) };
716
+ };
717
+ }
718
+
719
+ /** Prepared receiver: its caller authenticates the envelope; no canonical writes, dispatch or cleanup. */
720
+ export function createTelegramBusFollowerWorkspaceRestoreHandler<TContext>(deps: {
721
+ instanceId: string;
722
+ /** 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"]>;
728
+ recordRuntimeEvent?: TelegramBusFollowerWorkspaceAdmissionDeps["recordRuntimeEvent"];
729
+ }) {
730
+ return async (input: { operationId: string; registrationGeneration: string; mode: "apply" | "inspect" }, ctx: TContext) => {
731
+ const captured = deps.getContextAuthority(ctx);
732
+ if (!captured) throw new Error("Stale Telegram follower Restore context.");
733
+ 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))
741
+ throw new Error("Invalid Telegram follower Restore authority.");
742
+ const assertCurrent = (): void => {
743
+ if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
744
+ deps.registrationState.getGeneration() !== request.registrationGeneration)
745
+ throw new Error("Stale Telegram follower Restore authority.");
746
+ };
747
+ 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 () => {
730
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") ||
755
+ !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);
731
763
  await deps.topicTargetStore.load();
732
764
  assertCurrent();
733
- const nowMs = getNowMs();
734
- const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
735
- records: deps.topicTargetStore.list(),
736
- instanceId: deps.instanceId,
737
- preferredTarget: input.oldTarget,
738
- });
739
- if (
740
- currentRecord &&
741
- (currentRecord.target.chatId !== input.oldTarget!.chatId ||
742
- currentRecord.target.threadId !== input.oldTarget!.threadId)
743
- ) {
744
- throw new Error("Stale Telegram follower binding replacement target.");
745
- }
746
- const currentSlot =
747
- currentRecord?.slot ?? deps.registrationState.getSlot();
748
- if (!currentSlot || !/^[A-Z]$/u.test(currentSlot)) {
749
- throw new Error("Telegram Thread slot authority is unavailable.");
750
- }
751
- deps.topicTargetStore.markStaleByTarget(
752
- input.oldTarget!,
753
- "deleted",
754
- "Follower thread was replaced by thread restore.",
755
- );
756
- const profileKey =
757
- currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
758
- deps.topicTargetStore.upsert({
759
- profileKey,
760
- owner: {
761
- kind: "manual-follower",
762
- instanceId: deps.manualFollowerOwnerId,
763
- },
764
- target: input.target,
765
- status: "active",
766
- syncStatus: "open",
767
- createdAtMs: currentRecord?.createdAtMs ?? nowMs,
768
- updatedAtMs: nowMs,
769
- lastSyncObservedAtMs: nowMs,
770
- lastReconcileAction: "follower-thread-restore",
771
- instanceId: deps.instanceId,
772
- slot: currentSlot,
773
- threadName: currentRecord?.threadName,
774
- rerouteConfirmedAtMs: nowMs,
775
- });
776
- deps.registrationState.setRegistered(true, input.target, {
777
- slot: currentSlot,
778
- threadName: currentRecord?.threadName,
779
- generation: input.registrationGeneration,
780
- });
781
- await deps.topicTargetStore.persist();
782
- assertCurrent(input.target);
783
- deps.setSyncState(
784
- Sync.markTelegramSyncSliceFresh(
785
- deps.getSyncState(),
786
- "target-bindings",
787
- {
788
- nowMs,
789
- action: "follower-thread-restore",
790
- },
791
- ),
792
- );
793
- deps.updateStatus(ctx);
794
- deps.recordRuntimeEvent?.(
795
- "bus",
796
- "Telegram follower thread target replaced",
797
- {
798
- phase: "follower-thread-restore",
799
- chatId: input.target.chatId,
800
- threadId: input.target.threadId,
801
- oldThreadId:
802
- input.oldTarget?.threadId ?? currentRecord?.target.threadId,
803
- slot: currentSlot,
804
- },
805
- );
806
- },
807
- );
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.");
768
+ 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" ||
778
+ 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() });
792
+ }
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
+ });
808
+ 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
+ return result;
812
+ };
808
813
  }
809
814
 
810
815
  export function createTelegramBusFollowerClientRuntime<
@@ -855,10 +860,6 @@ export function createTelegramBusFollowerClientRuntime<
855
860
  getAuthSecret: deps.getApiAuthSecret,
856
861
  getRegistrationGeneration: deps.getRegistrationGeneration,
857
862
  }),
858
- targetController: createTelegramBusFollowerTargetController({
859
- ...sharedClientDeps,
860
- getAuthSecret: deps.getForwardingAuthSecret,
861
- }),
862
863
  };
863
864
  }
864
865
 
@@ -939,7 +940,7 @@ export function createTelegramBusFollowerQueueHandoffClient(
939
940
  };
940
941
  }
941
942
 
942
- export function createTelegramBusAgentMessageClient(
943
+ function createTelegramBusAgentMessageClient(
943
944
  deps: TelegramBusFollowerApiCallerDeps,
944
945
  ): {
945
946
  resolveTarget: (
@@ -1130,10 +1131,16 @@ function isTelegramStaleContextError(error: unknown): boolean {
1130
1131
 
1131
1132
  export function createTelegramBusFollowerSessionReplacementSuspender(
1132
1133
  deps: TelegramBusFollowerSessionReplacementSuspenderDeps,
1133
- ): () => Promise<void> {
1134
+ ): (preserveTarget?: boolean) => Promise<void> {
1134
1135
  const getNowMs = deps.getNowMs ?? Date.now;
1135
1136
  const getPid = deps.getPid ?? (() => process.pid);
1136
- return async () => {
1137
+ return async (preserveTarget = true) => {
1138
+ if (!preserveTarget) {
1139
+ setTelegramFollowerSessionHandoff(undefined);
1140
+ Threads.setTelegramLeaderSessionHandoff(undefined);
1141
+ await deps.suspendPolling();
1142
+ return;
1143
+ }
1137
1144
  const target = deps.registrationState.getTarget();
1138
1145
  if (deps.registrationState.isRegistered() && target) {
1139
1146
  setTelegramFollowerSessionHandoff({
@@ -1278,6 +1285,7 @@ export function createTelegramBusFollowerRegistrationState(
1278
1285
  let threadName: string | undefined;
1279
1286
  let displayTitle: string | undefined;
1280
1287
  let generation: string | undefined;
1288
+ let sessionId: string | undefined;
1281
1289
  let leaderProtocol: TelegramBusProtocolIdentity | undefined;
1282
1290
  let eligibleElectionSlots: string[] = [];
1283
1291
  let recoveryEpoch = 0;
@@ -1307,6 +1315,7 @@ export function createTelegramBusFollowerRegistrationState(
1307
1315
  return true;
1308
1316
  },
1309
1317
  getGeneration: () => generation,
1318
+ getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
1310
1319
  beginRecovery: () => {
1311
1320
  if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
1312
1321
  activeRecoveryEpoch = ++recoveryEpoch;
@@ -1348,6 +1357,7 @@ export function createTelegramBusFollowerRegistrationState(
1348
1357
  generation === metadata?.generation &&
1349
1358
  target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
1350
1359
  ? displayTitle : undefined;
1360
+ const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
1351
1361
  const availabilityChanged = registered !== next;
1352
1362
  registered = next;
1353
1363
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
@@ -1357,6 +1367,7 @@ export function createTelegramBusFollowerRegistrationState(
1357
1367
  metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
1358
1368
  ? metadata.displayTitle : retainedDisplayTitle;
1359
1369
  generation = next ? metadata?.generation : undefined;
1370
+ sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
1360
1371
  leaderProtocol =
1361
1372
  next && metadata?.leaderProtocol
1362
1373
  ? {
@@ -1650,7 +1661,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1650
1661
  let activeLeaderSocketPath: string | undefined;
1651
1662
  let activeAuthSecret: string | undefined;
1652
1663
  let activeRegistrationGeneration: string | undefined;
1653
- let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined } | undefined;
1664
+ let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
1654
1665
  let activeContext: TContext | undefined;
1655
1666
  let lastKnownTarget: TelegramTarget | undefined;
1656
1667
  let lastKnownSlot: string | undefined;
@@ -1787,10 +1798,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
1787
1798
  registerWithLeader: async (ctx, leader, options) => {
1788
1799
  if (deps.isContextActive?.(ctx) === false) return false;
1789
1800
  const sessionGeneration = deps.getSessionGeneration?.();
1790
- const attempt = { ctx, sessionGeneration };
1801
+ const sessionId = deps.getSessionId?.(ctx);
1802
+ const attempt = { ctx, sessionGeneration, sessionId };
1791
1803
  registrationAttempt = attempt;
1792
1804
  const isCurrentRequest = () => registrationAttempt === attempt &&
1793
- deps.getSessionGeneration?.() === sessionGeneration && deps.isContextActive?.(ctx) !== false;
1805
+ deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
1806
+ deps.isContextActive?.(ctx) !== false;
1794
1807
  const abandonOwnedRequest = async (): Promise<void> => {
1795
1808
  if (registrationAttempt !== attempt) return;
1796
1809
  registrationAttempt = undefined;
@@ -1853,7 +1866,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1853
1866
  ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
1854
1867
  : {}),
1855
1868
  cwd: ctx.cwd,
1856
- sessionId: deps.getSessionId?.(ctx),
1869
+ sessionId,
1857
1870
  pid: getPid(),
1858
1871
  processBirthId: deps.getProcessBirthId?.(),
1859
1872
  sessionGeneration,
@@ -1904,7 +1917,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
1904
1917
  local: deps.protocolIdentity,
1905
1918
  remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1906
1919
  });
1907
- if (!compatibility.compatible) {
1920
+ // 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;
1922
+ if (!compatibility.compatible && !unidentifiedRejection) {
1908
1923
  stopHeartbeat();
1909
1924
  activeLeaderSocketPath = undefined;
1910
1925
  activeAuthSecret = undefined;
@@ -1942,6 +1957,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1942
1957
  deps.registrationState?.setRegistered(true, registrationResult.target, {
1943
1958
  ...registrationResult,
1944
1959
  generation: registrationGeneration,
1960
+ sessionId,
1945
1961
  ...(response.protocol
1946
1962
  ? { leaderProtocol: response.protocol }
1947
1963
  : {}),
@@ -1988,7 +2004,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
1988
2004
  },
1989
2005
  setContext(ctx) {
1990
2006
  if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
1991
- registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.())) registrationAttempt = undefined;
2007
+ registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
2008
+ registrationAttempt.sessionId !== deps.getSessionId?.(ctx))) registrationAttempt = undefined;
1992
2009
  activeContext = ctx;
1993
2010
  },
1994
2011
  async setThreadDisplayMode(mode) {
@@ -2320,7 +2337,20 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2320
2337
  ): unknown;
2321
2338
  };
2322
2339
  signalWorker: (ctx: TContext) => void;
2340
+ getNowMs?: () => number;
2341
+ /** Bounded process-local replay window; omit for the production 24 h / 4,096-delivery defaults. */
2342
+ recentDeliveryLimit?: { maxAgeMs: number; maxEntries: number };
2323
2343
  }): TelegramBusFollowerDurableAdmissionPort<TContext> {
2344
+ // The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
2345
+ const recentDeliveries = new Map<string, number>();
2346
+ const getNowMs = deps.getNowMs ?? Date.now;
2347
+ const limit = deps.recentDeliveryLimit ?? { maxAgeMs: 24 * 60 * 60 * 1000, maxEntries: 4096 };
2348
+ const pruneRecentDeliveries = (nowMs: number): void => {
2349
+ for (const [deliveryId, admittedAtMs] of recentDeliveries) {
2350
+ if (recentDeliveries.size <= limit.maxEntries && nowMs - admittedAtMs < limit.maxAgeMs) break;
2351
+ recentDeliveries.delete(deliveryId);
2352
+ }
2353
+ };
2324
2354
  return {
2325
2355
  async admit(envelope, ctx) {
2326
2356
  const delivery = envelope.delivery;
@@ -2367,12 +2397,17 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2367
2397
  ? { message: carrier }
2368
2398
  : { edited_message: carrier }),
2369
2399
  };
2370
- deps.journal.appendBatch([update]);
2371
- deps.signalWorker(ctx);
2372
- return {
2400
+ const result = {
2373
2401
  deliveryId: delivery.deliveryId,
2374
2402
  sourceUpdateId: delivery.sourceUpdateId,
2375
2403
  };
2404
+ pruneRecentDeliveries(getNowMs());
2405
+ if (recentDeliveries.has(delivery.deliveryId)) return result;
2406
+ deps.journal.appendBatch([update]);
2407
+ recentDeliveries.set(delivery.deliveryId, getNowMs());
2408
+ pruneRecentDeliveries(getNowMs());
2409
+ deps.signalWorker(ctx);
2410
+ return result;
2376
2411
  },
2377
2412
  };
2378
2413
  }
@@ -2491,7 +2526,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2491
2526
  envelope.kind !== "leader.forwardEditedMessage" &&
2492
2527
  envelope.kind !== "leader.wakeInputCustody" &&
2493
2528
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
2494
- envelope.kind !== "leader.replaceFollowerTarget" &&
2529
+ envelope.kind !== "leader.workspaceRestore" &&
2495
2530
  envelope.kind !== "leader.offerQueueHandoff") ||
2496
2531
  envelope.recipientInstanceId !== deps.instanceId
2497
2532
  ) {
@@ -2514,13 +2549,29 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2514
2549
  message: "Stale Telegram bus follower registration generation.",
2515
2550
  };
2516
2551
  }
2552
+ if (envelope.kind === "leader.workspaceRestore") {
2553
+ 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
+ };
2557
+ 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 };
2563
+ } 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." };
2567
+ }
2568
+ }
2517
2569
  if (envelope.kind === "leader.offerInputCustodyHandoff" &&
2518
2570
  envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
2519
2571
  kind: "bus.ack", requestId: envelope.requestId, ok: false,
2520
2572
  message: "Mismatched Telegram follower handoff recipient identity.",
2521
2573
  };
2522
2574
  if (
2523
- envelope.kind !== "leader.replaceFollowerTarget" &&
2524
2575
  envelope.kind !== "leader.offerQueueHandoff" &&
2525
2576
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
2526
2577
  (!envelope.delivery ||
@@ -2557,10 +2608,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2557
2608
  const result = await deps.handleInputCustodyHandoff(envelope, ctx);
2558
2609
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
2559
2610
  }
2560
- if (
2561
- envelope.kind !== "leader.replaceFollowerTarget" &&
2562
- envelope.kind !== "leader.offerQueueHandoff"
2563
- ) {
2611
+ if (envelope.kind !== "leader.offerQueueHandoff") {
2564
2612
  const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
2565
2613
  (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
2566
2614
  if (sourceReferenceEnabled &&
@@ -2580,52 +2628,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2580
2628
  result: receipt,
2581
2629
  };
2582
2630
  }
2583
- if (envelope.kind === "leader.offerQueueHandoff") {
2584
- if (!deps.handleQueueHandoff) {
2585
- throw new Error(
2586
- "Telegram bus receiver cannot accept queue handoff payloads.",
2587
- );
2588
- }
2589
- const result = await deps.handleQueueHandoff(envelope, ctx);
2590
- const receipt = envelope.payload.admissionReceipts[0];
2591
- if (
2592
- envelope.payload.admissionReceipts.length !== 1 ||
2593
- !receipt ||
2594
- result.status !== "staged" ||
2595
- result.receiptId !== receipt.receiptId ||
2596
- result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
2597
- result.sourceUpdateIds.some(
2598
- (updateId, index) => updateId !== receipt.sourceUpdateIds[index],
2599
- )
2600
- ) {
2601
- throw new Error(
2602
- "Telegram queue handoff staging returned a mismatched receipt.",
2603
- );
2604
- }
2605
- return {
2606
- kind: "bus.ack",
2607
- requestId: envelope.requestId,
2608
- ok: true,
2609
- result,
2610
- };
2631
+ if (!deps.handleQueueHandoff) {
2632
+ throw new Error(
2633
+ "Telegram bus receiver cannot accept queue handoff payloads.",
2634
+ );
2611
2635
  }
2612
- {
2613
- if (!deps.handleReplaceTarget) {
2614
- throw new Error(
2615
- "Telegram bus receiver cannot replace follower target.",
2616
- );
2617
- }
2618
- await deps.handleReplaceTarget(
2619
- {
2620
- target: envelope.target,
2621
- ...(envelope.oldTarget ? { oldTarget: envelope.oldTarget } : {}),
2622
- reason: envelope.reason,
2623
- registrationGeneration,
2624
- },
2625
- ctx,
2636
+ const result = await deps.handleQueueHandoff(envelope, ctx);
2637
+ const receipt = envelope.payload.admissionReceipts[0];
2638
+ if (
2639
+ envelope.payload.admissionReceipts.length !== 1 ||
2640
+ !receipt ||
2641
+ result.status !== "staged" ||
2642
+ result.receiptId !== receipt.receiptId ||
2643
+ result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
2644
+ result.sourceUpdateIds.some(
2645
+ (updateId, index) => updateId !== receipt.sourceUpdateIds[index],
2646
+ )
2647
+ ) {
2648
+ throw new Error(
2649
+ "Telegram queue handoff staging returned a mismatched receipt.",
2626
2650
  );
2627
2651
  }
2628
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
2652
+ return {
2653
+ kind: "bus.ack",
2654
+ requestId: envelope.requestId,
2655
+ ok: true,
2656
+ result,
2657
+ };
2629
2658
  } catch (error) {
2630
2659
  deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
2631
2660
  return {
@@ -2670,7 +2699,3 @@ function parseTarget(value: unknown): TelegramTarget | undefined {
2670
2699
  if (typeof value.threadId === "number") target.threadId = value.threadId;
2671
2700
  return target;
2672
2701
  }
2673
-
2674
- function isRecord(value: unknown): value is Record<string, unknown> {
2675
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2676
- }