@llblab/pi-kit 0.27.5 → 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 (194) hide show
  1. package/BACKLOG.md +3 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +4 -4
  4. package/node_modules/@llblab/pi-state-flow/AGENTS.md +1 -1
  5. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +6 -2
  6. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -1
  7. package/node_modules/@llblab/pi-state-flow/dist/lib/compaction.js +4 -3
  8. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +26 -13
  9. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  10. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -1
  11. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +2 -2
  12. package/node_modules/@llblab/pi-state-flow/docs/usage.md +2 -1
  13. package/node_modules/@llblab/pi-state-flow/lib/compaction.ts +4 -3
  14. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +25 -12
  15. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  16. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  17. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  18. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  19. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  20. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  21. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  22. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  23. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  24. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  25. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  26. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  27. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  28. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  29. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  30. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  31. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  32. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  33. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  34. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  35. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  36. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  37. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  38. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  39. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  40. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  41. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  42. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  43. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  44. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  45. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  46. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  47. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  48. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  49. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  50. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  51. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  52. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  53. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  54. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  55. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  56. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  57. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  58. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  59. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  60. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  61. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  62. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  63. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  64. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  66. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  67. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  68. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  69. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  70. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  71. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  72. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  73. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  74. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  75. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  76. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  77. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  78. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  79. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  81. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  82. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  83. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  84. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  85. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  86. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  87. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  88. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  89. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  90. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  91. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  92. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  93. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  94. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  95. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  97. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  98. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  99. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  100. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  101. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  102. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  104. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  105. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  106. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  108. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  109. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  110. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  111. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  112. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  113. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  114. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  115. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  116. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  117. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  118. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  119. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  120. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  121. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  122. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  123. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  124. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  125. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  126. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  127. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  128. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  129. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  130. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  131. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  132. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  133. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  134. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  135. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  136. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  137. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  138. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  139. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  140. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  141. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  142. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  143. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  144. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  145. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  146. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  147. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  148. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  149. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  150. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  151. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  152. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  153. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  154. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  155. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  156. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  157. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  158. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  159. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  160. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  161. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  162. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  163. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  164. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  165. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  166. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  167. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  168. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  169. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  170. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  171. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  172. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  173. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  174. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  175. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  176. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  177. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  178. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  179. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  180. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  181. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  182. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  183. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  184. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  185. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  186. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  187. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  188. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  189. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  190. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  191. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  192. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  193. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  194. package/package.json +3 -3
@@ -6,21 +6,23 @@
6
6
  * It must not spawn Pi processes or create hidden Telegram-originated instances.
7
7
  */
8
8
  import { basename } from "node:path";
9
- import * as Sync from "./sync.js";
9
+ import { isDeepStrictEqual } from "node:util";
10
10
  import * as Threads from "./threads.js";
11
+ import * as WorkspaceIdentity from "./workspace-identity.js";
12
+ import { isWireRecord as isRecord } from "./wire.js";
11
13
  import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
12
14
  import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
13
15
  import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
14
- import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusFollowerTargetController, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, } from "./bus.js";
16
+ import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, } from "./bus.js";
15
17
  import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
16
18
  import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
17
- export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
18
- export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
19
- export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
20
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
19
+ const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
20
+ const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
21
+ const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
22
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
21
23
  export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
22
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
23
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
24
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
25
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
24
26
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY = "__piTelegramFollowerSessionHandoff";
25
27
  export function getTelegramFollowerSessionHandoff() {
26
28
  const value = globalThis[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY];
@@ -44,7 +46,7 @@ export function setTelegramFollowerSessionHandoff(handoff) {
44
46
  else
45
47
  store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY] = handoff;
46
48
  }
