@llblab/pi-kit 0.27.6 → 0.28.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 (182) hide show
  1. package/BACKLOG.md +2 -5
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  22. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  24. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  27. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  28. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  29. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  33. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  34. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  36. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  37. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  38. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  39. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  40. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  42. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  43. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  44. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  45. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  46. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  48. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  49. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  50. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  60. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  61. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  62. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  63. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  64. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  66. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  68. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  69. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  70. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  72. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  73. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  75. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  77. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  78. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  79. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  82. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  85. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  86. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  87. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  88. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  89. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  90. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  94. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  97. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  98. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  99. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  102. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  104. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  105. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  106. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  108. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  110. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  111. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  112. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  113. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  114. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  115. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  116. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  119. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  120. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  121. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  122. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  123. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  124. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  125. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  126. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  127. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  128. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  129. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  130. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  131. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  132. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  133. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  134. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  135. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  136. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  137. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  138. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  139. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  140. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  141. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  142. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  143. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  144. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  145. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  146. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  147. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  148. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  149. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  150. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  151. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  152. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  153. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  154. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  155. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  156. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  157. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  158. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  159. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  160. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  161. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  162. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  163. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  164. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  165. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  166. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  167. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  168. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  169. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  170. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  171. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  172. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  173. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  174. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  175. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  176. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  177. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  178. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  179. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  180. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  181. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  182. package/package.json +2 -2
@@ -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 () => {
750
+ assertCurrent();
751
+ const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
730
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: (
@@ -1284,6 +1285,7 @@ export function createTelegramBusFollowerRegistrationState(
1284
1285
  let threadName: string | undefined;
1285
1286
  let displayTitle: string | undefined;
1286
1287
  let generation: string | undefined;
1288
+ let sessionId: string | undefined;
1287
1289
  let leaderProtocol: TelegramBusProtocolIdentity | undefined;
1288
1290
  let eligibleElectionSlots: string[] = [];
1289
1291
  let recoveryEpoch = 0;
@@ -1313,6 +1315,7 @@ export function createTelegramBusFollowerRegistrationState(
1313
1315
  return true;
1314
1316
  },
1315
1317
  getGeneration: () => generation,
1318
+ getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
1316
1319
  beginRecovery: () => {
1317
1320
  if (activeRecoveryEpoch !== undefined) return activeRecoveryEpoch;
1318
1321
  activeRecoveryEpoch = ++recoveryEpoch;
@@ -1354,6 +1357,7 @@ export function createTelegramBusFollowerRegistrationState(
1354
1357
  generation === metadata?.generation &&
1355
1358
  target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
1356
1359
  ? displayTitle : undefined;
1360
+ const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
1357
1361
  const availabilityChanged = registered !== next;
1358
1362
  registered = next;
1359
1363
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
@@ -1363,6 +1367,7 @@ export function createTelegramBusFollowerRegistrationState(
1363
1367
  metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
1364
1368
  ? metadata.displayTitle : retainedDisplayTitle;
1365
1369
  generation = next ? metadata?.generation : undefined;
1370
+ sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
1366
1371
  leaderProtocol =
1367
1372
  next && metadata?.leaderProtocol
1368
1373
  ? {
@@ -1656,7 +1661,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1656
1661
  let activeLeaderSocketPath: string | undefined;
1657
1662
  let activeAuthSecret: string | undefined;
1658
1663
  let activeRegistrationGeneration: string | undefined;
1659
- let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined } | undefined;
1664
+ let registrationAttempt: { ctx: TContext; sessionGeneration: number | undefined; sessionId: string | undefined } | undefined;
1660
1665
  let activeContext: TContext | undefined;
1661
1666
  let lastKnownTarget: TelegramTarget | undefined;
1662
1667
  let lastKnownSlot: string | undefined;
@@ -1793,10 +1798,12 @@ export function createTelegramBusFollowerRegistrationRuntime<
1793
1798
  registerWithLeader: async (ctx, leader, options) => {
1794
1799
  if (deps.isContextActive?.(ctx) === false) return false;
1795
1800
  const sessionGeneration = deps.getSessionGeneration?.();
1796
- const attempt = { ctx, sessionGeneration };
1801
+ const sessionId = deps.getSessionId?.(ctx);
1802
+ const attempt = { ctx, sessionGeneration, sessionId };
1797
1803
  registrationAttempt = attempt;
1798
1804
  const isCurrentRequest = () => registrationAttempt === attempt &&
1799
- deps.getSessionGeneration?.() === sessionGeneration && deps.isContextActive?.(ctx) !== false;
1805
+ deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
1806
+ deps.isContextActive?.(ctx) !== false;
1800
1807
  const abandonOwnedRequest = async (): Promise<void> => {
1801
1808
  if (registrationAttempt !== attempt) return;
1802
1809
  registrationAttempt = undefined;
@@ -1859,7 +1866,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1859
1866
  ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
1860
1867
  : {}),
1861
1868
  cwd: ctx.cwd,
1862
- sessionId: deps.getSessionId?.(ctx),
1869
+ sessionId,
1863
1870
  pid: getPid(),
1864
1871
  processBirthId: deps.getProcessBirthId?.(),
1865
1872
  sessionGeneration,
@@ -1910,7 +1917,9 @@ export function createTelegramBusFollowerRegistrationRuntime<
1910
1917
  local: deps.protocolIdentity,
1911
1918
  remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1912
1919
  });
1913
- 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) {
1914
1923
  stopHeartbeat();
1915
1924
  activeLeaderSocketPath = undefined;
1916
1925
  activeAuthSecret = undefined;
@@ -1948,6 +1957,7 @@ export function createTelegramBusFollowerRegistrationRuntime<
1948
1957
  deps.registrationState?.setRegistered(true, registrationResult.target, {
1949
1958
  ...registrationResult,
1950
1959
  generation: registrationGeneration,
1960
+ sessionId,
1951
1961
  ...(response.protocol
1952
1962
  ? { leaderProtocol: response.protocol }
1953
1963
  : {}),
@@ -1994,7 +2004,8 @@ export function createTelegramBusFollowerRegistrationRuntime<
1994
2004
  },
1995
2005
  setContext(ctx) {
1996
2006
  if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
1997
- registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.())) registrationAttempt = undefined;
2007
+ registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
2008
+ registrationAttempt.sessionId !== deps.getSessionId?.(ctx))) registrationAttempt = undefined;
1998
2009
  activeContext = ctx;
1999
2010
  },
2000
2011
  async setThreadDisplayMode(mode) {
@@ -2326,7 +2337,20 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2326
2337
  ): unknown;
2327
2338
  };
2328
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 };
2329
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
+ };
2330
2354
  return {
2331
2355
  async admit(envelope, ctx) {
2332
2356
  const delivery = envelope.delivery;
@@ -2373,12 +2397,17 @@ export function createTelegramBusFollowerDurableAdmissionRuntime<TContext>(deps:
2373
2397
  ? { message: carrier }
2374
2398
  : { edited_message: carrier }),
2375
2399
  };
2376
- deps.journal.appendBatch([update]);
2377
- deps.signalWorker(ctx);
2378
- return {
2400
+ const result = {
2379
2401
  deliveryId: delivery.deliveryId,
2380
2402
  sourceUpdateId: delivery.sourceUpdateId,
2381
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;
2382
2411
  },
2383
2412
  };
2384
2413
  }
