@llblab/pi-telegram 0.46.0 → 0.47.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 (199) hide show
  1. package/BACKLOG.md +0 -1
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +2 -2
  4. package/dist/api/activity.d.ts +6 -0
  5. package/dist/api/activity.js +6 -0
  6. package/dist/api/commands.d.ts +6 -0
  7. package/dist/api/commands.js +6 -0
  8. package/dist/api/delivery.d.ts +6 -0
  9. package/dist/api/delivery.js +6 -0
  10. package/dist/api/inbound.d.ts +6 -0
  11. package/dist/api/inbound.js +6 -0
  12. package/dist/api/keyboard.d.ts +6 -0
  13. package/dist/api/keyboard.js +6 -0
  14. package/dist/api/outbound.d.ts +6 -0
  15. package/dist/api/outbound.js +6 -0
  16. package/dist/api/sections.d.ts +7 -0
  17. package/dist/api/sections.js +6 -0
  18. package/dist/api/status.d.ts +6 -0
  19. package/dist/api/status.js +6 -0
  20. package/dist/api/updates.d.ts +6 -0
  21. package/dist/api/updates.js +6 -0
  22. package/dist/api/voice.d.ts +6 -0
  23. package/dist/api/voice.js +6 -0
  24. package/dist/index.d.ts +6 -0
  25. package/dist/index.js +6 -0
  26. package/dist/lib/activity-verbosity.d.ts +52 -0
  27. package/dist/lib/activity-verbosity.js +596 -0
  28. package/dist/lib/activity.d.ts +220 -0
  29. package/dist/lib/activity.js +574 -0
  30. package/dist/lib/agent-messages.d.ts +28 -0
  31. package/dist/lib/agent-messages.js +86 -0
  32. package/dist/lib/bindings.d.ts +250 -0
  33. package/dist/lib/bindings.js +936 -0
  34. package/dist/lib/bus-api.d.ts +18 -0
  35. package/dist/lib/bus-api.js +254 -0
  36. package/dist/lib/bus-follower.d.ts +464 -0
  37. package/dist/lib/bus-follower.js +1686 -0
  38. package/dist/lib/bus-leader.d.ts +292 -0
  39. package/dist/lib/bus-leader.js +2242 -0
  40. package/dist/lib/bus-transport.d.ts +64 -0
  41. package/dist/lib/bus-transport.js +140 -0
  42. package/dist/lib/bus.d.ts +518 -0
  43. package/dist/lib/bus.js +2055 -0
  44. package/dist/lib/channel-posts.d.ts +170 -0
  45. package/dist/lib/channel-posts.js +611 -0
  46. package/dist/lib/command-templates.d.ts +70 -0
  47. package/dist/lib/command-templates.js +744 -0
  48. package/dist/lib/commands.d.ts +541 -0
  49. package/dist/lib/commands.js +1155 -0
  50. package/dist/lib/config.d.ts +252 -0
  51. package/dist/lib/config.js +889 -0
  52. package/dist/lib/delivery.d.ts +163 -0
  53. package/dist/lib/delivery.js +542 -0
  54. package/dist/lib/extension.d.ts +7 -0
  55. package/dist/lib/extension.js +1608 -0
  56. package/dist/lib/generative-app-worker.mjs +104 -0
  57. package/dist/lib/generative-apps.d.ts +212 -0
  58. package/dist/lib/generative-apps.js +1006 -0
  59. package/dist/lib/inbound.d.ts +91 -0
  60. package/dist/lib/inbound.js +502 -0
  61. package/dist/lib/journal.d.ts +665 -0
  62. package/dist/lib/journal.js +3701 -0
  63. package/dist/lib/keyboard.d.ts +23 -0
  64. package/dist/lib/keyboard.js +37 -0
  65. package/dist/lib/lifecycle.d.ts +157 -0
  66. package/dist/lib/lifecycle.js +395 -0
  67. package/dist/lib/locks.d.ts +164 -0
  68. package/dist/lib/locks.js +1208 -0
  69. package/dist/lib/logging.d.ts +50 -0
  70. package/dist/lib/logging.js +274 -0
  71. package/dist/lib/media.d.ts +181 -0
  72. package/dist/lib/media.js +602 -0
  73. package/dist/lib/menu-model.d.ts +217 -0
  74. package/dist/lib/menu-model.js +663 -0
  75. package/dist/lib/menu-queue.d.ts +37 -0
  76. package/dist/lib/menu-queue.js +408 -0
  77. package/dist/lib/menu-settings.d.ts +116 -0
  78. package/dist/lib/menu-settings.js +595 -0
  79. package/dist/lib/menu-status.d.ts +32 -0
  80. package/dist/lib/menu-status.js +112 -0
  81. package/dist/lib/menu-thinking.d.ts +29 -0
  82. package/dist/lib/menu-thinking.js +82 -0
  83. package/dist/lib/menu.d.ts +171 -0
  84. package/dist/lib/menu.js +323 -0
  85. package/dist/lib/model.d.ts +127 -0
  86. package/dist/lib/model.js +409 -0
  87. package/dist/lib/outbound-attachments.d.ts +241 -0
  88. package/dist/lib/outbound-attachments.js +639 -0
  89. package/dist/lib/outbound-buttons.d.ts +69 -0
  90. package/dist/lib/outbound-buttons.js +248 -0
  91. package/dist/lib/outbound-markup.d.ts +42 -0
  92. package/dist/lib/outbound-markup.js +678 -0
  93. package/dist/lib/outbound-voice.d.ts +55 -0
  94. package/dist/lib/outbound-voice.js +152 -0
  95. package/dist/lib/outbound.d.ts +185 -0
  96. package/dist/lib/outbound.js +516 -0
  97. package/dist/lib/ownership.d.ts +77 -0
  98. package/dist/lib/ownership.js +174 -0
  99. package/dist/lib/paths.d.ts +40 -0
  100. package/dist/lib/paths.js +94 -0
  101. package/dist/lib/pi.d.ts +72 -0
  102. package/dist/lib/pi.js +121 -0
  103. package/dist/lib/polling.d.ts +351 -0
  104. package/dist/lib/polling.js +1196 -0
  105. package/dist/lib/preview.d.ts +192 -0
  106. package/dist/lib/preview.js +614 -0
  107. package/dist/lib/prompt-templates.d.ts +24 -0
  108. package/dist/lib/prompt-templates.js +118 -0
  109. package/dist/lib/prompts.d.ts +57 -0
  110. package/dist/lib/prompts.js +167 -0
  111. package/dist/lib/queue.d.ts +766 -0
  112. package/dist/lib/queue.js +1965 -0
  113. package/dist/lib/recovery.d.ts +86 -0
  114. package/dist/lib/recovery.js +285 -0
  115. package/dist/lib/rendering.d.ts +20 -0
  116. package/dist/lib/rendering.js +983 -0
  117. package/dist/lib/replies.d.ts +214 -0
  118. package/dist/lib/replies.js +737 -0
  119. package/dist/lib/routing.d.ts +207 -0
  120. package/dist/lib/routing.js +2282 -0
  121. package/dist/lib/runtime.d.ts +172 -0
  122. package/dist/lib/runtime.js +402 -0
  123. package/dist/lib/sections.d.ts +165 -0
  124. package/dist/lib/sections.js +327 -0
  125. package/dist/lib/setup.d.ts +82 -0
  126. package/dist/lib/setup.js +150 -0
  127. package/dist/lib/skills.d.ts +8 -0
  128. package/dist/lib/skills.js +12 -0
  129. package/dist/lib/status.d.ts +442 -0
  130. package/dist/lib/status.js +1006 -0
  131. package/dist/lib/sync.d.ts +179 -0
  132. package/dist/lib/sync.js +782 -0
  133. package/dist/lib/target.d.ts +20 -0
  134. package/dist/lib/target.js +27 -0
  135. package/dist/lib/telegram-api.d.ts +541 -0
  136. package/dist/lib/telegram-api.js +1159 -0
  137. package/dist/lib/text-groups.d.ts +89 -0
  138. package/dist/lib/text-groups.js +317 -0
  139. package/dist/lib/thread-cleanup-manager.d.ts +288 -0
  140. package/dist/lib/thread-cleanup-manager.js +560 -0
  141. package/dist/lib/thread-display.d.ts +46 -0
  142. package/dist/lib/thread-display.js +257 -0
  143. package/dist/lib/thread-naming.d.ts +46 -0
  144. package/dist/lib/thread-naming.js +78 -0
  145. package/dist/lib/thread-reconciler.d.ts +239 -0
  146. package/dist/lib/thread-reconciler.js +644 -0
  147. package/dist/lib/threads.d.ts +621 -0
  148. package/dist/lib/threads.js +3679 -0
  149. package/dist/lib/time-injection.d.ts +15 -0
  150. package/dist/lib/time-injection.js +56 -0
  151. package/dist/lib/turns.d.ts +109 -0
  152. package/dist/lib/turns.js +500 -0
  153. package/dist/lib/updates.d.ts +1290 -0
  154. package/dist/lib/updates.js +3634 -0
  155. package/dist/lib/voice.d.ts +117 -0
  156. package/dist/lib/voice.js +174 -0
  157. package/dist/lib/workspace-admission.d.ts +264 -0
  158. package/dist/lib/workspace-admission.js +1136 -0
  159. package/dist/lib/workspace-retirement.d.ts +219 -0
  160. package/dist/lib/workspace-retirement.js +587 -0
  161. package/dist/lib/workspace-slots.d.ts +30 -0
  162. package/dist/lib/workspace-slots.js +54 -0
  163. package/dist/package.json +126 -0
  164. package/dist/pi-telegram/index.js +1 -0
  165. package/dist/skills/generated-control-surface/SKILL.md +107 -0
  166. package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
  167. package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
  168. package/dist/skills/generative-apps/SKILL.md +115 -0
  169. package/dist/skills/show-me/SKILL.md +166 -0
  170. package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
  171. package/dist/skills/telegram-bridge/SKILL.md +129 -0
  172. package/dist/skills/telegram-bridge/references/configuration.md +15 -0
  173. package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
  174. package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
  175. package/docs/architecture.md +1 -1
  176. package/docs/compact-matrix-literal.md +10 -3
  177. package/docs/generative-apps.md +15 -13
  178. package/docs/multi-instance-bus.md +4 -2
  179. package/docs/outbound.md +1 -1
  180. package/docs/public-api.md +2 -2
  181. package/lib/bindings.ts +89 -6
  182. package/lib/bus-follower.ts +7 -3
  183. package/lib/bus-leader.ts +44 -19
  184. package/lib/bus.ts +4 -1
  185. package/lib/commands.ts +2 -2
  186. package/lib/config.ts +14 -7
  187. package/lib/delivery.ts +38 -6
  188. package/lib/extension.ts +19 -0
  189. package/lib/generative-apps.ts +379 -13
  190. package/lib/menu-settings.ts +21 -5
  191. package/lib/outbound-markup.ts +16 -7
  192. package/lib/telegram-api.ts +12 -1
  193. package/lib/thread-display.ts +96 -18
  194. package/package.json +56 -13
  195. package/scripts/build-dist.mjs +44 -0
  196. package/scripts/measure-bus.mjs +8 -1
  197. package/scripts/measure-workspace.mjs +8 -1
  198. package/skills/generative-apps/SKILL.md +1 -1
  199. package/skills/telegram-bridge/SKILL.md +1 -1
