@llblab/pi-telegram 0.51.5 → 0.52.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 (181) hide show
  1. package/AGENTS.md +10 -8
  2. package/BACKLOG.md +1 -8
  3. package/CHANGELOG.md +23 -0
  4. package/LICENSE +21 -0
  5. package/README.md +16 -6
  6. package/dist/lib/activity-verbosity.d.ts +1 -5
  7. package/dist/lib/activity-verbosity.js +5 -7
  8. package/dist/lib/bindings.d.ts +9 -2
  9. package/dist/lib/bindings.js +22 -13
  10. package/dist/lib/bus-api.js +4 -2
  11. package/dist/lib/bus-follower.d.ts +55 -57
  12. package/dist/lib/bus-follower.js +213 -140
  13. package/dist/lib/bus-leader.d.ts +21 -2
  14. package/dist/lib/bus-leader.js +181 -25
  15. package/dist/lib/bus-transport.d.ts +4 -1
  16. package/dist/lib/bus-transport.js +28 -4
  17. package/dist/lib/bus.d.ts +52 -56
  18. package/dist/lib/bus.js +78 -246
  19. package/dist/lib/channel-posts.d.ts +1 -3
  20. package/dist/lib/channel-posts.js +23 -26
  21. package/dist/lib/command-templates.d.ts +0 -5
  22. package/dist/lib/command-templates.js +5 -5
  23. package/dist/lib/commands.d.ts +12 -26
  24. package/dist/lib/commands.js +152 -98
  25. package/dist/lib/config.d.ts +0 -17
  26. package/dist/lib/config.js +12 -14
  27. package/dist/lib/delivery.d.ts +0 -4
  28. package/dist/lib/delivery.js +2 -5
  29. package/dist/lib/extension.js +281 -61
  30. package/dist/lib/generative-apps.d.ts +0 -19
  31. package/dist/lib/inbound.d.ts +0 -2
  32. package/dist/lib/inbound.js +2 -2
  33. package/dist/lib/journal.d.ts +177 -16
  34. package/dist/lib/journal.js +1115 -249
  35. package/dist/lib/keyboard.d.ts +0 -2
  36. package/dist/lib/keyboard.js +2 -2
  37. package/dist/lib/lifecycle.d.ts +57 -2
  38. package/dist/lib/lifecycle.js +110 -5
  39. package/dist/lib/locks.d.ts +100 -12
  40. package/dist/lib/locks.js +380 -14
  41. package/dist/lib/logging.d.ts +30 -12
  42. package/dist/lib/logging.js +129 -72
  43. package/dist/lib/media.d.ts +28 -5
  44. package/dist/lib/media.js +26 -8
  45. package/dist/lib/menu-model.d.ts +0 -11
  46. package/dist/lib/menu-model.js +8 -8
  47. package/dist/lib/menu-settings.d.ts +0 -18
  48. package/dist/lib/menu-settings.js +18 -18
  49. package/dist/lib/menu-status.d.ts +4 -4
  50. package/dist/lib/menu-status.js +12 -7
  51. package/dist/lib/menu-thinking.d.ts +1 -1
  52. package/dist/lib/menu-thinking.js +1 -1
  53. package/dist/lib/menu.d.ts +1 -0
  54. package/dist/lib/menu.js +4 -3
  55. package/dist/lib/model.js +2 -4
  56. package/dist/lib/outbound-attachments.d.ts +0 -1
  57. package/dist/lib/outbound-attachments.js +1 -1
  58. package/dist/lib/outbound-buttons.js +1 -8
  59. package/dist/lib/outbound-markup.d.ts +1 -6
  60. package/dist/lib/outbound-markup.js +4 -21
  61. package/dist/lib/outbound.d.ts +0 -8
  62. package/dist/lib/outbound.js +5 -5
  63. package/dist/lib/paths.d.ts +50 -7
  64. package/dist/lib/paths.js +155 -17
  65. package/dist/lib/pi.d.ts +1 -0
  66. package/dist/lib/pi.js +2 -1
  67. package/dist/lib/polling.d.ts +0 -5
  68. package/dist/lib/polling.js +5 -5
  69. package/dist/lib/preview.d.ts +0 -3
  70. package/dist/lib/preview.js +3 -3
  71. package/dist/lib/prompt-templates.d.ts +0 -1
  72. package/dist/lib/prompt-templates.js +1 -1
  73. package/dist/lib/queue.d.ts +3 -15
  74. package/dist/lib/queue.js +69 -8
  75. package/dist/lib/recovery.d.ts +29 -9
  76. package/dist/lib/recovery.js +104 -33
  77. package/dist/lib/replies.d.ts +0 -4
  78. package/dist/lib/replies.js +1 -1
  79. package/dist/lib/routing.d.ts +73 -8
  80. package/dist/lib/routing.js +2206 -307
  81. package/dist/lib/sections.d.ts +0 -2
  82. package/dist/lib/sections.js +1 -1
  83. package/dist/lib/status.d.ts +46 -9
  84. package/dist/lib/status.js +179 -27
  85. package/dist/lib/sync.d.ts +0 -5
  86. package/dist/lib/sync.js +9 -8
  87. package/dist/lib/telegram-api.d.ts +11 -3
  88. package/dist/lib/telegram-api.js +34 -23
  89. package/dist/lib/thread-cleanup-manager.d.ts +1 -0
  90. package/dist/lib/thread-cleanup-manager.js +18 -21
  91. package/dist/lib/thread-naming.d.ts +32 -5
  92. package/dist/lib/thread-naming.js +191 -7
  93. package/dist/lib/thread-reconciler.d.ts +2 -0
  94. package/dist/lib/thread-reconciler.js +1 -1
  95. package/dist/lib/threads.d.ts +292 -59
  96. package/dist/lib/threads.js +1664 -373
  97. package/dist/lib/turns.d.ts +0 -1
  98. package/dist/lib/turns.js +1 -1
  99. package/dist/lib/updates.d.ts +184 -28
  100. package/dist/lib/updates.js +1032 -114
  101. package/dist/lib/wire.d.ts +12 -0
  102. package/dist/lib/wire.js +21 -0
  103. package/dist/lib/workspace-admission.d.ts +25 -17
  104. package/dist/lib/workspace-admission.js +95 -20
  105. package/dist/lib/workspace-identity.d.ts +20 -0
  106. package/dist/lib/workspace-identity.js +103 -0
  107. package/dist/lib/workspace-retirement.d.ts +45 -10
  108. package/dist/lib/workspace-retirement.js +196 -64
  109. package/dist/package.json +6 -6
  110. package/dist/skills/generated-control-surface/SKILL.md +3 -3
  111. package/dist/skills/telegram-bridge/references/diagnosis.md +3 -3
  112. package/docs/README.md +1 -1
  113. package/docs/activity.md +1 -1
  114. package/docs/architecture.md +158 -39
  115. package/docs/callback-namespaces.md +1 -1
  116. package/docs/delivery.md +1 -1
  117. package/docs/generative-apps.md +2 -2
  118. package/docs/inbound.md +1 -1
  119. package/docs/multi-instance-bus.md +127 -19
  120. package/docs/public-api.md +5 -3
  121. package/docs/ui-style.md +26 -8
  122. package/docs/updates.md +1 -1
  123. package/lib/activity-verbosity.ts +5 -9
  124. package/lib/bindings.ts +23 -10
  125. package/lib/bus-api.ts +5 -2
  126. package/lib/bus-follower.ts +251 -226
  127. package/lib/bus-leader.ts +178 -31
  128. package/lib/bus-transport.ts +30 -8
  129. package/lib/bus.ts +110 -334
  130. package/lib/channel-posts.ts +29 -29
  131. package/lib/command-templates.ts +5 -5
  132. package/lib/commands.ts +150 -112
  133. package/lib/config.ts +12 -17
  134. package/lib/delivery.ts +2 -8
  135. package/lib/extension.ts +282 -72
  136. package/lib/generative-apps.ts +0 -22
  137. package/lib/inbound.ts +2 -2
  138. package/lib/journal.ts +1182 -326
  139. package/lib/keyboard.ts +2 -2
  140. package/lib/lifecycle.ts +141 -5
  141. package/lib/locks.ts +402 -23
  142. package/lib/logging.ts +151 -101
  143. package/lib/media.ts +51 -9
  144. package/lib/menu-model.ts +8 -8
  145. package/lib/menu-settings.ts +18 -18
  146. package/lib/menu-status.ts +11 -0
  147. package/lib/menu-thinking.ts +2 -2
  148. package/lib/menu.ts +5 -1
  149. package/lib/model.ts +2 -4
  150. package/lib/outbound-attachments.ts +1 -1
  151. package/lib/outbound-buttons.ts +1 -10
  152. package/lib/outbound-markup.ts +5 -24
  153. package/lib/outbound.ts +5 -5
  154. package/lib/paths.ts +177 -17
  155. package/lib/pi.ts +5 -2
  156. package/lib/polling.ts +5 -5
  157. package/lib/preview.ts +3 -3
  158. package/lib/prompt-templates.ts +1 -1
  159. package/lib/queue.ts +67 -9
  160. package/lib/recovery.ts +104 -44
  161. package/lib/replies.ts +1 -1
  162. package/lib/routing.ts +1955 -375
  163. package/lib/sections.ts +1 -1
  164. package/lib/status.ts +185 -36
  165. package/lib/sync.ts +8 -8
  166. package/lib/telegram-api.ts +48 -33
  167. package/lib/thread-cleanup-manager.ts +24 -21
  168. package/lib/thread-naming.ts +267 -9
  169. package/lib/thread-reconciler.ts +3 -1
  170. package/lib/threads.ts +1718 -491
  171. package/lib/turns.ts +1 -1
  172. package/lib/updates.ts +1072 -151
  173. package/lib/wire.ts +28 -0
  174. package/lib/workspace-admission.ts +97 -47
  175. package/lib/workspace-identity.ts +147 -0
  176. package/lib/workspace-retirement.ts +234 -102
  177. package/package.json +6 -6
  178. package/scripts/audit-exports.mjs +100 -0
  179. package/scripts/check-downgrade.mjs +80 -43
  180. package/skills/generated-control-surface/SKILL.md +3 -3
  181. package/skills/telegram-bridge/references/diagnosis.md +3 -3
