@llblab/pi-telegram 0.52.2 → 0.53.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -5,10 +5,11 @@
5
5
  * successor recovery, and fenced one-shot deletion before durable slot reuse.
6
6
  */
7
7
  import { isDeepStrictEqual } from "node:util";
8
- import { areTelegramTargetsEqual as sameTarget } from "./target.js";
8
+ import { areTelegramTargetsEqual as sameTarget, } from "./target.js";
9
9
  import { getTelegramApiErrorRequestTarget, isTelegramApiRequestRejected, } from "./telegram-api.js";
10
- import { planTelegramWorkspaceSlotAllocation, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
10
+ import { isTelegramTopicDeletedErrorMessage } from "./thread-reconciler.js";
11
11
  import { createTelegramWorkspaceAdmissionOperationId, isTelegramWorkspaceRetirementFence, runWithTelegramWorkspaceAdmissionsAsync, } from "./workspace-admission.js";
12
+ import { planTelegramWorkspaceSlotAllocation, TelegramWorkspaceSlotUnavailableError, } from "./workspace-slots.js";
12
13
  export function createTelegramWorkspaceOperationGate() {
13
14
  let tail = Promise.resolve();
14
15
  return {
@@ -56,8 +57,12 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
56
57
  const snapshot = input.withJournalReference
57
58
  ? input.withJournalReference(binding, read)
58
59
  : read();
59
- sources.push({ kind: "available", scope, entries: snapshot.entries,
60
- ...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }) });
60
+ sources.push({
61
+ kind: "available",
62
+ scope,
63
+ entries: snapshot.entries,
64
+ ...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }),
65
+ });
61
66
  }
62
67
  catch {
63
68
  complete = false;
@@ -73,8 +78,12 @@ export function captureTelegramWorkspaceJournalProtectionSources(input) {
73
78
  }, input.createFollowerResolver(journalBindingKey));
74
79
  }
75
80
  for (const source of input.binding.journalSources ?? []) {
76
- capture({ kind: "binding", bindingKey: input.binding.bindingKey,
77
- journalBindingKey: source.recipientBindingKey, sessionId: source.sessionId }, input.createSessionResolver?.(source.recipientBindingKey, source.sessionId) ?? (() => undefined));
81
+ capture({
82
+ kind: "binding",
83
+ bindingKey: input.binding.bindingKey,
84
+ journalBindingKey: source.recipientBindingKey,
85
+ sessionId: source.sessionId,
86
+ }, input.createSessionResolver?.(source.recipientBindingKey, source.sessionId) ?? (() => undefined));
78
87
  }