47
- export function isTelegramFollowerSessionHandoffFresh(handoff, options = {}) {
49
+ function isTelegramFollowerSessionHandoffFresh(handoff, options = {}) {
48
50
  if (!handoff)
49
51
  return false;
50
52
  const pid = options.pid ?? process.pid;
@@ -147,16 +149,21 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
147
149
  (ctx === undefined || readyContext.ctx === ctx) &&
148
150
  ports.registrationState.getGeneration() === readyContext.generation &&
149
151
  ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
152
+ ports.registration.getSessionId?.(readyContext.ctx) === readyContext.sessionId &&
150
153
  ports.registration.isContextActive?.(readyContext.ctx) !== false);
151
154
  const prepareContext = async (ctx) => {
152
155
  const generation = ports.registrationState.getGeneration();
153
156
  const sessionGeneration = ports.registration.getSessionGeneration?.();
157
+ const sessionId = ports.registration.getSessionId?.(ctx);
154
158
  readyContext = undefined;
159
+ if (!generation || (sessionId !== undefined && ports.registrationState.getSessionId(sessionId) !== sessionId))
160
+ throw new Error("Telegram follower session change requires acknowledged leader registration.");
155
161
  await ports.registration.onRegistered?.(ctx);
156
162
  if (generation && ports.registrationState.getGeneration() === generation &&
157
163
  ports.registration.getSessionGeneration?.() === sessionGeneration &&
164
+ ports.registration.getSessionId?.(ctx) === sessionId &&
158
165
  ports.registration.isContextActive?.(ctx) !== false)
159
- readyContext = { generation, ctx, sessionGeneration };
166
+ readyContext = { generation, ctx, sessionGeneration, sessionId };
160
167
  };
161
168
  const sharedRuntimeDeps = {
162
169
  instanceId: ports.instanceId,
@@ -170,10 +177,6 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
170
177
  getRegistrationGeneration() {
171
178
  return isReadyContext() ? readyContext?.generation : undefined;
172
179
  },
173
- handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
174
- ...ports.targetReplacement,
175
- ...sharedRuntimeDeps,
176
- }),
177
180
  });
178
181
  let registration;
179
182
  const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
@@ -205,83 +208,123 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
205
208
  await prepareContext(ctx);
206
209
  },
207
210
  };
208
- return { receiver, registration };
211
+ return { receiver, registration, getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined };
209
212
  }
