@llblab/pi-kit 0.27.6 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/BACKLOG.md +2 -5
  2. package/CHANGELOG.md +5 -0
  3. package/README.md +2 -2
  4. package/node_modules/@llblab/pi-telegram/AGENTS.md +10 -8
  5. package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -9
  6. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +13 -0
  7. package/node_modules/@llblab/pi-telegram/LICENSE +21 -0
  8. package/node_modules/@llblab/pi-telegram/README.md +15 -5
  9. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
  10. package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +5 -7
  11. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +5 -1
  12. package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +6 -1
  13. package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +4 -2
  14. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +54 -56
  15. package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +206 -139
  16. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +21 -2
  17. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +181 -25
  18. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.d.ts +4 -1
  19. package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +28 -4
  20. package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +52 -56
  21. package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +78 -246
  22. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +1 -3
  23. package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +23 -26
  24. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +0 -5
  25. package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +5 -5
  26. package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +0 -23
  27. package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +15 -15
  28. package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +0 -17
  29. package/node_modules/@llblab/pi-telegram/dist/lib/config.js +12 -14
  30. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +0 -4
  31. package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +2 -5
  32. package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +255 -61
  33. package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -19
  34. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +0 -2
  35. package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +2 -2
  36. package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +177 -16
  37. package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +1115 -249
  38. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.d.ts +0 -2
  39. package/node_modules/@llblab/pi-telegram/dist/lib/keyboard.js +2 -2
  40. package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +1 -1
  41. package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +100 -12
  42. package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +380 -14
  43. package/node_modules/@llblab/pi-telegram/dist/lib/logging.d.ts +30 -12
  44. package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +129 -72
  45. package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +28 -5
  46. package/node_modules/@llblab/pi-telegram/dist/lib/media.js +26 -8
  47. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +0 -11
  48. package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +8 -8
  49. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +0 -18
  50. package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +18 -18
  51. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +4 -4
  52. package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +12 -7
  53. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +1 -1
  54. package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +1 -1
  55. package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +1 -0
  56. package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +4 -3
  57. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +0 -1
  58. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +1 -1
  59. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +1 -8
  60. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +1 -6
  61. package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +4 -21
  62. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +0 -8
  63. package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +5 -5
  64. package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +50 -7
  65. package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +155 -17
  66. package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +1 -0
  67. package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +2 -1
  68. package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +0 -5
  69. package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +5 -5
  70. package/node_modules/@llblab/pi-telegram/dist/lib/preview.d.ts +0 -3
  71. package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +3 -3
  72. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.d.ts +0 -1
  73. package/node_modules/@llblab/pi-telegram/dist/lib/prompt-templates.js +1 -1
  74. package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +3 -15
  75. package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +69 -8
  76. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +29 -9
  77. package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +104 -33
  78. package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +0 -4
  79. package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +1 -1
  80. package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +73 -8
  81. package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +2206 -307
  82. package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +0 -2
  83. package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +1 -1
  84. package/node_modules/@llblab/pi-telegram/dist/lib/status.d.ts +44 -9
  85. package/node_modules/@llblab/pi-telegram/dist/lib/status.js +149 -27
  86. package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +0 -5
  87. package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +5 -4
  88. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +11 -3
  89. package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +34 -23
  90. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +1 -0
  91. package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +18 -21
  92. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +32 -5
  93. package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +191 -7
  94. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +2 -0
  95. package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +1 -1
  96. package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +291 -59
  97. package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +1647 -369
  98. package/node_modules/@llblab/pi-telegram/dist/lib/turns.d.ts +0 -1
  99. package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -1
  100. package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +184 -28
  101. package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +1032 -114
  102. package/node_modules/@llblab/pi-telegram/dist/lib/wire.d.ts +12 -0
  103. package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +21 -0
  104. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +25 -17
  105. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +95 -20
  106. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.d.ts +20 -0
  107. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +103 -0
  108. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +43 -8
  109. package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +148 -52
  110. package/node_modules/@llblab/pi-telegram/dist/package.json +6 -6
  111. package/node_modules/@llblab/pi-telegram/dist/skills/generated-control-surface/SKILL.md +3 -3
  112. package/node_modules/@llblab/pi-telegram/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  113. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
  114. package/node_modules/@llblab/pi-telegram/docs/activity.md +1 -1
  115. package/node_modules/@llblab/pi-telegram/docs/architecture.md +152 -34
  116. package/node_modules/@llblab/pi-telegram/docs/callback-namespaces.md +1 -1
  117. package/node_modules/@llblab/pi-telegram/docs/delivery.md +1 -1
  118. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
  119. package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
  120. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +125 -19
  121. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -1
  122. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +22 -8
  123. package/node_modules/@llblab/pi-telegram/docs/updates.md +1 -1
  124. package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +5 -9
  125. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +6 -0
  126. package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +5 -2
  127. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +243 -224
  128. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +178 -31
  129. package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +30 -8
  130. package/node_modules/@llblab/pi-telegram/lib/bus.ts +110 -334
  131. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +29 -29
  132. package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +5 -5
  133. package/node_modules/@llblab/pi-telegram/lib/commands.ts +15 -15
  134. package/node_modules/@llblab/pi-telegram/lib/config.ts +12 -17
  135. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +2 -8
  136. package/node_modules/@llblab/pi-telegram/lib/extension.ts +257 -72
  137. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +0 -22
  138. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +2 -2
  139. package/node_modules/@llblab/pi-telegram/lib/journal.ts +1182 -326
  140. package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +2 -2
  141. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +1 -1
  142. package/node_modules/@llblab/pi-telegram/lib/locks.ts +402 -23
  143. package/node_modules/@llblab/pi-telegram/lib/logging.ts +151 -101
  144. package/node_modules/@llblab/pi-telegram/lib/media.ts +51 -9
  145. package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +8 -8
  146. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +18 -18
  147. package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +11 -0
  148. package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +2 -2
  149. package/node_modules/@llblab/pi-telegram/lib/menu.ts +5 -1
  150. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +1 -1
  151. package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +1 -10
  152. package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +5 -24
  153. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +5 -5
  154. package/node_modules/@llblab/pi-telegram/lib/paths.ts +177 -17
  155. package/node_modules/@llblab/pi-telegram/lib/pi.ts +5 -2
  156. package/node_modules/@llblab/pi-telegram/lib/polling.ts +5 -5
  157. package/node_modules/@llblab/pi-telegram/lib/preview.ts +3 -3
  158. package/node_modules/@llblab/pi-telegram/lib/prompt-templates.ts +1 -1
  159. package/node_modules/@llblab/pi-telegram/lib/queue.ts +67 -9
  160. package/node_modules/@llblab/pi-telegram/lib/recovery.ts +104 -44
  161. package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
  162. package/node_modules/@llblab/pi-telegram/lib/routing.ts +1955 -375
  163. package/node_modules/@llblab/pi-telegram/lib/sections.ts +1 -1
  164. package/node_modules/@llblab/pi-telegram/lib/status.ts +156 -36
  165. package/node_modules/@llblab/pi-telegram/lib/sync.ts +4 -4
  166. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +48 -33
  167. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +24 -21
  168. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +267 -9
  169. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +3 -1
  170. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1697 -488
  171. package/node_modules/@llblab/pi-telegram/lib/turns.ts +1 -1
  172. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1072 -151
  173. package/node_modules/@llblab/pi-telegram/lib/wire.ts +28 -0
  174. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +97 -47
  175. package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +147 -0
  176. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +188 -89
  177. package/node_modules/@llblab/pi-telegram/package.json +6 -6
  178. package/node_modules/@llblab/pi-telegram/scripts/audit-exports.mjs +100 -0
  179. package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +80 -43
  180. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +3 -3
  181. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +3 -3
  182. package/package.json +2 -2