@@ -6,21 +6,23 @@
6
6
  * It must not spawn Pi processes or create hidden Telegram-originated instances.
7
7
  */
8
8
  import { basename } from "node:path";
9
- import * as Sync from "./sync.js";
9
+ import { isDeepStrictEqual } from "node:util";
10
10
  import * as Threads from "./threads.js";
11
+ import * as WorkspaceIdentity from "./workspace-identity.js";
12
+ import { isWireRecord as isRecord } from "./wire.js";
11
13
  import { parseTelegramUpdateJournalQueueOwner } from "./journal.js";
12
14
  import { TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS, } from "./locks.js";
13
15
  import { isTelegramApiMethodRetrySafe, TelegramApiCommitUnknownError, TelegramApiStaleTargetError, } from "./telegram-api.js";
14
- 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, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, } from "./bus.js";
16
+ import { createTelegramBusFollowerDeliveryIdentity, 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, TELEGRAM_BUS_CAPABILITY_SESSION_REPLACEMENT_INTENT, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE, } from "./bus.js";
15
17
  import { getTelegramBusTransportRetryPolicy, TELEGRAM_BUS_REGISTRATION_RETRY, } from "./bus-transport.js";
16
18
  import { createTelegramWorkspaceAdmissionOperationId, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
17
- export const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
18
- export const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
19
- export const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
20
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
19
+ const TELEGRAM_BUS_FOLLOWER_PROMOTION_GRACE_MS = 2_500;
20
+ const TELEGRAM_FOLLOWER_SESSION_HANDOFF_TTL_MS = 30_000;
21
+ const TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS = 30_000;
22
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_WAIT_MS = 30_000;
21
23
  export const TELEGRAM_BUS_FOLLOWER_HEARTBEAT_TIMEOUT_MS = TELEGRAM_BUS_LEADER_STALE_HEARTBEAT_MS;
22
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
23
- export const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
24
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_ATTEMPTS = TELEGRAM_BUS_REGISTRATION_RETRY.attempts;
25
+ const TELEGRAM_BUS_FOLLOWER_REGISTRATION_RETRY_DELAY_MS = TELEGRAM_BUS_REGISTRATION_RETRY.delayMs;
24
26
  const TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY = "__piTelegramFollowerSessionHandoff";
25
27
  export function getTelegramFollowerSessionHandoff() {
26
28
  const value = globalThis[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY];
@@ -44,7 +46,7 @@ export function setTelegramFollowerSessionHandoff(handoff) {
44
46
  else
45
47
  store[TELEGRAM_FOLLOWER_SESSION_HANDOFF_KEY] = handoff;
46
48
  }
47
- export function isTelegramFollowerSessionHandoffFresh(handoff, options = {}) {
49
+ function isTelegramFollowerSessionHandoffFresh(handoff, options = {}) {
48
50
  if (!handoff)
49
51
  return false;
50
52
  const pid = options.pid ?? process.pid;
@@ -147,16 +149,21 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
147
149
  (ctx === undefined || readyContext.ctx === ctx) &&
148
150
  ports.registrationState.getGeneration() === readyContext.generation &&
149
151
  ports.registration.getSessionGeneration?.() === readyContext.sessionGeneration &&
152
+ ports.registration.getSessionId?.(readyContext.ctx) === readyContext.sessionId &&
150
153
  ports.registration.isContextActive?.(readyContext.ctx) !== false);
151
154
  const prepareContext = async (ctx) => {
152
155
  const generation = ports.registrationState.getGeneration();
153
156
  const sessionGeneration = ports.registration.getSessionGeneration?.();
157
+ const sessionId = ports.registration.getSessionId?.(ctx);
154
158
  readyContext = undefined;
159
+ if (!generation || (sessionId !== undefined && ports.registrationState.getSessionId(sessionId) !== sessionId))
160
+ throw new Error("Telegram follower session change requires acknowledged leader registration.");
155
161
  await ports.registration.onRegistered?.(ctx);
156
162
  if (generation && ports.registrationState.getGeneration() === generation &&
157
163
  ports.registration.getSessionGeneration?.() === sessionGeneration &&
164
+ ports.registration.getSessionId?.(ctx) === sessionId &&
158
165
  ports.registration.isContextActive?.(ctx) !== false)
159
- readyContext = { generation, ctx, sessionGeneration };
166
+ readyContext = { generation, ctx, sessionGeneration, sessionId };
160
167
  };
161
168
  const sharedRuntimeDeps = {
162
169
  instanceId: ports.instanceId,
@@ -170,10 +177,6 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
170
177
  getRegistrationGeneration() {
171
178
  return isReadyContext() ? readyContext?.generation : undefined;
172
179
  },
173
- handleReplaceTarget: createTelegramBusFollowerTargetReplacementHandler({
174
- ...ports.targetReplacement,
175
- ...sharedRuntimeDeps,
176
- }),
177
180
  });
178
181
  let registration;
179
182
  const recovery = createTelegramBusFollowerHeartbeatRecoveryHandler({
@@ -205,83 +208,123 @@ export function createTelegramBusFollowerRuntimeAssembly(ports) {
205
208
  await prepareContext(ctx);
206
209
  },
207
210
  };
208
- return { receiver, registration };
211
+ return { receiver, registration, getReadySessionId: () => isReadyContext() ? readyContext?.sessionId : undefined };
209
212
  }
210
- export function createTelegramBusFollowerTargetReplacementHandler(deps) {
211
- const getNowMs = deps.getNowMs ?? Date.now;
212
- return (input, ctx) => runTelegramBusFollowerWorkspaceMutation(deps, "workspace.replace-follower-target", async () => {
213
- const assertCurrent = (expectedTarget = input.oldTarget) => {
214
- const target = deps.registrationState.getTarget();
215
- if (!input.registrationGeneration ||
216
- deps.registrationState.getGeneration() !==
217
- input.registrationGeneration ||
218
- !input.oldTarget ||
219
- !expectedTarget ||
220
- target?.chatId !== expectedTarget.chatId ||
221
- target.threadId !== expectedTarget.threadId ||
222
- input.target.chatId !== input.oldTarget.chatId ||
223
- input.target.threadId === input.oldTarget.threadId) {
224
- throw new Error("Stale Telegram follower target replacement authority.");
225
- }
213
+ /** Binds Restore to real Pi lifetime and the current authenticated transport owner, not request fields. */
214
+ export function createTelegramBusFollowerRestoreContextGetter(deps) {
215
+ return (ctx) => {
216
+ if (!deps.isContextCurrent(ctx))
217
+ return undefined;
218
+ const leader = deps.getLeaderState();
219
+ const secret = deps.getAuthenticatedSecret();
220
+ const leaderProtocol = deps.getLeaderProtocol();
221
+ if (!leaderProtocol || !hasTelegramBusCapability(leaderProtocol, TELEGRAM_BUS_CAPABILITY_WORKSPACE_RESTORE))
222
+ return undefined;
223
+ if (leader.kind !== "active-elsewhere" || !secret || leader.lock.busSecret !== secret ||
224
+ !leader.lock.instanceId || leader.lock.leaderEpoch === undefined)
225
+ return undefined;
226
+ const profileBindingKey = deps.getProfileBindingKey();
227
+ const operatorUserId = deps.getOperatorUserId();
228
+ const sessionId = deps.getSessionId(ctx);
229
+ const cwd = deps.getCwd(ctx);
230
+ const generation = deps.getGeneration();
231
+ if (!profileBindingKey || !sessionId || !cwd || typeof operatorUserId !== "number" ||
232
+ !Number.isSafeInteger(operatorUserId) || operatorUserId <= 0 ||
233
+ !Number.isSafeInteger(generation) || generation < 0 || !deps.isContextCurrent(ctx))
234
+ return undefined;
235
+ return { executor: { instanceId: leader.lock.instanceId, leaderEpoch: String(leader.lock.leaderEpoch) },
236
+ profileBindingKey, operatorUserId, sessionId, cwd, generation, leaderProtocol: structuredClone(leaderProtocol) };
237
+ };
238
+ }
239
+ /** Prepared receiver: its caller authenticates the envelope; no canonical writes, dispatch or cleanup. */
240
+ export function createTelegramBusFollowerWorkspaceRestoreHandler(deps) {
241
+ return async (input, ctx) => {
242
+ const captured = deps.getContextAuthority(ctx);
243
+ if (!captured)
244
+ throw new Error("Stale Telegram follower Restore context.");
245
+ const authority = structuredClone(captured);
246
+ const request = structuredClone(input);
247
+ if (!authority.profileBindingKey || !authority.sessionId || !authority.cwd ||
248
+ !authority.executor?.instanceId || !authority.executor.leaderEpoch ||
249
+ !(typeof authority.generation === "string" ? authority.generation.length > 0 :
250
+ Number.isSafeInteger(authority.generation) && authority.generation >= 0) ||
251
+ !Number.isSafeInteger(authority.operatorUserId) || authority.operatorUserId <= 0 ||
252
+ !request.operationId || !request.registrationGeneration || !["apply", "inspect"].includes(request.mode))
253
+ throw new Error("Invalid Telegram follower Restore authority.");
254
+ const assertCurrent = () => {
255
+ if (!isDeepStrictEqual(deps.getContextAuthority(ctx), authority) ||
256
+ deps.registrationState.getGeneration() !== request.registrationGeneration)
257
+ throw new Error("Stale Telegram follower Restore authority.");
226
258
  };
227
259
  assertCurrent();
228
- await deps.topicTargetStore.load();
229
- assertCurrent();
230
- const nowMs = getNowMs();
231
- const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
232
- records: deps.topicTargetStore.list(),
233
- instanceId: deps.instanceId,
234
- preferredTarget: input.oldTarget,
235
- });
236
- if (currentRecord &&
237
- (currentRecord.target.chatId !== input.oldTarget.chatId ||
238
- currentRecord.target.threadId !== input.oldTarget.threadId)) {
239
- throw new Error("Stale Telegram follower binding replacement target.");
240
- }
241
- const currentSlot = currentRecord?.slot ?? deps.registrationState.getSlot();
242
- if (!currentSlot || !/^[A-Z]$/u.test(currentSlot)) {
243
- throw new Error("Telegram Thread slot authority is unavailable.");
244
- }
245
- deps.topicTargetStore.markStaleByTarget(input.oldTarget, "deleted", "Follower thread was replaced by thread restore.");
246
- const profileKey = currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
247
- deps.topicTargetStore.upsert({
248
- profileKey,
249
- owner: {
250
- kind: "manual-follower",
251
- instanceId: deps.manualFollowerOwnerId,
252
- },
253
- target: input.target,
254
- status: "active",
255
- syncStatus: "open",
256
- createdAtMs: currentRecord?.createdAtMs ?? nowMs,
257
- updatedAtMs: nowMs,
258
- lastSyncObservedAtMs: nowMs,
259
- lastReconcileAction: "follower-thread-restore",
260
- instanceId: deps.instanceId,
261
- slot: currentSlot,
262
- threadName: currentRecord?.threadName,
263
- rerouteConfirmedAtMs: nowMs,
264
- });
265
- deps.registrationState.setRegistered(true, input.target, {
266
- slot: currentSlot,
267
- threadName: currentRecord?.threadName,
268
- generation: input.registrationGeneration,
269
- });
270
- await deps.topicTargetStore.persist();
271
- assertCurrent(input.target);
272
- deps.setSyncState(Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
273
- nowMs,
274
- action: "follower-thread-restore",
275
- }));
276
- deps.updateStatus(ctx);
277
- deps.recordRuntimeEvent?.("bus", "Telegram follower thread target replaced", {
278
- phase: "follower-thread-restore",
279
- chatId: input.target.chatId,
280
- threadId: input.target.threadId,
281
- oldThreadId: input.oldTarget?.threadId ?? currentRecord?.target.threadId,
282
- slot: currentSlot,
260
+ if (!deps.getWorkspaceAdmission)
261
+ throw new Error("Telegram Workspace admission authority is unavailable.");
262
+ const result = await runTelegramBusFollowerWorkspaceMutation(deps, "workspace.restore-follower-target", async () => {
263
+ assertCurrent();
264
+ const intent = deps.readRestoreIntent(request.operationId, authority.profileBindingKey);
265
+ assertCurrent();
266
+ if (!intent || intent.request.operationId !== request.operationId ||
267
+ (intent.phase !== "recipient-issued" && intent.phase !== "ready") ||
268
+ !isDeepStrictEqual(intent.executor, authority.executor) ||
269
+ intent.operatorUserId !== authority.operatorUserId || !intent.recipient ||
270
+ (request.mode === "apply" && (intent.recipient.kind !== "follower" || intent.recipient.instanceId !== deps.instanceId ||
271
+ intent.recipient.generation !== request.registrationGeneration)) ||
272
+ intent.recipient.sessionId !== authority.sessionId || intent.request.binding.sessionId !== authority.sessionId ||
273
+ intent.request.binding.cwd !== WorkspaceIdentity.normalizeTelegramWorkspacePath(authority.cwd))
274
+ throw new Error("Telegram follower Restore intent does not match this recipient.");
275
+ const expected = structuredClone(intent);
276
+ await deps.topicTargetStore.load();
277
+ assertCurrent();
278
+ const observe = (canonical) => {
279
+ if (!isDeepStrictEqual(deps.readRestoreIntent(request.operationId, authority.profileBindingKey), expected))
280
+ throw new Error("Telegram follower Restore intent changed.");
281
+ assertCurrent();
282
+ const { binding, target } = expected.request;
283
+ const bindings = (canonical.workspaceBindings ?? []).filter(value => value.bindingKey === binding.bindingKey);
284
+ const snapshot = canonical.threads;
285
+ const records = snapshot.filter(value => value.instanceId === deps.instanceId);
286
+ const owners = snapshot.filter(value => value.status === "active" &&
287
+ (value.slot === binding.slot || isDeepStrictEqual(value.target, target)));
288
+ if (bindings.length !== 1 || records.length !== 1 || owners.length !== 1 || bindings[0]?.cwd !== binding.cwd ||
289
+ bindings[0]?.sessionId !== binding.sessionId || bindings[0]?.slot !== binding.slot || bindings[0]?.inactiveSinceMs !== undefined ||
290
+ !isDeepStrictEqual(bindings[0]?.target, target) || records[0]?.status !== "active" ||
291
+ records[0]?.owner?.kind !== "manual-follower" ||
292
+ records[0]?.slot !== binding.slot || !isDeepStrictEqual(records[0]?.target, target) ||
293
+ deps.registrationState.getSlot() !== binding.slot)
294
+ throw new Error("Telegram follower Restore canonical binding is not committed.");
295
+ const localTarget = deps.registrationState.getTarget();
296
+ const alreadyReady = isDeepStrictEqual(localTarget, target);
297
+ if (!alreadyReady && !isDeepStrictEqual(localTarget, binding.target))
298
+ throw new Error("Telegram follower Restore local target changed.");
299
+ assertCurrent();
300
+ if (!alreadyReady && request.mode === "apply") {
301
+ if (expected.phase !== "recipient-issued")
302
+ throw new Error("Telegram follower Restore readiness regressed.");
303
+ deps.registrationState.setRegistered(true, target, { slot: binding.slot,
304
+ threadName: records[0]?.threadName, generation: request.registrationGeneration,
305
+ leaderProtocol: deps.registrationState.getLeaderProtocol() });
306
+ }
307
+ assertCurrent();
308
+ const observedTarget = deps.registrationState.getTarget();
309
+ if (!observedTarget || (!isDeepStrictEqual(observedTarget, target) && !isDeepStrictEqual(observedTarget, binding.target)))
310
+ throw new Error("Telegram follower Restore local target changed.");
311
+ const ready = isDeepStrictEqual(observedTarget, target);
312
+ assertCurrent();
313
+ return { operationId: request.operationId, recipient: { kind: "follower", instanceId: deps.instanceId,
314
+ sessionId: authority.sessionId, generation: request.registrationGeneration },
315
+ target: { chatId: observedTarget.chatId, threadId: observedTarget.threadId }, slot: binding.slot, ready };
316
+ };
317
+ let result;
318
+ deps.topicTargetStore.withWorkspaceRestoreSnapshot(expected, snapshot => { result = observe(snapshot); });
319
+ if (!result)
320
+ throw new Error("Telegram follower Restore observation was not completed.");
321
+ return result;
283
322
  });
284
- });
323
+ assertCurrent();
324
+ if (!isDeepStrictEqual(deps.registrationState.getTarget(), result.target) || deps.registrationState.getSlot() !== result.slot)
325
+ throw new Error("Telegram follower Restore local registration changed after admission release.");
326
+ return result;
327
+ };
285
328
  }
286
329
  export function createTelegramBusFollowerClientRuntime(deps) {
287
330
  const createRequestId = createTelegramBusRequestIdFactory(deps.instanceId);
@@ -319,10 +362,6 @@ export function createTelegramBusFollowerClientRuntime(deps) {
319
362
  getAuthSecret: deps.getApiAuthSecret,
320
363
  getRegistrationGeneration: deps.getRegistrationGeneration,
321
364
  }),
322
- targetController: createTelegramBusFollowerTargetController({
323
- ...sharedClientDeps,
324
- getAuthSecret: deps.getForwardingAuthSecret,
325
- }),
326
365
  };
327
366
  }