210
- export function createTelegramBusFollowerTargetReplacementHandler(deps) {
211
- const getNowMs = deps.getNowMs ?? Date.now;
212
- return (input, ctx) => runTelegramBusFollowerWorkspaceMutation(deps, "workspace.replace-follower-target", async () => {
213
- const assertCurrent = (expectedTarget = input.oldTarget) => {
214
- const target = deps.registrationState.getTarget();
215
- if (!input.registrationGeneration ||
216
- deps.registrationState.getGeneration() !==
217
- input.registrationGeneration ||
218
- !input.oldTarget ||
219
- !expectedTarget ||
220
- target?.chatId !== expectedTarget.chatId ||
221
- target.threadId !== expectedTarget.threadId ||
222
- input.target.chatId !== input.oldTarget.chatId ||
223
- input.target.threadId === input.oldTarget.threadId) {
224
- throw new Error("Stale Telegram follower target replacement authority.");
225
- }
213
+ /** Binds Restore to real Pi lifetime and the current authenticated transport owner, not request fields. */
214
+ export function createTelegramBusFollowerRestoreContextGetter(deps) {
215
+ return (ctx) => {
216
+ if (!deps.isContextCurrent(ctx))
217
+ return undefined;
218
+ const leader = deps.getLeaderState();
219
+ const secret = deps.getAuthenticatedSecret();
220
+ const leaderProtocol = deps.getLeaderProtocol();
221
+ if (!leaderProtocol || !hasTelegramBusCapability(leaderProtocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
222
+ return undefined;
223
+ if (leader.kind !== "active-elsewhere" || !secret || leader.lock.busSecret !== secret ||
224
+ !leader.lock.instanceId || leader.lock.leaderEpoch === undefined)
225
+ return undefined;
226
+ const profileBindingKey = deps.getProfileBindingKey();
227
+ const operatorUserId = deps.getOperatorUserId();
228
+ const sessionId = deps.getSessionId(ctx);
229
+ const cwd = deps.getCwd(ctx);
230
+ const generation = deps.getGeneration();
231
+ if (!profileBindingKey || !sessionId || !cwd || typeof operatorUserId !== "number" ||
232
+ !Number.isSafeInteger(operatorUserId) || operatorUserId <= 0 ||
233
+ !Number.isSafeInteger(generation) || generation < 0 || !deps.isContextCurrent(ctx))
234
+ return undefined;
235
+ return { executor: { instanceId: leader.lock.instanceId, leaderEpoch: String(leader.lock.leaderEpoch) },
236
+ profileBindingKey, operatorUserId, sessionId, cwd, generation, leaderProtocol: structuredClone(leaderProtocol) };
237
+ };
238
+ }
239
+ /** Prepared receiver: its caller authenticates the envelope; no canonical writes, dispatch or cleanup. */
240
+ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
241
+ return async (input, ctx) => {
242
+ const captured = deps.getContextAuthority(ctx);
243
+ if (!captured)
244
+ throw new Error("Stale Telegram follower Restore context.");
245
+ const authority = structuredClone(captured);
246
+ const request = structuredClone(input);
247
+ if (!authority.profileBindingKey || !authority.sessionId || !authority.cwd ||
248
+ !authority.executor?.instanceId || !authority.executor.leaderEpoch ||
249
+ !(typeof authority.generation === "string" ? authority.generation.length > 0 :
250
+ Number.isSafeInteger(authority.generation) && authority.generation >= 0) ||
251
+ !Number.isSafeInteger(authority.operatorUserId) || authority.operatorUserId <= 0 ||
252
+ !request.operationId || !request.registrationGeneration || !["apply", "inspect"].includes(request.mode))
253
+ throw new Error("Invalid Telegram follower Restore authority.");
254
+ const assertCurrent = () => {
255
+ if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
256
+ deps.registrationState.getGeneration() !== request.registrationGeneration)
257
+ throw new Error("Stale Telegram follower Restore authority.");
226
258
  };
227
259
  assertCurrent();
228
- await deps.topicTargetStore.load();
229
- assertCurrent();
230
- const nowMs = getNowMs();
231
- const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
232
- records: deps.topicTargetStore.list(),
233
- instanceId: deps.instanceId,
234
- preferredTarget: input.oldTarget,
235
- });
236
- if (currentRecord &&
237
- (currentRecord.target.chatId !== input.oldTarget.chatId ||
238
- currentRecord.target.threadId !== input.oldTarget.threadId)) {
239
- throw new Error("Stale Telegram follower binding replacement target.");
240
- }
241
- const currentSlot = currentRecord?.slot ?? deps.registrationState.getSlot();
242
- if (!currentSlot || !/^[A-Z]$/u.test(currentSlot)) {
243
- throw new Error("Telegram Thread slot authority is unavailable.");
244
- }
245
- deps.topicTargetStore.markStaleByTarget(input.oldTarget, "deleted", "Follower thread was replaced by thread restore.");
246
- const profileKey = currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
247
- deps.topicTargetStore.upsert({
248
- profileKey,
249
- owner: {
250
- kind: "manual-follower",
251
- instanceId: deps.manualFollowerOwnerId,
252
- },
253
- target: input.target,
254
- status: "active",
255
- syncStatus: "open",
256
- createdAtMs: currentRecord?.createdAtMs ?? nowMs,
257
- updatedAtMs: nowMs,
258
- lastSyncObservedAtMs: nowMs,
259
- lastReconcileAction: "follower-thread-restore",
260
- instanceId: deps.instanceId,
261
- slot: currentSlot,
262
- threadName: currentRecord?.threadName,
263
- rerouteConfirmedAtMs: nowMs,
264
- });
265
- deps.registrationState.setRegistered(true, input.target, {
266
- slot: currentSlot,
267
- threadName: currentRecord?.threadName,
268
- generation: input.registrationGeneration,
269
- });
270
- await deps.topicTargetStore.persist();
271
- assertCurrent(input.target);
272
- deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
273
- nowMs,
274
- action: "follower-thread-restore",
275
- }));
276
- deps.updateStatus(ctx);
277
- deps.recordRuntimeEvent?.("bus", "Telegram follower thread target replaced", {
278
- phase: "follower-thread-restore",
279
- chatId: input.target.chatId,
280
- threadId: input.target.threadId,
281
- oldThreadId: input.oldTarget?.threadId ?? currentRecord?.target.threadId,
282
- slot: currentSlot,
260
+ if (!deps.getWorkspaceAdmission)
261
+ throw new Error("Telegram Workspace admission authority is unavailable.");
262
+ const result = await runTelegramBusFollowerWorkspaceMutation(deps, "workspace.restore-follower-target", async () => {
263
+ assertCurrent();
264
+ const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
265
+ assertCurrent();
266
+ if (!intent || intent.request.operationId !== request.operationId ||
267
+ (intent.phase !== "recipient-issued" && intent.phase !== "ready") ||
268
+ !isDeepStrictEqual(intent.executor, authority.executor) ||
269
+ intent.operatorUserId !== authority.operatorUserId || !intent.recipient ||
270
+ (request.mode === "apply" && (intent.recipient.kind !== "follower" || intent.recipient.instanceId !== deps.instanceId ||
271
+ intent.recipient.generation !== request.registrationGeneration)) ||
272
+ intent.recipient.sessionId !== authority.sessionId || intent.request.binding.sessionId !== authority.sessionId ||
273
+ intent.request.binding.cwd !== WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
274
+ throw new Error("Telegram follower Restore intent does not match this recipient.");
275
+ const expected = structuredClone(intent);
276
+ await deps.topicTargetStore.load();
277
+ assertCurrent();
278
+ const observe = (canonical) => {
279
+ if (!isDeepStrictEqual(deps.readRestoreIntent(request.operationId, authority.profileBindingKey), expected))
280
+ throw new Error("Telegram follower Restore intent changed.");
281
+ assertCurrent();
282
+ const { binding, target } = expected.request;
283
+ const bindings = (canonical.workspaceBindings ?? []).filter(value => value.bindingKey === binding.bindingKey);
284
+ const snapshot = canonical.threads;
285
+ const records = snapshot.filter(value => value.instanceId === deps.instanceId);
286
+ const owners = snapshot.filter(value => value.status === "active" &&
287
+ (value.slot === binding.slot || isDeepStrictEqual(value.target, target)));
288
+ if (bindings.length !== 1 || records.length !== 1 || owners.length !== 1 || bindings[0]?.cwd !== binding.cwd ||
289
+ bindings[0]?.sessionId !== binding.sessionId || bindings[0]?.slot !== binding.slot || bindings[0]?.inactiveSinceMs !== undefined ||
290
+ !isDeepStrictEqual(bindings[0]?.target, target) || records[0]?.status !== "active" ||
291
+ records[0]?.owner?.kind !== "manual-follower" ||
292
+ records[0]?.slot !== binding.slot || !isDeepStrictEqual(records[0]?.target, target) ||
293
+ deps.registrationState.getSlot() !== binding.slot)
294
+ throw new Error("Telegram follower Restore canonical binding is not committed.");
295
+ const localTarget = deps.registrationState.getTarget();
296
+ const alreadyReady = isDeepStrictEqual(localTarget, target);
297
+ if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
298
+ throw new Error("Telegram follower Restore local target changed.");
299
+ assertCurrent();
300
+ if (!alreadyReady && request.mode === "apply") {
301
+ if (expected.phase !== "recipient-issued")
302
+ throw new Error("Telegram follower Restore readiness regressed.");
303
+ deps.registrationState.setRegistered(true, target, { slot: binding.slot,
304
+ threadName: records[0]?.threadName, generation: request.registrationGeneration,
305
+ leaderProtocol: deps.registrationState.getLeaderProtocol() });
306
+ }
307
+ assertCurrent();
308
+ const observedTarget = deps.registrationState.getTarget();
309
+ if (!observedTarget || (!isDeepStrictEqual(observedTarget, target) && !isDeepStrictEqual(observedTarget, binding.target)))
310
+ throw new Error("Telegram follower Restore local target changed.");
311
+ const ready = isDeepStrictEqual(observedTarget, target);
312
+ assertCurrent();
313
+ return { operationId: request.operationId, recipient: { kind: "follower", instanceId: deps.instanceId,
314
+ sessionId: authority.sessionId, generation: request.registrationGeneration },
315
+ target: { chatId: observedTarget.chatId, threadId: observedTarget.threadId }, slot: binding.slot, ready };
316
+ };
317
+ let result;
318
+ deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, snapshot => { result = observe(snapshot); });
319
+ if (!result)
320
+ throw new Error("Telegram follower Restore observation was not completed.");
321
+ return result;
283
322
  });
284
- });
323
+ assertCurrent();
324
+ if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) || deps.registrationState.getSlot() !== result.slot)
325
+ throw new Error("Telegram follower Restore local registration changed after admission release.");
326
+ return result;
327
+ };
285
328
  }
