@llblab/pi-telegram 0.52.2 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +61 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +249 -126
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +125 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +511 -300
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -5,25 +5,21 @@
5
5
  * leader activation hot-switching, local bus server startup, and stale follower pruning.
6
6
  */
7
7
  import { isDeepStrictEqual } from "node:util";
8
+ import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
9
+ import { createTelegramBusLocalServer, createUnauthorizedBusAck, rejectTelegramBusRequest, getTelegramBusEnvelopeTrafficClass, getTelegramBusFollowerSocketPath, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, parseTelegramBusEnvelope, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, } from "./bus.js";
10
+ import { escapeHtml } from "./rendering.js";
8
11
  import * as Sync from "./sync.js";
12
+ import { parseTelegramIntegerId as asInteger, } from "./target.js";
13
+ import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, TelegramApiAuthorityError, } from "./telegram-api.js";
9
14
  import { createTelegramThreadDisplayReconciler, resolveTelegramInitialWorkspaceDisplayName, resolveTelegramLiveWorkspaceBindingKeys, } from "./thread-display.js";
10
15
  import * as ThreadNaming from "./thread-naming.js";
11
16
  import * as ThreadReconciler from "./thread-reconciler.js";
12
- import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, } from "./telegram-api.js";
13
17
  import * as Threads from "./threads.js";
18
+ import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
14
19
  import * as WorkspaceIdentity from "./workspace-identity.js";
15
- import { createTelegramBusLocalServer, createUnauthorizedBusAck, getTelegramBusEnvelopeTrafficClass, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, getTelegramBusFollowerSocketPath, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, 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 { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
17
20
  import { createTelegramWorkspaceOperationRuntime, createTelegramWorkspaceSlotRotation, } from "./workspace-retirement.js";
18
- import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
19
- import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.js";
21
+ import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
20
22
  export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
21
- function escapeHtml(text) {
22
- return text
23
- .replace(/&/g, "&")
24
- .replace(/</g, "&lt;")
25
- .replace(/>/g, "&gt;");
26
- }
27
23
  function formatTelegramBusInstanceLabel(input) {
28
24
  const threadName = input.threadName?.trim();
29
25
  if (threadName)
@@ -52,7 +48,9 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
52
48
  typeof intent.target.threadId !== "number" ||
53
49
  follower.target?.chatId !== intent.target.chatId ||
54
50
  follower.target.threadId !== intent.target.threadId ||
55
- !cwd || cwd !== intent.cwd || !sessionId ||
51
+ !cwd ||
52
+ cwd !== intent.cwd ||
53
+ !sessionId ||
56
54
  intent.profileName !== (deps.getTelegramProfile?.() ?? "default")) {
57
55
  throw new Error("Telegram session replacement does not match the current follower registration.");
58
56
  }
@@ -72,7 +70,8 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
72
70
  if (!isCurrent())
73
71
  return false;
74
72
  const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
75
- if (!binding || binding.cwd !== intent.cwd ||
73
+ if (!binding ||
74
+ binding.cwd !== intent.cwd ||
76
75
  binding.sessionId !== intent.sourceSessionId ||
77
76
  binding.slot !== intent.slot ||
78
77
  (binding.manualThreadName ?? binding.threadName) !== intent.threadName) {
@@ -91,8 +90,8 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps) {
91
90
  await deps.store.load();
92
91
  if (!isCurrent())
93
92
  return false;
94
- if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
95
- intent.cwd) {
93
+ if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
94
+ ?.cwd !== intent.cwd) {
96
95
  throw new Error("Telegram session replacement successor binding is unavailable.");
97
96
  }
98
97
  return deps.store.removeSessionReplacementIntent(intent, isCurrent);
@@ -129,13 +128,19 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
129
128
  });
130
129
  },
131
130
  }).run;
132
- const runWithWorkspaceCapacity = deps.workspaceRotation && deps.captureWorkspaceExternalProtection &&
133
- deps.getCurrentLeaderEpoch ? createTelegramWorkspaceSlotRotation({
134
- ...deps.workspaceRotation, store: deps.topicTargetStore,
135
- getLeaderEpoch: deps.getCurrentLeaderEpoch,
136
- getExternalProtection: deps.captureWorkspaceExternalProtection,
137
- recordEvent(message, details) { deps.recordRuntimeEvent("bus", message, details); },
138
- }) : undefined;
131
+ const runWithWorkspaceCapacity = deps.workspaceRotation &&
132
+ deps.captureWorkspaceExternalProtection &&
133
+ deps.getCurrentLeaderEpoch
134
+ ? createTelegramWorkspaceSlotRotation({
135
+ ...deps.workspaceRotation,
136
+ store: deps.topicTargetStore,
137
+ getLeaderEpoch: deps.getCurrentLeaderEpoch,
138
+ getExternalProtection: deps.captureWorkspaceExternalProtection,
139
+ recordEvent(message, details) {
140
+ deps.recordRuntimeEvent("bus", message, details);
141
+ },
142
+ })
143
+ : undefined;
139
144
  const captureLiveBindingKeys = (bindings) => resolveTelegramLiveWorkspaceBindingKeys(bindings, deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target, deps.runtime.followerRegistry.list());
140
145
  const provisionerPorts = {
141
146
  getAllowedUserId: deps.getAllowedUserId,
@@ -161,29 +166,34 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
161
166
  runWorkspaceOperation,
162
167
  recordRuntimeEvent: deps.recordRuntimeEvent,
163
168
  };
164
- const display = deps.getThreadDisplayMode ? createTelegramThreadDisplayReconciler({
165
- store: deps.topicTargetStore,
166
- getMode: deps.getThreadDisplayMode,
167
- getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
168
- getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
169
- captureLiveBindingKeys,
170
- captureBindingAuthority(binding) {
171
- const matches = (target) => target?.chatId === binding.target.chatId && target?.threadId === binding.target.threadId;
172
- const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
173
- if (leader && matches(leader.target)) {
174
- return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target);
175
- }
176
- const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
177
- if (!follower?.registrationGeneration)
178
- return undefined;
179
- const generation = follower.registrationGeneration;
180
- return () => {
181
- const current = deps.runtime.followerRegistry.get(follower.instanceId);
182
- return current?.registrationGeneration === generation && matches(current.target);
183
- };
184
- },
185
- callApi: deps.callApi,
186
- }) : undefined;
169
+ const display = deps.getThreadDisplayMode
170
+ ? createTelegramThreadDisplayReconciler({
171
+ store: deps.topicTargetStore,
172
+ getMode: deps.getThreadDisplayMode,
173
+ getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
174
+ getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
175
+ captureLiveBindingKeys,
176
+ captureBindingAuthority(binding) {
177
+ const matches = (target) => target?.chatId === binding.target.chatId &&
178
+ target?.threadId === binding.target.threadId;
179
+ const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
180
+ if (leader && matches(leader.target)) {
181
+ return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)
182
+ ?.target);
183
+ }
184
+ const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
185
+ if (!follower?.registrationGeneration)
186
+ return undefined;
187
+ const generation = follower.registrationGeneration;
188
+ return () => {
189
+ const current = deps.runtime.followerRegistry.get(follower.instanceId);
190
+ return (current?.registrationGeneration === generation &&
191
+ matches(current.target));
192
+ };
193
+ },
194
+ callApi: deps.callApi,
195
+ })
196
+ : undefined;
187
197
  const reconcileThreadDisplayOperation = async () => {
188
198
  const result = await display.reconcile();
189
199
  deps.onThreadDisplayChanged?.();
@@ -208,7 +218,9 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
208
218
  if (!display || !startupDisplaySettled)
209
219
  return;
210
220
  void reconcileThreadDisplay().catch((error) => {
211
- deps.recordRuntimeEvent("bus", error, { phase: "thread-display-reconcile" });
221
+ deps.recordRuntimeEvent("bus", error, {
222
+ phase: "thread-display-reconcile",
223
+ });
212
224
  });
213
225
  };
214
226
  const beginStartupDisplayStabilization = () => {
@@ -217,8 +229,7 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
217
229
  };