328
367
  export function createTelegramBusFollowerQueueHandoffClient(deps) {
@@ -377,7 +416,7 @@ export function createTelegramBusFollowerQueueHandoffClient(deps) {
377
416
  : "Telegram queue handoff did not return an acknowledgement.");
378
417
  };
379
418
  }
380
- export function createTelegramBusAgentMessageClient(deps) {
419
+ function createTelegramBusAgentMessageClient(deps) {
381
420
  const getNowMs = deps.getNowMs ?? Date.now;
382
421
  const timeoutMs = deps.timeoutMs ?? TELEGRAM_BUS_FOLLOWER_CLIENT_TIMEOUT_MS;
383
422
  const request = async (envelope, requestTimeoutMs = timeoutMs) => {
@@ -519,7 +558,13 @@ function isTelegramStaleContextError(error) {
519
558
  export function createTelegramBusFollowerSessionReplacementSuspender(deps) {
520
559
  const getNowMs = deps.getNowMs ?? Date.now;
521
560
  const getPid = deps.getPid ?? (() => process.pid);
522
- return async () => {
561
+ return async (preserveTarget = true) => {
562
+ if (!preserveTarget) {
563
+ setTelegramFollowerSessionHandoff(undefined);
564
+ Threads.setTelegramLeaderSessionHandoff(undefined);
565
+ await deps.suspendPolling();
566
+ return;
567
+ }
523
568
  const target = deps.registrationState.getTarget();
524
569
  if (deps.registrationState.isRegistered() && target) {
525
570
  setTelegramFollowerSessionHandoff({
@@ -646,6 +691,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
646
691
  let threadName;
647
692
  let displayTitle;
648
693
  let generation;
694
+ let sessionId;
649
695
  let leaderProtocol;
650
696
  let eligibleElectionSlots = [];
651
697
  let recoveryEpoch = 0;
@@ -671,6 +717,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
671
717
  return true;
672
718
  },
673
719
  getGeneration: () => generation,
720
+ getSessionId: currentSessionId => registered && generation && currentSessionId === sessionId ? sessionId : undefined,
674
721
  beginRecovery: () => {
675
722
  if (activeRecoveryEpoch !== undefined)
676
723
  return activeRecoveryEpoch;
@@ -714,6 +761,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
714
761
  generation === metadata?.generation &&
715
762
  target?.chatId === nextTarget?.chatId && target?.threadId === nextTarget?.threadId
716
763
  ? displayTitle : undefined;
764
+ const retainedSessionId = next && registered && generation === metadata?.generation ? sessionId : undefined;
717
765
  const availabilityChanged = registered !== next;
718
766
  registered = next;
719
767
  target = next ? (nextTarget ? { ...nextTarget } : undefined) : undefined;
@@ -723,6 +771,7 @@ export function createTelegramBusFollowerRegistrationState(options = {}) {
723
771
  metadata.displayTitle?.trim() && metadata.displayTitle.length <= 128
724
772
  ? metadata.displayTitle : retainedDisplayTitle;
725
773
  generation = next ? metadata?.generation : undefined;
774
+ sessionId = next ? metadata?.sessionId ?? retainedSessionId : undefined;
726
775
  leaderProtocol =
727
776
  next && metadata?.leaderProtocol
728
777
  ? {
@@ -1080,10 +1129,12 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1080
1129
  if (deps.isContextActive?.(ctx) === false)
1081
1130
  return false;
1082
1131
  const sessionGeneration = deps.getSessionGeneration?.();
1083
- const attempt = { ctx, sessionGeneration };
1132
+ const sessionId = deps.getSessionId?.(ctx);
1133
+ const attempt = { ctx, sessionGeneration, sessionId };
1084
1134
  registrationAttempt = attempt;
1085
1135
  const isCurrentRequest = () => registrationAttempt === attempt &&
1086
- deps.getSessionGeneration?.() === sessionGeneration && deps.isContextActive?.(ctx) !== false;
1136
+ deps.getSessionGeneration?.() === sessionGeneration && deps.getSessionId?.(ctx) === sessionId &&
1137
+ deps.isContextActive?.(ctx) !== false;
1087
1138
  const abandonOwnedRequest = async () => {
1088
1139
  if (registrationAttempt !== attempt)
1089
1140
  return;
@@ -1141,7 +1192,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1141
1192
  ? { slot: deps.registrationState?.getSlot() ?? lastKnownSlot }
1142
1193
  : {}),
1143
1194
  cwd: ctx.cwd,
1144
- sessionId: deps.getSessionId?.(ctx),
1195
+ sessionId,
1145
1196
  pid: getPid(),
1146
1197
  processBirthId: deps.getProcessBirthId?.(),
1147
1198
  sessionGeneration,
@@ -1197,7 +1248,9 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1197
1248
  local: deps.protocolIdentity,
1198
1249
  remote: response?.kind === "bus.ack" ? response.protocol : undefined,
1199
1250
  });
1200
- if (!compatibility.compatible) {
1251
+ // An identity-less error ACK is a rejection/transport failure, not evidence of an incompatible leader.
1252
+ const unidentifiedRejection = response?.kind === "bus.ack" && !response.ok && response.protocol === undefined;
1253
+ if (!compatibility.compatible && !unidentifiedRejection) {
1201
1254
  stopHeartbeat();
1202
1255
  activeLeaderSocketPath = undefined;
1203
1256
  activeAuthSecret = undefined;
@@ -1229,6 +1282,7 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1229
1282
  deps.registrationState?.setRegistered(true, registrationResult.target, {
1230
1283
  ...registrationResult,
1231
1284
  generation: registrationGeneration,
1285
+ sessionId,
1232
1286
  ...(response.protocol
1233
1287
  ? { leaderProtocol: response.protocol }
1234
1288
  : {}),
@@ -1283,7 +1337,8 @@ export function createTelegramBusFollowerRegistrationRuntime(deps) {
1283
1337
  },
1284
1338
  setContext(ctx) {
1285
1339
  if (registrationAttempt && (registrationAttempt.ctx !== ctx ||
1286
- registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.()))
1340
+ registrationAttempt.sessionGeneration !== deps.getSessionGeneration?.() ||
1341
+ registrationAttempt.sessionId !== deps.getSessionId?.(ctx)))
1287
1342
  registrationAttempt = undefined;
1288
1343
  activeContext = ctx;
1289
1344
  },
@@ -1551,6 +1606,17 @@ export function prepareTelegramBusFollowerJournaledUpdateForExecution(update, pr
1551
1606
  return prepared;
1552
1607
  }
1553
1608
  export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
1609
+ // The journal forgets a delivery once its worker completes; this window absorbs a leader retry after a lost ACK.
1610
+ const recentDeliveries = new Map();
1611
+ const getNowMs = deps.getNowMs ?? Date.now;
1612
+ const limit = deps.recentDeliveryLimit ?? { maxAgeMs: 24 * 60 * 60 * 1000, maxEntries: 4096 };
1613
+ const pruneRecentDeliveries = (nowMs) => {
1614
+ for (const [deliveryId, admittedAtMs] of recentDeliveries) {
1615
+ if (recentDeliveries.size <= limit.maxEntries && nowMs - admittedAtMs < limit.maxAgeMs)
1616
+ break;
1617
+ recentDeliveries.delete(deliveryId);
1618
+ }
1619
+ };
1554
1620
  return {
1555
1621
  async admit(envelope, ctx) {
1556
1622
  const delivery = envelope.delivery;
@@ -1592,12 +1658,18 @@ export function createTelegramBusFollowerDurableAdmissionRuntime(deps) {
1592
1658
  ? { message: carrier }
1593
1659
  : { edited_message: carrier }),
1594
1660
  };
1595
- deps.journal.appendBatch([update]);
1596
- deps.signalWorker(ctx);
1597
- return {
1661
+ const result = {
1598
1662
  deliveryId: delivery.deliveryId,
1599
1663
  sourceUpdateId: delivery.sourceUpdateId,
1600
1664
  };
1665
+ pruneRecentDeliveries(getNowMs());
1666
+ if (recentDeliveries.has(delivery.deliveryId))
1667
+ return result;
1668
+ deps.journal.appendBatch([update]);
1669
+ recentDeliveries.set(delivery.deliveryId, getNowMs());
1670
+ pruneRecentDeliveries(getNowMs());
1671
+ deps.signalWorker(ctx);
1672
+ return result;
1601
1673
  },
1602
1674
  };
1603
1675
  }
@@ -1674,7 +1746,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1674
1746
  envelope.kind !== "leader.forwardEditedMessage" &&
1675
1747
  envelope.kind !== "leader.wakeInputCustody" &&
1676
1748
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
1677
- envelope.kind !== "leader.replaceFollowerTarget" &&
1749
+ envelope.kind !== "leader.workspaceRestore" &&
1678
1750
  envelope.kind !== "leader.offerQueueHandoff") ||
1679
1751
  envelope.recipientInstanceId !== deps.instanceId) {
1680
1752
  return {
@@ -1694,14 +1766,33 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1694
1766
  message: "Stale Telegram bus follower registration generation.",
1695
1767
  };
1696
1768
  }
1769
+ if (envelope.kind === "leader.workspaceRestore") {
1770
+ const ctx = deps.getContext();
1771
+ if (!authSecret || !ctx || !deps.isWorkspaceRestoreEnabled?.() || !deps.handleWorkspaceRestore)
1772
+ return {
1773
+ kind: "bus.ack", requestId: envelope.requestId, ok: false, message: "Workspace Restore capability is unavailable.",
1774
+ };
1775
+ try {
1776
+ const result = await deps.handleWorkspaceRestore({ operationId: envelope.operationId,
1777
+ registrationGeneration, mode: envelope.mode }, ctx);
1778
+ if (deps.getAuthSecret?.() !== authSecret || deps.getRegistrationGeneration() !== registrationGeneration ||
1779
+ deps.getContext() !== ctx || !deps.isWorkspaceRestoreEnabled())
1780
+ throw new Error("Workspace Restore receiver authority changed.");
1781
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
1782
+ }
1783
+ catch (error) {
1784
+ deps.recordRuntimeEvent?.("bus", error, { phase: "follower-workspace-restore" });
1785
+ return { kind: "bus.ack", requestId: envelope.requestId, ok: false,
1786
+ message: error instanceof Error ? error.message : "Workspace Restore failed." };
1787
+ }
1788
+ }
1697
1789
  if (envelope.kind === "leader.offerInputCustodyHandoff" &&
1698
1790
  envelope.recipientBindingKey !== deps.getRecipientBindingKey())
1699
1791
  return {
1700
1792
  kind: "bus.ack", requestId: envelope.requestId, ok: false,
1701
1793
  message: "Mismatched Telegram follower handoff recipient identity.",
1702
1794
  };
1703
- if (envelope.kind !== "leader.replaceFollowerTarget" &&
1704
- envelope.kind !== "leader.offerQueueHandoff" &&
1795
+ if (envelope.kind !== "leader.offerQueueHandoff" &&
1705
1796
  envelope.kind !== "leader.offerInputCustodyHandoff" &&
1706
1797
  (!envelope.delivery ||
1707
1798
  envelope.delivery.recipientBindingKey !==
@@ -1733,8 +1824,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1733
1824
  const result = await deps.handleInputCustodyHandoff(envelope, ctx);
1734
1825
  return { kind: "bus.ack", requestId: envelope.requestId, ok: true, result };
1735
1826
  }
1736
- if (envelope.kind !== "leader.replaceFollowerTarget" &&
1737
- envelope.kind !== "leader.offerQueueHandoff") {
1827
+ if (envelope.kind !== "leader.offerQueueHandoff") {
1738
1828
  const sourceReferenceEnabled = envelope.kind === "leader.wakeInputCustody" ||
1739
1829
  (deps.isSourceReferenceAdmissionEnabled?.() ?? false);
1740
1830
  if (sourceReferenceEnabled &&
@@ -1752,39 +1842,25 @@ export function createTelegramBusForwardedUpdateReceiverRuntime(deps) {
1752
1842
  result: receipt,
1753
1843
  };
1754
1844
  }
1755
- if (envelope.kind === "leader.offerQueueHandoff") {
1756
- if (!deps.handleQueueHandoff) {
1757
- throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
1758
- }
1759
- const result = await deps.handleQueueHandoff(envelope, ctx);
1760
- const receipt = envelope.payload.admissionReceipts[0];
1761
- if (envelope.payload.admissionReceipts.length !== 1 ||
1762
- !receipt ||
1763
- result.status !== "staged" ||
1764
- result.receiptId !== receipt.receiptId ||
1765
- result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
1766
- result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
1767
- throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
1768
- }
1769
- return {
1770
- kind: "bus.ack",
1771
- requestId: envelope.requestId,
1772
- ok: true,
1773
- result,
1774
- };
1845
+ if (!deps.handleQueueHandoff) {
1846
+ throw new Error("Telegram bus receiver cannot accept queue handoff payloads.");
1775
1847
  }
1776
- {
1777
- if (!deps.handleReplaceTarget) {
1778
- throw new Error("Telegram bus receiver cannot replace follower target.");
1779
- }
1780
- await deps.handleReplaceTarget({
1781
- target: envelope.target,
1782
- ...(envelope.oldTarget ? { oldTarget: envelope.oldTarget } : {}),
1783
- reason: envelope.reason,
1784
- registrationGeneration,
1785
- }, ctx);
1848
+ const result = await deps.handleQueueHandoff(envelope, ctx);
1849
+ const receipt = envelope.payload.admissionReceipts[0];
1850
+ if (envelope.payload.admissionReceipts.length !== 1 ||
1851
+ !receipt ||
1852
+ result.status !== "staged" ||
1853
+ result.receiptId !== receipt.receiptId ||
1854
+ result.sourceUpdateIds.length !== receipt.sourceUpdateIds.length ||
1855
+ result.sourceUpdateIds.some((updateId, index) => updateId !== receipt.sourceUpdateIds[index])) {
1856
+ throw new Error("Telegram queue handoff staging returned a mismatched receipt.");
1786
1857
  }
1787
- return { kind: "bus.ack", requestId: envelope.requestId, ok: true };
1858
+ return {
1859
+ kind: "bus.ack",
1860
+ requestId: envelope.requestId,
1861
+ ok: true,
1862
+ result,
1863
+ };
1788
1864
  }
1789
1865
  catch (error) {
1790
1866
  deps.recordRuntimeEvent?.("bus", error, { phase: "follower-forward" });
@@ -1826,6 +1902,3 @@ function parseTarget(value) {
1826
1902
  target.threadId = value.threadId;
1827
1903
  return target;
1828
1904
  }
1829
- function isRecord(value) {
1830
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1831
- }
@@ -9,6 +9,7 @@ import type { TelegramThreadDisplayMode } from "./config.ts";
9
9
  import * as ThreadReconciler from "./thread-reconciler.ts";
10
10
  import { type TelegramApiCallOptions } from "./telegram-api.ts";
11
11
  import type { TelegramTarget } from "./target.ts";
12
+ import type { TelegramAttachmentSource } from "./media.ts";
12
13
  import * as Threads from "./threads.ts";
13
14
  import { type TelegramBusEnvelope, type TelegramBusFollowerRegistry, type TelegramBusFollowerView, type TelegramBusInstanceRegistration, type TelegramBusProtocolIdentity, type TelegramBusSocketPathSource } from "./bus.ts";
14
15
  import type { TelegramQueueHandoffPayload } from "./queue.ts";
@@ -101,14 +102,14 @@ export interface TelegramBusFollowerDisconnectHandlerDeps {
101
102
  export interface TelegramBusLeaderApiProxyDeps {
102
103
  call: (method: string, body: Record<string, unknown>, options?: TelegramApiCallOptions) => Promise<unknown>;
103
104
  callMultipart: (method: string, fields: Record<string, string>, fieldName: string, filePath: string, fileName: string, options?: TelegramApiCallOptions) => Promise<unknown>;
104
- downloadFile: (fileId: string, destinationDir: string) => Promise<unknown>;
105
+ downloadFile: (fileId: string, suggestedName: string, source?: TelegramAttachmentSource) => Promise<unknown>;
105
106
  recoverStaleTargetError?: (apiBody: unknown, error: unknown) => Promise<unknown> | unknown;
106
107
  }
107
108
  export interface TelegramBusLeaderRuntimeAssemblyDeps<TContext> {
108
109
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
109
110
  persistThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
110
111
  onThreadDisplayChanged?: () => void;
111
- runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "provisionFollowerTarget" | "provisionLeaderTarget" | "recordRuntimeEvent">;
112
+ runtime: Omit<TelegramBusLeaderRuntimeDeps<TContext>, "callApi" | "onFollowerDisconnected" | "onFollowerConfirmedDead" | "onFollowerConfirmedDeadPreserved" | "getTelegramProfile" | "getAllowedUserId" | "provisionFollowerTarget" | "commitFollowerRegistration" | "provisionLeaderTarget" | "recordRuntimeEvent">;
112
113
  getAllowedUserId: () => number | undefined;
113
114
  instanceId: string;
114
115
  getCwd?: (ctx: TContext) => string | undefined;
@@ -168,6 +169,11 @@ export interface TelegramBusFollowerMessageOwnershipRecord {
168
169
  target?: TelegramTarget;
169
170
  }
170
171
  export type TelegramBusFollowerMessageOwnershipRecorder = (record: TelegramBusFollowerMessageOwnershipRecord) => void;
172
+ export type TelegramBusFollowerRegistrationCommitter = (input: {
173
+ registration: TelegramBusInstanceRegistration;
174
+ target?: TelegramTarget;
175
+ slot?: string;
176
+ }, publish: () => void) => Promise<void> | void;
171
177
  export interface TelegramBusLeaderRuntimeDeps<TContext> {
172
178
  socketPath: TelegramBusSocketPathSource;
173
179
  commitEndpointPublication?: (commit: () => void) => boolean;
@@ -195,6 +201,7 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
195
201
  provisionFollowerTarget?: (registration: TelegramBusInstanceRegistration, options?: {
196
202
  existingWorkspaceBindingOnly?: boolean;
197
203
  }) => Promise<TelegramTarget | undefined> | TelegramTarget | undefined;
204
+ commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
198
205
  renameFollowerThread?: (follower: TelegramBusFollowerView, threadName: string) => Promise<{
199
206
  threadName: string;
200
207
  }> | {
@@ -209,16 +216,22 @@ export interface TelegramBusLeaderRuntimeDeps<TContext> {
209
216
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
210
217
  getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
211
218
  onFollowerRegistered?: () => void;
219
+ /** Observation only: reacquire admission and inspect work, never infer completion.
220
+ * Listener settlement ends its currentness fence; return asynchronous work to retain that fence. */
221
+ onWorkspaceRestoreRecipientObserved?: (follower: TelegramBusFollowerView, isCurrent: () => boolean) => Promise<void> | void;
212
222
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
213
223
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
214
224
  getCurrentLeaderEpoch?: () => number | string | undefined;
215
225
  getTelegramProfile?: () => string | undefined;
226
+ getAllowedUserId?: () => number | undefined;
216
227
  provisionLeaderTarget?: (ctx: TContext) => Promise<void> | void;
217
228
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
218
229
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;
219
230
  getNowMs?: () => number;
220
231
  timeoutMs?: number;
221
232
  followerPruneIntervalMs?: number;
233
+ /** Leader housekeeping after each current prune pass; throttling is the callee's concern. */
234
+ afterFollowerPrune?: () => void;
222
235
  followerStaleAfterMs?: number;
223
236
  isFollowerProcessAlive?: (pid: number) => boolean;
224
237
  shouldCleanupConfirmedDeadFollower?: () => Promise<boolean> | boolean;
@@ -281,6 +294,7 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
281
294
  slot?: string;
282
295
  threadName?: string;
283
296
  }) | undefined;
297
+ commitFollowerRegistration?: TelegramBusFollowerRegistrationCommitter;
284
298
  onFollowerDisconnected?: (follower: TelegramBusFollowerView) => Promise<void> | void;
285
299
  renameFollowerThread?: (follower: TelegramBusFollowerView, threadName: string) => Promise<{
286
300
  threadName: string;
@@ -296,9 +310,14 @@ export declare function createTelegramBusLeaderEnvelopeHandler(deps: {
296
310
  settleFollowerSessionReplacement?: TelegramBusFollowerSessionReplacementOperation;
297
311
  getFollowerDisplayTitle?: (follower: TelegramBusFollowerView) => string | undefined;
298
312
  onFollowerRegistered?: () => void;
313
+ onWorkspaceRestoreRecipientObserved?: TelegramBusLeaderRuntimeDeps<unknown>["onWorkspaceRestoreRecipientObserved"];
314
+ getWorkspaceRestoreObservationGeneration?: () => number | undefined;
315
+ recordRuntimeEvent?: TelegramBusLeaderRuntimeDeps<unknown>["recordRuntimeEvent"];
299
316
  applyThreadDisplayMode?: (mode: TelegramThreadDisplayMode, isCurrent: () => boolean) => Promise<void>;
300
317
  getThreadDisplayMode?: () => TelegramThreadDisplayMode;
301
318
  getCurrentLeaderEpoch?: () => number | string | undefined;
319
+ getTelegramProfile?: () => string | undefined;
320
+ getAllowedUserId?: () => number | undefined;
302
321
  runFollowerMutation?: TelegramBusFollowerMutationRunner;
303
322
  runWorkspaceAdmission?: TelegramBusWorkspaceAdmissionRunner;
304
323
  runWithWorkspaceCapacity?: TelegramWorkspaceCapacityRunner;