286
329
  export function createTelegramBusFollowerClientRuntime(deps) {
287
330
  const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
@@ -319,10 +362,6 @@ export function createTelegramBusFollowerClientRuntime(deps) {
319
362
  getAuthSecret: deps.getApiAuthSecret,
320
363
  getRegistrationGeneration: deps.getRegistrationGeneration,
321
364
  }),
322
- targetController: createTelegramBusFollowerTargetController({
323
- ...sharedClientDeps,
324
- getAuthSecret: deps.getForwardingAuthSecret,
325
- }),
326
365
  };
327
366
  }
328
367
  export function createTelegramBusFollowerQueueHandoffClient(deps) {
@@ -377,7 +416,7 @@ export function createTelegramBusFollowerQueueHandoffClient(deps) {
377
416
  : "Telegram queue handoff did not return an acknowledgement.");
378
417
  };
379
418
  }
380
- export function createTelegramBusAgentMessageClient(deps) {
419
+ function createTelegramBusAgentMessageClient(deps) {
381
420
  const getNowMs = deps.getNowMs ?? Date.now;
382
421
  const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
383
422
  const request = async (envelope, requestTimeoutMs = timeoutMs) => {
@@ -652,6 +691,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
652
691
  let threadName;
653
692
  let displayTitle;
654
693
  let generation;
694
+ let sessionId;
655
695
  let leaderProtocol;
656
696
  let eligibleElectionSlots = [];
657
697
  let recoveryEpoch = 0;
@@ -677,6 +717,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
677
717
  return true;
678
718
  },
679
719
  getGeneration: () => generation,
720
+ getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
680
721
  beginRecovery: () => {
681
722
  if (activeRecoveryEpoch !== undefined)
682
723
  return activeRecoveryEpoch;
@@ -720,6 +761,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
720
761
  generation === metadata?.generation &&
721
762
  target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
722
763
  ? displayTitle : undefined;
764
+ const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
723
765
  const availabilityChanged = registered !== next;
724
766
  registered = next;
725
767
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
@@ -729,6 +771,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
729
771
  metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
730
772
  ? metadata.displayTitle : retainedDisplayTitle;
731
773
  generation = next ? metadata?.generation : undefined;
774
+ sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
732
775
  leaderProtocol =
733
776
  next && metadata?.leaderProtocol
734
777
  ? {
@@ -1086,10 +1129,12 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1086
1129
  if (deps.isContextActive?.(ctx) === false)
1087
1130
  return false;
1088
1131
  const sessionGeneration = deps.getSessionGeneration?.();
1089
- const attempt = { ctx, sessionGeneration };
1132
+ const sessionId = deps.getSessionId?.(ctx);
1133
+ const attempt = { ctx, sessionGeneration, sessionId };
1090
1134
  registrationAttempt = attempt;
1091
1135
  const isCurrentRequest = () => registrationAttempt === attempt &&
1092
- deps.getSessionGeneration?.() === sessionGeneration && deps.isContextActive?.(ctx) !== false;
1136
+ deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
1137
+ deps.isContextActive?.(ctx) !== false;
1093
1138
  const abandonOwnedRequest = async () => {
1094
1139
  if (registrationAttempt !== attempt)
1095
1140
  return;
@@ -1147,7 +1192,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1147
1192
  ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
1148
1193
  : {}),
1149
1194
  cwd: ctx.cwd,
1150
- sessionId: deps.getSessionId?.(ctx),
1195
+ sessionId,
1151
1196
  pid: getPid(),
1152
1197
  processBirthId: deps.getProcessBirthId?.(),
1153
1198
  sessionGeneration,
@@ -1203,7 +1248,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1203
1248
  local: deps.protocolIdentity,
1204
1249
  remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1205
1250
  });
1206
- if (!compatibility.compatible) {
1251
+ // An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
1252
+ const unidentifiedRejection = response?.kind === "bus.ack" && !response.ok && response.protocol === undefined;
1253
+ if (!compatibility.compatible && !unidentifiedRejection) {
1207
1254
  stopHeartbeat();
1208
1255
  activeLeaderSocketPath = undefined;
1209
1256
  activeAuthSecret = undefined;
@@ -1235,6 +1282,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1235
1282
  deps.registrationState?.setRegistered(true, registrationResult.target, {
1236
1283
  ...registrationResult,
1237
1284
  generation: registrationGeneration,
1285
+ sessionId,
1238
1286
  ...(response.protocol
1239
1287
  ? { leaderProtocol: response.protocol }
1240
1288
  : {}),
@@ -1289,7 +1337,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1289
1337
  },
1290
1338
  setContext(ctx) {
1291
1339
  if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
1292
- registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.()))
1340
+ registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
1341
+ registrationAttempt.sessionId !== deps.getSessionId?.(ctx)))
1293
1342
  registrationAttempt = undefined;
1294
1343
  activeContext = ctx;
1295
1344
  },
@@ -1557,6 +1606,17 @@ export function prepareTelegramBusFollowerJournaledUpdateForExecution(update, pr
1557
1606
  return prepared;
1558
1607
  }
1559
1608
  export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
1609
+ // The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
1610
+ const recentDeliveries = new Map();
1611
+ const getNowMs = deps.getNowMs ?? Date.now;
1612
+ const limit = deps.recentDeliveryLimit ?? { maxAgeMs: 24 * 60 * 60 * 1000, maxEntries: 4096 };
1613
+ const pruneRecentDeliveries = (nowMs) => {
1614
+ for (const [deliveryId, admittedAtMs] of recentDeliveries) {
1615
+ if (recentDeliveries.size <= limit.maxEntries && nowMs - admittedAtMs < limit.maxAgeMs)
1616
+ break;
1617
+ recentDeliveries.delete(deliveryId);
1618
+ }
1619
+ };
1560
1620
  return {
1561
1621
  async admit(envelope, ctx) {
1562
1622
  const delivery = envelope.delivery;
@@ -1598,12 +1658,18 @@ export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
1598
1658
  ? { message: carrier }
1599
1659
  : { edited_message: carrier }),
1600
1660
  };
1601
- deps.journal.appendBatch([update]);
1602
- deps.signalWorker(ctx);
1603
- return {
1661
+ const result = {
1604
1662
  deliveryId: delivery.deliveryId,
1605
1663
  sourceUpdateId: delivery.sourceUpdateId,
1606
1664
  };
1665
+ pruneRecentDeliveries(getNowMs());
1666
+ if (recentDeliveries.has(delivery.deliveryId))
1667
+ return result;
1668
+ deps.journal.appendBatch([update]);
1669
+ recentDeliveries.set(delivery.deliveryId, getNowMs());
1670
+ pruneRecentDeliveries(getNowMs());
1671
+ deps.signalWorker(ctx);
1672
+ return result;
1607
1673
  },
1608
1674
  };
