@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,1686 @@
1
+ /**
2
+ * Telegram bus follower runtime
3
+ * Zones: multi-instance bus, follower lifecycle, manual registration
4
+ * Owns this Pi instance's follower-side bus behavior: manual registration,
5
+ * heartbeat, forwarded-update receiving, and follower-routed API calls.
6
+ * It must not spawn Pi processes or create hidden Telegram-originated instances.
7
+ */
8
+ import { basename } from "node:path";
9
+ import * as Sync from "./sync.js";
10
+ import * as Threads from "./threads.js";
11
+ import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
12
+ import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
13
+ import { createTelegramBusFollowerDeliveryIdentity, createTelegramBusFollowerTargetController, createTelegramBusForeignOwnedUpdateForwarder, createTelegramBusLocalServer, createTelegramBusRequestIdFactory, createUnauthorizedBusAck, getTelegramBusProtocolCompatibility, getTelegramBusSocketPath, hasTelegramBusCapability, isTelegramBusEnvelopeAuthorized, resolveTelegramBusSocketPath, sendTelegramBusLocalEnvelope, TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME, TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE, TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT, } from "./bus.js";
14
+ import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
15
+ import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
16
+ export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
17
+ export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
18
+ export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
19
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
20
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
21
+ export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
22
+ const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY = "__piTelegramFollowerSessionHandoff";
23
+ export function getTelegramFollowerSessionHandoff() {
24
+ const value = globalThis[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY];
25
+ if (!value || typeof value !== "object")
26
+ return undefined;
27
+ const handoff = value;
28
+ if (typeof handoff.pid !== "number" ||
29
+ typeof handoff.instanceId !== "string" ||
30
+ typeof handoff.createdAtMs !== "number" ||
31
+ !handoff.target ||
32
+ typeof handoff.target !== "object" ||
33
+ typeof handoff.target.chatId !== "number") {
34
+ return undefined;
35
+ }
36
+ return handoff;
37
+ }
38
+ export function setTelegramFollowerSessionHandoff(handoff) {
39
+ const store = globalThis;
40
+ if (!handoff)
41
+ delete store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY];
42
+ else
43
+ store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY] = handoff;
44
+ }
45
+ export function isTelegramFollowerSessionHandoffFresh(handoff, options = {}) {
46
+ if (!handoff)
47
+ return false;
48
+ const pid = options.pid ?? process.pid;
49
+ const nowMs = options.nowMs ?? Date.now();
50
+ const ttlMs = options.ttlMs ?? TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS;
51
+ return handoff.pid === pid && nowMs - handoff.createdAtMs <= ttlMs;
52
+ }
53
+ export function createTelegramManualFollowerProfileKeyResolver(input) {
54
+ return () => Threads.getTelegramThreadOwnerKey({
55
+ kind: "manual-follower",
56
+ instanceId: input.manualFollowerOwnerId,
57
+ telegramProfile: input.getActiveProfileName(),
58
+ });
59
+ }
60
+ function runTelegramBusFollowerWorkspaceMutation(deps, operationKind, operation) {
61
+ if (!deps.getWorkspaceAdmission)
62
+ return operation();
63
+ const admission = deps.getWorkspaceAdmission();
64
+ if (!admission) {
65
+ throw new Error("Telegram Workspace admission authority is unavailable.");
66
+ }
67
+ return runWithTelegramWorkspaceAdmissionsAsync({
68
+ ledger: admission,
69
+ operationId: createTelegramWorkspaceAdmissionOperationId(),
70
+ operationKind,
71
+ scopes: [{ kind: "profile" }],
72
+ operation,
73
+ onReleaseError(error) {
74
+ deps.recordRuntimeEvent?.("bus", error, {
75
+ phase: "workspace-admission-release",
76
+ operationKind,
77
+ });
78
+ },
79
+ });
80
+ }
81
+ export function createTelegramBusFollowerPromotionHandler(input) {
82
+ return (ctx, binding, election) => runTelegramBusFollowerWorkspaceMutation(input, "workspace.promote-follower", async () => {
83
+ let promotedRecord;
84
+ const promoted = await input.startLeader(ctx, election, async () => {
85
+ promotedRecord =
86
+ await Threads.promoteTelegramFollowerBindingToLeader({
87
+ store: input.topicTargetStore,
88
+ instanceId: input.instanceId,
89
+ cwd: ctx.cwd,
90
+ sessionId: input.getSessionId?.(ctx),
91
+ telegramProfile: input.getActiveProfileName(),
92
+ target: binding.target,
93
+ slot: binding.slot,
94
+ threadName: binding.threadName,
95
+ });
96
+ if (!promotedRecord && typeof binding.target?.threadId === "number") {
97
+ throw new Error("Telegram follower promotion slot authority is unavailable.");
98
+ }
99
+ if (promotedRecord) {
100
+ input.recordRuntimeEvent("bus", "Follower thread binding promoted to leader", {
101
+ phase: "follower-promoted-binding",
102
+ chatId: promotedRecord.target.chatId,
103
+ threadId: promotedRecord.target.threadId,
104
+ slot: promotedRecord.slot,
105
+ threadName: promotedRecord.threadName,
106
+ });
107
+ }
108
+ });
109
+ if (promoted &&
110
+ promotedRecord &&
111
+ typeof binding.target?.threadId === "number") {
112
+ const profileKey = Threads.getTelegramThreadOwnerKey({
113
+ kind: "leader",
114
+ cwd: ctx.cwd,
115
+ instanceId: input.instanceId,
116
+ telegramProfile: input.getActiveProfileName(),
117
+ });
118
+ Threads.setTelegramLeaderSessionHandoff({
119
+ pid: input.getPid?.() ?? process.pid,
120
+ instanceId: input.instanceId,
121
+ createdAtMs: input.getNowMs?.() ?? Date.now(),
122
+ profileKey,
123
+ target: {
124
+ chatId: binding.target.chatId,
125
+ threadId: binding.target.threadId,
126
+ },
127
+ slot: promotedRecord.slot,
128
+ threadName: promotedRecord.threadName,
129
+ });
130
+ input.recordRuntimeEvent("bus", "Promoted leader binding retained for session replacement", {
131
+ phase: "follower-promoted-session-handoff",
132
+ chatId: binding.target.chatId,
133
+ threadId: binding.target.threadId,
134
+ slot: promotedRecord.slot,
135
+ threadName: promotedRecord.threadName,
136
+ });
137
+ }
138
+ return promoted;
139
+ });
140
+ }
141
+ export function createTelegramBusFollowerRuntimeAssembly(ports) {
142
+ const sharedRuntimeDeps = {
143
+ instanceId: ports.instanceId,
144
+ registrationState: ports.registrationState,
145
+ recordRuntimeEvent: ports.recordRuntimeEvent,
146
+ };
147
+ const receiver = createTelegramBusForwardedUpdateReceiverRuntime({
148
+ ...ports.receiver,
149
+ instanceId: ports.instanceId,
150
+ recordRuntimeEvent: ports.recordRuntimeEvent,
151
+ getRegistrationGeneration: ports.registrationState.getGeneration,
152
+ handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
153
+ ...ports.targetReplacement,
154
+ ...sharedRuntimeDeps,
155
+ }),
156
+ });
157
+ let registration;
158
+ const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
159
+ ...ports.recovery,
160
+ registrationState: ports.registrationState,
161
+ recordRuntimeEvent: ports.recordRuntimeEvent,
162
+ getRegistrationRuntime: () => registration,
163
+ });
164
+ registration = createTelegramBusFollowerRegistrationRuntime({
165
+ ...ports.registration,
166
+ ...sharedRuntimeDeps,
167
+ startReceiving: receiver.start,
168
+ stopReceiving: receiver.stop,
169
+ onHeartbeatFailure: recovery,
170
+ });
171
+ return { receiver, registration };
172
+ }
173
+ export function createTelegramBusFollowerTargetReplacementHandler(deps) {
174
+ const getNowMs = deps.getNowMs ?? Date.now;
175
+ return (input, ctx) => runTelegramBusFollowerWorkspaceMutation(deps, "workspace.replace-follower-target", async () => {
176
+ const assertCurrent = (expectedTarget = input.oldTarget) => {
177
+ const target = deps.registrationState.getTarget();
178
+ if (!input.registrationGeneration ||
179
+ deps.registrationState.getGeneration() !==
180
+ input.registrationGeneration ||
181
+ !input.oldTarget ||
182
+ !expectedTarget ||
183
+ target?.chatId !== expectedTarget.chatId ||
184
+ target.threadId !== expectedTarget.threadId ||
185
+ input.target.chatId !== input.oldTarget.chatId ||
186
+ input.target.threadId === input.oldTarget.threadId) {
187
+ throw new Error("Stale Telegram follower target replacement authority.");
188
+ }
189
+ };
190
+ assertCurrent();
191
+ await deps.topicTargetStore.load();
192
+ assertCurrent();
193
+ const nowMs = getNowMs();
194
+ const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
195
+ records: deps.topicTargetStore.list(),
196
+ instanceId: deps.instanceId,
197
+ preferredTarget: input.oldTarget,
198
+ });
199
+ if (currentRecord &&
200
+ (currentRecord.target.chatId !== input.oldTarget.chatId ||
201
+ currentRecord.target.threadId !== input.oldTarget.threadId)) {
202
+ throw new Error("Stale Telegram follower binding replacement target.");
203
+ }
204
+ const currentSlot = currentRecord?.slot ?? deps.registrationState.getSlot();
205
+ if (!currentSlot || !/^[A-Z]$/u.test(currentSlot)) {
206
+ throw new Error("Telegram Thread slot authority is unavailable.");
207
+ }
208
+ deps.topicTargetStore.markStaleByTarget(input.oldTarget, "deleted", "Follower thread was replaced by thread restore.");
209
+ const profileKey = currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
210
+ deps.topicTargetStore.upsert({
211
+ profileKey,
212
+ owner: {
213
+ kind: "manual-follower",
214
+ instanceId: deps.manualFollowerOwnerId,
215
+ },
216
+ target: input.target,
217
+ status: "active",
218
+ syncStatus: "open",
219
+ createdAtMs: currentRecord?.createdAtMs ?? nowMs,
220
+ updatedAtMs: nowMs,
221
+ lastSyncObservedAtMs: nowMs,
222
+ lastReconcileAction: "follower-thread-restore",
223
+ instanceId: deps.instanceId,
224
+ slot: currentSlot,
225
+ threadName: currentRecord?.threadName,
226
+ rerouteConfirmedAtMs: nowMs,
227
+ });
228
+ deps.registrationState.setRegistered(true, input.target, {
229
+ slot: currentSlot,
230
+ threadName: currentRecord?.threadName,
231
+ generation: input.registrationGeneration,
232
+ });
233
+ await deps.topicTargetStore.persist();
234
+ assertCurrent(input.target);
235
+ deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
236
+ nowMs,
237
+ action: "follower-thread-restore",
238
+ }));
239
+ deps.updateStatus(ctx);
240
+ deps.recordRuntimeEvent?.("bus", "Telegram follower thread target replaced", {
241
+ phase: "follower-thread-restore",
242
+ chatId: input.target.chatId,
243
+ threadId: input.target.threadId,
244
+ oldThreadId: input.oldTarget?.threadId ?? currentRecord?.target.threadId,
245
+ slot: currentSlot,
246
+ });
247
+ });
248
+ }
249
+ export function createTelegramBusFollowerClientRuntime(deps) {
250
+ const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
251
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
252
+ const sharedClientDeps = {
253
+ socketPath: deps.socketPath,
254
+ createRequestId,
255
+ timeoutMs,
256
+ waitForRegistrationGeneration: deps.waitForRegistrationGeneration,
257
+ };
258
+ return {
259
+ createRequestId,
260
+ callApi: createTelegramBusFollowerApiCaller({
261
+ ...sharedClientDeps,
262
+ instanceId: deps.instanceId,
263
+ getAuthSecret: deps.getApiAuthSecret,
264
+ getRegistrationGeneration: deps.getRegistrationGeneration,
265
+ }),
266
+ agentMessages: createTelegramBusAgentMessageClient({
267
+ ...sharedClientDeps,
268
+ instanceId: deps.instanceId,
269
+ getAuthSecret: deps.getApiAuthSecret,
270
+ getRegistrationGeneration: deps.getRegistrationGeneration,
271
+ }),
272
+ foreignOwnedUpdateForwarder: createTelegramBusForeignOwnedUpdateForwarder({
273
+ ...sharedClientDeps,
274
+ getAuthSecret: deps.getForwardingAuthSecret,
275
+ getForwardCommentBatchPosition: deps.getForwardCommentBatchPosition,
276
+ validateForwardOwnership: deps.validateForwardOwnership,
277
+ recordRuntimeEvent: deps.recordRuntimeEvent,
278
+ }),
279
+ queueHandoff: createTelegramBusFollowerQueueHandoffClient({
280
+ ...sharedClientDeps,
281
+ instanceId: deps.instanceId,
282
+ getAuthSecret: deps.getApiAuthSecret,
283
+ getRegistrationGeneration: deps.getRegistrationGeneration,
284
+ }),
285
+ targetController: createTelegramBusFollowerTargetController({
286
+ ...sharedClientDeps,
287
+ getAuthSecret: deps.getForwardingAuthSecret,
288
+ }),
289
+ };
290
+ }
291
+ export function createTelegramBusFollowerQueueHandoffClient(deps) {
292
+ const getNowMs = deps.getNowMs ?? Date.now;
293
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
294
+ return async (input) => {
295
+ const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
296
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
297
+ const response = await sendTelegramBusLocalEnvelope({
298
+ socketPath,
299
+ timeoutMs: registration.remainingTimeoutMs,
300
+ retry: getTelegramBusTransportRetryPolicy({
301
+ endpoint: socketPath,
302
+ operation: "operation",
303
+ }),
304
+ envelope: {
305
+ kind: "follower.offerQueueHandoff",
306
+ requestId: deps.createRequestId(),
307
+ auth: deps.getAuthSecret?.(),
308
+ instanceId: deps.instanceId,
309
+ registrationGeneration: registration.generation,
310
+ ...input,
311
+ sentAtMs: getNowMs(),
312
+ },
313
+ });
314
+ const queueOwner = response?.kind === "bus.ack" && isRecord(response.result)
315
+ ? parseTelegramUpdateJournalQueueOwner(response.result.queueOwner)
316
+ : undefined;
317
+ if (response?.kind === "bus.ack" &&
318
+ response.ok &&
319
+ isRecord(response.result) &&
320
+ response.result.status === "staged" &&
321
+ typeof response.result.receiptId === "string" &&
322
+ Array.isArray(response.result.sourceUpdateIds) &&
323
+ response.result.sourceUpdateIds.every(Number.isSafeInteger) &&
324
+ input.payload.admissionReceipts.length === 1 &&
325
+ response.result.receiptId ===
326
+ input.payload.admissionReceipts[0]?.receiptId &&
327
+ response.result.sourceUpdateIds.length ===
328
+ input.payload.admissionReceipts[0].sourceUpdateIds.length &&
329
+ response.result.sourceUpdateIds.every((updateId, index) => updateId === input.payload.admissionReceipts[0].sourceUpdateIds[index]) &&
330
+ queueOwner) {
331
+ return {
332
+ status: "staged",
333
+ receiptId: response.result.receiptId,
334
+ sourceUpdateIds: response.result.sourceUpdateIds,
335
+ queueOwner,
336
+ };
337
+ }
338
+ throw new Error(response?.kind === "bus.ack"
339
+ ? response.message ?? "Telegram queue handoff was rejected."
340
+ : "Telegram queue handoff did not return an acknowledgement.");
341
+ };
342
+ }
343
+ export function createTelegramBusAgentMessageClient(deps) {
344
+ const getNowMs = deps.getNowMs ?? Date.now;
345
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
346
+ const request = async (envelope, requestTimeoutMs = timeoutMs) => {
347
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
348
+ const response = await sendTelegramBusLocalEnvelope({
349
+ socketPath,
350
+ timeoutMs: requestTimeoutMs,
351
+ retry: getTelegramBusTransportRetryPolicy({
352
+ endpoint: socketPath,
353
+ operation: "operation",
354
+ }),
355
+ envelope,
356
+ });
357
+ if (response?.kind === "bus.ack" && response.ok)
358
+ return response.result;
359
+ throw new Error(response?.kind === "bus.ack"
360
+ ? response.message ?? "Telegram bus agent message failed."
361
+ : "Telegram bus agent message did not return an acknowledgement.");
362
+ };
363
+ const registrationFields = async () => {
364
+ const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
365
+ return {
366
+ fields: {
367
+ auth: deps.getAuthSecret?.(),
368
+ instanceId: deps.instanceId,
369
+ registrationGeneration: registration.generation,
370
+ },
371
+ remainingTimeoutMs: registration.remainingTimeoutMs,
372
+ };
373
+ };
374
+ return {
375
+ async resolveTarget(selector) {
376
+ const registration = await registrationFields();
377
+ const result = await request({
378
+ kind: "follower.resolveAgentTarget",
379
+ requestId: deps.createRequestId(),
380
+ ...registration.fields,
381
+ selector,
382
+ sentAtMs: getNowMs(),
383
+ }, registration.remainingTimeoutMs);
384
+ if (!result || typeof result !== "object" || Array.isArray(result)) {
385
+ throw new Error("Telegram bus returned an invalid agent target.");
386
+ }
387
+ const target = result;
388
+ if (typeof target.chatId !== "number" ||
389
+ typeof target.threadId !== "number") {
390
+ throw new Error("Telegram bus returned an invalid agent target.");
391
+ }
392
+ return { chatId: target.chatId, threadId: target.threadId };
393
+ },
394
+ async routeMessage(message) {
395
+ const registration = await registrationFields();
396
+ await request({
397
+ kind: "follower.routeAgentMessage",
398
+ requestId: deps.createRequestId(),
399
+ ...registration.fields,
400
+ message,
401
+ sentAtMs: getNowMs(),
402
+ }, registration.remainingTimeoutMs);
403
+ },
404
+ };
405
+ }
406
+ export function createTelegramBusFollowerApiCaller(deps) {
407
+ const getNowMs = deps.getNowMs ?? Date.now;
408
+ const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
409
+ return async (method, args) => {
410
+ const registration = await resolveTelegramBusFollowerRegistration(deps, timeoutMs);
411
+ const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
412
+ let response;
413
+ try {
414
+ response = await sendTelegramBusLocalEnvelope({
415
+ socketPath,
416
+ timeoutMs: registration.remainingTimeoutMs,
417
+ retry: getTelegramBusTransportRetryPolicy({
418
+ endpoint: socketPath,
419
+ operation: "operation",
420
+ }),
421
+ envelope: {
422
+ kind: "follower.callApi",
423
+ requestId: deps.createRequestId(),
424
+ auth: deps.getAuthSecret?.(),
425
+ instanceId: deps.instanceId,
426
+ registrationGeneration: registration.generation,
427
+ method,
428
+ args,
429
+ sentAtMs: getNowMs(),
430
+ },
431
+ });
432
+ }
433
+ catch (error) {
434
+ const apiMethod = (method === "call" || method === "callMultipart") &&
435
+ typeof args[0] === "string"
436
+ ? args[0]
437
+ : method;
438
+ if (!isTelegramApiMethodRetrySafe(apiMethod)) {
439
+ throw new TelegramApiCommitUnknownError(apiMethod, error);
440
+ }
441
+ throw error;
442
+ }
443
+ if (response?.kind === "bus.ack" && response.ok)
444
+ return response.result;
445
+ const message = response?.kind === "bus.ack"
446
+ ? response.message
447
+ : "Telegram bus API call did not return an acknowledgement.";
448
+ if (response?.kind === "bus.ack" &&
449
+ response.error?.code === "stale-target" &&
450
+ response.error.chatId !== undefined &&
451
+ response.error.threadId !== undefined) {
452
+ throw new TelegramApiStaleTargetError(message ?? "Telegram thread target is stale.", {
453
+ chatId: response.error.chatId,
454
+ threadId: response.error.threadId,
455
+ });
456
+ }
457
+ if (response?.kind === "bus.ack" &&
458
+ response.error?.code === "commit-unknown") {
459
+ throw new TelegramApiCommitUnknownError(response.error.method ?? method, new Error(message ?? "Telegram bus API call result is ambiguous."));
460
+ }
461
+ throw new Error(message ?? "Telegram bus API call failed.");
462
+ };
463
+ }
464
+ async function resolveTelegramBusFollowerRegistration(deps, timeoutMs) {
465
+ const current = deps.getRegistrationGeneration();
466
+ if (current)
467
+ return { generation: current, remainingTimeoutMs: timeoutMs };
468
+ const getNowMs = deps.getNowMs ?? Date.now;
469
+ const startedAtMs = getNowMs();
470
+ const restored = await deps.waitForRegistrationGeneration?.(timeoutMs);
471
+ const remainingTimeoutMs = Math.max(0, timeoutMs - (getNowMs() - startedAtMs));
472
+ if (restored && remainingTimeoutMs > 0) {
473
+ return { generation: restored, remainingTimeoutMs };
474
+ }
475
+ throw new Error("Telegram bus follower is not registered.");
476
+ }
477
+ function isTelegramStaleContextError(error) {
478
+ return (error instanceof Error &&
479
+ (error.message.includes("stale after session") ||
480
+ error.message.includes("stale ctx")));
481
+ }
482
+ export function createTelegramBusFollowerSessionReplacementSuspender(deps) {
483
+ const getNowMs = deps.getNowMs ?? Date.now;
484
+ const getPid = deps.getPid ?? (() => process.pid);
485
+ return async () => {
486
+ const target = deps.registrationState.getTarget();
487
+ if (deps.registrationState.isRegistered() && target) {
488
+ setTelegramFollowerSessionHandoff({
489
+ pid: getPid(),
490
+ instanceId: deps.instanceId,
491
+ createdAtMs: getNowMs(),
492
+ target,
493
+ slot: deps.registrationState.getSlot(),
494
+ threadName: deps.registrationState.getThreadName(),
495
+ });
496
+ deps.recordRuntimeEvent("bus", "Telegram follower registration suspended for session replacement", {
497
+ phase: "follower-session-handoff",
498
+ instanceId: deps.instanceId,
499
+ chatId: target.chatId,
500
+ threadId: target.threadId,
501
+ });
502
+ }
503
+ else if (deps.isLeader?.()) {
504
+ const leaderBinding = deps.getLeaderBinding?.();
505
+ if (typeof leaderBinding?.target?.threadId === "number") {
506
+ const activeContext = deps.getActiveContext?.();
507
+ const profileKey = Threads.getTelegramThreadOwnerKey({
508
+ kind: "leader",
509
+ cwd: activeContext?.cwd,
510
+ instanceId: deps.instanceId,
511
+ telegramProfile: deps.getActiveProfileName?.(),
512
+ });
513
+ Threads.setTelegramLeaderSessionHandoff({
514
+ pid: getPid(),
515
+ instanceId: deps.instanceId,
516
+ createdAtMs: getNowMs(),
517
+ profileKey,
518
+ target: {
519
+ chatId: leaderBinding.target.chatId,
520
+ threadId: leaderBinding.target.threadId,
521
+ },
522
+ slot: leaderBinding.slot,
523
+ threadName: leaderBinding.threadName,
524
+ });
525
+ deps.recordRuntimeEvent("bus", "Telegram leader binding suspended for session replacement", {
526
+ phase: "leader-session-handoff",
527
+ instanceId: deps.instanceId,
528
+ chatId: leaderBinding.target.chatId,
529
+ threadId: leaderBinding.target.threadId,
530
+ slot: leaderBinding.slot,
531
+ threadName: leaderBinding.threadName,
532
+ });
533
+ }
534
+ }
535
+ await deps.suspendPolling();
536
+ };
537
+ }
538
+ export function createTelegramBusFollowerSessionRefreshHook(deps) {
539
+ return async (_event, ctx) => {
540
+ if (deps.isSessionActive && !deps.isSessionActive(ctx))
541
+ return;
542
+ if (!deps.registrationState.isRegistered()) {
543
+ const handoff = getTelegramFollowerSessionHandoff();
544
+ const lockState = deps.getLeaderState();
545
+ const handoffIsFresh = isTelegramFollowerSessionHandoffFresh(handoff);
546
+ if (handoffIsFresh && lockState.kind === "active-elsewhere") {
547
+ try {
548
+ const restored = await deps.registrationRuntime.registerWithLeader(ctx, lockState.lock, {
549
+ target: handoff.target,
550
+ previousInstanceId: handoff.instanceId,
551
+ });
552
+ if (deps.isSessionActive && !deps.isSessionActive(ctx))
553
+ return;
554
+ if (restored) {
555
+ setTelegramFollowerSessionHandoff(undefined);
556
+ deps.updateStatus(ctx);
557
+ deps.recordRuntimeEvent("bus", "Telegram follower registration restored after session replacement", {
558
+ phase: "follower-session-restore",
559
+ previousInstanceId: handoff.instanceId,
560
+ });
561
+ }
562
+ }
563
+ catch (error) {
564
+ deps.recordRuntimeEvent("bus", error, {
565
+ phase: "follower-session-restore",
566
+ previousInstanceId: handoff?.instanceId,
567
+ });
568
+ }
569
+ }
570
+ else if (handoff) {
571
+ setTelegramFollowerSessionHandoff(undefined);
572
+ }
573
+ }
574
+ if (!deps.registrationState.isRegistered())
575
+ return;
576
+ if (deps.isSessionActive && !deps.isSessionActive(ctx))
577
+ return;
578
+ deps.registrationRuntime.setContext(ctx);
579
+ deps.updateStatus(ctx);
580
+ deps.recordRuntimeEvent("bus", "Telegram follower session context refreshed", { phase: "follower-session-refresh" });
581
+ };
582
+ }
583
+ export function createTelegramBusFollowerControlState() {
584
+ let activeAuthSecret;
585
+ let lifecyclePhase;
586
+ return {
587
+ getActiveAuthSecret: () => activeAuthSecret,
588
+ setActiveAuthSecret(secret) {
589
+ activeAuthSecret = secret;
590
+ },
591
+ getLifecyclePhase: () => lifecyclePhase,
592
+ setLifecyclePhase(phase) {
593
+ lifecyclePhase = phase;
594
+ },
595
+ };
596
+ }
597
+ export function createTelegramBusFollowerRegistrationState(options = {}) {
598
+ let registered = false;
599
+ let target;
600
+ let slot;
601
+ let threadName;
602
+ let displayTitle;
603
+ let generation;
604
+ let leaderProtocol;
605
+ let eligibleElectionSlots = [];
606
+ let recoveryEpoch = 0;
607
+ let activeRecoveryEpoch;
608
+ const generationWaiters = new Set();
609
+ const settleGenerationWaiters = (value, epoch) => {
610
+ for (const waiter of [...generationWaiters]) {
611
+ if (epoch === undefined || waiter.epoch === epoch)
612
+ waiter.settle(value);
613
+ }
614
+ };
615
+ return {
616
+ isRegistered: () => registered,
617
+ getTarget: () => (target ? { ...target } : undefined),
618
+ getSlot: () => slot,
619
+ getThreadName: () => threadName,
620
+ getDisplayTitle: () => displayTitle,
621
+ setDisplayTitle(title, expectedGeneration) {
622
+ if (!registered || !generation || expectedGeneration !== generation ||
623
+ !title.trim() || title.length > 128 || displayTitle === title)
624
+ return false;
625
+ displayTitle = title;
626
+ return true;
627
+ },
628
+ getGeneration: () => generation,
629
+ beginRecovery: () => {
630
+ if (activeRecoveryEpoch !== undefined)
631
+ return activeRecoveryEpoch;
632
+ activeRecoveryEpoch = ++recoveryEpoch;
633
+ return activeRecoveryEpoch;
634
+ },
635
+ cancelRecovery: () => {
636
+ const epoch = activeRecoveryEpoch;
637
+ activeRecoveryEpoch = undefined;
638
+ if (epoch !== undefined)
639
+ settleGenerationWaiters(undefined, epoch);
640
+ },
641
+ waitForGeneration: (timeoutMs = TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS) => {
642
+ if (generation)
643
+ return Promise.resolve(generation);
644
+ const epoch = activeRecoveryEpoch;
645
+ if (epoch === undefined)
646
+ return Promise.resolve(undefined);
647
+ return new Promise((resolve) => {
648
+ let timer;
649
+ const settle = (value) => {
650
+ generationWaiters.delete(waiter);
651
+ if (timer)
652
+ clearTimeout(timer);
653
+ resolve(value);
654
+ };
655
+ const waiter = { epoch, settle };
656
+ generationWaiters.add(waiter);
657
+ timer = setTimeout(() => settle(undefined), Math.max(0, timeoutMs));
658
+ });
659
+ },
660
+ getLeaderProtocol: () => leaderProtocol
661
+ ? { ...leaderProtocol, capabilities: [...leaderProtocol.capabilities] }
662
+ : undefined,
663
+ getEligibleElectionSlots: () => [...eligibleElectionSlots],
664
+ setEligibleElectionSlots: (slots) => {
665
+ eligibleElectionSlots = Array.from(new Set(slots.filter((slot) => /^[A-Z]$/.test(slot)))).sort();
666
+ },
667
+ setRegistered: (next, nextTarget, metadata) => {
668
+ const retainedDisplayTitle = next && registered &&
669
+ generation === metadata?.generation &&
670
+ target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
671
+ ? displayTitle : undefined;
672
+ const availabilityChanged = registered !== next;
673
+ registered = next;
674
+ target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
675
+ slot = next ? metadata?.slot : undefined;
676
+ threadName = next ? metadata?.threadName : undefined;
677
+ displayTitle = next && nextTarget && metadata?.generation &&
678
+ metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
679
+ ? metadata.displayTitle : retainedDisplayTitle;
680
+ generation = next ? metadata?.generation : undefined;
681
+ leaderProtocol =
682
+ next && metadata?.leaderProtocol
683
+ ? {
684
+ ...metadata.leaderProtocol,
685
+ capabilities: [...metadata.leaderProtocol.capabilities],
686
+ }
687
+ : undefined;
688
+ if (availabilityChanged)
689
+ options.onAvailabilityChanged?.();
690
+ if (generation) {
691
+ activeRecoveryEpoch = undefined;
692
+ settleGenerationWaiters(generation);
693
+ }
694
+ },
695
+ };
696
+ }
697
+ export function createTelegramBusFollowerHeartbeatRecoveryHandler(deps) {
698
+ const promotionGraceMs = deps.promotionGraceMs ?? TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS;
699
+ const sleep = deps.sleep ??
700
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
701
+ const scheduleRetry = deps.scheduleRetry ??
702
+ ((retry, delayMs) => {
703
+ const timer = setTimeout(retry, delayMs);
704
+ timer.unref?.();
705
+ });
706
+ let promotionPending = false;
707
+ const safeUpdateStatus = (ctx) => {
708
+ try {
709
+ deps.updateStatus(ctx);
710
+ }
711
+ catch (error) {
712
+ if (!isTelegramStaleContextError(error))
713
+ throw error;
714
+ deps.recordRuntimeEvent("bus", error, {
715
+ phase: "follower-stale-context-status",
716
+ });
717
+ }
718
+ };
719
+ const clearRegisteredState = (ctx) => {
720
+ deps.registrationState.setRegistered(false);
721
+ safeUpdateStatus(ctx);
722
+ };
723
+ const tryRegisterWithLeader = async (ctx, leader, phase, binding) => {
724
+ try {
725
+ const restored = await deps
726
+ .getRegistrationRuntime()
727
+ .registerWithLeader(ctx, leader, binding?.target ? { target: binding.target } : undefined);
728
+ if (!restored)
729
+ return false;
730
+ deps.setLifecyclePhase(undefined);
731
+ safeUpdateStatus(ctx);
732
+ deps.recordRuntimeEvent("bus", "Telegram follower registration restored", {
733
+ phase,
734
+ });
735
+ return true;
736
+ }
737
+ catch (error) {
738
+ clearRegisteredState(ctx);
739
+ deps.recordRuntimeEvent("bus", error, { phase });
740
+ return false;
741
+ }
742
+ };
743
+ const snapshotBinding = () => ({
744
+ target: deps.registrationState.getTarget(),
745
+ slot: deps.registrationState.getSlot(),
746
+ threadName: deps.registrationState.getThreadName(),
747
+ });
748
+ const scheduleRecovery = (reason, fallbackCtx, binding) => {
749
+ const retry = () => {
750
+ const activeCtx = deps.getActiveContext
751
+ ? deps.getActiveContext()
752
+ : fallbackCtx;
753
+ if (!activeCtx) {
754
+ scheduleRetry(retry, promotionGraceMs);
755
+ return;
756
+ }
757
+ void recover(reason, activeCtx, binding);
758
+ };
759
+ scheduleRetry(retry, promotionGraceMs);
760
+ };
761
+ const promoteToLeader = async (reason, ctx, binding, election) => {
762
+ const activeCtx = deps.getActiveContext?.();
763
+ if (deps.getActiveContext && activeCtx !== ctx) {
764
+ scheduleRecovery(reason, ctx, binding);
765
+ return;
766
+ }
767
+ deps.setLifecyclePhase("electing");
768
+ safeUpdateStatus(ctx);
769
+ deps.recordRuntimeEvent("bus", reason, {
770
+ phase: "follower-promotion-electing",
771
+ });
772
+ deps.getRegistrationRuntime().stop();
773
+ deps.setLifecyclePhase("electing");
774
+ safeUpdateStatus(ctx);
775
+ deps.recordRuntimeEvent("bus", "Telegram follower attempting promotion", {
776
+ phase: "follower-promotion-electing",
777
+ });
778
+ const promoted = await deps.promoteToLeader(ctx, binding, election);
779
+ deps.setLifecyclePhase(undefined);
780
+ safeUpdateStatus(ctx);
781
+ deps.recordRuntimeEvent("bus", promoted
782
+ ? "Telegram follower promotion completed"
783
+ : "Telegram follower promotion lost election", {
784
+ phase: promoted
785
+ ? "follower-promotion-complete"
786
+ : "follower-promotion-lost",
787
+ });
788
+ if (!promoted)
789
+ scheduleRecovery(reason, ctx, binding);
790
+ };
791
+ const attemptPreferredPromotion = async (reason, ctx, binding, candidateState) => {
792
+ const slot = binding.slot;
793
+ const lowerEligibleSlot = slot
794
+ ? deps.registrationState
795
+ .getEligibleElectionSlots()
796
+ .find((candidate) => candidate < slot)
797
+ : undefined;
798
+ if (lowerEligibleSlot) {
799
+ deps.recordRuntimeEvent("bus", "Telegram follower deferring to a lower-slot election candidate", {
800
+ phase: "follower-promotion-slot-priority",
801
+ slot,
802
+ lowerEligibleSlot,
803
+ });
804
+ await sleep(promotionGraceMs);
805
+ candidateState = deps.getLeaderState();
806
+ if (candidateState.kind === "active-elsewhere") {
807
+ if (!(await tryRegisterWithLeader(ctx, candidateState.lock, "follower-register-preferred-successor", binding))) {
808
+ scheduleRecovery(reason, ctx, binding);
809
+ }
810
+ return;
811
+ }
812
+ }
813
+ if (candidateState.kind !== "stale" && candidateState.kind !== "inactive")
814
+ return;
815
+ await promoteToLeader(reason, ctx, binding, {
816
+ expectedOwner: candidateState.kind === "stale" ? candidateState.lock : undefined,
817
+ });
818
+ };
819
+ const recover = async (error, ctx, carriedBinding) => {
820
+ if (promotionPending)
821
+ return;
822
+ promotionPending = true;
823
+ deps.registrationState.beginRecovery();
824
+ try {
825
+ const initialBinding = carriedBinding ?? snapshotBinding();
826
+ const state = deps.getLeaderState();
827
+ if (state.kind === "active-elsewhere") {
828
+ clearRegisteredState(ctx);
829
+ if (await tryRegisterWithLeader(ctx, state.lock, "follower-register-restore", initialBinding)) {
830
+ return;
831
+ }
832
+ deps.setLifecyclePhase("electing");
833
+ safeUpdateStatus(ctx);
834
+ deps.recordRuntimeEvent("bus", "Telegram follower waiting for leader reload recovery", { phase: "follower-promotion-grace" });
835
+ await sleep(promotionGraceMs);
836
+ const graceState = deps.getLeaderState();
837
+ if (graceState.kind === "active-elsewhere") {
838
+ if (await tryRegisterWithLeader(ctx, graceState.lock, "follower-register-restore-grace", initialBinding)) {
839
+ return;
840
+ }
841
+ deps.setLifecyclePhase(undefined);
842
+ safeUpdateStatus(ctx);
843
+ deps.recordRuntimeEvent("bus", "Telegram follower promotion blocked by live leader lease", {
844
+ phase: "follower-promotion-live-owner",
845
+ leaderInstanceId: graceState.lock.instanceId,
846
+ leaderEpoch: graceState.lock.leaderEpoch,
847
+ });
848
+ scheduleRecovery(error, ctx, initialBinding);
849
+ return;
850
+ }
851
+ await attemptPreferredPromotion(error, ctx, initialBinding, graceState);
852
+ return;
853
+ }
854
+ await attemptPreferredPromotion(error, ctx, initialBinding, state);
855
+ }
856
+ catch (promotionError) {
857
+ deps.setLifecyclePhase(undefined);
858
+ safeUpdateStatus(ctx);
859
+ if (isTelegramStaleContextError(promotionError)) {
860
+ deps.recordRuntimeEvent("bus", promotionError, {
861
+ phase: "follower-heartbeat-stale-context",
862
+ });
863
+ return;
864
+ }
865
+ throw promotionError;
866
+ }
867
+ finally {
868
+ promotionPending = false;
869
+ }
870
+ };
871
+ return recover;
872
+ }
873
+ export function createTelegramBusFollowerRegistrationRuntime(deps) {
874
+ const getNowMs = deps.getNowMs ?? Date.now;
875
+ const getPid = deps.getPid ?? (() => process.pid);
876
+ const heartbeatMs = deps.heartbeatMs ?? 1000;
877
+ const registrationTimeoutMs = deps.registrationTimeoutMs ?? deps.timeoutMs ?? 30000;
878
+ const registrationRetryAttempts = deps.registrationRetryAttempts ??
879
+ TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS;
880
+ const registrationRetryDelayMs = deps.registrationRetryDelayMs ??
881
+ TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS;
882
+ let heartbeatInterval;
883
+ let heartbeatPromise;
884
+ let heartbeatPromiseGeneration;
885
+ let activeLeaderSocketPath;
886
+ let activeAuthSecret;
887
+ let activeRegistrationGeneration;
888
+ let activeContext;
889
+ let lastKnownTarget;
890
+ let lastKnownSlot;
891
+ let lastKnownThreadName;
892
+ const stopHeartbeat = () => {
893
+ if (!heartbeatInterval)
894
+ return;
895
+ clearInterval(heartbeatInterval);
896
+ heartbeatInterval = undefined;
897
+ };
898
+ const stop = () => {
899
+ stopHeartbeat();
900
+ activeAuthSecret = undefined;
901
+ activeRegistrationGeneration = undefined;
902
+ heartbeatPromise = undefined;
903
+ heartbeatPromiseGeneration = undefined;
904
+ deps.setActiveAuthSecret?.(undefined);
905
+ deps.registrationState?.cancelRecovery();
906
+ deps.registrationState?.setRegistered(false);
907
+ lastKnownTarget = undefined;
908
+ lastKnownSlot = undefined;
909
+ lastKnownThreadName = undefined;
910
+ activeContext = undefined;
911
+ void Promise.resolve(deps.stopReceiving?.()).catch((error) => {
912
+ try {
913
+ deps.recordRuntimeEvent?.("bus", error, {
914
+ phase: "follower-receiver-stop",
915
+ });
916
+ }
917
+ catch {
918
+ // Stop diagnostics cannot create an unhandled Promise.
919
+ }
920
+ });
921
+ };
922
+ const sendHeartbeat = async () => {
923
+ const leaderSocketPath = activeLeaderSocketPath;
924
+ const registrationGeneration = activeRegistrationGeneration;
925
+ const heartbeatContext = activeContext;
926
+ if (!leaderSocketPath || !registrationGeneration)
927
+ return;
928
+ const isCurrentHeartbeat = () => activeLeaderSocketPath === leaderSocketPath &&
929
+ activeRegistrationGeneration === registrationGeneration &&
930
+ activeContext === heartbeatContext;
931
+ try {
932
+ const response = await sendTelegramBusLocalEnvelope({
933
+ socketPath: leaderSocketPath,
934
+ timeoutMs: deps.timeoutMs,
935
+ retry: getTelegramBusTransportRetryPolicy({
936
+ endpoint: leaderSocketPath,
937
+ operation: "operation",
938
+ }),
939
+ envelope: {
940
+ kind: "follower.heartbeat",
941
+ requestId: deps.createRequestId(),
942
+ auth: activeAuthSecret,
943
+ instanceId: deps.instanceId,
944
+ registrationGeneration,
945
+ sentAtMs: getNowMs(),
946
+ },
947
+ });
948
+ if (!isCurrentHeartbeat())
949
+ return;
950
+ if (response?.kind === "bus.ack" && response.ok) {
951
+ const heartbeatResult = isRecord(response.result)
952
+ ? response.result
953
+ : undefined;
954
+ const slots = Array.isArray(heartbeatResult?.eligibleElectionSlots)
955
+ ? heartbeatResult.eligibleElectionSlots.filter((slot) => typeof slot === "string")
956
+ : [];
957
+ deps.registrationState?.setEligibleElectionSlots(slots);
958
+ if (typeof heartbeatResult?.displayTitle === "string" &&
959
+ deps.registrationState?.setDisplayTitle(heartbeatResult.displayTitle, registrationGeneration) &&
960
+ heartbeatContext) {
961
+ try {
962
+ deps.onDisplayTitleChanged?.(heartbeatContext);
963
+ }
964
+ catch {
965
+ // Display refresh failure must not invalidate a successful heartbeat.
966
+ }
967
+ }
968
+ }
969
+ if (response?.kind === "bus.ack" && !response.ok) {
970
+ throw new Error(response.message ?? "Telegram bus follower heartbeat was rejected.");
971
+ }
972
+ }
973
+ catch (error) {
974
+ if (!isCurrentHeartbeat())
975
+ return;
976
+ try {
977
+ deps.recordRuntimeEvent?.("bus", error, {
978
+ phase: "follower-heartbeat",
979
+ });
980
+ }
981
+ catch {
982
+ // Diagnostics cannot replace heartbeat recovery.
983
+ }
984
+ if (!heartbeatContext)
985
+ return;
986
+ try {
987
+ await deps.onHeartbeatFailure?.(error, heartbeatContext);
988
+ }
989
+ catch (recoveryError) {
990
+ try {
991
+ deps.recordRuntimeEvent?.("bus", recoveryError, {
992
+ phase: "follower-heartbeat-recovery",
993
+ });
994
+ }
995
+ catch {
996
+ // A diagnostic sink cannot create an unhandled interval rejection.
997
+ }
998
+ }
999
+ }
1000
+ };
1001
+ const requestHeartbeat = () => {
1002
+ const generation = activeRegistrationGeneration;
1003
+ if (heartbeatPromise && heartbeatPromiseGeneration === generation) {
1004
+ return heartbeatPromise;
1005
+ }
1006
+ let tracked;
1007
+ tracked = sendHeartbeat().finally(() => {
1008
+ if (heartbeatPromise === tracked) {
1009
+ heartbeatPromise = undefined;
1010
+ heartbeatPromiseGeneration = undefined;
1011
+ }
1012
+ });
1013
+ heartbeatPromise = tracked;
1014
+ heartbeatPromiseGeneration = generation;
1015
+ return tracked;
1016
+ };
1017
+ const startHeartbeat = (socketPath) => {
1018
+ stopHeartbeat();
1019
+ activeLeaderSocketPath = socketPath;
1020
+ heartbeatInterval = setInterval(() => {
1021
+ void requestHeartbeat();
1022
+ }, heartbeatMs);
1023
+ heartbeatInterval.unref?.();
1024
+ };
1025
+ return {
1026
+ registerWithLeader: async (ctx, leader, options) => {
1027
+ const pendingHandoff = options
1028
+ ? undefined
1029
+ : getTelegramFollowerSessionHandoff();
1030
+ const pendingHandoffOptions = isTelegramFollowerSessionHandoffFresh(pendingHandoff)
1031
+ ? {
1032
+ target: pendingHandoff.target,
1033
+ previousInstanceId: pendingHandoff.instanceId,
1034
+ }
1035
+ : undefined;
1036
+ const registrationOptions = options ?? pendingHandoffOptions;
1037
+ const leaderSocketPath = leader.busSocketPath ??
1038
+ deps.getLeaderSocketPath?.() ??
1039
+ getTelegramBusSocketPath();
1040
+ await deps.startReceiving?.();
1041
+ activeAuthSecret = deps.getLeaderAuthSecret
1042
+ ? deps.getLeaderAuthSecret(leader)
1043
+ : leader?.busSecret;
1044
+ deps.setActiveAuthSecret?.(activeAuthSecret);
1045
+ const registrationGeneration = deps.createRequestId();
1046
+ const registrationEnvelope = {
1047
+ kind: registrationOptions?.restoreWorkspace
1048
+ ? "follower.restoreWorkspace"
1049
+ : "follower.register",
1050
+ requestId: registrationGeneration,
1051
+ auth: activeAuthSecret,
1052
+ registration: {
1053
+ instanceId: deps.instanceId,
1054
+ ...(registrationOptions?.previousInstanceId
1055
+ ? { previousInstanceId: registrationOptions.previousInstanceId }
1056
+ : {}),
1057
+ profileKey: deps.getProfileKey?.(ctx) ??
1058
+ (ctx.cwd ? `cwd:${ctx.cwd}` : undefined),
1059
+ threadName: deps.registrationState?.getThreadName() ??
1060
+ lastKnownThreadName ??
1061
+ deps.getThreadName?.(ctx) ??
1062
+ (ctx.cwd ? basename(ctx.cwd) : undefined),
1063
+ ...((deps.registrationState?.getSlot() ?? lastKnownSlot)
1064
+ ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
1065
+ : {}),
1066
+ cwd: ctx.cwd,
1067
+ sessionId: deps.getSessionId?.(ctx),
1068
+ pid: getPid(),
1069
+ processBirthId: deps.getProcessBirthId?.(),
1070
+ sessionGeneration: deps.getSessionGeneration?.(),
1071
+ target: registrationOptions?.target ??
1072
+ deps.registrationState?.getTarget() ??
1073
+ lastKnownTarget,
1074
+ busSocketPath: deps.getFollowerBusSocketPath?.() ?? deps.followerBusSocketPath,
1075
+ registrationGeneration,
1076
+ protocol: deps.protocolIdentity,
1077
+ connectedAtMs: getNowMs(),
1078
+ },
1079
+ };
1080
+ let response;
1081
+ try {
1082
+ response = await sendTelegramBusLocalEnvelope({
1083
+ socketPath: leaderSocketPath,
1084
+ timeoutMs: registrationTimeoutMs,
1085
+ envelope: registrationEnvelope,
1086
+ retry: getTelegramBusTransportRetryPolicy({
1087
+ endpoint: leaderSocketPath,
1088
+ operation: "registration",
1089
+ overrides: {
1090
+ attempts: registrationRetryAttempts,
1091
+ delayMs: registrationRetryDelayMs,
1092
+ },
1093
+ }),
1094
+ recordTransportEvent(phase, details) {
1095
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
1096
+ phase: `follower-register-${phase}`,
1097
+ ...details,
1098
+ });
1099
+ },
1100
+ });
1101
+ }
1102
+ catch (error) {
1103
+ stopHeartbeat();
1104
+ activeLeaderSocketPath = undefined;
1105
+ activeAuthSecret = undefined;
1106
+ deps.registrationState?.setRegistered(false);
1107
+ deps.setActiveAuthSecret?.(undefined);
1108
+ await deps.stopReceiving?.();
1109
+ throw error;
1110
+ }
1111
+ if (deps.isContextActive && !deps.isContextActive(ctx)) {
1112
+ stopHeartbeat();
1113
+ activeLeaderSocketPath = undefined;
1114
+ activeAuthSecret = undefined;
1115
+ deps.setActiveAuthSecret?.(undefined);
1116
+ await deps.stopReceiving?.();
1117
+ return false;
1118
+ }
1119
+ const compatibility = getTelegramBusProtocolCompatibility({
1120
+ local: deps.protocolIdentity,
1121
+ remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1122
+ });
1123
+ if (!compatibility.compatible) {
1124
+ stopHeartbeat();
1125
+ activeLeaderSocketPath = undefined;
1126
+ activeAuthSecret = undefined;
1127
+ deps.registrationState?.setRegistered(false);
1128
+ deps.setActiveAuthSecret?.(undefined);
1129
+ await deps.stopReceiving?.();
1130
+ throw new Error(`Incompatible Telegram bus leader protocol: ${compatibility.reason}.`);
1131
+ }
1132
+ if (response?.kind === "bus.ack" && !response.ok) {
1133
+ stopHeartbeat();
1134
+ activeLeaderSocketPath = undefined;
1135
+ activeAuthSecret = undefined;
1136
+ deps.registrationState?.setRegistered(false);
1137
+ deps.setActiveAuthSecret?.(undefined);
1138
+ await deps.stopReceiving?.();
1139
+ if (registrationOptions?.restoreWorkspace &&
1140
+ response.error?.code === "workspace-binding-unavailable")
1141
+ return false;
1142
+ throw new Error(response.message ??
1143
+ "Telegram bus follower registration was rejected.");
1144
+ }
1145
+ if (response?.kind === "bus.ack" && response.ok) {
1146
+ const registrationResult = parseRegistrationResult(response.result);
1147
+ deps.registrationState?.setRegistered(true, registrationResult.target, {
1148
+ ...registrationResult,
1149
+ generation: registrationGeneration,
1150
+ ...(response.protocol
1151
+ ? { leaderProtocol: response.protocol }
1152
+ : {}),
1153
+ });
1154
+ lastKnownTarget = registrationResult.target;
1155
+ lastKnownSlot = registrationResult.slot;
1156
+ lastKnownThreadName = registrationResult.threadName;
1157
+ activeLeaderSocketPath = leaderSocketPath;
1158
+ activeRegistrationGeneration = registrationGeneration;
1159
+ activeContext = ctx;
1160
+ try {
1161
+ await deps.onRegistered?.(ctx);
1162
+ }
1163
+ catch (error) {
1164
+ stopHeartbeat();
1165
+ activeLeaderSocketPath = undefined;
1166
+ activeAuthSecret = undefined;
1167
+ activeRegistrationGeneration = undefined;
1168
+ deps.registrationState?.setRegistered(false);
1169
+ deps.setActiveAuthSecret?.(undefined);
1170
+ await deps.stopReceiving?.();
1171
+ throw error;
1172
+ }
1173
+ await requestHeartbeat();
1174
+ startHeartbeat(leaderSocketPath);
1175
+ if (pendingHandoffOptions &&
1176
+ getTelegramFollowerSessionHandoff()?.instanceId ===
1177
+ pendingHandoffOptions.previousInstanceId) {
1178
+ setTelegramFollowerSessionHandoff(undefined);
1179
+ }
1180
+ return true;
1181
+ }
1182
+ stopHeartbeat();
1183
+ activeLeaderSocketPath = undefined;
1184
+ activeAuthSecret = undefined;
1185
+ deps.registrationState?.setRegistered(false);
1186
+ deps.setActiveAuthSecret?.(undefined);
1187
+ await deps.stopReceiving?.();
1188
+ return false;
1189
+ },
1190
+ setContext(ctx) {
1191
+ activeContext = ctx;
1192
+ },
1193
+ async setThreadDisplayMode(mode) {
1194
+ const socketPath = activeLeaderSocketPath;
1195
+ const generation = activeRegistrationGeneration;
1196
+ const auth = activeAuthSecret;
1197
+ if (!socketPath || !generation || !deps.registrationState?.isRegistered()) {
1198
+ throw new Error("Telegram follower is not registered with the leader.");
1199
+ }
1200
+ const requiredCapability = mode === "directory-snake" || mode === "directory-title"
1201
+ ? TELEGRAM_BUS_CAPABILITY_DIRECTORY_DISPLAY_FORMAT
1202
+ : TELEGRAM_BUS_CAPABILITY_THREAD_DISPLAY_MODE;
1203
+ if (!hasTelegramBusCapability(deps.protocolIdentity, requiredCapability) ||
1204
+ !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), requiredCapability)) {
1205
+ throw new Error("The Telegram peers do not support this Thread display setting. Update or restart both instances.");
1206
+ }
1207
+ const requestId = deps.createRequestId();
1208
+ const response = await sendTelegramBusLocalEnvelope({
1209
+ socketPath, timeoutMs: registrationTimeoutMs,
1210
+ envelope: {
1211
+ kind: "follower.setThreadDisplayMode", requestId, auth,
1212
+ instanceId: deps.instanceId, registrationGeneration: generation, mode,
1213
+ },
1214
+ });
1215
+ if (activeLeaderSocketPath !== socketPath || activeRegistrationGeneration !== generation ||
1216
+ activeAuthSecret !== auth || deps.registrationState.getGeneration() !== generation) {
1217
+ throw new Error("Telegram Thread display setting completed for a stale registration.");
1218
+ }
1219
+ if (response?.kind !== "bus.ack" || !response.ok || response.requestId !== requestId ||
1220
+ !isRecord(response.result) || response.result.mode !== mode) {
1221
+ throw new Error(response?.kind === "bus.ack"
1222
+ ? response.message ?? "Telegram Thread display setting was rejected."
1223
+ : "Telegram Thread display setting was not acknowledged.");
1224
+ }
1225
+ },
1226
+ async renameThread(target, threadName) {
1227
+ if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
1228
+ throw new Error("Telegram follower is not registered with the leader.");
1229
+ }
1230
+ if (deps.registrationState &&
1231
+ !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME)) {
1232
+ throw new Error("The active Telegram leader does not support Workspace Thread rename. Update or restart that Pi instance.");
1233
+ }
1234
+ const expectedLeaderSocketPath = activeLeaderSocketPath;
1235
+ const expectedRegistrationGeneration = activeRegistrationGeneration;
1236
+ const expectedAuthSecret = activeAuthSecret;
1237
+ const response = await sendTelegramBusLocalEnvelope({
1238
+ socketPath: expectedLeaderSocketPath,
1239
+ timeoutMs: registrationTimeoutMs,
1240
+ retry: getTelegramBusTransportRetryPolicy({
1241
+ endpoint: expectedLeaderSocketPath,
1242
+ operation: "operation",
1243
+ }),
1244
+ envelope: {
1245
+ kind: "follower.renameThread",
1246
+ requestId: deps.createRequestId(),
1247
+ auth: expectedAuthSecret,
1248
+ instanceId: deps.instanceId,
1249
+ registrationGeneration: expectedRegistrationGeneration,
1250
+ target,
1251
+ threadName,
1252
+ sentAtMs: getNowMs(),
1253
+ },
1254
+ });
1255
+ if (response?.kind !== "bus.ack" || !response.ok) {
1256
+ throw new Error(response?.kind === "bus.ack"
1257
+ ? (response.message ?? "Telegram Workspace Thread rename was rejected.")
1258
+ : "Telegram Workspace Thread rename was not acknowledged.");
1259
+ }
1260
+ if (activeLeaderSocketPath !== expectedLeaderSocketPath ||
1261
+ activeRegistrationGeneration !== expectedRegistrationGeneration ||
1262
+ (deps.registrationState &&
1263
+ deps.registrationState.getGeneration() !==
1264
+ expectedRegistrationGeneration)) {
1265
+ throw new Error("Telegram Workspace Thread rename completed for a stale follower registration.");
1266
+ }
1267
+ const renamedThreadName = response.result &&
1268
+ typeof response.result === "object" &&
1269
+ "threadName" in response.result &&
1270
+ typeof response.result.threadName === "string"
1271
+ ? response.result.threadName
1272
+ : threadName;
1273
+ const registrationTarget = deps.registrationState?.getTarget();
1274
+ if (deps.registrationState && registrationTarget) {
1275
+ deps.registrationState.setRegistered(true, registrationTarget, {
1276
+ slot: deps.registrationState.getSlot(),
1277
+ threadName: renamedThreadName,
1278
+ generation: expectedRegistrationGeneration,
1279
+ leaderProtocol: deps.registrationState.getLeaderProtocol(),
1280
+ });
1281
+ }
1282
+ return renamedThreadName;
1283
+ },
1284
+ async resetThreadName(target) {
1285
+ if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
1286
+ throw new Error("Telegram follower is not registered with the leader.");
1287
+ }
1288
+ if (deps.registrationState && !hasTelegramBusCapability(deps.registrationState.getLeaderProtocol(), TELEGRAM_BUS_CAPABILITY_WORKSPACE_THREAD_RENAME)) {
1289
+ throw new Error("The active Telegram leader does not support Workspace Thread reset. Update or restart that Pi instance.");
1290
+ }
1291
+ const expectedLeaderSocketPath = activeLeaderSocketPath;
1292
+ const expectedRegistrationGeneration = activeRegistrationGeneration;
1293
+ const response = await sendTelegramBusLocalEnvelope({
1294
+ socketPath: expectedLeaderSocketPath,
1295
+ timeoutMs: registrationTimeoutMs,
1296
+ retry: getTelegramBusTransportRetryPolicy({
1297
+ endpoint: expectedLeaderSocketPath,
1298
+ operation: "operation",
1299
+ }),
1300
+ envelope: {
1301
+ kind: "follower.resetThreadName",
1302
+ requestId: deps.createRequestId(),
1303
+ auth: activeAuthSecret,
1304
+ instanceId: deps.instanceId,
1305
+ registrationGeneration: expectedRegistrationGeneration,
1306
+ target,
1307
+ sentAtMs: getNowMs(),
1308
+ },
1309
+ });
1310
+ const resetName = response?.kind === "bus.ack" && response.ok &&
1311
+ response.result && typeof response.result === "object" &&
1312
+ "threadName" in response.result &&
1313
+ typeof response.result.threadName === "string"
1314
+ ? response.result.threadName : undefined;
1315
+ if (!resetName) {
1316
+ throw new Error(response?.kind === "bus.ack"
1317
+ ? response.message ?? "Telegram Workspace Thread reset was rejected."
1318
+ : "Telegram Workspace Thread reset was not acknowledged.");
1319
+ }
1320
+ if (activeLeaderSocketPath !== expectedLeaderSocketPath ||
1321
+ activeRegistrationGeneration !== expectedRegistrationGeneration ||
1322
+ (deps.registrationState && deps.registrationState.getGeneration() !==
1323
+ expectedRegistrationGeneration)) {
1324
+ throw new Error("Telegram Workspace Thread reset completed for a stale follower registration.");
1325
+ }
1326
+ const registrationTarget = deps.registrationState?.getTarget();
1327
+ if (deps.registrationState && registrationTarget) {
1328
+ deps.registrationState.setRegistered(true, registrationTarget, {
1329
+ slot: deps.registrationState.getSlot(),
1330
+ threadName: resetName,
1331
+ generation: expectedRegistrationGeneration,
1332
+ leaderProtocol: deps.registrationState.getLeaderProtocol(),
1333
+ });
1334
+ }
1335
+ return resetName;
1336
+ },
1337
+ async disconnectFromLeader() {
1338
+ if (!activeLeaderSocketPath || !activeRegistrationGeneration) {
1339
+ return false;
1340
+ }
1341
+ const response = await sendTelegramBusLocalEnvelope({
1342
+ socketPath: activeLeaderSocketPath,
1343
+ timeoutMs: registrationTimeoutMs,
1344
+ retry: getTelegramBusTransportRetryPolicy({
1345
+ endpoint: activeLeaderSocketPath,
1346
+ operation: "operation",
1347
+ }),
1348
+ envelope: {
1349
+ kind: "follower.disconnect",
1350
+ requestId: deps.createRequestId(),
1351
+ auth: activeAuthSecret,
1352
+ instanceId: deps.instanceId,
1353
+ registrationGeneration: activeRegistrationGeneration,
1354
+ sentAtMs: getNowMs(),
1355
+ },
1356
+ });
1357
+ if (response?.kind === "bus.ack" && response.ok)
1358
+ return true;
1359
+ throw new Error(response?.kind === "bus.ack"
1360
+ ? (response.message ?? "Telegram follower disconnect was rejected.")
1361
+ : "Telegram follower disconnect was not acknowledged.");
1362
+ },
1363
+ stop,
1364
+ };
1365
+ }
1366
+ const TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD = "pi_telegram_forward_comment_batch_position";
1367
+ /** Bind source-owned sender checks to the journal's already-admitted synchronous v1 hook. */
1368
+ export function createTelegramBusFollowerPairedAdmission(deps) {
1369
+ return (updates, publish) => {
1370
+ if (updates.length !== 1)
1371
+ return { admitted: false };
1372
+ const update = updates[0];
1373
+ const keys = Object.keys(update).filter((key) => key !== "update_id" && key !== TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD);
1374
+ if (keys.length !== 1)
1375
+ return { admitted: false };
1376
+ const kind = keys[0];
1377
+ if (kind !== "message" && kind !== "edited_message" &&
1378
+ kind !== "callback_query" && kind !== "message_reaction")
1379
+ return { admitted: false };
1380
+ const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
1381
+ if (position !== undefined && (kind !== "message" ||
1382
+ (position !== "comment" && position !== "forward")))
1383
+ return { admitted: false };
1384
+ const carrier = update[kind];
1385
+ if (!Number.isSafeInteger(update.update_id) || update.update_id < 0 ||
1386
+ !isRecord(carrier) || carrier.pi_telegram_source_update_id !== update.update_id ||
1387
+ carrier.sender_chat !== undefined || carrier.actor_chat !== undefined)
1388
+ return { admitted: false };
1389
+ const sender = kind === "message_reaction" ? carrier.user : carrier.from;
1390
+ if (!isRecord(sender) || typeof sender.id !== "number" || !Number.isSafeInteger(sender.id) ||
1391
+ sender.id <= 0 || sender.is_bot !== false)
1392
+ return { admitted: false };
1393
+ return deps.configStore.withPairedUserAdmission(deps.profileName, deps.tokenSha256, sender.id, publish, deps.assertExecutionCurrent);
1394
+ };
1395
+ }
1396
+ export function prepareTelegramBusFollowerJournaledUpdateForExecution(update, prepareForwardedMessage) {
1397
+ const position = update[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
1398
+ if (update.message !== undefined &&
1399
+ (position === "comment" || position === "forward")) {
1400
+ prepareForwardedMessage(update.message, position);
1401
+ }
1402
+ if (position === undefined)
1403
+ return update;
1404
+ const prepared = { ...update };
1405
+ delete prepared[TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD];
1406
+ return prepared;
1407
+ }
1408
+ export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
1409
+ return {
1410
+ async admit(envelope, ctx) {
1411
+ const delivery = envelope.delivery;
1412
+ if (!delivery) {
1413
+ throw new Error("Telegram follower durable admission requires delivery identity.");
1414
+ }
1415
+ if (envelope.kind === "leader.wakeInputCustody")
1416
+ throw new Error("Telegram follower custody wake cannot use executable-copy admission.");
1417
+ const expected = createTelegramBusFollowerDeliveryIdentity({
1418
+ kind: envelope.kind,
1419
+ recipientBindingKey: delivery.recipientBindingKey,
1420
+ sourceUpdateId: delivery.sourceUpdateId,
1421
+ });
1422
+ if (expected.deliveryId !== delivery.deliveryId) {
1423
+ throw new Error("Invalid Telegram follower delivery id.");
1424
+ }
1425
+ const carrier = envelope.kind === "leader.forwardCallback"
1426
+ ? envelope.query
1427
+ : envelope.kind === "leader.forwardReaction"
1428
+ ? envelope.reactionUpdate
1429
+ : envelope.message;
1430
+ if (!isRecord(carrier) ||
1431
+ carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId) {
1432
+ throw new Error("Telegram follower delivery source update id mismatch.");
1433
+ }
1434
+ const update = {
1435
+ update_id: delivery.sourceUpdateId,
1436
+ ...(envelope.kind === "leader.forwardMessage" &&
1437
+ envelope.forwardCommentBatchPosition
1438
+ ? {
1439
+ [TELEGRAM_FOLLOWER_FORWARD_BATCH_POSITION_FIELD]: envelope.forwardCommentBatchPosition,
1440
+ }
1441
+ : {}),
1442
+ ...(envelope.kind === "leader.forwardCallback"
1443
+ ? { callback_query: carrier }
1444
+ : envelope.kind === "leader.forwardReaction"
1445
+ ? { message_reaction: carrier }
1446
+ : envelope.kind === "leader.forwardMessage"
1447
+ ? { message: carrier }
1448
+ : { edited_message: carrier }),
1449
+ };
1450
+ deps.journal.appendBatch([update]);
1451
+ deps.signalWorker(ctx);
1452
+ return {
1453
+ deliveryId: delivery.deliveryId,
1454
+ sourceUpdateId: delivery.sourceUpdateId,
1455
+ };
1456
+ },
1457
+ };
1458
+ }
1459
+ export function createTelegramBusFollowerInputCustodyPorts(deps) {
1460
+ const getRequired = () => {
1461
+ const bundle = deps.getInputCustodyBus();
1462
+ if (!bundle)
1463
+ throw new Error("Telegram input custody bus binding is unavailable.");
1464
+ return bundle;
1465
+ };
1466
+ return {
1467
+ isSourceReferenceAdmissionEnabled: () => Boolean(deps.getInputCustodyBus()),
1468
+ handleInputCustodyHandoff(envelope, ctx) {
1469
+ return getRequired().acceptHandoff({ sourceRecoveryKey: envelope.sourceRecoveryKey,
1470
+ recipientBindingKey: envelope.recipientBindingKey,
1471
+ source: envelope.source, handoffId: envelope.handoffId }, ctx);
1472
+ },
1473
+ sourceReferenceAdmission: createTelegramBusFollowerSourceReferenceAdmissionRuntime({
1474
+ wakeSource(input, ctx) { getRequired().wakeSource(input, ctx); },
1475
+ }),
1476
+ resolveInputCustodyReference(input) {
1477
+ return deps.getInputCustodyBus()?.resolveForwardReference(input);
1478
+ },
1479
+ };
1480
+ }
1481
+ export function createTelegramBusFollowerSourceReferenceAdmissionRuntime(deps) {
1482
+ return {
1483
+ async admit(envelope, ctx) {
1484
+ const delivery = envelope.delivery;
1485
+ if (!delivery)
1486
+ throw new Error("Telegram follower source-reference admission requires delivery identity.");
1487
+ if (!delivery.sourceRecoveryKey)
1488
+ throw new Error("Telegram follower source-reference admission requires a recovery key.");
1489
+ if (!delivery.sourceClaim)
1490
+ throw new Error("Telegram follower source-reference admission requires exact claim evidence.");
1491
+ const expected = createTelegramBusFollowerDeliveryIdentity({ kind: envelope.kind,
1492
+ recipientBindingKey: delivery.recipientBindingKey,
1493
+ sourceUpdateId: delivery.sourceUpdateId });
1494
+ if (expected.deliveryId !== delivery.deliveryId)
1495
+ throw new Error("Invalid Telegram follower source-reference delivery id.");
1496
+ const carrier = envelope.kind === "leader.wakeInputCustody" ? undefined
1497
+ : envelope.kind === "leader.forwardCallback" ? envelope.query
1498
+ : envelope.kind === "leader.forwardReaction" ? envelope.reactionUpdate : envelope.message;
1499
+ if (envelope.kind !== "leader.wakeInputCustody" &&
1500
+ (!isRecord(carrier) || carrier.pi_telegram_source_update_id !== delivery.sourceUpdateId))
1501
+ throw new Error("Telegram follower source-reference update id mismatch.");
1502
+ await deps.wakeSource({ deliveryId: delivery.deliveryId,
1503
+ sourceUpdateId: delivery.sourceUpdateId,
1504
+ recipientBindingKey: delivery.recipientBindingKey,
1505
+ sourceRecoveryKey: delivery.sourceRecoveryKey,
1506
+ sourceClaim: { ...delivery.sourceClaim } }, ctx);
1507
+ return { deliveryId: delivery.deliveryId, sourceUpdateId: delivery.sourceUpdateId };
1508
+ },
1509
+ };
1510
+ }
1511
+ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1512
+ const server = createTelegramBusLocalServer({
1513
+ socketPath: deps.socketPath,
1514
+ recordTransportEvent(phase, details) {
1515
+ deps.recordRuntimeEvent?.("bus", `Telegram bus ${phase}`, {
1516
+ phase: `follower-receiver-${phase}`,
1517
+ ...details,
1518
+ });
1519
+ },
1520
+ async handleEnvelope(envelope) {
1521
+ const authSecret = deps.getAuthSecret?.();
1522
+ if (deps.getAuthSecret &&
1523
+ (!authSecret || !isTelegramBusEnvelopeAuthorized(envelope, authSecret))) {
1524
+ return createUnauthorizedBusAck(envelope.requestId);
1525
+ }
1526
+ if ((envelope.kind !== "leader.forwardCallback" &&
1527
+ envelope.kind !== "leader.forwardReaction" &&
1528
+ envelope.kind !== "leader.forwardMessage" &&
1529
+ envelope.kind !== "leader.forwardEditedMessage" &&
1530
+ envelope.kind !== "leader.wakeInputCustody" &&
1531
+ envelope.kind !== "leader.offerInputCustodyHandoff" &&
1532
+ envelope.kind !== "leader.replaceFollowerTarget" &&
1533
+ envelope.kind !== "leader.offerQueueHandoff") ||
1534
+ envelope.recipientInstanceId !== deps.instanceId) {
1535
+ return {
1536
+ kind: "bus.ack",
1537
+ requestId: envelope.requestId,
1538
+ ok: false,
1539
+ message: "Telegram bus receiver cannot handle this envelope.",
1540
+ };
1541
+ }
1542
+ const registrationGeneration = deps.getRegistrationGeneration();
1543
+ if (!registrationGeneration ||
1544
+ envelope.recipientRegistrationGeneration !== registrationGeneration) {
1545
+ return {
1546
+ kind: "bus.ack",
1547
+ requestId: envelope.requestId,
1548
+ ok: false,
1549
+ message: "Stale Telegram bus follower registration generation.",
1550
+ };
1551
+ }
1552
+ if (envelope.kind === "leader.offerInputCustodyHandoff" &&
1553
+ envelope.recipientBindingKey !== deps.getRecipientBindingKey())
1554
+ return {
1555
+ kind: "bus.ack", requestId: envelope.requestId, ok: false,
1556
+ message: "Mismatched Telegram follower handoff recipient identity.",
1557
+ };
1558
+ if (envelope.kind !== "leader.replaceFollowerTarget" &&
1559
+ envelope.kind !== "leader.offerQueueHandoff" &&
1560
+ envelope.kind !== "leader.offerInputCustodyHandoff" &&
1561
+ (!envelope.delivery ||
1562
+ envelope.delivery.recipientBindingKey !==
1563
+ deps.getRecipientBindingKey())) {
1564
+ return {
1565
+ kind: "bus.ack",
1566
+ requestId: envelope.requestId,
1567
+ ok: false,
1568
+ message: "Mismatched Telegram follower delivery identity.",
1569
+ };
1570
+ }
1571
+ const ctx = deps.getContext();
1572
+ if (!ctx) {
1573
+ return {
1574
+ kind: "bus.ack",
1575
+ requestId: envelope.requestId,
1576
+ ok: false,
1577
+ message: "Telegram bus follower has no active context.",
1578
+ };
1579
+ }
1580
+ try {
1581
+ if (envelope.kind === "leader.offerInputCustodyHandoff") {
1582
+ if (!(deps.isSourceReferenceAdmissionEnabled?.() ?? false))
1583
+ throw new Error("Telegram input custody handoff capability is not enabled.");
1584
+ if (!deps.hasAuthenticatedSourceReferenceTransport?.())
1585
+ throw new Error("Telegram input custody handoff requires authenticated transport.");
1586
+ if (!deps.handleInputCustodyHandoff)
1587
+ throw new Error("Telegram input custody handoff acceptance is unavailable.");
1588
+ const result = await deps.handleInputCustodyHandoff(envelope, ctx);
1589
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
1590
+ }
1591
+ if (envelope.kind !== "leader.replaceFollowerTarget" &&
1592
+ envelope.kind !== "leader.offerQueueHandoff") {
1593
+ const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
1594
+ (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
1595
+ if (sourceReferenceEnabled &&
1596
+ !deps.hasAuthenticatedSourceReferenceTransport?.())
1597
+ throw new Error("Telegram follower source-reference admission requires authenticated transport.");
1598
+ const admission = sourceReferenceEnabled
1599
+ ? deps.sourceReferenceAdmission : deps.durableAdmission;
1600
+ if (!admission)
1601
+ throw new Error("Telegram follower source-reference admission is enabled without a wake authority.");
1602
+ const receipt = await admission.admit(envelope, ctx);
1603
+ return {
1604
+ kind: "bus.ack",
1605
+ requestId: envelope.requestId,
1606
+ ok: true,
1607
+ result: receipt,
1608
+ };
1609
+ }
1610
+ if (envelope.kind === "leader.offerQueueHandoff") {
1611
+ if (!deps.handleQueueHandoff) {
1612
+ throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
1613
+ }
1614
+ const result = await deps.handleQueueHandoff(envelope, ctx);
1615
+ const receipt = envelope.payload.admissionReceipts[0];
1616
+ if (envelope.payload.admissionReceipts.length !== 1 ||
1617
+ !receipt ||
1618
+ result.status !== "staged" ||
1619
+ result.receiptId !== receipt.receiptId ||
1620
+ result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
1621
+ result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
1622
+ throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
1623
+ }
1624
+ return {
1625
+ kind: "bus.ack",
1626
+ requestId: envelope.requestId,
1627
+ ok: true,
1628
+ result,
1629
+ };
1630
+ }
1631
+ {
1632
+ if (!deps.handleReplaceTarget) {
1633
+ throw new Error("Telegram bus receiver cannot replace follower target.");
1634
+ }
1635
+ await deps.handleReplaceTarget({
1636
+ target: envelope.target,
1637
+ ...(envelope.oldTarget ? { oldTarget: envelope.oldTarget } : {}),
1638
+ reason: envelope.reason,
1639
+ registrationGeneration,
1640
+ }, ctx);
1641
+ }
1642
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
1643
+ }
1644
+ catch (error) {
1645
+ deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
1646
+ return {
1647
+ kind: "bus.ack",
1648
+ requestId: envelope.requestId,
1649
+ ok: false,
1650
+ message: error instanceof Error
1651
+ ? error.message
1652
+ : "Telegram bus follower dispatch failed.",
1653
+ };
1654
+ }
1655
+ },
1656
+ });
1657
+ return server;
1658
+ }
1659
+ function parseRegistrationResult(value) {
1660
+ if (!isRecord(value))
1661
+ return {};
1662
+ const target = parseTarget(isRecord(value.target) ? value.target : value);
1663
+ return {
1664
+ ...(target ? { target } : {}),
1665
+ ...(typeof value.slot === "string" ? { slot: value.slot } : {}),
1666
+ ...(typeof value.threadName === "string"
1667
+ ? { threadName: value.threadName }
1668
+ : {}),
1669
+ ...(typeof value.displayTitle === "string"
1670
+ ? { displayTitle: value.displayTitle }
1671
+ : {}),
1672
+ };
1673
+ }
1674
+ function parseTarget(value) {
1675
+ if (value === undefined)
1676
+ return undefined;
1677
+ if (!isRecord(value) || typeof value.chatId !== "number")
1678
+ return undefined;
1679
+ const target = { chatId: value.chatId };
1680
+ if (typeof value.threadId === "number")
1681
+ target.threadId = value.threadId;
1682
+ return target;
1683
+ }
1684
+ function isRecord(value) {
1685
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1686
+ }