@llblab/pi-telegram 0.52.2 → 0.53.1

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 +15 -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 +65 -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 +252 -127
  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 +128 -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 +516 -301
  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
package/lib/bus-leader.ts CHANGED
@@ -6,64 +6,78 @@
6
6
  */
7
7
 
8
8
  import { isDeepStrictEqual } from "node:util";
9
- import * as Sync from "./sync.ts";
10
- import {
11
- createTelegramThreadDisplayReconciler,
12
- resolveTelegramInitialWorkspaceDisplayName,
13
- resolveTelegramLiveWorkspaceBindingKeys,
14
- } from "./thread-display.ts";
15
- import type { TelegramThreadDisplayMode } from "./config.ts";
16
- import * as ThreadNaming from "./thread-naming.ts";
17
- import * as ThreadReconciler from "./thread-reconciler.ts";
18
- import {
19
- getTelegramApiErrorRequestTarget,
20
- isTelegramApiCommitUnknownError,
21
- type TelegramApiCallOptions,
22
- } from "./telegram-api.ts";
23
- import type { TelegramTarget } from "./target.ts";
24
- import type { TelegramAttachmentSource } from "./media.ts";
25
- import * as Threads from "./threads.ts";
26
- import * as WorkspaceIdentity from "./workspace-identity.ts";
9
+
10
+ import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
27
11
  import {
28
12
  createTelegramBusLocalServer,
29
13
  createUnauthorizedBusAck,
14
+ rejectTelegramBusRequest,
30
15
  getTelegramBusEnvelopeTrafficClass,
16
+ getTelegramBusFollowerSocketPath,
31
17
  getTelegramBusProtocolCompatibility,
32
18
  hasTelegramBusCapability,
33
19
  isTelegramBusEnvelopeAuthorized,
34
- getTelegramBusFollowerSocketPath,
20
+ parseTelegramBusEnvelope,
35
21
  sendTelegramBusLocalEnvelope,
36
22
  stripTelegramBusApiMetadata,
23
+ TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
24
+ TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
25
+ TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
26
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
27
+ TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
28
+ TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
29
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
30
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
31
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
37
32
  type TelegramBusEnvelope,
33
+ type TelegramBusLeaderQueueHandoffOffer,
38
34
  type TelegramBusFollowerRegistry,
39
35
  type TelegramBusFollowerView,
40
36
  type TelegramBusInstanceRegistration,
41
37
  type TelegramBusProtocolIdentity,
38
+ type TelegramBusSelectedMenuDelivery,
42
39
  type TelegramBusSocketPathSource,
43
- TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
44
- TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
45
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
46
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
47
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
48
- TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
49
- TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
50
- TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
51
40
  } from "./bus.ts";
52
- import { getTelegramBusTransportRetryPolicy } from "./bus-transport.ts";
53
- import type { TelegramQueueHandoffPayload } from "./queue.ts";
41
+ import type { TelegramThreadDisplayMode } from "./config.ts";
42
+ import type { TelegramAttachmentSource } from "./media.ts";
43
+ import { escapeHtml } from "./rendering.ts";
44
+ import * as Sync from "./sync.ts";
45
+ import {
46
+ parseTelegramIntegerId as asInteger,
47
+ type TelegramTarget,
48
+ } from "./target.ts";
49
+ import {
50
+ getTelegramApiErrorRequestTarget,
51
+ isTelegramApiCommitUnknownError,
52
+ TelegramApiAuthorityError,
53
+ type TelegramApiCallOptions,
54
+ type TelegramBridgeApiRuntime,
55
+ } from "./telegram-api.ts";
56
+ import {
57
+ createTelegramThreadDisplayReconciler,
58
+ resolveTelegramInitialWorkspaceDisplayName,
59
+ resolveTelegramLiveWorkspaceBindingKeys,
60
+ } from "./thread-display.ts";
61
+ import * as ThreadNaming from "./thread-naming.ts";
62
+ import * as ThreadReconciler from "./thread-reconciler.ts";
63
+ import * as Threads from "./threads.ts";
64
+ import {
65
+ createTelegramWorkspaceAdmissionOperationId,
66
+ runWithTelegramWorkspaceAdmissionsAsync,
67
+ type TelegramWorkspaceAdmissionLedger,
68
+ } from "./workspace-admission.ts";
69
+ import * as WorkspaceIdentity from "./workspace-identity.ts";
54
70
  import {
55
71
  createTelegramWorkspaceOperationRuntime,
56
72
  createTelegramWorkspaceSlotRotation,
57
73
  type TelegramWorkspaceCapacityRunner,
58
- type TelegramWorkspaceSlotRotationPorts,
59
74
  type TelegramWorkspaceOperationRunner,
75
+ type TelegramWorkspaceSlotRotationPorts,
60
76
  } from "./workspace-retirement.ts";
61
77
  import {
62
- createTelegramWorkspaceAdmissionOperationId,
63
- runWithTelegramWorkspaceAdmissionsAsync,
64
- type TelegramWorkspaceAdmissionLedger,
65
- } from "./workspace-admission.ts";
66
- import { TELEGRAM_WORKSPACE_SLOTS, TelegramWorkspaceSlotUnavailableError } from "./workspace-slots.ts";
78
+ TELEGRAM_WORKSPACE_SLOTS,
79
+ TelegramWorkspaceSlotUnavailableError,
80
+ } from "./workspace-slots.ts";
67
81
 
68
82
  export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
69
83
 
@@ -82,21 +96,9 @@ export interface TelegramBusLeaderRuntime<TContext> {
82
96
  ) => Promise<Threads.TelegramTopicTargetRecord>;
83
97
  startPolling: (ctx: TContext) => Promise<void>;
84
98
  stopPolling: () => Promise<void>;
85
- routeQueueHandoff: (input: {
86
- requestId: string;
87
- auth?: string;
88
- recipientInstanceId: string;
89
- recipientRegistrationGeneration: string;
90
- donorInstanceId: string;
91
- donorProcessId: number;
92
- donorProcessBirthId: string;
93
- donorSessionGeneration: number;
94
- donorAcquisitionId: string;
95
- donorAcquiredAtMs: number;
96
- handoffToken: string;
97
- payload: TelegramQueueHandoffPayload;
98
- sentAtMs: number;
99
- }) => Promise<TelegramBusEnvelope>;
99
+ routeQueueHandoff: (
100
+ input: TelegramBusLeaderQueueHandoffOffer,
101
+ ) => Promise<TelegramBusEnvelope>;
100
102
  }
101
103
 