1609
1675
  }
@@ -1680,7 +1746,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1680
1746
  envelope.kind !== "leader.forwardEditedMessage" &&
1681
1747
  envelope.kind !== "leader.wakeInputCustody" &&
1682
1748
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
1683
- envelope.kind !== "leader.replaceFollowerTarget" &&
1749
+ envelope.kind !== "leader.workspaceRestore" &&
1684
1750
  envelope.kind !== "leader.offerQueueHandoff") ||
1685
1751
  envelope.recipientInstanceId !== deps.instanceId) {
1686
1752
  return {
@@ -1700,14 +1766,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1700
1766
  message: "Stale Telegram bus follower registration generation.",
1701
1767
  };
1702
1768
  }
1769
+ if (envelope.kind === "leader.workspaceRestore") {
1770
+ const ctx = deps.getContext();
1771
+ if (!authSecret || !ctx || !deps.isWorkspaceRestoreEnabled?.() || !deps.handleWorkspaceRestore)
1772
+ return {
1773
+ kind: "bus.ack", requestId: envelope.requestId, ok: false, message: "Workspace Restore capability is unavailable.",
1774
+ };
1775
+ try {
1776
+ const result = await deps.handleWorkspaceRestore({ operationId: envelope.operationId,
1777
+ registrationGeneration, mode: envelope.mode }, ctx);
1778
+ if (deps.getAuthSecret?.() !== authSecret || deps.getRegistrationGeneration() !== registrationGeneration ||
1779
+ deps.getContext() !== ctx || !deps.isWorkspaceRestoreEnabled())
1780
+ throw new Error("Workspace Restore receiver authority changed.");
1781
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
1782
+ }
1783
+ catch (error) {
1784
+ deps.recordRuntimeEvent?.("bus", error, { phase: "follower-workspace-restore" });
1785
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
1786
+ message: error instanceof Error ? error.message : "Workspace Restore failed." };
1787
+ }
1788
+ }
1703
1789
  if (envelope.kind === "leader.offerInputCustodyHandoff" &&
1704
1790
  envelope.recipientBindingKey !== deps.getRecipientBindingKey())