79
88
  for (const path of input.discovery?.paths ?? []) {
80
89
  capture({ kind: "discovered", path }, input.discovery.createResolver(path));
@@ -89,9 +98,10 @@ function getJournalUpdateTarget(update) {
89
98
  return undefined;
90
99
  const direct = record.message ?? record.edited_message ?? record.guest_message;
91
100
  const callback = record.callback_query;
92
- const message = direct ?? (callback && typeof callback === "object" && !Array.isArray(callback)
93
- ? callback.message
94
- : undefined);
101
+ const message = direct ??
102
+ (callback && typeof callback === "object" && !Array.isArray(callback)
103
+ ? callback.message
104
+ : undefined);
95
105
  if (!message || typeof message !== "object" || Array.isArray(message))
96
106
  return undefined;
97
107
  const messageRecord = message;
@@ -121,7 +131,9 @@ export function resolveTelegramWorkspaceAcceptedWorkProtection(input) {
121
131
  unknown = true;
122
132
  continue;
123
133
  }
124
- if (input.requireBindingProvenance && source.scope.kind === "binding" && source.exists === false) {
134
+ if (input.requireBindingProvenance &&
135
+ source.scope.kind === "binding" &&
136
+ source.exists === false) {
125
137
  unknown = true;
126
138
  continue;
127
139
  }
@@ -166,37 +178,49 @@ export async function pruneTelegramWorkspaceJournalEvidence(input) {
166
178
  const keys = input.binding.journalBindingKeys ?? [];
167
179
  const sessions = input.binding.journalSources ?? [];
168
180
  const expected = [
169
- ...keys.map(key => address(key)),
170
- ...sessions.map(source => address(source.recipientBindingKey, source.sessionId)),
181
+ ...keys.map((key) => address(key)),
182
+ ...sessions.map((source) => address(source.recipientBindingKey, source.sessionId)),
171
183
  ];
172
- const shared = capture.sources.filter(source => source.scope.kind === "shared");
173
- const bindingSources = capture.sources.filter(source => source.scope.kind === "binding" &&
184
+ const shared = capture.sources.filter((source) => source.scope.kind === "shared");
185
+ const bindingSources = capture.sources.filter((source) => source.scope.kind === "binding" &&
174
186
  source.scope.bindingKey === input.binding.bindingKey);
175
- const sourceByAddress = new Map(bindingSources.flatMap(source => source.scope.kind === "binding"
176
- ? [[address(source.scope.journalBindingKey, source.scope.sessionId), source]] : []));
177
- if (!capture.complete || shared.length !== 1 || shared[0]?.kind !== "available" ||
178
- bindingSources.length !== expected.length || sourceByAddress.size !== expected.length ||
179
- expected.some(key => sourceByAddress.get(key)?.kind !== "available")) {
187
+ const sourceByAddress = new Map(bindingSources.flatMap((source) => source.scope.kind === "binding"
188
+ ? [
189
+ [
190
+ address(source.scope.journalBindingKey, source.scope.sessionId),
191
+ source,
192
+ ],
193
+ ]
194
+ : []));
195
+ if (!capture.complete ||
196
+ shared.length !== 1 ||
197
+ shared[0]?.kind !== "available" ||
198
+ bindingSources.length !== expected.length ||
199
+ sourceByAddress.size !== expected.length ||
200
+ expected.some((key) => sourceByAddress.get(key)?.kind !== "available")) {
180
201
  return { kind: "blocked", reason: "incomplete-evidence" };
181
202
  }
182
- const empty = new Set(expected.filter(key => {
203
+ const empty = new Set(expected.filter((key) => {
183
204
  const source = sourceByAddress.get(key);
184
205
  return source?.kind === "available" && source.entries.length === 0;
185
206
  }));
186
207
  const emptyWriterKeys = new Set([
187
- ...keys.filter(key => empty.has(address(key))),
188
- ...sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)))
189
- .map(source => source.recipientBindingKey),
208
+ ...keys.filter((key) => empty.has(address(key))),
209
+ ...sessions
210
+ .filter((source) => empty.has(address(source.recipientBindingKey, source.sessionId)))
211
+ .map((source) => source.recipientBindingKey),
190
212
  ]);
191
- const quiescent = new Set([...emptyWriterKeys].filter(key => input.getJournalWriterProtection(key) === "clear"));
192
- const removedKeys = keys.filter(key => empty.has(address(key)) && quiescent.has(key));
193
- const removedSources = sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)) &&
213
+ const quiescent = new Set([...emptyWriterKeys].filter((key) => input.getJournalWriterProtection(key) === "clear"));
214
+ const removedKeys = keys.filter((key) => empty.has(address(key)) && quiescent.has(key));
215
+ const removedSources = sessions.filter((source) => empty.has(address(source.recipientBindingKey, source.sessionId)) &&
194
216
  quiescent.has(source.recipientBindingKey));
195
- if (empty.size > 0 && removedKeys.length === 0 && removedSources.length === 0) {
217
+ if (empty.size > 0 &&
218
+ removedKeys.length === 0 &&
219
+ removedSources.length === 0) {
196
220
  return { kind: "blocked", reason: "writer-not-quiescent" };
197
221
  }
198
- const retainedKeys = keys.filter(key => !removedKeys.includes(key));
199
- const retainedSources = sessions.filter(source => !removedSources.includes(source));
222
+ const retainedKeys = keys.filter((key) => !removedKeys.includes(key));
223
+ const retainedSources = sessions.filter((source) => !removedSources.includes(source));
200
224
  if (!isCurrent()) {
201
225
  throw new Error("Telegram Workspace journal pruning lost leader authority.");
202
226
  }
@@ -213,8 +237,14 @@ export async function pruneTelegramWorkspaceJournalEvidence(input) {
213
237
  if (!isCurrent()) {
214
238
  throw new Error("Telegram Workspace journal pruning lost leader authority.");
215
239
  }
216
- return { kind: "committed", binding, removedKeys,
217
- ...(removedSources.length ? { removedSources: removedSources.map(source => ({ ...source })) } : {}) };
240
+ return {
241
+ kind: "committed",
242
+ binding,
243
+ removedKeys,
244
+ ...(removedSources.length
245
+ ? { removedSources: removedSources.map((source) => ({ ...source })) }
246
+ : {}),
247
+ };
218
248
  };
219
249
  return runWithTelegramWorkspaceAdmissionsAsync({
220
250
  ledger: input.admission,
@@ -252,7 +282,8 @@ export function captureTelegramWorkspaceExternalProtection(input) {
252
282
  // Unavailable queue or journal evidence must not clear accepted work.
253
283
  }
254
284
  try {
255
- deliveryAuthority = input.getDeliveryAuthorityProtection?.(input.binding) ?? "unknown";
285
+ deliveryAuthority =
286
+ input.getDeliveryAuthorityProtection?.(input.binding) ?? "unknown";
256
287
  }
257
288
  catch {
258
289
  // Unavailable delivery evidence must not clear a binding.
@@ -263,24 +294,44 @@ export function captureTelegramWorkspaceExternalProtection(input) {
263
294
  export function createTelegramWorkspaceProtectionObserver(deps) {
264
295
  const captureJournalSources = (candidate) => {
265
296
  const namespace = deps.inspectJournalNamespace?.();
266
- if (deps.inspectJournalNamespace && (!namespace || namespace.sources.filter(source => source.role === "polling").length !== 1))
297
+ if (deps.inspectJournalNamespace &&
298
+ (!namespace ||
299
+ namespace.sources.filter((source) => source.role === "polling")
300
+ .length !== 1))
267
301
  throw new Error("Telegram strict namespace evidence is unavailable or incomplete.");
268
- if (namespace?.retainedInputs?.some(original => original.state !== "committed"))
302
+ if (namespace?.retainedInputs?.some((original) => original.state !== "committed"))
269
303
  throw new Error("Telegram namespace contains uncommitted private retention evidence.");
270
304
  const discovery = namespace
271
- ? { paths: namespace.sources.filter(source => source.role !== "polling" && source.evidence.kind === "present")
272
- .map(source => source.path), complete: true }
273
- : candidate.journalBindingsComplete === true ? undefined : deps.discoverFollowerJournals?.();
305
+ ? {
306
+ paths: namespace.sources
307
+ .filter((source) => source.role !== "polling" &&
308
+ source.evidence.kind === "present")
309
+ .map((source) => source.path),
310
+ complete: true,
311
+ }
312
+ : candidate.journalBindingsComplete === true
313
+ ? undefined
314
+ : deps.discoverFollowerJournals?.();
274
315
  if (namespace && !deps.createJournalPathResolver)
275
316
  throw new Error("Telegram strict namespace protection requires discovered source resolution.");
276
317
  return captureTelegramWorkspaceJournalProtectionSources({
277
318
  binding: candidate,
278
319
  resolveLeader: deps.resolveLeaderJournal,
279
320
  createFollowerResolver: deps.createFollowerJournalResolver,
280
- ...(deps.createSessionJournalResolver ? { createSessionResolver: deps.createSessionJournalResolver } : {}),
281
- ...(deps.withJournalReference ? { withJournalReference: deps.withJournalReference } : {}),
321
+ ...(deps.createSessionJournalResolver
322
+ ? { createSessionResolver: deps.createSessionJournalResolver }
323
+ : {}),
324
+ ...(deps.withJournalReference
325
+ ? { withJournalReference: deps.withJournalReference }
326
+ : {}),
282
327
  ...(discovery && deps.createJournalPathResolver
283
- ? { discovery: { ...discovery, createResolver: deps.createJournalPathResolver } } : {}),
328
+ ? {
329
+ discovery: {
330
+ ...discovery,
331
+ createResolver: deps.createJournalPathResolver,
332
+ },
333
+ }
334
+ : {}),
284
335
  });
285
336
  };
286
337
  const capture = function (binding, options) {
@@ -288,14 +339,17 @@ export function createTelegramWorkspaceProtectionObserver(deps) {
288
339
  binding,
289
340
  requireBindingProvenance: options?.requireBindingProvenance,
290
341
  getLiveOwnerProtection(candidate) {
291
- if (deps.listFollowers().some((follower) => !!follower.target && sameTarget(follower.target, candidate.target)))
342
+ if (deps
343
+ .listFollowers()
344
+ .some((follower) => !!follower.target &&
345
+ sameTarget(follower.target, candidate.target)))
292
346
  return "protected";
293
347
  if (!deps.getJournalWriterProtection)
294
348
  return "unknown";
295
349
  let unknown = candidate.journalBindingsComplete !== true;
296
350
  const writerKeys = new Set([
297
351
  ...(candidate.journalBindingKeys ?? []),
298
- ...(candidate.journalSources ?? []).map(source => source.recipientBindingKey),
352
+ ...(candidate.journalSources ?? []).map((source) => source.recipientBindingKey),
299
353
  ]);
300
354
  for (const journalBindingKey of writerKeys) {
301
355
  const protection = deps.getJournalWriterProtection(journalBindingKey);
@@ -323,12 +377,17 @@ export function createTelegramWorkspaceProtectionObserver(deps) {
323
377
  },
324
378
  captureJournalSources,
325
379
  ...(deps.getDeliveryAuthorityProtection
326
- ? { getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection }
380
+ ? {
381
+ getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection,
382
+ }
327
383
  : {}),
328
384
  });
329
385
  };
330
- return { capture, captureJournalSources,
331
- getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown") };
386
+ return {
387
+ capture,
388
+ captureJournalSources,
389
+ getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown"),
390
+ };
332
391
  }
333
392
  /** Callable protection view retained for callers that need no metadata observation ports. */
334
393
  export function createTelegramWorkspaceExternalProtectionCapture(deps) {
@@ -341,7 +400,10 @@ export function createTelegramWorkspaceJournalEvidencePruner(deps) {
341
400
  return { kind: "blocked", reason: "state-changed" };
342
401
  const profileKey = admission.getProfileKey();
343
402
  return pruneTelegramWorkspaceJournalEvidence({
344
- store: deps.store, binding, admission, runExclusive: deps.runExclusive,
403
+ store: deps.store,
404
+ binding,
405
+ admission,
406
+ runExclusive: deps.runExclusive,
345
407
  capture: () => deps.protection.captureJournalSources(binding),
346
408
  getJournalWriterProtection: deps.protection.getJournalWriterProtection,
347
409
  getLeaderEpoch: deps.getLeaderEpoch,
@@ -351,7 +413,9 @@ export function createTelegramWorkspaceJournalEvidencePruner(deps) {
351
413
  };
352
414
  }
353
415
  export function isCurrentTelegramWorkspaceBinding(store, expected) {
354
- return store.listWorkspaceBindings().some((binding) => isDeepStrictEqual(binding, expected));
416
+ return store
417
+ .listWorkspaceBindings()
418
+ .some((binding) => isDeepStrictEqual(binding, expected));
355
419
  }
356
420
  /**
357
421
  * Demand-only preparation for pressure retirement. Every removed group is still
@@ -380,7 +444,8 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
380
444
  if (binding.journalSources?.length && !deps.createSessionJournalResolver)
381
445
  return { kind: "blocked", reason: "incomplete-source" };
382
446
  const initial = deps.getExternalProtection(binding);
383
- if (initial.liveOwner !== "clear" || initial.deliveryAuthority !== "clear") {
447
+ if (initial.liveOwner !== "clear" ||
448
+ initial.deliveryAuthority !== "clear") {
384
449
  return { kind: "blocked", reason: "live-owner" };
385
450
  }
386
451
  if (localProtection(binding) !== "clear") {
@@ -437,29 +502,35 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
437
502
  const target = getJournalUpdateTarget(entry.update);
438
503
  return !!target && sameTarget(target, binding.target);
439
504
  });
440
- if (source.scope === "binding" && relevant.some((entry) => {
441
- const target = getJournalUpdateTarget(entry.update);
442
- return !target || !sameTarget(target, binding.target);
443
- }))
505
+ if (source.scope === "binding" &&
506
+ relevant.some((entry) => {
507
+ const target = getJournalUpdateTarget(entry.update);
508
+ return !target || !sameTarget(target, binding.target);
509
+ }))
444
510
  return { kind: "blocked", reason: "unsupported-custody" };
445
511
  const seen = new Set();
446
512
  for (const entry of relevant) {
447
513
  if (entry.state !== "queued" ||
448
514
  (entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
449
- !entry.queueReceiptId || !entry.queueOwner || entry.queueHandoff) {
515
+ !entry.queueReceiptId ||
516
+ !entry.queueOwner ||
517
+ entry.queueHandoff) {
450
518
  return { kind: "blocked", reason: "unsupported-custody" };
451
519
  }
452
520
  if (seen.has(entry.queueReceiptId))
453
521
  continue;
454
522
  const receiptEntries = source.entries.filter((candidate) => candidate.queueReceiptId === entry.queueReceiptId);
455
- if (!receiptEntries.length || receiptEntries.some((candidate) => {
456
- const target = getJournalUpdateTarget(candidate.update);
457
- return candidate.state !== "queued" ||
458
- candidate.queueKind !== entry.queueKind ||
459
- !candidate.queueOwner || candidate.queueHandoff !== undefined ||
460
- !isDeepStrictEqual(candidate.queueOwner, entry.queueOwner) ||
461
- !target || !sameTarget(target, binding.target);
462
- }))
523
+ if (!receiptEntries.length ||
524
+ receiptEntries.some((candidate) => {
525
+ const target = getJournalUpdateTarget(candidate.update);
526
+ return (candidate.state !== "queued" ||
527
+ candidate.queueKind !== entry.queueKind ||
528
+ !candidate.queueOwner ||
529
+ candidate.queueHandoff !== undefined ||
530
+ !isDeepStrictEqual(candidate.queueOwner, entry.queueOwner) ||
531
+ !target ||
532
+ !sameTarget(target, binding.target));
533
+ }))
463
534
  return { kind: "blocked", reason: "unsupported-custody" };
464
535
  seen.add(entry.queueReceiptId);
465
536
  plans.push({
@@ -467,7 +538,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
467
538
  recovery: {
468
539
  queueKind: entry.queueKind,
469
540
  receiptId: entry.queueReceiptId,
470
- sourceUpdateIds: receiptEntries.map((candidate) => candidate.updateId).sort((a, b) => a - b),
541
+ sourceUpdateIds: receiptEntries
542
+ .map((candidate) => candidate.updateId)
543
+ .sort((a, b) => a - b),
471
544
  deadOwner: entry.queueOwner,
472
545
  recoveryOwner: deps.getRecoveryOwner(),
473
546
  },
@@ -481,7 +554,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
481
554
  try {
482
555
  liveness = deps.getQueueOwnerLiveness(plan.recovery.deadOwner);
483
556
  }
484
- catch { /* Unknown process evidence is never destructive authority. */ }
557
+ catch {
558
+ /* Unknown process evidence is never destructive authority. */
559
+ }
485
560
  if (liveness === "alive")
486
561
  return { kind: "blocked", reason: "owner-alive" };
487
562
  if (liveness !== "dead")
@@ -493,7 +568,8 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
493
568
  return { kind: "blocked", reason: "authority-changed" };
494
569
  }
495
570
  const current = deps.getExternalProtection(binding);
496
- if (current.liveOwner !== "clear" || current.deliveryAuthority !== "clear") {
571
+ if (current.liveOwner !== "clear" ||
572
+ current.deliveryAuthority !== "clear") {
497
573
  return { kind: "blocked", reason: "live-owner" };
498
574
  }
499
575
  if (localProtection(binding) !== "clear") {
@@ -509,7 +585,9 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
509
585
  try {
510
586
  deps.onMutationError?.(error);
511
587
  }
512
- catch { /* Diagnostics are fail-open. */ }
588
+ catch {
589
+ /* Diagnostics are fail-open. */
590
+ }
513
591
  return { kind: "blocked", reason: "mutation-refused" };
514
592
  }
515
593
  if (result.status === "owner-alive") {
@@ -524,7 +602,8 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps) {
524
602
  return { kind: "blocked", reason: "authority-changed" };
525
603
  }
526
604
  const after = deps.getExternalProtection(binding);
527
- if (after.liveOwner !== "clear" || after.acceptedWork !== "clear" ||
605
+ if (after.liveOwner !== "clear" ||
606
+ after.acceptedWork !== "clear" ||
528
607
  after.deliveryAuthority !== "clear") {
529
608
  return { kind: "blocked", reason: "protection-retained" };
530
609
  }
@@ -539,8 +618,10 @@ export async function adoptTelegramWorkspaceRetirementIntent(input) {
539
618
  return input.runExclusive(async () => {
540
619
  const leaderEpoch = input.getLeaderEpoch();
541
620
  const profileKey = input.getProfileKey();
542
- const isCurrent = () => leaderEpoch !== undefined && input.getLeaderEpoch() === leaderEpoch &&
543
- input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
621
+ const isCurrent = () => leaderEpoch !== undefined &&
622
+ input.getLeaderEpoch() === leaderEpoch &&
623
+ input.getProfileKey() === profileKey &&
624
+ input.isCurrent?.() !== false;
544
625
  if (!isCurrent())
545
626
  throw new Error("Telegram Workspace retirement adoption requires current leader authority.");
546
627
  const intents = input.store.listWorkspaceRetirementIntents();
@@ -550,17 +631,23 @@ export async function adoptTelegramWorkspaceRetirementIntent(input) {
550
631
  if (input.intent.profileKey !== profileKey) {
551
632
  return { kind: "blocked", reason: "profile-changed" };
552
633
  }
553
- const binding = input.store.listWorkspaceBindings().find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey);
634
+ const binding = input.store
635
+ .listWorkspaceBindings()
636
+ .find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey);
554
637
  if (!binding || !isDeepStrictEqual(binding, input.intent.binding)) {
555
638
  return { kind: "blocked", reason: "binding-changed" };
556
639
  }
557
- const eligible = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection, { expectedRetirement: input.intent }).bindings.find((candidate) => candidate.bindingKey === binding.bindingKey)?.protection === "eligible";
640
+ const eligible = input.store
641
+ .captureWorkspaceSlotOccupancy(input.getExternalProtection, {
642
+ expectedRetirement: input.intent,
643
+ })
644
+ .bindings.find((candidate) => candidate.bindingKey === binding.bindingKey)?.protection === "eligible";
558
645
  if (!eligible)
559
646
  return { kind: "blocked", reason: "protection-changed" };
560
647
  const replacement = { ...input.intent, leaderEpoch: leaderEpoch };
561
648
  if (!isCurrent())
562
649
  throw new Error("Telegram Workspace retirement adoption lost leader authority.");
563
- if (!await input.store.replaceWorkspaceRetirementIntent(input.intent, replacement, isCurrent))
650
+ if (!(await input.store.replaceWorkspaceRetirementIntent(input.intent, replacement, isCurrent)))
564
651
  return { kind: "blocked", reason: "commit-rejected" };
565
652
  return { kind: "adopted", intent: replacement };
566
653
  });
@@ -569,15 +656,9 @@ function isTelegramWorkspaceDeletionConfirmedAbsent(error) {
569
656
  if (!(error instanceof Error))
570
657
  return false;
571
658
  const status = "status" in error && typeof error.status === "number"
572
- ? error.status : undefined;
573
- if (status !== 400)
574
- return false;
575
- const message = error.message.toLowerCase();
576
- return message.includes("topic_id_invalid") ||
577
- message.includes("message thread not found") ||
578
- message.includes("thread not found") ||
579
- message.includes("topic not found") ||
580
- message.includes("topic deleted");
659
+ ? error.status
660
+ : undefined;
661
+ return status === 400 && isTelegramTopicDeletedErrorMessage(error.message);
581
662
  }
582
663
  function matchesTelegramWorkspaceRetirementFence(fence, intent) {
583
664
  return (fence.retirementIntentId === intent.id &&
@@ -590,22 +671,34 @@ function matchesTelegramWorkspaceRetirementFence(fence, intent) {
590
671
  }
591
672
  async function cancelRejectedTelegramWorkspaceRetirement(input, retained) {
592
673
  const epoch = input.getLeaderEpoch();
593
- const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
594
- input.getProfileKey() === retained.profileKey && input.isCurrent?.() !== false;
674
+ const isCurrent = () => epoch !== undefined &&
675
+ input.getLeaderEpoch() === epoch &&
676
+ input.getProfileKey() === retained.profileKey &&
677
+ input.isCurrent?.() !== false;
595
678
  if (!isCurrent())
596
679
  return { kind: "retained", reason: "authority-changed" };
597
680
  if (retained.phase !== "deletion-rejected" ||
598
- (retained.destructiveKind ?? "pressure-retirement") !== "pressure-retirement") {
681
+ (retained.destructiveKind ?? "pressure-retirement") !==
682
+ "pressure-retirement") {
599
683
  return { kind: "retained", reason: "fence-conflict" };
600
684
  }
601
685
  const owner = input.admission.getOwner();
602
686
  const fence = retained.leaderEpoch === epoch && isDeepStrictEqual(retained.owner, owner)
603
- ? retained : input.admission.adoptRetirementFence(retained, { owner, leaderEpoch: epoch });
687
+ ? retained
688
+ : input.admission.adoptRetirementFence(retained, {
689
+ owner,
690
+ leaderEpoch: epoch,
691
+ });
604
692
  if (fence.phase !== "deletion-rejected")
605
693
  return { kind: "retained", reason: "fence-conflict" };
606
- const bindingRetained = () => input.store.listWorkspaceBindings().some(binding => binding.bindingKey === fence.bindingKey && binding.slot === fence.slot && sameTarget(binding.target, fence.target));
694
+ const bindingRetained = () => input.store
695
+ .listWorkspaceBindings()
696
+ .some((binding) => binding.bindingKey === fence.bindingKey &&
697
+ binding.slot === fence.slot &&
698
+ sameTarget(binding.target, fence.target));
607
699
  const intents = input.store.listWorkspaceRetirementIntents();
608
- if (!bindingRetained() || intents.length > 1 ||
700
+ if (!bindingRetained() ||
701
+ intents.length > 1 ||
609
702
  (intents[0] && !matchesTelegramWorkspaceRetirementFence(fence, intents[0]))) {
610
703
  return { kind: "retained", reason: "stale-intent" };
611
704
  }
@@ -617,7 +710,8 @@ async function cancelRejectedTelegramWorkspaceRetirement(input, retained) {
617
710
  await input.store.persist();
618
711
  if (!isCurrent())
619
712
  return { kind: "retained", reason: "authority-changed" };
620
- if (input.store.listWorkspaceRetirementIntents().length || !bindingRetained()) {
713
+ if (input.store.listWorkspaceRetirementIntents().length ||
714
+ !bindingRetained()) {
621
715
  return { kind: "retained", reason: "commit-rejected" };
622
716
  }
623
717
  try {
@@ -636,7 +730,8 @@ export async function executeTelegramWorkspaceRetirement(input) {
636
730
  input.isCurrent?.() !== false;
637
731
  if (!isCurrent())
638
732
  return { kind: "retained", reason: "authority-changed" };
639
- if (!input.intent.binding.slot || !/^[A-Z]$/u.test(input.intent.binding.slot)) {
733
+ if (!input.intent.binding.slot ||
734
+ !/^[A-Z]$/u.test(input.intent.binding.slot)) {
640
735
  return { kind: "retained", reason: "stale-intent" };
641
736
  }
642
737
  const owner = input.admission.getOwner();
@@ -645,7 +740,8 @@ export async function executeTelegramWorkspaceRetirement(input) {
645
740
  return { kind: "retained", reason: "fence-conflict" };
646
741
  }
647
742
  let fence = storedFence;
648
- if (fence && !matchesTelegramWorkspaceRetirementFence(fence, input.intent)) {
743
+ if (fence &&
744
+ !matchesTelegramWorkspaceRetirementFence(fence, input.intent)) {
649
745
  return { kind: "retained", reason: "fence-conflict" };
650
746
  }
651
747
  if (fence &&
@@ -660,9 +756,15 @@ export async function executeTelegramWorkspaceRetirement(input) {
660
756
  if (fence?.phase === "deletion-rejected") {
661
757
  return cancelRejectedTelegramWorkspaceRetirement({ ...input, isCurrent }, fence);
662
758
  }
663
- const intent = input.store.listWorkspaceRetirementIntents().find((candidate) => isDeepStrictEqual(candidate, input.intent));
664
- const binding = input.store.listWorkspaceBindings().find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey);
665
- if (!intent || !binding || !isDeepStrictEqual(binding, input.intent.binding)) {
759
+ const intent = input.store
760
+ .listWorkspaceRetirementIntents()
761
+ .find((candidate) => isDeepStrictEqual(candidate, input.intent));
762
+ const binding = input.store
763
+ .listWorkspaceBindings()
764
+ .find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey);
765
+ if (!intent ||
766
+ !binding ||
767
+ !isDeepStrictEqual(binding, input.intent.binding)) {
666
768
  if (!intent && !binding && fence?.phase === "commit-ready") {
667
769
  try {
668
770
  input.admission.completeRetirementFence(fence);
@@ -686,7 +788,11 @@ export async function executeTelegramWorkspaceRetirement(input) {
686
788
  // publication while this stale local projection is removed.
687
789
  input.store.markStaleByTarget(binding.target, "deleted");
688
790
  }
689
- const eligible = () => input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection, { expectedRetirement: input.intent }).bindings.find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey)?.protection === "eligible";
791
+ const eligible = () => input.store
792
+ .captureWorkspaceSlotOccupancy(input.getExternalProtection, {
793
+ expectedRetirement: input.intent,
794
+ })
795
+ .bindings.find((candidate) => candidate.bindingKey === input.intent.binding.bindingKey)?.protection === "eligible";
690
796
  if (!eligible())
691
797
  return { kind: "retained", reason: "protection-changed" };
692
798
  if (!fence) {
@@ -731,10 +837,14 @@ export async function executeTelegramWorkspaceRetirement(input) {
731
837
  catch (error) {
732
838
  if (!isTelegramWorkspaceDeletionConfirmedAbsent(error)) {
733
839
  const target = getTelegramApiErrorRequestTarget(error);
734
- if (target && sameTarget(target, binding.target) && isTelegramApiRequestRejected(error, "deleteForumTopic")) {
840
+ if (target &&
841
+ sameTarget(target, binding.target) &&
842
+ isTelegramApiRequestRejected(error, "deleteForumTopic")) {
735
843
  fence = input.admission.confirmRetirementRejection(fence);
736
844
  const cancellation = await cancelRejectedTelegramWorkspaceRetirement({ ...input, isCurrent }, fence);
737
- return cancellation.kind === "cancelled" ? { kind: "retained", reason: "delete-rejected" } : cancellation;
845
+ return cancellation.kind === "cancelled"
846
+ ? { kind: "retained", reason: "delete-rejected" }
847
+ : cancellation;
738
848
  }
739
849
  return { kind: "retained", reason: "delete-unconfirmed" };
740
850
  }
@@ -755,7 +865,7 @@ export async function executeTelegramWorkspaceRetirement(input) {
755
865
  }
756
866
  if (!eligible())
757
867
  return { kind: "retained", reason: "protection-changed" };
758
- if (!await input.store.commitWorkspaceRetirement(input.intent, isCurrent)) {
868
+ if (!(await input.store.commitWorkspaceRetirement(input.intent, isCurrent))) {
759
869
  return { kind: "retained", reason: "commit-rejected" };
760
870
  }
761
871
  try {
@@ -802,12 +912,15 @@ export async function prepareTelegramWorkspaceRetirement(deps) {
802
912
  }
803
913
  if (existing.length === 1) {
804
914
  const intent = existing[0];
805
- const binding = deps.store.listWorkspaceBindings().find((candidate) => candidate.bindingKey === intent.binding.bindingKey);
915
+ const binding = deps.store
916
+ .listWorkspaceBindings()
917
+ .find((candidate) => candidate.bindingKey === intent.binding.bindingKey);
806
918
  const snapshot = deps.store.captureWorkspaceSlotOccupancy(deps.getExternalProtection, { expectedRetirement: intent });
807
919
  const candidate = snapshot.bindings.find((entry) => entry.bindingKey === intent.binding.bindingKey);
808
920
  if (intent.profileKey !== profileKey ||
809
921
  intent.leaderEpoch !== leaderEpoch ||
810
- !binding || !isDeepStrictEqual(binding, intent.binding) ||
922
+ !binding ||
923
+ !isDeepStrictEqual(binding, intent.binding) ||
811
924
  candidate?.protection !== "eligible") {
812
925
  return { kind: "blocked", reason: "stale-intent" };
813
926
  }
@@ -824,7 +937,9 @@ export async function prepareTelegramWorkspaceRetirement(deps) {
824
937
  if (selection.kind !== "candidate")
825
938
  return selection;
826
939
  const selected = selection.candidate;
827
- const binding = deps.store.listWorkspaceBindings().find((candidate) => candidate.bindingKey === selected.bindingKey);
940
+ const binding = deps.store
941
+ .listWorkspaceBindings()
942
+ .find((candidate) => candidate.bindingKey === selected.bindingKey);
828
943
  if (!binding?.slot ||
829
944
  binding.slot.toLowerCase() !== selected.slot ||
830
945
  binding.inactiveSinceMs !== selected.inactiveSinceMs) {
@@ -843,7 +958,11 @@ export async function prepareTelegramWorkspaceRetirement(deps) {
843
958
  if (!deps.store.upsertWorkspaceRetirementIntent(intent)) {
844
959
  return { kind: "blocked", reason: "state-changed" };
845
960
  }
846
- const rechecked = deps.store.captureWorkspaceSlotOccupancy(deps.getExternalProtection, { expectedRetirement: intent }).bindings.find((candidate) => candidate.bindingKey === binding.bindingKey);
961
+ const rechecked = deps.store
962
+ .captureWorkspaceSlotOccupancy(deps.getExternalProtection, {
963
+ expectedRetirement: intent,
964
+ })
965
+ .bindings.find((candidate) => candidate.bindingKey === binding.bindingKey);
847
966
  if (rechecked?.protection !== "eligible" || !isCurrent()) {
848
967
  deps.store.removeWorkspaceRetirementIntent(intent);
849
968
  if (!isCurrent())
@@ -858,54 +977,81 @@ export async function prepareTelegramWorkspaceRetirement(deps) {
858
977
  export async function runTelegramWorkspaceRetirementLifecycle(input) {
859
978
  let intent = input.store.listWorkspaceRetirementIntents()[0];
860
979
  const retainedFence = input.admission.read().fence;
861
- if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
980
+ if (retainedFence &&
981
+ isTelegramWorkspaceRetirementFence(retainedFence) &&
862
982
  retainedFence.phase === "deletion-rejected") {
863
983
  return input.runExclusive(() => cancelRejectedTelegramWorkspaceRetirement(input, retainedFence));
864
984
  }
865
- if (!intent && retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
866
- (retainedFence.destructiveKind ?? "pressure-retirement") === "pressure-retirement" &&
985
+ if (!intent &&
986
+ retainedFence &&
987
+ isTelegramWorkspaceRetirementFence(retainedFence) &&
988
+ (retainedFence.destructiveKind ?? "pressure-retirement") ===
989
+ "pressure-retirement" &&
867
990
  retainedFence.phase === "commit-ready") {
868
991
  return input.runExclusive(async () => {
869
992
  const epoch = input.getLeaderEpoch();
870
- if (epoch === undefined || input.getProfileKey() !== retainedFence.profileKey ||
993
+ if (epoch === undefined ||
994
+ input.getProfileKey() !== retainedFence.profileKey ||
871
995
  input.isCurrent?.() === false)
872
996
  return { kind: "retained", reason: "authority-changed" };
873
997
  if (input.store.listWorkspaceRetirementIntents().length ||
874
- input.store.listWorkspaceBindings().some((binding) => binding.bindingKey === retainedFence.bindingKey)) {
998
+ input.store
999
+ .listWorkspaceBindings()
1000
+ .some((binding) => binding.bindingKey === retainedFence.bindingKey)) {
875
1001
  return { kind: "retained", reason: "stale-intent" };
876
1002
  }
877
1003
  const owner = input.admission.getOwner();
878
1004
  const fence = retainedFence.leaderEpoch === epoch &&
879
- isDeepStrictEqual(retainedFence.owner, owner) ? retainedFence :
880
- input.admission.adoptRetirementFence(retainedFence, { owner, leaderEpoch: epoch });
1005
+ isDeepStrictEqual(retainedFence.owner, owner)
1006
+ ? retainedFence
1007
+ : input.admission.adoptRetirementFence(retainedFence, {
1008
+ owner,
1009
+ leaderEpoch: epoch,
1010
+ });
881
1011
  input.admission.completeRetirementFence(fence);
882
- return { kind: "retired", bindingKey: fence.bindingKey, slot: fence.slot };
1012
+ return {
1013
+ kind: "retired",
1014
+ bindingKey: fence.bindingKey,
1015
+ slot: fence.slot,
1016
+ };
883
1017
  });
884
1018
  }
885
1019
  if (intent) {
886
- if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
1020
+ if (retainedFence &&
1021
+ isTelegramWorkspaceRetirementFence(retainedFence) &&
887
1022
  retainedFence.phase === "commit-ready" &&
888
1023
  matchesTelegramWorkspaceRetirementFence(retainedFence, intent)) {
889
1024
  const resumed = await input.runExclusive(async () => {
890
1025
  const epoch = input.getLeaderEpoch();
891
1026
  const profileKey = input.getProfileKey();
892
- const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
893
- input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
1027
+ const isCurrent = () => epoch !== undefined &&
1028
+ input.getLeaderEpoch() === epoch &&
1029
+ input.getProfileKey() === profileKey &&
1030
+ input.isCurrent?.() !== false;
894
1031
  if (!isCurrent() || intent.profileKey !== profileKey)
895
1032
  return undefined;
896
1033
  const intents = input.store.listWorkspaceRetirementIntents();
897
- const binding = input.store.listWorkspaceBindings().find(candidate => candidate.bindingKey === intent.binding.bindingKey);
898
- if (intents.length !== 1 || !isDeepStrictEqual(intents[0], intent) ||
899
- !binding || !isDeepStrictEqual(binding, intent.binding))
1034
+ const binding = input.store
1035
+ .listWorkspaceBindings()
1036
+ .find((candidate) => candidate.bindingKey === intent.binding.bindingKey);
1037
+ if (intents.length !== 1 ||
1038
+ !isDeepStrictEqual(intents[0], intent) ||
1039
+ !binding ||
1040
+ !isDeepStrictEqual(binding, intent.binding))
900
1041
  return undefined;
901
1042
  if (intent.leaderEpoch === epoch)
902
1043
  return intent;
903
1044
  const replacement = { ...intent, leaderEpoch: epoch };
904
- return await input.store.replaceWorkspaceRetirementIntent(intent, replacement, isCurrent)
905
- ? replacement : undefined;
1045
+ return (await input.store.replaceWorkspaceRetirementIntent(intent, replacement, isCurrent))
1046
+ ? replacement
1047
+ : undefined;
906
1048
  });
907
1049
  if (!resumed)
908
- return { kind: "blocked", stage: "adoption", reason: "commit-rejected" };
1050
+ return {
1051
+ kind: "blocked",
1052
+ stage: "adoption",
1053
+ reason: "commit-rejected",
1054
+ };
909
1055
  intent = resumed;
910
1056
  }
911
1057
  else {
@@ -936,7 +1082,11 @@ export async function runTelegramWorkspaceRetirementLifecycle(input) {
936
1082
  if (preparation.kind === "not-needed")
937
1083
  return preparation;
938
1084
  if (preparation.kind === "blocked") {
939
- return { kind: "blocked", stage: "preparation", reason: preparation.reason };
1085
+ return {
1086
+ kind: "blocked",
1087
+ stage: "preparation",
1088
+ reason: preparation.reason,
1089
+ };
940
1090
  }
941
1091
  intent = preparation.intent;
942
1092
  }
@@ -975,24 +1125,36 @@ export function createTelegramWorkspaceSlotRotation(input) {
975
1125
  if (!isCurrent())
976
1126
  throw new Error("Telegram Workspace rotation lost leader authority.");
977
1127
  return runTelegramWorkspaceRetirementLifecycle({
978
- store: input.store, admission, getExternalProtection: input.getExternalProtection,
979
- getLeaderEpoch: input.getLeaderEpoch, getProfileKey: () => profileKey, isCurrent,
1128
+ store: input.store,
1129
+ admission,
1130
+ getExternalProtection: input.getExternalProtection,
1131
+ getLeaderEpoch: input.getLeaderEpoch,
1132
+ getProfileKey: () => profileKey,
1133
+ isCurrent,
980
1134
  // This entire lifecycle already owns the shared gate, without an admission lease.
981
1135
  runExclusive: async (action) => action(),
982
1136
  deleteForumTopic(permit, body) {
983
1137
  return input.deleteThread(() => {
984
1138
  const fence = admission.read().fence;
985
- if (!isCurrent() || !fence || !isTelegramWorkspaceRetirementFence(fence) ||
986
- (fence.destructiveKind ?? "pressure-retirement") !== "pressure-retirement" ||
987
- (permit.destructiveKind ?? "pressure-retirement") !== "pressure-retirement" ||
1139
+ if (!isCurrent() ||
1140
+ !fence ||
1141
+ !isTelegramWorkspaceRetirementFence(fence) ||
1142
+ (fence.destructiveKind ?? "pressure-retirement") !==
1143
+ "pressure-retirement" ||
1144
+ (permit.destructiveKind ?? "pressure-retirement") !==
1145
+ "pressure-retirement" ||
988
1146
  fence.phase !== "deletion-issued" ||
989
1147
  !isDeepStrictEqual(fence.owner, admission.getOwner()) ||
990
- fence.profileKey !== permit.profileKey || fence.leaderEpoch !== permit.leaderEpoch ||
991
- fence.operationId !== permit.operationId || fence.retirementIntentId !== permit.retirementIntentId ||
992
- fence.bindingKey !== permit.bindingKey || fence.slot !== permit.slot ||
1148
+ fence.profileKey !== permit.profileKey ||
1149
+ fence.leaderEpoch !== permit.leaderEpoch ||
1150
+ fence.operationId !== permit.operationId ||
1151
+ fence.retirementIntentId !== permit.retirementIntentId ||
1152
+ fence.bindingKey !== permit.bindingKey ||
1153
+ fence.slot !== permit.slot ||
993
1154
  fence.deletionIssuedAtMs !== permit.issuedAtMs ||
994
1155
  !sameTarget(fence.target, permit.target) ||
995
- body.chat_id !== permit.target.chatId || body.message_thread_id !== permit.target.threadId) {
1156
+ body.chat_id !== permit.target.chatId ||
1157
+ body.message_thread_id !== permit.target.threadId) {
996
1158
  throw new Error("Telegram Workspace deletion permit is stale.");
997
1159
  }
998
1160
  return { ...permit.target };
@@ -1000,14 +1162,20 @@ export function createTelegramWorkspaceSlotRotation(input) {
1000
1162
  },
1001
1163
  });
1002
1164
  });
1003
- if (result.kind !== "retired" && result.kind !== "not-needed" && result.kind !== "cancelled") {
1165
+ if (result.kind !== "retired" &&
1166
+ result.kind !== "not-needed" &&
1167
+ result.kind !== "cancelled") {
1004
1168
  throw new Error(`Telegram Workspace slots A-Z are unavailable; rotation blocked (${result.reason}).`);
1005
1169
  }
1006
1170
  if (result.kind === "retired") {
1007
1171
  try {
1008
- input.recordEvent("Telegram Workspace slot rotated.", { slot: result.slot });
1172
+ input.recordEvent("Telegram Workspace slot rotated.", {
1173
+ slot: result.slot,
1174
+ });
1175
+ }
1176
+ catch {
1177
+ /* Diagnostics cannot turn completed retirement into another attempt. */
1009
1178
  }
1010
- catch { /* Diagnostics cannot turn completed retirement into another attempt. */ }
1011
1179
  }
1012
1180
  if (!isCurrent())
1013
1181
  throw new Error("Telegram Workspace rotation lost leader authority.");
@@ -1020,8 +1188,11 @@ export function createTelegramWorkspaceSlotRotation(input) {
1020
1188
  return input.store.listWorkspaceRetirementIntents().length > 0;
1021
1189
  });
1022
1190
  const fence = admission.read().fence;
1023
- if (pending || (fence && isTelegramWorkspaceRetirementFence(fence) &&
1024
- (fence.destructiveKind ?? "pressure-retirement") === "pressure-retirement")) {
1191
+ if (pending ||
1192
+ (fence &&
1193
+ isTelegramWorkspaceRetirementFence(fence) &&
1194
+ (fence.destructiveKind ?? "pressure-retirement") ===
1195
+ "pressure-retirement")) {
1025
1196
  const recovered = await rotate();
1026
1197
  if (recovered.kind !== "cancelled")
1027
1198
  return operation();
@@ -1038,32 +1209,47 @@ export function createTelegramWorkspaceSlotRotation(input) {
1038
1209
  if (!isCurrent())
1039
1210
  throw new Error("Telegram Workspace reclamation lost leader authority.");
1040
1211
  const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
1041
- const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
1042
- if (allocation.kind === "free" || (allocation.kind === "blocked" && allocation.reason !== "protected-capacity"))
1212
+ const allocation = planTelegramWorkspaceSlotAllocation({
1213
+ ...snapshot,
1214
+ nowMs: Date.now(),
1215
+ });
1216
+ if (allocation.kind === "free" ||
1217
+ (allocation.kind === "blocked" &&
1218
+ allocation.reason !== "protected-capacity"))
1043
1219
  return [];
1044
1220
  if (allocation.kind === "reclaim" && !input.pruneJournalEvidence)
1045
1221
  return [];
1046
- return input.store.listWorkspaceBindings().filter((binding) => (allocation.kind !== "reclaim" || binding.bindingKey === allocation.candidate.bindingKey) &&
1222
+ return input.store
1223
+ .listWorkspaceBindings()
1224
+ .filter((binding) => (allocation.kind !== "reclaim" ||
1225
+ binding.bindingKey === allocation.candidate.bindingKey) &&
1047
1226
  typeof binding.inactiveSinceMs === "number" &&
1048
1227
  Number.isFinite(binding.inactiveSinceMs) &&
1049
- binding.inactiveSinceMs >= 0).sort((left, right) => left.inactiveSinceMs - right.inactiveSinceMs ||
1228
+ binding.inactiveSinceMs >= 0)
1229
+ .sort((left, right) => left.inactiveSinceMs - right.inactiveSinceMs ||
1050
1230
  (left.slot ?? "").localeCompare(right.slot ?? ""));
1051
1231
  });
1052
1232
  for (const original of candidates) {
1053
1233
  let binding = original;
1054
- if (input.pruneJournalEvidence && ((binding.journalBindingKeys?.length ?? 0) + (binding.journalSources?.length ?? 0) > 0)) {
1234
+ if (input.pruneJournalEvidence &&
1235
+ (binding.journalBindingKeys?.length ?? 0) +
1236
+ (binding.journalSources?.length ?? 0) >
1237
+ 0) {
1055
1238
  const pruned = await input.pruneJournalEvidence(binding, isCurrent);
1056
1239
  if (!isCurrent())
1057
1240
  throw new Error("Telegram Workspace pruning lost leader authority.");
1058
1241
  if (pruned.kind !== "committed")
1059
1242
  continue;
1060
1243
  binding = pruned.binding;
1061
- const removed = pruned.removedKeys.length + (pruned.removedSources?.length ?? 0);
1244
+ const removed = pruned.removedKeys.length +
1245
+ (pruned.removedSources?.length ?? 0);
1062
1246
  if (removed > 0) {
1063
1247
  try {
1064
1248
  input.recordEvent("Telegram Workspace journal evidence pruned.", { slot: binding.slot, removed });
1065
1249
  }
1066
- catch { /* Diagnostics cannot revoke a published metadata subset. */ }
1250
+ catch {
1251
+ /* Diagnostics cannot revoke a published metadata subset. */
1252
+ }
1067
1253
  }
1068
1254
  }
1069
1255
  if (!input.reclaimDeadOwnerQueuedWork)
@@ -1082,7 +1268,9 @@ export function createTelegramWorkspaceSlotRotation(input) {
1082
1268
  updateCount: reclaimed.updateIds.length,
1083
1269
  });
1084
1270
  }
1085
- catch { /* Diagnostics cannot revoke journal-owned recovery. */ }
1271
+ catch {
1272
+ /* Diagnostics cannot revoke journal-owned recovery. */
1273
+ }
1086
1274
  break;
1087
1275
  }
1088
1276
  }