102
104
  export interface TelegramBusFollowerLifecycleAnnouncement {
@@ -105,13 +107,6 @@ export interface TelegramBusFollowerLifecycleAnnouncement {
105
107
  parseMode: "HTML";
106
108
  }
107
109
 
108
- function escapeHtml(text: string): string {
109
- return text
110
- .replace(/&/g, "&amp;")
111
- .replace(/</g, "&lt;")
112
- .replace(/>/g, "&gt;");
113
- }
114
-
115
110
  function formatTelegramBusInstanceLabel(input: {
116
111
  threadName?: string;
117
112
  slot?: string;
@@ -223,7 +218,11 @@ export interface TelegramBusLeaderApiProxyDeps {
223
218
  fileName: string,
224
219
  options?: TelegramApiCallOptions,
225
220
  ) => Promise<unknown>;
226
- downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<unknown>;
221
+ downloadFile: (
222
+ fileId: string,
223
+ suggestedName: string,
224
+ source?: TelegramAttachmentSource,
225
+ ) => Promise<unknown>;
227
226
  recoverStaleTargetError?: (
228
227
  apiBody: unknown,
229
228
  error: unknown,
@@ -232,7 +231,10 @@ export interface TelegramBusLeaderApiProxyDeps {
232
231
 
233
232
  export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
234
233
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
235
- persistThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
234
+ persistThreadDisplayMode?: (
235
+ mode: TelegramThreadDisplayMode,
236
+ isCurrent: () => boolean,
237
+ ) => Promise<void>;
236
238
  onThreadDisplayChanged?: () => void;
237
239
  runtime: Omit<
238
240
  TelegramBusLeaderRuntimeDeps<TContext>,
@@ -273,10 +275,12 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
273
275
  captureWorkspaceExternalProtection?: (
274
276
  binding: Threads.TelegramWorkspaceThreadBinding,
275
277
  ) => Threads.TelegramWorkspaceExternalProtectionEvidence;
276
- getWorkspaceAdmission?: () => Pick<
277
- TelegramWorkspaceAdmissionLedger,
278
- "acquireAdmission" | "releaseAdmission"
279
- > | undefined;
278
+ getWorkspaceAdmission?: () =>
279
+ | Pick<
280
+ TelegramWorkspaceAdmissionLedger,
281
+ "acquireAdmission" | "releaseAdmission"
282
+ >
283
+ | undefined;
280
284
  runWorkspaceOperation?: TelegramBusWorkspaceAdmissionRunner;
281
285
  workspaceRotation?: TelegramWorkspaceSlotRotationPorts;
282
286
  }
@@ -328,7 +332,9 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
328
332
  typeof intent.target.threadId !== "number" ||
329
333
  follower.target?.chatId !== intent.target.chatId ||
330
334
  follower.target.threadId !== intent.target.threadId ||
331
- !cwd || cwd !== intent.cwd || !sessionId ||
335
+ !cwd ||
336
+ cwd !== intent.cwd ||
337
+ !sessionId ||
332
338
  intent.profileName !== (deps.getTelegramProfile?.() ?? "default")
333
339
  ) {
334
340
  throw new Error(
@@ -336,15 +342,19 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
336
342
  );
337
343
  }
338
344
  if (intent.expiresAtMs <= nowMs || intent.expiresAtMs > nowMs + ttlMs) {
339
- throw new Error("Telegram session replacement intent is expired or unbounded.");
345
+ throw new Error(
346
+ "Telegram session replacement intent is expired or unbounded.",
347
+ );
340
348
  }
341
349
  return { cwd, sessionId };
342
350
  };
343
351
  return {
344
352
  async publish(follower, intent, isCurrent) {
345
353
  const { sessionId } = assertCommon(follower, intent);
346
- if (intent.sourceInstanceId !== follower.instanceId ||
347
- intent.sourceSessionId !== sessionId) {
354
+ if (
355
+ intent.sourceInstanceId !== follower.instanceId ||
356
+ intent.sourceSessionId !== sessionId
357
+ ) {
348
358
  throw new Error(
349
359
  "Telegram session replacement source does not match the current follower registration.",
350
360
  );
@@ -353,7 +363,8 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
353
363
  if (!isCurrent()) return false;
354
364
  const binding = deps.store.getWorkspaceBindingByTarget(intent.target);
355
365
  if (
356
- !binding || binding.cwd !== intent.cwd ||
366
+ !binding ||
367
+ binding.cwd !== intent.cwd ||
357
368
  binding.sessionId !== intent.sourceSessionId ||
358
369
  binding.slot !== intent.slot ||
359
370
  (binding.manualThreadName ?? binding.threadName) !== intent.threadName
@@ -376,9 +387,13 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
376
387
  }
377
388
  await deps.store.load();
378
389
  if (!isCurrent()) return false;
379
- if (deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)?.cwd !==
380
- intent.cwd) {
381
- throw new Error("Telegram session replacement successor binding is unavailable.");
390
+ if (
391
+ deps.store.getWorkspaceBindingByTarget(intent.target, sessionId)
392
+ ?.cwd !== intent.cwd
393
+ ) {
394
+ throw new Error(
395
+ "Telegram session replacement successor binding is unavailable.",
396
+ );
382
397
  }
383
398
  return deps.store.removeSessionReplacementIntent(intent, isCurrent);
384
399
  },
@@ -391,9 +406,11 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
391
406
  renameLeaderThreadAdmitted: (
392
407
  threadName: string,
393
408
  expectedTarget?: Threads.TelegramTopicTargetRecord["target"],
409
+ assertRecipientAuthority?: () => void,
394
410
  ) => Promise<Threads.TelegramTopicTargetRecord>;
395
411
  resetLeaderThreadName: (
396
412
  expectedTarget: Threads.TelegramTopicTargetRecord["target"],
413
+ assertRecipientAuthority?: () => void,
397
414
  ) => Promise<{ threadName: string }>;
398
415
  resetThreadNameAdmitted: (
399
416
  target: Threads.TelegramTopicTargetRecord["target"],
@@ -404,7 +421,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
404
421
  ? (input, operation) => {
405
422
  const admission = deps.getWorkspaceAdmission?.();
406
423
  if (!admission) {
407
- throw new Error("Telegram Workspace admission authority is unavailable.");
424
+ throw new Error(
425
+ "Telegram Workspace admission authority is unavailable.",
426
+ );
408
427
  }
409
428
  return runWithTelegramWorkspaceAdmissionsAsync({
410
429
  ledger: admission,
@@ -419,7 +438,8 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
419
438
  });
420
439
  }
421
440
  : undefined;
422
- const runWorkspaceOperation = deps.runWorkspaceOperation ??
441
+ const runWorkspaceOperation =
442
+ deps.runWorkspaceOperation ??
423
443
  createTelegramWorkspaceOperationRuntime({
424
444
  getWorkspaceAdmission: deps.getWorkspaceAdmission,
425
445
  onReleaseError(error, operationKind) {
@@ -429,20 +449,28 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
429
449
  });
430
450
  },
431
451
  }).run;
432
- const runWithWorkspaceCapacity = deps.workspaceRotation && deps.captureWorkspaceExternalProtection &&
433
- deps.getCurrentLeaderEpoch ? createTelegramWorkspaceSlotRotation({
434
- ...deps.workspaceRotation, store: deps.topicTargetStore,
435
- getLeaderEpoch: deps.getCurrentLeaderEpoch,
436
- getExternalProtection: deps.captureWorkspaceExternalProtection,
437
- recordEvent(message, details) { deps.recordRuntimeEvent("bus", message, details); },
438
- }) : undefined;
452
+ const runWithWorkspaceCapacity =
453
+ deps.workspaceRotation &&
454
+ deps.captureWorkspaceExternalProtection &&
455
+ deps.getCurrentLeaderEpoch
456
+ ? createTelegramWorkspaceSlotRotation({
457
+ ...deps.workspaceRotation,
458
+ store: deps.topicTargetStore,
459
+ getLeaderEpoch: deps.getCurrentLeaderEpoch,
460
+ getExternalProtection: deps.captureWorkspaceExternalProtection,
461
+ recordEvent(message, details) {
462
+ deps.recordRuntimeEvent("bus", message, details);
463
+ },
464
+ })
465
+ : undefined;
439
466
  const captureLiveBindingKeys = (
440
467
  bindings: readonly Threads.TelegramWorkspaceThreadBinding[],
441
- ): ReadonlySet<string> => resolveTelegramLiveWorkspaceBindingKeys(
442
- bindings,
443
- deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target,
444
- deps.runtime.followerRegistry.list(),
445
- );
468
+ ): ReadonlySet<string> =>
469
+ resolveTelegramLiveWorkspaceBindingKeys(
470
+ bindings,
471
+ deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target,
472
+ deps.runtime.followerRegistry.list(),
473
+ );
446
474
  const provisionerPorts = {
447
475
  getAllowedUserId: deps.getAllowedUserId,
448
476
  topicTargetStore: deps.topicTargetStore,
@@ -471,42 +499,59 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
471
499
  runWorkspaceOperation,
472
500
  recordRuntimeEvent: deps.recordRuntimeEvent,
473
501
  };
474
- const display = deps.getThreadDisplayMode ? createTelegramThreadDisplayReconciler({
475
- store: deps.topicTargetStore,
476
- getMode: deps.getThreadDisplayMode,
477
- getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
478
- getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
479
- captureLiveBindingKeys,
480
- captureBindingAuthority(binding) {
481
- const matches = (target: TelegramTarget | undefined) =>
482
- target?.chatId === binding.target.chatId && target?.threadId === binding.target.threadId;
483
- const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
484
- if (leader && matches(leader.target)) {
485
- return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target);
486
- }
487
- const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
488
- if (!follower?.registrationGeneration) return undefined;
489
- const generation = follower.registrationGeneration;
490
- return () => {
491
- const current = deps.runtime.followerRegistry.get(follower.instanceId);
492
- return current?.registrationGeneration === generation && matches(current.target);
493
- };
494
- },
495
- callApi: deps.callApi,
496
- }) : undefined;
502
+ const display = deps.getThreadDisplayMode
503
+ ? createTelegramThreadDisplayReconciler({
504
+ store: deps.topicTargetStore,
505
+ getMode: deps.getThreadDisplayMode,
506
+ getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
507
+ getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
508
+ captureLiveBindingKeys,
509
+ captureBindingAuthority(binding) {
510
+ const matches = (target: TelegramTarget | undefined) =>
511
+ target?.chatId === binding.target.chatId &&
512
+ target?.threadId === binding.target.threadId;
513
+ const leader = deps.topicTargetStore.getActiveByInstanceId(
514
+ deps.instanceId,
515
+ );
516
+ if (leader && matches(leader.target)) {
517
+ return () =>
518
+ matches(
519
+ deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)
520
+ ?.target,
521
+ );
522
+ }
523
+ const follower = deps.runtime.followerRegistry.getByTarget(
524
+ binding.target,
525
+ );
526
+ if (!follower?.registrationGeneration) return undefined;
527
+ const generation = follower.registrationGeneration;
528
+ return () => {
529
+ const current = deps.runtime.followerRegistry.get(
530
+ follower.instanceId,
531
+ );
532
+ return (
533
+ current?.registrationGeneration === generation &&
534
+ matches(current.target)
535
+ );
536
+ };
537
+ },
538
+ callApi: deps.callApi,
539
+ })
540
+ : undefined;
497
541
  const reconcileThreadDisplayOperation = async () => {
498
542
  const result = await display!.reconcile();
499
543
  deps.onThreadDisplayChanged?.();
500
544
  return result;
501
545
  };
502
- const reconcileThreadDisplay = () => runWorkspaceOperation(
503
- {
504
- operationId: createTelegramWorkspaceAdmissionOperationId(),
505
- operationKind: "workspace.reconcile-display",
506
- scopes: [{ kind: "profile" }],
507
- },
508
- reconcileThreadDisplayOperation,
509
- );
546
+ const reconcileThreadDisplay = () =>
547
+ runWorkspaceOperation(
548
+ {
549
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
550
+ operationKind: "workspace.reconcile-display",
551
+ scopes: [{ kind: "profile" }],
552
+ },
553
+ reconcileThreadDisplayOperation,
554
+ );
510
555
  let startupDisplayTimer: ReturnType<typeof setTimeout> | undefined;
511
556
  let startupDisplayGeneration = 0;
512
557
  let startupDisplaySettled = true;
@@ -519,7 +564,9 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
519
564
  const scheduleDisplay = () => {
520
565
  if (!display || !startupDisplaySettled) return;
521
566
  void reconcileThreadDisplay().catch((error) => {
522
- deps.recordRuntimeEvent("bus", error, { phase: "thread-display-reconcile" });
567
+ deps.recordRuntimeEvent("bus", error, {
568
+ phase: "thread-display-reconcile",
569
+ });
523
570
  });
524
571
  };
525
572
  const beginStartupDisplayStabilization = () => {
@@ -528,8 +575,8 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
528
575
  };
529
576
  const armStartupDisplayReconciliation = () => {
530
577
  const generation = startupDisplayGeneration;
531
- const delayMs = deps.runtime.followerStaleAfterMs ??
532
- TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
578
+ const delayMs =
579
+ deps.runtime.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
533
580
  startupDisplayTimer = setTimeout(() => {
534
581
  if (startupDisplayGeneration !== generation) return;
535
582
  startupDisplayTimer = undefined;
@@ -539,41 +586,68 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
539
586
  startupDisplayTimer.unref?.();
540
587
  };
541
588
  let modeTail: Promise<void> = Promise.resolve();
542
- const applyThreadDisplayMode = (mode: TelegramThreadDisplayMode, isCurrent: () => boolean): Promise<void> => {
589
+ const applyThreadDisplayMode = (
590
+ mode: TelegramThreadDisplayMode,
591
+ isCurrent: () => boolean,
592
+ ): Promise<void> => {
543
593
  const epoch = deps.getCurrentLeaderEpoch?.();
544
594
  const profile = deps.getTelegramProfile?.();
545
- const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
546
- deps.getTelegramProfile?.() === profile && isCurrent();
547
- const run = modeTail.then(() => runWorkspaceOperation(
548
- {
549
- operationId: createTelegramWorkspaceAdmissionOperationId(),
550
- operationKind: "workspace.set-display-mode",
551
- scopes: [{ kind: "profile" }],
552
- },
553
- async () => {
554
- if (!display || !deps.persistThreadDisplayMode || !current()) {
555
- throw new Error("Telegram Thread display setting requires current leader authority.");
556
- }
557
- const assertDisplayPeers = () => {
558
- const requiredCapability = mode === "directory-snake" || mode === "directory-title"
559
- ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
560
- : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
561
- if (mode !== "names" && deps.runtime.followerRegistry.list().some((follower) =>
562
- !hasTelegramBusCapability(follower.protocol, requiredCapability),
563
- )) throw new Error("Update or restart all connected followers before changing Thread display mode.");
564
- };
565
- assertDisplayPeers();
566
- await deps.persistThreadDisplayMode(mode, current);
567
- assertDisplayPeers();
568
- if (!current() || deps.getThreadDisplayMode?.() !== mode) {
569
- throw new Error("Telegram Thread display preference changed before application.");
570
- }
571
- await reconcileThreadDisplayOperation();
572
- if (!current() || deps.getThreadDisplayMode?.() !== mode) {
573
- throw new Error("Telegram Thread display application lost its originating authority.");
574
- }
575
- },
576
- ));
595
+ const current = () =>
596
+ epoch !== undefined &&
597
+ deps.getCurrentLeaderEpoch?.() === epoch &&
598
+ deps.getTelegramProfile?.() === profile &&
599
+ isCurrent();
600
+ const run = modeTail.then(() =>
601
+ runWorkspaceOperation(
602
+ {
603
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
604
+ operationKind: "workspace.set-display-mode",
605
+ scopes: [{ kind: "profile" }],
606
+ },
607
+ async () => {
608
+ if (!display || !deps.persistThreadDisplayMode || !current()) {
609
+ throw new Error(
610
+ "Telegram Thread display setting requires current leader authority.",
611
+ );
612
+ }
613
+ const assertDisplayPeers = () => {
614
+ const requiredCapability =
615
+ mode === "directory-snake" || mode === "directory-title"
616
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
617
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
618
+ if (
619
+ mode !== "names" &&
620
+ deps.runtime.followerRegistry
621
+ .list()
622
+ .some(
623
+ (follower) =>
624
+ !hasTelegramBusCapability(
625
+ follower.protocol,
626
+ requiredCapability,
627
+ ),
628
+ )
629
+ )
630
+ throw new Error(
631
+ "Update or restart all connected followers before changing Thread display mode.",
632
+ );
633
+ };
634
+ assertDisplayPeers();
635
+ await deps.persistThreadDisplayMode(mode, current);
636
+ assertDisplayPeers();
637
+ if (!current() || deps.getThreadDisplayMode?.() !== mode) {
638
+ throw new Error(
639
+ "Telegram Thread display preference changed before application.",
640
+ );
641
+ }
642
+ await reconcileThreadDisplayOperation();
643
+ if (!current() || deps.getThreadDisplayMode?.() !== mode) {
644
+ throw new Error(
645
+ "Telegram Thread display application lost its originating authority.",
646
+ );
647
+ }
648
+ },
649
+ ),
650
+ );
577
651
  modeTail = run.catch(() => undefined);
578
652
  return run;
579
653
  };
@@ -593,149 +667,414 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
593
667
  const disconnectFollower = createTelegramBusFollowerDisconnectHandler({
594
668
  ...provisionerPorts,
595
669
  });
596
- const cleanupConfirmedDeadFollower = createTelegramBusFollowerConfirmedDeadHandler({
597
- ...provisionerPorts,
598
- });
670
+ const cleanupConfirmedDeadFollower =
671
+ createTelegramBusFollowerConfirmedDeadHandler({
672
+ ...provisionerPorts,
673
+ });
599
674
  const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
600
675
  ...provisionerPorts,
601
676
  });
602
677
  const renameLeaderThreadAdmitted = async (
603
678
  threadName: string,
604
679
  expectedTarget?: Threads.TelegramTopicTargetRecord["target"],
680
+ assertRecipientAuthority?: () => void,
605
681
  ) => {
606
- const leaderEpoch = deps.getCurrentLeaderEpoch?.();
607
- if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
682
+ const store = deps.topicTargetStore,
683
+ callApi = deps.callApi;
684
+ const publishName = store.renameByTargetAndPersist.bind(store);
685
+ const persist = store.persist.bind(store);
686
+ const instanceId = deps.instanceId;
687
+ const getProfile = deps.getTelegramProfile,
688
+ getUser = deps.getAllowedUserId;
689
+ const getEpoch = deps.getCurrentLeaderEpoch;
690
+ const profile = getProfile?.(),
691
+ user = getUser();
692
+ const recipientAuthority = assertRecipientAuthority;
693
+ recipientAuthority?.();
694
+ const leaderEpoch = getEpoch?.();
695
+ if (getEpoch && leaderEpoch === undefined) {
608
696
  throw new Error(
609
697
  "Telegram Workspace Thread rename requires leader ownership.",
610
698
  );
611
699
  }
612
700
  const record = Threads.findCurrentTelegramInstanceThreadRecord({
613
- records: deps.topicTargetStore.list(),
614
- instanceId: deps.instanceId,
701
+ records: store.list(),
702
+ instanceId,
615
703
  });
616
704
  if (typeof record?.target.threadId !== "number") {
617
705
  throw new Error("No Workspace Thread is bound to this Pi instance.");
618
706
  }
619
- if (expectedTarget && (record.target.chatId !== expectedTarget.chatId ||
620
- record.target.threadId !== expectedTarget.threadId)) {
707
+ if (
708
+ expectedTarget &&
709
+ (record.target.chatId !== expectedTarget.chatId ||
710
+ record.target.threadId !== expectedTarget.threadId)
711
+ ) {
621
712
  throw new Error("Telegram Workspace Thread rename target changed.");
622
713
  }
623
- const rename = Threads.createTelegramTopicTargetRenamer({
624
- store: deps.topicTargetStore,
625
- callApi: deps.callApi,
626
- assertAuthority() {
627
- if (
628
- deps.getCurrentLeaderEpoch &&
629
- deps.getCurrentLeaderEpoch() !== leaderEpoch
630
- ) {
631
- throw new Error(
632
- "Telegram Workspace Thread rename lost leader ownership.",
633
- );
634
- }
635
- const current = Threads.findCurrentTelegramInstanceThreadRecord({
636
- records: deps.topicTargetStore.list(),
637
- instanceId: deps.instanceId,
638
- });
639
- if (current?.target.chatId !== record.target.chatId ||
640
- current.target.threadId !== record.target.threadId) {
641
- throw new Error("Telegram Workspace Thread rename target changed.");
642
- }
643
- },
714
+ const target = { ...record.target },
715
+ slot = record.slot,
716
+ owner = structuredClone(record.owner);
717
+ const bindingIdentity = (
718
+ binding: Threads.TelegramWorkspaceThreadBinding,
719
+ ) => ({
720
+ bindingKey: binding.bindingKey,
721
+ cwd: binding.cwd,
722
+ workspaceKey: binding.workspaceKey,
723
+ sessionId: binding.sessionId,
724
+ sessionKey: binding.sessionKey,
725
+ instanceSlot: binding.instanceSlot,
726
+ slot: binding.slot,
727
+ target: { ...binding.target },
644
728
  });
645
- const renamed = await rename({
646
- target: {
647
- chatId: record.target.chatId,
648
- threadId: record.target.threadId,
649
- },
650
- threadName,
651
- slot: record.slot,
729
+ const bindings = store
730
+ .listWorkspaceBindings()
731
+ .filter(
732
+ (binding) =>
733
+ binding.target.chatId === target.chatId &&
734
+ binding.target.threadId === target.threadId,
735
+ );
736
+ if (
737
+ recipientAuthority &&
738
+ (bindings.length !== 1 ||
739
+ !bindings[0]?.sessionId ||
740
+ bindings[0].slot !== slot)
741
+ )
742
+ throw new Error(
743
+ "Telegram Workspace Thread rename recipient binding authority is unavailable.",
744
+ );
745
+ const capturedBindings = bindings.map(bindingIdentity);
746
+ const assertAuthority = () => {
747
+ recipientAuthority?.();
748
+ if (
749
+ (getEpoch && getEpoch() !== leaderEpoch) ||
750
+ getProfile?.() !== profile ||
751
+ getUser() !== user ||
752
+ deps.topicTargetStore !== store ||
753
+ deps.instanceId !== instanceId
754
+ )
755
+ throw new Error(
756
+ "Telegram Workspace Thread rename lost leader ownership.",
757
+ );
758
+ const current = Threads.findCurrentTelegramInstanceThreadRecord({
759
+ records: store.list(),
760
+ instanceId,
761
+ });
762
+ const currentBindings = store
763
+ .listWorkspaceBindings()
764
+ .filter(
765
+ (binding) =>
766
+ binding.target.chatId === target.chatId &&
767
+ binding.target.threadId === target.threadId,
768
+ )
769
+ .map(bindingIdentity);
770
+ if (
771
+ current?.target.chatId !== target.chatId ||
772
+ current.target.threadId !== target.threadId ||
773
+ current.slot !== slot ||
774
+ current.status !== record.status ||
775
+ !isDeepStrictEqual(current.owner, owner) ||
776
+ !isDeepStrictEqual(currentBindings, capturedBindings)
777
+ )
778
+ throw new Error(
779
+ "Telegram Workspace Thread rename target changed or binding authority was lost.",
780
+ );
781
+ };
782
+ assertAuthority();
783
+ const current = () => {
784
+ try {
785
+ assertAuthority();
786
+ return true;
787
+ } catch {
788
+ return false;
789
+ }
790
+ };
791
+ const rename = Threads.createTelegramTopicTargetRenamer({
792
+ store: recipientAuthority
793
+ ? {
794
+ list: store.list.bind(store),
795
+ listWorkspaceBindings: store.listWorkspaceBindings.bind(store),
796
+ listPendingProvisions: store.listPendingProvisions.bind(store),
797
+ renameByTarget: (target, name, options) =>
798
+ publishName(
799
+ target,
800
+ name,
801
+ { updateDisplayTitle: options?.updateDisplayTitle ?? true },
802
+ current,
803
+ ),
804
+ }
805
+ : store,
806
+ callApi,
807
+ assertAuthority,
652
808
  });
809
+ const renamed = await rename({ target, threadName, slot });
810
+ assertAuthority();
653
811
  if (!renamed) {
654
812
  throw new Error(
655
813
  ThreadNaming.getTelegramTopicThreadNameValidationError(
656
814
  threadName,
657
- record.slot,
815
+ slot,
658
816
  ) ?? "Telegram Workspace Thread name is already reserved.",
659
817
  );
660
818
  }
661
- await deps.topicTargetStore.persist();
819
+ if (!recipientAuthority) await persist(current);
820
+ assertAuthority();
662
821
  return renamed;
663
822
  };
664
- const resetThreadNameAdmitted = async (
665
- target: Threads.TelegramTopicTargetRecord["target"],
666
- assertTargetCurrent: () => void = () => undefined,
667
- ): Promise<{ threadName: string }> => {
668
- const leaderEpoch = deps.getCurrentLeaderEpoch?.();
669
- if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
670
- throw new Error("Telegram Workspace Thread reset requires leader ownership.");
823
+ const prepareThreadNameReset = (
824
+ expectedTarget: Threads.TelegramTopicTargetRecord["target"],
825
+ assertTargetCurrent: () => void,
826
+ assertRecipientAuthority?: () => void,
827
+ ): (() => Promise<{ threadName: string }>) => {
828
+ const target = { ...expectedTarget },
829
+ store = deps.topicTargetStore,
830
+ callApi = deps.callApi;
831
+ const publishReset = store.clearManualNameByTargetAndPersist.bind(store);
832
+ const clearManualName = store.clearManualNameByTarget.bind(store),
833
+ persist = store.persist.bind(store);
834
+ const getEpoch = deps.getCurrentLeaderEpoch,
835
+ getProfile = deps.getTelegramProfile;
836
+ const getUser = deps.getAllowedUserId,
837
+ getMode = deps.getThreadDisplayMode;
838
+ const recipientAuthority = assertRecipientAuthority,
839
+ targetAuthority = assertTargetCurrent;
840
+ recipientAuthority?.();
841
+ const leaderEpoch = getEpoch?.(),
842
+ profile = getProfile?.(),
843
+ user = getUser(),
844
+ instanceId = deps.instanceId;
845
+ const mode = getMode?.() ?? "letters";
846
+ if (getEpoch && leaderEpoch === undefined) {
847
+ throw new Error(
848
+ "Telegram Workspace Thread reset requires leader ownership.",
849
+ );
671
850
  }
672
- const binding = deps.topicTargetStore.listWorkspaceBindings().find(
673
- (candidate) => candidate.target.chatId === target.chatId &&
674
- candidate.target.threadId === target.threadId,
675
- );
676
- if (!binding) throw new Error("No Workspace Thread binding is available to reset.");
677
- const automaticTitle = resolveTelegramInitialWorkspaceDisplayName({
678
- bindings: deps.topicTargetStore.listWorkspaceBindings(),
679
- binding: { ...binding, manualThreadName: undefined },
680
- mode: deps.getThreadDisplayMode?.() ?? "letters",
681
- preserveRetainedManualName: false,
851
+ const matchingBindings = () =>
852
+ store
853
+ .listWorkspaceBindings()
854
+ .filter(
855
+ (candidate) =>
856
+ candidate.target.chatId === target.chatId &&
857
+ candidate.target.threadId === target.threadId,
858
+ );
859
+ const bindings = matchingBindings(),
860
+ binding = bindings[0];
861
+ if (!binding)
862
+ throw new Error("No Workspace Thread binding is available to reset.");
863
+ const record = Threads.findCurrentTelegramInstanceThreadRecord({
864
+ records: store.list(),
865
+ instanceId,
682
866
  });
683
- if (!automaticTitle) {
684
- throw new Error("Telegram Workspace automatic title is unavailable.");
685
- }
686
- assertTargetCurrent();
687
- await deps.callApi("editForumTopic", {
688
- chat_id: target.chatId,
689
- message_thread_id: target.threadId,
690
- name: automaticTitle,
867
+ if (
868
+ recipientAuthority &&
869
+ (bindings.length !== 1 ||
870
+ !binding.sessionId ||
871
+ record?.status !== "active" ||
872
+ record.owner?.kind !== "leader" ||
873
+ record.owner.cwd !== binding.cwd ||
874
+ record.target.chatId !== target.chatId ||
875
+ record.target.threadId !== target.threadId ||
876
+ record.slot !== binding.slot)
877
+ )
878
+ throw new Error(
879
+ "Telegram Workspace Thread reset recipient binding authority is unavailable.",
880
+ );
881
+ const bindingIdentity = (
882
+ value: Threads.TelegramWorkspaceThreadBinding,
883
+ ) => ({
884
+ bindingKey: value.bindingKey,
885
+ cwd: value.cwd,
886
+ workspaceKey: value.workspaceKey,
887
+ sessionId: value.sessionId,
888
+ sessionKey: value.sessionKey,
889
+ instanceSlot: value.instanceSlot,
890
+ slot: value.slot,
891
+ target: { ...value.target },
691
892
  });
692
- if (deps.getCurrentLeaderEpoch &&
693
- deps.getCurrentLeaderEpoch() !== leaderEpoch) {
694
- throw new Error("Telegram Workspace Thread reset lost leader ownership.");
695
- }
696
- assertTargetCurrent();
697
- const reset = deps.topicTargetStore.clearManualNameByTarget(
698
- target,
699
- automaticTitle,
893
+ const capturedBinding = bindingIdentity(binding),
894
+ owner = record && structuredClone(record.owner);
895
+ const resolveTitle = (value: Threads.TelegramWorkspaceThreadBinding) =>
896
+ resolveTelegramInitialWorkspaceDisplayName({
897
+ bindings: store.listWorkspaceBindings(),
898
+ binding: { ...value, manualThreadName: undefined },
899
+ mode,
900
+ preserveRetainedManualName: false,
901
+ });
902
+ const automaticTitle = resolveTitle(binding);
903
+ if (!automaticTitle)
904
+ throw new Error("Telegram Workspace automatic title is unavailable.");
905
+ const assertAuthority = () => {
906
+ recipientAuthority?.();
907
+ if (getEpoch && getEpoch() !== leaderEpoch)
908
+ throw new Error(
909
+ "Telegram Workspace Thread reset lost leader ownership.",
910
+ );
911
+ targetAuthority();
912
+ if (!recipientAuthority) return;
913
+ if (
914
+ getProfile?.() !== profile ||
915
+ getUser() !== user ||
916
+ deps.topicTargetStore !== store ||
917
+ deps.instanceId !== instanceId
918
+ )
919
+ throw new Error(
920
+ "Telegram Workspace Thread reset lost recipient authority.",
921
+ );
922
+ const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
923
+ records: store.list(),
924
+ instanceId,
925
+ });
926
+ const currentBindings = matchingBindings();
927
+ if (
928
+ currentRecord?.target.chatId !== target.chatId ||
929
+ currentRecord.target.threadId !== target.threadId ||
930
+ currentRecord.slot !== record!.slot ||
931
+ currentRecord.status !== record!.status ||
932
+ !isDeepStrictEqual(currentRecord.owner, owner) ||
933
+ currentBindings.length !== 1 ||
934
+ !isDeepStrictEqual(
935
+ bindingIdentity(currentBindings[0]!),
936
+ capturedBinding,
937
+ )
938
+ )
939
+ throw new Error(
940
+ "Telegram Workspace Thread reset target changed or binding authority was lost.",
941
+ );
942
+ if (
943
+ (getMode?.() ?? "letters") !== mode ||
944
+ resolveTitle(currentBindings[0]!) !== automaticTitle
945
+ )
946
+ throw new Error("Telegram Workspace automatic-title policy changed.");
947
+ };
948
+ assertAuthority();
949
+ const current = () => {
950
+ try {
951
+ assertAuthority();
952
+ return true;
953
+ } catch {
954
+ return false;
955
+ }
956
+ };
957
+ return async () => {
958
+ assertAuthority();
959
+ const body = {
960
+ chat_id: target.chatId,
961
+ message_thread_id: target.threadId,
962
+ name: automaticTitle,
963
+ };
964
+ if (recipientAuthority)
965
+ await callApi("editForumTopic", body, { assertAuthority });
966
+ else await callApi("editForumTopic", body);
967
+ assertAuthority();
968
+ if (
969
+ recipientAuthority &&
970
+ (!isDeepStrictEqual(matchingBindings()[0], binding) ||
971
+ !isDeepStrictEqual(
972
+ Threads.findCurrentTelegramInstanceThreadRecord({
973
+ records: store.list(),
974
+ instanceId,
975
+ }),
976
+ record,
977
+ ))
978
+ )
979
+ throw new Error(
980
+ "Telegram Workspace Thread reset metadata changed before publication.",
981
+ );
982
+ const reset = recipientAuthority
983
+ ? await publishReset(target, automaticTitle, current)
984
+ : clearManualName(target, automaticTitle);
985
+ if (!reset)
986
+ throw new Error("Telegram Workspace Thread reset changed binding.");
987
+ if (!recipientAuthority) await persist();
988
+ assertAuthority();
989
+ if (
990
+ recipientAuthority &&
991
+ (reset.manualThreadName !== undefined ||
992
+ matchingBindings()[0]?.manualThreadName !== undefined ||
993
+ matchingBindings()[0]?.displayTitle !== automaticTitle ||
994
+ store.getActiveByInstanceId(instanceId)?.manualThreadName !==
995
+ undefined)
996
+ )
997
+ throw new Error(
998
+ "Telegram Workspace Thread reset result metadata changed.",
999
+ );
1000
+ return { threadName: automaticTitle };
1001
+ };
1002
+ };
1003
+ const resetThreadNameAdmitted = async (
1004
+ target: Threads.TelegramTopicTargetRecord["target"],
1005
+ assertTargetCurrent: () => void = () => undefined,
1006
+ ): Promise<{ threadName: string }> =>
1007
+ prepareThreadNameReset(target, assertTargetCurrent)();
1008
+ const renameLeaderThread = async (threadName: string) => {
1009
+ const getEpoch = deps.getCurrentLeaderEpoch,
1010
+ getProfile = deps.getTelegramProfile;
1011
+ const getUser = deps.getAllowedUserId,
1012
+ store = deps.topicTargetStore;
1013
+ const epoch = getEpoch?.(),
1014
+ profile = getProfile?.(),
1015
+ user = getUser();
1016
+ return runWorkspaceOperation(
1017
+ {
1018
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1019
+ operationKind: "workspace.rename-leader",
1020
+ scopes: [{ kind: "profile" }],
1021
+ },
1022
+ () => {
1023
+ if (
1024
+ getEpoch?.() !== epoch ||
1025
+ getProfile?.() !== profile ||
1026
+ getUser() !== user ||
1027
+ deps.topicTargetStore !== store
1028
+ )
1029
+ throw new Error(
1030
+ "Telegram Workspace Thread rename lost leader ownership during admission.",
1031
+ );
1032
+ return renameLeaderThreadAdmitted(threadName);
1033
+ },
700
1034
  );
701
- if (!reset) throw new Error("Telegram Workspace Thread reset changed binding.");
702
- await deps.topicTargetStore.persist();
703
- return { threadName: automaticTitle };
704
1035
  };
705
- const renameLeaderThread = (threadName: string) => runWorkspaceOperation({
706
- operationId: createTelegramWorkspaceAdmissionOperationId(),
707
- operationKind: "workspace.rename-leader",
708
- scopes: [{ kind: "profile" }],
709
- }, () => renameLeaderThreadAdmitted(threadName));
710
- const resetLeaderThreadName = (
1036
+ const resetLeaderThreadName = async (
711
1037
  expectedTarget: Threads.TelegramTopicTargetRecord["target"],
712
- ) => runWorkspaceOperation({
713
- operationId: createTelegramWorkspaceAdmissionOperationId(),
714
- operationKind: "workspace.reset-leader-name",
715
- scopes: [{ kind: "profile" }],
716
- }, () => {
717
- const record = Threads.findCurrentTelegramInstanceThreadRecord({
718
- records: deps.topicTargetStore.list(),
719
- instanceId: deps.instanceId,
720
- });
721
- if (!record || typeof record.target.threadId !== "number") {
722
- throw new Error("No Workspace Thread is bound to this Pi instance.");
723
- }
724
- if (record.target.chatId !== expectedTarget.chatId ||
725
- record.target.threadId !== expectedTarget.threadId) {
726
- throw new Error("Telegram Workspace Thread reset target changed.");
727
- }
728
- return resetThreadNameAdmitted(record.target, () => {
729
- const current = Threads.findCurrentTelegramInstanceThreadRecord({
730
- records: deps.topicTargetStore.list(),
731
- instanceId: deps.instanceId,
732
- });
733
- if (current?.target.chatId !== expectedTarget.chatId ||
734
- current.target.threadId !== expectedTarget.threadId) {
735
- throw new Error("Telegram Workspace Thread reset target changed.");
736
- }
737
- });
738
- });
1038
+ assertRecipientAuthority?: () => void,
1039
+ ): Promise<{ threadName: string }> => {
1040
+ const target = { ...expectedTarget },
1041
+ store = deps.topicTargetStore,
1042
+ instanceId = deps.instanceId;
1043
+ const assertTargetCurrent = () => {
1044
+ const record = Threads.findCurrentTelegramInstanceThreadRecord({
1045
+ records: store.list(),
1046
+ instanceId,
1047
+ });
1048
+ if (!record || typeof record.target.threadId !== "number")
1049
+ throw new Error("No Workspace Thread is bound to this Pi instance.");
1050
+ if (
1051
+ record.target.chatId !== target.chatId ||
1052
+ record.target.threadId !== target.threadId
1053
+ )
1054
+ throw new Error("Telegram Workspace Thread reset target changed.");
1055
+ };
1056
+ // Selected lifetimes are captured before admission; ordinary reset keeps its post-admission preparation.
1057
+ const prepared = assertRecipientAuthority
1058
+ ? prepareThreadNameReset(
1059
+ target,
1060
+ assertTargetCurrent,
1061
+ assertRecipientAuthority,
1062
+ )
1063
+ : undefined;
1064
+ return runWorkspaceOperation(
1065
+ {
1066
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1067
+ operationKind: "workspace.reset-leader-name",
1068
+ scopes: [{ kind: "profile" }],
1069
+ },
1070
+ () => {
1071
+ assertTargetCurrent();
1072
+ return prepared
1073
+ ? prepared()
1074
+ : resetThreadNameAdmitted(target, assertTargetCurrent);
1075
+ },
1076
+ );
1077
+ };
739
1078
  const followerSessionReplacement =
740
1079
  createTelegramBusFollowerSessionReplacementAuthority({
741
1080
  store: deps.topicTargetStore,
@@ -749,25 +1088,31 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
749
1088
  onFollowerRegistered: scheduleDisplay,
750
1089
  provisionLeaderTarget: (ctx) => {
751
1090
  const chatId = deps.getAllowedUserId();
752
- const provision = () => runWorkspaceOperation(
753
- {
754
- operationId: `leader-provision:${deps.instanceId}`,
755
- operationKind: "workspace.provision-leader",
756
- scopes: [
757
- typeof chatId === "number"
758
- ? { kind: "chat", chatId }
759
- : { kind: "profile" },
760
- ],
761
- },
762
- () => provisionLeaderTarget(ctx),
763
- );
764
- return runWithWorkspaceCapacity ? runWithWorkspaceCapacity(provision) : provision();
1091
+ const provision = () =>
1092
+ runWorkspaceOperation(
1093
+ {
1094
+ operationId: `leader-provision:${deps.instanceId}`,
1095
+ operationKind: "workspace.provision-leader",
1096
+ scopes: [
1097
+ typeof chatId === "number"
1098
+ ? { kind: "chat", chatId }
1099
+ : { kind: "profile" },
1100
+ ],
1101
+ },
1102
+ () => provisionLeaderTarget(ctx),
1103
+ );
1104
+ return runWithWorkspaceCapacity
1105
+ ? runWithWorkspaceCapacity(provision)
1106
+ : provision();
765
1107
  },
766
1108
  getFollowerDisplayTitle(follower) {
767
- const binding = deps.topicTargetStore.listWorkspaceBindings().find((binding) =>
768
- binding.target.chatId === follower.target?.chatId &&
769
- binding.target.threadId === follower.target?.threadId,
770
- );
1109
+ const binding = deps.topicTargetStore
1110
+ .listWorkspaceBindings()
1111
+ .find(
1112
+ (binding) =>
1113
+ binding.target.chatId === follower.target?.chatId &&
1114
+ binding.target.threadId === follower.target?.threadId,
1115
+ );
771
1116
  return binding?.displayTitle ?? binding?.threadName;
772
1117
  },
773
1118
  onFollowerDisconnected: async (follower) => {
@@ -782,24 +1127,57 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
782
1127
  scheduleDisplay();
783
1128
  },
784
1129
  async renameFollowerThread(follower, threadName) {
785
- return runWorkspaceOperation({
786
- operationId: createTelegramWorkspaceAdmissionOperationId(),
787
- operationKind: "workspace.rename-follower",
788
- scopes: [{ kind: "profile" }],
789
- }, async () => {
790
- const leaderEpoch = deps.getCurrentLeaderEpoch?.();
791
- if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
792
- throw new Error(
793
- "Telegram Workspace Thread rename requires leader ownership.",
794
- );
795
- }
796
- if (typeof follower.target?.threadId !== "number") {
797
- throw new Error("Telegram follower has no bound Workspace Thread.");
798
- }
799
- const rename = Threads.createTelegramTopicTargetRenamer({
800
- store: deps.topicTargetStore,
801
- callApi: deps.callApi,
802
- assertAuthority() {
1130
+ return runWorkspaceOperation(
1131
+ {
1132
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1133
+ operationKind: "workspace.rename-follower",
1134
+ scopes: [{ kind: "profile" }],
1135
+ },
1136
+ async () => {
1137
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
1138
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
1139
+ throw new Error(
1140
+ "Telegram Workspace Thread rename requires leader ownership.",
1141
+ );
1142
+ }
1143
+ if (typeof follower.target?.threadId !== "number") {
1144
+ throw new Error("Telegram follower has no bound Workspace Thread.");
1145
+ }
1146
+ const rename = Threads.createTelegramTopicTargetRenamer({
1147
+ store: deps.topicTargetStore,
1148
+ callApi: deps.callApi,
1149
+ assertAuthority() {
1150
+ if (
1151
+ deps.getCurrentLeaderEpoch &&
1152
+ deps.getCurrentLeaderEpoch() !== leaderEpoch
1153
+ ) {
1154
+ throw new Error(
1155
+ "Telegram Workspace Thread rename lost leader ownership.",
1156
+ );
1157
+ }
1158
+ const current = deps.runtime.followerRegistry.get(
1159
+ follower.instanceId,
1160
+ );
1161
+ if (
1162
+ current?.registrationGeneration !==
1163
+ follower.registrationGeneration ||
1164
+ current?.target?.chatId !== follower.target?.chatId ||
1165
+ current?.target?.threadId !== follower.target?.threadId
1166
+ ) {
1167
+ throw new Error(
1168
+ "Telegram follower Workspace Thread target changed.",
1169
+ );
1170
+ }
1171
+ },
1172
+ });
1173
+ const renamed = await rename({
1174
+ target: {
1175
+ chatId: follower.target.chatId,
1176
+ threadId: follower.target.threadId,
1177
+ },
1178
+ threadName,
1179
+ slot: follower.slot,
1180
+ });
803
1181
  if (
804
1182
  deps.getCurrentLeaderEpoch &&
805
1183
  deps.getCurrentLeaderEpoch() !== leaderEpoch
@@ -808,78 +1186,72 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
808
1186
  "Telegram Workspace Thread rename lost leader ownership.",
809
1187
  );
810
1188
  }
811
- const current = deps.runtime.followerRegistry.get(follower.instanceId);
812
- if (current?.registrationGeneration !== follower.registrationGeneration ||
813
- current?.target?.chatId !== follower.target?.chatId ||
814
- current?.target?.threadId !== follower.target?.threadId) {
815
- throw new Error("Telegram follower Workspace Thread target changed.");
1189
+ if (!renamed) {
1190
+ throw new Error(
1191
+ ThreadNaming.getTelegramTopicThreadNameValidationError(
1192
+ threadName,
1193
+ follower.slot,
1194
+ ) ?? "Telegram Workspace Thread name is already reserved.",
1195
+ );
816
1196
  }
1197
+ await deps.topicTargetStore.persist();
1198
+ return { threadName: renamed.manualThreadName ?? threadName };
817
1199
  },
818
- });
819
- const renamed = await rename({
820
- target: {
821
- chatId: follower.target.chatId,
822
- threadId: follower.target.threadId,
823
- },
824
- threadName,
825
- slot: follower.slot,
826
- });
827
- if (
828
- deps.getCurrentLeaderEpoch &&
829
- deps.getCurrentLeaderEpoch() !== leaderEpoch
830
- ) {
831
- throw new Error(
832
- "Telegram Workspace Thread rename lost leader ownership.",
833
- );
834
- }
835
- if (!renamed) {
836
- throw new Error(
837
- ThreadNaming.getTelegramTopicThreadNameValidationError(
838
- threadName,
839
- follower.slot,
840
- ) ?? "Telegram Workspace Thread name is already reserved.",
841
- );
842
- }
843
- await deps.topicTargetStore.persist();
844
- return { threadName: renamed.manualThreadName ?? threadName };
845
- });
1200
+ );
846
1201
  },
847
- resetFollowerThreadName: (follower) => runWorkspaceOperation(
848
- {
849
- operationId: createTelegramWorkspaceAdmissionOperationId(),
850
- operationKind: "workspace.reset-follower-name",
851
- scopes: [{ kind: "profile" }],
852
- },
853
- () => {
854
- if (!follower.target || typeof follower.target.threadId !== "number") {
855
- throw new Error("Telegram follower has no bound Workspace Thread.");
856
- }
857
- const target = {
858
- chatId: follower.target.chatId,
859
- threadId: follower.target.threadId,
860
- };
861
- return resetThreadNameAdmitted(target, () => {
862
- const current = deps.runtime.followerRegistry.get(follower.instanceId);
863
- if (current?.registrationGeneration !== follower.registrationGeneration ||
864
- current?.target?.chatId !== target.chatId ||
865
- current?.target?.threadId !== target.threadId) {
866
- throw new Error("Telegram follower Workspace Thread target changed.");
1202
+ resetFollowerThreadName: (follower) =>
1203
+ runWorkspaceOperation(
1204
+ {
1205
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1206
+ operationKind: "workspace.reset-follower-name",
1207
+ scopes: [{ kind: "profile" }],
1208
+ },
1209
+ () => {
1210
+ if (
1211
+ !follower.target ||
1212
+ typeof follower.target.threadId !== "number"
1213
+ ) {
1214
+ throw new Error("Telegram follower has no bound Workspace Thread.");
867
1215
  }
868
- });
869
- },
870
- ),
1216
+ const target = {
1217
+ chatId: follower.target.chatId,
1218
+ threadId: follower.target.threadId,
1219
+ };
1220
+ return resetThreadNameAdmitted(target, () => {
1221
+ const current = deps.runtime.followerRegistry.get(
1222
+ follower.instanceId,
1223
+ );
1224
+ if (
1225
+ current?.registrationGeneration !==
1226
+ follower.registrationGeneration ||
1227
+ current?.target?.chatId !== target.chatId ||
1228
+ current?.target?.threadId !== target.threadId
1229
+ ) {
1230
+ throw new Error(
1231
+ "Telegram follower Workspace Thread target changed.",
1232
+ );
1233
+ }
1234
+ });
1235
+ },
1236
+ ),
871
1237
  publishFollowerSessionReplacement: (follower, intent, isCurrent) =>
872
- runWorkspaceOperation({
873
- operationId: createTelegramWorkspaceAdmissionOperationId(),
874
- operationKind: "workspace.publish-follower-session-replacement",
875
- scopes: [{ kind: "profile" }],
876
- }, () => followerSessionReplacement.publish(follower, intent, isCurrent)),
1238
+ runWorkspaceOperation(
1239
+ {
1240
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1241
+ operationKind: "workspace.publish-follower-session-replacement",
1242
+ scopes: [{ kind: "profile" }],
1243
+ },
1244
+ () => followerSessionReplacement.publish(follower, intent, isCurrent),
1245
+ ),
877
1246
  settleFollowerSessionReplacement: (follower, intent, isCurrent) =>
878
- runWorkspaceOperation({
879
- operationId: createTelegramWorkspaceAdmissionOperationId(),
880
- operationKind: "workspace.settle-follower-session-replacement",
881
- scopes: [{ kind: "profile" }],
882
- }, () => followerSessionReplacement.settle(follower, intent, isCurrent)),
1247
+ runWorkspaceOperation(
1248
+ {
1249
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1250
+ operationKind: "workspace.settle-follower-session-replacement",
1251
+ scopes: [{ kind: "profile" }],
1252
+ },
1253
+ () => followerSessionReplacement.settle(follower, intent, isCurrent),
1254
+ ),
883
1255
  onFollowerConfirmedDead: async (follower) => {
884
1256
  await runWorkspaceOperation(
885
1257
  {
@@ -894,54 +1266,106 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
894
1266
  async onFollowerConfirmedDeadPreserved(follower, isDetached, operationId) {
895
1267
  const epoch = deps.getCurrentLeaderEpoch?.();
896
1268
  const profile = deps.getTelegramProfile?.();
897
- if (epoch === undefined || !follower.registrationGeneration ||
898
- typeof follower.target?.threadId !== "number") return false;
899
- const isCurrent = () => isDetached() && deps.getCurrentLeaderEpoch?.() === epoch &&
1269
+ if (
1270
+ epoch === undefined ||
1271
+ !follower.registrationGeneration ||
1272
+ typeof follower.target?.threadId !== "number"
1273
+ )
1274
+ return false;
1275
+ const isCurrent = () =>
1276
+ isDetached() &&
1277
+ deps.getCurrentLeaderEpoch?.() === epoch &&
900
1278
  deps.getTelegramProfile?.() === profile;
901
1279
  if (!isCurrent()) return false;
902
- const settled = await runWorkspaceOperation({
903
- operationId,
904
- operationKind: "workspace.preserve-dead-follower",
905
- scopes: [{ kind: "profile" }],
906
- }, async () => {
907
- if (!isCurrent()) return false;
908
- await deps.topicTargetStore.load();
909
- if (!isCurrent()) return false;
910
- const record = deps.topicTargetStore.getActiveByInstanceId(follower.instanceId);
911
- if (!record || (follower.profileKey && record.profileKey !== follower.profileKey) ||
1280
+ const settled = await runWorkspaceOperation(
1281
+ {
1282
+ operationId,
1283
+ operationKind: "workspace.preserve-dead-follower",
1284
+ scopes: [{ kind: "profile" }],
1285
+ },
1286
+ async () => {
1287
+ if (!isCurrent()) return false;
1288
+ await deps.topicTargetStore.load();
1289
+ if (!isCurrent()) return false;
1290
+ const record = deps.topicTargetStore.getActiveByInstanceId(
1291
+ follower.instanceId,
1292
+ );
1293
+ if (
1294
+ !record ||
1295
+ (follower.profileKey &&
1296
+ record.profileKey !== follower.profileKey) ||
912
1297
  record.target.chatId !== follower.target!.chatId ||
913
- record.target.threadId !== follower.target!.threadId) return true;
914
- const committed = await deps.topicTargetStore.detachTargetOwner(record, isCurrent);
915
- if (!committed && isCurrent()) {
916
- throw new Error("Telegram preserved follower detachment was not committed.");
917
- }
918
- return committed;
919
- });
1298
+ record.target.threadId !== follower.target!.threadId
1299
+ )
1300
+ return true;
1301
+ const committed = await deps.topicTargetStore.detachTargetOwner(
1302
+ record,
1303
+ isCurrent,
1304
+ );
1305
+ if (!committed && isCurrent()) {
1306
+ throw new Error(
1307
+ "Telegram preserved follower detachment was not committed.",
1308
+ );
1309
+ }
1310
+ return committed;
1311
+ },
1312
+ );
920
1313
  if (settled && isCurrent()) scheduleDisplay();
921
1314
  return settled;
922
1315
  },
923
- provisionFollowerTarget: (registration, options) => runWorkspaceOperation(
924
- {
925
- operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
926
- operationKind: "workspace.provision-follower",
927
- scopes: [{ kind: "profile" }],
928
- },
929
- () => provisionFollowerTarget(registration, options),
930
- ),
931
- commitFollowerRegistration: (input, publish) => runWorkspaceOperation({
932
- operationId: createTelegramWorkspaceAdmissionOperationId(),
933
- operationKind: "workspace.publish-follower-registration", scopes: [{ kind: "profile" }],
934
- }, async () => {
935
- if (!input.target) { publish(); return; }
936
- await deps.topicTargetStore.load();
937
- const binding = input.registration.cwd && input.registration.sessionId
938
- ? deps.topicTargetStore.getWorkspaceBindingByTarget(input.target, input.registration.sessionId) : undefined;
939
- const bindingKey = binding && input.registration.cwd && binding.cwd === WorkspaceIdentity.normalizeTelegramWorkspacePath(input.registration.cwd)
940
- ? binding.bindingKey : undefined;
941
- if (input.target.threadId !== undefined && input.registration.cwd && input.registration.sessionId && !bindingKey)
942
- throw new Error("Telegram follower Workspace binding changed before registration publication.");
943
- deps.topicTargetStore.commitWorkspaceRestoreRegistration({ target: input.target, slot: input.slot, bindingKey }, publish);
944
- }),
1316
+ provisionFollowerTarget: (registration, options) =>
1317
+ runWorkspaceOperation(
1318
+ {
1319
+ operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
1320
+ operationKind: "workspace.provision-follower",
1321
+ scopes: [{ kind: "profile" }],
1322
+ },
1323
+ () => provisionFollowerTarget(registration, options),
1324
+ ),
1325
+ commitFollowerRegistration: (input, publish) =>
1326
+ runWorkspaceOperation(
1327
+ {
1328
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
1329
+ operationKind: "workspace.publish-follower-registration",
1330
+ scopes: [{ kind: "profile" }],
1331
+ },
1332
+ async () => {
1333
+ if (!input.target) {
1334
+ publish();
1335
+ return;
1336
+ }
1337
+ await deps.topicTargetStore.load();
1338
+ const binding =
1339
+ input.registration.cwd && input.registration.sessionId
1340
+ ? deps.topicTargetStore.getWorkspaceBindingByTarget(
1341
+ input.target,
1342
+ input.registration.sessionId,
1343
+ )
1344
+ : undefined;
1345
+ const bindingKey =
1346
+ binding &&
1347
+ input.registration.cwd &&
1348
+ binding.cwd ===
1349
+ WorkspaceIdentity.normalizeTelegramWorkspacePath(
1350
+ input.registration.cwd,
1351
+ )
1352
+ ? binding.bindingKey
1353
+ : undefined;
1354
+ if (
1355
+ input.target.threadId !== undefined &&
1356
+ input.registration.cwd &&
1357
+ input.registration.sessionId &&
1358
+ !bindingKey
1359
+ )
1360
+ throw new Error(
1361
+ "Telegram follower Workspace binding changed before registration publication.",
1362
+ );
1363
+ deps.topicTargetStore.commitWorkspaceRestoreRegistration(
1364
+ { target: input.target, slot: input.slot, bindingKey },
1365
+ publish,
1366
+ );
1367
+ },
1368
+ ),
945
1369
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
946
1370
  getTelegramProfile: deps.getTelegramProfile,
947
1371
  getAllowedUserId: deps.getAllowedUserId,
@@ -997,86 +1421,30 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
997
1421
  record: TelegramBusFollowerMessageOwnershipRecord,
998
1422
  ) => void;
999
1423
 
1000
- export type TelegramBusFollowerRegistrationCommitter = (input: {
1001
- registration: TelegramBusInstanceRegistration;
1002
- target?: TelegramTarget;
1003
- slot?: string;
1004
- }, publish: () => void) => Promise<void> | void;
1424
+ export type TelegramBusFollowerRegistrationCommitter = (
1425
+ input: {
1426
+ registration: TelegramBusInstanceRegistration;
1427
+ target?: TelegramTarget;
1428
+ slot?: string;
1429
+ },
1430
+ publish: () => void,
1431
+ ) => Promise<void> | void;
1005
1432
 
1006
- export interface TelegramBusLeaderRuntimeDeps<TContext> {
1433
+ export interface TelegramBusLeaderRuntimeDeps<TContext> extends Omit<
1434
+ TelegramBusLeaderEnvelopeHandlerDeps,
1435
+ "runFollowerMutation" | "getWorkspaceRestoreObservationGeneration"
1436
+ > {
1007
1437
  socketPath: TelegramBusSocketPathSource;
1008
1438
  commitEndpointPublication?: (commit: () => void) => boolean;
1009
- followerRegistry: TelegramBusFollowerRegistry;
1010
- authSecret?: string;
1011
- protocolIdentity: TelegramBusProtocolIdentity;
1012
1439
  startPolling: (ctx: TContext) => void | Promise<void>;
1013
1440
  stopPolling: () => void | Promise<void>;
1014
- callApi?: (method: string, args: unknown[]) => Promise<unknown> | unknown;
1015
- authorizeFollowerApiCall?: (input: {
1016
- follower: TelegramBusFollowerView;
1017
- method: string;
1018
- args: unknown[];
1019
- }) => boolean;
1020
- recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
1021
- resolveAgentTarget?: (
1022
- follower: TelegramBusFollowerView,
1023
- selector: Extract<
1024
- TelegramBusEnvelope,
1025
- { kind: "follower.resolveAgentTarget" }
1026
- >["selector"],
1027
- ) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
1028
- routeAgentMessage?: (
1029
- follower: TelegramBusFollowerView,
1030
- message: Extract<
1031
- TelegramBusEnvelope,
1032
- { kind: "follower.routeAgentMessage" }
1033
- >["message"],
1034
- ) => Promise<void> | void;
1035
- routeQueueHandoff?: (
1036
- follower: TelegramBusFollowerView,
1037
- envelope: Extract<
1038
- TelegramBusEnvelope,
1039
- { kind: "follower.offerQueueHandoff" }
1040
- >,
1041
- ) => Promise<unknown> | unknown;
1042
- provisionFollowerTarget?: (
1043
- registration: TelegramBusInstanceRegistration,
1044
- options?: { existingWorkspaceBindingOnly?: boolean },
1045
- ) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
1046
- commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
1047
- renameFollowerThread?: (
1048
- follower: TelegramBusFollowerView,
1049
- threadName: string,
1050
- ) => Promise<{ threadName: string }> | { threadName: string };
1051
- resetFollowerThreadName?: (
1052
- follower: TelegramBusFollowerView,
1053
- ) => Promise<{ threadName: string }> | { threadName: string };
1054
- publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
1055
- settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
1056
- getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
1057
- onFollowerRegistered?: () => void;
1058
- /** Observation only: reacquire admission and inspect work, never infer completion.
1059
- * Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
1060
- onWorkspaceRestoreRecipientObserved?: (follower: TelegramBusFollowerView, isCurrent: () => boolean) => Promise<void> | void;
1061
- applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
1062
- getThreadDisplayMode?: () => TelegramThreadDisplayMode;
1063
- getCurrentLeaderEpoch?: () => number | string | undefined;
1064
- getTelegramProfile?: () => string | undefined;
1065
- getAllowedUserId?: () => number | undefined;
1066
1441
  provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
1067
- runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
1068
- runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
1069
- getNowMs?: () => number;
1070
- timeoutMs?: number;
1071
1442
  followerPruneIntervalMs?: number;
1072
1443
  /** Leader housekeeping after each current prune pass; throttling is the callee's concern. */
1073
1444
  afterFollowerPrune?: () => void;
1074
1445
  followerStaleAfterMs?: number;
1075
1446
  isFollowerProcessAlive?: (pid: number) => boolean;
1076
1447
  shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
1077
- onFollowerDisconnected?: (
1078
- follower: TelegramBusFollowerView,
1079
- ) => Promise<void> | void;
1080
1448
  onFollowerConfirmedDead?: (
1081
1449
  follower: TelegramBusFollowerView,
1082
1450
  ) => Promise<void> | void;
@@ -1086,11 +1454,6 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
1086
1454
  isDetached: () => boolean,
1087
1455
  operationId: string,
1088
1456
  ) => Promise<boolean> | boolean;
1089
- recordRuntimeEvent?: (
1090
- category: string,
1091
- error: unknown,
1092
- details?: Record<string, unknown>,
1093
- ) => void;
1094
1457
  }
1095
1458
 
1096
1459
  export function createTelegramBusInstanceLifecycleAnnouncement(input: {
@@ -1179,8 +1542,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1179
1542
  registration.instanceId,
1180
1543
  registration.previousInstanceId,
1181
1544
  {
1182
- existingBindingOnly:
1183
- options?.existingWorkspaceBindingOnly === true,
1545
+ existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
1184
1546
  sessionId: registration.sessionId,
1185
1547
  onCapacityUnavailable() {
1186
1548
  capacityUnavailable = true;
@@ -1240,7 +1602,8 @@ export function createTelegramBusFollowerTargetProvisioner(
1240
1602
  return (
1241
1603
  record.owner?.kind === "manual-follower" &&
1242
1604
  ((matchesCurrentIdentity && matchesRequestedTarget) ||
1243
- matchesSessionHandoff || matchesWorkspaceRecovery)
1605
+ matchesSessionHandoff ||
1606
+ matchesWorkspaceRecovery)
1244
1607
  );
1245
1608
  });
1246
1609
  const followerOwner =
@@ -1281,7 +1644,10 @@ export function createTelegramBusFollowerTargetProvisioner(
1281
1644
  },
1282
1645
  profileKey: followerProfileKey,
1283
1646
  threadName: workspaceBinding?.threadName ?? registration.threadName,
1284
- preferredSlot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
1647
+ preferredSlot:
1648
+ workspaceIdentity?.slot ??
1649
+ workspaceBinding?.slot ??
1650
+ registration.slot,
1285
1651
  ...(workspaceIdentity
1286
1652
  ? {
1287
1653
  workspaceBindingKey: workspaceIdentity.bindingKey,
@@ -1312,7 +1678,10 @@ export function createTelegramBusFollowerTargetProvisioner(
1312
1678
  ? recoveryHint.threadName
1313
1679
  : undefined;
1314
1680
  const requestedSlot =
1315
- workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot ?? recoveryHint?.slot;
1681
+ workspaceIdentity?.slot ??
1682
+ workspaceBinding?.slot ??
1683
+ registration.slot ??
1684
+ recoveryHint?.slot;
1316
1685
  const recoveredSlot = deps.topicTargetStore.allocateSlot(
1317
1686
  followerProfileKey,
1318
1687
  requestedSlot,
@@ -1352,19 +1721,33 @@ export function createTelegramBusFollowerTargetProvisioner(
1352
1721
  const runRegistration = async (): Promise<
1353
1722
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
1354
1723
  > => {
1355
- if (requestedTarget) deps.topicTargetStore.assertWorkspaceRestoreRegistration({
1356
- target: requestedTarget, bindingKey: workspaceIdentity?.bindingKey,
1357
- slot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
1358
- });
1724
+ if (requestedTarget)
1725
+ deps.topicTargetStore.assertWorkspaceRestoreRegistration({
1726
+ target: requestedTarget,
1727
+ bindingKey: workspaceIdentity?.bindingKey,
1728
+ slot:
1729
+ workspaceIdentity?.slot ??
1730
+ workspaceBinding?.slot ??
1731
+ registration.slot,
1732
+ });
1359
1733
  const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
1360
1734
  if (recoveryTarget) {
1361
- Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
1735
+ Threads.assertTelegramPendingTopicRecoveryAllowed(
1736
+ deps.topicTargetStore,
1737
+ recoveryTarget,
1738
+ );
1362
1739
  }
1363
- const pendingTargetRecovery = recoverableTarget && deps.topicTargetStore.listPendingProvisions().some(
1364
- (entry) => entry.target?.chatId === recoverableTarget.chatId &&
1365
- entry.target?.threadId === recoverableTarget.threadId &&
1366
- (entry.instanceId === registration.instanceId || entry.profileKey === followerProfileKey),
1367
- );
1740
+ const pendingTargetRecovery =
1741
+ recoverableTarget &&
1742
+ deps.topicTargetStore
1743
+ .listPendingProvisions()
1744
+ .some(
1745
+ (entry) =>
1746
+ entry.target?.chatId === recoverableTarget.chatId &&
1747
+ entry.target?.threadId === recoverableTarget.threadId &&
1748
+ (entry.instanceId === registration.instanceId ||
1749
+ entry.profileKey === followerProfileKey),
1750
+ );
1368
1751
  let result: Threads.TelegramTopicTargetProvisionResult = reconnectRecord
1369
1752
  ? {
1370
1753
  target: reconnectRecord.target,
@@ -1375,10 +1758,8 @@ export function createTelegramBusFollowerTargetProvisioner(
1375
1758
  ? await recoverRequestedTarget()
1376
1759
  : await provisionTarget();
1377
1760
  const alignResultWithWorkspaceSlot = (): void => {
1378
- if (
1379
- !workspaceIdentity ||
1380
- result.record.slot === workspaceIdentity.slot
1381
- ) return;
1761
+ if (!workspaceIdentity || result.record.slot === workspaceIdentity.slot)
1762
+ return;
1382
1763
  deps.recordRuntimeEvent(
1383
1764
  "bus",
1384
1765
  "Telegram follower record slot reconciled to its Workspace claim",
@@ -1397,9 +1778,12 @@ export function createTelegramBusFollowerTargetProvisioner(
1397
1778
  };
1398
1779
  };
1399
1780
  alignResultWithWorkspaceSlot();
1400
- const assertRestoreRegistration = (): void => deps.topicTargetStore.assertWorkspaceRestoreRegistration({
1401
- target: result.target, slot: result.record.slot, bindingKey: workspaceIdentity?.bindingKey,
1402
- });
1781
+ const assertRestoreRegistration = (): void =>
1782
+ deps.topicTargetStore.assertWorkspaceRestoreRegistration({
1783
+ target: result.target,
1784
+ slot: result.record.slot,
1785
+ bindingKey: workspaceIdentity?.bindingKey,
1786
+ });
1403
1787
  assertRestoreRegistration();
1404
1788
  const crossSessionReuse =
1405
1789
  !!reconnectRecord &&
@@ -1420,8 +1804,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1420
1804
  record: refreshedRecord,
1421
1805
  };
1422
1806
  }
1423
- const probeRequiredRecord =
1424
- reconnectRecord?.status === "probe-required";
1807
+ const probeRequiredRecord = reconnectRecord?.status === "probe-required";
1425
1808
  const exactSessionHandoff =
1426
1809
  crossSessionReuse &&
1427
1810
  !!requestedTarget &&
@@ -1436,10 +1819,13 @@ export function createTelegramBusFollowerTargetProvisioner(
1436
1819
  !result.reused || requiresVisibilityProbe
1437
1820
  ? createTelegramBusInstanceLifecycleAnnouncement({
1438
1821
  target: result.target,
1439
- threadName: result.displayTitle ??
1822
+ threadName:
1823
+ result.displayTitle ??
1440
1824
  (workspaceBinding?.target.chatId === result.target.chatId &&
1441
- workspaceBinding.target.threadId === result.target.threadId
1442
- ? workspaceBinding.displayTitle : undefined) ?? result.record.threadName,
1825
+ workspaceBinding.target.threadId === result.target.threadId
1826
+ ? workspaceBinding.displayTitle
1827
+ : undefined) ??
1828
+ result.record.threadName,
1443
1829
  slot: result.record.slot,
1444
1830
  state: "connected",
1445
1831
  })
@@ -1557,8 +1943,8 @@ export function createTelegramBusFollowerTargetProvisioner(
1557
1943
  }
1558
1944
  if (workspaceIdentity) {
1559
1945
  alignResultWithWorkspaceSlot();
1560
- const workspaceCommit =
1561
- Threads.commitTelegramWorkspaceProvisionBinding({
1946
+ const workspaceCommit = Threads.commitTelegramWorkspaceProvisionBinding(
1947
+ {
1562
1948
  store: deps.topicTargetStore,
1563
1949
  instanceId: registration.instanceId,
1564
1950
  profileKey: followerProfileKey,
@@ -1571,18 +1957,29 @@ export function createTelegramBusFollowerTargetProvisioner(
1571
1957
  : {}),
1572
1958
  slot: workspaceIdentity.slot,
1573
1959
  ...(workspaceIdentity.sessionId
1574
- ? { journalSources: [{ sessionId: workspaceIdentity.sessionId, recipientBindingKey: followerProfileKey }],
1575
- journalBindingKeys: [] }
1960
+ ? {
1961
+ journalSources: [
1962
+ {
1963
+ sessionId: workspaceIdentity.sessionId,
1964
+ recipientBindingKey: followerProfileKey,
1965
+ },
1966
+ ],
1967
+ journalBindingKeys: [],
1968
+ }
1576
1969
  : { journalBindingKeys: [followerProfileKey] }),
1577
1970
  journalBindingsComplete: true,
1578
1971
  updatedAtMs: getNowMs(),
1579
1972
  },
1580
- });
1973
+ },
1974
+ );
1581
1975
  if (connectedAnnouncement && workspaceCommit.displayTitle) {
1582
- connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
1583
- target: result.target, threadName: workspaceCommit.displayTitle,
1584
- slot: result.record.slot, state: "connected",
1585
- });
1976
+ connectedAnnouncement =
1977
+ createTelegramBusInstanceLifecycleAnnouncement({
1978
+ target: result.target,
1979
+ threadName: workspaceCommit.displayTitle,
1980
+ slot: result.record.slot,
1981
+ state: "connected",
1982
+ });
1586
1983
  }
1587
1984
  deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
1588
1985
  await deps.topicTargetStore.persist();
@@ -1605,95 +2002,102 @@ export function createTelegramBusFollowerTargetProvisioner(
1605
2002
  target: result.target,
1606
2003
  reused: result.reused,
1607
2004
  });
1608
- scheduleTelegramBusLeaderBackgroundTask(async () => {
1609
- const backgroundStartedAtMs = Date.now();
1610
- if (connectedAnnouncement) {
1611
- try {
1612
- await deps.callApi("sendMessage", {
1613
- chat_id: connectedAnnouncement.target.chatId,
1614
- message_thread_id: connectedAnnouncement.target.threadId,
1615
- text: connectedAnnouncement.text,
1616
- parse_mode: connectedAnnouncement.parseMode,
1617
- });
1618
- } catch (error) {
2005
+ scheduleTelegramBusLeaderBackgroundTask(
2006
+ async () => {
2007
+ const backgroundStartedAtMs = Date.now();
2008
+ if (connectedAnnouncement) {
2009
+ try {
2010
+ await deps.callApi("sendMessage", {
2011
+ chat_id: connectedAnnouncement.target.chatId,
2012
+ message_thread_id: connectedAnnouncement.target.threadId,
2013
+ text: connectedAnnouncement.text,
2014
+ parse_mode: connectedAnnouncement.parseMode,
2015
+ });
2016
+ } catch (error) {
2017
+ deps.recordRuntimeEvent("telegram", error, {
2018
+ phase: "follower-topic-announce",
2019
+ instanceId: registration.instanceId,
2020
+ chatId: result.target.chatId,
2021
+ threadId: result.target.threadId,
2022
+ });
2023
+ }
2024
+ }
2025
+ const reconcile = async (): Promise<void> => {
2026
+ await ThreadReconciler.applyThreadReconciliationPlan(
2027
+ ThreadReconciler.planThreadReconciliation({
2028
+ nowMs: getNowMs(),
2029
+ currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
2030
+ records: recordsBeforeProvision,
2031
+ pendingProvisions:
2032
+ deps.topicTargetStore.listPendingProvisions(),
2033
+ replacedBindings: [
2034
+ {
2035
+ instanceId: registration.instanceId,
2036
+ replacementTarget: result.target,
2037
+ },
2038
+ ],
2039
+ }),
2040
+ {
2041
+ isCleanupTargetProtected:
2042
+ Threads.createTelegramCleanupTargetProtection(
2043
+ deps.topicTargetStore,
2044
+ ),
2045
+ callApi: deps.callApi,
2046
+ markStaleByTarget(target, syncStatus, lastSyncError) {
2047
+ return deps.topicTargetStore.markStaleByTarget(
2048
+ target,
2049
+ syncStatus,
2050
+ lastSyncError,
2051
+ );
2052
+ },
2053
+ persist() {
2054
+ return deps.topicTargetStore.persist();
2055
+ },
2056
+ removePendingProvisionById(id) {
2057
+ return deps.topicTargetStore.removePendingProvision(id);
2058
+ },
2059
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
2060
+ recordRuntimeEvent: deps.recordRuntimeEvent,
2061
+ },
2062
+ );
2063
+ await deps.topicTargetStore.persist();
2064
+ };
2065
+ try {
2066
+ if (deps.runWorkspaceOperation) {
2067
+ await deps.runWorkspaceOperation(
2068
+ {
2069
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
2070
+ operationKind: "workspace.reconcile-follower-provision",
2071
+ scopes: [{ kind: "profile" }],
2072
+ },
2073
+ reconcile,
2074
+ );
2075
+ } else {
2076
+ await reconcile();
2077
+ }
2078
+ } catch (error) {
1619
2079
  deps.recordRuntimeEvent("telegram", error, {
1620
- phase: "follower-topic-announce",
2080
+ phase: "follower-register-background-reconcile",
1621
2081
  instanceId: registration.instanceId,
1622
2082
  chatId: result.target.chatId,
1623
2083
  threadId: result.target.threadId,
1624
2084
  });
1625
2085
  }
1626
- }
1627
- const reconcile = async (): Promise<void> => {
1628
- await ThreadReconciler.applyThreadReconciliationPlan(
1629
- ThreadReconciler.planThreadReconciliation({
1630
- nowMs: getNowMs(),
1631
- currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
1632
- records: recordsBeforeProvision,
1633
- pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
1634
- replacedBindings: [
1635
- {
1636
- instanceId: registration.instanceId,
1637
- replacementTarget: result.target,
1638
- },
1639
- ],
1640
- }),
1641
- {
1642
- isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
1643
- callApi: deps.callApi,
1644
- markStaleByTarget(target, syncStatus, lastSyncError) {
1645
- return deps.topicTargetStore.markStaleByTarget(
1646
- target,
1647
- syncStatus,
1648
- lastSyncError,
1649
- );
1650
- },
1651
- persist() {
1652
- return deps.topicTargetStore.persist();
1653
- },
1654
- removePendingProvisionById(id) {
1655
- return deps.topicTargetStore.removePendingProvision(id);
1656
- },
1657
- getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
1658
- recordRuntimeEvent: deps.recordRuntimeEvent,
1659
- },
1660
- );
1661
- await deps.topicTargetStore.persist();
1662
- };
1663
- try {
1664
- if (deps.runWorkspaceOperation) {
1665
- await deps.runWorkspaceOperation(
1666
- {
1667
- operationId: createTelegramWorkspaceAdmissionOperationId(),
1668
- operationKind: "workspace.reconcile-follower-provision",
1669
- scopes: [{ kind: "profile" }],
1670
- },
1671
- reconcile,
1672
- );
1673
- } else {
1674
- await reconcile();
1675
- }
1676
- } catch (error) {
1677
- deps.recordRuntimeEvent("telegram", error, {
1678
- phase: "follower-register-background-reconcile",
2086
+ recordSlowTelegramBusFollowerRegistrationStep(deps, {
2087
+ phase: "follower-register-background",
2088
+ elapsedMs: Date.now() - backgroundStartedAtMs,
1679
2089
  instanceId: registration.instanceId,
1680
- chatId: result.target.chatId,
1681
- threadId: result.target.threadId,
2090
+ target: result.target,
2091
+ reused: result.reused,
1682
2092
  });
1683
- }
1684
- recordSlowTelegramBusFollowerRegistrationStep(deps, {
1685
- phase: "follower-register-background",
1686
- elapsedMs: Date.now() - backgroundStartedAtMs,
1687
- instanceId: registration.instanceId,
1688
- target: result.target,
1689
- reused: result.reused,
1690
- });
1691
- }, (error) => {
1692
- deps.recordRuntimeEvent("bus", error, {
1693
- phase: "follower-register-background-owner",
1694
- instanceId: registration.instanceId,
1695
- });
1696
- });
2093
+ },
2094
+ (error) => {
2095
+ deps.recordRuntimeEvent("bus", error, {
2096
+ phase: "follower-register-background-owner",
2097
+ instanceId: registration.instanceId,
2098
+ });
2099
+ },
2100
+ );
1697
2101
  return {
1698
2102
  ...result.target,
1699
2103
  slot: result.record.slot,
@@ -1734,9 +2138,19 @@ function createTelegramBusFollowerCleanupHandler(
1734
2138
  target: { chatId: target.chatId, threadId: target.threadId },
1735
2139
  requestedAtMs: (deps.getNowMs ?? Date.now)(),
1736
2140
  };
1737
- const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === follower.instanceId &&
1738
- record.target.chatId === target.chatId && record.target.threadId === target.threadId);
1739
- const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
2141
+ const departingRecord = deps.topicTargetStore
2142
+ .list()
2143
+ .find(
2144
+ (record) =>
2145
+ record.instanceId === follower.instanceId &&
2146
+ record.target.chatId === target.chatId &&
2147
+ record.target.threadId === target.threadId,
2148
+ );
2149
+ const isCleanupTargetProtected =
2150
+ Threads.createTelegramCleanupTargetProtection(
2151
+ deps.topicTargetStore,
2152
+ departingRecord,
2153
+ );
1740
2154
  deps.topicTargetStore.upsertPendingCleanup(intent);
1741
2155
  await deps.topicTargetStore.persist();
1742
2156
  const cleanupPlan = ThreadReconciler.planThreadReconciliation({
@@ -1751,7 +2165,11 @@ function createTelegramBusFollowerCleanupHandler(
1751
2165
  isCleanupTargetProtected,
1752
2166
  callApi: deps.callApi,
1753
2167
  markStaleByTarget(target, syncStatus, lastSyncError) {
1754
- return deps.topicTargetStore.markStaleByTarget(target, syncStatus, lastSyncError);
2168
+ return deps.topicTargetStore.markStaleByTarget(
2169
+ target,
2170
+ syncStatus,
2171
+ lastSyncError,
2172
+ );
1755
2173
  },
1756
2174
  removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
1757
2175
  persist: deps.topicTargetStore.persist,
@@ -1759,7 +2177,12 @@ function createTelegramBusFollowerCleanupHandler(
1759
2177
  recordRuntimeEvent: deps.recordRuntimeEvent,
1760
2178
  },
1761
2179
  );
1762
- if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action))) return;
2180
+ if (
2181
+ cleanupPlan.actions.some((action) =>
2182
+ isCleanupTargetProtected(action.target, action),
2183
+ )
2184
+ )
2185
+ return;
1763
2186
  if (cleanup.incompleteActions?.length) {
1764
2187
  throw new Error(
1765
2188
  "Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.",
@@ -1857,7 +2280,9 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
1857
2280
  });
1858
2281
  deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
1859
2282
  const cleanupPorts = {
1860
- isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
2283
+ isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(
2284
+ deps.topicTargetStore,
2285
+ ),
1861
2286
  callApi: deps.callApi,
1862
2287
  markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
1863
2288
  removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
@@ -1977,22 +2402,50 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
1977
2402
  }
1978
2403
 
1979
2404
  /** Strict copy of a follower-supplied attachment source; anything malformed falls back to the plain generated name. */
1980
- function parseTelegramBusAttachmentSource(value: unknown): TelegramAttachmentSource | undefined {
1981
- if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
1982
- const raw = value as Record<string, unknown>, chat = raw.chat as Record<string, unknown> | undefined;
1983
- const text = (field: unknown) => typeof field === "string" && field.length > 0 && field.length <= 256;
1984
- if (!text(raw.kind) || !Number.isSafeInteger(raw.messageId) ||
1985
- (raw.index !== undefined && !Number.isSafeInteger(raw.index)) ||
1986
- (raw.userFileName !== undefined && !text(raw.userFileName)) || (raw.scope !== undefined && !text(raw.scope)) ||
1987
- (chat !== undefined && (!chat || typeof chat !== "object" || Array.isArray(chat) || !Number.isSafeInteger(chat.id) ||
1988
- (chat.type !== undefined && !text(chat.type)) || (chat.username !== undefined && !text(chat.username))))) return undefined;
1989
- return { kind: raw.kind as string, messageId: raw.messageId as number,
2405
+ function parseTelegramBusAttachmentSource(
2406
+ value: unknown,
2407
+ ): TelegramAttachmentSource | undefined {
2408
+ if (!value || typeof value !== "object" || Array.isArray(value))
2409
+ return undefined;
2410
+ const raw = value as Record<string, unknown>,
2411
+ chat = raw.chat as Record<string, unknown> | undefined;
2412
+ const text = (field: unknown) =>
2413
+ typeof field === "string" && field.length > 0 && field.length <= 256;
2414
+ if (
2415
+ !text(raw.kind) ||
2416
+ !Number.isSafeInteger(raw.messageId) ||
2417
+ (raw.index !== undefined && !Number.isSafeInteger(raw.index)) ||
2418
+ (raw.userFileName !== undefined && !text(raw.userFileName)) ||
2419
+ (raw.scope !== undefined && !text(raw.scope)) ||
2420
+ (chat !== undefined &&
2421
+ (!chat ||
2422
+ typeof chat !== "object" ||
2423
+ Array.isArray(chat) ||
2424
+ !Number.isSafeInteger(chat.id) ||
2425
+ (chat.type !== undefined && !text(chat.type)) ||
2426
+ (chat.username !== undefined && !text(chat.username))))
2427
+ )
2428
+ return undefined;
2429
+ return {
2430
+ kind: raw.kind as string,
2431
+ messageId: raw.messageId as number,
1990
2432
  ...(raw.index !== undefined ? { index: raw.index as number } : {}),
1991
- ...(raw.userFileName !== undefined ? { userFileName: raw.userFileName as string } : {}),
2433
+ ...(raw.userFileName !== undefined
2434
+ ? { userFileName: raw.userFileName as string }
2435
+ : {}),
1992
2436
  ...(raw.scope !== undefined ? { scope: raw.scope as string } : {}),
1993
- ...(chat ? { chat: { id: chat.id as number,
1994
- ...(chat.type !== undefined ? { type: chat.type as string } : {}),
1995
- ...(chat.username !== undefined ? { username: chat.username as string } : {}) } } : {}) };
2437
+ ...(chat
2438
+ ? {
2439
+ chat: {
2440
+ id: chat.id as number,
2441
+ ...(chat.type !== undefined ? { type: chat.type as string } : {}),
2442
+ ...(chat.username !== undefined
2443
+ ? { username: chat.username as string }
2444
+ : {}),
2445
+ },
2446
+ }
2447
+ : {}),
2448
+ };
1996
2449
  }
1997
2450
 
1998
2451
  export function createTelegramBusLeaderApiProxy(
@@ -2033,7 +2486,11 @@ export function createTelegramBusLeaderApiProxy(
2033
2486
  if (method === "downloadFile") {
2034
2487
  // Follower IPC is a trust boundary: only an exact attachment-source shape reaches the file-name builder.
2035
2488
  const source = parseTelegramBusAttachmentSource(args[2]);
2036
- return deps.downloadFile(args[0] as string, args[1] as string, ...(source ? [source] : []));
2489
+ return deps.downloadFile(
2490
+ args[0] as string,
2491
+ args[1] as string,
2492
+ ...(source ? [source] : []),
2493
+ );
2037
2494
  }
2038
2495
  throw new Error(`Unsupported Telegram bus API method: ${method}`);
2039
2496
  };
@@ -2066,12 +2523,348 @@ function createTelegramBusFollowerMutationRunner(): TelegramBusFollowerMutationR
2066
2523
  };
2067
2524
  }
2068
2525
 
2069
- export function createTelegramBusLeaderEnvelopeHandler(deps: {
2526
+ /** One delegated text effect through existing Workspace/API owners; no closure transport, ordinary proxy or replay loop. */
2527
+ export function createTelegramBusSelectedMenuDeliveryHandler(deps: {
2528
+ followerRegistry: TelegramBusFollowerRegistry;
2529
+ protocolIdentity: TelegramBusProtocolIdentity;
2530
+ workspace: {
2531
+ /** Exact active bot/profile namespace, independently captured by the leader; never follower-supplied. */
2532
+ getScopeKey(): string | undefined;
2533
+ captureAuthority(): Threads.TelegramWorkspaceRestoreAuthority | undefined;
2534
+ getStore(): Threads.TelegramWorkspaceRestore | undefined;
2535
+ threadStore: Pick<
2536
+ Threads.TelegramTopicTargetStore,
2537
+ "withWorkspaceLiveRebindSnapshot"
2538
+ >;
2539
+ getJournalBindingKey(follower: TelegramBusFollowerView): string | undefined;
2540
+ run: TelegramBusWorkspaceAdmissionRunner;
2541
+ };
2542
+ api: {
2543
+ runtime: Pick<TelegramBridgeApiRuntime, "call">;
2544
+ authorize(input: {
2545
+ follower: TelegramBusFollowerView;
2546
+ method: string;
2547
+ args: unknown[];
2548
+ }): boolean;
2549
+ /** Synchronous publication acceptance through the existing ownership owner, never delivery/cleanup permission. */
2550
+ record(
2551
+ record: TelegramBusFollowerMessageOwnershipRecord,
2552
+ assertCurrent: () => void,
2553
+ ): boolean;
2554
+ };
2555
+ }) {
2556
+ return async (
2557
+ input: TelegramBusSelectedMenuDelivery,
2558
+ isCallerCurrent: () => boolean,
2559
+ ): Promise<TelegramBusEnvelope> => {
2560
+ let issued = false,
2561
+ method = "sendMessage";
2562
+ try {
2563
+ const decoded = parseTelegramBusEnvelope(JSON.stringify(input));
2564
+ if (decoded?.kind !== "follower.deliverSelectedMenu")
2565
+ throw new Error("Selected-menu wire is unavailable.");
2566
+ const expected = decoded,
2567
+ { recipient, executor, effect } = expected;
2568
+ method = effect.kind === "send-text" ? "sendMessage" : "editMessageText";
2569
+ const workspace = deps.workspace,
2570
+ api = deps.api,
2571
+ registry = deps.followerRegistry,
2572
+ protocol = structuredClone(deps.protocolIdentity);
2573
+ const scope = workspace?.getScopeKey,
2574
+ scopeKey = scope?.call(workspace);
2575
+ const capture = workspace?.captureAuthority,
2576
+ getStore = workspace?.getStore,
2577
+ threads = workspace?.threadStore,
2578
+ resolveJournal = workspace?.getJournalBindingKey,
2579
+ run = workspace?.run,
2580
+ runtime = api?.runtime,
2581
+ call = runtime?.call,
2582
+ authorize = api?.authorize,
2583
+ record = api?.record;
2584
+ const authority = capture?.call(workspace),
2585
+ store = getStore?.call(workspace),
2586
+ follower = registry.get(expected.instanceId);
2587
+ if (
2588
+ !scopeKey?.trim() ||
2589
+ !authority ||
2590
+ !isDeepStrictEqual(authority.executor, executor) ||
2591
+ authority.operatorUserId !== expected.operatorUserId ||
2592
+ !store ||
2593
+ typeof threads?.withWorkspaceLiveRebindSnapshot !== "function" ||
2594
+ typeof resolveJournal !== "function" ||
2595
+ typeof run !== "function" ||
2596
+ typeof call !== "function" ||
2597
+ typeof authorize !== "function" ||
2598
+ typeof record !== "function" ||
2599
+ !follower
2600
+ )
2601
+ throw new Error("Selected-menu proof/API owner is unavailable.");
2602
+ const retained = store
2603
+ .listLiveRebindings()
2604
+ .find((value) => value.request.operationId === expected.operationId);
2605
+ if (
2606
+ !retained ||
2607
+ retained.phase !== "released" ||
2608
+ retained.cleanup ||
2609
+ retained.request.source.updateIds.length !== 1 ||
2610
+ retained.recipient.kind !== "follower" ||
2611
+ !isDeepStrictEqual(retained.executor, executor) ||
2612
+ retained.operatorUserId !== expected.operatorUserId ||
2613
+ retained.recipient.instanceId !== expected.instanceId ||
2614
+ retained.recipient.sessionId !== recipient.sessionId ||
2615
+ retained.recipient.generation !== expected.registrationGeneration ||
2616
+ retained.request.owner.profileKey !== recipient.profileKey ||
2617
+ !isDeepStrictEqual(retained.request.target, recipient.target)
2618
+ )
2619
+ throw new Error("Selected-menu operation/recipient is unavailable.");
2620
+ const intent = structuredClone(retained),
2621
+ snapshot = threads.withWorkspaceLiveRebindSnapshot;
2622
+ const registered = structuredClone(follower);
2623
+ const body: Record<string, unknown> = {
2624
+ chat_id: recipient.target.chatId,
2625
+ text: effect.text,
2626
+ ...(effect.parseMode ? { parse_mode: effect.parseMode } : {}),
2627
+ ...(effect.replyMarkup
2628
+ ? { reply_markup: structuredClone(effect.replyMarkup) }
2629
+ : {}),
2630
+ ...(effect.kind === "send-text"
2631
+ ? {
2632
+ message_thread_id: recipient.target.threadId,
2633
+ ...(effect.replyToMessageId
2634
+ ? {
2635
+ reply_parameters: {
2636
+ message_id: effect.replyToMessageId,
2637
+ allow_sending_without_reply: true,
2638
+ },
2639
+ }
2640
+ : {}),
2641
+ }
2642
+ : { message_id: effect.messageId }),
2643
+ };
2644
+ const assertRecipientCurrent = () => {
2645
+ const activeAuthority = capture.call(workspace),
2646
+ live = registry.get(expected.instanceId);
2647
+ if (
2648
+ !isCallerCurrent() ||
2649
+ deps.workspace !== workspace ||
2650
+ deps.api !== api ||
2651
+ deps.followerRegistry !== registry ||
2652
+ workspace.getScopeKey !== scope ||
2653
+ scope!.call(workspace) !== scopeKey ||
2654
+ workspace.captureAuthority !== capture ||
2655
+ workspace.getStore !== getStore ||
2656
+ workspace.threadStore !== threads ||
2657
+ workspace.getJournalBindingKey !== resolveJournal ||
2658
+ workspace.run !== run ||
2659
+ threads.withWorkspaceLiveRebindSnapshot !== snapshot ||
2660
+ api.runtime !== runtime ||
2661
+ runtime.call !== call ||
2662
+ api.authorize !== authorize ||
2663
+ api.record !== record ||
2664
+ !authority.isCurrent() ||
2665
+ !activeAuthority?.isCurrent() ||
2666
+ activeAuthority.operatorUserId !== expected.operatorUserId ||
2667
+ !isDeepStrictEqual(activeAuthority.executor, executor) ||
2668
+ getStore.call(workspace) !== store ||
2669
+ !isDeepStrictEqual(deps.protocolIdentity, protocol) ||
2670
+ !hasTelegramBusCapability(
2671
+ protocol,
2672
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
2673
+ ) ||
2674
+ !live ||
2675
+ !getTelegramBusProtocolCompatibility({
2676
+ local: protocol,
2677
+ remote: live.protocol,
2678
+ }).compatible ||
2679
+ !hasTelegramBusCapability(
2680
+ live.protocol,
2681
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
2682
+ ) ||
2683
+ !isDeepStrictEqual(live.protocol, registered.protocol) ||
2684
+ live.registrationGeneration !== expected.registrationGeneration ||
2685
+ live.sessionId !== recipient.sessionId ||
2686
+ live.sessionGeneration !== recipient.sessionGeneration ||
2687
+ live.pid !== recipient.processId ||
2688
+ live.processBirthId !== recipient.processBirthId ||
2689
+ live.profileKey !== recipient.profileKey ||
2690
+ live.cwd !== registered.cwd ||
2691
+ live.slot !== registered.slot ||
2692
+ live.busSocketPath !== registered.busSocketPath ||
2693
+ live.target?.chatId !== recipient.target.chatId ||
2694
+ live.target.threadId !== recipient.target.threadId ||
2695
+ resolveJournal.call(workspace, live) !== recipient.journalBindingKey
2696
+ )
2697
+ throw new Error("Selected-menu authority changed.");
2698
+ let confirmed = false;
2699
+ snapshot.call(threads, intent, (view) => {
2700
+ const bindings = (view.workspaceBindings ?? []).filter(
2701
+ (value) => value.bindingKey === intent.request.binding.bindingKey,
2702
+ );
2703
+ const owners = view.threads.filter(
2704
+ (value) =>
2705
+ value.status === "active" &&
2706
+ (value.slot === intent.request.binding.slot ||
2707
+ isDeepStrictEqual(value.target, recipient.target)),
2708
+ );
2709
+ const binding = bindings[0],
2710
+ owner = owners[0];
2711
+ confirmed =
2712
+ bindings.length === 1 &&
2713
+ binding?.sessionId === recipient.sessionId &&
2714
+ binding.cwd === intent.request.binding.cwd &&
2715
+ binding.workspaceKey === intent.request.binding.workspaceKey &&
2716
+ binding.slot === live.slot &&
2717
+ binding.inactiveSinceMs === undefined &&
2718
+ isDeepStrictEqual(binding.target, recipient.target) &&
2719
+ owners.length === 1 &&
2720
+ owner?.instanceId === expected.instanceId &&
2721
+ owner.slot === binding.slot &&
2722
+ owner.profileKey === recipient.profileKey &&
2723
+ isDeepStrictEqual(owner.owner, intent.request.owner.owner) &&
2724
+ isDeepStrictEqual(owner.target, recipient.target) &&
2725
+ !!live.cwd &&
2726
+ WorkspaceIdentity.normalizeTelegramWorkspacePath(live.cwd) ===
2727
+ binding.cwd;
2728
+ });
2729
+ if (!confirmed || !isCallerCurrent() || !authority.isCurrent())
2730
+ throw new Error("Selected-menu canonical recipient is unavailable.");
2731
+ };
2732
+ const assertCurrent = () => {
2733
+ assertRecipientCurrent();
2734
+ const live = registry.get(expected.instanceId);
2735
+ if (
2736
+ !live ||
2737
+ authorize.call(api, {
2738
+ follower: structuredClone(live),
2739
+ method: "call",
2740
+ args: [method, structuredClone(body)],
2741
+ }) !== true
2742
+ )
2743
+ throw new Error("Selected-menu API recipient is unavailable.");
2744
+ assertRecipientCurrent();
2745
+ };
2746
+ assertCurrent();
2747
+ const result = await run(
2748
+ {
2749
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
2750
+ operationKind: "workspace.selected-menu-delivery",
2751
+ scopes: [{ kind: "profile" }],
2752
+ },
2753
+ async () => {
2754
+ assertCurrent();
2755
+ issued = true;
2756
+ const result = await call.call(runtime, method, body, {
2757
+ assertAuthority: assertCurrent,
2758
+ maxAttempts: 1,
2759
+ retryRateLimit: false,
2760
+ retrySafety: "non-idempotent",
2761
+ });
2762
+ assertCurrent();
2763
+ const value =
2764
+ result && typeof result === "object" && !Array.isArray(result)
2765
+ ? (result as Record<string, unknown>)
2766
+ : undefined;
2767
+ const messageId =
2768
+ effect.kind === "edit-text" ? effect.messageId : value?.message_id;
2769
+ if (
2770
+ !Number.isSafeInteger(messageId) ||
2771
+ (messageId as number) <= 0 ||
2772
+ (effect.kind === "edit-text" &&
2773
+ result !== true &&
2774
+ value?.message_id !== messageId)
2775
+ )
2776
+ throw new Error("Selected-menu API result is unconfirmed.");
2777
+ if (
2778
+ effect.kind === "send-text" &&
2779
+ record.call(
2780
+ api,
2781
+ {
2782
+ follower: structuredClone(registered),
2783
+ chatId: recipient.target.chatId,
2784
+ messageId: messageId as number,
2785
+ target: { ...recipient.target },
2786
+ },
2787
+ assertCurrent,
2788
+ ) !== true
2789
+ )
2790
+ throw new Error(
2791
+ "Selected-menu ownership publication is unconfirmed.",
2792
+ );
2793
+ assertCurrent();
2794
+ return {
2795
+ operationId: expected.operationId,
2796
+ recipient: structuredClone(recipient),
2797
+ registrationGeneration: expected.registrationGeneration,
2798
+ effect: effect.kind,
2799
+ messageId,
2800
+ };
2801
+ },
2802
+ );
2803
+ assertCurrent();
2804
+ return {
2805
+ kind: "bus.ack",
2806
+ requestId: expected.requestId,
2807
+ ok: true,
2808
+ result,
2809
+ };
2810
+ } catch (error) {
2811
+ const unknown =
2812
+ issued &&
2813
+ !(error instanceof TelegramApiAuthorityError && !error.requestIssued);
2814
+ return {
2815
+ kind: "bus.ack",
2816
+ requestId: input.requestId,
2817
+ ok: false,
2818
+ message: "Selected-menu delivery is refused or unconfirmed.",
2819
+ ...(unknown
2820
+ ? { error: { code: "commit-unknown" as const, method } }
2821
+ : {}),
2822
+ };
2823
+ }
2824
+ };
2825
+ }
2826
+
2827
+ /** The current registration a follower request names, or the negative ack refusing an unknown or stale one. */
2828
+ function resolveRegisteredTelegramBusFollower(
2829
+ registry: Pick<TelegramBusFollowerRegistry, "get">,
2830
+ requestId: string,
2831
+ instanceId: string,
2832
+ registrationGeneration: string | undefined,
2833
+ ):
2834
+ | { follower: TelegramBusFollowerView }
2835
+ | { rejection: Extract<TelegramBusEnvelope, { kind: "bus.ack" }> } {
2836
+ const follower = registry.get(instanceId);
2837
+ if (!follower) {
2838
+ return {
2839
+ rejection: rejectTelegramBusRequest(
2840
+ requestId,
2841
+ "Unknown Telegram bus follower instance.",
2842
+ ),
2843
+ };
2844
+ }
2845
+ if (
2846
+ !follower.registrationGeneration ||
2847
+ registrationGeneration !== follower.registrationGeneration
2848
+ ) {
2849
+ return {
2850
+ rejection: rejectTelegramBusRequest(
2851
+ requestId,
2852
+ "Stale Telegram bus follower registration generation.",
2853
+ ),
2854
+ };
2855
+ }
2856
+ return { follower };
2857
+ }
2858
+
2859
+ export interface TelegramBusLeaderEnvelopeHandlerDeps {
2070
2860
  followerRegistry: TelegramBusFollowerRegistry;
2071
2861
  authSecret?: string;
2072
2862
  protocolIdentity: TelegramBusProtocolIdentity;
2073
2863
  getNowMs?: () => number;
2074
2864
  timeoutMs?: number;
2865
+ selectedMenuDelivery?: ReturnType<
2866
+ typeof createTelegramBusSelectedMenuDeliveryHandler
2867
+ >;
2075
2868
  callApi?: (method: string, args: unknown[]) => Promise<unknown> | unknown;
2076
2869
  authorizeFollowerApiCall?: (input: {
2077
2870
  follower: TelegramBusFollowerView;
@@ -2105,8 +2898,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2105
2898
  options?: { existingWorkspaceBindingOnly?: boolean },
2106
2899
  ) =>
2107
2900
  | Promise<
2108
- | (TelegramTarget & { slot?: string; threadName?: string })
2109
- | undefined
2901
+ (TelegramTarget & { slot?: string; threadName?: string }) | undefined
2110
2902
  >
2111
2903
  | (TelegramTarget & { slot?: string; threadName?: string })
2112
2904
  | undefined;
@@ -2123,12 +2915,26 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2123
2915
  ) => Promise<{ threadName: string }> | { threadName: string };
2124
2916
  publishFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
2125
2917
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
2126
- getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
2918
+ getFollowerDisplayTitle?: (
2919
+ follower: TelegramBusFollowerView,
2920
+ ) => string | undefined;
2127
2921
  onFollowerRegistered?: () => void;
2128
- onWorkspaceRestoreRecipientObserved?: TelegramBusLeaderRuntimeDeps<unknown>["onWorkspaceRestoreRecipientObserved"];
2922
+ /** Observation only: reacquire admission and inspect work, never infer completion.
2923
+ * Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
2924
+ onWorkspaceRestoreRecipientObserved?: (
2925
+ follower: TelegramBusFollowerView,
2926
+ isCurrent: () => boolean,
2927
+ ) => Promise<void> | void;
2129
2928
  getWorkspaceRestoreObservationGeneration?: () => number | undefined;
2130
- recordRuntimeEvent?: TelegramBusLeaderRuntimeDeps<unknown>["recordRuntimeEvent"];
2131
- applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
2929
+ recordRuntimeEvent?: (
2930
+ category: string,
2931
+ error: unknown,
2932
+ details?: Record<string, unknown>,
2933
+ ) => void;
2934
+ applyThreadDisplayMode?: (
2935
+ mode: TelegramThreadDisplayMode,
2936
+ isCurrent: () => boolean,
2937
+ ) => Promise<void>;
2132
2938
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
2133
2939
  getCurrentLeaderEpoch?: () => number | string | undefined;
2134
2940
  getTelegramProfile?: () => string | undefined;
@@ -2136,83 +2942,227 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2136
2942
  runFollowerMutation?: TelegramBusFollowerMutationRunner;
2137
2943
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
2138
2944
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
2139
- }): (
2945
+ }
2946
+
2947
+ export function createTelegramBusLeaderEnvelopeHandler(
2948
+ deps: TelegramBusLeaderEnvelopeHandlerDeps,
2949
+ ): (
2140
2950
  envelope: TelegramBusEnvelope,
2141
2951
  ) => Promise<TelegramBusEnvelope> | TelegramBusEnvelope {
2142
2952
  const getNowMs = deps.getNowMs ?? Date.now;
2143
2953
  const runFollowerMutation =
2144
2954
  deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
2145
- const recipientObservations = new Map<string, {
2146
- epoch: number | string; operator: number; profile: string | undefined; runtimeGeneration: number | undefined;
2147
- follower: Omit<TelegramBusFollowerView, "lastHeartbeatMs" | "connectedAtMs" | "threadName">;
2148
- }>();
2955
+ const recipientObservations = new Map<
2956
+ string,
2957
+ {
2958
+ epoch: number | string;
2959
+ operator: number;
2960
+ profile: string | undefined;
2961
+ runtimeGeneration: number | undefined;
2962
+ follower: Omit<
2963
+ TelegramBusFollowerView,
2964
+ "lastHeartbeatMs" | "connectedAtMs" | "threadName"
2965
+ >;
2966
+ }
2967
+ >();
2968
+ /** Rename and reset share one generation- and target-fenced Workspace Thread name change. */
2969
+ const changeFollowerThreadName = (
2970
+ envelope: Extract<
2971
+ TelegramBusEnvelope,
2972
+ { kind: "follower.renameThread" | "follower.resetThreadName" }
2973
+ >,
2974
+ action: "rename" | "reset",
2975
+ apply:
2976
+ | ((
2977
+ follower: TelegramBusFollowerView,
2978
+ ) => Promise<{ threadName: string }> | { threadName: string })
2979
+ | undefined,
2980
+ ): Promise<TelegramBusEnvelope> =>
2981
+ runFollowerMutation(
2982
+ deps.followerRegistry.get(envelope.instanceId) ?? {
2983
+ instanceId: envelope.instanceId,
2984
+ },
2985
+ async (): Promise<TelegramBusEnvelope> => {
2986
+ const resolved = resolveRegisteredTelegramBusFollower(
2987
+ deps.followerRegistry,
2988
+ envelope.requestId,
2989
+ envelope.instanceId,
2990
+ envelope.registrationGeneration,
2991
+ );
2992
+ if ("rejection" in resolved) return resolved.rejection;
2993
+ const follower = resolved.follower;
2994
+ if (
2995
+ follower.target?.chatId !== envelope.target.chatId ||
2996
+ follower.target?.threadId !== envelope.target.threadId
2997
+ )
2998
+ return rejectTelegramBusRequest(
2999
+ envelope.requestId,
3000
+ "Stale Telegram bus follower registration generation.",
3001
+ );
3002
+ if (
3003
+ !hasTelegramBusCapability(
3004
+ deps.protocolIdentity,
3005
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
3006
+ ) ||
3007
+ !hasTelegramBusCapability(
3008
+ follower.protocol,
3009
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
3010
+ ) ||
3011
+ !apply
3012
+ )
3013
+ return rejectTelegramBusRequest(
3014
+ envelope.requestId,
3015
+ `Telegram Workspace Thread ${action} is unavailable.`,
3016
+ );
3017
+ try {
3018
+ const result = await apply(follower);
3019
+ const current = deps.followerRegistry.get(follower.instanceId);
3020
+ if (
3021
+ !current ||
3022
+ current.registrationGeneration !== follower.registrationGeneration
3023
+ )
3024
+ return rejectTelegramBusRequest(
3025
+ envelope.requestId,
3026
+ "Stale Telegram bus follower registration generation.",
3027
+ );
3028
+ deps.followerRegistry.register({
3029
+ ...current,
3030
+ threadName: result.threadName,
3031
+ connectedAtMs: current.connectedAtMs,
3032
+ });
3033
+ return {
3034
+ kind: "bus.ack",
3035
+ requestId: envelope.requestId,
3036
+ ok: true,
3037
+ result,
3038
+ };
3039
+ } catch (error) {
3040
+ return rejectTelegramBusRequest(
3041
+ envelope.requestId,
3042
+ error instanceof Error
3043
+ ? error.message
3044
+ : `Telegram Workspace Thread ${action} failed.`,
3045
+ );
3046
+ }
3047
+ },
3048
+ );
2149
3049
  const observeRestoreRecipient = (follower: TelegramBusFollowerView): void => {
2150
- if (!deps.authSecret || !deps.onWorkspaceRestoreRecipientObserved || !deps.getTelegramProfile ||
2151
- !hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) ||
2152
- !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE)) return;
3050
+ if (
3051
+ !deps.authSecret ||
3052
+ !deps.onWorkspaceRestoreRecipientObserved ||
3053
+ !deps.getTelegramProfile ||
3054
+ !hasTelegramBusCapability(
3055
+ deps.protocolIdentity,
3056
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
3057
+ ) ||
3058
+ !hasTelegramBusCapability(
3059
+ follower.protocol,
3060
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
3061
+ )
3062
+ )
3063
+ return;
2153
3064
  const instanceId = follower.instanceId;
2154
3065
  const report = (error: unknown): void => {
2155
- try { deps.recordRuntimeEvent?.("bus", error, { phase: "workspace-restore-recipient-observation", instanceId }); }
2156
- catch { /* Optional observation diagnostics must not break transport liveness. */ }
3066
+ try {
3067
+ deps.recordRuntimeEvent?.("bus", error, {
3068
+ phase: "workspace-restore-recipient-observation",
3069
+ instanceId,
3070
+ });
3071
+ } catch {
3072
+ /* Optional observation diagnostics must not break transport liveness. */
3073
+ }
2157
3074
  };
2158
3075
  try {
2159
- const epoch = deps.getCurrentLeaderEpoch?.(), operator = deps.getAllowedUserId?.();
2160
- const runtimeGeneration = deps.getWorkspaceRestoreObservationGeneration?.();
2161
- if (epoch === undefined || operator === undefined ||
2162
- (deps.getWorkspaceRestoreObservationGeneration && runtimeGeneration === undefined)) return;
3076
+ const epoch = deps.getCurrentLeaderEpoch?.(),
3077
+ operator = deps.getAllowedUserId?.();
3078
+ const runtimeGeneration =
3079
+ deps.getWorkspaceRestoreObservationGeneration?.();
3080
+ if (
3081
+ epoch === undefined ||
3082
+ operator === undefined ||
3083
+ (deps.getWorkspaceRestoreObservationGeneration &&
3084
+ runtimeGeneration === undefined)
3085
+ )
3086
+ return;
2163
3087
  const snapshot = structuredClone(follower);
2164
- const observed = { epoch, operator, profile: deps.getTelegramProfile(), runtimeGeneration,
2165
- follower: { ...snapshot, lastHeartbeatMs: undefined, connectedAtMs: undefined, threadName: undefined } };
2166
- if (isDeepStrictEqual(recipientObservations.get(instanceId), observed)) return;
3088
+ const observed = {
3089
+ epoch,
3090
+ operator,
3091
+ profile: deps.getTelegramProfile(),
3092
+ runtimeGeneration,
3093
+ follower: {
3094
+ ...snapshot,
3095
+ lastHeartbeatMs: undefined,
3096
+ connectedAtMs: undefined,
3097
+ threadName: undefined,
3098
+ },
3099
+ };
3100
+ if (isDeepStrictEqual(recipientObservations.get(instanceId), observed))
3101
+ return;
2167
3102
  recipientObservations.set(instanceId, observed);
2168
3103
  const isCurrent = (): boolean => {
2169
3104
  const live = deps.followerRegistry.get(instanceId);
2170
- return recipientObservations.get(instanceId) === observed &&
2171
- deps.getWorkspaceRestoreObservationGeneration?.() === observed.runtimeGeneration &&
2172
- hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
2173
- deps.getCurrentLeaderEpoch?.() === observed.epoch && deps.getAllowedUserId?.() === observed.operator &&
2174
- deps.getTelegramProfile?.() === observed.profile && !!live &&
2175
- hasTelegramBusCapability(live.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) &&
2176
- isDeepStrictEqual({ ...live, lastHeartbeatMs: undefined, connectedAtMs: undefined, threadName: undefined }, observed.follower);
3105
+ return (
3106
+ recipientObservations.get(instanceId) === observed &&
3107
+ deps.getWorkspaceRestoreObservationGeneration?.() ===
3108
+ observed.runtimeGeneration &&
3109
+ hasTelegramBusCapability(
3110
+ deps.protocolIdentity,
3111
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
3112
+ ) &&
3113
+ deps.getCurrentLeaderEpoch?.() === observed.epoch &&
3114
+ deps.getAllowedUserId?.() === observed.operator &&
3115
+ deps.getTelegramProfile?.() === observed.profile &&
3116
+ !!live &&
3117
+ hasTelegramBusCapability(
3118
+ live.protocol,
3119
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
3120
+ ) &&
3121
+ isDeepStrictEqual(
3122
+ {
3123
+ ...live,
3124
+ lastHeartbeatMs: undefined,
3125
+ connectedAtMs: undefined,
3126
+ threadName: undefined,
3127
+ },
3128
+ observed.follower,
3129
+ )
3130
+ );
2177
3131
  };
2178
3132
  // A heartbeat is a hint, not completion proof. Never await the listener before acknowledging it.
2179
- void Promise.resolve().then(() => {
2180
- if (isCurrent()) return deps.onWorkspaceRestoreRecipientObserved?.(structuredClone(snapshot), isCurrent);
2181
- }).catch(report).finally(() => {
2182
- if (recipientObservations.get(instanceId) === observed) recipientObservations.delete(instanceId);
2183
- });
2184
- } catch (error) { report(error); }
3133
+ void Promise.resolve()
3134
+ .then(() => {
3135
+ if (isCurrent())
3136
+ return deps.onWorkspaceRestoreRecipientObserved?.(
3137
+ structuredClone(snapshot),
3138
+ isCurrent,
3139
+ );
3140
+ })
3141
+ .catch(report)
3142
+ .finally(() => {
3143
+ if (recipientObservations.get(instanceId) === observed)
3144
+ recipientObservations.delete(instanceId);
3145
+ });
3146
+ } catch (error) {
3147
+ report(error);
3148
+ }
2185
3149
  };
2186
3150
  const handleAgentRequest = async (
2187
3151
  envelope: Extract<
2188
3152
  TelegramBusEnvelope,
2189
3153
  {
2190
- kind:
2191
- | "follower.resolveAgentTarget"
2192
- | "follower.routeAgentMessage";
3154
+ kind: "follower.resolveAgentTarget" | "follower.routeAgentMessage";
2193
3155
  }
2194
3156
  >,
2195
3157
  ): Promise<TelegramBusEnvelope> => {
2196
- const follower = deps.followerRegistry.get(envelope.instanceId);
2197
- if (!follower) {
2198
- return {
2199
- kind: "bus.ack",
2200
- requestId: envelope.requestId,
2201
- ok: false,
2202
- message: "Unknown Telegram bus follower instance.",
2203
- };
2204
- }
2205
- if (
2206
- !follower.registrationGeneration ||
2207
- envelope.registrationGeneration !== follower.registrationGeneration
2208
- ) {
2209
- return {
2210
- kind: "bus.ack",
2211
- requestId: envelope.requestId,
2212
- ok: false,
2213
- message: "Stale Telegram bus follower registration generation.",
2214
- };
2215
- }
3158
+ const followerResolution = resolveRegisteredTelegramBusFollower(
3159
+ deps.followerRegistry,
3160
+ envelope.requestId,
3161
+ envelope.instanceId,
3162
+ envelope.registrationGeneration,
3163
+ );
3164
+ if ("rejection" in followerResolution) return followerResolution.rejection;
3165
+ const follower = followerResolution.follower;
2216
3166
  deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
2217
3167
  if (envelope.kind === "follower.resolveAgentTarget") {
2218
3168
  const target = await deps.resolveAgentTarget?.(
@@ -2226,20 +3176,16 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2226
3176
  ok: true,
2227
3177
  result: target,
2228
3178
  }
2229
- : {
2230
- kind: "bus.ack",
2231
- requestId: envelope.requestId,
2232
- ok: false,
2233
- message: "Telegram agent target is unavailable or ambiguous.",
2234
- };
3179
+ : rejectTelegramBusRequest(
3180
+ envelope.requestId,
3181
+ "Telegram agent target is unavailable or ambiguous.",
3182
+ );
2235
3183
  }
2236
3184
  if (!deps.routeAgentMessage) {
2237
- return {
2238
- kind: "bus.ack",
2239
- requestId: envelope.requestId,
2240
- ok: false,
2241
- message: "Telegram agent message routing is unavailable.",
2242
- };
3185
+ return rejectTelegramBusRequest(
3186
+ envelope.requestId,
3187
+ "Telegram agent message routing is unavailable.",
3188
+ );
2243
3189
  }
2244
3190
  await deps.routeAgentMessage(follower, envelope.message);
2245
3191
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
@@ -2250,26 +3196,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2250
3196
  { kind: "follower.offerQueueHandoff" }
2251
3197
  >,
2252
3198
  ): Promise<TelegramBusEnvelope> => {
2253
- const donor = deps.followerRegistry.get(envelope.instanceId);
2254
- if (!donor) {
2255
- return {
2256
- kind: "bus.ack",
2257
- requestId: envelope.requestId,
2258
- ok: false,
2259
- message: "Unknown Telegram bus follower instance.",
2260
- };
2261
- }
2262
- if (
2263
- !donor.registrationGeneration ||
2264
- envelope.registrationGeneration !== donor.registrationGeneration
2265
- ) {
2266
- return {
2267
- kind: "bus.ack",
2268
- requestId: envelope.requestId,
2269
- ok: false,
2270
- message: "Stale Telegram bus follower registration generation.",
2271
- };
2272
- }
3199
+ const donorResolution = resolveRegisteredTelegramBusFollower(
3200
+ deps.followerRegistry,
3201
+ envelope.requestId,
3202
+ envelope.instanceId,
3203
+ envelope.registrationGeneration,
3204
+ );
3205
+ if ("rejection" in donorResolution) return donorResolution.rejection;
3206
+ const donor = donorResolution.follower;
2273
3207
  const recipient = deps.followerRegistry.get(envelope.recipientInstanceId);
2274
3208
  if (
2275
3209
  !hasTelegramBusCapability(
@@ -2285,32 +3219,26 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2285
3219
  TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
2286
3220
  )
2287
3221
  ) {
2288
- return {
2289
- kind: "bus.ack",
2290
- requestId: envelope.requestId,
2291
- ok: false,
2292
- message: "Telegram queue handoff capability was not negotiated.",
2293
- };
3222
+ return rejectTelegramBusRequest(
3223
+ envelope.requestId,
3224
+ "Telegram queue handoff capability was not negotiated.",
3225
+ );
2294
3226
  }
2295
3227
  if (
2296
3228
  !recipient?.registrationGeneration ||
2297
3229
  envelope.recipientRegistrationGeneration !==
2298
3230
  recipient.registrationGeneration
2299
3231
  ) {
2300
- return {
2301
- kind: "bus.ack",
2302
- requestId: envelope.requestId,
2303
- ok: false,
2304
- message: "Stale Telegram queue handoff recipient registration generation.",
2305
- };
3232
+ return rejectTelegramBusRequest(
3233
+ envelope.requestId,
3234
+ "Stale Telegram queue handoff recipient registration generation.",
3235
+ );
2306
3236
  }
2307
3237
  if (donor.instanceId === recipient.instanceId) {
2308
- return {
2309
- kind: "bus.ack",
2310
- requestId: envelope.requestId,
2311
- ok: false,
2312
- message: "Telegram queue handoff recipient must be another runtime.",
2313
- };
3238
+ return rejectTelegramBusRequest(
3239
+ envelope.requestId,
3240
+ "Telegram queue handoff recipient must be another runtime.",
3241
+ );
2314
3242
  }
2315
3243
  if (deps.routeQueueHandoff) {
2316
3244
  const result = await deps.routeQueueHandoff(donor, envelope);
@@ -2380,27 +3308,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2380
3308
  }
2381
3309
  >,
2382
3310
  ): Promise<TelegramBusEnvelope> => {
2383
- const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
2384
- if (!follower) {
2385
- return {
2386
- kind: "bus.ack",
2387
- requestId: envelope.requestId,
2388
- ok: false,
2389
- message: "Unknown Telegram bus follower instance.",
2390
- };
2391
- }
2392
- if (
2393
- !follower.registrationGeneration ||
2394
- envelope.recipientRegistrationGeneration !==
2395
- follower.registrationGeneration
2396
- ) {
2397
- return {
2398
- kind: "bus.ack",
2399
- requestId: envelope.requestId,
2400
- ok: false,
2401
- message: "Stale Telegram bus follower registration generation.",
2402
- };
2403
- }
3311
+ const followerResolution = resolveRegisteredTelegramBusFollower(
3312
+ deps.followerRegistry,
3313
+ envelope.requestId,
3314
+ envelope.recipientInstanceId,
3315
+ envelope.recipientRegistrationGeneration,
3316
+ );
3317
+ if ("rejection" in followerResolution) return followerResolution.rejection;
3318
+ const follower = followerResolution.follower;
2404
3319
  const followerSocketPath =
2405
3320
  follower.busSocketPath ??
2406
3321
  getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
@@ -2447,12 +3362,10 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2447
3362
  return async (envelope) => {
2448
3363
  const trafficClass = getTelegramBusEnvelopeTrafficClass(envelope);
2449
3364
  if (trafficClass === "response") {
2450
- return {
2451
- kind: "bus.ack",
2452
- requestId: envelope.requestId,
2453
- ok: false,
2454
- message: "Telegram bus response envelope cannot be used as a request.",
2455
- };
3365
+ return rejectTelegramBusRequest(
3366
+ envelope.requestId,
3367
+ "Telegram bus response envelope cannot be used as a request.",
3368
+ );
2456
3369
  }
2457
3370
  if (!isTelegramBusEnvelopeAuthorized(envelope, deps.authSecret)) {
2458
3371
  return createUnauthorizedBusAck(envelope.requestId);
@@ -2465,10 +3378,15 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2465
3378
  remote: envelope.registration.protocol,
2466
3379
  });
2467
3380
  const displayCompatible = () =>
2468
- (deps.getThreadDisplayMode?.() ?? "names") === "names" || (
2469
- hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
2470
- hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE)
2471
- );
3381
+ (deps.getThreadDisplayMode?.() ?? "names") === "names" ||
3382
+ (hasTelegramBusCapability(
3383
+ deps.protocolIdentity,
3384
+ TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
3385
+ ) &&
3386
+ hasTelegramBusCapability(
3387
+ envelope.registration.protocol,
3388
+ TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
3389
+ ));
2472
3390
  const restoringWorkspace =
2473
3391
  envelope.kind === "follower.restoreWorkspace";
2474
3392
  const supportsWorkspaceAutoConnect =
@@ -2481,7 +3399,8 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2481
3399
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
2482
3400
  );
2483
3401
  if (
2484
- !compatibility.compatible || !displayCompatible() ||
3402
+ !compatibility.compatible ||
3403
+ !displayCompatible() ||
2485
3404
  (envelope.registration.cwd !== undefined &&
2486
3405
  envelope.registration.sessionId === undefined) ||
2487
3406
  (restoringWorkspace && !supportsWorkspaceAutoConnect)
@@ -2494,133 +3413,211 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2494
3413
  error: { code: "incompatible-protocol" as const },
2495
3414
  message: `Incompatible Telegram bus protocol: ${
2496
3415
  compatibility.reason ??
2497
- (envelope.registration.cwd !== undefined &&
2498
- envelope.registration.sessionId === undefined
2499
- ? "missing-session-identity"
2500
- : "missing-capability")
3416
+ (envelope.registration.cwd !== undefined &&
3417
+ envelope.registration.sessionId === undefined
3418
+ ? "missing-session-identity"
3419
+ : "missing-capability")
2501
3420
  }.`,
2502
3421
  };
2503
3422
  }
2504
3423
  if (!envelope.registration.registrationGeneration) {
2505
3424
  return {
2506
- kind: "bus.ack", requestId: envelope.requestId, ok: false,
3425
+ kind: "bus.ack",
3426
+ requestId: envelope.requestId,
3427
+ ok: false,
2507
3428
  protocol: deps.protocolIdentity,
2508
- message: "Telegram follower registration requires an exact generation.",
3429
+ message:
3430
+ "Telegram follower registration requires an exact generation.",
2509
3431
  };
2510
3432
  }
2511
3433
  const registrationOperation = () =>
2512
3434
  runFollowerMutation(envelope.registration, async () => {
2513
- try {
2514
- const leaderEpoch = deps.getCurrentLeaderEpoch?.();
2515
- const telegramProfile = deps.getTelegramProfile?.();
2516
- const operatorUserId = deps.getAllowedUserId?.();
2517
- const registrationCurrent = (): boolean =>
2518
- (!deps.getCurrentLeaderEpoch || deps.getCurrentLeaderEpoch() === leaderEpoch) &&
2519
- (!deps.getTelegramProfile || deps.getTelegramProfile() === telegramProfile) &&
2520
- (!deps.getAllowedUserId || deps.getAllowedUserId() === operatorUserId);
2521
- if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
2522
- throw new Error(
2523
- "Telegram follower registration requires leader ownership.",
3435
+ try {
3436
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
3437
+ const telegramProfile = deps.getTelegramProfile?.();
3438
+ const operatorUserId = deps.getAllowedUserId?.();
3439
+ const registrationCurrent = (): boolean =>
3440
+ (!deps.getCurrentLeaderEpoch ||
3441
+ deps.getCurrentLeaderEpoch() === leaderEpoch) &&
3442
+ (!deps.getTelegramProfile ||
3443
+ deps.getTelegramProfile() === telegramProfile) &&
3444
+ (!deps.getAllowedUserId ||
3445
+ deps.getAllowedUserId() === operatorUserId);
3446
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
3447
+ throw new Error(
3448
+ "Telegram follower registration requires leader ownership.",
3449
+ );
3450
+ }
3451
+ if (
3452
+ hasTelegramBusCapability(
3453
+ deps.protocolIdentity,
3454
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
3455
+ ) &&
3456
+ !deps.commitFollowerRegistration
3457
+ )
3458
+ throw new Error(
3459
+ "Workspace Restore registration publication protection is unavailable.",
3460
+ );
3461
+ const target = await deps.provisionFollowerTarget?.(
3462
+ envelope.registration,
3463
+ { existingWorkspaceBindingOnly: restoringWorkspace },
2524
3464
  );
2525
- }
2526
- if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) && !deps.commitFollowerRegistration)
2527
- throw new Error("Workspace Restore registration publication protection is unavailable.");
2528
- const target = await deps.provisionFollowerTarget?.(
2529
- envelope.registration,
2530
- { existingWorkspaceBindingOnly: restoringWorkspace },
2531
- );
2532
- if (restoringWorkspace && !target) {
3465
+ if (restoringWorkspace && !target) {
3466
+ return {
3467
+ kind: "bus.ack" as const,
3468
+ requestId: envelope.requestId,
3469
+ ok: false,
3470
+ protocol: deps.protocolIdentity,
3471
+ error: { code: "workspace-binding-unavailable" as const },
3472
+ message:
3473
+ "No remembered Telegram Workspace Thread is available.",
3474
+ };
3475
+ }
3476
+ if (!registrationCurrent())
3477
+ throw new Error(
3478
+ "Telegram follower registration lost leader ownership.",
3479
+ );
3480
+ if (!displayCompatible()) {
3481
+ throw new Error(
3482
+ "Thread display mode changed; update or restart this follower before connecting.",
3483
+ );
3484
+ }
3485
+ const registeredTarget = target ?? envelope.registration.target;
3486
+ const registeredSlot = target?.slot ?? envelope.registration.slot;
3487
+ if (
3488
+ deps.provisionFollowerTarget &&
3489
+ registeredTarget?.threadId !== undefined &&
3490
+ (!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))
3491
+ ) {
3492
+ throw new Error(
3493
+ "Telegram Thread slot authority is unavailable.",
3494
+ );
3495
+ }
3496
+ const assertRegistrationPublishable = (): void => {
3497
+ if (!registrationCurrent())
3498
+ throw new Error(
3499
+ "Telegram follower registration lost leader ownership.",
3500
+ );
3501
+ if (!displayCompatible())
3502
+ throw new Error(
3503
+ "Thread display mode changed; update or restart this follower before connecting.",
3504
+ );
3505
+ };
3506
+ let publicationOpen = true;
3507
+ let published: TelegramBusFollowerView | undefined;
3508
+ const publish = (): void => {
3509
+ if (!publicationOpen)
3510
+ throw new Error(
3511
+ "Telegram follower registration publication is no longer available.",
3512
+ );
3513
+ publicationOpen = false;
3514
+ assertRegistrationPublishable();
3515
+ published = deps.followerRegistry.register({
3516
+ ...envelope.registration,
3517
+ connectedAtMs: getNowMs(),
3518
+ target: registeredTarget,
3519
+ ...(registeredSlot ? { slot: registeredSlot } : {}),
3520
+ ...((target?.threadName ?? envelope.registration.threadName)
3521
+ ? {
3522
+ threadName:
3523
+ target?.threadName ??
3524
+ envelope.registration.threadName,
3525
+ }
3526
+ : {}),
3527
+ });
3528
+ };
3529
+ try {
3530
+ if (deps.commitFollowerRegistration)
3531
+ await deps.commitFollowerRegistration(
3532
+ {
3533
+ registration: envelope.registration,
3534
+ target: registeredTarget,
3535
+ slot: registeredSlot,
3536
+ },
3537
+ publish,
3538
+ );
3539
+ else publish();
3540
+ } finally {
3541
+ publicationOpen = false;
3542
+ }
3543
+ if (!published)
3544
+ throw new Error(
3545
+ "Telegram follower registration publication was not confirmed.",
3546
+ );
3547
+ assertRegistrationPublishable();
3548
+ const follower = deps.followerRegistry.get(
3549
+ envelope.registration.instanceId,
3550
+ );
3551
+ if (!follower)
3552
+ throw new Error(
3553
+ "Telegram follower registration changed before acknowledgement.",
3554
+ );
3555
+ const {
3556
+ lastHeartbeatMs: _publishedHeartbeat,
3557
+ connectedAtMs: _publishedAt,
3558
+ threadName: _publishedName,
3559
+ ...publishedIdentity
3560
+ } = published;
3561
+ const {
3562
+ lastHeartbeatMs: _heartbeat,
3563
+ connectedAtMs: _connectedAt,
3564
+ threadName: _threadName,
3565
+ ...identity
3566
+ } = follower;
3567
+ if (!isDeepStrictEqual(publishedIdentity, identity))
3568
+ throw new Error(
3569
+ "Telegram follower registration changed before acknowledgement.",
3570
+ );
3571
+ const displayTitle = deps.getFollowerDisplayTitle?.(follower);
3572
+ deps.onFollowerRegistered?.();
3573
+ return {
3574
+ kind: "bus.ack" as const,
3575
+ requestId: envelope.requestId,
3576
+ ok: true,
3577
+ protocol: deps.protocolIdentity,
3578
+ ...(registeredTarget
3579
+ ? {
3580
+ result: {
3581
+ ...registeredTarget,
3582
+ ...(displayTitle !== undefined ? { displayTitle } : {}),
3583
+ },
3584
+ }
3585
+ : {}),
3586
+ };
3587
+ } catch (error) {
3588
+ if (
3589
+ !restoringWorkspace &&
3590
+ deps.runWithWorkspaceCapacity &&
3591
+ error instanceof TelegramWorkspaceSlotUnavailableError
3592
+ )
3593
+ throw error;
2533
3594
  return {
2534
3595
  kind: "bus.ack" as const,
2535
3596
  requestId: envelope.requestId,
2536
3597
  ok: false,
2537
3598
  protocol: deps.protocolIdentity,
2538
- error: { code: "workspace-binding-unavailable" as const },
2539
- message: "No remembered Telegram Workspace Thread is available.",
3599
+ message:
3600
+ error instanceof Error
3601
+ ? error.message
3602
+ : "Telegram bus follower target provisioning failed.",
2540
3603
  };
2541
3604
  }
2542
- if (!registrationCurrent()) throw new Error("Telegram follower registration lost leader ownership.");
2543
- if (!displayCompatible()) {
2544
- throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2545
- }
2546
- const registeredTarget = target ?? envelope.registration.target;
2547
- const registeredSlot = target?.slot ?? envelope.registration.slot;
2548
- if (deps.provisionFollowerTarget &&
2549
- registeredTarget?.threadId !== undefined &&
2550
- (!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
2551
- throw new Error("Telegram Thread slot authority is unavailable.");
2552
- }
2553
- let publicationOpen = true;
2554
- let published: TelegramBusFollowerView | undefined;
2555
- const publish = (): void => {
2556
- if (!publicationOpen) throw new Error("Telegram follower registration publication is no longer available.");
2557
- publicationOpen = false;
2558
- if (!registrationCurrent()) throw new Error("Telegram follower registration lost leader ownership.");
2559
- if (!displayCompatible()) throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2560
- published = deps.followerRegistry.register({
2561
- ...envelope.registration,
2562
- connectedAtMs: getNowMs(),
2563
- target: registeredTarget,
2564
- ...(registeredSlot ? { slot: registeredSlot } : {}),
2565
- ...((target?.threadName ?? envelope.registration.threadName)
2566
- ? { threadName: target?.threadName ?? envelope.registration.threadName } : {}),
2567
- });
2568
- };
2569
- try {
2570
- if (deps.commitFollowerRegistration) await deps.commitFollowerRegistration({
2571
- registration: envelope.registration, target: registeredTarget, slot: registeredSlot,
2572
- }, publish);
2573
- else publish();
2574
- } finally { publicationOpen = false; }
2575
- if (!published) throw new Error("Telegram follower registration publication was not confirmed.");
2576
- if (!registrationCurrent()) throw new Error("Telegram follower registration lost leader ownership.");
2577
- if (!displayCompatible()) throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2578
- const follower = deps.followerRegistry.get(envelope.registration.instanceId);
2579
- if (!follower) throw new Error("Telegram follower registration changed before acknowledgement.");
2580
- const { lastHeartbeatMs: _publishedHeartbeat, connectedAtMs: _publishedAt, threadName: _publishedName, ...publishedIdentity } = published;
2581
- const { lastHeartbeatMs: _heartbeat, connectedAtMs: _connectedAt, threadName: _threadName, ...identity } = follower;
2582
- if (!isDeepStrictEqual(publishedIdentity, identity)) throw new Error("Telegram follower registration changed before acknowledgement.");
2583
- const displayTitle = deps.getFollowerDisplayTitle?.(follower);
2584
- deps.onFollowerRegistered?.();
2585
- return {
2586
- kind: "bus.ack" as const,
2587
- requestId: envelope.requestId,
2588
- ok: true,
2589
- protocol: deps.protocolIdentity,
2590
- ...(registeredTarget ? {
2591
- result: {
2592
- ...registeredTarget,
2593
- ...(displayTitle !== undefined ? { displayTitle } : {}),
3605
+ });
3606
+ const register = () =>
3607
+ deps.runWorkspaceAdmission
3608
+ ? deps.runWorkspaceAdmission(
3609
+ {
3610
+ operationId: `follower-registration:${envelope.requestId}`,
3611
+ operationKind: "workspace.register-follower",
3612
+ scopes: [{ kind: "profile" }],
2594
3613
  },
2595
- } : {}),
2596
- };
2597
- } catch (error) {
2598
- if (!restoringWorkspace && deps.runWithWorkspaceCapacity &&
2599
- error instanceof TelegramWorkspaceSlotUnavailableError) throw error;
2600
- return {
2601
- kind: "bus.ack" as const,
2602
- requestId: envelope.requestId,
2603
- ok: false,
2604
- protocol: deps.protocolIdentity,
2605
- message:
2606
- error instanceof Error
2607
- ? error.message
2608
- : "Telegram bus follower target provisioning failed.",
2609
- };
2610
- }
2611
- },
2612
- );
2613
- const register = () => deps.runWorkspaceAdmission ? deps.runWorkspaceAdmission(
2614
- {
2615
- operationId: `follower-registration:${envelope.requestId}`,
2616
- operationKind: "workspace.register-follower",
2617
- scopes: [{ kind: "profile" }],
2618
- },
2619
- registrationOperation,
2620
- ) : registrationOperation();
3614
+ registrationOperation,
3615
+ )
3616
+ : registrationOperation();
2621
3617
  try {
2622
3618
  return await (!restoringWorkspace && deps.runWithWorkspaceCapacity
2623
- ? deps.runWithWorkspaceCapacity(register) : register());
3619
+ ? deps.runWithWorkspaceCapacity(register)
3620
+ : register());
2624
3621
  } catch (error) {
2625
3622
  return {
2626
3623
  kind: "bus.ack" as const,
@@ -2638,199 +3635,96 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2638
3635
  return routeQueueHandoff(envelope);
2639
3636
  case "follower.setThreadDisplayMode": {
2640
3637
  const epoch = deps.getCurrentLeaderEpoch?.();
2641
- const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
2642
- deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration === envelope.registrationGeneration;
3638
+ const current = () =>
3639
+ epoch !== undefined &&
3640
+ deps.getCurrentLeaderEpoch?.() === epoch &&
3641
+ deps.followerRegistry.get(envelope.instanceId)
3642
+ ?.registrationGeneration === envelope.registrationGeneration;
2643
3643
  const follower = deps.followerRegistry.get(envelope.instanceId);
2644
- const requiredCapability = envelope.mode === "directory-snake" || envelope.mode === "directory-title"
2645
- ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
2646
- : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
2647
- if (!current() || !follower?.registrationGeneration || !deps.applyThreadDisplayMode ||
2648
- !hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
2649
- !hasTelegramBusCapability(follower.protocol, requiredCapability)) {
2650
- return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
2651
- message: "Thread display settings require current registration and compatible leader authority." };
3644
+ const requiredCapability =
3645
+ envelope.mode === "directory-snake" ||
3646
+ envelope.mode === "directory-title"
3647
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
3648
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
3649
+ if (
3650
+ !current() ||
3651
+ !follower?.registrationGeneration ||
3652
+ !deps.applyThreadDisplayMode ||
3653
+ !hasTelegramBusCapability(
3654
+ deps.protocolIdentity,
3655
+ requiredCapability,
3656
+ ) ||
3657
+ !hasTelegramBusCapability(follower.protocol, requiredCapability)
3658
+ ) {
3659
+ return rejectTelegramBusRequest(
3660
+ envelope.requestId,
3661
+ "Thread display settings require current registration and compatible leader authority.",
3662
+ );
2652
3663
  }
2653
3664
  try {
2654
3665
  await deps.applyThreadDisplayMode(envelope.mode, current);
2655
- if (!current()) throw new Error("Thread display setting completed for a stale registration.");
2656
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result: { mode: envelope.mode } };
3666
+ if (!current())
3667
+ throw new Error(
3668
+ "Thread display setting completed for a stale registration.",
3669
+ );
3670
+ return {
3671
+ kind: "bus.ack",
3672
+ requestId: envelope.requestId,
3673
+ ok: true,
3674
+ result: { mode: envelope.mode },
3675
+ };
2657
3676
  } catch (error) {
2658
- return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
2659
- message: error instanceof Error ? error.message : "Thread display setting was not fully applied." };
3677
+ return {
3678
+ kind: "bus.ack",
3679
+ requestId: envelope.requestId,
3680
+ ok: false,
3681
+ message:
3682
+ error instanceof Error
3683
+ ? error.message
3684
+ : "Thread display setting was not fully applied.",
3685
+ };
2660
3686
  }
2661
3687
  }
2662
- case "follower.renameThread": {
2663
- const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
2664
- return runFollowerMutation(
2665
- registeredFollower ?? { instanceId: envelope.instanceId },
2666
- async () => {
2667
- const follower = deps.followerRegistry.get(envelope.instanceId);
2668
- if (!follower) {
2669
- return {
2670
- kind: "bus.ack" as const,
2671
- requestId: envelope.requestId,
2672
- ok: false,
2673
- message: "Unknown Telegram bus follower instance.",
2674
- };
2675
- }
2676
- if (
2677
- !follower.registrationGeneration ||
2678
- envelope.registrationGeneration !== follower.registrationGeneration ||
2679
- follower.target?.chatId !== envelope.target.chatId ||
2680
- follower.target?.threadId !== envelope.target.threadId
2681
- ) {
2682
- return {
2683
- kind: "bus.ack" as const,
2684
- requestId: envelope.requestId,
2685
- ok: false,
2686
- message: "Stale Telegram bus follower registration generation.",
2687
- };
2688
- }
2689
- if (
2690
- !hasTelegramBusCapability(
2691
- deps.protocolIdentity,
2692
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2693
- ) ||
2694
- !hasTelegramBusCapability(
2695
- follower.protocol,
2696
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2697
- ) ||
2698
- !deps.renameFollowerThread
2699
- ) {
2700
- return {
2701
- kind: "bus.ack" as const,
2702
- requestId: envelope.requestId,
2703
- ok: false,
2704
- message: "Telegram Workspace Thread rename is unavailable.",
2705
- };
2706
- }
2707
- try {
2708
- const result = await deps.renameFollowerThread(
2709
- follower,
2710
- envelope.threadName,
2711
- );
2712
- const current = deps.followerRegistry.get(follower.instanceId);
2713
- if (
2714
- current?.registrationGeneration !== follower.registrationGeneration
2715
- ) {
2716
- return {
2717
- kind: "bus.ack" as const,
2718
- requestId: envelope.requestId,
2719
- ok: false,
2720
- message: "Stale Telegram bus follower registration generation.",
2721
- };
2722
- }
2723
- deps.followerRegistry.register({
2724
- ...current,
2725
- threadName: result.threadName,
2726
- connectedAtMs: current.connectedAtMs,
2727
- });
2728
- return {
2729
- kind: "bus.ack" as const,
2730
- requestId: envelope.requestId,
2731
- ok: true,
2732
- result,
2733
- };
2734
- } catch (error) {
2735
- return {
2736
- kind: "bus.ack" as const,
2737
- requestId: envelope.requestId,
2738
- ok: false,
2739
- message:
2740
- error instanceof Error
2741
- ? error.message
2742
- : "Telegram Workspace Thread rename failed.",
2743
- };
2744
- }
2745
- },
3688
+ case "follower.renameThread":
3689
+ return changeFollowerThreadName(
3690
+ envelope,
3691
+ "rename",
3692
+ deps.renameFollowerThread &&
3693
+ ((follower) =>
3694
+ deps.renameFollowerThread!(follower, envelope.threadName)),
2746
3695
  );
2747
- }
2748
- case "follower.resetThreadName": {
2749
- const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
2750
- return runFollowerMutation(
2751
- registeredFollower ?? { instanceId: envelope.instanceId },
2752
- async () => {
2753
- const follower = deps.followerRegistry.get(envelope.instanceId);
2754
- if (!follower || !follower.registrationGeneration ||
2755
- envelope.registrationGeneration !== follower.registrationGeneration ||
2756
- follower.target?.chatId !== envelope.target.chatId ||
2757
- follower.target?.threadId !== envelope.target.threadId) {
2758
- return {
2759
- kind: "bus.ack" as const,
2760
- requestId: envelope.requestId,
2761
- ok: false,
2762
- message: "Stale or unknown Telegram bus follower registration.",
2763
- };
2764
- }
2765
- if (!hasTelegramBusCapability(
2766
- deps.protocolIdentity,
2767
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2768
- ) || !hasTelegramBusCapability(
2769
- follower.protocol,
2770
- TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
2771
- ) || !deps.resetFollowerThreadName) {
2772
- return {
2773
- kind: "bus.ack" as const,
2774
- requestId: envelope.requestId,
2775
- ok: false,
2776
- message: "Telegram Workspace Thread reset is unavailable.",
2777
- };
2778
- }
2779
- try {
2780
- const result = await deps.resetFollowerThreadName(follower);
2781
- const current = deps.followerRegistry.get(follower.instanceId);
2782
- if (current?.registrationGeneration !==
2783
- follower.registrationGeneration) {
2784
- return {
2785
- kind: "bus.ack" as const,
2786
- requestId: envelope.requestId,
2787
- ok: false,
2788
- message: "Stale Telegram bus follower registration generation.",
2789
- };
2790
- }
2791
- deps.followerRegistry.register({
2792
- ...current,
2793
- threadName: result.threadName,
2794
- connectedAtMs: current.connectedAtMs,
2795
- });
2796
- return {
2797
- kind: "bus.ack" as const,
2798
- requestId: envelope.requestId,
2799
- ok: true,
2800
- result,
2801
- };
2802
- } catch (error) {
2803
- return {
2804
- kind: "bus.ack" as const,
2805
- requestId: envelope.requestId,
2806
- ok: false,
2807
- message: error instanceof Error
2808
- ? error.message : "Telegram Workspace Thread reset failed.",
2809
- };
2810
- }
2811
- },
3696
+ case "follower.resetThreadName":
3697
+ return changeFollowerThreadName(
3698
+ envelope,
3699
+ "reset",
3700
+ deps.resetFollowerThreadName,
2812
3701
  );
2813
- }
2814
3702
  case "follower.publishSessionReplacement":
2815
3703
  case "follower.settleSessionReplacement": {
2816
- const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
3704
+ const registeredFollower = deps.followerRegistry.get(
3705
+ envelope.instanceId,
3706
+ );
2817
3707
  return runFollowerMutation(
2818
3708
  registeredFollower ?? { instanceId: envelope.instanceId },
2819
3709
  async () => {
2820
- const publish = envelope.kind === "follower.publishSessionReplacement";
3710
+ const publish =
3711
+ envelope.kind === "follower.publishSessionReplacement";
2821
3712
  const operation = publish
2822
3713
  ? deps.publishFollowerSessionReplacement
2823
3714
  : deps.settleFollowerSessionReplacement;
2824
3715
  const epoch = deps.getCurrentLeaderEpoch?.();
2825
3716
  const follower = deps.followerRegistry.get(envelope.instanceId);
2826
- const isCurrent = () => epoch !== undefined &&
3717
+ const isCurrent = () =>
3718
+ epoch !== undefined &&
2827
3719
  deps.getCurrentLeaderEpoch?.() === epoch &&
2828
- deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration ===
2829
- envelope.registrationGeneration;
3720
+ deps.followerRegistry.get(envelope.instanceId)
3721
+ ?.registrationGeneration === envelope.registrationGeneration;
2830
3722
  if (
2831
3723
  !follower?.registrationGeneration ||
2832
- follower.registrationGeneration !== envelope.registrationGeneration ||
2833
- !isCurrent() || !operation ||
3724
+ follower.registrationGeneration !==
3725
+ envelope.registrationGeneration ||
3726
+ !isCurrent() ||
3727
+ !operation ||
2834
3728
  !hasTelegramBusCapability(
2835
3729
  deps.protocolIdentity,
2836
3730
  TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
@@ -2840,16 +3734,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2840
3734
  TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT,
2841
3735
  )
2842
3736
  ) {
2843
- return {
2844
- kind: "bus.ack" as const,
2845
- requestId: envelope.requestId,
2846
- ok: false,
2847
- message:
2848
- "Telegram session replacement requires current follower registration and compatible leader authority.",
2849
- };
3737
+ return rejectTelegramBusRequest(
3738
+ envelope.requestId,
3739
+ "Telegram session replacement requires current follower registration and compatible leader authority.",
3740
+ );
2850
3741
  }
2851
3742
  try {
2852
- const committed = await operation(follower, envelope.intent, isCurrent);
3743
+ const committed = await operation(
3744
+ follower,
3745
+ envelope.intent,
3746
+ isCurrent,
3747
+ );
2853
3748
  if (!committed || !isCurrent()) {
2854
3749
  return {
2855
3750
  kind: "bus.ack" as const,
@@ -2871,88 +3766,76 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2871
3766
  kind: "bus.ack" as const,
2872
3767
  requestId: envelope.requestId,
2873
3768
  ok: false,
2874
- message: error instanceof Error
2875
- ? error.message
2876
- : "Telegram session replacement request failed.",
3769
+ message:
3770
+ error instanceof Error
3771
+ ? error.message
3772
+ : "Telegram session replacement request failed.",
2877
3773
  };
2878
3774
  }
2879
3775
  },
2880
3776
  );
2881
3777
  }
2882
3778
  case "follower.disconnect": {
2883
- const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
3779
+ const registeredFollower = deps.followerRegistry.get(
3780
+ envelope.instanceId,
3781
+ );
2884
3782
  return runFollowerMutation(
2885
3783
  registeredFollower ?? { instanceId: envelope.instanceId },
2886
3784
  async () => {
2887
- const follower = deps.followerRegistry.get(envelope.instanceId);
2888
- if (!follower) {
2889
- return {
2890
- kind: "bus.ack" as const,
2891
- requestId: envelope.requestId,
2892
- ok: false,
2893
- message: "Unknown Telegram bus follower instance.",
2894
- };
2895
- }
2896
- if (
2897
- !follower.registrationGeneration ||
2898
- !envelope.registrationGeneration ||
2899
- envelope.registrationGeneration !== follower.registrationGeneration
2900
- ) {
2901
- return {
2902
- kind: "bus.ack" as const,
2903
- requestId: envelope.requestId,
2904
- ok: false,
2905
- message: "Stale Telegram bus follower registration generation.",
2906
- };
2907
- }
2908
- await deps.onFollowerDisconnected?.(follower);
2909
- const current = deps.followerRegistry.get(follower.instanceId);
2910
- if (
2911
- current?.registrationGeneration !== follower.registrationGeneration
2912
- ) {
3785
+ const follower = deps.followerRegistry.get(envelope.instanceId);
3786
+ if (!follower) {
3787
+ return rejectTelegramBusRequest(
3788
+ envelope.requestId,
3789
+ "Unknown Telegram bus follower instance.",
3790
+ );
3791
+ }
3792
+ if (
3793
+ !follower.registrationGeneration ||
3794
+ !envelope.registrationGeneration ||
3795
+ envelope.registrationGeneration !==
3796
+ follower.registrationGeneration
3797
+ ) {
3798
+ return rejectTelegramBusRequest(
3799
+ envelope.requestId,
3800
+ "Stale Telegram bus follower registration generation.",
3801
+ );
3802
+ }
3803
+ await deps.onFollowerDisconnected?.(follower);
3804
+ const current = deps.followerRegistry.get(follower.instanceId);
3805
+ if (
3806
+ current?.registrationGeneration !==
3807
+ follower.registrationGeneration
3808
+ ) {
3809
+ return rejectTelegramBusRequest(
3810
+ envelope.requestId,
3811
+ "Stale Telegram bus follower registration generation.",
3812
+ );
3813
+ }
3814
+ deps.followerRegistry.remove(follower.instanceId);
2913
3815
  return {
2914
3816
  kind: "bus.ack" as const,
2915
3817
  requestId: envelope.requestId,
2916
- ok: false,
2917
- message: "Stale Telegram bus follower registration generation.",
3818
+ ok: true,
2918
3819
  };
2919
- }
2920
- deps.followerRegistry.remove(follower.instanceId);
2921
- return {
2922
- kind: "bus.ack" as const,
2923
- requestId: envelope.requestId,
2924
- ok: true,
2925
- };
2926
3820
  },
2927
3821
  );
2928
3822
  }
2929
3823
  case "follower.heartbeat": {
2930
- const current = deps.followerRegistry.get(envelope.instanceId);
2931
- if (!current) {
2932
- return {
2933
- kind: "bus.ack",
2934
- requestId: envelope.requestId,
2935
- ok: false,
2936
- message: "Unknown Telegram bus follower instance.",
2937
- };
2938
- }
2939
- if (
2940
- !current.registrationGeneration ||
2941
- envelope.registrationGeneration !== current.registrationGeneration
2942
- ) {
2943
- return {
2944
- kind: "bus.ack",
2945
- requestId: envelope.requestId,
2946
- ok: false,
2947
- message: "Stale Telegram bus follower registration generation.",
2948
- };
2949
- }
3824
+ const current = resolveRegisteredTelegramBusFollower(
3825
+ deps.followerRegistry,
3826
+ envelope.requestId,
3827
+ envelope.instanceId,
3828
+ envelope.registrationGeneration,
3829
+ );
3830
+ if ("rejection" in current) return current.rejection;
2950
3831
  const follower = deps.followerRegistry.heartbeat(
2951
3832
  envelope.instanceId,
2952
3833
  getNowMs(),
2953
3834
  );
2954
3835
  if (follower) observeRestoreRecipient(follower);
2955
- const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
3836
+ const displayTitle = follower
3837
+ ? deps.getFollowerDisplayTitle?.(follower)
3838
+ : undefined;
2956
3839
  return follower
2957
3840
  ? {
2958
3841
  kind: "bus.ack",
@@ -2972,18 +3855,17 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2972
3855
  ),
2973
3856
  )
2974
3857
  .map((candidate) => candidate.slot)
2975
- .filter((slot): slot is string =>
2976
- typeof slot === "string" && /^[A-Z]$/.test(slot),
3858
+ .filter(
3859
+ (slot): slot is string =>
3860
+ typeof slot === "string" && /^[A-Z]$/.test(slot),
2977
3861
  )
2978
3862
  .sort(),
2979
3863
  },
2980
3864
  }
2981
- : {
2982
- kind: "bus.ack",
2983
- requestId: envelope.requestId,
2984
- ok: false,
2985
- message: "Unknown Telegram bus follower instance.",
2986
- };
3865
+ : rejectTelegramBusRequest(
3866
+ envelope.requestId,
3867
+ "Unknown Telegram bus follower instance.",
3868
+ );
2987
3869
  }
2988
3870
  case "follower.resolveAgentTarget":
2989
3871
  case "follower.routeAgentMessage":
@@ -2993,15 +3875,33 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2993
3875
  case "leader.forwardMessage":
2994
3876
  case "leader.forwardEditedMessage":
2995
3877
  return forwardToFollower(envelope);
3878
+ case "follower.deliverSelectedMenu": {
3879
+ const deliver = deps.selectedMenuDelivery,
3880
+ secret = deps.authSecret,
3881
+ protocol = structuredClone(deps.protocolIdentity);
3882
+ const current = () =>
3883
+ !!secret &&
3884
+ deps.authSecret === secret &&
3885
+ deps.selectedMenuDelivery === deliver &&
3886
+ isDeepStrictEqual(deps.protocolIdentity, protocol) &&
3887
+ hasTelegramBusCapability(
3888
+ protocol,
3889
+ TELEGRAM_BUS_CAPABILITY_SELECTED_MENU_DELIVERY,
3890
+ );
3891
+ if (!deliver || !current())
3892
+ return rejectTelegramBusRequest(
3893
+ envelope.requestId,
3894
+ "Selected-menu delivery is unavailable.",
3895
+ );
3896
+ return deliver(structuredClone(envelope), current);
3897
+ }
2996
3898
  case "follower.callApi":
2997
3899
  return handleFollowerApiCall(envelope, { ...deps, getNowMs });
2998
3900
  default:
2999
- return {
3000
- kind: "bus.ack",
3001
- requestId: envelope.requestId,
3002
- ok: false,
3003
- message: "Telegram bus envelope is not handled by this leader.",
3004
- };
3901
+ return rejectTelegramBusRequest(
3902
+ envelope.requestId,
3903
+ "Telegram bus envelope is not handled by this leader.",
3904
+ );
3005
3905
  }
3006
3906
  };
3007
3907
  }
@@ -3012,13 +3912,6 @@ function asRecord(value: unknown): Record<string, unknown> | undefined {
3012
3912
  : undefined;
3013
3913
  }
3014
3914
 
3015
- function asInteger(value: unknown): number | undefined {
3016
- if (typeof value === "number" && Number.isInteger(value)) return value;
3017
- if (typeof value !== "string" || value.trim() === "") return undefined;
3018
- const parsed = Number(value);
3019
- return Number.isInteger(parsed) ? parsed : undefined;
3020
- }
3021
-
3022
3915
  function getFollowerApiMethodAndBody(
3023
3916
  envelope: Extract<TelegramBusEnvelope, { kind: "follower.callApi" }>,
3024
3917
  ): {
@@ -3088,26 +3981,14 @@ async function handleFollowerApiCall(
3088
3981
  recordFollowerMessageOwnership?: TelegramBusFollowerMessageOwnershipRecorder;
3089
3982
  },
3090
3983
  ): Promise<TelegramBusEnvelope> {
3091
- const follower = deps.followerRegistry.get(envelope.instanceId);
3092
- if (!follower) {
3093
- return {
3094
- kind: "bus.ack",
3095
- requestId: envelope.requestId,
3096
- ok: false,
3097
- message: "Unknown Telegram bus follower instance.",
3098
- };
3099
- }
3100
- if (
3101
- !follower.registrationGeneration ||
3102
- envelope.registrationGeneration !== follower.registrationGeneration
3103
- ) {
3104
- return {
3105
- kind: "bus.ack",
3106
- requestId: envelope.requestId,
3107
- ok: false,
3108
- message: "Stale Telegram bus follower registration generation.",
3109
- };
3110
- }
3984
+ const followerResolution = resolveRegisteredTelegramBusFollower(
3985
+ deps.followerRegistry,
3986
+ envelope.requestId,
3987
+ envelope.instanceId,
3988
+ envelope.registrationGeneration,
3989
+ );
3990
+ if ("rejection" in followerResolution) return followerResolution.rejection;
3991
+ const follower = followerResolution.follower;
3111
3992
  deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
3112
3993
  if (
3113
3994
  deps.authorizeFollowerApiCall &&
@@ -3117,20 +3998,16 @@ async function handleFollowerApiCall(
3117
3998
  args: envelope.args,
3118
3999
  })
3119
4000
  ) {
3120
- return {
3121
- kind: "bus.ack",
3122
- requestId: envelope.requestId,
3123
- ok: false,
3124
- message: "Telegram bus API call is not allowed for this follower.",
3125
- };
4001
+ return rejectTelegramBusRequest(
4002
+ envelope.requestId,
4003
+ "Telegram bus API call is not allowed for this follower.",
4004
+ );
3126
4005
  }
3127
4006
  if (!deps.callApi) {
3128
- return {
3129
- kind: "bus.ack",
3130
- requestId: envelope.requestId,
3131
- ok: false,
3132
- message: "Telegram bus leader does not expose API calling.",
3133
- };
4007
+ return rejectTelegramBusRequest(
4008
+ envelope.requestId,
4009
+ "Telegram bus leader does not expose API calling.",
4010
+ );
3134
4011
  }
3135
4012
  try {
3136
4013
  const result = await deps.callApi(envelope.method, envelope.args);
@@ -3147,10 +4024,9 @@ async function handleFollowerApiCall(
3147
4024
  result,
3148
4025
  };
3149
4026
  } catch (error) {
3150
- const staleTarget =
3151
- Threads.isTelegramTopicTargetStaleError(error)
3152
- ? getTelegramApiErrorRequestTarget(error)
3153
- : undefined;
4027
+ const staleTarget = Threads.isTelegramTopicTargetStaleError(error)
4028
+ ? getTelegramApiErrorRequestTarget(error)
4029
+ : undefined;
3154
4030
  return {
3155
4031
  kind: "bus.ack",
3156
4032
  requestId: envelope.requestId,
@@ -3179,63 +4055,6 @@ async function handleFollowerApiCall(
3179
4055
  }
3180
4056
  }
3181
4057
 
3182
- export interface TelegramBusLeaderActivationSchedulerDeps<TContext> {
3183
- isBusEnabled: () => boolean;
3184
- ownsPolling: (ctx: TContext) => boolean;
3185
- isBusPollingStarted: () => boolean;
3186
- setBusPollingStarted: (started: boolean) => void;
3187
- stopClassicPolling: () => Promise<void>;
3188
- startClassicPolling: (ctx: TContext) => void | Promise<void>;
3189
- startBusLeaderPolling: (ctx: TContext) => Promise<void>;
3190
- updateStatus: (ctx: TContext) => void;
3191
- recordRuntimeEvent?: (
3192
- category: string,
3193
- error: unknown,
3194
- details?: Record<string, unknown>,
3195
- ) => void;
3196
- }
3197
-
3198
- export function createTelegramBusLeaderActivationScheduler<TContext>(
3199
- deps: TelegramBusLeaderActivationSchedulerDeps<TContext>,
3200
- ): (ctx: TContext) => void {
3201
- let pending = false;
3202
- return (ctx) => {
3203
- if (deps.isBusPollingStarted() || pending) return;
3204
- if (!deps.isBusEnabled()) return;
3205
- if (!deps.ownsPolling(ctx)) return;
3206
- pending = true;
3207
- const timer = setTimeout(() => {
3208
- void (async () => {
3209
- try {
3210
- if (deps.isBusPollingStarted()) return;
3211
- if (!deps.isBusEnabled()) return;
3212
- if (!deps.ownsPolling(ctx)) return;
3213
- await deps.stopClassicPolling();
3214
- try {
3215
- await deps.startBusLeaderPolling(ctx);
3216
- deps.setBusPollingStarted(true);
3217
- deps.updateStatus(ctx);
3218
- deps.recordRuntimeEvent?.(
3219
- "bus",
3220
- "Telegram bus leader mode activated",
3221
- { phase: "leader-hot-switch" },
3222
- );
3223
- } catch (error) {
3224
- deps.recordRuntimeEvent?.("bus", error, {
3225
- phase: "leader-hot-switch",
3226
- });
3227
- deps.setBusPollingStarted(false);
3228
- await deps.startClassicPolling(ctx);
3229
- }
3230
- } finally {
3231
- pending = false;
3232
- }
3233
- })();
3234
- }, 0);
3235
- timer.unref?.();
3236
- };
3237
- }
3238
-
3239
4058
  export function createTelegramBusLeaderRuntime<TContext>(
3240
4059
  deps: TelegramBusLeaderRuntimeDeps<TContext>,
3241
4060
  ): TelegramBusLeaderRuntime<TContext> {
@@ -3250,20 +4069,27 @@ export function createTelegramBusLeaderRuntime<TContext>(
3250
4069
  type PreservationObservation = {
3251
4070
  follower: TelegramBusFollowerView;
3252
4071
  operationId: string;
3253
- registration: ReturnType<TelegramBusFollowerRegistry["observeUnregistered"]>;
4072
+ registration: ReturnType<
4073
+ TelegramBusFollowerRegistry["observeUnregistered"]
4074
+ >;
3254
4075
  isCurrent: () => boolean;
3255
4076
  };
3256
4077
  const pendingPreservations = new Map<string, PreservationObservation>();
3257
- const forgetPreservation = (observation: PreservationObservation | undefined) => {
4078
+ const forgetPreservation = (
4079
+ observation: PreservationObservation | undefined,
4080
+ ) => {
3258
4081
  if (!observation) return;
3259
- if (pendingPreservations.get(observation.follower.instanceId) === observation) {
4082
+ if (
4083
+ pendingPreservations.get(observation.follower.instanceId) === observation
4084
+ ) {
3260
4085
  pendingPreservations.delete(observation.follower.instanceId);
3261
4086
  }
3262
4087
  observation.registration.release();
3263
4088
  };
3264
4089
  const stopPruning = () => {
3265
4090
  pruneGeneration += 1;
3266
- for (const observation of pendingPreservations.values()) forgetPreservation(observation);
4091
+ for (const observation of pendingPreservations.values())
4092
+ forgetPreservation(observation);
3267
4093
  if (pruneInterval) clearInterval(pruneInterval);
3268
4094
  pruneInterval = undefined;
3269
4095
  prunePromise = undefined;
@@ -3281,9 +4107,11 @@ export function createTelegramBusLeaderRuntime<TContext>(
3281
4107
  const pruneFollowers = async (expectedGeneration: number) => {
3282
4108
  const epoch = deps.getCurrentLeaderEpoch?.();
3283
4109
  const profile = deps.getTelegramProfile?.();
3284
- const isCurrent = (): boolean => pruneGeneration === expectedGeneration &&
4110
+ const isCurrent = (): boolean =>
4111
+ pruneGeneration === expectedGeneration &&
3285
4112
  deps.getTelegramProfile?.() === profile &&
3286
- (!deps.getCurrentLeaderEpoch || (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
4113
+ (!deps.getCurrentLeaderEpoch ||
4114
+ (epoch !== undefined && deps.getCurrentLeaderEpoch() === epoch));
3287
4115
  try {
3288
4116
  await localServer.ensureEndpoint();
3289
4117
  } catch (error) {
@@ -3294,22 +4122,38 @@ export function createTelegramBusLeaderRuntime<TContext>(
3294
4122
  if (!observation.isCurrent()) forgetPreservation(observation);
3295
4123
  }
3296
4124
  if (!isCurrent()) return;
3297
- const attempts: { follower: TelegramBusFollowerView; observation?: PreservationObservation; fresh: boolean }[] =
3298
- [...pendingPreservations.values()].map((observation) => ({
3299
- follower: observation.follower, observation, fresh: false,
3300
- }));
3301
- const removed = deps.followerRegistry.pruneStale(getNowMs(), followerStaleAfterMs);
4125
+ const attempts: {
4126
+ follower: TelegramBusFollowerView;
4127
+ observation?: PreservationObservation;
4128
+ fresh: boolean;
4129
+ }[] = [...pendingPreservations.values()].map((observation) => ({
4130
+ follower: observation.follower,
4131
+ observation,
4132
+ fresh: false,
4133
+ }));
4134
+ const removed = deps.followerRegistry.pruneStale(
4135
+ getNowMs(),
4136
+ followerStaleAfterMs,
4137
+ );
3302
4138
  for (const follower of removed) {
3303
4139
  forgetPreservation(pendingPreservations.get(follower.instanceId));
3304
4140
  let observation: PreservationObservation | undefined;
3305
- if (deps.onFollowerConfirmedDeadPreserved && epoch !== undefined &&
3306
- follower.registrationGeneration && Number.isSafeInteger(follower.pid) && follower.pid! > 0 &&
3307
- Number.isSafeInteger(follower.target?.threadId) && follower.target!.threadId! > 0) {
4141
+ if (
4142
+ deps.onFollowerConfirmedDeadPreserved &&
4143
+ epoch !== undefined &&
4144
+ follower.registrationGeneration &&
4145
+ Number.isSafeInteger(follower.pid) &&
4146
+ follower.pid! > 0 &&
4147
+ Number.isSafeInteger(follower.target?.threadId) &&
4148
+ follower.target!.threadId! > 0
4149
+ ) {
3308
4150
  if (pendingPreservations.size < TELEGRAM_WORKSPACE_SLOTS.length) {
3309
- const registration = deps.followerRegistry.observeUnregistered(follower);
4151
+ const registration =
4152
+ deps.followerRegistry.observeUnregistered(follower);
3310
4153
  let current = true;
3311
4154
  observation = {
3312
- follower, registration,
4155
+ follower,
4156
+ registration,
3313
4157
  // Reuse exact admission authority after a lost acquisition acknowledgement.
3314
4158
  operationId: createTelegramWorkspaceAdmissionOperationId(),
3315
4159
  isCurrent() {
@@ -3319,9 +4163,13 @@ export function createTelegramBusLeaderRuntime<TContext>(
3319
4163
  };
3320
4164
  pendingPreservations.set(follower.instanceId, observation);
3321
4165
  } else {
3322
- recordPruneEvent("Telegram follower preservation observation capacity exhausted; retaining binding", {
3323
- phase: "follower-preservation-capacity", instanceId: follower.instanceId,
3324
- });
4166
+ recordPruneEvent(
4167
+ "Telegram follower preservation observation capacity exhausted; retaining binding",
4168
+ {
4169
+ phase: "follower-preservation-capacity",
4170
+ instanceId: follower.instanceId,
4171
+ },
4172
+ );
3325
4173
  }
3326
4174
  }
3327
4175
  attempts.push({ follower, observation, fresh: true });
@@ -3333,21 +4181,37 @@ export function createTelegramBusLeaderRuntime<TContext>(
3333
4181
  continue;
3334
4182
  }
3335
4183
  let processConfirmedDead = false;
3336
- if (follower.pid !== undefined && Number.isSafeInteger(follower.pid) &&
3337
- follower.pid > 0 && deps.isFollowerProcessAlive) {
4184
+ if (
4185
+ follower.pid !== undefined &&
4186
+ Number.isSafeInteger(follower.pid) &&
4187
+ follower.pid > 0 &&
4188
+ deps.isFollowerProcessAlive
4189
+ ) {
3338
4190
  try {
3339
- processConfirmedDead = deps.isFollowerProcessAlive(follower.pid) === false;
4191
+ processConfirmedDead =
4192
+ deps.isFollowerProcessAlive(follower.pid) === false;
3340
4193
  } catch (error) {
3341
- if (fresh) recordPruneEvent(error, {
3342
- phase: "follower-process-liveness", instanceId: follower.instanceId, pid: follower.pid,
3343
- });
4194
+ if (fresh)
4195
+ recordPruneEvent(error, {
4196
+ phase: "follower-process-liveness",
4197
+ instanceId: follower.instanceId,
4198
+ pid: follower.pid,
4199
+ });
3344
4200
  }
3345
4201
  }
3346
4202
  if (!processConfirmedDead) {
3347
- if (fresh) recordPruneEvent("Telegram bus follower heartbeat stale; preserving thread binding", {
3348
- phase: "follower-pruned", instanceId: follower.instanceId,
3349
- processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive ? "unknown" : "alive-or-unknown",
3350
- });
4203
+ if (fresh)
4204
+ recordPruneEvent(
4205
+ "Telegram bus follower heartbeat stale; preserving thread binding",
4206
+ {
4207
+ phase: "follower-pruned",
4208
+ instanceId: follower.instanceId,
4209
+ processLiveness:
4210
+ follower.pid === undefined || !deps.isFollowerProcessAlive
4211
+ ? "unknown"
4212
+ : "alive-or-unknown",
4213
+ },
4214
+ );
3351
4215
  continue;
3352
4216
  }
3353
4217
  let cleanupEnabled: boolean | undefined;
@@ -3355,55 +4219,89 @@ export function createTelegramBusLeaderRuntime<TContext>(
3355
4219
  cleanupEnabled = await deps.shouldCleanupConfirmedDeadFollower?.();
3356
4220
  } catch (error) {
3357
4221
  recordPruneEvent(error, {
3358
- phase: "follower-confirmed-dead-cleanup-policy", instanceId: follower.instanceId, pid: follower.pid,
4222
+ phase: "follower-confirmed-dead-cleanup-policy",
4223
+ instanceId: follower.instanceId,
4224
+ pid: follower.pid,
3359
4225
  });
3360
4226
  }
3361
4227
  if (!isCurrent()) {
3362
4228
  forgetPreservation(observation);
3363
4229
  return;
3364
4230
  }
3365
- const recordPreserved = () => recordPruneEvent(
3366
- "Telegram bus follower process confirmed dead; preserving thread binding",
3367
- { phase: "follower-confirmed-dead-preserved", instanceId: follower.instanceId,
3368
- pid: follower.pid, cleanupEnabled },
3369
- );
4231
+ const recordPreserved = () =>
4232
+ recordPruneEvent(
4233
+ "Telegram bus follower process confirmed dead; preserving thread binding",
4234
+ {
4235
+ phase: "follower-confirmed-dead-preserved",
4236
+ instanceId: follower.instanceId,
4237
+ pid: follower.pid,
4238
+ cleanupEnabled,
4239
+ },
4240
+ );
3370
4241
  // Deferred observations authorize only non-destructive publication, never deletion retries.
3371
4242
  if (cleanupEnabled === true && !fresh) {
3372
4243
  forgetPreservation(observation);
3373
4244
  recordPreserved();
3374
4245
  continue;
3375
4246
  }
3376
- const onConfirmedDead = cleanupEnabled === true ? deps.onFollowerConfirmedDead :
3377
- cleanupEnabled === false && observation ? deps.onFollowerConfirmedDeadPreserved : undefined;
4247
+ const onConfirmedDead =
4248
+ cleanupEnabled === true
4249
+ ? deps.onFollowerConfirmedDead
4250
+ : cleanupEnabled === false && observation
4251
+ ? deps.onFollowerConfirmedDeadPreserved
4252
+ : undefined;
3378
4253
  if (!onConfirmedDead) {
3379
4254
  if (cleanupEnabled === true) forgetPreservation(observation);
3380
4255
  if (fresh) recordPreserved();
3381
4256
  continue;
3382
4257
  }
3383
- const findReplacement = () => deps.followerRegistry.list().find((candidate) =>
3384
- candidate.instanceId === follower.instanceId ||
3385
- (!!follower.profileKey && candidate.profileKey === follower.profileKey) ||
3386
- (!!follower.target && candidate.target?.chatId === follower.target.chatId &&
3387
- candidate.target?.threadId === follower.target.threadId),
3388
- );
4258
+ const findReplacement = () =>
4259
+ deps.followerRegistry
4260
+ .list()
4261
+ .find(
4262
+ (candidate) =>
4263
+ candidate.instanceId === follower.instanceId ||
4264
+ (!!follower.profileKey &&
4265
+ candidate.profileKey === follower.profileKey) ||
4266
+ (!!follower.target &&
4267
+ candidate.target?.chatId === follower.target.chatId &&
4268
+ candidate.target?.threadId === follower.target.threadId),
4269
+ );
3389
4270
  let settled = false;
3390
4271
  try {
3391
4272
  await runFollowerMutation(follower, async () => {
3392
- const isDetached = () => isCurrent() && (!observation || observation.isCurrent()) &&
3393
- !findReplacement() && deps.isFollowerProcessAlive?.(follower.pid!) === false;
4273
+ const isDetached = () =>
4274
+ isCurrent() &&
4275
+ (!observation || observation.isCurrent()) &&
4276
+ !findReplacement() &&
4277
+ deps.isFollowerProcessAlive?.(follower.pid!) === false;
3394
4278
  if (!isDetached()) return;
3395
- if (cleanupEnabled === true) await deps.onFollowerConfirmedDead!(follower);
3396
- else settled = await deps.onFollowerConfirmedDeadPreserved!(follower, isDetached, observation!.operationId);
4279
+ if (cleanupEnabled === true)
4280
+ await deps.onFollowerConfirmedDead!(follower);
4281
+ else
4282
+ settled = await deps.onFollowerConfirmedDeadPreserved!(
4283
+ follower,
4284
+ isDetached,
4285
+ observation!.operationId,
4286
+ );
3397
4287
  });
3398
4288
  if (!cleanupEnabled) recordPreserved();
3399
4289
  } catch (error) {
3400
4290
  recordPruneEvent(error, {
3401
- phase: cleanupEnabled ? "follower-confirmed-dead-cleanup" : "follower-confirmed-dead-preserve",
3402
- instanceId: follower.instanceId, pid: follower.pid,
3403
- chatId: follower.target?.chatId, threadId: follower.target?.threadId,
4291
+ phase: cleanupEnabled
4292
+ ? "follower-confirmed-dead-cleanup"
4293
+ : "follower-confirmed-dead-preserve",
4294
+ instanceId: follower.instanceId,
4295
+ pid: follower.pid,
4296
+ chatId: follower.target?.chatId,
4297
+ threadId: follower.target?.threadId,
3404
4298
  });
3405
4299
  } finally {
3406
- if (settled || cleanupEnabled === true || (observation && !observation.isCurrent())) {
4300
+ if (
4301
+ settled ||
4302
+ cleanupEnabled === true ||
4303
+ (observation && !observation.isCurrent())
4304
+ ) {
3407
4305
  forgetPreservation(observation);
3408
4306
  }
3409
4307
  }
@@ -3435,7 +4333,28 @@ export function createTelegramBusLeaderRuntime<TContext>(
3435
4333
  }, followerPruneIntervalMs);
3436
4334
  pruneInterval.unref?.();
3437
4335
  };
4336
+ const selectedMenuDelivery = deps.selectedMenuDelivery,
4337
+ selectedMenuProtocol = deps.protocolIdentity,
4338
+ selectedMenuRegistry = deps.followerRegistry,
4339
+ selectedMenuSecret = deps.authSecret;
3438
4340
  const handleEnvelope = createTelegramBusLeaderEnvelopeHandler({
4341
+ selectedMenuDelivery: selectedMenuDelivery
4342
+ ? (input, isCallerCurrent) => {
4343
+ const generation = pruneGeneration;
4344
+ return selectedMenuDelivery.call(
4345
+ deps,
4346
+ input,
4347
+ () =>
4348
+ !!pruneInterval &&
4349
+ pruneGeneration === generation &&
4350
+ deps.selectedMenuDelivery === selectedMenuDelivery &&
4351
+ deps.protocolIdentity === selectedMenuProtocol &&
4352
+ deps.followerRegistry === selectedMenuRegistry &&
4353
+ deps.authSecret === selectedMenuSecret &&
4354
+ isCallerCurrent(),
4355
+ );
4356
+ }
4357
+ : undefined,
3439
4358
  followerRegistry: deps.followerRegistry,
3440
4359
  authSecret: deps.authSecret,
3441
4360
  protocolIdentity: deps.protocolIdentity,
@@ -3446,11 +4365,13 @@ export function createTelegramBusLeaderRuntime<TContext>(
3446
4365
  resolveAgentTarget: deps.resolveAgentTarget,
3447
4366
  routeAgentMessage: deps.routeAgentMessage,
3448
4367
  routeQueueHandoff: deps.routeQueueHandoff,
3449
- provisionFollowerTarget: deps.provisionFollowerTarget ? (registration, options) => {
3450
- // Same-instance ownership can change in the store before registry publication.
3451
- forgetPreservation(pendingPreservations.get(registration.instanceId));
3452
- return deps.provisionFollowerTarget!(registration, options);
3453
- } : undefined,
4368
+ provisionFollowerTarget: deps.provisionFollowerTarget
4369
+ ? (registration, options) => {
4370
+ // Same-instance ownership can change in the store before registry publication.
4371
+ forgetPreservation(pendingPreservations.get(registration.instanceId));
4372
+ return deps.provisionFollowerTarget!(registration, options);
4373
+ }
4374
+ : undefined,
3454
4375
  commitFollowerRegistration: deps.commitFollowerRegistration,
3455
4376
  onFollowerDisconnected: deps.onFollowerDisconnected,
3456
4377
  renameFollowerThread: deps.renameFollowerThread,
@@ -3459,8 +4380,10 @@ export function createTelegramBusLeaderRuntime<TContext>(
3459
4380
  settleFollowerSessionReplacement: deps.settleFollowerSessionReplacement,
3460
4381
  getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
3461
4382
  onFollowerRegistered: deps.onFollowerRegistered,
3462
- onWorkspaceRestoreRecipientObserved: deps.onWorkspaceRestoreRecipientObserved,
3463
- getWorkspaceRestoreObservationGeneration: () => pruneInterval ? pruneGeneration : undefined,
4383
+ onWorkspaceRestoreRecipientObserved:
4384
+ deps.onWorkspaceRestoreRecipientObserved,
4385
+ getWorkspaceRestoreObservationGeneration: () =>
4386
+ pruneInterval ? pruneGeneration : undefined,
3464
4387
  recordRuntimeEvent: deps.recordRuntimeEvent,
3465
4388
  applyThreadDisplayMode: deps.applyThreadDisplayMode,
3466
4389
  getThreadDisplayMode: deps.getThreadDisplayMode,
@@ -3472,7 +4395,9 @@ export function createTelegramBusLeaderRuntime<TContext>(
3472
4395
  runWithWorkspaceCapacity: deps.runWithWorkspaceCapacity,
3473
4396
  });
3474
4397
  const routeQueueHandoffEnvelope = async (
3475
- input: Parameters<TelegramBusLeaderRuntime<TContext>["routeQueueHandoff"]>[0],
4398
+ input: Parameters<
4399
+ TelegramBusLeaderRuntime<TContext>["routeQueueHandoff"]
4400
+ >[0],
3476
4401
  ): Promise<TelegramBusEnvelope> => {
3477
4402
  const recipient = deps.followerRegistry.get(input.recipientInstanceId);
3478
4403
  if (
@@ -3485,24 +4410,19 @@ export function createTelegramBusLeaderRuntime<TContext>(
3485
4410
  TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
3486
4411
  )
3487
4412
  ) {
3488
- return {
3489
- kind: "bus.ack",
3490
- requestId: input.requestId,
3491
- ok: false,
3492
- message: "Telegram queue handoff capability was not negotiated.",
3493
- };
4413
+ return rejectTelegramBusRequest(
4414
+ input.requestId,
4415
+ "Telegram queue handoff capability was not negotiated.",
4416
+ );
3494
4417
  }
3495
4418
  if (
3496
4419
  !recipient?.registrationGeneration ||
3497
- input.recipientRegistrationGeneration !==
3498
- recipient.registrationGeneration
4420
+ input.recipientRegistrationGeneration !== recipient.registrationGeneration
3499
4421
  ) {
3500
- return {
3501
- kind: "bus.ack",
3502
- requestId: input.requestId,
3503
- ok: false,
3504
- message: "Stale Telegram queue handoff recipient registration generation.",
3505
- };
4422
+ return rejectTelegramBusRequest(
4423
+ input.requestId,
4424
+ "Stale Telegram queue handoff recipient registration generation.",
4425
+ );
3506
4426
  }
3507
4427
  const recipientSocketPath =
3508
4428
  recipient.busSocketPath ??
@@ -3562,8 +4482,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
3562
4482
  handleEnvelope,
3563
4483
  });
3564
4484
  return {
3565
- routeQueueHandoff: (envelope) =>
3566
- routeQueueHandoffEnvelope(envelope),
4485
+ routeQueueHandoff: (envelope) => routeQueueHandoffEnvelope(envelope),
3567
4486
  startPolling: async (ctx) => {
3568
4487
  // Replay durable cleanup before publishing the follower endpoint so a
3569
4488
  // replacement registration cannot reclaim a target while it is deleted.