218
230
  const armStartupDisplayReconciliation = () => {
219
231
  const generation = startupDisplayGeneration;
220
- const delayMs = deps.runtime.followerStaleAfterMs ??
221
- TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
232
+ const delayMs = deps.runtime.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
222
233
  startupDisplayTimer = setTimeout(() => {
223
234
  if (startupDisplayGeneration !== generation)
224
235
  return;
@@ -232,8 +243,10 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
232
243
  const applyThreadDisplayMode = (mode, isCurrent) => {
233
244
  const epoch = deps.getCurrentLeaderEpoch?.();
234
245
  const profile = deps.getTelegramProfile?.();
235
- const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
236
- deps.getTelegramProfile?.() === profile && isCurrent();
246
+ const current = () => epoch !== undefined &&
247
+ deps.getCurrentLeaderEpoch?.() === epoch &&
248
+ deps.getTelegramProfile?.() === profile &&
249
+ isCurrent();
237
250
  const run = modeTail.then(() => runWorkspaceOperation({
238
251
  operationId: createTelegramWorkspaceAdmissionOperationId(),
239
252
  operationKind: "workspace.set-display-mode",
@@ -246,7 +259,10 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
246
259
  const requiredCapability = mode === "directory-snake" || mode === "directory-title"
247
260
  ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
248
261
  : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
249
- if (mode !== "names" && deps.runtime.followerRegistry.list().some((follower) => !hasTelegramBusCapability(follower.protocol, requiredCapability)))
262
+ if (mode !== "names" &&
263
+ deps.runtime.followerRegistry
264
+ .list()
265
+ .some((follower) => !hasTelegramBusCapability(follower.protocol, requiredCapability)))
250
266
  throw new Error("Update or restart all connected followers before changing Thread display mode.");
251
267
  };
252
268
  assertDisplayPeers();
@@ -284,121 +300,286 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
284
300
  const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
285
301
  ...provisionerPorts,
286
302
  });
287
- const renameLeaderThreadAdmitted = async (threadName, expectedTarget) => {
288
- const leaderEpoch = deps.getCurrentLeaderEpoch?.();
289
- if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
303
+ const renameLeaderThreadAdmitted = async (threadName, expectedTarget, assertRecipientAuthority) => {
304
+ const store = deps.topicTargetStore, callApi = deps.callApi;
305
+ const publishName = store.renameByTargetAndPersist.bind(store);
306
+ const persist = store.persist.bind(store);
307
+ const instanceId = deps.instanceId;
308
+ const getProfile = deps.getTelegramProfile, getUser = deps.getAllowedUserId;
309
+ const getEpoch = deps.getCurrentLeaderEpoch;
310
+ const profile = getProfile?.(), user = getUser();
311
+ const recipientAuthority = assertRecipientAuthority;
312
+ recipientAuthority?.();
313
+ const leaderEpoch = getEpoch?.();
314
+ if (getEpoch && leaderEpoch === undefined) {
290
315
  throw new Error("Telegram Workspace Thread rename requires leader ownership.");
291
316
  }
292
317
  const record = Threads.findCurrentTelegramInstanceThreadRecord({
293
- records: deps.topicTargetStore.list(),
294
- instanceId: deps.instanceId,
318
+ records: store.list(),
319
+ instanceId,
295
320
  });
296
321
  if (typeof record?.target.threadId !== "number") {
297
322
  throw new Error("No Workspace Thread is bound to this Pi instance.");
298
323
  }
299
- if (expectedTarget && (record.target.chatId !== expectedTarget.chatId ||
300
- record.target.threadId !== expectedTarget.threadId)) {
324
+ if (expectedTarget &&
325
+ (record.target.chatId !== expectedTarget.chatId ||
326
+ record.target.threadId !== expectedTarget.threadId)) {
301
327
  throw new Error("Telegram Workspace Thread rename target changed.");
302
328
  }
329
+ const target = { ...record.target }, slot = record.slot, owner = structuredClone(record.owner);
330
+ const bindingIdentity = (binding) => ({
331
+ bindingKey: binding.bindingKey,
332
+ cwd: binding.cwd,
333
+ workspaceKey: binding.workspaceKey,
334
+ sessionId: binding.sessionId,
335
+ sessionKey: binding.sessionKey,
336
+ instanceSlot: binding.instanceSlot,
337
+ slot: binding.slot,
338
+ target: { ...binding.target },
339
+ });
340
+ const bindings = store
341
+ .listWorkspaceBindings()
342
+ .filter((binding) => binding.target.chatId === target.chatId &&
343
+ binding.target.threadId === target.threadId);
344
+ if (recipientAuthority &&
345
+ (bindings.length !== 1 ||
346
+ !bindings[0]?.sessionId ||
347
+ bindings[0].slot !== slot))
348
+ throw new Error("Telegram Workspace Thread rename recipient binding authority is unavailable.");
349
+ const capturedBindings = bindings.map(bindingIdentity);
350
+ const assertAuthority = () => {
351
+ recipientAuthority?.();
352
+ if ((getEpoch && getEpoch() !== leaderEpoch) ||
353
+ getProfile?.() !== profile ||
354
+ getUser() !== user ||
355
+ deps.topicTargetStore !== store ||
356
+ deps.instanceId !== instanceId)
357
+ throw new Error("Telegram Workspace Thread rename lost leader ownership.");
358
+ const current = Threads.findCurrentTelegramInstanceThreadRecord({
359
+ records: store.list(),
360
+ instanceId,
361
+ });
362
+ const currentBindings = store
363
+ .listWorkspaceBindings()
364
+ .filter((binding) => binding.target.chatId === target.chatId &&
365
+ binding.target.threadId === target.threadId)
366
+ .map(bindingIdentity);
367
+ if (current?.target.chatId !== target.chatId ||
368
+ current.target.threadId !== target.threadId ||
369
+ current.slot !== slot ||
370
+ current.status !== record.status ||
371
+ !isDeepStrictEqual(current.owner, owner) ||
372
+ !isDeepStrictEqual(currentBindings, capturedBindings))
373
+ throw new Error("Telegram Workspace Thread rename target changed or binding authority was lost.");
374
+ };
375
+ assertAuthority();
376
+ const current = () => {
377
+ try {
378
+ assertAuthority();
379
+ return true;
380
+ }
381
+ catch {
382
+ return false;
383
+ }
384
+ };
303
385
  const rename = Threads.createTelegramTopicTargetRenamer({
304
- store: deps.topicTargetStore,
305
- callApi: deps.callApi,
306
- assertAuthority() {
307
- if (deps.getCurrentLeaderEpoch &&
308
- deps.getCurrentLeaderEpoch() !== leaderEpoch) {
309
- throw new Error("Telegram Workspace Thread rename lost leader ownership.");
310
- }
311
- const current = Threads.findCurrentTelegramInstanceThreadRecord({
312
- records: deps.topicTargetStore.list(),
313
- instanceId: deps.instanceId,
314
- });
315
- if (current?.target.chatId !== record.target.chatId ||
316
- current.target.threadId !== record.target.threadId) {
317
- throw new Error("Telegram Workspace Thread rename target changed.");
386
+ store: recipientAuthority
387
+ ? {
388
+ list: store.list.bind(store),
389
+ listWorkspaceBindings: store.listWorkspaceBindings.bind(store),
390
+ listPendingProvisions: store.listPendingProvisions.bind(store),
391
+ renameByTarget: (target, name, options) => publishName(target, name, { updateDisplayTitle: options?.updateDisplayTitle ?? true }, current),
318
392
  }
319
- },
320
- });
321
- const renamed = await rename({
322
- target: {
323
- chatId: record.target.chatId,
324
- threadId: record.target.threadId,
325
- },
326
- threadName,
327
- slot: record.slot,
393
+ : store,
394
+ callApi,
395
+ assertAuthority,
328
396
  });
397
+ const renamed = await rename({ target, threadName, slot });
398
+ assertAuthority();
329
399
  if (!renamed) {
330
- throw new Error(ThreadNaming.getTelegramTopicThreadNameValidationError(threadName, record.slot) ?? "Telegram Workspace Thread name is already reserved.");
400
+ throw new Error(ThreadNaming.getTelegramTopicThreadNameValidationError(threadName, slot) ?? "Telegram Workspace Thread name is already reserved.");
331
401
  }
332
- await deps.topicTargetStore.persist();
402
+ if (!recipientAuthority)
403
+ await persist(current);
404
+ assertAuthority();
333
405
  return renamed;
334
406
  };
335
- const resetThreadNameAdmitted = async (target, assertTargetCurrent = () => undefined) => {
336
- const leaderEpoch = deps.getCurrentLeaderEpoch?.();
337
- if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
407
+ const prepareThreadNameReset = (expectedTarget, assertTargetCurrent, assertRecipientAuthority) => {
408
+ const target = { ...expectedTarget }, store = deps.topicTargetStore, callApi = deps.callApi;
409
+ const publishReset = store.clearManualNameByTargetAndPersist.bind(store);
410
+ const clearManualName = store.clearManualNameByTarget.bind(store), persist = store.persist.bind(store);
411
+ const getEpoch = deps.getCurrentLeaderEpoch, getProfile = deps.getTelegramProfile;
412
+ const getUser = deps.getAllowedUserId, getMode = deps.getThreadDisplayMode;
413
+ const recipientAuthority = assertRecipientAuthority, targetAuthority = assertTargetCurrent;
414
+ recipientAuthority?.();
415
+ const leaderEpoch = getEpoch?.(), profile = getProfile?.(), user = getUser(), instanceId = deps.instanceId;
416
+ const mode = getMode?.() ?? "letters";
417
+ if (getEpoch && leaderEpoch === undefined) {
338
418
  throw new Error("Telegram Workspace Thread reset requires leader ownership.");
339
419
  }
340
- const binding = deps.topicTargetStore.listWorkspaceBindings().find((candidate) => candidate.target.chatId === target.chatId &&
420
+ const matchingBindings = () => store
421
+ .listWorkspaceBindings()
422
+ .filter((candidate) => candidate.target.chatId === target.chatId &&
341
423
  candidate.target.threadId === target.threadId);
424
+ const bindings = matchingBindings(), binding = bindings[0];
342
425
  if (!binding)
343
426
  throw new Error("No Workspace Thread binding is available to reset.");
344
- const automaticTitle = resolveTelegramInitialWorkspaceDisplayName({
345
- bindings: deps.topicTargetStore.listWorkspaceBindings(),
346
- binding: { ...binding, manualThreadName: undefined },
347
- mode: deps.getThreadDisplayMode?.() ?? "letters",
427
+ const record = Threads.findCurrentTelegramInstanceThreadRecord({
428
+ records: store.list(),
429
+ instanceId,
430
+ });
431
+ if (recipientAuthority &&
432
+ (bindings.length !== 1 ||
433
+ !binding.sessionId ||
434
+ record?.status !== "active" ||
435
+ record.owner?.kind !== "leader" ||
436
+ record.owner.cwd !== binding.cwd ||
437
+ record.target.chatId !== target.chatId ||
438
+ record.target.threadId !== target.threadId ||
439
+ record.slot !== binding.slot))
440
+ throw new Error("Telegram Workspace Thread reset recipient binding authority is unavailable.");
441
+ const bindingIdentity = (value) => ({
442
+ bindingKey: value.bindingKey,
443
+ cwd: value.cwd,
444
+ workspaceKey: value.workspaceKey,
445
+ sessionId: value.sessionId,
446
+ sessionKey: value.sessionKey,
447
+ instanceSlot: value.instanceSlot,
448
+ slot: value.slot,
449
+ target: { ...value.target },
450
+ });
451
+ const capturedBinding = bindingIdentity(binding), owner = record && structuredClone(record.owner);
452
+ const resolveTitle = (value) => resolveTelegramInitialWorkspaceDisplayName({
453
+ bindings: store.listWorkspaceBindings(),
454
+ binding: { ...value, manualThreadName: undefined },
455
+ mode,
348
456
  preserveRetainedManualName: false,
349
457
  });
350
- if (!automaticTitle) {
458
+ const automaticTitle = resolveTitle(binding);
459
+ if (!automaticTitle)
351
460
  throw new Error("Telegram Workspace automatic title is unavailable.");
352
- }
353
- assertTargetCurrent();
354
- await deps.callApi("editForumTopic", {
355
- chat_id: target.chatId,
356
- message_thread_id: target.threadId,
357
- name: automaticTitle,
358
- });
359
- if (deps.getCurrentLeaderEpoch &&
360
- deps.getCurrentLeaderEpoch() !== leaderEpoch) {
361
- throw new Error("Telegram Workspace Thread reset lost leader ownership.");
362
- }
363
- assertTargetCurrent();
364
- const reset = deps.topicTargetStore.clearManualNameByTarget(target, automaticTitle);
365
- if (!reset)
366
- throw new Error("Telegram Workspace Thread reset changed binding.");
367
- await deps.topicTargetStore.persist();
368
- return { threadName: automaticTitle };
461
+ const assertAuthority = () => {
462
+ recipientAuthority?.();
463
+ if (getEpoch && getEpoch() !== leaderEpoch)
464
+ throw new Error("Telegram Workspace Thread reset lost leader ownership.");
465
+ targetAuthority();
466
+ if (!recipientAuthority)
467
+ return;
468
+ if (getProfile?.() !== profile ||
469
+ getUser() !== user ||
470
+ deps.topicTargetStore !== store ||
471
+ deps.instanceId !== instanceId)
472
+ throw new Error("Telegram Workspace Thread reset lost recipient authority.");
473
+ const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
474
+ records: store.list(),
475
+ instanceId,
476
+ });
477
+ const currentBindings = matchingBindings();
478
+ if (currentRecord?.target.chatId !== target.chatId ||
479
+ currentRecord.target.threadId !== target.threadId ||
480
+ currentRecord.slot !== record.slot ||
481
+ currentRecord.status !== record.status ||
482
+ !isDeepStrictEqual(currentRecord.owner, owner) ||
483
+ currentBindings.length !== 1 ||
484
+ !isDeepStrictEqual(bindingIdentity(currentBindings[0]), capturedBinding))
485
+ throw new Error("Telegram Workspace Thread reset target changed or binding authority was lost.");
486
+ if ((getMode?.() ?? "letters") !== mode ||
487
+ resolveTitle(currentBindings[0]) !== automaticTitle)
488
+ throw new Error("Telegram Workspace automatic-title policy changed.");
489
+ };
490
+ assertAuthority();
491
+ const current = () => {
492
+ try {
493
+ assertAuthority();
494
+ return true;
495
+ }
496
+ catch {
497
+ return false;
498
+ }
499
+ };
500
+ return async () => {
501
+ assertAuthority();
502
+ const body = {
503
+ chat_id: target.chatId,
504
+ message_thread_id: target.threadId,
505
+ name: automaticTitle,
506
+ };
507
+ if (recipientAuthority)
508
+ await callApi("editForumTopic", body, { assertAuthority });
509
+ else
510
+ await callApi("editForumTopic", body);
511
+ assertAuthority();
512
+ if (recipientAuthority &&
513
+ (!isDeepStrictEqual(matchingBindings()[0], binding) ||
514
+ !isDeepStrictEqual(Threads.findCurrentTelegramInstanceThreadRecord({
515
+ records: store.list(),
516
+ instanceId,
517
+ }), record)))
518
+ throw new Error("Telegram Workspace Thread reset metadata changed before publication.");
519
+ const reset = recipientAuthority
520
+ ? await publishReset(target, automaticTitle, current)
521
+ : clearManualName(target, automaticTitle);
522
+ if (!reset)
523
+ throw new Error("Telegram Workspace Thread reset changed binding.");
524
+ if (!recipientAuthority)
525
+ await persist();
526
+ assertAuthority();
527
+ if (recipientAuthority &&
528
+ (reset.manualThreadName !== undefined ||
529
+ matchingBindings()[0]?.manualThreadName !== undefined ||
530
+ matchingBindings()[0]?.displayTitle !== automaticTitle ||
531
+ store.getActiveByInstanceId(instanceId)?.manualThreadName !==
532
+ undefined))
533
+ throw new Error("Telegram Workspace Thread reset result metadata changed.");
534
+ return { threadName: automaticTitle };
535
+ };
369
536
  };
370
- const renameLeaderThread = (threadName) => runWorkspaceOperation({
371
- operationId: createTelegramWorkspaceAdmissionOperationId(),
372
- operationKind: "workspace.rename-leader",
373
- scopes: [{ kind: "profile" }],
374
- }, () => renameLeaderThreadAdmitted(threadName));
375
- const resetLeaderThreadName = (expectedTarget) => runWorkspaceOperation({
376
- operationId: createTelegramWorkspaceAdmissionOperationId(),
377
- operationKind: "workspace.reset-leader-name",
378
- scopes: [{ kind: "profile" }],
379
- }, () => {
380
- const record = Threads.findCurrentTelegramInstanceThreadRecord({
381
- records: deps.topicTargetStore.list(),
382
- instanceId: deps.instanceId,
537
+ const resetThreadNameAdmitted = async (target, assertTargetCurrent = () => undefined) => prepareThreadNameReset(target, assertTargetCurrent)();
538
+ const renameLeaderThread = async (threadName) => {
539
+ const getEpoch = deps.getCurrentLeaderEpoch, getProfile = deps.getTelegramProfile;
540
+ const getUser = deps.getAllowedUserId, store = deps.topicTargetStore;
541
+ const epoch = getEpoch?.(), profile = getProfile?.(), user = getUser();
542
+ return runWorkspaceOperation({
543
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
544
+ operationKind: "workspace.rename-leader",
545
+ scopes: [{ kind: "profile" }],
546
+ }, () => {
547
+ if (getEpoch?.() !== epoch ||
548
+ getProfile?.() !== profile ||
549
+ getUser() !== user ||
550
+ deps.topicTargetStore !== store)
551
+ throw new Error("Telegram Workspace Thread rename lost leader ownership during admission.");
552
+ return renameLeaderThreadAdmitted(threadName);
383
553
  });
384
- if (!record || typeof record.target.threadId !== "number") {
385
- throw new Error("No Workspace Thread is bound to this Pi instance.");
386
- }
387
- if (record.target.chatId !== expectedTarget.chatId ||
388
- record.target.threadId !== expectedTarget.threadId) {
389
- throw new Error("Telegram Workspace Thread reset target changed.");
390
- }
391
- return resetThreadNameAdmitted(record.target, () => {
392
- const current = Threads.findCurrentTelegramInstanceThreadRecord({
393
- records: deps.topicTargetStore.list(),
394
- instanceId: deps.instanceId,
554
+ };
555
+ const resetLeaderThreadName = async (expectedTarget, assertRecipientAuthority) => {
556
+ const target = { ...expectedTarget }, store = deps.topicTargetStore, instanceId = deps.instanceId;
557
+ const assertTargetCurrent = () => {
558
+ const record = Threads.findCurrentTelegramInstanceThreadRecord({
559
+ records: store.list(),
560
+ instanceId,
395
561
  });
396
- if (current?.target.chatId !== expectedTarget.chatId ||
397
- current.target.threadId !== expectedTarget.threadId) {
562
+ if (!record || typeof record.target.threadId !== "number")
563
+ throw new Error("No Workspace Thread is bound to this Pi instance.");
564
+ if (record.target.chatId !== target.chatId ||
565
+ record.target.threadId !== target.threadId)
398
566
  throw new Error("Telegram Workspace Thread reset target changed.");
399
- }
567
+ };
568
+ // Selected lifetimes are captured before admission; ordinary reset keeps its post-admission preparation.
569
+ const prepared = assertRecipientAuthority
570
+ ? prepareThreadNameReset(target, assertTargetCurrent, assertRecipientAuthority)
571
+ : undefined;
572
+ return runWorkspaceOperation({
573
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
574
+ operationKind: "workspace.reset-leader-name",
575
+ scopes: [{ kind: "profile" }],
576
+ }, () => {
577
+ assertTargetCurrent();
578
+ return prepared
579
+ ? prepared()
580
+ : resetThreadNameAdmitted(target, assertTargetCurrent);
400
581
  });
401
- });
582
+ };
402
583
  const followerSessionReplacement = createTelegramBusFollowerSessionReplacementAuthority({
403
584
  store: deps.topicTargetStore,
404
585
  getTelegramProfile: deps.getTelegramProfile,
@@ -420,10 +601,14 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
420
601
  : { kind: "profile" },
421
602
  ],
422
603
  }, () => provisionLeaderTarget(ctx));
423
- return runWithWorkspaceCapacity ? runWithWorkspaceCapacity(provision) : provision();
604
+ return runWithWorkspaceCapacity
605
+ ? runWithWorkspaceCapacity(provision)
606
+ : provision();
424
607
  },
425
608
  getFollowerDisplayTitle(follower) {
426
- const binding = deps.topicTargetStore.listWorkspaceBindings().find((binding) => binding.target.chatId === follower.target?.chatId &&
609
+ const binding = deps.topicTargetStore
610
+ .listWorkspaceBindings()
611
+ .find((binding) => binding.target.chatId === follower.target?.chatId &&
427
612
  binding.target.threadId === follower.target?.threadId);
428
613
  return binding?.displayTitle ?? binding?.threadName;
429
614
  },
@@ -457,7 +642,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
457
642
  throw new Error("Telegram Workspace Thread rename lost leader ownership.");
458
643
  }
459
644
  const current = deps.runtime.followerRegistry.get(follower.instanceId);
460
- if (current?.registrationGeneration !== follower.registrationGeneration ||
645
+ if (current?.registrationGeneration !==
646
+ follower.registrationGeneration ||
461
647
  current?.target?.chatId !== follower.target?.chatId ||
462
648
  current?.target?.threadId !== follower.target?.threadId) {
463
649
  throw new Error("Telegram follower Workspace Thread target changed.");
@@ -488,7 +674,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
488
674
  operationKind: "workspace.reset-follower-name",
489
675
  scopes: [{ kind: "profile" }],
490
676
  }, () => {
491
- if (!follower.target || typeof follower.target.threadId !== "number") {
677
+ if (!follower.target ||
678
+ typeof follower.target.threadId !== "number") {
492
679
  throw new Error("Telegram follower has no bound Workspace Thread.");
493
680
  }
494
681
  const target = {
@@ -497,7 +684,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
497
684
  };
498
685
  return resetThreadNameAdmitted(target, () => {
499
686
  const current = deps.runtime.followerRegistry.get(follower.instanceId);
500
- if (current?.registrationGeneration !== follower.registrationGeneration ||
687
+ if (current?.registrationGeneration !==
688
+ follower.registrationGeneration ||
501
689
  current?.target?.chatId !== target.chatId ||
502
690
  current?.target?.threadId !== target.threadId) {
503
691
  throw new Error("Telegram follower Workspace Thread target changed.");
@@ -525,10 +713,12 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
525
713
  async onFollowerConfirmedDeadPreserved(follower, isDetached, operationId) {
526
714
  const epoch = deps.getCurrentLeaderEpoch?.();
527
715
  const profile = deps.getTelegramProfile?.();
528
- if (epoch === undefined || !follower.registrationGeneration ||
716
+ if (epoch === undefined ||
717
+ !follower.registrationGeneration ||
529
718
  typeof follower.target?.threadId !== "number")
530
719
  return false;
531
- const isCurrent = () => isDetached() && deps.getCurrentLeaderEpoch?.() === epoch &&
720
+ const isCurrent = () => isDetached() &&
721
+ deps.getCurrentLeaderEpoch?.() === epoch &&
532
722
  deps.getTelegramProfile?.() === profile;
533
723
  if (!isCurrent())
534
724
  return false;
@@ -543,7 +733,9 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
543
733
  if (!isCurrent())
544
734
  return false;
545
735
  const record = deps.topicTargetStore.getActiveByInstanceId(follower.instanceId);
546
- if (!record || (follower.profileKey && record.profileKey !== follower.profileKey) ||
736
+ if (!record ||
737
+ (follower.profileKey &&
738
+ record.profileKey !== follower.profileKey) ||
547
739
  record.target.chatId !== follower.target.chatId ||
548
740
  record.target.threadId !== follower.target.threadId)
549
741
  return true;
@@ -564,7 +756,8 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
564
756
  }, () => provisionFollowerTarget(registration, options)),
565
757
  commitFollowerRegistration: (input, publish) => runWorkspaceOperation({
566
758
  operationId: createTelegramWorkspaceAdmissionOperationId(),
567
- operationKind: "workspace.publish-follower-registration", scopes: [{ kind: "profile" }],
759
+ operationKind: "workspace.publish-follower-registration",
760
+ scopes: [{ kind: "profile" }],
568
761
  }, async () => {
569
762
  if (!input.target) {
570
763
  publish();
@@ -572,10 +765,18 @@ export function createTelegramBusLeaderRuntimeAssembly(deps) {
572
765
  }
573
766
  await deps.topicTargetStore.load();
574
767
  const binding = input.registration.cwd && input.registration.sessionId
575
- ? deps.topicTargetStore.getWorkspaceBindingByTarget(input.target, input.registration.sessionId) : undefined;
576
- const bindingKey = binding && input.registration.cwd && binding.cwd === WorkspaceIdentity.normalizeTelegramWorkspacePath(input.registration.cwd)
577
- ? binding.bindingKey : undefined;
578
- if (input.target.threadId !== undefined && input.registration.cwd && input.registration.sessionId && !bindingKey)
768
+ ? deps.topicTargetStore.getWorkspaceBindingByTarget(input.target, input.registration.sessionId)
769
+ : undefined;
770
+ const bindingKey = binding &&
771
+ input.registration.cwd &&
772
+ binding.cwd ===
773
+ WorkspaceIdentity.normalizeTelegramWorkspacePath(input.registration.cwd)
774
+ ? binding.bindingKey
775
+ : undefined;
776
+ if (input.target.threadId !== undefined &&
777
+ input.registration.cwd &&
778
+ input.registration.sessionId &&
779
+ !bindingKey)
579
780
  throw new Error("Telegram follower Workspace binding changed before registration publication.");
580
781
  deps.topicTargetStore.commitWorkspaceRestoreRegistration({ target: input.target, slot: input.slot, bindingKey }, publish);
581
782
  }),
@@ -723,7 +924,8 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
723
924
  matchesRequestedTarget;
724
925
  return (record.owner?.kind === "manual-follower" &&
725
926
  ((matchesCurrentIdentity && matchesRequestedTarget) ||
726
- matchesSessionHandoff || matchesWorkspaceRecovery));
927
+ matchesSessionHandoff ||
928
+ matchesWorkspaceRecovery));
727
929
  });
728
930
  const followerOwner = Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
729
931
  const recoverableTarget = !reconnectRecord &&
@@ -755,7 +957,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
755
957
  },
756
958
  profileKey: followerProfileKey,
757
959
  threadName: workspaceBinding?.threadName ?? registration.threadName,
758
- preferredSlot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
960
+ preferredSlot: workspaceIdentity?.slot ??
961
+ workspaceBinding?.slot ??
962
+ registration.slot,
759
963
  ...(workspaceIdentity
760
964
  ? {
761
965
  workspaceBindingKey: workspaceIdentity.bindingKey,
@@ -778,7 +982,10 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
778
982
  ThreadNaming.isTelegramTopicThreadNameValidForSlot(recoveryHint.threadName, recoveryHint.slot)
779
983
  ? recoveryHint.threadName
780
984
  : undefined;
781
- const requestedSlot = workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot ?? recoveryHint?.slot;
985
+ const requestedSlot = workspaceIdentity?.slot ??
986
+ workspaceBinding?.slot ??
987
+ registration.slot ??
988
+ recoveryHint?.slot;
782
989
  const recoveredSlot = deps.topicTargetStore.allocateSlot(followerProfileKey, requestedSlot, workspaceIdentity?.bindingKey);
783
990
  if (!recoveredSlot) {
784
991
  throw new TelegramWorkspaceSlotUnavailableError();
@@ -813,16 +1020,23 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
813
1020
  const runRegistration = async () => {
814
1021
  if (requestedTarget)
815
1022
  deps.topicTargetStore.assertWorkspaceRestoreRegistration({
816
- target: requestedTarget, bindingKey: workspaceIdentity?.bindingKey,
817
- slot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
1023
+ target: requestedTarget,
1024
+ bindingKey: workspaceIdentity?.bindingKey,
1025
+ slot: workspaceIdentity?.slot ??
1026
+ workspaceBinding?.slot ??
1027
+ registration.slot,
818
1028
  });
819
1029
  const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
820
1030
  if (recoveryTarget) {
821
1031
  Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
822
1032
  }
823
- const pendingTargetRecovery = recoverableTarget && deps.topicTargetStore.listPendingProvisions().some((entry) => entry.target?.chatId === recoverableTarget.chatId &&
824
- entry.target?.threadId === recoverableTarget.threadId &&
825
- (entry.instanceId === registration.instanceId || entry.profileKey === followerProfileKey));
1033
+ const pendingTargetRecovery = recoverableTarget &&
1034
+ deps.topicTargetStore
1035
+ .listPendingProvisions()
1036
+ .some((entry) => entry.target?.chatId === recoverableTarget.chatId &&
1037
+ entry.target?.threadId === recoverableTarget.threadId &&
1038
+ (entry.instanceId === registration.instanceId ||
1039
+ entry.profileKey === followerProfileKey));
826
1040
  let result = reconnectRecord
827
1041
  ? {
828
1042
  target: reconnectRecord.target,
@@ -833,8 +1047,7 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
833
1047
  ? await recoverRequestedTarget()
834
1048
  : await provisionTarget();
835
1049
  const alignResultWithWorkspaceSlot = () => {
836
- if (!workspaceIdentity ||
837
- result.record.slot === workspaceIdentity.slot)
1050
+ if (!workspaceIdentity || result.record.slot === workspaceIdentity.slot)
838
1051
  return;
839
1052
  deps.recordRuntimeEvent("bus", "Telegram follower record slot reconciled to its Workspace claim", {
840
1053
  phase: "follower-register-slot-reconcile",
@@ -851,7 +1064,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
851
1064
  };
852
1065
  alignResultWithWorkspaceSlot();
853
1066
  const assertRestoreRegistration = () => deps.topicTargetStore.assertWorkspaceRestoreRegistration({
854
- target: result.target, slot: result.record.slot, bindingKey: workspaceIdentity?.bindingKey,
1067
+ target: result.target,
1068
+ slot: result.record.slot,
1069
+ bindingKey: workspaceIdentity?.bindingKey,
855
1070
  });
856
1071
  assertRestoreRegistration();
857
1072
  const crossSessionReuse = !!reconnectRecord &&
@@ -887,7 +1102,9 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
887
1102
  threadName: result.displayTitle ??
888
1103
  (workspaceBinding?.target.chatId === result.target.chatId &&
889
1104
  workspaceBinding.target.threadId === result.target.threadId
890
- ? workspaceBinding.displayTitle : undefined) ?? result.record.threadName,
1105
+ ? workspaceBinding.displayTitle
1106
+ : undefined) ??
1107
+ result.record.threadName,
891
1108
  slot: result.record.slot,
892
1109
  state: "connected",
893
1110
  })
@@ -1011,18 +1228,28 @@ export function createTelegramBusFollowerTargetProvisioner(deps) {
1011
1228
  : {}),
1012
1229
  slot: workspaceIdentity.slot,
1013
1230
  ...(workspaceIdentity.sessionId
1014
- ? { journalSources: [{ sessionId: workspaceIdentity.sessionId, recipientBindingKey: followerProfileKey }],
1015
- journalBindingKeys: [] }
1231
+ ? {
1232
+ journalSources: [
1233
+ {
1234
+ sessionId: workspaceIdentity.sessionId,
1235
+ recipientBindingKey: followerProfileKey,
1236
+ },
1237
+ ],
1238
+ journalBindingKeys: [],
1239
+ }
1016
1240
  : { journalBindingKeys: [followerProfileKey] }),
1017
1241
  journalBindingsComplete: true,
1018
1242
  updatedAtMs: getNowMs(),
1019
1243
  },
1020
1244
  });
1021
1245
  if (connectedAnnouncement && workspaceCommit.displayTitle) {
1022
- connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
1023
- target: result.target, threadName: workspaceCommit.displayTitle,
1024
- slot: result.record.slot, state: "connected",
1025
- });
1246
+ connectedAnnouncement =
1247
+ createTelegramBusInstanceLifecycleAnnouncement({
1248
+ target: result.target,
1249
+ threadName: workspaceCommit.displayTitle,
1250
+ slot: result.record.slot,
1251
+ state: "connected",
1252
+ });
1026
1253
  }
1027
1254
  deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
1028
1255
  await deps.topicTargetStore.persist();
@@ -1156,8 +1383,11 @@ function createTelegramBusFollowerCleanupHandler(deps, trigger) {
1156
1383
  target: { chatId: target.chatId, threadId: target.threadId },
1157
1384
  requestedAtMs: (deps.getNowMs ?? Date.now)(),
1158
1385
  };
1159
- const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === follower.instanceId &&
1160
- record.target.chatId === target.chatId && record.target.threadId === target.threadId);
1386
+ const departingRecord = deps.topicTargetStore
1387
+ .list()
1388
+ .find((record) => record.instanceId === follower.instanceId &&
1389
+ record.target.chatId === target.chatId &&
1390
+ record.target.threadId === target.threadId);
1161
1391
  const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
1162
1392
  deps.topicTargetStore.upsertPendingCleanup(intent);
1163
1393
  await deps.topicTargetStore.persist();
@@ -1363,19 +1593,39 @@ function parseTelegramBusAttachmentSource(value) {
1363
1593
  return undefined;
1364
1594
  const raw = value, chat = raw.chat;
1365
1595
  const text = (field) => typeof field === "string" && field.length > 0 && field.length <= 256;
1366
- if (!text(raw.kind) || !Number.isSafeInteger(raw.messageId) ||
1596
+ if (!text(raw.kind) ||
1597
+ !Number.isSafeInteger(raw.messageId) ||
1367
1598
  (raw.index !== undefined && !Number.isSafeInteger(raw.index)) ||
1368
- (raw.userFileName !== undefined && !text(raw.userFileName)) || (raw.scope !== undefined && !text(raw.scope)) ||
1369
- (chat !== undefined && (!chat || typeof chat !== "object" || Array.isArray(chat) || !Number.isSafeInteger(chat.id) ||
1370
- (chat.type !== undefined && !text(chat.type)) || (chat.username !== undefined && !text(chat.username)))))
1599
+ (raw.userFileName !== undefined && !text(raw.userFileName)) ||
1600
+ (raw.scope !== undefined && !text(raw.scope)) ||
1601
+ (chat !== undefined &&
1602
+ (!chat ||
1603
+ typeof chat !== "object" ||
1604
+ Array.isArray(chat) ||
1605
+ !Number.isSafeInteger(chat.id) ||
1606
+ (chat.type !== undefined && !text(chat.type)) ||
1607
+ (chat.username !== undefined && !text(chat.username)))))
1371
1608
  return undefined;
1372
- return { kind: raw.kind, messageId: raw.messageId,
1609
+ return {
1610
+ kind: raw.kind,
1611
+ messageId: raw.messageId,
1373
1612
  ...(raw.index !== undefined ? { index: raw.index } : {}),
1374
- ...(raw.userFileName !== undefined ? { userFileName: raw.userFileName } : {}),
1613
+ ...(raw.userFileName !== undefined
1614
+ ? { userFileName: raw.userFileName }
1615
+ : {}),
1375
1616
  ...(raw.scope !== undefined ? { scope: raw.scope } : {}),
1376
- ...(chat ? { chat: { id: chat.id,
1377
- ...(chat.type !== undefined ? { type: chat.type } : {}),
1378
- ...(chat.username !== undefined ? { username: chat.username } : {}) } } : {}) };
1617
+ ...(chat
1618
+ ? {
1619
+ chat: {
1620
+ id: chat.id,
1621
+ ...(chat.type !== undefined ? { type: chat.type } : {}),
1622
+ ...(chat.username !== undefined
1623
+ ? { username: chat.username }
1624
+ : {}),
1625
+ },
1626
+ }
1627
+ : {}),
1628
+ };
1379
1629
  }
1380
1630
  export function createTelegramBusLeaderApiProxy(deps) {
1381
1631
  return async (method, args) => {
@@ -1431,49 +1681,350 @@ function createTelegramBusFollowerMutationRunner() {
1431
1681
  }
1432
1682
  };
1433
1683
  }
1684
+ /** One delegated text effect through existing Workspace/API owners; no closure transport, ordinary proxy or replay loop. */
1685
+ export function createTelegramBusSelectedMenuDeliveryHandler(deps) {
1686
+ return async (input, isCallerCurrent) => {
1687
+ let issued = false, method = "sendMessage";
1688
+ try {
1689
+ const decoded = parseTelegramBusEnvelope(JSON.stringify(input));
1690
+ if (decoded?.kind !== "follower.deliverSelectedMenu")
1691
+ throw new Error("Selected-menu wire is unavailable.");
1692
+ const expected = decoded, { recipient, executor, effect } = expected;
1693
+ method = effect.kind === "send-text" ? "sendMessage" : "editMessageText";
1694
+ const workspace = deps.workspace, api = deps.api, registry = deps.followerRegistry, protocol = structuredClone(deps.protocolIdentity);
1695
+ const scope = workspace?.getScopeKey, scopeKey = scope?.call(workspace);
1696
+ const capture = workspace?.captureAuthority, getStore = workspace?.getStore, threads = workspace?.threadStore, resolveJournal = workspace?.getJournalBindingKey, run = workspace?.run, runtime = api?.runtime, call = runtime?.call, authorize = api?.authorize, record = api?.record;
1697
+ const authority = capture?.call(workspace), store = getStore?.call(workspace), follower = registry.get(expected.instanceId);
1698
+ if (!scopeKey?.trim() ||
1699
+ !authority ||
1700
+ !isDeepStrictEqual(authority.executor, executor) ||
1701
+ authority.operatorUserId !== expected.operatorUserId ||
1702
+ !store ||
1703
+ typeof threads?.withWorkspaceLiveRebindSnapshot !== "function" ||
1704
+ typeof resolveJournal !== "function" ||
1705
+ typeof run !== "function" ||
1706
+ typeof call !== "function" ||
1707
+ typeof authorize !== "function" ||
1708
+ typeof record !== "function" ||
1709
+ !follower)
1710
+ throw new Error("Selected-menu proof/API owner is unavailable.");
1711
+ const retained = store
1712
+ .listLiveRebindings()
1713
+ .find((value) => value.request.operationId === expected.operationId);
1714
+ if (!retained ||
1715
+ retained.phase !== "released" ||
1716
+ retained.cleanup ||
1717
+ retained.request.source.updateIds.length !== 1 ||
1718
+ retained.recipient.kind !== "follower" ||
1719
+ !isDeepStrictEqual(retained.executor, executor) ||
1720
+ retained.operatorUserId !== expected.operatorUserId ||
1721
+ retained.recipient.instanceId !== expected.instanceId ||
1722
+ retained.recipient.sessionId !== recipient.sessionId ||
1723
+ retained.recipient.generation !== expected.registrationGeneration ||
1724
+ retained.request.owner.profileKey !== recipient.profileKey ||
1725
+ !isDeepStrictEqual(retained.request.target, recipient.target))
1726
+ throw new Error("Selected-menu operation/recipient is unavailable.");
1727
+ const intent = structuredClone(retained), snapshot = threads.withWorkspaceLiveRebindSnapshot;
1728
+ const registered = structuredClone(follower);
1729
+ const body = {
1730
+ chat_id: recipient.target.chatId,
1731
+ text: effect.text,
1732
+ ...(effect.parseMode ? { parse_mode: effect.parseMode } : {}),
1733
+ ...(effect.replyMarkup
1734
+ ? { reply_markup: structuredClone(effect.replyMarkup) }
1735
+ : {}),
1736
+ ...(effect.kind === "send-text"
1737
+ ? {
1738
+ message_thread_id: recipient.target.threadId,
1739
+ ...(effect.replyToMessageId
1740
+ ? {
1741
+ reply_parameters: {
1742
+ message_id: effect.replyToMessageId,
1743
+ allow_sending_without_reply: true,
1744
+ },
1745
+ }
1746
+ : {}),
1747
+ }
1748
+ : { message_id: effect.messageId }),
1749
+ };
1750
+ const assertRecipientCurrent = () => {
1751
+ const activeAuthority = capture.call(workspace), live = registry.get(expected.instanceId);
1752
+ if (!isCallerCurrent() ||
1753
+ deps.workspace !== workspace ||
1754
+ deps.api !== api ||
1755
+ deps.followerRegistry !== registry ||
1756
+ workspace.getScopeKey !== scope ||
1757
+ scope.call(workspace) !== scopeKey ||
1758
+ workspace.captureAuthority !== capture ||
1759
+ workspace.getStore !== getStore ||
1760
+ workspace.threadStore !== threads ||
1761
+ workspace.getJournalBindingKey !== resolveJournal ||
1762
+ workspace.run !== run ||
1763
+ threads.withWorkspaceLiveRebindSnapshot !== snapshot ||
1764
+ api.runtime !== runtime ||
1765
+ runtime.call !== call ||
1766
+ api.authorize !== authorize ||
1767
+ api.record !== record ||
1768
+ !authority.isCurrent() ||
1769
+ !activeAuthority?.isCurrent() ||
1770
+ activeAuthority.operatorUserId !== expected.operatorUserId ||
1771
+ !isDeepStrictEqual(activeAuthority.executor, executor) ||
1772
+ getStore.call(workspace) !== store ||
1773
+ !isDeepStrictEqual(deps.protocolIdentity, protocol) ||
1774
+ !hasTelegramBusCapability(protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY) ||
1775
+ !live ||
1776
+ !getTelegramBusProtocolCompatibility({
1777
+ local: protocol,
1778
+ remote: live.protocol,
1779
+ }).compatible ||
1780
+ !hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY) ||
1781
+ !isDeepStrictEqual(live.protocol, registered.protocol) ||
1782
+ live.registrationGeneration !== expected.registrationGeneration ||
1783
+ live.sessionId !== recipient.sessionId ||
1784
+ live.sessionGeneration !== recipient.sessionGeneration ||
1785
+ live.pid !== recipient.processId ||
1786
+ live.processBirthId !== recipient.processBirthId ||
1787
+ live.profileKey !== recipient.profileKey ||
1788
+ live.cwd !== registered.cwd ||
1789
+ live.slot !== registered.slot ||
1790
+ live.busSocketPath !== registered.busSocketPath ||
1791
+ live.target?.chatId !== recipient.target.chatId ||
1792
+ live.target.threadId !== recipient.target.threadId ||
1793
+ resolveJournal.call(workspace, live) !== recipient.journalBindingKey)
1794
+ throw new Error("Selected-menu authority changed.");
1795
+ let confirmed = false;
1796
+ snapshot.call(threads, intent, (view) => {
1797
+ const bindings = (view.workspaceBindings ?? []).filter((value) => value.bindingKey === intent.request.binding.bindingKey);
1798
+ const owners = view.threads.filter((value) => value.status === "active" &&
1799
+ (value.slot === intent.request.binding.slot ||
1800
+ isDeepStrictEqual(value.target, recipient.target)));
1801
+ const binding = bindings[0], owner = owners[0];
1802
+ confirmed =
1803
+ bindings.length === 1 &&
1804
+ binding?.sessionId === recipient.sessionId &&
1805
+ binding.cwd === intent.request.binding.cwd &&
1806
+ binding.workspaceKey === intent.request.binding.workspaceKey &&
1807
+ binding.slot === live.slot &&
1808
+ binding.inactiveSinceMs === undefined &&
1809
+ isDeepStrictEqual(binding.target, recipient.target) &&
1810
+ owners.length === 1 &&
1811
+ owner?.instanceId === expected.instanceId &&
1812
+ owner.slot === binding.slot &&
1813
+ owner.profileKey === recipient.profileKey &&
1814
+ isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
1815
+ isDeepStrictEqual(owner.target, recipient.target) &&
1816
+ !!live.cwd &&
1817
+ WorkspaceIdentity.normalizeTelegramWorkspacePath(live.cwd) ===
1818
+ binding.cwd;
1819
+ });
1820
+ if (!confirmed || !isCallerCurrent() || !authority.isCurrent())
1821
+ throw new Error("Selected-menu canonical recipient is unavailable.");
1822
+ };
1823
+ const assertCurrent = () => {
1824
+ assertRecipientCurrent();
1825
+ const live = registry.get(expected.instanceId);
1826
+ if (!live ||
1827
+ authorize.call(api, {
1828
+ follower: structuredClone(live),
1829
+ method: "call",
1830
+ args: [method, structuredClone(body)],
1831
+ }) !== true)
1832
+ throw new Error("Selected-menu API recipient is unavailable.");
1833
+ assertRecipientCurrent();
1834
+ };
1835
+ assertCurrent();
1836
+ const result = await run({
1837
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1838
+ operationKind: "workspace.selected-menu-delivery",
1839
+ scopes: [{ kind: "profile" }],
1840
+ }, async () => {
1841
+ assertCurrent();
1842
+ issued = true;
1843
+ const result = await call.call(runtime, method, body, {
1844
+ assertAuthority: assertCurrent,
1845
+ maxAttempts: 1,
1846
+ retryRateLimit: false,
1847
+ retrySafety: "non-idempotent",
1848
+ });
1849
+ assertCurrent();
1850
+ const value = result && typeof result === "object" && !Array.isArray(result)
1851
+ ? result
1852
+ : undefined;
1853
+ const messageId = effect.kind === "edit-text" ? effect.messageId : value?.message_id;
1854
+ if (!Number.isSafeInteger(messageId) ||
1855
+ messageId <= 0 ||
1856
+ (effect.kind === "edit-text" &&
1857
+ result !== true &&
1858
+ value?.message_id !== messageId))
1859
+ throw new Error("Selected-menu API result is unconfirmed.");
1860
+ if (effect.kind === "send-text" &&
1861
+ record.call(api, {
1862
+ follower: structuredClone(registered),
1863
+ chatId: recipient.target.chatId,
1864
+ messageId: messageId,
1865
+ target: { ...recipient.target },
1866
+ }, assertCurrent) !== true)
1867
+ throw new Error("Selected-menu ownership publication is unconfirmed.");
1868
+ assertCurrent();
1869
+ return {
1870
+ operationId: expected.operationId,
1871
+ recipient: structuredClone(recipient),
1872
+ registrationGeneration: expected.registrationGeneration,
1873
+ effect: effect.kind,
1874
+ messageId,
1875
+ };
1876
+ });
1877
+ assertCurrent();
1878
+ return {
1879
+ kind: "bus.ack",
1880
+ requestId: expected.requestId,
1881
+ ok: true,
1882
+ result,
1883
+ };
1884
+ }
1885
+ catch (error) {
1886
+ const unknown = issued &&
1887
+ !(error instanceof TelegramApiAuthorityError && !error.requestIssued);
1888
+ return {
1889
+ kind: "bus.ack",
1890
+ requestId: input.requestId,
1891
+ ok: false,
1892
+ message: "Selected-menu delivery is refused or unconfirmed.",
1893
+ ...(unknown
1894
+ ? { error: { code: "commit-unknown", method } }
1895
+ : {}),
1896
+ };
1897
+ }
1898
+ };
1899
+ }
1900
+ /** The current registration a follower request names, or the negative ack refusing an unknown or stale one. */
1901
+ function resolveRegisteredTelegramBusFollower(registry, requestId, instanceId, registrationGeneration) {
1902
+ const follower = registry.get(instanceId);
1903
+ if (!follower) {
1904
+ return {
1905
+ rejection: rejectTelegramBusRequest(requestId, "Unknown Telegram bus follower instance."),
1906
+ };
1907
+ }
1908
+ if (!follower.registrationGeneration ||
1909
+ registrationGeneration !== follower.registrationGeneration) {
1910
+ return {
1911
+ rejection: rejectTelegramBusRequest(requestId, "Stale Telegram bus follower registration generation."),
1912
+ };
1913
+ }
1914
+ return { follower };
1915
+ }
1434
1916
  export function createTelegramBusLeaderEnvelopeHandler(deps) {
1435
1917
  const getNowMs = deps.getNowMs ?? Date.now;
1436
1918
  const runFollowerMutation = deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
1437
1919
  const recipientObservations = new Map();
1920
+ /** Rename and reset share one generation- and target-fenced Workspace Thread name change. */
1921
+ const changeFollowerThreadName = (envelope, action, apply) => runFollowerMutation(deps.followerRegistry.get(envelope.instanceId) ?? {
1922
+ instanceId: envelope.instanceId,
1923
+ }, async () => {
1924
+ const resolved = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
1925
+ if ("rejection" in resolved)
1926
+ return resolved.rejection;
1927
+ const follower = resolved.follower;
1928
+ if (follower.target?.chatId !== envelope.target.chatId ||
1929
+ follower.target?.threadId !== envelope.target.threadId)
1930
+ return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
1931
+ if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
1932
+ !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
1933
+ !apply)
1934
+ return rejectTelegramBusRequest(envelope.requestId, `Telegram Workspace Thread ${action} is unavailable.`);
1935
+ try {
1936
+ const result = await apply(follower);
1937
+ const current = deps.followerRegistry.get(follower.instanceId);
1938
+ if (!current ||
1939
+ current.registrationGeneration !== follower.registrationGeneration)
1940
+ return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
1941
+ deps.followerRegistry.register({
1942
+ ...current,
1943
+ threadName: result.threadName,
1944
+ connectedAtMs: current.connectedAtMs,
1945
+ });
1946
+ return {
1947
+ kind: "bus.ack",
1948
+ requestId: envelope.requestId,
1949
+ ok: true,
1950
+ result,
1951
+ };
1952
+ }
1953
+ catch (error) {
1954
+ return rejectTelegramBusRequest(envelope.requestId, error instanceof Error
1955
+ ? error.message
1956
+ : `Telegram Workspace Thread ${action} failed.`);
1957
+ }
1958
+ });
1438
1959
  const observeRestoreRecipient = (follower) => {
1439
- if (!deps.authSecret || !deps.onWorkspaceRestoreRecipientObserved || !deps.getTelegramProfile ||
1960
+ if (!deps.authSecret ||
1961
+ !deps.onWorkspaceRestoreRecipientObserved ||
1962
+ !deps.getTelegramProfile ||
1440
1963
  !hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
1441
1964
  !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
1442
1965
  return;
1443
1966
  const instanceId = follower.instanceId;
1444
1967
  const report = (error) => {
1445
1968
  try {
1446
- deps.recordRuntimeEvent?.("bus", error, { phase: "workspace-restore-recipient-observation", instanceId });
1969
+ deps.recordRuntimeEvent?.("bus", error, {
1970
+ phase: "workspace-restore-recipient-observation",
1971
+ instanceId,
1972
+ });
1973
+ }
1974
+ catch {
1975
+ /* Optional observation diagnostics must not break transport liveness. */
1447
1976
  }
1448
- catch { /* Optional observation diagnostics must not break transport liveness. */ }
1449
1977
  };
1450
1978
  try {
1451
1979
  const epoch = deps.getCurrentLeaderEpoch?.(), operator = deps.getAllowedUserId?.();
1452
1980
  const runtimeGeneration = deps.getWorkspaceRestoreObservationGeneration?.();
1453
- if (epoch === undefined || operator === undefined ||
1454
- (deps.getWorkspaceRestoreObservationGeneration && runtimeGeneration === undefined))
1981
+ if (epoch === undefined ||
1982
+ operator === undefined ||
1983
+ (deps.getWorkspaceRestoreObservationGeneration &&
1984
+ runtimeGeneration === undefined))
1455
1985
  return;
1456
1986
  const snapshot = structuredClone(follower);
1457
- const observed = { epoch, operator, profile: deps.getTelegramProfile(), runtimeGeneration,
1458
- follower: { ...snapshot, lastHeartbeatMs: undefined, connectedAtMs: undefined, threadName: undefined } };
1987
+ const observed = {
1988
+ epoch,
1989
+ operator,
1990
+ profile: deps.getTelegramProfile(),
1991
+ runtimeGeneration,
1992
+ follower: {
1993
+ ...snapshot,
1994
+ lastHeartbeatMs: undefined,
1995
+ connectedAtMs: undefined,
1996
+ threadName: undefined,
1997
+ },
1998
+ };
1459
1999
  if (isDeepStrictEqual(recipientObservations.get(instanceId), observed))
1460
2000
  return;
1461
2001
  recipientObservations.set(instanceId, observed);
1462
2002
  const isCurrent = () => {
1463
2003
  const live = deps.followerRegistry.get(instanceId);
1464
- return recipientObservations.get(instanceId) === observed &&
1465
- deps.getWorkspaceRestoreObservationGeneration?.() === observed.runtimeGeneration &&
2004
+ return (recipientObservations.get(instanceId) === observed &&
2005
+ deps.getWorkspaceRestoreObservationGeneration?.() ===
2006
+ observed.runtimeGeneration &&
1466
2007
  hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
1467
- deps.getCurrentLeaderEpoch?.() === observed.epoch && deps.getAllowedUserId?.() === observed.operator &&
1468
- deps.getTelegramProfile?.() === observed.profile && !!live &&
2008
+ deps.getCurrentLeaderEpoch?.() === observed.epoch &&
2009
+ deps.getAllowedUserId?.() === observed.operator &&
2010
+ deps.getTelegramProfile?.() === observed.profile &&
2011
+ !!live &&
1469
2012
  hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
1470
- isDeepStrictEqual({ ...live, lastHeartbeatMs: undefined, connectedAtMs: undefined, threadName: undefined }, observed.follower);
2013
+ isDeepStrictEqual({
2014
+ ...live,
2015
+ lastHeartbeatMs: undefined,
2016
+ connectedAtMs: undefined,
2017
+ threadName: undefined,
2018
+ }, observed.follower));
1471
2019
  };
1472
2020
  // A heartbeat is a hint, not completion proof. Never await the listener before acknowledging it.
1473
- void Promise.resolve().then(() => {
2021
+ void Promise.resolve()
2022
+ .then(() => {
1474
2023
  if (isCurrent())
1475
2024
  return deps.onWorkspaceRestoreRecipientObserved?.(structuredClone(snapshot), isCurrent);
1476
- }).catch(report).finally(() => {
2025
+ })
2026
+ .catch(report)
2027
+ .finally(() => {
1477
2028
  if (recipientObservations.get(instanceId) === observed)
1478
2029
  recipientObservations.delete(instanceId);
1479
2030
  });
@@ -1483,24 +2034,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1483
2034
  }
1484
2035
  };
1485
2036
  const handleAgentRequest = async (envelope) => {
1486
- const follower = deps.followerRegistry.get(envelope.instanceId);
1487
- if (!follower) {
1488
- return {
1489
- kind: "bus.ack",
1490
- requestId: envelope.requestId,
1491
- ok: false,
1492
- message: "Unknown Telegram bus follower instance.",
1493
- };
1494
- }
1495
- if (!follower.registrationGeneration ||
1496
- envelope.registrationGeneration !== follower.registrationGeneration) {
1497
- return {
1498
- kind: "bus.ack",
1499
- requestId: envelope.requestId,
1500
- ok: false,
1501
- message: "Stale Telegram bus follower registration generation.",
1502
- };
1503
- }
2037
+ const followerResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
2038
+ if ("rejection" in followerResolution)
2039
+ return followerResolution.rejection;
2040
+ const follower = followerResolution.follower;
1504
2041
  deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
1505
2042
  if (envelope.kind === "follower.resolveAgentTarget") {
1506
2043
  const target = await deps.resolveAgentTarget?.(follower, envelope.selector);
@@ -1511,71 +2048,32 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1511
2048
  ok: true,
1512
2049
  result: target,
1513
2050
  }
1514
- : {
1515
- kind: "bus.ack",
1516
- requestId: envelope.requestId,
1517
- ok: false,
1518
- message: "Telegram agent target is unavailable or ambiguous.",
1519
- };
2051
+ : rejectTelegramBusRequest(envelope.requestId, "Telegram agent target is unavailable or ambiguous.");
1520
2052
  }
1521
2053
  if (!deps.routeAgentMessage) {
1522
- return {
1523
- kind: "bus.ack",
1524
- requestId: envelope.requestId,
1525
- ok: false,
1526
- message: "Telegram agent message routing is unavailable.",
1527
- };
2054
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram agent message routing is unavailable.");
1528
2055
  }
1529
2056
  await deps.routeAgentMessage(follower, envelope.message);
1530
2057
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
1531
2058
  };
1532
2059
  const routeQueueHandoff = async (envelope) => {
1533
- const donor = deps.followerRegistry.get(envelope.instanceId);
1534
- if (!donor) {
1535
- return {
1536
- kind: "bus.ack",
1537
- requestId: envelope.requestId,
1538
- ok: false,
1539
- message: "Unknown Telegram bus follower instance.",
1540
- };
1541
- }
1542
- if (!donor.registrationGeneration ||
1543
- envelope.registrationGeneration !== donor.registrationGeneration) {
1544
- return {
1545
- kind: "bus.ack",
1546
- requestId: envelope.requestId,
1547
- ok: false,
1548
- message: "Stale Telegram bus follower registration generation.",
1549
- };
1550
- }
2060
+ const donorResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
2061
+ if ("rejection" in donorResolution)
2062
+ return donorResolution.rejection;
2063
+ const donor = donorResolution.follower;
1551
2064
  const recipient = deps.followerRegistry.get(envelope.recipientInstanceId);
1552
2065
  if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
1553
2066
  !hasTelegramBusCapability(donor.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
1554
2067
  !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
1555
- return {
1556
- kind: "bus.ack",
1557
- requestId: envelope.requestId,
1558
- ok: false,
1559
- message: "Telegram queue handoff capability was not negotiated.",
1560
- };
2068
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram queue handoff capability was not negotiated.");
1561
2069
  }
1562
2070
  if (!recipient?.registrationGeneration ||
1563
2071
  envelope.recipientRegistrationGeneration !==
1564
2072
  recipient.registrationGeneration) {
1565
- return {
1566
- kind: "bus.ack",
1567
- requestId: envelope.requestId,
1568
- ok: false,
1569
- message: "Stale Telegram queue handoff recipient registration generation.",
1570
- };
2073
+ return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram queue handoff recipient registration generation.");
1571
2074
  }
1572
2075
  if (donor.instanceId === recipient.instanceId) {
1573
- return {
1574
- kind: "bus.ack",
1575
- requestId: envelope.requestId,
1576
- ok: false,
1577
- message: "Telegram queue handoff recipient must be another runtime.",
1578
- };
2076
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram queue handoff recipient must be another runtime.");
1579
2077
  }
1580
2078
  if (deps.routeQueueHandoff) {
1581
2079
  const result = await deps.routeQueueHandoff(donor, envelope);
@@ -1632,25 +2130,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1632
2130
  };
1633
2131
  };
1634
2132
  const forwardToFollower = async (envelope) => {
1635
- const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
1636
- if (!follower) {
1637
- return {
1638
- kind: "bus.ack",
1639
- requestId: envelope.requestId,
1640
- ok: false,
1641
- message: "Unknown Telegram bus follower instance.",
1642
- };
1643
- }
1644
- if (!follower.registrationGeneration ||
1645
- envelope.recipientRegistrationGeneration !==
1646
- follower.registrationGeneration) {
1647
- return {
1648
- kind: "bus.ack",
1649
- requestId: envelope.requestId,
1650
- ok: false,
1651
- message: "Stale Telegram bus follower registration generation.",
1652
- };
1653
- }
2133
+ const followerResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.recipientInstanceId, envelope.recipientRegistrationGeneration);
2134
+ if ("rejection" in followerResolution)
2135
+ return followerResolution.rejection;
2136
+ const follower = followerResolution.follower;
1654
2137
  const followerSocketPath = follower.busSocketPath ??
1655
2138
  getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
1656
2139
  deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
@@ -1695,12 +2178,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1695
2178
  return async (envelope) => {
1696
2179
  const trafficClass = getTelegramBusEnvelopeTrafficClass(envelope);
1697
2180
  if (trafficClass === "response") {
1698
- return {
1699
- kind: "bus.ack",
1700
- requestId: envelope.requestId,
1701
- ok: false,
1702
- message: "Telegram bus response envelope cannot be used as a request.",
1703
- };
2181
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram bus response envelope cannot be used as a request.");
1704
2182
  }
1705
2183
  if (!isTelegramBusEnvelopeAuthorized(envelope, deps.authSecret)) {
1706
2184
  return createUnauthorizedBusAck(envelope.requestId);
@@ -1712,12 +2190,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1712
2190
  local: deps.protocolIdentity,
1713
2191
  remote: envelope.registration.protocol,
1714
2192
  });
1715
- const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" || (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
1716
- hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
2193
+ const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" ||
2194
+ (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
2195
+ hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
1717
2196
  const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
1718
2197
  const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
1719
2198
  hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
1720
- if (!compatibility.compatible || !displayCompatible() ||
2199
+ if (!compatibility.compatible ||
2200
+ !displayCompatible() ||
1721
2201
  (envelope.registration.cwd !== undefined &&
1722
2202
  envelope.registration.sessionId === undefined) ||
1723
2203
  (restoringWorkspace && !supportsWorkspaceAutoConnect)) {
@@ -1736,7 +2216,9 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1736
2216
  }
1737
2217
  if (!envelope.registration.registrationGeneration) {
1738
2218
  return {
1739
- kind: "bus.ack", requestId: envelope.requestId, ok: false,
2219
+ kind: "bus.ack",
2220
+ requestId: envelope.requestId,
2221
+ ok: false,
1740
2222
  protocol: deps.protocolIdentity,
1741
2223
  message: "Telegram follower registration requires an exact generation.",
1742
2224
  };
@@ -1746,13 +2228,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1746
2228
  const leaderEpoch = deps.getCurrentLeaderEpoch?.();
1747
2229
  const telegramProfile = deps.getTelegramProfile?.();
1748
2230
  const operatorUserId = deps.getAllowedUserId?.();
1749
- const registrationCurrent = () => (!deps.getCurrentLeaderEpoch || deps.getCurrentLeaderEpoch() === leaderEpoch) &&
1750
- (!deps.getTelegramProfile || deps.getTelegramProfile() === telegramProfile) &&
1751
- (!deps.getAllowedUserId || deps.getAllowedUserId() === operatorUserId);
2231
+ const registrationCurrent = () => (!deps.getCurrentLeaderEpoch ||
2232
+ deps.getCurrentLeaderEpoch() === leaderEpoch) &&
2233
+ (!deps.getTelegramProfile ||
2234
+ deps.getTelegramProfile() === telegramProfile) &&
2235
+ (!deps.getAllowedUserId ||
2236
+ deps.getAllowedUserId() === operatorUserId);
1752
2237
  if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
1753
2238
  throw new Error("Telegram follower registration requires leader ownership.");
1754
2239
  }
1755
- if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) && !deps.commitFollowerRegistration)
2240
+ if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
2241
+ !deps.commitFollowerRegistration)
1756
2242
  throw new Error("Workspace Restore registration publication protection is unavailable.");
1757
2243
  const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
1758
2244
  if (restoringWorkspace && !target) {
@@ -1777,29 +2263,38 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1777
2263
  (!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
1778
2264
  throw new Error("Telegram Thread slot authority is unavailable.");
1779
2265
  }
2266
+ const assertRegistrationPublishable = () => {
2267
+ if (!registrationCurrent())
2268
+ throw new Error("Telegram follower registration lost leader ownership.");
2269
+ if (!displayCompatible())
2270
+ throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2271
+ };
1780
2272
  let publicationOpen = true;
1781
2273
  let published;
1782
2274
  const publish = () => {
1783
2275
  if (!publicationOpen)
1784
2276
  throw new Error("Telegram follower registration publication is no longer available.");
1785
2277
  publicationOpen = false;
1786
- if (!registrationCurrent())
1787
- throw new Error("Telegram follower registration lost leader ownership.");
1788
- if (!displayCompatible())
1789
- throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2278
+ assertRegistrationPublishable();
1790
2279
  published = deps.followerRegistry.register({
1791
2280
  ...envelope.registration,
1792
2281
  connectedAtMs: getNowMs(),
1793
2282
  target: registeredTarget,
1794
2283
  ...(registeredSlot ? { slot: registeredSlot } : {}),
1795
2284
  ...((target?.threadName ?? envelope.registration.threadName)
1796
- ? { threadName: target?.threadName ?? envelope.registration.threadName } : {}),
2285
+ ? {
2286
+ threadName: target?.threadName ??
2287
+ envelope.registration.threadName,
2288
+ }
2289
+ : {}),
1797
2290
  });
1798
2291
  };
1799
2292
  try {
1800
2293
  if (deps.commitFollowerRegistration)
1801
2294
  await deps.commitFollowerRegistration({
1802
- registration: envelope.registration, target: registeredTarget, slot: registeredSlot,
2295
+ registration: envelope.registration,
2296
+ target: registeredTarget,
2297
+ slot: registeredSlot,
1803
2298
  }, publish);
1804
2299
  else
1805
2300
  publish();
@@ -1809,10 +2304,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1809
2304
  }
1810
2305
  if (!published)
1811
2306
  throw new Error("Telegram follower registration publication was not confirmed.");
1812
- if (!registrationCurrent())
1813
- throw new Error("Telegram follower registration lost leader ownership.");
1814
- if (!displayCompatible())
1815
- throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2307
+ assertRegistrationPublishable();
1816
2308
  const follower = deps.followerRegistry.get(envelope.registration.instanceId);
1817
2309
  if (!follower)
1818
2310
  throw new Error("Telegram follower registration changed before acknowledgement.");
@@ -1827,16 +2319,19 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1827
2319
  requestId: envelope.requestId,
1828
2320
  ok: true,
1829
2321
  protocol: deps.protocolIdentity,
1830
- ...(registeredTarget ? {
1831
- result: {
1832
- ...registeredTarget,
1833
- ...(displayTitle !== undefined ? { displayTitle } : {}),
1834
- },
1835
- } : {}),
2322
+ ...(registeredTarget
2323
+ ? {
2324
+ result: {
2325
+ ...registeredTarget,
2326
+ ...(displayTitle !== undefined ? { displayTitle } : {}),
2327
+ },
2328
+ }
2329
+ : {}),
1836
2330
  };
1837
2331
  }
1838
2332
  catch (error) {
1839
- if (!restoringWorkspace && deps.runWithWorkspaceCapacity &&
2333
+ if (!restoringWorkspace &&
2334
+ deps.runWithWorkspaceCapacity &&
1840
2335
  error instanceof TelegramWorkspaceSlotUnavailableError)
1841
2336
  throw error;
1842
2337
  return {
@@ -1850,14 +2345,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1850
2345
  };
1851
2346
  }
1852
2347
  });
1853
- const register = () => deps.runWorkspaceAdmission ? deps.runWorkspaceAdmission({
1854
- operationId: `follower-registration:${envelope.requestId}`,
1855
- operationKind: "workspace.register-follower",
1856
- scopes: [{ kind: "profile" }],
1857
- }, registrationOperation) : registrationOperation();
2348
+ const register = () => deps.runWorkspaceAdmission
2349
+ ? deps.runWorkspaceAdmission({
2350
+ operationId: `follower-registration:${envelope.requestId}`,
2351
+ operationKind: "workspace.register-follower",
2352
+ scopes: [{ kind: "profile" }],
2353
+ }, registrationOperation)
2354
+ : registrationOperation();
1858
2355
  try {
1859
2356
  return await (!restoringWorkspace && deps.runWithWorkspaceCapacity
1860
- ? deps.runWithWorkspaceCapacity(register) : register());
2357
+ ? deps.runWithWorkspaceCapacity(register)
2358
+ : register());
1861
2359
  }
1862
2360
  catch (error) {
1863
2361
  return {
@@ -1875,155 +2373,49 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1875
2373
  return routeQueueHandoff(envelope);
1876
2374
  case "follower.setThreadDisplayMode": {
1877
2375
  const epoch = deps.getCurrentLeaderEpoch?.();
1878
- const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
1879
- deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration === envelope.registrationGeneration;
2376
+ const current = () => epoch !== undefined &&
2377
+ deps.getCurrentLeaderEpoch?.() === epoch &&
2378
+ deps.followerRegistry.get(envelope.instanceId)
2379
+ ?.registrationGeneration === envelope.registrationGeneration;
1880
2380
  const follower = deps.followerRegistry.get(envelope.instanceId);
1881
- const requiredCapability = envelope.mode === "directory-snake" || envelope.mode === "directory-title"
2381
+ const requiredCapability = envelope.mode === "directory-snake" ||
2382
+ envelope.mode === "directory-title"
1882
2383
  ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
1883
2384
  : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
1884
- if (!current() || !follower?.registrationGeneration || !deps.applyThreadDisplayMode ||
2385
+ if (!current() ||
2386
+ !follower?.registrationGeneration ||
2387
+ !deps.applyThreadDisplayMode ||
1885
2388
  !hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
1886
2389
  !hasTelegramBusCapability(follower.protocol, requiredCapability)) {
1887
- return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
1888
- message: "Thread display settings require current registration and compatible leader authority." };
2390
+ return rejectTelegramBusRequest(envelope.requestId, "Thread display settings require current registration and compatible leader authority.");
1889
2391
  }
1890
2392
  try {
1891
2393
  await deps.applyThreadDisplayMode(envelope.mode, current);
1892
2394
  if (!current())
1893
2395
  throw new Error("Thread display setting completed for a stale registration.");
1894
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result: { mode: envelope.mode } };
2396
+ return {
2397
+ kind: "bus.ack",
2398
+ requestId: envelope.requestId,
2399
+ ok: true,
2400
+ result: { mode: envelope.mode },
2401
+ };
1895
2402
  }
1896
2403
  catch (error) {
1897
- return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
1898
- message: error instanceof Error ? error.message : "Thread display setting was not fully applied." };
2404
+ return {
2405
+ kind: "bus.ack",
2406
+ requestId: envelope.requestId,
2407
+ ok: false,
2408
+ message: error instanceof Error
2409
+ ? error.message
2410
+ : "Thread display setting was not fully applied.",
2411
+ };
1899
2412
  }
1900
2413
  }
1901
- case "follower.renameThread": {
1902
- const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
1903
- return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
1904
- const follower = deps.followerRegistry.get(envelope.instanceId);
1905
- if (!follower) {
1906
- return {
1907
- kind: "bus.ack",
1908
- requestId: envelope.requestId,
1909
- ok: false,
1910
- message: "Unknown Telegram bus follower instance.",
1911
- };
1912
- }
1913
- if (!follower.registrationGeneration ||
1914
- envelope.registrationGeneration !== follower.registrationGeneration ||
1915
- follower.target?.chatId !== envelope.target.chatId ||
1916
- follower.target?.threadId !== envelope.target.threadId) {
1917
- return {
1918
- kind: "bus.ack",
1919
- requestId: envelope.requestId,
1920
- ok: false,
1921
- message: "Stale Telegram bus follower registration generation.",
1922
- };
1923
- }
1924
- if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
1925
- !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
1926
- !deps.renameFollowerThread) {
1927
- return {
1928
- kind: "bus.ack",
1929
- requestId: envelope.requestId,
1930
- ok: false,
1931
- message: "Telegram Workspace Thread rename is unavailable.",
1932
- };
1933
- }
1934
- try {
1935
- const result = await deps.renameFollowerThread(follower, envelope.threadName);
1936
- const current = deps.followerRegistry.get(follower.instanceId);
1937
- if (current?.registrationGeneration !== follower.registrationGeneration) {
1938
- return {
1939
- kind: "bus.ack",
1940
- requestId: envelope.requestId,
1941
- ok: false,
1942
- message: "Stale Telegram bus follower registration generation.",
1943
- };
1944
- }
1945
- deps.followerRegistry.register({
1946
- ...current,
1947
- threadName: result.threadName,
1948
- connectedAtMs: current.connectedAtMs,
1949
- });
1950
- return {
1951
- kind: "bus.ack",
1952
- requestId: envelope.requestId,
1953
- ok: true,
1954
- result,
1955
- };
1956
- }
1957
- catch (error) {
1958
- return {
1959
- kind: "bus.ack",
1960
- requestId: envelope.requestId,
1961
- ok: false,
1962
- message: error instanceof Error
1963
- ? error.message
1964
- : "Telegram Workspace Thread rename failed.",
1965
- };
1966
- }
1967
- });
1968
- }
1969
- case "follower.resetThreadName": {
1970
- const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
1971
- return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
1972
- const follower = deps.followerRegistry.get(envelope.instanceId);
1973
- if (!follower || !follower.registrationGeneration ||
1974
- envelope.registrationGeneration !== follower.registrationGeneration ||
1975
- follower.target?.chatId !== envelope.target.chatId ||
1976
- follower.target?.threadId !== envelope.target.threadId) {
1977
- return {
1978
- kind: "bus.ack",
1979
- requestId: envelope.requestId,
1980
- ok: false,
1981
- message: "Stale or unknown Telegram bus follower registration.",
1982
- };
1983
- }
1984
- if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !deps.resetFollowerThreadName) {
1985
- return {
1986
- kind: "bus.ack",
1987
- requestId: envelope.requestId,
1988
- ok: false,
1989
- message: "Telegram Workspace Thread reset is unavailable.",
1990
- };
1991
- }
1992
- try {
1993
- const result = await deps.resetFollowerThreadName(follower);
1994
- const current = deps.followerRegistry.get(follower.instanceId);
1995
- if (current?.registrationGeneration !==
1996
- follower.registrationGeneration) {
1997
- return {
1998
- kind: "bus.ack",
1999
- requestId: envelope.requestId,
2000
- ok: false,
2001
- message: "Stale Telegram bus follower registration generation.",
2002
- };
2003
- }
2004
- deps.followerRegistry.register({
2005
- ...current,
2006
- threadName: result.threadName,
2007
- connectedAtMs: current.connectedAtMs,
2008
- });
2009
- return {
2010
- kind: "bus.ack",
2011
- requestId: envelope.requestId,
2012
- ok: true,
2013
- result,
2014
- };
2015
- }
2016
- catch (error) {
2017
- return {
2018
- kind: "bus.ack",
2019
- requestId: envelope.requestId,
2020
- ok: false,
2021
- message: error instanceof Error
2022
- ? error.message : "Telegram Workspace Thread reset failed.",
2023
- };
2024
- }
2025
- });
2026
- }
2414
+ case "follower.renameThread":
2415
+ return changeFollowerThreadName(envelope, "rename", deps.renameFollowerThread &&
2416
+ ((follower) => deps.renameFollowerThread(follower, envelope.threadName)));
2417
+ case "follower.resetThreadName":
2418
+ return changeFollowerThreadName(envelope, "reset", deps.resetFollowerThreadName);
2027
2419
  case "follower.publishSessionReplacement":
2028
2420
  case "follower.settleSessionReplacement": {
2029
2421
  const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
@@ -2036,19 +2428,16 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2036
2428
  const follower = deps.followerRegistry.get(envelope.instanceId);
2037
2429
  const isCurrent = () => epoch !== undefined &&
2038
2430
  deps.getCurrentLeaderEpoch?.() === epoch &&
2039
- deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration ===
2040
- envelope.registrationGeneration;
2431
+ deps.followerRegistry.get(envelope.instanceId)
2432
+ ?.registrationGeneration === envelope.registrationGeneration;
2041
2433
  if (!follower?.registrationGeneration ||
2042
- follower.registrationGeneration !== envelope.registrationGeneration ||
2043
- !isCurrent() || !operation ||
2434
+ follower.registrationGeneration !==
2435
+ envelope.registrationGeneration ||
2436
+ !isCurrent() ||
2437
+ !operation ||
2044
2438
  !hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT) ||
2045
2439
  !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT)) {
2046
- return {
2047
- kind: "bus.ack",
2048
- requestId: envelope.requestId,
2049
- ok: false,
2050
- message: "Telegram session replacement requires current follower registration and compatible leader authority.",
2051
- };
2440
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram session replacement requires current follower registration and compatible leader authority.");
2052
2441
  }
2053
2442
  try {
2054
2443
  const committed = await operation(follower, envelope.intent, isCurrent);
@@ -2086,32 +2475,19 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2086
2475
  return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
2087
2476
  const follower = deps.followerRegistry.get(envelope.instanceId);
2088
2477
  if (!follower) {
2089
- return {
2090
- kind: "bus.ack",
2091
- requestId: envelope.requestId,
2092
- ok: false,
2093
- message: "Unknown Telegram bus follower instance.",
2094
- };
2478
+ return rejectTelegramBusRequest(envelope.requestId, "Unknown Telegram bus follower instance.");
2095
2479
  }
2096
2480
  if (!follower.registrationGeneration ||
2097
2481
  !envelope.registrationGeneration ||
2098
- envelope.registrationGeneration !== follower.registrationGeneration) {
2099
- return {
2100
- kind: "bus.ack",
2101
- requestId: envelope.requestId,
2102
- ok: false,
2103
- message: "Stale Telegram bus follower registration generation.",
2104
- };
2482
+ envelope.registrationGeneration !==
2483
+ follower.registrationGeneration) {
2484
+ return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
2105
2485
  }
2106
2486
  await deps.onFollowerDisconnected?.(follower);
2107
2487
  const current = deps.followerRegistry.get(follower.instanceId);
2108
- if (current?.registrationGeneration !== follower.registrationGeneration) {
2109
- return {
2110
- kind: "bus.ack",
2111
- requestId: envelope.requestId,
2112
- ok: false,
2113
- message: "Stale Telegram bus follower registration generation.",
2114
- };
2488
+ if (current?.registrationGeneration !==
2489
+ follower.registrationGeneration) {
2490
+ return rejectTelegramBusRequest(envelope.requestId, "Stale Telegram bus follower registration generation.");
2115
2491
  }
2116
2492
  deps.followerRegistry.remove(follower.instanceId);
2117
2493
  return {
@@ -2122,28 +2498,15 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2122
2498
  });
2123
2499
  }
2124
2500
  case "follower.heartbeat": {
2125
- const current = deps.followerRegistry.get(envelope.instanceId);
2126
- if (!current) {
2127
- return {
2128
- kind: "bus.ack",
2129
- requestId: envelope.requestId,
2130
- ok: false,
2131
- message: "Unknown Telegram bus follower instance.",
2132
- };
2133
- }
2134
- if (!current.registrationGeneration ||
2135
- envelope.registrationGeneration !== current.registrationGeneration) {
2136
- return {
2137
- kind: "bus.ack",
2138
- requestId: envelope.requestId,
2139
- ok: false,
2140
- message: "Stale Telegram bus follower registration generation.",
2141
- };
2142
- }
2501
+ const current = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
2502
+ if ("rejection" in current)
2503
+ return current.rejection;
2143
2504
  const follower = deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
2144
2505
  if (follower)
2145
2506
  observeRestoreRecipient(follower);
2146
- const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
2507
+ const displayTitle = follower
2508
+ ? deps.getFollowerDisplayTitle?.(follower)
2509
+ : undefined;
2147
2510
  return follower
2148
2511
  ? {
2149
2512
  kind: "bus.ack",
@@ -2160,12 +2523,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2160
2523
  .sort(),
2161
2524
  },
2162
2525
  }
2163
- : {
2164
- kind: "bus.ack",
2165
- requestId: envelope.requestId,
2166
- ok: false,
2167
- message: "Unknown Telegram bus follower instance.",
2168
- };
2526
+ : rejectTelegramBusRequest(envelope.requestId, "Unknown Telegram bus follower instance.");
2169
2527
  }
2170
2528
  case "follower.resolveAgentTarget":
2171
2529
  case "follower.routeAgentMessage":
@@ -2175,15 +2533,21 @@ export function createTelegramBusLeaderEnvelopeHandler(deps) {
2175
2533
  case "leader.forwardMessage":
2176
2534
  case "leader.forwardEditedMessage":
2177
2535
  return forwardToFollower(envelope);
2536
+ case "follower.deliverSelectedMenu": {
2537
+ const deliver = deps.selectedMenuDelivery, secret = deps.authSecret, protocol = structuredClone(deps.protocolIdentity);
2538
+ const current = () => !!secret &&
2539
+ deps.authSecret === secret &&
2540
+ deps.selectedMenuDelivery === deliver &&
2541
+ isDeepStrictEqual(deps.protocolIdentity, protocol) &&
2542
+ hasTelegramBusCapability(protocol, TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY);
2543
+ if (!deliver || !current())
2544
+ return rejectTelegramBusRequest(envelope.requestId, "Selected-menu delivery is unavailable.");
2545
+ return deliver(structuredClone(envelope), current);
2546
+ }
2178
2547
  case "follower.callApi":
2179
2548
  return handleFollowerApiCall(envelope, { ...deps, getNowMs });
2180
2549
  default:
2181
- return {
2182
- kind: "bus.ack",
2183
- requestId: envelope.requestId,
2184
- ok: false,
2185
- message: "Telegram bus envelope is not handled by this leader.",
2186
- };
2550
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram bus envelope is not handled by this leader.");
2187
2551
  }
2188
2552
  };
2189
2553
  }
@@ -2192,14 +2556,6 @@ function asRecord(value) {
2192
2556
  ? value
2193
2557
  : undefined;
2194
2558
  }
2195
- function asInteger(value) {
2196
- if (typeof value === "number" && Number.isInteger(value))
2197
- return value;
2198
- if (typeof value !== "string" || value.trim() === "")
2199
- return undefined;
2200
- const parsed = Number(value);
2201
- return Number.isInteger(parsed) ? parsed : undefined;
2202
- }
2203
2559
  function getFollowerApiMethodAndBody(envelope) {
2204
2560
  if (envelope.method === "call" || envelope.method === "callMultipart") {
2205
2561
  return {
@@ -2242,24 +2598,10 @@ function recordFollowerApiMessageOwnership(input) {
2242
2598
  }
2243
2599
  }
2244
2600
  async function handleFollowerApiCall(envelope, deps) {
2245
- const follower = deps.followerRegistry.get(envelope.instanceId);
2246
- if (!follower) {
2247
- return {
2248
- kind: "bus.ack",
2249
- requestId: envelope.requestId,
2250
- ok: false,
2251
- message: "Unknown Telegram bus follower instance.",
2252
- };
2253
- }
2254
- if (!follower.registrationGeneration ||
2255
- envelope.registrationGeneration !== follower.registrationGeneration) {
2256
- return {
2257
- kind: "bus.ack",
2258
- requestId: envelope.requestId,
2259
- ok: false,
2260
- message: "Stale Telegram bus follower registration generation.",
2261
- };
2262
- }
2601
+ const followerResolution = resolveRegisteredTelegramBusFollower(deps.followerRegistry, envelope.requestId, envelope.instanceId, envelope.registrationGeneration);
2602
+ if ("rejection" in followerResolution)
2603
+ return followerResolution.rejection;
2604
+ const follower = followerResolution.follower;
2263
2605
  deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
2264
2606
  if (deps.authorizeFollowerApiCall &&
2265
2607
  !deps.authorizeFollowerApiCall({
@@ -2267,20 +2609,10 @@ async function handleFollowerApiCall(envelope, deps) {
2267
2609
  method: envelope.method,
2268
2610
  args: envelope.args,
2269
2611
  })) {
2270
- return {
2271
- kind: "bus.ack",
2272
- requestId: envelope.requestId,
2273
- ok: false,
2274
- message: "Telegram bus API call is not allowed for this follower.",
2275
- };
2612
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram bus API call is not allowed for this follower.");
2276
2613
  }
2277
2614
  if (!deps.callApi) {
2278
- return {
2279
- kind: "bus.ack",
2280
- requestId: envelope.requestId,
2281
- ok: false,
2282
- message: "Telegram bus leader does not expose API calling.",
2283
- };
2615
+ return rejectTelegramBusRequest(envelope.requestId, "Telegram bus leader does not expose API calling.");
2284
2616
  }
2285
2617
  try {
2286
2618
  const result = await deps.callApi(envelope.method, envelope.args);
@@ -2327,48 +2659,6 @@ async function handleFollowerApiCall(envelope, deps) {
2327
2659
  };
2328
2660
  }
2329
2661
  }
2330
- export function createTelegramBusLeaderActivationScheduler(deps) {
2331
- let pending = false;
2332
- return (ctx) => {
2333
- if (deps.isBusPollingStarted() || pending)
2334
- return;
2335
- if (!deps.isBusEnabled())
2336
- return;
2337
- if (!deps.ownsPolling(ctx))
2338
- return;
2339
- pending = true;
2340
- const timer = setTimeout(() => {
2341
- void (async () => {
2342
- try {
2343
- if (deps.isBusPollingStarted())
2344
- return;
2345
- if (!deps.isBusEnabled())
2346
- return;
2347
- if (!deps.ownsPolling(ctx))
2348
- return;
2349
- await deps.stopClassicPolling();
2350
- try {
2351
- await deps.startBusLeaderPolling(ctx);
2352
- deps.setBusPollingStarted(true);
2353
- deps.updateStatus(ctx);
2354
- deps.recordRuntimeEvent?.("bus", "Telegram bus leader mode activated", { phase: "leader-hot-switch" });
2355
- }
2356
- catch (error) {
2357
- deps.recordRuntimeEvent?.("bus", error, {
2358
- phase: "leader-hot-switch",
2359
- });
2360
- deps.setBusPollingStarted(false);
2361
- await deps.startClassicPolling(ctx);
2362
- }
2363
- }
2364
- finally {
2365
- pending = false;
2366
- }
2367
- })();
2368
- }, 0);
2369
- timer.unref?.();
2370
- };
2371
- }
2372
2662
  export function createTelegramBusLeaderRuntime(deps) {
2373
2663
  const getNowMs = deps.getNowMs ?? Date.now;
2374
2664
  const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
@@ -2408,7 +2698,8 @@ export function createTelegramBusLeaderRuntime(deps) {
2408
2698
  const profile = deps.getTelegramProfile?.();
2409
2699
  const isCurrent = () => pruneGeneration === expectedGeneration &&
2410
2700
  deps.getTelegramProfile?.() === profile &&
2411
- (!deps.getCurrentLeaderEpoch || (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
2701
+ (!deps.getCurrentLeaderEpoch ||
2702
+ (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
2412
2703
  try {
2413
2704
  await localServer.ensureEndpoint();
2414
2705
  }
@@ -2424,20 +2715,27 @@ export function createTelegramBusLeaderRuntime(deps) {
2424
2715
  if (!isCurrent())
2425
2716
  return;
2426
2717
  const attempts = [...pendingPreservations.values()].map((observation) => ({
2427
- follower: observation.follower, observation, fresh: false,
2718
+ follower: observation.follower,
2719
+ observation,
2720
+ fresh: false,
2428
2721
  }));
2429
2722
  const removed = deps.followerRegistry.pruneStale(getNowMs(), followerStaleAfterMs);
2430
2723
  for (const follower of removed) {
2431
2724
  forgetPreservation(pendingPreservations.get(follower.instanceId));
2432
2725
  let observation;
2433
- if (deps.onFollowerConfirmedDeadPreserved && epoch !== undefined &&
2434
- follower.registrationGeneration && Number.isSafeInteger(follower.pid) && follower.pid > 0 &&
2435
- Number.isSafeInteger(follower.target?.threadId) && follower.target.threadId > 0) {
2726
+ if (deps.onFollowerConfirmedDeadPreserved &&
2727
+ epoch !== undefined &&
2728
+ follower.registrationGeneration &&
2729
+ Number.isSafeInteger(follower.pid) &&
2730
+ follower.pid > 0 &&
2731
+ Number.isSafeInteger(follower.target?.threadId) &&
2732
+ follower.target.threadId > 0) {
2436
2733
  if (pendingPreservations.size < TELEGRAM_WORKSPACE_SLOTS.length) {
2437
2734
  const registration = deps.followerRegistry.observeUnregistered(follower);
2438
2735
  let current = true;
2439
2736
  observation = {
2440
- follower, registration,
2737
+ follower,
2738
+ registration,
2441
2739
  // Reuse exact admission authority after a lost acquisition acknowledgement.
2442
2740
  operationId: createTelegramWorkspaceAdmissionOperationId(),
2443
2741
  isCurrent() {
@@ -2449,7 +2747,8 @@ export function createTelegramBusLeaderRuntime(deps) {
2449
2747
  }
2450
2748
  else {
2451
2749
  recordPruneEvent("Telegram follower preservation observation capacity exhausted; retaining binding", {
2452
- phase: "follower-preservation-capacity", instanceId: follower.instanceId,
2750
+ phase: "follower-preservation-capacity",
2751
+ instanceId: follower.instanceId,
2453
2752
  });
2454
2753
  }
2455
2754
  }
@@ -2463,23 +2762,31 @@ export function createTelegramBusLeaderRuntime(deps) {
2463
2762
  continue;
2464
2763
  }
2465
2764
  let processConfirmedDead = false;
2466
- if (follower.pid !== undefined && Number.isSafeInteger(follower.pid) &&
2467
- follower.pid > 0 && deps.isFollowerProcessAlive) {
2765
+ if (follower.pid !== undefined &&
2766
+ Number.isSafeInteger(follower.pid) &&
2767
+ follower.pid > 0 &&
2768
+ deps.isFollowerProcessAlive) {
2468
2769
  try {
2469
- processConfirmedDead = deps.isFollowerProcessAlive(follower.pid) === false;
2770
+ processConfirmedDead =
2771
+ deps.isFollowerProcessAlive(follower.pid) === false;
2470
2772
  }
2471
2773
  catch (error) {
2472
2774
  if (fresh)
2473
2775
  recordPruneEvent(error, {
2474
- phase: "follower-process-liveness", instanceId: follower.instanceId, pid: follower.pid,
2776
+ phase: "follower-process-liveness",
2777
+ instanceId: follower.instanceId,
2778
+ pid: follower.pid,
2475
2779
  });
2476
2780
  }
2477
2781
  }
2478
2782
  if (!processConfirmedDead) {
2479
2783
  if (fresh)
2480
2784
  recordPruneEvent("Telegram bus follower heartbeat stale; preserving thread binding", {
2481
- phase: "follower-pruned", instanceId: follower.instanceId,
2482
- processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive ? "unknown" : "alive-or-unknown",
2785
+ phase: "follower-pruned",
2786
+ instanceId: follower.instanceId,
2787
+ processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive
2788
+ ? "unknown"
2789
+ : "alive-or-unknown",
2483
2790
  });
2484
2791
  continue;
2485
2792
  }
@@ -2489,23 +2796,32 @@ export function createTelegramBusLeaderRuntime(deps) {
2489
2796
  }
2490
2797
  catch (error) {
2491
2798
  recordPruneEvent(error, {
2492
- phase: "follower-confirmed-dead-cleanup-policy", instanceId: follower.instanceId, pid: follower.pid,
2799
+ phase: "follower-confirmed-dead-cleanup-policy",
2800
+ instanceId: follower.instanceId,
2801
+ pid: follower.pid,
2493
2802
  });
2494
2803
  }
2495
2804
  if (!isCurrent()) {
2496
2805
  forgetPreservation(observation);
2497
2806
  return;
2498
2807
  }
2499
- const recordPreserved = () => recordPruneEvent("Telegram bus follower process confirmed dead; preserving thread binding", { phase: "follower-confirmed-dead-preserved", instanceId: follower.instanceId,
2500
- pid: follower.pid, cleanupEnabled });
2808
+ const recordPreserved = () => recordPruneEvent("Telegram bus follower process confirmed dead; preserving thread binding", {
2809
+ phase: "follower-confirmed-dead-preserved",
2810
+ instanceId: follower.instanceId,
2811
+ pid: follower.pid,
2812
+ cleanupEnabled,
2813
+ });
2501
2814
  // Deferred observations authorize only non-destructive publication, never deletion retries.
2502
2815
  if (cleanupEnabled === true && !fresh) {
2503
2816
  forgetPreservation(observation);
2504
2817
  recordPreserved();
2505
2818
  continue;
2506
2819
  }
2507
- const onConfirmedDead = cleanupEnabled === true ? deps.onFollowerConfirmedDead :
2508
- cleanupEnabled === false && observation ? deps.onFollowerConfirmedDeadPreserved : undefined;
2820
+ const onConfirmedDead = cleanupEnabled === true
2821
+ ? deps.onFollowerConfirmedDead
2822
+ : cleanupEnabled === false && observation
2823
+ ? deps.onFollowerConfirmedDeadPreserved
2824
+ : undefined;
2509
2825
  if (!onConfirmedDead) {
2510
2826
  if (cleanupEnabled === true)
2511
2827
  forgetPreservation(observation);
@@ -2513,15 +2829,21 @@ export function createTelegramBusLeaderRuntime(deps) {
2513
2829
  recordPreserved();
2514
2830
  continue;
2515
2831
  }
2516
- const findReplacement = () => deps.followerRegistry.list().find((candidate) => candidate.instanceId === follower.instanceId ||
2517
- (!!follower.profileKey && candidate.profileKey === follower.profileKey) ||
2518
- (!!follower.target && candidate.target?.chatId === follower.target.chatId &&
2832
+ const findReplacement = () => deps.followerRegistry
2833
+ .list()
2834
+ .find((candidate) => candidate.instanceId === follower.instanceId ||
2835
+ (!!follower.profileKey &&
2836
+ candidate.profileKey === follower.profileKey) ||
2837
+ (!!follower.target &&
2838
+ candidate.target?.chatId === follower.target.chatId &&
2519
2839
  candidate.target?.threadId === follower.target.threadId));
2520
2840
  let settled = false;
2521
2841
  try {
2522
2842
  await runFollowerMutation(follower, async () => {
2523
- const isDetached = () => isCurrent() && (!observation || observation.isCurrent()) &&
2524
- !findReplacement() && deps.isFollowerProcessAlive?.(follower.pid) === false;
2843
+ const isDetached = () => isCurrent() &&
2844
+ (!observation || observation.isCurrent()) &&
2845
+ !findReplacement() &&
2846
+ deps.isFollowerProcessAlive?.(follower.pid) === false;
2525
2847
  if (!isDetached())
2526
2848
  return;
2527
2849
  if (cleanupEnabled === true)
@@ -2534,13 +2856,19 @@ export function createTelegramBusLeaderRuntime(deps) {
2534
2856
  }
2535
2857
  catch (error) {
2536
2858
  recordPruneEvent(error, {
2537
- phase: cleanupEnabled ? "follower-confirmed-dead-cleanup" : "follower-confirmed-dead-preserve",
2538
- instanceId: follower.instanceId, pid: follower.pid,
2539
- chatId: follower.target?.chatId, threadId: follower.target?.threadId,
2859
+ phase: cleanupEnabled
2860
+ ? "follower-confirmed-dead-cleanup"
2861
+ : "follower-confirmed-dead-preserve",
2862
+ instanceId: follower.instanceId,
2863
+ pid: follower.pid,
2864
+ chatId: follower.target?.chatId,
2865
+ threadId: follower.target?.threadId,
2540
2866
  });
2541
2867
  }
2542
2868
  finally {
2543
- if (settled || cleanupEnabled === true || (observation && !observation.isCurrent())) {
2869
+ if (settled ||
2870
+ cleanupEnabled === true ||
2871
+ (observation && !observation.isCurrent())) {
2544
2872
  forgetPreservation(observation);
2545
2873
  }
2546
2874
  }
@@ -2575,7 +2903,20 @@ export function createTelegramBusLeaderRuntime(deps) {
2575
2903
  }, followerPruneIntervalMs);
2576
2904
  pruneInterval.unref?.();
2577
2905
  };
2906
+ const selectedMenuDelivery = deps.selectedMenuDelivery, selectedMenuProtocol = deps.protocolIdentity, selectedMenuRegistry = deps.followerRegistry, selectedMenuSecret = deps.authSecret;
2578
2907
  const handleEnvelope = createTelegramBusLeaderEnvelopeHandler({
2908
+ selectedMenuDelivery: selectedMenuDelivery
2909
+ ? (input, isCallerCurrent) => {
2910
+ const generation = pruneGeneration;
2911
+ return selectedMenuDelivery.call(deps, input, () => !!pruneInterval &&
2912
+ pruneGeneration === generation &&
2913
+ deps.selectedMenuDelivery === selectedMenuDelivery &&
2914
+ deps.protocolIdentity === selectedMenuProtocol &&
2915
+ deps.followerRegistry === selectedMenuRegistry &&
2916
+ deps.authSecret === selectedMenuSecret &&
2917
+ isCallerCurrent());
2918
+ }
2919
+ : undefined,
2579
2920
  followerRegistry: deps.followerRegistry,
2580
2921
  authSecret: deps.authSecret,
2581
2922
  protocolIdentity: deps.protocolIdentity,
@@ -2586,11 +2927,13 @@ export function createTelegramBusLeaderRuntime(deps) {
2586
2927
  resolveAgentTarget: deps.resolveAgentTarget,
2587
2928
  routeAgentMessage: deps.routeAgentMessage,
2588
2929
  routeQueueHandoff: deps.routeQueueHandoff,
2589
- provisionFollowerTarget: deps.provisionFollowerTarget ? (registration, options) => {
2590
- // Same-instance ownership can change in the store before registry publication.
2591
- forgetPreservation(pendingPreservations.get(registration.instanceId));
2592
- return deps.provisionFollowerTarget(registration, options);
2593
- } : undefined,
2930
+ provisionFollowerTarget: deps.provisionFollowerTarget
2931
+ ? (registration, options) => {
2932
+ // Same-instance ownership can change in the store before registry publication.
2933
+ forgetPreservation(pendingPreservations.get(registration.instanceId));
2934
+ return deps.provisionFollowerTarget(registration, options);
2935
+ }
2936
+ : undefined,
2594
2937
  commitFollowerRegistration: deps.commitFollowerRegistration,
2595
2938
  onFollowerDisconnected: deps.onFollowerDisconnected,
2596
2939
  renameFollowerThread: deps.renameFollowerThread,
@@ -2615,22 +2958,11 @@ export function createTelegramBusLeaderRuntime(deps) {
2615
2958
  const recipient = deps.followerRegistry.get(input.recipientInstanceId);
2616
2959
  if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
2617
2960
  !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
2618
- return {
2619
- kind: "bus.ack",
2620
- requestId: input.requestId,
2621
- ok: false,
2622
- message: "Telegram queue handoff capability was not negotiated.",
2623
- };
2961
+ return rejectTelegramBusRequest(input.requestId, "Telegram queue handoff capability was not negotiated.");
2624
2962
  }
2625
2963
  if (!recipient?.registrationGeneration ||
2626
- input.recipientRegistrationGeneration !==
2627
- recipient.registrationGeneration) {
2628
- return {
2629
- kind: "bus.ack",
2630
- requestId: input.requestId,
2631
- ok: false,
2632
- message: "Stale Telegram queue handoff recipient registration generation.",
2633
- };
2964
+ input.recipientRegistrationGeneration !== recipient.registrationGeneration) {
2965
+ return rejectTelegramBusRequest(input.requestId, "Stale Telegram queue handoff recipient registration generation.");
2634
2966
  }
2635
2967
  const recipientSocketPath = recipient.busSocketPath ??
2636
2968
  getTelegramBusFollowerSocketPath(recipient.instanceId);