@@ -5,6 +5,7 @@
5
5
  * leader activation hot-switching, local bus server startup, and stale follower pruning.
6
6
  */
7
7
 
8
+ import { isDeepStrictEqual } from "node:util";
8
9
  import * as Sync from "./sync.ts";
9
10
  import {
10
11
  createTelegramThreadDisplayReconciler,
@@ -12,6 +13,7 @@ import {
12
13
  resolveTelegramLiveWorkspaceBindingKeys,
13
14
  } from "./thread-display.ts";
14
15
  import type { TelegramThreadDisplayMode } from "./config.ts";
16
+ import * as ThreadNaming from "./thread-naming.ts";
15
17
  import * as ThreadReconciler from "./thread-reconciler.ts";
16
18
  import {
17
19
  getTelegramApiErrorRequestTarget,
@@ -19,7 +21,9 @@ import {
19
21
  type TelegramApiCallOptions,
20
22
  } from "./telegram-api.ts";
21
23
  import type { TelegramTarget } from "./target.ts";
24
+ import type { TelegramAttachmentSource } from "./media.ts";
22
25
  import * as Threads from "./threads.ts";
26
+ import * as WorkspaceIdentity from "./workspace-identity.ts";
23
27
  import {
24
28
  createTelegramBusLocalServer,
25
29
  createUnauthorizedBusAck,
@@ -39,6 +43,7 @@ import {
39
43
  TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION,
40
44
  TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF,
41
45
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT,
46
+ TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE,
42
47
  TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME,
43
48
  TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE,
44
49
  TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT,
@@ -218,7 +223,7 @@ export interface TelegramBusLeaderApiProxyDeps {
218
223
  fileName: string,
219
224
  options?: TelegramApiCallOptions,
220
225
  ) => Promise<unknown>;
221
- downloadFile: (fileId: string, destinationDir: string) => Promise<unknown>;
226
+ downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<unknown>;
222
227
  recoverStaleTargetError?: (
223
228
  apiBody: unknown,
224
229
  error: unknown,
@@ -236,7 +241,9 @@ export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
236
241
  | "onFollowerConfirmedDead"
237
242
  | "onFollowerConfirmedDeadPreserved"
238
243
  | "getTelegramProfile"
244
+ | "getAllowedUserId"
239
245
  | "provisionFollowerTarget"
246
+ | "commitFollowerRegistration"
240
247
  | "provisionLeaderTarget"
241
248
  | "recordRuntimeEvent"
242
249
  >;
@@ -310,10 +317,10 @@ export function createTelegramBusFollowerSessionReplacementAuthority(deps: {
310
317
  intent: Threads.TelegramSessionReplacementIntent,
311
318
  ): { cwd: string; sessionId: string } => {
312
319
  const cwd = follower.cwd
313
- ? Threads.normalizeTelegramWorkspacePath(follower.cwd)
320
+ ? WorkspaceIdentity.normalizeTelegramWorkspacePath(follower.cwd)
314
321
  : undefined;
315
322
  const sessionId = follower.sessionId
316
- ? Threads.normalizeTelegramSessionId(follower.sessionId)
323
+ ? WorkspaceIdentity.normalizeTelegramSessionId(follower.sessionId)
317
324
  : undefined;
318
325
  const nowMs = getNowMs();
319
326
  if (
@@ -645,7 +652,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
645
652
  });
646
653
  if (!renamed) {
647
654
  throw new Error(
648
- Threads.getTelegramTopicThreadNameValidationError(
655
+ ThreadNaming.getTelegramTopicThreadNameValidationError(
649
656
  threadName,
650
657
  record.slot,
651
658
  ) ?? "Telegram Workspace Thread name is already reserved.",
@@ -827,7 +834,7 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
827
834
  }
828
835
  if (!renamed) {
829
836
  throw new Error(
830
- Threads.getTelegramTopicThreadNameValidationError(
837
+ ThreadNaming.getTelegramTopicThreadNameValidationError(
831
838
  threadName,
832
839
  follower.slot,
833
840
  ) ?? "Telegram Workspace Thread name is already reserved.",
@@ -921,8 +928,23 @@ export function createTelegramBusLeaderRuntimeAssembly<TContext>(
921
928
  },
922
929
  () => provisionFollowerTarget(registration, options),
923
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
+ }),
924
945
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
925
946
  getTelegramProfile: deps.getTelegramProfile,
947
+ getAllowedUserId: deps.getAllowedUserId,
926
948
  runWorkspaceAdmission,
927
949
  runWithWorkspaceCapacity,
928
950
  callApi: createTelegramBusLeaderApiProxy({
@@ -975,6 +997,12 @@ export type TelegramBusFollowerMessageOwnershipRecorder = (
975
997
  record: TelegramBusFollowerMessageOwnershipRecord,
976
998
  ) => void;
977
999
 
1000
+ export type TelegramBusFollowerRegistrationCommitter = (input: {
1001
+ registration: TelegramBusInstanceRegistration;
1002
+ target?: TelegramTarget;
1003
+ slot?: string;
1004
+ }, publish: () => void) => Promise<void> | void;
1005
+
978
1006
  export interface TelegramBusLeaderRuntimeDeps<TContext> {
979
1007
  socketPath: TelegramBusSocketPathSource;
980
1008
  commitEndpointPublication?: (commit: () => void) => boolean;
@@ -1015,6 +1043,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
1015
1043
  registration: TelegramBusInstanceRegistration,
1016
1044
  options?: { existingWorkspaceBindingOnly?: boolean },
1017
1045
  ) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
1046
+ commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
1018
1047
  renameFollowerThread?: (
1019
1048
  follower: TelegramBusFollowerView,
1020
1049
  threadName: string,
@@ -1026,16 +1055,22 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
1026
1055
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
1027
1056
  getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
1028
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;
1029
1061
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
1030
1062
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
1031
1063
  getCurrentLeaderEpoch?: () => number | string | undefined;
1032
1064
  getTelegramProfile?: () => string | undefined;
1065
+ getAllowedUserId?: () => number | undefined;
1033
1066
  provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
1034
1067
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
1035
1068
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
1036
1069
  getNowMs?: () => number;
1037
1070
  timeoutMs?: number;
1038
1071
  followerPruneIntervalMs?: number;
1072
+ /** Leader housekeeping after each current prune pass; throttling is the callee's concern. */
1073
+ afterFollowerPrune?: () => void;
1039
1074
  followerStaleAfterMs?: number;
1040
1075
  isFollowerProcessAlive?: (pid: number) => boolean;
1041
1076
  shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
@@ -1264,13 +1299,13 @@ export function createTelegramBusFollowerTargetProvisioner(
1264
1299
  workspaceBinding?.threadName ?? registration.threadName;
1265
1300
  const requestedThreadName =
1266
1301
  carriedThreadName &&
1267
- Threads.isTelegramTopicThreadNameValidForSlot(
1302
+ ThreadNaming.isTelegramTopicThreadNameValidForSlot(
1268
1303
  carriedThreadName,
1269
1304
  registration.slot,
1270
1305
  )
1271
1306
  ? carriedThreadName
1272
1307
  : recoveryHint?.threadName &&
1273
- Threads.isTelegramTopicThreadNameValidForSlot(
1308
+ ThreadNaming.isTelegramTopicThreadNameValidForSlot(
1274
1309
  recoveryHint.threadName,
1275
1310
  recoveryHint.slot,
1276
1311
  )
@@ -1317,6 +1352,10 @@ export function createTelegramBusFollowerTargetProvisioner(
1317
1352
  const runRegistration = async (): Promise<
1318
1353
  (TelegramTarget & { slot?: string; threadName?: string }) | undefined
1319
1354
  > => {
1355
+ if (requestedTarget) deps.topicTargetStore.assertWorkspaceRestoreRegistration({
1356
+ target: requestedTarget, bindingKey: workspaceIdentity?.bindingKey,
1357
+ slot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
1358
+ });
1320
1359
  const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
1321
1360
  if (recoveryTarget) {
1322
1361
  Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
@@ -1358,6 +1397,10 @@ export function createTelegramBusFollowerTargetProvisioner(
1358
1397
  };
1359
1398
  };
1360
1399
  alignResultWithWorkspaceSlot();
1400
+ const assertRestoreRegistration = (): void => deps.topicTargetStore.assertWorkspaceRestoreRegistration({
1401
+ target: result.target, slot: result.record.slot, bindingKey: workspaceIdentity?.bindingKey,
1402
+ });
1403
+ assertRestoreRegistration();
1361
1404
  const crossSessionReuse =
1362
1405
  !!reconnectRecord &&
1363
1406
  reconnectRecord.instanceId !== registration.instanceId;
@@ -1403,6 +1446,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1403
1446
  : undefined;
1404
1447
  if (requiresVisibilityProbe && connectedAnnouncement) {
1405
1448
  try {
1449
+ assertRestoreRegistration();
1406
1450
  await deps.callApi(
1407
1451
  exactSessionHandoff ? "sendChatAction" : "sendMessage",
1408
1452
  exactSessionHandoff
@@ -1418,6 +1462,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1418
1462
  parse_mode: connectedAnnouncement.parseMode,
1419
1463
  },
1420
1464
  );
1465
+ assertRestoreRegistration();
1421
1466
  if (recoverableTarget || probeRequiredRecord) {
1422
1467
  const activatedRecord = deps.topicTargetStore.upsert({
1423
1468
  ...result.record,
@@ -1471,6 +1516,8 @@ export function createTelegramBusFollowerTargetProvisioner(
1471
1516
  }
1472
1517
  connectedAnnouncement = undefined;
1473
1518
  } catch (error) {
1519
+ // Restore protection failures are not evidence that a Telegram topic is missing.
1520
+ assertRestoreRegistration();
1474
1521
  if (Threads.isTelegramTopicTargetStaleError(error)) {
1475
1522
  deps.topicTargetStore.markStaleByTarget(
1476
1523
  result.target,
@@ -1523,7 +1570,10 @@ export function createTelegramBusFollowerTargetProvisioner(
1523
1570
  ? { threadName: result.record.threadName }
1524
1571
  : {}),
1525
1572
  slot: workspaceIdentity.slot,
1526
- journalBindingKeys: [followerProfileKey],
1573
+ ...(workspaceIdentity.sessionId
1574
+ ? { journalSources: [{ sessionId: workspaceIdentity.sessionId, recipientBindingKey: followerProfileKey }],
1575
+ journalBindingKeys: [] }
1576
+ : { journalBindingKeys: [followerProfileKey] }),
1527
1577
  journalBindingsComplete: true,
1528
1578
  updatedAtMs: getNowMs(),
1529
1579
  },
@@ -1537,6 +1587,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1537
1587
  deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
1538
1588
  await deps.topicTargetStore.persist();
1539
1589
  }
1590
+ assertRestoreRegistration();
1540
1591
  deps.setSyncState(
1541
1592
  Sync.markTelegramSyncSliceFresh(
1542
1593
  deps.getSyncState(),
@@ -1773,7 +1824,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
1773
1824
  await deps.topicTargetStore.load();
1774
1825
  const cwd = deps.getCwd?.(ctx);
1775
1826
  const normalizedCwd = cwd
1776
- ? Threads.normalizeTelegramWorkspacePath(cwd)
1827
+ ? WorkspaceIdentity.normalizeTelegramWorkspacePath(cwd)
1777
1828
  : undefined;
1778
1829
  const profileKey = Threads.getTelegramThreadOwnerKey({
1779
1830
  kind: "leader",
@@ -1925,6 +1976,25 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
1925
1976
  };
1926
1977
  }
1927
1978
 
1979
+ /** 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,
1990
+ ...(raw.index !== undefined ? { index: raw.index as number } : {}),
1991
+ ...(raw.userFileName !== undefined ? { userFileName: raw.userFileName as string } : {}),
1992
+ ...(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 } : {}) } } : {}) };
1996
+ }
1997
+
1928
1998
  export function createTelegramBusLeaderApiProxy(
1929
1999
  deps: TelegramBusLeaderApiProxyDeps,
1930
2000
  ): (method: string, args: unknown[]) => Promise<unknown> {
@@ -1961,7 +2031,9 @@ export function createTelegramBusLeaderApiProxy(
1961
2031
  }
1962
2032
  }
1963
2033
  if (method === "downloadFile") {
1964
- return deps.downloadFile(args[0] as string, args[1] as string);
2034
+ // Follower IPC is a trust boundary: only an exact attachment-source shape reaches the file-name builder.
2035
+ const source = parseTelegramBusAttachmentSource(args[2]);
2036
+ return deps.downloadFile(args[0] as string, args[1] as string, ...(source ? [source] : []));
1965
2037
  }
1966
2038
  throw new Error(`Unsupported Telegram bus API method: ${method}`);
1967
2039
  };
@@ -2038,6 +2110,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2038
2110
  >
2039
2111
  | (TelegramTarget & { slot?: string; threadName?: string })
2040
2112
  | undefined;
2113
+ commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
2041
2114
  onFollowerDisconnected?: (
2042
2115
  follower: TelegramBusFollowerView,
2043
2116
  ) => Promise<void> | void;
@@ -2052,9 +2125,14 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2052
2125
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
2053
2126
  getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
2054
2127
  onFollowerRegistered?: () => void;
2128
+ onWorkspaceRestoreRecipientObserved?: TelegramBusLeaderRuntimeDeps<unknown>["onWorkspaceRestoreRecipientObserved"];
2129
+ getWorkspaceRestoreObservationGeneration?: () => number | undefined;
2130
+ recordRuntimeEvent?: TelegramBusLeaderRuntimeDeps<unknown>["recordRuntimeEvent"];
2055
2131
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
2056
2132
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
2057
2133
  getCurrentLeaderEpoch?: () => number | string | undefined;
2134
+ getTelegramProfile?: () => string | undefined;
2135
+ getAllowedUserId?: () => number | undefined;
2058
2136
  runFollowerMutation?: TelegramBusFollowerMutationRunner;
2059
2137
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
2060
2138
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
@@ -2064,6 +2142,47 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2064
2142
  const getNowMs = deps.getNowMs ?? Date.now;
2065
2143
  const runFollowerMutation =
2066
2144
  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
+ }>();
2149
+ 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;
2153
+ const instanceId = follower.instanceId;
2154
+ 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. */ }
2157
+ };
2158
+ 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;
2163
+ 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;
2167
+ recipientObservations.set(instanceId, observed);
2168
+ const isCurrent = (): boolean => {
2169
+ 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);
2177
+ };
2178
+ // 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); }
2185
+ };
2067
2186
  const handleAgentRequest = async (
2068
2187
  envelope: Extract<
2069
2188
  TelegramBusEnvelope,
@@ -2393,11 +2512,19 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2393
2512
  runFollowerMutation(envelope.registration, async () => {
2394
2513
  try {
2395
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);
2396
2521
  if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
2397
2522
  throw new Error(
2398
2523
  "Telegram follower registration requires leader ownership.",
2399
2524
  );
2400
2525
  }
2526
+ if (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE) && !deps.commitFollowerRegistration)
2527
+ throw new Error("Workspace Restore registration publication protection is unavailable.");
2401
2528
  const target = await deps.provisionFollowerTarget?.(
2402
2529
  envelope.registration,
2403
2530
  { existingWorkspaceBindingOnly: restoringWorkspace },
@@ -2412,14 +2539,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2412
2539
  message: "No remembered Telegram Workspace Thread is available.",
2413
2540
  };
2414
2541
  }
2415
- if (
2416
- deps.getCurrentLeaderEpoch &&
2417
- deps.getCurrentLeaderEpoch() !== leaderEpoch
2418
- ) {
2419
- throw new Error(
2420
- "Telegram follower registration lost leader ownership.",
2421
- );
2422
- }
2542
+ if (!registrationCurrent()) throw new Error("Telegram follower registration lost leader ownership.");
2423
2543
  if (!displayCompatible()) {
2424
2544
  throw new Error("Thread display mode changed; update or restart this follower before connecting.");
2425
2545
  }
@@ -2430,20 +2550,37 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2430
2550
  (!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
2431
2551
  throw new Error("Telegram Thread slot authority is unavailable.");
2432
2552
  }
2433
- deps.followerRegistry.register({
2434
- ...envelope.registration,
2435
- connectedAtMs: getNowMs(),
2436
- target: registeredTarget,
2437
- ...(registeredSlot ? { slot: registeredSlot } : {}),
2438
- ...((target?.threadName ?? envelope.registration.threadName)
2439
- ? {
2440
- threadName:
2441
- target?.threadName ?? envelope.registration.threadName,
2442
- }
2443
- : {}),
2444
- });
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.");
2445
2578
  const follower = deps.followerRegistry.get(envelope.registration.instanceId);
2446
- const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
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);
2447
2584
  deps.onFollowerRegistered?.();
2448
2585
  return {
2449
2586
  kind: "bus.ack" as const,
@@ -2814,6 +2951,7 @@ export function createTelegramBusLeaderEnvelopeHandler(deps: {
2814
2951
  envelope.instanceId,
2815
2952
  getNowMs(),
2816
2953
  );
2954
+ if (follower) observeRestoreRecipient(follower);
2817
2955
  const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
2818
2956
  return follower
2819
2957
  ? {
@@ -3276,6 +3414,9 @@ export function createTelegramBusLeaderRuntime<TContext>(
3276
3414
  const expectedGeneration = pruneGeneration;
3277
3415
  let tracked: Promise<void>;
3278
3416
  tracked = pruneFollowers(expectedGeneration)
3417
+ .then(() => {
3418
+ if (pruneGeneration === expectedGeneration) deps.afterFollowerPrune?.();
3419
+ })
3279
3420
  .catch((error) => {
3280
3421
  if (pruneGeneration === expectedGeneration) {
3281
3422
  recordPruneEvent(error, { phase: "follower-prune-owner" });
@@ -3310,6 +3451,7 @@ export function createTelegramBusLeaderRuntime<TContext>(
3310
3451
  forgetPreservation(pendingPreservations.get(registration.instanceId));
3311
3452
  return deps.provisionFollowerTarget!(registration, options);
3312
3453
  } : undefined,
3454
+ commitFollowerRegistration: deps.commitFollowerRegistration,
3313
3455
  onFollowerDisconnected: deps.onFollowerDisconnected,
3314
3456
  renameFollowerThread: deps.renameFollowerThread,
3315
3457
  resetFollowerThreadName: deps.resetFollowerThreadName,
@@ -3317,9 +3459,14 @@ export function createTelegramBusLeaderRuntime<TContext>(
3317
3459
  settleFollowerSessionReplacement: deps.settleFollowerSessionReplacement,
3318
3460
  getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
3319
3461
  onFollowerRegistered: deps.onFollowerRegistered,
3462
+ onWorkspaceRestoreRecipientObserved: deps.onWorkspaceRestoreRecipientObserved,
3463
+ getWorkspaceRestoreObservationGeneration: () => pruneInterval ? pruneGeneration : undefined,
3464
+ recordRuntimeEvent: deps.recordRuntimeEvent,
3320
3465
  applyThreadDisplayMode: deps.applyThreadDisplayMode,
3321
3466
  getThreadDisplayMode: deps.getThreadDisplayMode,
3322
3467
  getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
3468
+ getTelegramProfile: deps.getTelegramProfile,
3469
+ getAllowedUserId: deps.getAllowedUserId,
3323
3470
  runFollowerMutation,
3324
3471
  runWorkspaceAdmission: deps.runWorkspaceAdmission,
3325
3472
  runWithWorkspaceCapacity: deps.runWithWorkspaceCapacity,
@@ -6,7 +6,17 @@
6
6
 
7
7
  import { createHash } from "node:crypto";
8
8
  import { createConnection } from "node:net";
9
- import { join, resolve } from "node:path";
9
+ import { isAbsolute, join, resolve } from "node:path";
10
+ import { resolveTelegramTempDir, resolveTelegramRuntimeDir } from "./paths.ts";
11
+
12
+ export type TelegramBusEndpointLayout = "consolidated";
13
+ export const TELEGRAM_BUS_MAX_DIRECT_UNIX_ENDPOINT_BYTES = 80;
14
+
15
+ function requireConsolidatedUnixEndpoint(path: string): string {
16
+ if (!isAbsolute(path) || resolve(path) !== path) throw new Error("Consolidated Telegram IPC requires an exact absolute runtime path.");
17
+ // Over-budget logical paths keep their identity; socket resolution applies the bounded private external fallback.
18
+ return path;
19
+ }
10
20
 
11
21
  export type TelegramBusTransportKind = "pipe" | "socket";
12
22
 
@@ -41,7 +51,7 @@ export const TELEGRAM_BUS_REGISTRATION_RETRY: TelegramBusTransportRetryPolicy =
41
51
  delayMs: 150,
42
52
  };
43
53
 
44
- export const TELEGRAM_BUS_OPERATION_RETRY: TelegramBusTransportRetryPolicy = {
54
+ const TELEGRAM_BUS_OPERATION_RETRY: TelegramBusTransportRetryPolicy = {
45
55
  attempts: 3,
46
56
  delayMs: 100,
47
57
  };
@@ -103,7 +113,15 @@ export function getTelegramBusLeaderEndpoint(input: {
103
113
  agentDir: string;
104
114
  platform: NodeJS.Platform | string;
105
115
  profileName?: string;
116
+ layout?: TelegramBusEndpointLayout;
106
117
  }): string {
118
+ if (input.layout !== undefined && input.layout !== "consolidated") throw new Error("Telegram IPC layout is invalid.");
119
+ if (input.layout === "consolidated") {
120
+ const scope = createHash("sha256").update(JSON.stringify([input.profileName ?? "default"])).digest("hex").slice(0, 16);
121
+ return input.platform === "win32"
122
+ ? getTelegramBusPipePath({ agentDir: input.agentDir, scope: `bus-${scope}` })
123
+ : requireConsolidatedUnixEndpoint(join(resolveTelegramRuntimeDir(input.agentDir), `bus.${scope}.sock`));
124
+ }
107
125
  const profileScope = input.profileName
108
126
  ? normalizeTelegramBusEndpointScope(input.profileName)
109
127
  : undefined;
@@ -113,9 +131,7 @@ export function getTelegramBusLeaderEndpoint(input: {
113
131
  scope: profileScope ? `bus-${profileScope}` : "bus",
114
132
  })
115
133
  : join(
116
- input.agentDir,
117
- "tmp",
118
- "telegram",
134
+ resolveTelegramTempDir(input.agentDir),
119
135
  profileScope ? `bus.${profileScope}.sock` : "bus.sock",
120
136
  );
121
137
  }
@@ -125,7 +141,15 @@ export function getTelegramBusFollowerEndpoint(input: {
125
141
  platform: NodeJS.Platform | string;
126
142
  instanceId: string;
127
143
  profileName?: string;
144
+ layout?: TelegramBusEndpointLayout;
128
145
  }): string {
146
+ if (input.layout !== undefined && input.layout !== "consolidated") throw new Error("Telegram IPC layout is invalid.");
147
+ if (input.layout === "consolidated") {
148
+ const scope = createHash("sha256").update(JSON.stringify([input.profileName ?? "default", input.instanceId])).digest("hex").slice(0, 16);
149
+ return input.platform === "win32"
150
+ ? getTelegramBusPipePath({ agentDir: input.agentDir, scope: `follower-${scope}` })
151
+ : requireConsolidatedUnixEndpoint(join(resolveTelegramRuntimeDir(input.agentDir), `f.${scope}.sock`));
152
+ }
129
153
  const instanceScope = normalizeTelegramBusEndpointScope(input.instanceId);
130
154
  const profileScope = input.profileName
131
155
  ? normalizeTelegramBusEndpointScope(input.profileName)
@@ -138,9 +162,7 @@ export function getTelegramBusFollowerEndpoint(input: {
138
162
  : `follower-${instanceScope}`,
139
163
  })
140
164
  : join(
141
- input.agentDir,
142
- "tmp",
143
- "telegram",
165
+ resolveTelegramTempDir(input.agentDir),
144
166
  "followers",
145
167
  ...(profileScope ? [profileScope] : []),
146
168
  `${instanceScope}.sock`,