@@ -2497,7 +2526,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2497
2526
  envelope.kind !== "leader.forwardEditedMessage" &&
2498
2527
  envelope.kind !== "leader.wakeInputCustody" &&
2499
2528
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
2500
- envelope.kind !== "leader.replaceFollowerTarget" &&
2529
+ envelope.kind !== "leader.workspaceRestore" &&
2501
2530
  envelope.kind !== "leader.offerQueueHandoff") ||
2502
2531
  envelope.recipientInstanceId !== deps.instanceId
2503
2532
  ) {
@@ -2520,13 +2549,29 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2520
2549
  message: "Stale Telegram bus follower registration generation.",
2521
2550
  };
2522
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
+ }
2523
2569
  if (envelope.kind === "leader.offerInputCustodyHandoff" &&
2524
2570
  envelope.recipientBindingKey !== deps.getRecipientBindingKey()) return {
2525
2571
  kind: "bus.ack", requestId: envelope.requestId, ok: false,
2526
2572
  message: "Mismatched Telegram follower handoff recipient identity.",
2527
2573
  };
2528
2574
  if (
2529
- envelope.kind !== "leader.replaceFollowerTarget" &&
2530
2575
  envelope.kind !== "leader.offerQueueHandoff" &&
2531
2576
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
2532
2577
  (!envelope.delivery ||
@@ -2563,10 +2608,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2563
2608
  const result = await deps.handleInputCustodyHandoff(envelope, ctx);
2564
2609
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
2565
2610
  }
2566
- if (
2567
- envelope.kind !== "leader.replaceFollowerTarget" &&
2568
- envelope.kind !== "leader.offerQueueHandoff"
2569
- ) {
2611
+ if (envelope.kind !== "leader.offerQueueHandoff") {
2570
2612
  const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
2571
2613
  (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
2572
2614
  if (sourceReferenceEnabled &&
@@ -2586,52 +2628,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
2586
2628
  result: receipt,
2587
2629
  };
2588
2630
  }
2589
- if (envelope.kind === "leader.offerQueueHandoff") {
2590
- if (!deps.handleQueueHandoff) {
2591
- throw new Error(
2592
- "Telegram bus receiver cannot accept queue handoff payloads.",
2593
- );
2594
- }
2595
- const result = await deps.handleQueueHandoff(envelope, ctx);
2596
- const receipt = envelope.payload.admissionReceipts[0];
2597
- if (
2598
- envelope.payload.admissionReceipts.length !== 1 ||
2599
- !receipt ||
2600
- result.status !== "staged" ||
2601
- result.receiptId !== receipt.receiptId ||
2602
- result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
2603
- result.sourceUpdateIds.some(
2604
- (updateId, index) => updateId !== receipt.sourceUpdateIds[index],
2605
- )
2606
- ) {
2607
- throw new Error(
2608
- "Telegram queue handoff staging returned a mismatched receipt.",
2609
- );
2610
- }
2611
- return {
2612
- kind: "bus.ack",
2613
- requestId: envelope.requestId,
2614
- ok: true,
2615
- result,
2616
- };
2631
+ if (!deps.handleQueueHandoff) {
2632
+ throw new Error(
2633
+ "Telegram bus receiver cannot accept queue handoff payloads.",
2634
+ );
2617
2635
  }
2618
- {
2619
- if (!deps.handleReplaceTarget) {
2620
- throw new Error(
2621
- "Telegram bus receiver cannot replace follower target.",
2622
- );
2623
- }
2624
- await deps.handleReplaceTarget(
2625
- {
2626
- target: envelope.target,
2627
- ...(envelope.oldTarget ? { oldTarget: envelope.oldTarget } : {}),
2628
- reason: envelope.reason,
2629
- registrationGeneration,
2630
- },
2631
- 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.",
2632
2650
  );
2633
2651
  }
2634
- 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
+ };
2635
2658
  } catch (error) {
2636
2659
  deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
2637
2660
  return {
@@ -2676,7 +2699,3 @@ function parseTarget(value: unknown): TelegramTarget | undefined {
2676
2699
  if (typeof value.threadId === "number") target.threadId = value.threadId;
2677
2700
  return target;
2678
2701
  }
2679
-
2680
- function isRecord(value: unknown): value is Record<string, unknown> {
2681
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
2682
- }