@@ -0,0 +1,2242 @@
1
+ /**
2
+ * Telegram bus leader orchestration
3
+ * Zones: multi-instance bus, leader polling/server lifecycle, follower routing
4
+ * Owns leader-only runtime orchestration: follower registration envelopes, follower API proxying,
5
+ * leader activation hot-switching, local bus server startup, and stale follower pruning.
6
+ */
7
+ import * as Sync from "./sync.js";
8
+ import { createTelegramThreadDisplayReconciler, resolveTelegramInitialWorkspaceDisplayName, resolveTelegramLiveWorkspaceBindingKeys, } from "./thread-display.js";
9
+ import * as ThreadReconciler from "./thread-reconciler.js";
10
+ import { getTelegramApiErrorRequestTarget, isTelegramApiCommitUnknownError, } from "./telegram-api.js";
11
+ import * as Threads from "./threads.js";
12
+ import { createTelegramBusLocalServer, createUnauthorizedBusAck, getTelegramBusEnvelopeTrafficClass, getTelegramBusProtocolCompatibility, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, getTelegramBusFollowerSocketPath, sendTelegramBusLocalEnvelope, stripTelegramBusApiMetadata, TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
13
+ import { getTelegramBusTransportRetryPolicy } from "./bus-transport.js";
14
+ import { createTelegramWorkspaceOperationRuntime, } from "./workspace-retirement.js";
15
+ import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
16
+ export const TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS = 15_000;
17
+ function escapeHtml(text) {
18
+ return text
19
+ .replace(/&/g, "&")
20
+ .replace(/</g, "&lt;")
21
+ .replace(/>/g, "&gt;");
22
+ }
23
+ function formatTelegramBusInstanceLabel(input) {
24
+ const threadName = input.threadName?.trim();
25
+ if (threadName)
26
+ return escapeHtml(threadName);
27
+ return input.slot && /^[A-Z]$/.test(input.slot) ? input.slot : "?";
28
+ }
29
+ export function createTelegramBusLeaderRuntimeAssembly(deps) {
30
+ const runWorkspaceAdmission = deps.getWorkspaceAdmission
31
+ ? (input, operation) => {
32
+ const admission = deps.getWorkspaceAdmission?.();
33
+ if (!admission) {
34
+ throw new Error("Telegram Workspace admission authority is unavailable.");
35
+ }
36
+ return runWithTelegramWorkspaceAdmissionsAsync({
37
+ ledger: admission,
38
+ ...input,
39
+ operation,
40
+ onReleaseError(error) {
41
+ deps.recordRuntimeEvent("bus", error, {
42
+ phase: "workspace-admission-release",
43
+ operationKind: input.operationKind,
44
+ });
45
+ },
46
+ });
47
+ }
48
+ : undefined;
49
+ const runWorkspaceOperation = deps.runWorkspaceOperation ??
50
+ createTelegramWorkspaceOperationRuntime({
51
+ getWorkspaceAdmission: deps.getWorkspaceAdmission,
52
+ onReleaseError(error, operationKind) {
53
+ deps.recordRuntimeEvent("bus", error, {
54
+ phase: "workspace-admission-release",
55
+ operationKind,
56
+ });
57
+ },
58
+ }).run;
59
+ const captureLiveBindingKeys = (bindings) => resolveTelegramLiveWorkspaceBindingKeys(bindings, deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target, deps.runtime.followerRegistry.list());
60
+ const provisionerPorts = {
61
+ getAllowedUserId: deps.getAllowedUserId,
62
+ topicTargetStore: deps.topicTargetStore,
63
+ callApi: deps.callApi,
64
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
65
+ getSyncState: deps.getSyncState,
66
+ setSyncState: deps.setSyncState,
67
+ onProvisioningStart: deps.onProvisioningStart,
68
+ onProvisioningEnd: deps.onProvisioningEnd,
69
+ ...(deps.getThreadDisplayMode
70
+ ? {
71
+ resolveInitialWorkspaceDisplayTitle(binding) {
72
+ return resolveTelegramInitialWorkspaceDisplayName({
73
+ bindings: deps.topicTargetStore.listWorkspaceBindings(),
74
+ binding,
75
+ mode: deps.getThreadDisplayMode(),
76
+ liveBindingKeys: captureLiveBindingKeys(deps.topicTargetStore.listWorkspaceBindings()),
77
+ });
78
+ },
79
+ }
80
+ : {}),
81
+ runWorkspaceOperation,
82
+ recordRuntimeEvent: deps.recordRuntimeEvent,
83
+ };
84
+ const display = deps.getThreadDisplayMode ? createTelegramThreadDisplayReconciler({
85
+ store: deps.topicTargetStore,
86
+ getMode: deps.getThreadDisplayMode,
87
+ getProfileKey: () => deps.getTelegramProfile?.() ?? "default",
88
+ getLeaderEpoch: () => deps.getCurrentLeaderEpoch?.(),
89
+ captureLiveBindingKeys,
90
+ captureBindingAuthority(binding) {
91
+ const matches = (target) => target?.chatId === binding.target.chatId && target?.threadId === binding.target.threadId;
92
+ const leader = deps.topicTargetStore.getActiveByInstanceId(deps.instanceId);
93
+ if (leader && matches(leader.target)) {
94
+ return () => matches(deps.topicTargetStore.getActiveByInstanceId(deps.instanceId)?.target);
95
+ }
96
+ const follower = deps.runtime.followerRegistry.getByTarget(binding.target);
97
+ if (!follower?.registrationGeneration)
98
+ return undefined;
99
+ const generation = follower.registrationGeneration;
100
+ return () => {
101
+ const current = deps.runtime.followerRegistry.get(follower.instanceId);
102
+ return current?.registrationGeneration === generation && matches(current.target);
103
+ };
104
+ },
105
+ callApi: deps.callApi,
106
+ }) : undefined;
107
+ const reconcileThreadDisplayOperation = async () => {
108
+ const result = await display.reconcile();
109
+ deps.onThreadDisplayChanged?.();
110
+ return result;
111
+ };
112
+ const reconcileThreadDisplay = () => runWorkspaceOperation({
113
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
114
+ operationKind: "workspace.reconcile-display",
115
+ scopes: [{ kind: "profile" }],
116
+ }, reconcileThreadDisplayOperation);
117
+ const scheduleDisplay = () => {
118
+ if (!display)
119
+ return;
120
+ void reconcileThreadDisplay().catch((error) => {
121
+ deps.recordRuntimeEvent("bus", error, { phase: "thread-display-reconcile" });
122
+ });
123
+ };
124
+ let modeTail = Promise.resolve();
125
+ const applyThreadDisplayMode = (mode, isCurrent) => {
126
+ const epoch = deps.getCurrentLeaderEpoch?.();
127
+ const profile = deps.getTelegramProfile?.();
128
+ const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
129
+ deps.getTelegramProfile?.() === profile && isCurrent();
130
+ const run = modeTail.then(() => runWorkspaceOperation({
131
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
132
+ operationKind: "workspace.set-display-mode",
133
+ scopes: [{ kind: "profile" }],
134
+ }, async () => {
135
+ if (!display || !deps.persistThreadDisplayMode || !current()) {
136
+ throw new Error("Telegram Thread display setting requires current leader authority.");
137
+ }
138
+ const assertDisplayPeers = () => {
139
+ const requiredCapability = mode === "directory-snake" || mode === "directory-title"
140
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
141
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
142
+ if (mode !== "names" && deps.runtime.followerRegistry.list().some((follower) => !hasTelegramBusCapability(follower.protocol, requiredCapability)))
143
+ throw new Error("Update or restart all connected followers before changing Thread display mode.");
144
+ };
145
+ assertDisplayPeers();
146
+ await deps.persistThreadDisplayMode(mode, current);
147
+ assertDisplayPeers();
148
+ if (!current() || deps.getThreadDisplayMode?.() !== mode) {
149
+ throw new Error("Telegram Thread display preference changed before application.");
150
+ }
151
+ await reconcileThreadDisplayOperation();
152
+ if (!current() || deps.getThreadDisplayMode?.() !== mode) {
153
+ throw new Error("Telegram Thread display application lost its originating authority.");
154
+ }
155
+ }));
156
+ modeTail = run.catch(() => undefined);
157
+ return run;
158
+ };
159
+ const provisionLeaderTarget = createTelegramBusLeaderTargetProvisioner({
160
+ ...provisionerPorts,
161
+ instanceId: deps.instanceId,
162
+ getCwd: deps.getCwd,
163
+ getSessionId: deps.getSessionId,
164
+ getTelegramProfile: deps.getTelegramProfile,
165
+ shouldForceFreshUnnamed: deps.shouldForceFreshUnnamed,
166
+ getRequestedThreadName: deps.getRequestedThreadName,
167
+ getThreadReconciliationMachineState: deps.getThreadReconciliationMachineState,
168
+ recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
169
+ setLeaderTarget: deps.setLeaderTarget,
170
+ });
171
+ const disconnectFollower = createTelegramBusFollowerDisconnectHandler({
172
+ ...provisionerPorts,
173
+ });
174
+ const cleanupConfirmedDeadFollower = createTelegramBusFollowerConfirmedDeadHandler({
175
+ ...provisionerPorts,
176
+ });
177
+ const provisionFollowerTarget = createTelegramBusFollowerTargetProvisioner({
178
+ ...provisionerPorts,
179
+ });
180
+ const renameLeaderThreadAdmitted = async (threadName, expectedTarget) => {
181
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
182
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
183
+ throw new Error("Telegram Workspace Thread rename requires leader ownership.");
184
+ }
185
+ const record = Threads.findCurrentTelegramInstanceThreadRecord({
186
+ records: deps.topicTargetStore.list(),
187
+ instanceId: deps.instanceId,
188
+ });
189
+ if (typeof record?.target.threadId !== "number") {
190
+ throw new Error("No Workspace Thread is bound to this Pi instance.");
191
+ }
192
+ if (expectedTarget && (record.target.chatId !== expectedTarget.chatId ||
193
+ record.target.threadId !== expectedTarget.threadId)) {
194
+ throw new Error("Telegram Workspace Thread rename target changed.");
195
+ }
196
+ const rename = Threads.createTelegramTopicTargetRenamer({
197
+ store: deps.topicTargetStore,
198
+ callApi: deps.callApi,
199
+ assertAuthority() {
200
+ if (deps.getCurrentLeaderEpoch &&
201
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
202
+ throw new Error("Telegram Workspace Thread rename lost leader ownership.");
203
+ }
204
+ const current = Threads.findCurrentTelegramInstanceThreadRecord({
205
+ records: deps.topicTargetStore.list(),
206
+ instanceId: deps.instanceId,
207
+ });
208
+ if (current?.target.chatId !== record.target.chatId ||
209
+ current.target.threadId !== record.target.threadId) {
210
+ throw new Error("Telegram Workspace Thread rename target changed.");
211
+ }
212
+ },
213
+ });
214
+ const renamed = await rename({
215
+ target: {
216
+ chatId: record.target.chatId,
217
+ threadId: record.target.threadId,
218
+ },
219
+ threadName,
220
+ slot: record.slot,
221
+ });
222
+ if (!renamed) {
223
+ throw new Error(Threads.getTelegramTopicThreadNameValidationError(threadName, record.slot) ?? "Telegram Workspace Thread name is already reserved.");
224
+ }
225
+ await deps.topicTargetStore.persist();
226
+ return renamed;
227
+ };
228
+ const resetThreadNameAdmitted = async (target, assertTargetCurrent = () => undefined) => {
229
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
230
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
231
+ throw new Error("Telegram Workspace Thread reset requires leader ownership.");
232
+ }
233
+ const binding = deps.topicTargetStore.listWorkspaceBindings().find((candidate) => candidate.target.chatId === target.chatId &&
234
+ candidate.target.threadId === target.threadId);
235
+ if (!binding)
236
+ throw new Error("No Workspace Thread binding is available to reset.");
237
+ const automaticTitle = resolveTelegramInitialWorkspaceDisplayName({
238
+ bindings: deps.topicTargetStore.listWorkspaceBindings(),
239
+ binding: { ...binding, manualThreadName: undefined },
240
+ mode: deps.getThreadDisplayMode?.() ?? "letters",
241
+ preserveRetainedManualName: false,
242
+ });
243
+ if (!automaticTitle) {
244
+ throw new Error("Telegram Workspace automatic title is unavailable.");
245
+ }
246
+ assertTargetCurrent();
247
+ await deps.callApi("editForumTopic", {
248
+ chat_id: target.chatId,
249
+ message_thread_id: target.threadId,
250
+ name: automaticTitle,
251
+ });
252
+ if (deps.getCurrentLeaderEpoch &&
253
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
254
+ throw new Error("Telegram Workspace Thread reset lost leader ownership.");
255
+ }
256
+ assertTargetCurrent();
257
+ const reset = deps.topicTargetStore.clearManualNameByTarget(target, automaticTitle);
258
+ if (!reset)
259
+ throw new Error("Telegram Workspace Thread reset changed binding.");
260
+ await deps.topicTargetStore.persist();
261
+ return { threadName: automaticTitle };
262
+ };
263
+ const renameLeaderThread = (threadName) => runWorkspaceOperation({
264
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
265
+ operationKind: "workspace.rename-leader",
266
+ scopes: [{ kind: "profile" }],
267
+ }, () => renameLeaderThreadAdmitted(threadName));
268
+ const resetLeaderThreadName = (expectedTarget) => runWorkspaceOperation({
269
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
270
+ operationKind: "workspace.reset-leader-name",
271
+ scopes: [{ kind: "profile" }],
272
+ }, () => {
273
+ const record = Threads.findCurrentTelegramInstanceThreadRecord({
274
+ records: deps.topicTargetStore.list(),
275
+ instanceId: deps.instanceId,
276
+ });
277
+ if (!record || typeof record.target.threadId !== "number") {
278
+ throw new Error("No Workspace Thread is bound to this Pi instance.");
279
+ }
280
+ if (record.target.chatId !== expectedTarget.chatId ||
281
+ record.target.threadId !== expectedTarget.threadId) {
282
+ throw new Error("Telegram Workspace Thread reset target changed.");
283
+ }
284
+ return resetThreadNameAdmitted(record.target, () => {
285
+ const current = Threads.findCurrentTelegramInstanceThreadRecord({
286
+ records: deps.topicTargetStore.list(),
287
+ instanceId: deps.instanceId,
288
+ });
289
+ if (current?.target.chatId !== expectedTarget.chatId ||
290
+ current.target.threadId !== expectedTarget.threadId) {
291
+ throw new Error("Telegram Workspace Thread reset target changed.");
292
+ }
293
+ });
294
+ });
295
+ const runtime = createTelegramBusLeaderRuntime({
296
+ ...deps.runtime,
297
+ applyThreadDisplayMode,
298
+ getThreadDisplayMode: deps.getThreadDisplayMode,
299
+ onFollowerRegistered: scheduleDisplay,
300
+ provisionLeaderTarget: (ctx) => {
301
+ const chatId = deps.getAllowedUserId();
302
+ return runWorkspaceOperation({
303
+ operationId: `leader-provision:${deps.instanceId}`,
304
+ operationKind: "workspace.provision-leader",
305
+ scopes: [
306
+ typeof chatId === "number"
307
+ ? { kind: "chat", chatId }
308
+ : { kind: "profile" },
309
+ ],
310
+ }, () => provisionLeaderTarget(ctx));
311
+ },
312
+ getFollowerDisplayTitle(follower) {
313
+ const binding = deps.topicTargetStore.listWorkspaceBindings().find((binding) => binding.target.chatId === follower.target?.chatId &&
314
+ binding.target.threadId === follower.target?.threadId);
315
+ return binding?.displayTitle ?? binding?.threadName;
316
+ },
317
+ onFollowerDisconnected: async (follower) => {
318
+ await runWorkspaceOperation({
319
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
320
+ operationKind: "workspace.disconnect-follower",
321
+ scopes: [{ kind: "profile" }],
322
+ }, () => disconnectFollower(follower));
323
+ scheduleDisplay();
324
+ },
325
+ async renameFollowerThread(follower, threadName) {
326
+ return runWorkspaceOperation({
327
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
328
+ operationKind: "workspace.rename-follower",
329
+ scopes: [{ kind: "profile" }],
330
+ }, async () => {
331
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
332
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
333
+ throw new Error("Telegram Workspace Thread rename requires leader ownership.");
334
+ }
335
+ if (typeof follower.target?.threadId !== "number") {
336
+ throw new Error("Telegram follower has no bound Workspace Thread.");
337
+ }
338
+ const rename = Threads.createTelegramTopicTargetRenamer({
339
+ store: deps.topicTargetStore,
340
+ callApi: deps.callApi,
341
+ assertAuthority() {
342
+ if (deps.getCurrentLeaderEpoch &&
343
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
344
+ throw new Error("Telegram Workspace Thread rename lost leader ownership.");
345
+ }
346
+ const current = deps.runtime.followerRegistry.get(follower.instanceId);
347
+ if (current?.registrationGeneration !== follower.registrationGeneration ||
348
+ current?.target?.chatId !== follower.target?.chatId ||
349
+ current?.target?.threadId !== follower.target?.threadId) {
350
+ throw new Error("Telegram follower Workspace Thread target changed.");
351
+ }
352
+ },
353
+ });
354
+ const renamed = await rename({
355
+ target: {
356
+ chatId: follower.target.chatId,
357
+ threadId: follower.target.threadId,
358
+ },
359
+ threadName,
360
+ slot: follower.slot,
361
+ });
362
+ if (deps.getCurrentLeaderEpoch &&
363
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
364
+ throw new Error("Telegram Workspace Thread rename lost leader ownership.");
365
+ }
366
+ if (!renamed) {
367
+ throw new Error(Threads.getTelegramTopicThreadNameValidationError(threadName, follower.slot) ?? "Telegram Workspace Thread name is already reserved.");
368
+ }
369
+ await deps.topicTargetStore.persist();
370
+ return { threadName: renamed.manualThreadName ?? threadName };
371
+ });
372
+ },
373
+ resetFollowerThreadName: (follower) => runWorkspaceOperation({
374
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
375
+ operationKind: "workspace.reset-follower-name",
376
+ scopes: [{ kind: "profile" }],
377
+ }, () => {
378
+ if (!follower.target || typeof follower.target.threadId !== "number") {
379
+ throw new Error("Telegram follower has no bound Workspace Thread.");
380
+ }
381
+ const target = {
382
+ chatId: follower.target.chatId,
383
+ threadId: follower.target.threadId,
384
+ };
385
+ return resetThreadNameAdmitted(target, () => {
386
+ const current = deps.runtime.followerRegistry.get(follower.instanceId);
387
+ if (current?.registrationGeneration !== follower.registrationGeneration ||
388
+ current?.target?.chatId !== target.chatId ||
389
+ current?.target?.threadId !== target.threadId) {
390
+ throw new Error("Telegram follower Workspace Thread target changed.");
391
+ }
392
+ });
393
+ }),
394
+ onFollowerConfirmedDead: async (follower) => {
395
+ await runWorkspaceOperation({
396
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
397
+ operationKind: "workspace.cleanup-dead-follower",
398
+ scopes: [{ kind: "profile" }],
399
+ }, () => cleanupConfirmedDeadFollower(follower));
400
+ scheduleDisplay();
401
+ },
402
+ provisionFollowerTarget: (registration, options) => runWorkspaceOperation({
403
+ operationId: `follower-provision:${registration.instanceId}:${registration.registrationGeneration}`,
404
+ operationKind: "workspace.provision-follower",
405
+ scopes: [{ kind: "profile" }],
406
+ }, () => provisionFollowerTarget(registration, options)),
407
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
408
+ runWorkspaceAdmission,
409
+ callApi: createTelegramBusLeaderApiProxy({
410
+ call: deps.callApi,
411
+ callMultipart: deps.callMultipart,
412
+ downloadFile: deps.downloadFile,
413
+ recoverStaleTargetError: deps.recoverStaleTargetError,
414
+ }),
415
+ recordRuntimeEvent: deps.recordRuntimeEvent,
416
+ });
417
+ const assembled = {
418
+ ...runtime,
419
+ runWorkspaceOperation,
420
+ renameLeaderThread,
421
+ renameLeaderThreadAdmitted,
422
+ resetLeaderThreadName,
423
+ resetThreadNameAdmitted,
424
+ captureWorkspaceExternalProtection: deps.captureWorkspaceExternalProtection,
425
+ async startPolling(ctx) {
426
+ await runtime.startPolling(ctx);
427
+ if (display)
428
+ scheduleDisplay();
429
+ },
430
+ };
431
+ if (!display)
432
+ return assembled;
433
+ return {
434
+ ...assembled,
435
+ reconcileThreadDisplay,
436
+ setThreadDisplayMode: (mode) => applyThreadDisplayMode(mode, () => true),
437
+ };
438
+ }
439
+ export function createTelegramBusInstanceLifecycleAnnouncement(input) {
440
+ return {
441
+ target: { ...input.target },
442
+ text: `<b>📡 Instance <i>${formatTelegramBusInstanceLabel(input)}</i> ${input.state}.</b>`,
443
+ parseMode: "HTML",
444
+ };
445
+ }
446
+ const TELEGRAM_BUS_SLOW_FOLLOWER_REGISTRATION_MS = 1000;
447
+ function scheduleTelegramBusLeaderBackgroundTask(task, onError) {
448
+ const timer = setTimeout(() => {
449
+ void task().catch((error) => {
450
+ try {
451
+ onError(error);
452
+ }
453
+ catch {
454
+ // Background diagnostics cannot create an unhandled timer rejection.
455
+ }
456
+ });
457
+ }, 0);
458
+ timer.unref?.();
459
+ }
460
+ function recordSlowTelegramBusFollowerRegistrationStep(deps, input) {
461
+ if (input.elapsedMs < TELEGRAM_BUS_SLOW_FOLLOWER_REGISTRATION_MS)
462
+ return;
463
+ deps.recordRuntimeEvent("bus", `Telegram bus follower registration step was slow (${input.elapsedMs}ms).`, {
464
+ phase: input.phase,
465
+ elapsedMs: input.elapsedMs,
466
+ instanceId: input.instanceId,
467
+ reused: input.reused,
468
+ chatId: input.target?.chatId,
469
+ threadId: input.target?.threadId,
470
+ });
471
+ }
472
+ export function createTelegramBusFollowerTargetProvisioner(deps) {
473
+ const getNowMs = deps.getNowMs ?? Date.now;
474
+ const pendingRegistrations = new Map();
475
+ return async (registration, options) => {
476
+ if (options?.existingWorkspaceBindingOnly && !registration.cwd) {
477
+ return undefined;
478
+ }
479
+ const registrationStartedAtMs = Date.now();
480
+ const chatId = deps.getAllowedUserId();
481
+ if (typeof chatId !== "number")
482
+ return registration.target;
483
+ await deps.topicTargetStore.load();
484
+ let capacityUnavailable = false;
485
+ const workspaceIdentity = registration.cwd
486
+ ? deps.topicTargetStore.claimWorkspaceIdentity(registration.cwd, registration.instanceId, registration.previousInstanceId, {
487
+ existingBindingOnly: options?.existingWorkspaceBindingOnly === true,
488
+ sessionId: registration.sessionId,
489
+ onCapacityUnavailable() {
490
+ capacityUnavailable = true;
491
+ },
492
+ })
493
+ : undefined;
494
+ if (registration.cwd && !workspaceIdentity) {
495
+ if (options?.existingWorkspaceBindingOnly)
496
+ return undefined;
497
+ if (capacityUnavailable) {
498
+ throw new Error("Telegram Workspace slot reservation is unavailable.");
499
+ }
500
+ throw new Error("Telegram Workspace identity is already claimed.");
501
+ }
502
+ const workspaceBinding = workspaceIdentity
503
+ ? deps.topicTargetStore.getWorkspaceBinding(workspaceIdentity.cwd, workspaceIdentity.instanceSlot, workspaceIdentity.sessionId)
504
+ : undefined;
505
+ const provision = Threads.createTelegramTopicTargetProvisioner({
506
+ topicChatId: chatId,
507
+ store: deps.topicTargetStore,
508
+ getNowMs,
509
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
510
+ callApi: deps.callApi,
511
+ // Manual follower registration should create a visible fresh topic unless
512
+ // the same profile key already has a known live/reusable binding. Do not
513
+ // silently claim old offline/failed tabs: they may be closed/deleted in
514
+ // Telegram and therefore invisible to the operator.
515
+ claimPendingTargets: false,
516
+ resolveInitialWorkspaceDisplayTitle: deps.resolveInitialWorkspaceDisplayTitle,
517
+ });
518
+ const recordsBeforeProvision = deps.topicTargetStore.list();
519
+ const followerProfileKey = registration.profileKey ?? `manual:${registration.instanceId}`;
520
+ const requestedTarget = registration.target ?? workspaceBinding?.target;
521
+ const reconnectRecord = recordsBeforeProvision.find((record) => {
522
+ const matchesRequestedTarget = !requestedTarget ||
523
+ (record.target.chatId === requestedTarget.chatId &&
524
+ record.target.threadId === requestedTarget.threadId);
525
+ const matchesCurrentIdentity = record.instanceId === registration.instanceId ||
526
+ record.profileKey === followerProfileKey;
527
+ const matchesSessionHandoff = !!requestedTarget &&
528
+ !!registration.previousInstanceId &&
529
+ record.instanceId === registration.previousInstanceId &&
530
+ matchesRequestedTarget;
531
+ const matchesWorkspaceRecovery = !!workspaceBinding &&
532
+ record.status === "probe-required" &&
533
+ matchesRequestedTarget;
534
+ return (record.owner?.kind === "manual-follower" &&
535
+ ((matchesCurrentIdentity && matchesRequestedTarget) ||
536
+ matchesSessionHandoff || matchesWorkspaceRecovery));
537
+ });
538
+ const followerOwner = Threads.getTelegramThreadOwnerFromProfileKey(followerProfileKey);
539
+ const recoverableTarget = !reconnectRecord &&
540
+ requestedTarget?.chatId === chatId &&
541
+ requestedTarget.threadId !== undefined &&
542
+ !recordsBeforeProvision.some((record) => record.target.chatId === requestedTarget.chatId &&
543
+ record.target.threadId === requestedTarget.threadId)
544
+ ? requestedTarget
545
+ : undefined;
546
+ const recoveryHint = recoverableTarget
547
+ ? deps.topicTargetStore.getFollowerRecoveryHintByTarget?.(recoverableTarget)
548
+ : undefined;
549
+ const registrationKey = workspaceIdentity?.bindingKey ||
550
+ followerProfileKey ||
551
+ registration.instanceId;
552
+ const pendingRegistration = pendingRegistrations.get(registrationKey);
553
+ if (pendingRegistration)
554
+ return pendingRegistration;
555
+ const provisionTarget = async () => {
556
+ deps.onProvisioningStart?.();
557
+ try {
558
+ return await provision({
559
+ instanceId: registration.instanceId,
560
+ owner: followerOwner.kind === "manual-follower"
561
+ ? followerOwner
562
+ : {
563
+ kind: "manual-follower",
564
+ instanceId: registration.instanceId,
565
+ },
566
+ profileKey: followerProfileKey,
567
+ threadName: workspaceBinding?.threadName ?? registration.threadName,
568
+ preferredSlot: workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot,
569
+ ...(workspaceIdentity
570
+ ? {
571
+ workspaceBindingKey: workspaceIdentity.bindingKey,
572
+ workspaceCwd: workspaceIdentity.cwd,
573
+ }
574
+ : {}),
575
+ });
576
+ }
577
+ finally {
578
+ deps.onProvisioningEnd?.();
579
+ }
580
+ };
581
+ const recoverRequestedTarget = async () => {
582
+ const nowMs = getNowMs();
583
+ const carriedThreadName = workspaceBinding?.threadName ?? registration.threadName;
584
+ const requestedThreadName = carriedThreadName &&
585
+ Threads.isTelegramTopicThreadNameValidForSlot(carriedThreadName, registration.slot)
586
+ ? carriedThreadName
587
+ : recoveryHint?.threadName &&
588
+ Threads.isTelegramTopicThreadNameValidForSlot(recoveryHint.threadName, recoveryHint.slot)
589
+ ? recoveryHint.threadName
590
+ : undefined;
591
+ const requestedSlot = workspaceIdentity?.slot ?? workspaceBinding?.slot ?? registration.slot ?? recoveryHint?.slot;
592
+ const recoveredSlot = deps.topicTargetStore.allocateSlot(followerProfileKey, requestedSlot, workspaceIdentity?.bindingKey);
593
+ if (!recoveredSlot) {
594
+ throw new Error("Telegram Workspace slot reservation is unavailable.");
595
+ }
596
+ const recoveredRecord = {
597
+ profileKey: followerProfileKey,
598
+ owner: followerOwner.kind === "manual-follower"
599
+ ? followerOwner
600
+ : {
601
+ kind: "manual-follower",
602
+ instanceId: registration.instanceId,
603
+ },
604
+ target: {
605
+ chatId: recoverableTarget.chatId,
606
+ threadId: recoverableTarget.threadId,
607
+ },
608
+ status: "active",
609
+ createdAtMs: registration.connectedAtMs || nowMs,
610
+ updatedAtMs: nowMs,
611
+ instanceId: registration.instanceId,
612
+ ...(requestedThreadName ? { threadName: requestedThreadName } : {}),
613
+ slot: recoveredSlot,
614
+ lastSyncObservedAtMs: nowMs,
615
+ lastReconcileAction: "follower-live-target-recovery",
616
+ };
617
+ return {
618
+ target: recoveredRecord.target,
619
+ reused: true,
620
+ record: recoveredRecord,
621
+ };
622
+ };
623
+ const runRegistration = async () => {
624
+ const recoveryTarget = reconnectRecord?.target ?? recoverableTarget;
625
+ if (recoveryTarget) {
626
+ Threads.assertTelegramPendingTopicRecoveryAllowed(deps.topicTargetStore, recoveryTarget);
627
+ }
628
+ const pendingTargetRecovery = recoverableTarget && deps.topicTargetStore.listPendingProvisions().some((entry) => entry.target?.chatId === recoverableTarget.chatId &&
629
+ entry.target?.threadId === recoverableTarget.threadId &&
630
+ (entry.instanceId === registration.instanceId || entry.profileKey === followerProfileKey));
631
+ let result = reconnectRecord
632
+ ? {
633
+ target: reconnectRecord.target,
634
+ reused: true,
635
+ record: reconnectRecord,
636
+ }
637
+ : recoverableTarget && !pendingTargetRecovery
638
+ ? await recoverRequestedTarget()
639
+ : await provisionTarget();
640
+ const crossSessionReuse = !!reconnectRecord &&
641
+ reconnectRecord.instanceId !== registration.instanceId;
642
+ if (reconnectRecord && !crossSessionReuse) {
643
+ const nowMs = getNowMs();
644
+ const refreshedRecord = deps.topicTargetStore.upsert({
645
+ ...reconnectRecord,
646
+ instanceId: registration.instanceId,
647
+ updatedAtMs: nowMs,
648
+ lastSyncObservedAtMs: nowMs,
649
+ lastReconcileAction: "follower-register-reuse",
650
+ });
651
+ await deps.topicTargetStore.persist();
652
+ result = {
653
+ target: refreshedRecord.target,
654
+ reused: true,
655
+ record: refreshedRecord,
656
+ };
657
+ }
658
+ const probeRequiredRecord = reconnectRecord?.status === "probe-required";
659
+ const exactSessionHandoff = crossSessionReuse &&
660
+ !!requestedTarget &&
661
+ registration.previousInstanceId === reconnectRecord?.instanceId &&
662
+ requestedTarget.chatId === reconnectRecord.target.chatId &&
663
+ requestedTarget.threadId === reconnectRecord.target.threadId;
664
+ const requiresVisibilityProbe = crossSessionReuse ||
665
+ probeRequiredRecord ||
666
+ recoverableTarget !== undefined;
667
+ let connectedAnnouncement = !result.reused || requiresVisibilityProbe
668
+ ? createTelegramBusInstanceLifecycleAnnouncement({
669
+ target: result.target,
670
+ threadName: result.displayTitle ??
671
+ (workspaceBinding?.target.chatId === result.target.chatId &&
672
+ workspaceBinding.target.threadId === result.target.threadId
673
+ ? workspaceBinding.displayTitle : undefined) ?? result.record.threadName,
674
+ slot: result.record.slot,
675
+ state: "connected",
676
+ })
677
+ : undefined;
678
+ if (requiresVisibilityProbe && connectedAnnouncement) {
679
+ try {
680
+ await deps.callApi(exactSessionHandoff ? "sendChatAction" : "sendMessage", exactSessionHandoff
681
+ ? {
682
+ chat_id: connectedAnnouncement.target.chatId,
683
+ message_thread_id: connectedAnnouncement.target.threadId,
684
+ action: "typing",
685
+ }
686
+ : {
687
+ chat_id: connectedAnnouncement.target.chatId,
688
+ message_thread_id: connectedAnnouncement.target.threadId,
689
+ text: connectedAnnouncement.text,
690
+ parse_mode: connectedAnnouncement.parseMode,
691
+ });
692
+ if (recoverableTarget || probeRequiredRecord) {
693
+ const activatedRecord = deps.topicTargetStore.upsert({
694
+ ...result.record,
695
+ ...(probeRequiredRecord && crossSessionReuse
696
+ ? {
697
+ profileKey: followerProfileKey,
698
+ owner: followerOwner.kind === "manual-follower"
699
+ ? followerOwner
700
+ : {
701
+ kind: "manual-follower",
702
+ instanceId: registration.instanceId,
703
+ },
704
+ instanceId: registration.instanceId,
705
+ }
706
+ : {}),
707
+ status: "active",
708
+ updatedAtMs: getNowMs(),
709
+ lastSyncObservedAtMs: getNowMs(),
710
+ lastReconcileAction: "follower-live-target-recovery",
711
+ });
712
+ await deps.topicTargetStore.persist();
713
+ result = {
714
+ target: activatedRecord.target,
715
+ reused: true,
716
+ record: activatedRecord,
717
+ };
718
+ }
719
+ else if (crossSessionReuse && reconnectRecord) {
720
+ const nowMs = getNowMs();
721
+ const transferredRecord = deps.topicTargetStore.upsert({
722
+ ...reconnectRecord,
723
+ profileKey: followerProfileKey,
724
+ owner: followerOwner.kind === "manual-follower"
725
+ ? followerOwner
726
+ : {
727
+ kind: "manual-follower",
728
+ instanceId: registration.instanceId,
729
+ },
730
+ instanceId: registration.instanceId,
731
+ updatedAtMs: nowMs,
732
+ lastSyncObservedAtMs: nowMs,
733
+ lastReconcileAction: "follower-session-handoff",
734
+ });
735
+ await deps.topicTargetStore.persist();
736
+ result = {
737
+ target: transferredRecord.target,
738
+ reused: true,
739
+ record: transferredRecord,
740
+ };
741
+ }
742
+ connectedAnnouncement = undefined;
743
+ }
744
+ catch (error) {
745
+ if (Threads.isTelegramTopicTargetStaleError(error)) {
746
+ deps.topicTargetStore.markStaleByTarget(result.target, "deleted", error instanceof Error ? error.message : String(error));
747
+ await deps.topicTargetStore.persist();
748
+ result = await provisionTarget();
749
+ connectedAnnouncement =
750
+ createTelegramBusInstanceLifecycleAnnouncement({
751
+ target: result.target,
752
+ threadName: result.displayTitle ?? result.record.threadName,
753
+ slot: result.record.slot,
754
+ state: "connected",
755
+ });
756
+ }
757
+ else {
758
+ deps.recordRuntimeEvent("telegram", error, {
759
+ phase: "follower-topic-reuse-probe",
760
+ instanceId: registration.instanceId,
761
+ chatId: result.target.chatId,
762
+ threadId: result.target.threadId,
763
+ });
764
+ if (recoverableTarget) {
765
+ deps.topicTargetStore.upsert({
766
+ ...result.record,
767
+ status: "probe-required",
768
+ updatedAtMs: getNowMs(),
769
+ lastSyncError: error instanceof Error ? error.message : String(error),
770
+ lastReconcileAction: "follower-visibility-probe-required",
771
+ });
772
+ await deps.topicTargetStore.persist();
773
+ }
774
+ throw error;
775
+ }
776
+ }
777
+ }
778
+ if (workspaceIdentity) {
779
+ const workspaceCommit = Threads.commitTelegramWorkspaceProvisionBinding({
780
+ store: deps.topicTargetStore,
781
+ instanceId: registration.instanceId,
782
+ profileKey: followerProfileKey,
783
+ displayTitle: result.displayTitle,
784
+ binding: {
785
+ ...workspaceIdentity,
786
+ target: { ...result.target },
787
+ ...(result.record.threadName
788
+ ? { threadName: result.record.threadName }
789
+ : {}),
790
+ ...(result.record.slot ? { slot: result.record.slot } : {}),
791
+ journalBindingKeys: [followerProfileKey],
792
+ journalBindingsComplete: true,
793
+ updatedAtMs: getNowMs(),
794
+ },
795
+ });
796
+ if (connectedAnnouncement && workspaceCommit.displayTitle) {
797
+ connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
798
+ target: result.target, threadName: workspaceCommit.displayTitle,
799
+ slot: result.record.slot, state: "connected",
800
+ });
801
+ }
802
+ deps.topicTargetStore.markWorkspaceBindingActiveByTarget(result.target);
803
+ await deps.topicTargetStore.persist();
804
+ }
805
+ deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
806
+ nowMs: getNowMs(),
807
+ action: "follower-register",
808
+ }));
809
+ recordSlowTelegramBusFollowerRegistrationStep(deps, {
810
+ phase: "follower-register-critical",
811
+ elapsedMs: Date.now() - registrationStartedAtMs,
812
+ instanceId: registration.instanceId,
813
+ target: result.target,
814
+ reused: result.reused,
815
+ });
816
+ scheduleTelegramBusLeaderBackgroundTask(async () => {
817
+ const backgroundStartedAtMs = Date.now();
818
+ if (connectedAnnouncement) {
819
+ try {
820
+ await deps.callApi("sendMessage", {
821
+ chat_id: connectedAnnouncement.target.chatId,
822
+ message_thread_id: connectedAnnouncement.target.threadId,
823
+ text: connectedAnnouncement.text,
824
+ parse_mode: connectedAnnouncement.parseMode,
825
+ });
826
+ }
827
+ catch (error) {
828
+ deps.recordRuntimeEvent("telegram", error, {
829
+ phase: "follower-topic-announce",
830
+ instanceId: registration.instanceId,
831
+ chatId: result.target.chatId,
832
+ threadId: result.target.threadId,
833
+ });
834
+ }
835
+ }
836
+ const reconcile = async () => {
837
+ await ThreadReconciler.applyThreadReconciliationPlan(ThreadReconciler.planThreadReconciliation({
838
+ nowMs: getNowMs(),
839
+ currentLeaderEpoch: deps.getCurrentLeaderEpoch?.(),
840
+ records: recordsBeforeProvision,
841
+ pendingProvisions: deps.topicTargetStore.listPendingProvisions(),
842
+ replacedBindings: [
843
+ {
844
+ instanceId: registration.instanceId,
845
+ replacementTarget: result.target,
846
+ },
847
+ ],
848
+ }), {
849
+ isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
850
+ callApi: deps.callApi,
851
+ markStaleByTarget(target, syncStatus, lastSyncError) {
852
+ return deps.topicTargetStore.markStaleByTarget(target, syncStatus, lastSyncError);
853
+ },
854
+ persist() {
855
+ return deps.topicTargetStore.persist();
856
+ },
857
+ removePendingProvisionById(id) {
858
+ return deps.topicTargetStore.removePendingProvision(id);
859
+ },
860
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
861
+ recordRuntimeEvent: deps.recordRuntimeEvent,
862
+ });
863
+ await deps.topicTargetStore.persist();
864
+ };
865
+ try {
866
+ if (deps.runWorkspaceOperation) {
867
+ await deps.runWorkspaceOperation({
868
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
869
+ operationKind: "workspace.reconcile-follower-provision",
870
+ scopes: [{ kind: "profile" }],
871
+ }, reconcile);
872
+ }
873
+ else {
874
+ await reconcile();
875
+ }
876
+ }
877
+ catch (error) {
878
+ deps.recordRuntimeEvent("telegram", error, {
879
+ phase: "follower-register-background-reconcile",
880
+ instanceId: registration.instanceId,
881
+ chatId: result.target.chatId,
882
+ threadId: result.target.threadId,
883
+ });
884
+ }
885
+ recordSlowTelegramBusFollowerRegistrationStep(deps, {
886
+ phase: "follower-register-background",
887
+ elapsedMs: Date.now() - backgroundStartedAtMs,
888
+ instanceId: registration.instanceId,
889
+ target: result.target,
890
+ reused: result.reused,
891
+ });
892
+ }, (error) => {
893
+ deps.recordRuntimeEvent("bus", error, {
894
+ phase: "follower-register-background-owner",
895
+ instanceId: registration.instanceId,
896
+ });
897
+ });
898
+ return {
899
+ ...result.target,
900
+ slot: result.record.slot,
901
+ threadName: result.record.threadName,
902
+ };
903
+ };
904
+ const registrationPromise = runRegistration().finally(() => {
905
+ pendingRegistrations.delete(registrationKey);
906
+ deps.topicTargetStore.releaseWorkspaceClaim(registration.instanceId);
907
+ });
908
+ pendingRegistrations.set(registrationKey, registrationPromise);
909
+ return registrationPromise;
910
+ };
911
+ }
912
+ function createTelegramBusFollowerCleanupHandler(deps, trigger) {
913
+ return async (follower) => {
914
+ const target = follower.target;
915
+ if (!target?.threadId)
916
+ return;
917
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
918
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
919
+ throw new Error("Follower disconnect cleanup requires leader ownership.");
920
+ }
921
+ if (!follower.registrationGeneration) {
922
+ throw new Error("Follower disconnect cleanup requires an exact registration generation.");
923
+ }
924
+ const intent = {
925
+ id: `cleanup:${follower.instanceId}:${follower.registrationGeneration}:${target.chatId}:${target.threadId}`,
926
+ owner: "manual-follower",
927
+ instanceId: follower.instanceId,
928
+ runtimeGeneration: follower.registrationGeneration,
929
+ ...(follower.profileKey ? { profileKey: follower.profileKey } : {}),
930
+ target: { chatId: target.chatId, threadId: target.threadId },
931
+ requestedAtMs: (deps.getNowMs ?? Date.now)(),
932
+ };
933
+ const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === follower.instanceId &&
934
+ record.target.chatId === target.chatId && record.target.threadId === target.threadId);
935
+ const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
936
+ deps.topicTargetStore.upsertPendingCleanup(intent);
937
+ await deps.topicTargetStore.persist();
938
+ const cleanupPlan = ThreadReconciler.planThreadReconciliation({
939
+ nowMs: (deps.getNowMs ?? Date.now)(),
940
+ currentLeaderEpoch: leaderEpoch,
941
+ records: [],
942
+ pendingCleanups: [intent],
943
+ });
944
+ const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(cleanupPlan, {
945
+ isCleanupTargetProtected,
946
+ callApi: deps.callApi,
947
+ markStaleByTarget(target, syncStatus, lastSyncError) {
948
+ const stale = deps.topicTargetStore.markStaleByTarget(target, syncStatus, lastSyncError);
949
+ const inactive = deps.topicTargetStore.markWorkspaceBindingInactiveByTarget(target, (deps.getNowMs ?? Date.now)());
950
+ return stale || inactive;
951
+ },
952
+ removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
953
+ persist: deps.topicTargetStore.persist,
954
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
955
+ recordRuntimeEvent: deps.recordRuntimeEvent,
956
+ });
957
+ if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action)))
958
+ return;
959
+ if (cleanup.incompleteActions?.length) {
960
+ throw new Error("Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.");
961
+ }
962
+ if (deps.getCurrentLeaderEpoch &&
963
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
964
+ throw new Error("Follower disconnect cleanup lost leader ownership.");
965
+ }
966
+ deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
967
+ nowMs: (deps.getNowMs ?? Date.now)(),
968
+ action: trigger === "confirmed-dead"
969
+ ? "manual-follower-confirmed-dead"
970
+ : "manual-follower-disconnect",
971
+ }));
972
+ deps.recordRuntimeEvent("bus", trigger === "confirmed-dead"
973
+ ? "Confirmed-dead Telegram bus follower thread cleaned up"
974
+ : "Telegram bus follower disconnected", {
975
+ phase: trigger === "confirmed-dead"
976
+ ? "follower-confirmed-dead-cleanup"
977
+ : "follower-disconnect",
978
+ instanceId: follower.instanceId,
979
+ chatId: target.chatId,
980
+ threadId: target.threadId,
981
+ });
982
+ };
983
+ }
984
+ export function createTelegramBusFollowerDisconnectHandler(deps) {
985
+ return createTelegramBusFollowerCleanupHandler(deps, "graceful-disconnect");
986
+ }
987
+ export function createTelegramBusFollowerConfirmedDeadHandler(deps) {
988
+ return createTelegramBusFollowerCleanupHandler(deps, "confirmed-dead");
989
+ }
990
+ export function createTelegramBusLeaderTargetProvisioner(deps) {
991
+ const getNowMs = deps.getNowMs ?? Date.now;
992
+ return async (ctx) => {
993
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
994
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
995
+ throw new Error("Telegram leader target provisioning requires ownership.");
996
+ }
997
+ await deps.topicTargetStore.load();
998
+ const cwd = deps.getCwd?.(ctx);
999
+ const normalizedCwd = cwd
1000
+ ? Threads.normalizeTelegramWorkspacePath(cwd)
1001
+ : undefined;
1002
+ const profileKey = Threads.getTelegramThreadOwnerKey({
1003
+ kind: "leader",
1004
+ cwd: normalizedCwd,
1005
+ instanceId: deps.instanceId,
1006
+ telegramProfile: deps.getTelegramProfile?.(),
1007
+ });
1008
+ const reusableOwnRecord = deps.topicTargetStore.getByProfileKey(profileKey);
1009
+ const pendingCleanups = deps.topicTargetStore.listPendingCleanups();
1010
+ const deferredOwnCleanups = reusableOwnRecord?.status === "active"
1011
+ ? pendingCleanups.filter((cleanup) => cleanup.owner === "leader" &&
1012
+ cleanup.target.chatId === reusableOwnRecord.target.chatId &&
1013
+ cleanup.target.threadId === reusableOwnRecord.target.threadId)
1014
+ : [];
1015
+ const deferredOwnCleanupIds = new Set(deferredOwnCleanups.map((cleanup) => cleanup.id));
1016
+ const pendingCleanupPlan = ThreadReconciler.planThreadReconciliation({
1017
+ nowMs: getNowMs(),
1018
+ currentLeaderEpoch: leaderEpoch,
1019
+ previousState: deps.getThreadReconciliationMachineState?.(),
1020
+ records: deps.topicTargetStore.list(),
1021
+ pendingCleanups: pendingCleanups.filter((cleanup) => !deferredOwnCleanupIds.has(cleanup.id)),
1022
+ });
1023
+ deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
1024
+ const cleanupPorts = {
1025
+ isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
1026
+ callApi: deps.callApi,
1027
+ markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
1028
+ removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
1029
+ persist: deps.topicTargetStore.persist,
1030
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
1031
+ recordRuntimeEvent: deps.recordRuntimeEvent,
1032
+ };
1033
+ await ThreadReconciler.applyThreadReconciliationPlan(pendingCleanupPlan, cleanupPorts);
1034
+ deps.onProvisioningStart?.();
1035
+ let ownTarget;
1036
+ try {
1037
+ ownTarget = await Sync.ensureTelegramLeaderThreadBinding({
1038
+ getAllowedUserId: deps.getAllowedUserId,
1039
+ instanceId: deps.instanceId,
1040
+ cwd: normalizedCwd,
1041
+ sessionId: deps.getSessionId?.(ctx),
1042
+ telegramProfile: deps.getTelegramProfile?.(),
1043
+ forceFreshUnnamed: deps.shouldForceFreshUnnamed?.(),
1044
+ requestedThreadName: deps.getRequestedThreadName?.(),
1045
+ resolveInitialWorkspaceDisplayTitle: deps.resolveInitialWorkspaceDisplayTitle,
1046
+ getNowMs,
1047
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
1048
+ getThreadReconciliationMachineState: deps.getThreadReconciliationMachineState,
1049
+ recordThreadReconciliationPlan: deps.recordThreadReconciliationPlan,
1050
+ topicTargetStore: deps.topicTargetStore,
1051
+ callApi: deps.callApi,
1052
+ probeWorkspaceBinding: async (binding) => {
1053
+ const announcement = createTelegramBusInstanceLifecycleAnnouncement({
1054
+ target: binding.target,
1055
+ threadName: binding.displayTitle ?? binding.threadName,
1056
+ slot: binding.slot,
1057
+ state: "connected",
1058
+ });
1059
+ await deps.callApi("sendMessage", {
1060
+ chat_id: announcement.target.chatId,
1061
+ message_thread_id: announcement.target.threadId,
1062
+ text: announcement.text,
1063
+ parse_mode: announcement.parseMode,
1064
+ });
1065
+ },
1066
+ recordEvent: deps.recordRuntimeEvent,
1067
+ });
1068
+ }
1069
+ finally {
1070
+ deps.onProvisioningEnd?.();
1071
+ }
1072
+ if (deferredOwnCleanups.length > 0) {
1073
+ const supersededCleanupPlan = ThreadReconciler.planThreadReconciliation({
1074
+ nowMs: getNowMs(),
1075
+ currentLeaderEpoch: leaderEpoch,
1076
+ previousState: deps.getThreadReconciliationMachineState?.(),
1077
+ records: deps.topicTargetStore.list(),
1078
+ pendingCleanups: deferredOwnCleanups,
1079
+ });
1080
+ deps.recordThreadReconciliationPlan?.(supersededCleanupPlan);
1081
+ await ThreadReconciler.applyThreadReconciliationPlan(supersededCleanupPlan, cleanupPorts);
1082
+ }
1083
+ if (deps.getCurrentLeaderEpoch &&
1084
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
1085
+ throw new Error("Telegram leader target provisioning lost ownership.");
1086
+ }
1087
+ if (!ownTarget)
1088
+ return;
1089
+ deps.setLeaderTarget({
1090
+ target: ownTarget.target,
1091
+ slot: ownTarget.slot,
1092
+ threadName: ownTarget.threadName,
1093
+ });
1094
+ const nowMs = getNowMs();
1095
+ let syncState = deps.getSyncState();
1096
+ syncState = Sync.markTelegramSyncSliceFresh(syncState, "target-bindings", {
1097
+ nowMs,
1098
+ action: "leader-startup",
1099
+ });
1100
+ syncState = Sync.markTelegramSyncSliceFresh(syncState, "reservations", {
1101
+ nowMs,
1102
+ action: "leader-startup",
1103
+ });
1104
+ syncState = Sync.markTelegramSyncSliceFresh(syncState, "topic-capability", {
1105
+ nowMs,
1106
+ action: "leader-startup",
1107
+ });
1108
+ deps.setSyncState(syncState);
1109
+ if (ownTarget.reused)
1110
+ return;
1111
+ const connectedAnnouncement = createTelegramBusInstanceLifecycleAnnouncement({
1112
+ target: ownTarget.target,
1113
+ threadName: ownTarget.displayTitle ?? ownTarget.threadName,
1114
+ slot: ownTarget.slot,
1115
+ state: "connected",
1116
+ });
1117
+ try {
1118
+ await deps.callApi("sendMessage", {
1119
+ chat_id: connectedAnnouncement.target.chatId,
1120
+ message_thread_id: connectedAnnouncement.target.threadId,
1121
+ text: connectedAnnouncement.text,
1122
+ parse_mode: connectedAnnouncement.parseMode,
1123
+ });
1124
+ }
1125
+ catch (error) {
1126
+ deps.recordRuntimeEvent("telegram", error, {
1127
+ phase: "leader-topic-announce",
1128
+ instanceId: deps.instanceId,
1129
+ chatId: ownTarget.target.chatId,
1130
+ threadId: ownTarget.target.threadId,
1131
+ slot: ownTarget.slot,
1132
+ });
1133
+ }
1134
+ };
1135
+ }
1136
+ export function createTelegramBusLeaderApiProxy(deps) {
1137
+ return async (method, args) => {
1138
+ if (method === "call") {
1139
+ const body = stripTelegramBusApiMetadata(args[1]);
1140
+ try {
1141
+ return await deps.call(args[0], body, args[2]);
1142
+ }
1143
+ catch (error) {
1144
+ await deps.recoverStaleTargetError?.(body, error);
1145
+ throw error;
1146
+ }
1147
+ }
1148
+ if (method === "callMultipart") {
1149
+ const fields = args[1];
1150
+ try {
1151
+ return await deps.callMultipart(args[0], fields, args[2], args[3], args[4], args[5]);
1152
+ }
1153
+ catch (error) {
1154
+ await deps.recoverStaleTargetError?.(fields, error);
1155
+ throw error;
1156
+ }
1157
+ }
1158
+ if (method === "downloadFile") {
1159
+ return deps.downloadFile(args[0], args[1]);
1160
+ }
1161
+ throw new Error(`Unsupported Telegram bus API method: ${method}`);
1162
+ };
1163
+ }
1164
+ function createTelegramBusFollowerMutationRunner() {
1165
+ const tails = new Map();
1166
+ return async (follower, operation) => {
1167
+ const key = follower.profileKey
1168
+ ? `profile:${follower.profileKey}`
1169
+ : `instance:${follower.instanceId}`;
1170
+ const previous = tails.get(key);
1171
+ let release;
1172
+ const current = new Promise((resolve) => {
1173
+ release = resolve;
1174
+ });
1175
+ tails.set(key, current);
1176
+ if (previous)
1177
+ await previous;
1178
+ try {
1179
+ return await operation();
1180
+ }
1181
+ finally {
1182
+ release();
1183
+ if (tails.get(key) === current)
1184
+ tails.delete(key);
1185
+ }
1186
+ };
1187
+ }
1188
+ export function createTelegramBusLeaderEnvelopeHandler(deps) {
1189
+ const getNowMs = deps.getNowMs ?? Date.now;
1190
+ const runFollowerMutation = deps.runFollowerMutation ?? createTelegramBusFollowerMutationRunner();
1191
+ const handleAgentRequest = async (envelope) => {
1192
+ const follower = deps.followerRegistry.get(envelope.instanceId);
1193
+ if (!follower) {
1194
+ return {
1195
+ kind: "bus.ack",
1196
+ requestId: envelope.requestId,
1197
+ ok: false,
1198
+ message: "Unknown Telegram bus follower instance.",
1199
+ };
1200
+ }
1201
+ if (!follower.registrationGeneration ||
1202
+ envelope.registrationGeneration !== follower.registrationGeneration) {
1203
+ return {
1204
+ kind: "bus.ack",
1205
+ requestId: envelope.requestId,
1206
+ ok: false,
1207
+ message: "Stale Telegram bus follower registration generation.",
1208
+ };
1209
+ }
1210
+ deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
1211
+ if (envelope.kind === "follower.resolveAgentTarget") {
1212
+ const target = await deps.resolveAgentTarget?.(follower, envelope.selector);
1213
+ return target
1214
+ ? {
1215
+ kind: "bus.ack",
1216
+ requestId: envelope.requestId,
1217
+ ok: true,
1218
+ result: target,
1219
+ }
1220
+ : {
1221
+ kind: "bus.ack",
1222
+ requestId: envelope.requestId,
1223
+ ok: false,
1224
+ message: "Telegram agent target is unavailable or ambiguous.",
1225
+ };
1226
+ }
1227
+ if (!deps.routeAgentMessage) {
1228
+ return {
1229
+ kind: "bus.ack",
1230
+ requestId: envelope.requestId,
1231
+ ok: false,
1232
+ message: "Telegram agent message routing is unavailable.",
1233
+ };
1234
+ }
1235
+ await deps.routeAgentMessage(follower, envelope.message);
1236
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
1237
+ };
1238
+ const routeQueueHandoff = async (envelope) => {
1239
+ const donor = deps.followerRegistry.get(envelope.instanceId);
1240
+ if (!donor) {
1241
+ return {
1242
+ kind: "bus.ack",
1243
+ requestId: envelope.requestId,
1244
+ ok: false,
1245
+ message: "Unknown Telegram bus follower instance.",
1246
+ };
1247
+ }
1248
+ if (!donor.registrationGeneration ||
1249
+ envelope.registrationGeneration !== donor.registrationGeneration) {
1250
+ return {
1251
+ kind: "bus.ack",
1252
+ requestId: envelope.requestId,
1253
+ ok: false,
1254
+ message: "Stale Telegram bus follower registration generation.",
1255
+ };
1256
+ }
1257
+ const recipient = deps.followerRegistry.get(envelope.recipientInstanceId);
1258
+ if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
1259
+ !hasTelegramBusCapability(donor.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
1260
+ !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
1261
+ return {
1262
+ kind: "bus.ack",
1263
+ requestId: envelope.requestId,
1264
+ ok: false,
1265
+ message: "Telegram queue handoff capability was not negotiated.",
1266
+ };
1267
+ }
1268
+ if (!recipient?.registrationGeneration ||
1269
+ envelope.recipientRegistrationGeneration !==
1270
+ recipient.registrationGeneration) {
1271
+ return {
1272
+ kind: "bus.ack",
1273
+ requestId: envelope.requestId,
1274
+ ok: false,
1275
+ message: "Stale Telegram queue handoff recipient registration generation.",
1276
+ };
1277
+ }
1278
+ if (donor.instanceId === recipient.instanceId) {
1279
+ return {
1280
+ kind: "bus.ack",
1281
+ requestId: envelope.requestId,
1282
+ ok: false,
1283
+ message: "Telegram queue handoff recipient must be another runtime.",
1284
+ };
1285
+ }
1286
+ if (deps.routeQueueHandoff) {
1287
+ const result = await deps.routeQueueHandoff(donor, envelope);
1288
+ return {
1289
+ kind: "bus.ack",
1290
+ requestId: envelope.requestId,
1291
+ ok: true,
1292
+ ...(result !== undefined ? { result } : {}),
1293
+ };
1294
+ }
1295
+ const recipientSocketPath = recipient.busSocketPath ??
1296
+ getTelegramBusFollowerSocketPath(recipient.instanceId);
1297
+ const response = await sendTelegramBusLocalEnvelope({
1298
+ socketPath: recipientSocketPath,
1299
+ timeoutMs: deps.timeoutMs,
1300
+ retry: getTelegramBusTransportRetryPolicy({
1301
+ endpoint: recipientSocketPath,
1302
+ operation: "operation",
1303
+ }),
1304
+ envelope: {
1305
+ kind: "leader.offerQueueHandoff",
1306
+ requestId: envelope.requestId,
1307
+ auth: envelope.auth,
1308
+ recipientInstanceId: recipient.instanceId,
1309
+ recipientRegistrationGeneration: recipient.registrationGeneration,
1310
+ donorInstanceId: donor.instanceId,
1311
+ donorProcessId: envelope.donorProcessId,
1312
+ donorProcessBirthId: envelope.donorProcessBirthId,
1313
+ donorSessionGeneration: envelope.donorSessionGeneration,
1314
+ donorAcquisitionId: envelope.donorAcquisitionId,
1315
+ donorAcquiredAtMs: envelope.donorAcquiredAtMs,
1316
+ handoffToken: envelope.handoffToken,
1317
+ payload: envelope.payload,
1318
+ sentAtMs: envelope.sentAtMs,
1319
+ },
1320
+ });
1321
+ if (response?.kind === "bus.ack" && response.ok) {
1322
+ deps.followerRegistry.heartbeat(donor.instanceId, getNowMs());
1323
+ deps.followerRegistry.heartbeat(recipient.instanceId, getNowMs());
1324
+ return {
1325
+ kind: "bus.ack",
1326
+ requestId: envelope.requestId,
1327
+ ok: true,
1328
+ ...(response.result !== undefined ? { result: response.result } : {}),
1329
+ };
1330
+ }
1331
+ return {
1332
+ kind: "bus.ack",
1333
+ requestId: envelope.requestId,
1334
+ ok: false,
1335
+ message: response?.kind === "bus.ack"
1336
+ ? response.message
1337
+ : "Telegram queue handoff recipient did not acknowledge staging.",
1338
+ };
1339
+ };
1340
+ const forwardToFollower = async (envelope) => {
1341
+ const follower = deps.followerRegistry.get(envelope.recipientInstanceId);
1342
+ if (!follower) {
1343
+ return {
1344
+ kind: "bus.ack",
1345
+ requestId: envelope.requestId,
1346
+ ok: false,
1347
+ message: "Unknown Telegram bus follower instance.",
1348
+ };
1349
+ }
1350
+ if (!follower.registrationGeneration ||
1351
+ envelope.recipientRegistrationGeneration !==
1352
+ follower.registrationGeneration) {
1353
+ return {
1354
+ kind: "bus.ack",
1355
+ requestId: envelope.requestId,
1356
+ ok: false,
1357
+ message: "Stale Telegram bus follower registration generation.",
1358
+ };
1359
+ }
1360
+ const followerSocketPath = follower.busSocketPath ??
1361
+ getTelegramBusFollowerSocketPath(envelope.recipientInstanceId);
1362
+ deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
1363
+ try {
1364
+ const response = await sendTelegramBusLocalEnvelope({
1365
+ socketPath: followerSocketPath,
1366
+ envelope,
1367
+ timeoutMs: deps.timeoutMs,
1368
+ retry: getTelegramBusTransportRetryPolicy({
1369
+ endpoint: followerSocketPath,
1370
+ operation: "operation",
1371
+ }),
1372
+ });
1373
+ if (response?.kind === "bus.ack" && response.ok) {
1374
+ deps.followerRegistry.heartbeat(follower.instanceId, getNowMs());
1375
+ return {
1376
+ kind: "bus.ack",
1377
+ requestId: envelope.requestId,
1378
+ ok: true,
1379
+ ...(response.result !== undefined ? { result: response.result } : {}),
1380
+ };
1381
+ }
1382
+ const message = response?.kind === "bus.ack" ? response.message : undefined;
1383
+ return {
1384
+ kind: "bus.ack",
1385
+ requestId: envelope.requestId,
1386
+ ok: false,
1387
+ message: message ?? "Telegram bus follower rejected forwarded update.",
1388
+ };
1389
+ }
1390
+ catch (error) {
1391
+ return {
1392
+ kind: "bus.ack",
1393
+ requestId: envelope.requestId,
1394
+ ok: false,
1395
+ message: error instanceof Error
1396
+ ? error.message
1397
+ : "Telegram bus follower forwarding failed.",
1398
+ };
1399
+ }
1400
+ };
1401
+ return async (envelope) => {
1402
+ const trafficClass = getTelegramBusEnvelopeTrafficClass(envelope);
1403
+ if (trafficClass === "response") {
1404
+ return {
1405
+ kind: "bus.ack",
1406
+ requestId: envelope.requestId,
1407
+ ok: false,
1408
+ message: "Telegram bus response envelope cannot be used as a request.",
1409
+ };
1410
+ }
1411
+ if (!isTelegramBusEnvelopeAuthorized(envelope, deps.authSecret)) {
1412
+ return createUnauthorizedBusAck(envelope.requestId);
1413
+ }
1414
+ switch (envelope.kind) {
1415
+ case "follower.register":
1416
+ case "follower.restoreWorkspace": {
1417
+ const compatibility = getTelegramBusProtocolCompatibility({
1418
+ local: deps.protocolIdentity,
1419
+ remote: envelope.registration.protocol,
1420
+ });
1421
+ const displayCompatible = () => (deps.getThreadDisplayMode?.() ?? "names") === "names" || (hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE) &&
1422
+ hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE));
1423
+ const restoringWorkspace = envelope.kind === "follower.restoreWorkspace";
1424
+ const supportsWorkspaceAutoConnect = hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT) &&
1425
+ hasTelegramBusCapability(envelope.registration.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_FOLLOWER_AUTO_CONNECT);
1426
+ if (!compatibility.compatible || !displayCompatible() ||
1427
+ (envelope.registration.cwd !== undefined &&
1428
+ envelope.registration.sessionId === undefined) ||
1429
+ (restoringWorkspace && !supportsWorkspaceAutoConnect)) {
1430
+ return {
1431
+ kind: "bus.ack",
1432
+ requestId: envelope.requestId,
1433
+ ok: false,
1434
+ protocol: deps.protocolIdentity,
1435
+ error: { code: "incompatible-protocol" },
1436
+ message: `Incompatible Telegram bus protocol: ${compatibility.reason ??
1437
+ (envelope.registration.cwd !== undefined &&
1438
+ envelope.registration.sessionId === undefined
1439
+ ? "missing-session-identity"
1440
+ : "missing-capability")}.`,
1441
+ };
1442
+ }
1443
+ const registrationOperation = () => runFollowerMutation(envelope.registration, async () => {
1444
+ try {
1445
+ if (!envelope.registration.registrationGeneration) {
1446
+ throw new Error("Telegram follower registration requires an exact generation.");
1447
+ }
1448
+ const leaderEpoch = deps.getCurrentLeaderEpoch?.();
1449
+ if (deps.getCurrentLeaderEpoch && leaderEpoch === undefined) {
1450
+ throw new Error("Telegram follower registration requires leader ownership.");
1451
+ }
1452
+ const target = await deps.provisionFollowerTarget?.(envelope.registration, { existingWorkspaceBindingOnly: restoringWorkspace });
1453
+ if (restoringWorkspace && !target) {
1454
+ return {
1455
+ kind: "bus.ack",
1456
+ requestId: envelope.requestId,
1457
+ ok: false,
1458
+ protocol: deps.protocolIdentity,
1459
+ error: { code: "workspace-binding-unavailable" },
1460
+ message: "No remembered Telegram Workspace Thread is available.",
1461
+ };
1462
+ }
1463
+ if (deps.getCurrentLeaderEpoch &&
1464
+ deps.getCurrentLeaderEpoch() !== leaderEpoch) {
1465
+ throw new Error("Telegram follower registration lost leader ownership.");
1466
+ }
1467
+ if (!displayCompatible()) {
1468
+ throw new Error("Thread display mode changed; update or restart this follower before connecting.");
1469
+ }
1470
+ const registeredTarget = target ?? envelope.registration.target;
1471
+ const registeredSlot = target?.slot ?? envelope.registration.slot;
1472
+ if (deps.provisionFollowerTarget &&
1473
+ registeredTarget?.threadId !== undefined &&
1474
+ (!registeredSlot || !/^[A-Z]$/u.test(registeredSlot))) {
1475
+ throw new Error("Telegram Thread slot authority is unavailable.");
1476
+ }
1477
+ deps.followerRegistry.register({
1478
+ ...envelope.registration,
1479
+ connectedAtMs: getNowMs(),
1480
+ target: registeredTarget,
1481
+ ...(registeredSlot ? { slot: registeredSlot } : {}),
1482
+ ...((target?.threadName ?? envelope.registration.threadName)
1483
+ ? {
1484
+ threadName: target?.threadName ?? envelope.registration.threadName,
1485
+ }
1486
+ : {}),
1487
+ });
1488
+ const follower = deps.followerRegistry.get(envelope.registration.instanceId);
1489
+ const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
1490
+ deps.onFollowerRegistered?.();
1491
+ return {
1492
+ kind: "bus.ack",
1493
+ requestId: envelope.requestId,
1494
+ ok: true,
1495
+ protocol: deps.protocolIdentity,
1496
+ ...(registeredTarget ? {
1497
+ result: {
1498
+ ...registeredTarget,
1499
+ ...(displayTitle !== undefined ? { displayTitle } : {}),
1500
+ },
1501
+ } : {}),
1502
+ };
1503
+ }
1504
+ catch (error) {
1505
+ return {
1506
+ kind: "bus.ack",
1507
+ requestId: envelope.requestId,
1508
+ ok: false,
1509
+ protocol: deps.protocolIdentity,
1510
+ message: error instanceof Error
1511
+ ? error.message
1512
+ : "Telegram bus follower target provisioning failed.",
1513
+ };
1514
+ }
1515
+ });
1516
+ if (!deps.runWorkspaceAdmission)
1517
+ return registrationOperation();
1518
+ try {
1519
+ return await deps.runWorkspaceAdmission({
1520
+ operationId: `follower-registration:${envelope.requestId}`,
1521
+ operationKind: "workspace.register-follower",
1522
+ scopes: [{ kind: "profile" }],
1523
+ }, registrationOperation);
1524
+ }
1525
+ catch (error) {
1526
+ return {
1527
+ kind: "bus.ack",
1528
+ requestId: envelope.requestId,
1529
+ ok: false,
1530
+ protocol: deps.protocolIdentity,
1531
+ message: error instanceof Error
1532
+ ? error.message
1533
+ : "Telegram bus follower registration admission failed.",
1534
+ };
1535
+ }
1536
+ }
1537
+ case "follower.offerQueueHandoff":
1538
+ return routeQueueHandoff(envelope);
1539
+ case "follower.setThreadDisplayMode": {
1540
+ const epoch = deps.getCurrentLeaderEpoch?.();
1541
+ const current = () => epoch !== undefined && deps.getCurrentLeaderEpoch?.() === epoch &&
1542
+ deps.followerRegistry.get(envelope.instanceId)?.registrationGeneration === envelope.registrationGeneration;
1543
+ const follower = deps.followerRegistry.get(envelope.instanceId);
1544
+ const requiredCapability = envelope.mode === "directory-snake" || envelope.mode === "directory-title"
1545
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
1546
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
1547
+ if (!current() || !follower?.registrationGeneration || !deps.applyThreadDisplayMode ||
1548
+ !hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
1549
+ !hasTelegramBusCapability(follower.protocol, requiredCapability)) {
1550
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
1551
+ message: "Thread display settings require current registration and compatible leader authority." };
1552
+ }
1553
+ try {
1554
+ await deps.applyThreadDisplayMode(envelope.mode, current);
1555
+ if (!current())
1556
+ throw new Error("Thread display setting completed for a stale registration.");
1557
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result: { mode: envelope.mode } };
1558
+ }
1559
+ catch (error) {
1560
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
1561
+ message: error instanceof Error ? error.message : "Thread display setting was not fully applied." };
1562
+ }
1563
+ }
1564
+ case "follower.renameThread": {
1565
+ const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
1566
+ return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
1567
+ const follower = deps.followerRegistry.get(envelope.instanceId);
1568
+ if (!follower) {
1569
+ return {
1570
+ kind: "bus.ack",
1571
+ requestId: envelope.requestId,
1572
+ ok: false,
1573
+ message: "Unknown Telegram bus follower instance.",
1574
+ };
1575
+ }
1576
+ if (!follower.registrationGeneration ||
1577
+ envelope.registrationGeneration !== follower.registrationGeneration ||
1578
+ follower.target?.chatId !== envelope.target.chatId ||
1579
+ follower.target?.threadId !== envelope.target.threadId) {
1580
+ return {
1581
+ kind: "bus.ack",
1582
+ requestId: envelope.requestId,
1583
+ ok: false,
1584
+ message: "Stale Telegram bus follower registration generation.",
1585
+ };
1586
+ }
1587
+ if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
1588
+ !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) ||
1589
+ !deps.renameFollowerThread) {
1590
+ return {
1591
+ kind: "bus.ack",
1592
+ requestId: envelope.requestId,
1593
+ ok: false,
1594
+ message: "Telegram Workspace Thread rename is unavailable.",
1595
+ };
1596
+ }
1597
+ try {
1598
+ const result = await deps.renameFollowerThread(follower, envelope.threadName);
1599
+ const current = deps.followerRegistry.get(follower.instanceId);
1600
+ if (current?.registrationGeneration !== follower.registrationGeneration) {
1601
+ return {
1602
+ kind: "bus.ack",
1603
+ requestId: envelope.requestId,
1604
+ ok: false,
1605
+ message: "Stale Telegram bus follower registration generation.",
1606
+ };
1607
+ }
1608
+ deps.followerRegistry.register({
1609
+ ...current,
1610
+ threadName: result.threadName,
1611
+ connectedAtMs: current.connectedAtMs,
1612
+ });
1613
+ return {
1614
+ kind: "bus.ack",
1615
+ requestId: envelope.requestId,
1616
+ ok: true,
1617
+ result,
1618
+ };
1619
+ }
1620
+ catch (error) {
1621
+ return {
1622
+ kind: "bus.ack",
1623
+ requestId: envelope.requestId,
1624
+ ok: false,
1625
+ message: error instanceof Error
1626
+ ? error.message
1627
+ : "Telegram Workspace Thread rename failed.",
1628
+ };
1629
+ }
1630
+ });
1631
+ }
1632
+ case "follower.resetThreadName": {
1633
+ const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
1634
+ return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
1635
+ const follower = deps.followerRegistry.get(envelope.instanceId);
1636
+ if (!follower || !follower.registrationGeneration ||
1637
+ envelope.registrationGeneration !== follower.registrationGeneration ||
1638
+ follower.target?.chatId !== envelope.target.chatId ||
1639
+ follower.target?.threadId !== envelope.target.threadId) {
1640
+ return {
1641
+ kind: "bus.ack",
1642
+ requestId: envelope.requestId,
1643
+ ok: false,
1644
+ message: "Stale or unknown Telegram bus follower registration.",
1645
+ };
1646
+ }
1647
+ if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !hasTelegramBusCapability(follower.protocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME) || !deps.resetFollowerThreadName) {
1648
+ return {
1649
+ kind: "bus.ack",
1650
+ requestId: envelope.requestId,
1651
+ ok: false,
1652
+ message: "Telegram Workspace Thread reset is unavailable.",
1653
+ };
1654
+ }
1655
+ try {
1656
+ const result = await deps.resetFollowerThreadName(follower);
1657
+ const current = deps.followerRegistry.get(follower.instanceId);
1658
+ if (current?.registrationGeneration !==
1659
+ follower.registrationGeneration) {
1660
+ return {
1661
+ kind: "bus.ack",
1662
+ requestId: envelope.requestId,
1663
+ ok: false,
1664
+ message: "Stale Telegram bus follower registration generation.",
1665
+ };
1666
+ }
1667
+ deps.followerRegistry.register({
1668
+ ...current,
1669
+ threadName: result.threadName,
1670
+ connectedAtMs: current.connectedAtMs,
1671
+ });
1672
+ return {
1673
+ kind: "bus.ack",
1674
+ requestId: envelope.requestId,
1675
+ ok: true,
1676
+ result,
1677
+ };
1678
+ }
1679
+ catch (error) {
1680
+ return {
1681
+ kind: "bus.ack",
1682
+ requestId: envelope.requestId,
1683
+ ok: false,
1684
+ message: error instanceof Error
1685
+ ? error.message : "Telegram Workspace Thread reset failed.",
1686
+ };
1687
+ }
1688
+ });
1689
+ }
1690
+ case "follower.disconnect": {
1691
+ const registeredFollower = deps.followerRegistry.get(envelope.instanceId);
1692
+ return runFollowerMutation(registeredFollower ?? { instanceId: envelope.instanceId }, async () => {
1693
+ const follower = deps.followerRegistry.get(envelope.instanceId);
1694
+ if (!follower) {
1695
+ return {
1696
+ kind: "bus.ack",
1697
+ requestId: envelope.requestId,
1698
+ ok: false,
1699
+ message: "Unknown Telegram bus follower instance.",
1700
+ };
1701
+ }
1702
+ if (!follower.registrationGeneration ||
1703
+ !envelope.registrationGeneration ||
1704
+ envelope.registrationGeneration !== follower.registrationGeneration) {
1705
+ return {
1706
+ kind: "bus.ack",
1707
+ requestId: envelope.requestId,
1708
+ ok: false,
1709
+ message: "Stale Telegram bus follower registration generation.",
1710
+ };
1711
+ }
1712
+ await deps.onFollowerDisconnected?.(follower);
1713
+ const current = deps.followerRegistry.get(follower.instanceId);
1714
+ if (current?.registrationGeneration !== follower.registrationGeneration) {
1715
+ return {
1716
+ kind: "bus.ack",
1717
+ requestId: envelope.requestId,
1718
+ ok: false,
1719
+ message: "Stale Telegram bus follower registration generation.",
1720
+ };
1721
+ }
1722
+ deps.followerRegistry.remove(follower.instanceId);
1723
+ return {
1724
+ kind: "bus.ack",
1725
+ requestId: envelope.requestId,
1726
+ ok: true,
1727
+ };
1728
+ });
1729
+ }
1730
+ case "follower.heartbeat": {
1731
+ const current = deps.followerRegistry.get(envelope.instanceId);
1732
+ if (!current) {
1733
+ return {
1734
+ kind: "bus.ack",
1735
+ requestId: envelope.requestId,
1736
+ ok: false,
1737
+ message: "Unknown Telegram bus follower instance.",
1738
+ };
1739
+ }
1740
+ if (!current.registrationGeneration ||
1741
+ envelope.registrationGeneration !== current.registrationGeneration) {
1742
+ return {
1743
+ kind: "bus.ack",
1744
+ requestId: envelope.requestId,
1745
+ ok: false,
1746
+ message: "Stale Telegram bus follower registration generation.",
1747
+ };
1748
+ }
1749
+ const follower = deps.followerRegistry.heartbeat(envelope.instanceId, getNowMs());
1750
+ const displayTitle = follower ? deps.getFollowerDisplayTitle?.(follower) : undefined;
1751
+ return follower
1752
+ ? {
1753
+ kind: "bus.ack",
1754
+ requestId: envelope.requestId,
1755
+ ok: true,
1756
+ result: {
1757
+ ...(displayTitle !== undefined ? { displayTitle } : {}),
1758
+ eligibleElectionSlots: deps.followerRegistry
1759
+ .list()
1760
+ .filter((candidate) => !deps.protocolIdentity.capabilities.includes(TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION) ||
1761
+ candidate.protocol?.capabilities.includes(TELEGRAM_BUS_CAPABILITY_DURABLE_FOLLOWER_ADMISSION))
1762
+ .map((candidate) => candidate.slot)
1763
+ .filter((slot) => typeof slot === "string" && /^[A-Z]$/.test(slot))
1764
+ .sort(),
1765
+ },
1766
+ }
1767
+ : {
1768
+ kind: "bus.ack",
1769
+ requestId: envelope.requestId,
1770
+ ok: false,
1771
+ message: "Unknown Telegram bus follower instance.",
1772
+ };
1773
+ }
1774
+ case "follower.resolveAgentTarget":
1775
+ case "follower.routeAgentMessage":
1776
+ return handleAgentRequest(envelope);
1777
+ case "leader.forwardCallback":
1778
+ case "leader.forwardReaction":
1779
+ case "leader.forwardMessage":
1780
+ case "leader.forwardEditedMessage":
1781
+ return forwardToFollower(envelope);
1782
+ case "follower.callApi":
1783
+ return handleFollowerApiCall(envelope, { ...deps, getNowMs });
1784
+ default:
1785
+ return {
1786
+ kind: "bus.ack",
1787
+ requestId: envelope.requestId,
1788
+ ok: false,
1789
+ message: "Telegram bus envelope is not handled by this leader.",
1790
+ };
1791
+ }
1792
+ };
1793
+ }
1794
+ function asRecord(value) {
1795
+ return value && typeof value === "object" && !Array.isArray(value)
1796
+ ? value
1797
+ : undefined;
1798
+ }
1799
+ function asInteger(value) {
1800
+ if (typeof value === "number" && Number.isInteger(value))
1801
+ return value;
1802
+ if (typeof value !== "string" || value.trim() === "")
1803
+ return undefined;
1804
+ const parsed = Number(value);
1805
+ return Number.isInteger(parsed) ? parsed : undefined;
1806
+ }
1807
+ function getFollowerApiMethodAndBody(envelope) {
1808
+ if (envelope.method === "call" || envelope.method === "callMultipart") {
1809
+ return {
1810
+ apiMethod: typeof envelope.args[0] === "string" ? envelope.args[0] : "",
1811
+ body: asRecord(envelope.args[1]),
1812
+ };
1813
+ }
1814
+ return { apiMethod: envelope.method, body: asRecord(envelope.args[0]) };
1815
+ }
1816
+ function getSentMessageIds(result) {
1817
+ const values = Array.isArray(result) ? result : [result];
1818
+ return values
1819
+ .map((value) => asInteger(asRecord(value)?.message_id))
1820
+ .filter((messageId) => messageId !== undefined);
1821
+ }
1822
+ function recordFollowerApiMessageOwnership(input) {
1823
+ if (!input.record)
1824
+ return;
1825
+ const { apiMethod, body } = getFollowerApiMethodAndBody(input.envelope);
1826
+ if (apiMethod !== "sendMessage" &&
1827
+ apiMethod !== "sendRichMessage" &&
1828
+ apiMethod !== "sendPhoto" &&
1829
+ apiMethod !== "sendDocument" &&
1830
+ apiMethod !== "sendVoice" &&
1831
+ apiMethod !== "sendMediaGroup") {
1832
+ return;
1833
+ }
1834
+ const chatId = asInteger(body?.chat_id) ?? input.follower.target?.chatId;
1835
+ if (chatId === undefined)
1836
+ return;
1837
+ const threadId = asInteger(body?.message_thread_id) ?? input.follower.target?.threadId;
1838
+ const target = threadId !== undefined ? { chatId, threadId } : { chatId };
1839
+ for (const messageId of getSentMessageIds(input.result)) {
1840
+ input.record({
1841
+ follower: input.follower,
1842
+ chatId,
1843
+ messageId,
1844
+ target,
1845
+ });
1846
+ }
1847
+ }
1848
+ async function handleFollowerApiCall(envelope, deps) {
1849
+ const follower = deps.followerRegistry.get(envelope.instanceId);
1850
+ if (!follower) {
1851
+ return {
1852
+ kind: "bus.ack",
1853
+ requestId: envelope.requestId,
1854
+ ok: false,
1855
+ message: "Unknown Telegram bus follower instance.",
1856
+ };
1857
+ }
1858
+ if (!follower.registrationGeneration ||
1859
+ envelope.registrationGeneration !== follower.registrationGeneration) {
1860
+ return {
1861
+ kind: "bus.ack",
1862
+ requestId: envelope.requestId,
1863
+ ok: false,
1864
+ message: "Stale Telegram bus follower registration generation.",
1865
+ };
1866
+ }
1867
+ deps.followerRegistry.heartbeat(envelope.instanceId, deps.getNowMs());
1868
+ if (deps.authorizeFollowerApiCall &&
1869
+ !deps.authorizeFollowerApiCall({
1870
+ follower,
1871
+ method: envelope.method,
1872
+ args: envelope.args,
1873
+ })) {
1874
+ return {
1875
+ kind: "bus.ack",
1876
+ requestId: envelope.requestId,
1877
+ ok: false,
1878
+ message: "Telegram bus API call is not allowed for this follower.",
1879
+ };
1880
+ }
1881
+ if (!deps.callApi) {
1882
+ return {
1883
+ kind: "bus.ack",
1884
+ requestId: envelope.requestId,
1885
+ ok: false,
1886
+ message: "Telegram bus leader does not expose API calling.",
1887
+ };
1888
+ }
1889
+ try {
1890
+ const result = await deps.callApi(envelope.method, envelope.args);
1891
+ recordFollowerApiMessageOwnership({
1892
+ envelope,
1893
+ follower,
1894
+ result,
1895
+ record: deps.recordFollowerMessageOwnership,
1896
+ });
1897
+ return {
1898
+ kind: "bus.ack",
1899
+ requestId: envelope.requestId,
1900
+ ok: true,
1901
+ result,
1902
+ };
1903
+ }
1904
+ catch (error) {
1905
+ const staleTarget = Threads.isTelegramTopicTargetStaleError(error)
1906
+ ? getTelegramApiErrorRequestTarget(error)
1907
+ : undefined;
1908
+ return {
1909
+ kind: "bus.ack",
1910
+ requestId: envelope.requestId,
1911
+ ok: false,
1912
+ message: error instanceof Error
1913
+ ? error.message
1914
+ : "Telegram bus API call failed.",
1915
+ ...(staleTarget
1916
+ ? {
1917
+ error: {
1918
+ code: "stale-target",
1919
+ chatId: staleTarget.chatId,
1920
+ threadId: staleTarget.threadId,
1921
+ },
1922
+ }
1923
+ : isTelegramApiCommitUnknownError(error)
1924
+ ? {
1925
+ error: {
1926
+ code: "commit-unknown",
1927
+ method: error.method,
1928
+ },
1929
+ }
1930
+ : {}),
1931
+ };
1932
+ }
1933
+ }
1934
+ export function createTelegramBusLeaderActivationScheduler(deps) {
1935
+ let pending = false;
1936
+ return (ctx) => {
1937
+ if (deps.isBusPollingStarted() || pending)
1938
+ return;
1939
+ if (!deps.isBusEnabled())
1940
+ return;
1941
+ if (!deps.ownsPolling(ctx))
1942
+ return;
1943
+ pending = true;
1944
+ const timer = setTimeout(() => {
1945
+ void (async () => {
1946
+ try {
1947
+ if (deps.isBusPollingStarted())
1948
+ return;
1949
+ if (!deps.isBusEnabled())
1950
+ return;
1951
+ if (!deps.ownsPolling(ctx))
1952
+ return;
1953
+ await deps.stopClassicPolling();
1954
+ try {
1955
+ await deps.startBusLeaderPolling(ctx);
1956
+ deps.setBusPollingStarted(true);
1957
+ deps.updateStatus(ctx);
1958
+ deps.recordRuntimeEvent?.("bus", "Telegram bus leader mode activated", { phase: "leader-hot-switch" });
1959
+ }
1960
+ catch (error) {
1961
+ deps.recordRuntimeEvent?.("bus", error, {
1962
+ phase: "leader-hot-switch",
1963
+ });
1964
+ deps.setBusPollingStarted(false);
1965
+ await deps.startClassicPolling(ctx);
1966
+ }
1967
+ }
1968
+ finally {
1969
+ pending = false;
1970
+ }
1971
+ })();
1972
+ }, 0);
1973
+ timer.unref?.();
1974
+ };
1975
+ }
1976
+ export function createTelegramBusLeaderRuntime(deps) {
1977
+ const getNowMs = deps.getNowMs ?? Date.now;
1978
+ const followerPruneIntervalMs = deps.followerPruneIntervalMs ?? 1000;
1979
+ const followerStaleAfterMs = deps.followerStaleAfterMs ?? TELEGRAM_BUS_FOLLOWER_STALE_AFTER_MS;
1980
+ const runFollowerMutation = createTelegramBusFollowerMutationRunner();
1981
+ let pruneInterval;
1982
+ let pruneGeneration = 0;
1983
+ let prunePromise;
1984
+ const stopPruning = () => {
1985
+ pruneGeneration += 1;
1986
+ if (pruneInterval)
1987
+ clearInterval(pruneInterval);
1988
+ pruneInterval = undefined;
1989
+ prunePromise = undefined;
1990
+ };
1991
+ const recordPruneEvent = (error, details) => {
1992
+ try {
1993
+ deps.recordRuntimeEvent?.("bus", error, details);
1994
+ }
1995
+ catch {
1996
+ // Prune diagnostics cannot replace lifecycle-owned reconciliation.
1997
+ }
1998
+ };
1999
+ const pruneFollowers = async (expectedGeneration) => {
2000
+ const isCurrent = () => pruneGeneration === expectedGeneration;
2001
+ try {
2002
+ await localServer.ensureEndpoint();
2003
+ }
2004
+ catch (error) {
2005
+ recordPruneEvent(error, {
2006
+ phase: "leader-endpoint-recovery",
2007
+ });
2008
+ }
2009
+ if (!isCurrent())
2010
+ return;
2011
+ const removed = deps.followerRegistry.pruneStale(getNowMs(), followerStaleAfterMs);
2012
+ for (const follower of removed) {
2013
+ let processConfirmedDead = false;
2014
+ if (follower.pid !== undefined && deps.isFollowerProcessAlive) {
2015
+ try {
2016
+ processConfirmedDead = !deps.isFollowerProcessAlive(follower.pid);
2017
+ }
2018
+ catch (error) {
2019
+ recordPruneEvent(error, {
2020
+ phase: "follower-process-liveness",
2021
+ instanceId: follower.instanceId,
2022
+ pid: follower.pid,
2023
+ });
2024
+ }
2025
+ }
2026
+ if (!processConfirmedDead) {
2027
+ recordPruneEvent("Telegram bus follower heartbeat stale; preserving thread binding", {
2028
+ phase: "follower-pruned",
2029
+ instanceId: follower.instanceId,
2030
+ processLiveness: follower.pid === undefined || !deps.isFollowerProcessAlive
2031
+ ? "unknown"
2032
+ : "alive-or-unknown",
2033
+ });
2034
+ continue;
2035
+ }
2036
+ let cleanupEnabled = false;
2037
+ try {
2038
+ cleanupEnabled =
2039
+ (await deps.shouldCleanupConfirmedDeadFollower?.()) ?? false;
2040
+ }
2041
+ catch (error) {
2042
+ recordPruneEvent(error, {
2043
+ phase: "follower-confirmed-dead-cleanup-policy",
2044
+ instanceId: follower.instanceId,
2045
+ pid: follower.pid,
2046
+ });
2047
+ }
2048
+ if (!isCurrent())
2049
+ return;
2050
+ if (!cleanupEnabled || !deps.onFollowerConfirmedDead) {
2051
+ recordPruneEvent("Telegram bus follower process confirmed dead; preserving thread binding", {
2052
+ phase: "follower-confirmed-dead-preserved",
2053
+ instanceId: follower.instanceId,
2054
+ pid: follower.pid,
2055
+ cleanupEnabled,
2056
+ });
2057
+ continue;
2058
+ }
2059
+ try {
2060
+ await runFollowerMutation(follower, async () => {
2061
+ if (!isCurrent())
2062
+ return;
2063
+ const replacement = deps.followerRegistry.list().find((candidate) => follower.profileKey
2064
+ ? candidate.profileKey === follower.profileKey
2065
+ : candidate.instanceId === follower.instanceId);
2066
+ if (replacement) {
2067
+ recordPruneEvent("Telegram bus follower replaced before confirmed-dead cleanup; preserving thread binding", {
2068
+ phase: "follower-confirmed-dead-replaced",
2069
+ instanceId: follower.instanceId,
2070
+ replacementInstanceId: replacement.instanceId,
2071
+ });
2072
+ return;
2073
+ }
2074
+ await deps.onFollowerConfirmedDead(follower);
2075
+ });
2076
+ }
2077
+ catch (error) {
2078
+ recordPruneEvent(error, {
2079
+ phase: "follower-confirmed-dead-cleanup",
2080
+ instanceId: follower.instanceId,
2081
+ pid: follower.pid,
2082
+ chatId: follower.target?.chatId,
2083
+ threadId: follower.target?.threadId,
2084
+ });
2085
+ }
2086
+ }
2087
+ };
2088
+ const requestPrune = () => {
2089
+ if (prunePromise)
2090
+ return prunePromise;
2091
+ const expectedGeneration = pruneGeneration;
2092
+ let tracked;
2093
+ tracked = pruneFollowers(expectedGeneration)
2094
+ .catch((error) => {
2095
+ if (pruneGeneration === expectedGeneration) {
2096
+ recordPruneEvent(error, { phase: "follower-prune-owner" });
2097
+ }
2098
+ })
2099
+ .finally(() => {
2100
+ if (prunePromise === tracked)
2101
+ prunePromise = undefined;
2102
+ });
2103
+ prunePromise = tracked;
2104
+ return tracked;
2105
+ };
2106
+ const startPruning = () => {
2107
+ stopPruning();
2108
+ pruneInterval = setInterval(() => {
2109
+ void requestPrune();
2110
+ }, followerPruneIntervalMs);
2111
+ pruneInterval.unref?.();
2112
+ };
2113
+ const handleEnvelope = createTelegramBusLeaderEnvelopeHandler({
2114
+ followerRegistry: deps.followerRegistry,
2115
+ authSecret: deps.authSecret,
2116
+ protocolIdentity: deps.protocolIdentity,
2117
+ getNowMs,
2118
+ callApi: deps.callApi,
2119
+ authorizeFollowerApiCall: deps.authorizeFollowerApiCall,
2120
+ recordFollowerMessageOwnership: deps.recordFollowerMessageOwnership,
2121
+ resolveAgentTarget: deps.resolveAgentTarget,
2122
+ routeAgentMessage: deps.routeAgentMessage,
2123
+ routeQueueHandoff: deps.routeQueueHandoff,
2124
+ provisionFollowerTarget: deps.provisionFollowerTarget,
2125
+ onFollowerDisconnected: deps.onFollowerDisconnected,
2126
+ renameFollowerThread: deps.renameFollowerThread,
2127
+ resetFollowerThreadName: deps.resetFollowerThreadName,
2128
+ getFollowerDisplayTitle: deps.getFollowerDisplayTitle,
2129
+ onFollowerRegistered: deps.onFollowerRegistered,
2130
+ applyThreadDisplayMode: deps.applyThreadDisplayMode,
2131
+ getThreadDisplayMode: deps.getThreadDisplayMode,
2132
+ getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
2133
+ runFollowerMutation,
2134
+ runWorkspaceAdmission: deps.runWorkspaceAdmission,
2135
+ });
2136
+ const routeQueueHandoffEnvelope = async (input) => {
2137
+ const recipient = deps.followerRegistry.get(input.recipientInstanceId);
2138
+ if (!hasTelegramBusCapability(deps.protocolIdentity, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF) ||
2139
+ !hasTelegramBusCapability(recipient?.protocol, TELEGRAM_BUS_CAPABILITY_QUEUE_HANDOFF)) {
2140
+ return {
2141
+ kind: "bus.ack",
2142
+ requestId: input.requestId,
2143
+ ok: false,
2144
+ message: "Telegram queue handoff capability was not negotiated.",
2145
+ };
2146
+ }
2147
+ if (!recipient?.registrationGeneration ||
2148
+ input.recipientRegistrationGeneration !==
2149
+ recipient.registrationGeneration) {
2150
+ return {
2151
+ kind: "bus.ack",
2152
+ requestId: input.requestId,
2153
+ ok: false,
2154
+ message: "Stale Telegram queue handoff recipient registration generation.",
2155
+ };
2156
+ }
2157
+ const recipientSocketPath = recipient.busSocketPath ??
2158
+ getTelegramBusFollowerSocketPath(recipient.instanceId);
2159
+ const response = await sendTelegramBusLocalEnvelope({
2160
+ socketPath: recipientSocketPath,
2161
+ timeoutMs: deps.timeoutMs,
2162
+ retry: getTelegramBusTransportRetryPolicy({
2163
+ endpoint: recipientSocketPath,
2164
+ operation: "operation",
2165
+ }),
2166
+ envelope: {
2167
+ kind: "leader.offerQueueHandoff",
2168
+ requestId: input.requestId,
2169
+ auth: input.auth,
2170
+ recipientInstanceId: recipient.instanceId,
2171
+ recipientRegistrationGeneration: recipient.registrationGeneration,
2172
+ donorInstanceId: input.donorInstanceId,
2173
+ donorProcessId: input.donorProcessId,
2174
+ donorProcessBirthId: input.donorProcessBirthId,
2175
+ donorSessionGeneration: input.donorSessionGeneration,
2176
+ donorAcquisitionId: input.donorAcquisitionId,
2177
+ donorAcquiredAtMs: input.donorAcquiredAtMs,
2178
+ handoffToken: input.handoffToken,
2179
+ payload: input.payload,
2180
+ sentAtMs: input.sentAtMs,
2181
+ },
2182
+ });
2183
+ if (response?.kind === "bus.ack" && response.ok) {
2184
+ deps.followerRegistry.heartbeat(recipient.instanceId, getNowMs());
2185
+ return {
2186
+ kind: "bus.ack",
2187
+ requestId: input.requestId,
2188
+ ok: true,
2189
+ ...(response.result !== undefined ? { result: response.result } : {}),
2190
+ };
2191
+ }
2192
+ return {
2193
+ kind: "bus.ack",
2194
+ requestId: input.requestId,
2195
+ ok: false,
2196
+ message: response?.kind === "bus.ack"
2197
+ ? response.message
2198
+ : "Telegram queue handoff recipient did not acknowledge staging.",
2199
+ };
2200
+ };
2201
+ const localServer = createTelegramBusLocalServer({
2202
+ socketPath: deps.socketPath,
2203
+ commitEndpointPublication: deps.commitEndpointPublication,
2204
+ recordTransportEvent(phase, details) {
2205
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
2206
+ phase: `leader-${phase}`,
2207
+ ...details,
2208
+ });
2209
+ },
2210
+ handleEnvelope,
2211
+ });
2212
+ return {
2213
+ routeQueueHandoff: (envelope) => routeQueueHandoffEnvelope(envelope),
2214
+ startPolling: async (ctx) => {
2215
+ // Replay durable cleanup before publishing the follower endpoint so a
2216
+ // replacement registration cannot reclaim a target while it is deleted.
2217
+ await deps.provisionLeaderTarget?.(ctx);
2218
+ await localServer.start();
2219
+ startPruning();
2220
+ try {
2221
+ await deps.startPolling(ctx);
2222
+ }
2223
+ catch (error) {
2224
+ stopPruning();
2225
+ await localServer.stop();
2226
+ throw error;
2227
+ }
2228
+ },
2229
+ stopPolling: async () => {
2230
+ stopPruning();
2231
+ try {
2232
+ await deps.stopPolling();
2233
+ }
2234
+ finally {
2235
+ await localServer
2236
+ .stop()
2237
+ .catch((error) => deps.recordRuntimeEvent?.("bus", error, { phase: "stop" }));
2238
+ deps.followerRegistry.clear();
2239
+ }
2240
+ },
2241
+ };
2242
+ }