1705
1791
  return {
1706
1792
  kind: "bus.ack", requestId: envelope.requestId, ok: false,
1707
1793
  message: "Mismatched Telegram follower handoff recipient identity.",
1708
1794
  };
1709
- if (envelope.kind !== "leader.replaceFollowerTarget" &&
1710
- envelope.kind !== "leader.offerQueueHandoff" &&
1795
+ if (envelope.kind !== "leader.offerQueueHandoff" &&
1711
1796
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
1712
1797
  (!envelope.delivery ||
1713
1798
  envelope.delivery.recipientBindingKey !==
@@ -1739,8 +1824,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1739
1824
  const result = await deps.handleInputCustodyHandoff(envelope, ctx);
1740
1825
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
1741
1826
  }
1742
- if (envelope.kind !== "leader.replaceFollowerTarget" &&
1743
- envelope.kind !== "leader.offerQueueHandoff") {
1827
+ if (envelope.kind !== "leader.offerQueueHandoff") {
1744
1828
  const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
1745
1829
  (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
1746
1830
  if (sourceReferenceEnabled &&
@@ -1758,39 +1842,25 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1758
1842
  result: receipt,
1759
1843
  };
1760
1844
  }
1761
- if (envelope.kind === "leader.offerQueueHandoff") {
1762
- if (!deps.handleQueueHandoff) {
1763
- throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
1764
- }
1765
- const result = await deps.handleQueueHandoff(envelope, ctx);
1766
- const receipt = envelope.payload.admissionReceipts[0];
1767
- if (envelope.payload.admissionReceipts.length !== 1 ||
1768
- !receipt ||
1769
- result.status !== "staged" ||
1770
- result.receiptId !== receipt.receiptId ||
1771
- result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
1772
- result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
1773
- throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
1774
- }
1775
- return {
1776
- kind: "bus.ack",
1777
- requestId: envelope.requestId,
1778
- ok: true,
1779
- result,
1780
- };
1845
+ if (!deps.handleQueueHandoff) {
1846
+ throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
1781
1847
  }
1782
- {
1783
- if (!deps.handleReplaceTarget) {
1784
- throw new Error("Telegram bus receiver cannot replace follower target.");
1785
- }
1786
- await deps.handleReplaceTarget({
1787
- target: envelope.target,
1788
- ...(envelope.oldTarget ? { oldTarget: envelope.oldTarget } : {}),
1789
- reason: envelope.reason,
1790
- registrationGeneration,
1791
- }, ctx);
1848
+ const result = await deps.handleQueueHandoff(envelope, ctx);
1849
+ const receipt = envelope.payload.admissionReceipts[0];
1850
+ if (envelope.payload.admissionReceipts.length !== 1 ||
1851
+ !receipt ||
1852
+ result.status !== "staged" ||
1853
+ result.receiptId !== receipt.receiptId ||
1854
+ result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
1855
+ result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
1856
+ throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
1792
1857
  }
1793
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
1858
+ return {
1859
+ kind: "bus.ack",
1860
+ requestId: envelope.requestId,
1861
+ ok: true,
1862
+ result,
1863
+ };
1794
1864
  }
1795
1865
  catch (error) {
1796
1866
  deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
@@ -1832,6 +1902,3 @@ function parseTarget(value) {
1832
1902
  target.threadId = value.threadId;
1833
1903
  return target;
1834
1904
  }
1835
- function isRecord(value) {
1836
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1837
- }
@@ -9,6 +9,7 @@ import type { TelegramThreadDisplayMode } from "./config.ts";
9
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
10
10
  import { type TelegramApiCallOptions } from "./telegram-api.ts";
11
11
  import type { TelegramTarget } from "./target.ts";
12
+ import type { TelegramAttachmentSource } from "./media.ts";
12
13
  import * as Threads from "./threads.ts";
13
14
  import { type TelegramBusEnvelope, type TelegramBusFollowerRegistry, type TelegramBusFollowerView, type TelegramBusInstanceRegistration, type TelegramBusProtocolIdentity, type TelegramBusSocketPathSource } from "./bus.ts";
14
15
  import type { TelegramQueueHandoffPayload } from "./queue.ts";
@@ -101,14 +102,14 @@ export interface TelegramBusFollowerDisconnectHandlerDeps {
101
102
  export interface TelegramBusLeaderApiProxyDeps {
102
103
  call: (method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<unknown>;
103
104
  callMultipart: (method: string, fields: Record<string, string>, fieldName: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<unknown>;
104
- downloadFile: (fileId: string, destinationDir: string) => Promise<unknown>;
105
+ downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<unknown>;
105
106
  recoverStaleTargetError?: (apiBody: unknown, error: unknown) => Promise<unknown> | unknown;
106
107
  }
107
108
  export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
108
109
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
109
110
  persistThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
110
111
  onThreadDisplayChanged?: () => void;
111
- runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "provisionFollowerTarget" | "provisionLeaderTarget" | "recordRuntimeEvent">;
112
+ runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "getAllowedUserId" | "provisionFollowerTarget" | "commitFollowerRegistration" | "provisionLeaderTarget" | "recordRuntimeEvent">;
112
113
  getAllowedUserId: () => number | undefined;
113
114
  instanceId: string;
114
115
  getCwd?: (ctx: TContext) => string | undefined;
@@ -168,6 +169,11 @@ export interface TelegramBusFollowerMessageOwnershipRecord {
168
169
  target?: TelegramTarget;
169
170
  }
170
171
  export type TelegramBusFollowerMessageOwnershipRecorder = (record: TelegramBusFollowerMessageOwnershipRecord) => void;
172
+ export type TelegramBusFollowerRegistrationCommitter = (input: {
173
+ registration: TelegramBusInstanceRegistration;
174
+ target?: TelegramTarget;
175
+ slot?: string;
176
+ }, publish: () => void) => Promise<void> | void;
171
177
  export interface TelegramBusLeaderRuntimeDeps<TContext> {
172
178
  socketPath: TelegramBusSocketPathSource;
173
179
  commitEndpointPublication?: (commit: () => void) => boolean;
@@ -195,6 +201,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
195
201
  provisionFollowerTarget?: (registration: TelegramBusInstanceRegistration, options?: {
196
202
  existingWorkspaceBindingOnly?: boolean;
197
203
  }) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
204
+ commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
198
205
  renameFollowerThread?: (follower: TelegramBusFollowerView, threadName: string) => Promise<{
199
206
  threadName: string;
200
207
  }> | {
@@ -209,16 +216,22 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
209
216
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
210
217
  getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
211
218
  onFollowerRegistered?: () => void;
219
+ /** Observation only: reacquire admission and inspect work, never infer completion.
220
+ * Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
221
+ onWorkspaceRestoreRecipientObserved?: (follower: TelegramBusFollowerView, isCurrent: () => boolean) => Promise<void> | void;
212
222
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
213
223
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
214
224
  getCurrentLeaderEpoch?: () => number | string | undefined;
215
225
  getTelegramProfile?: () => string | undefined;
226
+ getAllowedUserId?: () => number | undefined;
216
227
  provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
217
228
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
218
229
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
219
230
  getNowMs?: () => number;
220
231
  timeoutMs?: number;
221
232
  followerPruneIntervalMs?: number;
233
+ /** Leader housekeeping after each current prune pass; throttling is the callee's concern. */
234
+ afterFollowerPrune?: () => void;
222
235
  followerStaleAfterMs?: number;
223
236
  isFollowerProcessAlive?: (pid: number) => boolean;
224
237
  shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
@@ -281,6 +294,7 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
281
294
  slot?: string;
282
295
  threadName?: string;
283
296
  }) | undefined;
297
+ commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
284
298
  onFollowerDisconnected?: (follower: TelegramBusFollowerView) => Promise<void> | void;
285
299
  renameFollowerThread?: (follower: TelegramBusFollowerView, threadName: string) => Promise<{
286
300
  threadName: string;
@@ -296,9 +310,14 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
296
310
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
297
311
  getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
298
312
  onFollowerRegistered?: () => void;
313
+ onWorkspaceRestoreRecipientObserved?: TelegramBusLeaderRuntimeDeps<unknown>["onWorkspaceRestoreRecipientObserved"];
314
+ getWorkspaceRestoreObservationGeneration?: () => number | undefined;
315
+ recordRuntimeEvent?: TelegramBusLeaderRuntimeDeps<unknown>["recordRuntimeEvent"];
299
316
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
300
317
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
301
318
  getCurrentLeaderEpoch?: () => number | string | undefined;
319
+ getTelegramProfile?: () => string | undefined;
320
+ getAllowedUserId?: () => number | undefined;
302
321
  runFollowerMutation?: TelegramBusFollowerMutationRunner;
303
322
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
304
323
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;