@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +11 -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 +61 -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 +249 -126
  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 +125 -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 +511 -300
  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
@@ -14,25 +14,24 @@ import type {
14
14
  TelegramUpdateJournalEntry,
15
15
  TelegramUpdateJournalQueueOwnerIdentity,
16
16
  } from "./journal.ts";
17
- import { areTelegramTargetsEqual as sameTarget, type TelegramTarget } from "./target.ts";
17
+ import {
18
+ areTelegramTargetsEqual as sameTarget,
19
+ type TelegramTarget,
20
+ } from "./target.ts";
18
21
  import {
19
22
  getTelegramApiErrorRequestTarget,
20
23
  isTelegramApiRequestRejected,
21
24
  type TelegramWorkspaceThreadDeletionTransport,
22
25
  } from "./telegram-api.ts";
26
+ import { isTelegramTopicDeletedErrorMessage } from "./thread-reconciler.ts";
23
27
  import type {
24
28
  TelegramTopicTargetStore,
25
- TelegramWorkspaceJournalSource,
26
29
  TelegramWorkspaceExternalProtectionEvidence,
27
- TelegramWorkspaceRetirementIntent,
30
+ TelegramWorkspaceJournalSource,
28
31
  TelegramWorkspaceProtectionState,
32
+ TelegramWorkspaceRetirementIntent,
29
33
  TelegramWorkspaceThreadBinding,
30
34
  } from "./threads.ts";
31
- import {
32
- planTelegramWorkspaceSlotAllocation,
33
- TelegramWorkspaceSlotUnavailableError,
34
- type TelegramWorkspaceSlotOccupancy,
35
- } from "./workspace-slots.ts";
36
35
  import {
37
36
  createTelegramWorkspaceAdmissionOperationId,
38
37
  isTelegramWorkspaceRetirementFence,
@@ -42,6 +41,11 @@ import {
42
41
  type TelegramWorkspaceDeletionPermit,
43
42
  type TelegramWorkspaceRetirementFence,
44
43
  } from "./workspace-admission.ts";
44
+ import {
45
+ planTelegramWorkspaceSlotAllocation,
46
+ TelegramWorkspaceSlotUnavailableError,
47
+ type TelegramWorkspaceSlotOccupancy,
48
+ } from "./workspace-slots.ts";
45
49
 
46
50
  export interface TelegramWorkspaceOperationGate {
47
51
  runExclusive: <T>(operation: () => Promise<T>) => Promise<T>;
@@ -52,7 +56,10 @@ export function createTelegramWorkspaceOperationGate(): TelegramWorkspaceOperati
52
56
  return {
53
57
  runExclusive<T>(operation: () => Promise<T>): Promise<T> {
54
58
  const run = tail.then(operation);
55
- tail = run.then(() => undefined, () => undefined);
59
+ tail = run.then(
60
+ () => undefined,
61
+ () => undefined,
62
+ );
56
63
  return run;
57
64
  },
58
65
  };
@@ -67,18 +74,21 @@ export type TelegramWorkspaceOperationRunner = <T>(
67
74
  operation: () => Promise<T>,
68
75
  ) => Promise<T>;
69
76
 
70
- export interface TelegramWorkspaceOperationRuntime
71
- extends TelegramWorkspaceOperationGate {
77
+ export interface TelegramWorkspaceOperationRuntime extends TelegramWorkspaceOperationGate {
72
78
  run: TelegramWorkspaceOperationRunner;
73
79
  }
74
80
 
75
- export function createTelegramWorkspaceOperationRuntime(input: {
76
- getWorkspaceAdmission?: () => Pick<
77
- TelegramWorkspaceAdmissionLedger,
78
- "acquireAdmission" | "releaseAdmission"
79
- > | undefined;
80
- onReleaseError?: (error: unknown, operationKind: string) => void;
81
- } = {}): TelegramWorkspaceOperationRuntime {
81
+ export function createTelegramWorkspaceOperationRuntime(
82
+ input: {
83
+ getWorkspaceAdmission?: () =>
84
+ | Pick<
85
+ TelegramWorkspaceAdmissionLedger,
86
+ "acquireAdmission" | "releaseAdmission"
87
+ >
88
+ | undefined;
89
+ onReleaseError?: (error: unknown, operationKind: string) => void;
90
+ } = {},
91
+ ): TelegramWorkspaceOperationRuntime {
82
92
  const gate = createTelegramWorkspaceOperationGate();
83
93
  const run: TelegramWorkspaceOperationRunner = (metadata, operation) => {
84
94
  const gated = () => gate.runExclusive(operation);
@@ -106,8 +116,13 @@ export interface TelegramWorkspaceJournalProtectionCapture {
106
116
 
107
117
  interface TelegramWorkspaceJournalReader {
108
118
  recoveryKey?: string;
109
- journal: { read: () => { entries: readonly { update: unknown }[]; exists?: boolean } };
110
- readForProtection?: () => { entries: readonly { update: unknown }[]; exists?: boolean };
119
+ journal: {
120
+ read: () => { entries: readonly { update: unknown }[]; exists?: boolean };
121
+ };
122
+ readForProtection?: () => {
123
+ entries: readonly { update: unknown }[];
124
+ exists?: boolean;
125
+ };
111
126
  }
112
127
 
113
128
  export function captureTelegramWorkspaceJournalProtectionSources(input: {
@@ -120,16 +135,21 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
120
135
  recipientBindingKey: string,
121
136
  sessionId: string,
122
137
  ) => () => TelegramWorkspaceJournalReader | undefined;
123
- withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader,
124
- operation: () => T) => T;
138
+ withJournalReference?: <T>(
139
+ binding: TelegramWorkspaceJournalReader,
140
+ operation: () => T,
141
+ ) => T;
125
142
  discovery?: {
126
143
  paths: readonly string[];
127
144
  complete: boolean;
128
- createResolver: (path: string) => () => TelegramWorkspaceJournalReader | undefined;
145
+ createResolver: (
146
+ path: string,
147
+ ) => () => TelegramWorkspaceJournalReader | undefined;
129
148
  };
130
149
  }): TelegramWorkspaceJournalProtectionCapture {
131
150
  const sources: TelegramWorkspaceJournalProtectionSource[] = [];
132
- let complete = input.binding.journalBindingsComplete === true ||
151
+ let complete =
152
+ input.binding.journalBindingsComplete === true ||
133
153
  input.discovery?.complete === true;
134
154
  const capture = (
135
155
  scope: TelegramWorkspaceJournalProtectionSource["scope"],
@@ -146,8 +166,12 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
146
166
  const snapshot = input.withJournalReference
147
167
  ? input.withJournalReference(binding, read)
148
168
  : read();
149
- sources.push({ kind: "available", scope, entries: snapshot.entries,
150
- ...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }) });
169
+ sources.push({
170
+ kind: "available",
171
+ scope,
172
+ entries: snapshot.entries,
173
+ ...(snapshot.exists === undefined ? {} : { exists: snapshot.exists }),
174
+ });
151
175
  } catch {
152
176
  complete = false;
153
177
  sources.push({ kind: "unknown", scope });
@@ -165,12 +189,24 @@ export function captureTelegramWorkspaceJournalProtectionSources(input: {
165
189
  );
166
190
  }
167
191
  for (const source of input.binding.journalSources ?? []) {
168
- capture({ kind: "binding", bindingKey: input.binding.bindingKey,
169
- journalBindingKey: source.recipientBindingKey, sessionId: source.sessionId },
170
- input.createSessionResolver?.(source.recipientBindingKey, source.sessionId) ?? (() => undefined));
192
+ capture(
193
+ {
194
+ kind: "binding",
195
+ bindingKey: input.binding.bindingKey,
196
+ journalBindingKey: source.recipientBindingKey,
197
+ sessionId: source.sessionId,
198
+ },
199
+ input.createSessionResolver?.(
200
+ source.recipientBindingKey,
201
+ source.sessionId,
202
+ ) ?? (() => undefined),
203
+ );
171
204
  }
172
205
  for (const path of input.discovery?.paths ?? []) {
173
- capture({ kind: "discovered", path }, input.discovery!.createResolver(path));
206
+ capture(
207
+ { kind: "discovered", path },
208
+ input.discovery!.createResolver(path),
209
+ );
174
210
  }
175
211
  return { sources, complete };
176
212
  }
@@ -180,7 +216,12 @@ export type TelegramWorkspaceJournalProtectionSource =
180
216
  kind: "available";
181
217
  scope:
182
218
  | { kind: "shared" }
183
- | { kind: "binding"; bindingKey: string; journalBindingKey: string; sessionId?: string }
219
+ | {
220
+ kind: "binding";
221
+ bindingKey: string;
222
+ journalBindingKey: string;
223
+ sessionId?: string;
224
+ }
184
225
  | { kind: "discovered"; path: string };
185
226
  entries: readonly { update: unknown }[];
186
227
  /** Positive filesystem presence is distinct from a complete namespace with a missing historical family. */
@@ -190,27 +231,36 @@ export type TelegramWorkspaceJournalProtectionSource =
190
231
  kind: "unknown";
191
232
  scope:
192
233
  | { kind: "shared" }
193
- | { kind: "binding"; bindingKey: string; journalBindingKey: string; sessionId?: string }
234
+ | {
235
+ kind: "binding";
236
+ bindingKey: string;
237
+ journalBindingKey: string;
238
+ sessionId?: string;
239
+ }
194
240
  | { kind: "discovered"; path: string };
195
241
  };
196
242
 
197
- function getJournalUpdateTarget(update: unknown):
198
- | { chatId: number; threadId?: number }
199
- | undefined {
200
- if (!update || typeof update !== "object" || Array.isArray(update)) return undefined;
243
+ function getJournalUpdateTarget(
244
+ update: unknown,
245
+ ): { chatId: number; threadId?: number } | undefined {
246
+ if (!update || typeof update !== "object" || Array.isArray(update))
247
+ return undefined;
201
248
  const record = update as Record<string, unknown>;
202
249
  if (record.message_reaction !== undefined) return undefined;
203
- const direct = record.message ?? record.edited_message ?? record.guest_message;
250
+ const direct =
251
+ record.message ?? record.edited_message ?? record.guest_message;
204
252
  const callback = record.callback_query;
205
- const message = direct ?? (
206
- callback && typeof callback === "object" && !Array.isArray(callback)
253
+ const message =
254
+ direct ??
255
+ (callback && typeof callback === "object" && !Array.isArray(callback)
207
256
  ? (callback as Record<string, unknown>).message
208
- : undefined
209
- );
210
- if (!message || typeof message !== "object" || Array.isArray(message)) return undefined;
257
+ : undefined);
258
+ if (!message || typeof message !== "object" || Array.isArray(message))
259
+ return undefined;
211
260
  const messageRecord = message as Record<string, unknown>;
212
261
  const chat = messageRecord.chat;
213
- if (!chat || typeof chat !== "object" || Array.isArray(chat)) return undefined;
262
+ if (!chat || typeof chat !== "object" || Array.isArray(chat))
263
+ return undefined;
214
264
  const chatId = (chat as Record<string, unknown>).id;
215
265
  if (typeof chatId !== "number") return undefined;
216
266
  const threadId = messageRecord.message_thread_id;
@@ -228,19 +278,28 @@ export function resolveTelegramWorkspaceAcceptedWorkProtection(input: {
228
278
  /** Relocation cannot infer execution ownership from the original Telegram message target. */
229
279
  requireBindingProvenance?: boolean;
230
280
  }): TelegramWorkspaceProtectionState {
231
- if (input.localAcceptedTargets.some((target) => sameTarget(target, input.binding.target))) {
281
+ if (
282
+ input.localAcceptedTargets.some((target) =>
283
+ sameTarget(target, input.binding.target),
284
+ )
285
+ ) {
232
286
  return "protected";
233
287
  }
234
288
  let unknown = !input.sourcesComplete;
235
289
  for (const source of input.journalSources) {
236
- const relevant = source.scope.kind !== "binding" ||
290
+ const relevant =
291
+ source.scope.kind !== "binding" ||
237
292
  source.scope.bindingKey === input.binding.bindingKey;
238
293
  if (!relevant) continue;
239
294
  if (source.kind === "unknown") {
240
295
  unknown = true;
241
296
  continue;
242
297
  }
243
- if (input.requireBindingProvenance && source.scope.kind === "binding" && source.exists === false) {
298
+ if (
299
+ input.requireBindingProvenance &&
300
+ source.scope.kind === "binding" &&
301
+ source.exists === false
302
+ ) {
244
303
  unknown = true;
245
304
  continue;
246
305
  }
@@ -272,7 +331,11 @@ export type TelegramWorkspaceJournalPruneResult =
272
331
  }
273
332
  | {
274
333
  kind: "blocked";
275
- reason: "incomplete-evidence" | "writer-not-quiescent" | "state-changed" | "publication-refused";
334
+ reason:
335
+ | "incomplete-evidence"
336
+ | "writer-not-quiescent"
337
+ | "state-changed"
338
+ | "publication-refused";
276
339
  };
277
340
 
278
341
  export async function pruneTelegramWorkspaceJournalEvidence(input: {
@@ -310,60 +373,125 @@ export async function pruneTelegramWorkspaceJournalEvidence(input: {
310
373
  );
311
374
  }
312
375
  let capture: TelegramWorkspaceJournalProtectionCapture;
313
- try { capture = input.capture(); }
314
- catch { return { kind: "blocked", reason: "incomplete-evidence" }; }
315
- const address = (key: string, sessionId?: string) => JSON.stringify([sessionId ?? null, key]);
376
+ try {
377
+ capture = input.capture();
378
+ } catch {
379
+ return { kind: "blocked", reason: "incomplete-evidence" };
380
+ }
381
+ const address = (key: string, sessionId?: string) =>
382
+ JSON.stringify([sessionId ?? null, key]);
316
383
  const keys = input.binding.journalBindingKeys ?? [];
317
384
  const sessions = input.binding.journalSources ?? [];
318
385
  const expected = [
319
- ...keys.map(key => address(key)),
320
- ...sessions.map(source => address(source.recipientBindingKey, source.sessionId)),
386
+ ...keys.map((key) => address(key)),
387
+ ...sessions.map((source) =>
388
+ address(source.recipientBindingKey, source.sessionId),
389
+ ),
321
390
  ];
322
- const shared = capture.sources.filter(source => source.scope.kind === "shared");
323
- const bindingSources = capture.sources.filter(source => source.scope.kind === "binding" &&
324
- source.scope.bindingKey === input.binding.bindingKey);
325
- const sourceByAddress = new Map(bindingSources.flatMap(source => source.scope.kind === "binding"
326
- ? [[address(source.scope.journalBindingKey, source.scope.sessionId), source] as const] : []));
327
- if (!capture.complete || shared.length !== 1 || shared[0]?.kind !== "available" ||
328
- bindingSources.length !== expected.length || sourceByAddress.size !== expected.length ||
329
- expected.some(key => sourceByAddress.get(key)?.kind !== "available")) {
391
+ const shared = capture.sources.filter(
392
+ (source) => source.scope.kind === "shared",
393
+ );
394
+ const bindingSources = capture.sources.filter(
395
+ (source) =>
396
+ source.scope.kind === "binding" &&
397
+ source.scope.bindingKey === input.binding.bindingKey,
398
+ );
399
+ const sourceByAddress = new Map(
400
+ bindingSources.flatMap((source) =>
401
+ source.scope.kind === "binding"
402
+ ? [
403
+ [
404
+ address(source.scope.journalBindingKey, source.scope.sessionId),
405
+ source,
406
+ ] as const,
407
+ ]
408
+ : [],
409
+ ),
410
+ );
411
+ if (
412
+ !capture.complete ||
413
+ shared.length !== 1 ||
414
+ shared[0]?.kind !== "available" ||
415
+ bindingSources.length !== expected.length ||
416
+ sourceByAddress.size !== expected.length ||
417
+ expected.some((key) => sourceByAddress.get(key)?.kind !== "available")
418
+ ) {
330
419
  return { kind: "blocked", reason: "incomplete-evidence" };
331
420
  }
332
- const empty = new Set(expected.filter(key => {
333
- const source = sourceByAddress.get(key);
334
- return source?.kind === "available" && source.entries.length === 0;
335
- }));
421
+ const empty = new Set(
422
+ expected.filter((key) => {
423
+ const source = sourceByAddress.get(key);
424
+ return source?.kind === "available" && source.entries.length === 0;
425
+ }),
426
+ );
336
427
  const emptyWriterKeys = new Set([
337
- ...keys.filter(key => empty.has(address(key))),
338
- ...sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)))
339
- .map(source => source.recipientBindingKey),
428
+ ...keys.filter((key) => empty.has(address(key))),
429
+ ...sessions
430
+ .filter((source) =>
431
+ empty.has(address(source.recipientBindingKey, source.sessionId)),
432
+ )
433
+ .map((source) => source.recipientBindingKey),
340
434
  ]);
341
- const quiescent = new Set([...emptyWriterKeys].filter(key => input.getJournalWriterProtection(key) === "clear"));
342
- const removedKeys = keys.filter(key => empty.has(address(key)) && quiescent.has(key));
343
- const removedSources = sessions.filter(source => empty.has(address(source.recipientBindingKey, source.sessionId)) &&
344
- quiescent.has(source.recipientBindingKey));
345
- if (empty.size > 0 && removedKeys.length === 0 && removedSources.length === 0) {
435
+ const quiescent = new Set(
436
+ [...emptyWriterKeys].filter(
437
+ (key) => input.getJournalWriterProtection(key) === "clear",
438
+ ),
439
+ );
440
+ const removedKeys = keys.filter(
441
+ (key) => empty.has(address(key)) && quiescent.has(key),
442
+ );
443
+ const removedSources = sessions.filter(
444
+ (source) =>
445
+ empty.has(address(source.recipientBindingKey, source.sessionId)) &&
446
+ quiescent.has(source.recipientBindingKey),
447
+ );
448
+ if (
449
+ empty.size > 0 &&
450
+ removedKeys.length === 0 &&
451
+ removedSources.length === 0
452
+ ) {
346
453
  return { kind: "blocked", reason: "writer-not-quiescent" };
347
454
  }
348
- const retainedKeys = keys.filter(key => !removedKeys.includes(key));
349
- const retainedSources = sessions.filter(source => !removedSources.includes(source));
455
+ const retainedKeys = keys.filter((key) => !removedKeys.includes(key));
456
+ const retainedSources = sessions.filter(
457
+ (source) => !removedSources.includes(source),
458
+ );
350
459
  if (!isCurrent()) {
351
- throw new Error("Telegram Workspace journal pruning lost leader authority.");
460
+ throw new Error(
461
+ "Telegram Workspace journal pruning lost leader authority.",
462
+ );
352
463
  }
353
464
  const binding = input.store.commitWorkspaceJournalEvidence(
354
- input.binding, retainedKeys, input.binding.journalBindingsComplete === true, retainedSources,
465
+ input.binding,
466
+ retainedKeys,
467
+ input.binding.journalBindingsComplete === true,
468
+ retainedSources,
355
469
  );
356
470
  if (!binding) return { kind: "blocked", reason: "state-changed" };
357
471
  if (removedKeys.length > 0 || removedSources.length > 0) {
358
- const published = await input.store.persistWorkspaceJournalEvidence(binding, isCurrent);
359
- if (!isCurrent()) throw new Error("Telegram Workspace journal pruning lost leader authority.");
472
+ const published = await input.store.persistWorkspaceJournalEvidence(
473
+ binding,
474
+ isCurrent,
475
+ );
476
+ if (!isCurrent())
477
+ throw new Error(
478
+ "Telegram Workspace journal pruning lost leader authority.",
479
+ );
360
480
  if (!published) return { kind: "blocked", reason: "publication-refused" };
361
481
  }
362
482
  if (!isCurrent()) {
363
- throw new Error("Telegram Workspace journal pruning lost leader authority.");
483
+ throw new Error(
484
+ "Telegram Workspace journal pruning lost leader authority.",
485
+ );
364
486
  }
365
- return { kind: "committed", binding, removedKeys,
366
- ...(removedSources.length ? { removedSources: removedSources.map(source => ({ ...source })) } : {}) };
487
+ return {
488
+ kind: "committed",
489
+ binding,
490
+ removedKeys,
491
+ ...(removedSources.length
492
+ ? { removedSources: removedSources.map((source) => ({ ...source })) }
493
+ : {}),
494
+ };
367
495
  };
368
496
  return runWithTelegramWorkspaceAdmissionsAsync({
369
497
  ledger: input.admission,
@@ -383,9 +511,10 @@ export function captureTelegramWorkspaceExternalProtection(input: {
383
511
  getLiveOwnerProtection: (
384
512
  binding: TelegramWorkspaceThreadBinding,
385
513
  ) => TelegramWorkspaceProtectionState;
386
- getLocalAcceptedTargets: (
387
- binding: TelegramWorkspaceThreadBinding,
388
- ) => { targets: readonly TelegramTarget[]; complete: boolean };
514
+ getLocalAcceptedTargets: (binding: TelegramWorkspaceThreadBinding) => {
515
+ targets: readonly TelegramTarget[];
516
+ complete: boolean;
517
+ };
389
518
  captureJournalSources: (
390
519
  binding: TelegramWorkspaceThreadBinding,
391
520
  ) => TelegramWorkspaceJournalProtectionCapture;
@@ -415,7 +544,8 @@ export function captureTelegramWorkspaceExternalProtection(input: {
415
544
  // Unavailable queue or journal evidence must not clear accepted work.
416
545
  }
417
546
  try {
418
- deliveryAuthority = input.getDeliveryAuthorityProtection?.(input.binding) ?? "unknown";
547
+ deliveryAuthority =
548
+ input.getDeliveryAuthorityProtection?.(input.binding) ?? "unknown";
419
549
  } catch {
420
550
  // Unavailable delivery evidence must not clear a binding.
421
551
  }
@@ -434,14 +564,19 @@ interface TelegramWorkspaceProtectionObserverDeps {
434
564
  recipientBindingKey: string,
435
565
  sessionId: string,
436
566
  ) => () => TelegramWorkspaceJournalReader | undefined;
437
- discoverFollowerJournals?: () => { paths: readonly string[]; complete: boolean };
567
+ discoverFollowerJournals?: () => {
568
+ paths: readonly string[];
569
+ complete: boolean;
570
+ };
438
571
  /** Strict read-only namespace evidence; it never substitutes for writer closure. */
439
572
  inspectJournalNamespace?: () => TelegramJournalNamespaceInspection;
440
573
  createJournalPathResolver?: (
441
574
  path: string,
442
575
  ) => () => TelegramWorkspaceJournalReader | undefined;
443
- withJournalReference?: <T>(binding: TelegramWorkspaceJournalReader,
444
- operation: () => T) => T;
576
+ withJournalReference?: <T>(
577
+ binding: TelegramWorkspaceJournalReader,
578
+ operation: () => T,
579
+ ) => T;
445
580
  getJournalWriterProtection?: (
446
581
  journalBindingKey: string,
447
582
  ) => TelegramWorkspaceProtectionState;
@@ -451,51 +586,105 @@ interface TelegramWorkspaceProtectionObserverDeps {
451
586
  }
452
587
 
453
588
  export interface TelegramWorkspaceProtectionObserver {
454
- capture: (binding: TelegramWorkspaceThreadBinding,
455
- options?: { requireBindingProvenance?: boolean }) => TelegramWorkspaceExternalProtectionEvidence;
456
- captureJournalSources: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceJournalProtectionCapture;
457
- getJournalWriterProtection: (journalBindingKey: string) => TelegramWorkspaceProtectionState;
589
+ capture: (
590
+ binding: TelegramWorkspaceThreadBinding,
591
+ options?: { requireBindingProvenance?: boolean },
592
+ ) => TelegramWorkspaceExternalProtectionEvidence;
593
+ captureJournalSources: (
594
+ binding: TelegramWorkspaceThreadBinding,
595
+ ) => TelegramWorkspaceJournalProtectionCapture;
596
+ getJournalWriterProtection: (
597
+ journalBindingKey: string,
598
+ ) => TelegramWorkspaceProtectionState;
458
599
  }
459
600
 
460
601
  /** Shared read-only observer: protection and metadata pruning use the same scoped evidence path. */
461
- export function createTelegramWorkspaceProtectionObserver(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver {
462
- const captureJournalSources: TelegramWorkspaceProtectionObserver["captureJournalSources"] = (candidate) => {
463
- const namespace = deps.inspectJournalNamespace?.();
464
- if (deps.inspectJournalNamespace && (!namespace || namespace.sources.filter(source => source.role === "polling").length !== 1))
465
- throw new Error("Telegram strict namespace evidence is unavailable or incomplete.");
466
- if (namespace?.retainedInputs?.some(original => original.state !== "committed")) throw new Error(
467
- "Telegram namespace contains uncommitted private retention evidence.",
468
- );
469
- const discovery = namespace
470
- ? { paths: namespace.sources.filter(source => source.role !== "polling" && source.evidence.kind === "present")
471
- .map(source => source.path), complete: true }
472
- : candidate.journalBindingsComplete === true ? undefined : deps.discoverFollowerJournals?.();
473
- if (namespace && !deps.createJournalPathResolver) throw new Error(
474
- "Telegram strict namespace protection requires discovered source resolution.",
475
- );
476
- return captureTelegramWorkspaceJournalProtectionSources({
477
- binding: candidate,
478
- resolveLeader: deps.resolveLeaderJournal,
479
- createFollowerResolver: deps.createFollowerJournalResolver,
480
- ...(deps.createSessionJournalResolver ? { createSessionResolver: deps.createSessionJournalResolver } : {}),
481
- ...(deps.withJournalReference ? { withJournalReference: deps.withJournalReference } : {}),
482
- ...(discovery && deps.createJournalPathResolver
483
- ? { discovery: { ...discovery, createResolver: deps.createJournalPathResolver } } : {}),
484
- });
485
- };
486
- const capture: TelegramWorkspaceProtectionObserver["capture"] = function (binding, options) {
602
+ export function createTelegramWorkspaceProtectionObserver(
603
+ deps: TelegramWorkspaceProtectionObserverDeps,
604
+ ): TelegramWorkspaceProtectionObserver {
605
+ const captureJournalSources: TelegramWorkspaceProtectionObserver["captureJournalSources"] =
606
+ (candidate) => {
607
+ const namespace = deps.inspectJournalNamespace?.();
608
+ if (
609
+ deps.inspectJournalNamespace &&
610
+ (!namespace ||
611
+ namespace.sources.filter((source) => source.role === "polling")
612
+ .length !== 1)
613
+ )
614
+ throw new Error(
615
+ "Telegram strict namespace evidence is unavailable or incomplete.",
616
+ );
617
+ if (
618
+ namespace?.retainedInputs?.some(
619
+ (original) => original.state !== "committed",
620
+ )
621
+ )
622
+ throw new Error(
623
+ "Telegram namespace contains uncommitted private retention evidence.",
624
+ );
625
+ const discovery = namespace
626
+ ? {
627
+ paths: namespace.sources
628
+ .filter(
629
+ (source) =>
630
+ source.role !== "polling" &&
631
+ source.evidence.kind === "present",
632
+ )
633
+ .map((source) => source.path),
634
+ complete: true,
635
+ }
636
+ : candidate.journalBindingsComplete === true
637
+ ? undefined
638
+ : deps.discoverFollowerJournals?.();
639
+ if (namespace && !deps.createJournalPathResolver)
640
+ throw new Error(
641
+ "Telegram strict namespace protection requires discovered source resolution.",
642
+ );
643
+ return captureTelegramWorkspaceJournalProtectionSources({
644
+ binding: candidate,
645
+ resolveLeader: deps.resolveLeaderJournal,
646
+ createFollowerResolver: deps.createFollowerJournalResolver,
647
+ ...(deps.createSessionJournalResolver
648
+ ? { createSessionResolver: deps.createSessionJournalResolver }
649
+ : {}),
650
+ ...(deps.withJournalReference
651
+ ? { withJournalReference: deps.withJournalReference }
652
+ : {}),
653
+ ...(discovery && deps.createJournalPathResolver
654
+ ? {
655
+ discovery: {
656
+ ...discovery,
657
+ createResolver: deps.createJournalPathResolver,
658
+ },
659
+ }
660
+ : {}),
661
+ });
662
+ };
663
+ const capture: TelegramWorkspaceProtectionObserver["capture"] = function (
664
+ binding,
665
+ options,
666
+ ) {
487
667
  return captureTelegramWorkspaceExternalProtection({
488
668
  binding,
489
669
  requireBindingProvenance: options?.requireBindingProvenance,
490
670
  getLiveOwnerProtection(candidate) {
491
- if (deps.listFollowers().some((follower) =>
492
- !!follower.target && sameTarget(follower.target, candidate.target),
493
- )) return "protected";
671
+ if (
672
+ deps
673
+ .listFollowers()
674
+ .some(
675
+ (follower) =>
676
+ !!follower.target &&
677
+ sameTarget(follower.target, candidate.target),
678
+ )
679
+ )
680
+ return "protected";
494
681
  if (!deps.getJournalWriterProtection) return "unknown";
495
682
  let unknown = candidate.journalBindingsComplete !== true;
496
683
  const writerKeys = new Set([
497
684
  ...(candidate.journalBindingKeys ?? []),
498
- ...(candidate.journalSources ?? []).map(source => source.recipientBindingKey),
685
+ ...(candidate.journalSources ?? []).map(
686
+ (source) => source.recipientBindingKey,
687
+ ),
499
688
  ]);
500
689
  for (const journalBindingKey of writerKeys) {
501
690
  const protection = deps.getJournalWriterProtection(journalBindingKey);
@@ -518,36 +707,56 @@ export function createTelegramWorkspaceProtectionObserver(deps: TelegramWorkspac
518
707
  },
519
708
  captureJournalSources,
520
709
  ...(deps.getDeliveryAuthorityProtection
521
- ? { getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection }
710
+ ? {
711
+ getDeliveryAuthorityProtection: deps.getDeliveryAuthorityProtection,
712
+ }
522
713
  : {}),
523
714
  });
524
715
  };
525
- return { capture, captureJournalSources,
526
- getJournalWriterProtection: deps.getJournalWriterProtection ?? (() => "unknown") };
716
+ return {
717
+ capture,
718
+ captureJournalSources,
719
+ getJournalWriterProtection:
720
+ deps.getJournalWriterProtection ?? (() => "unknown"),
721
+ };
527
722
  }
528
723
 
529
724
  /** Callable protection view retained for callers that need no metadata observation ports. */
530
- export function createTelegramWorkspaceExternalProtectionCapture(deps: TelegramWorkspaceProtectionObserverDeps): TelegramWorkspaceProtectionObserver["capture"] {
725
+ export function createTelegramWorkspaceExternalProtectionCapture(
726
+ deps: TelegramWorkspaceProtectionObserverDeps,
727
+ ): TelegramWorkspaceProtectionObserver["capture"] {
531
728
  return createTelegramWorkspaceProtectionObserver(deps).capture;
532
729
  }
533
730
 
534
- export function createTelegramWorkspaceJournalEvidencePruner(deps: TelegramWorkspaceOperationGate & {
535
- store: TelegramTopicTargetStore;
536
- getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
537
- getLeaderEpoch: () => number | string | undefined;
538
- protection: Pick<TelegramWorkspaceProtectionObserver, "captureJournalSources" | "getJournalWriterProtection">;
539
- }): (binding: TelegramWorkspaceThreadBinding, isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult> {
731
+ export function createTelegramWorkspaceJournalEvidencePruner(
732
+ deps: TelegramWorkspaceOperationGate & {
733
+ store: TelegramTopicTargetStore;
734
+ getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
735
+ getLeaderEpoch: () => number | string | undefined;
736
+ protection: Pick<
737
+ TelegramWorkspaceProtectionObserver,
738
+ "captureJournalSources" | "getJournalWriterProtection"
739
+ >;
740
+ },
741
+ ): (
742
+ binding: TelegramWorkspaceThreadBinding,
743
+ isCurrent: () => boolean,
744
+ ) => Promise<TelegramWorkspaceJournalPruneResult> {
540
745
  return async (binding, isCurrent) => {
541
746
  const admission = deps.getAdmission();
542
747
  if (!admission) return { kind: "blocked", reason: "state-changed" };
543
748
  const profileKey = admission.getProfileKey();
544
749
  return pruneTelegramWorkspaceJournalEvidence({
545
- store: deps.store, binding, admission, runExclusive: deps.runExclusive,
750
+ store: deps.store,
751
+ binding,
752
+ admission,
753
+ runExclusive: deps.runExclusive,
546
754
  capture: () => deps.protection.captureJournalSources(binding),
547
755
  getJournalWriterProtection: deps.protection.getJournalWriterProtection,
548
756
  getLeaderEpoch: deps.getLeaderEpoch,
549
757
  getProfileKey: () => deps.getAdmission()?.getProfileKey() ?? "",
550
- isCurrent: () => isCurrent() && deps.getAdmission()?.getProfileKey() === profileKey,
758
+ isCurrent: () =>
759
+ isCurrent() && deps.getAdmission()?.getProfileKey() === profileKey,
551
760
  });
552
761
  };
553
762
  }
@@ -556,8 +765,9 @@ export function isCurrentTelegramWorkspaceBinding(
556
765
  store: Pick<TelegramTopicTargetStore, "listWorkspaceBindings">,
557
766
  expected: TelegramWorkspaceThreadBinding,
558
767
  ): boolean {
559
- return store.listWorkspaceBindings().some((binding) =>
560
- isDeepStrictEqual(binding, expected));
768
+ return store
769
+ .listWorkspaceBindings()
770
+ .some((binding) => isDeepStrictEqual(binding, expected));
561
771
  }
562
772
 
563
773
  export interface TelegramWorkspaceDeadQueueJournalBinding {
@@ -598,7 +808,8 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
598
808
  ) => TelegramWorkspaceExternalProtectionEvidence;
599
809
  getActiveTurnTarget: () => TelegramTarget | undefined;
600
810
  getQueuedItems: () => readonly { chatId: number; target?: TelegramTarget }[];
601
- resolveLeaderJournal: () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
811
+ resolveLeaderJournal: () =>
812
+ TelegramWorkspaceDeadQueueJournalBinding | undefined;
602
813
  createFollowerJournalResolver: (
603
814
  journalBindingKey: string,
604
815
  ) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
@@ -606,7 +817,10 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
606
817
  recipientBindingKey: string,
607
818
  sessionId: string,
608
819
  ) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
609
- discoverFollowerJournals?: () => { paths: readonly string[]; complete: boolean };
820
+ discoverFollowerJournals?: () => {
821
+ paths: readonly string[];
822
+ complete: boolean;
823
+ };
610
824
  createJournalPathResolver?: (
611
825
  path: string,
612
826
  ) => () => TelegramWorkspaceDeadQueueJournalBinding | undefined;
@@ -615,9 +829,10 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
615
829
  operation: () => T,
616
830
  ) => T;
617
831
  getRecoveryOwner: () => TelegramUpdateJournalQueueOwnerIdentity;
618
- getQueueOwnerLiveness: (
619
- owner: { processId: number; processBirthId: string },
620
- ) => "alive" | "dead" | "unverifiable";
832
+ getQueueOwnerLiveness: (owner: {
833
+ processId: number;
834
+ processBirthId: string;
835
+ }) => "alive" | "dead" | "unverifiable";
621
836
  isBindingCurrent: (binding: TelegramWorkspaceThreadBinding) => boolean;
622
837
  onMutationError?: (error: unknown) => void;
623
838
  }): (
@@ -631,7 +846,8 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
631
846
  if (active && sameTarget(active, binding.target)) return "protected";
632
847
  let unknown = false;
633
848
  for (const item of deps.getQueuedItems()) {
634
- if (item.target && sameTarget(item.target, binding.target)) return "protected";
849
+ if (item.target && sameTarget(item.target, binding.target))
850
+ return "protected";
635
851
  if (!item.target && item.chatId === binding.target.chatId) unknown = true;
636
852
  }
637
853
  return unknown ? "unknown" : "clear";
@@ -644,7 +860,10 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
644
860
  if (binding.journalSources?.length && !deps.createSessionJournalResolver)
645
861
  return { kind: "blocked", reason: "incomplete-source" };
646
862
  const initial = deps.getExternalProtection(binding);
647
- if (initial.liveOwner !== "clear" || initial.deliveryAuthority !== "clear") {
863
+ if (
864
+ initial.liveOwner !== "clear" ||
865
+ initial.deliveryAuthority !== "clear"
866
+ ) {
648
867
  return { kind: "blocked", reason: "live-owner" };
649
868
  }
650
869
  if (localProtection(binding) !== "clear") {
@@ -652,10 +871,12 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
652
871
  }
653
872
  if (initial.acceptedWork === "clear") return { kind: "not-needed" };
654
873
 
655
- const discovery = binding.journalBindingsComplete === true
656
- ? undefined
657
- : deps.discoverFollowerJournals?.();
658
- let complete = binding.journalBindingsComplete === true || discovery?.complete === true;
874
+ const discovery =
875
+ binding.journalBindingsComplete === true
876
+ ? undefined
877
+ : deps.discoverFollowerJournals?.();
878
+ let complete =
879
+ binding.journalBindingsComplete === true || discovery?.complete === true;
659
880
  const sources: Array<{
660
881
  scope: "shared" | "binding" | "discovered";
661
882
  binding: TelegramWorkspaceDeadQueueJournalBinding;
@@ -687,7 +908,13 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
687
908
  capture("binding", deps.createFollowerJournalResolver(key));
688
909
  }
689
910
  for (const source of binding.journalSources ?? []) {
690
- capture("binding", deps.createSessionJournalResolver!(source.recipientBindingKey, source.sessionId));
911
+ capture(
912
+ "binding",
913
+ deps.createSessionJournalResolver!(
914
+ source.recipientBindingKey,
915
+ source.sessionId,
916
+ ),
917
+ );
691
918
  }
692
919
  for (const path of discovery?.paths ?? []) {
693
920
  if (!deps.createJournalPathResolver) {
@@ -703,55 +930,80 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
703
930
  recovery: TelegramUpdateJournalDeadQueueOwnerRecoveryInput;
704
931
  }> = [];
705
932
  for (const source of sources) {
706
- const relevant = source.scope === "binding"
707
- ? [...source.entries]
708
- : source.entries.filter((entry) => {
709
- const target = getJournalUpdateTarget(entry.update);
710
- return !!target && sameTarget(target, binding.target);
711
- });
712
- if (source.scope === "binding" && relevant.some((entry) => {
713
- const target = getJournalUpdateTarget(entry.update);
714
- return !target || !sameTarget(target, binding.target);
715
- })) return { kind: "blocked", reason: "unsupported-custody" };
933
+ const relevant =
934
+ source.scope === "binding"
935
+ ? [...source.entries]
936
+ : source.entries.filter((entry) => {
937
+ const target = getJournalUpdateTarget(entry.update);
938
+ return !!target && sameTarget(target, binding.target);
939
+ });
940
+ if (
941
+ source.scope === "binding" &&
942
+ relevant.some((entry) => {
943
+ const target = getJournalUpdateTarget(entry.update);
944
+ return !target || !sameTarget(target, binding.target);
945
+ })
946
+ )
947
+ return { kind: "blocked", reason: "unsupported-custody" };
716
948
  const seen = new Set<string>();
717
949
  for (const entry of relevant) {
718
- if (entry.state !== "queued" ||
719
- (entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
720
- !entry.queueReceiptId || !entry.queueOwner || entry.queueHandoff) {
950
+ if (
951
+ entry.state !== "queued" ||
952
+ (entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
953
+ !entry.queueReceiptId ||
954
+ !entry.queueOwner ||
955
+ entry.queueHandoff
956
+ ) {
721
957
  return { kind: "blocked", reason: "unsupported-custody" };
722
958
  }
723
959
  if (seen.has(entry.queueReceiptId)) continue;
724
960
  const receiptEntries = source.entries.filter(
725
961
  (candidate) => candidate.queueReceiptId === entry.queueReceiptId,
726
962
  );
727
- if (!receiptEntries.length || receiptEntries.some((candidate) => {
728
- const target = getJournalUpdateTarget(candidate.update);
729
- return candidate.state !== "queued" ||
730
- candidate.queueKind !== entry.queueKind ||
731
- !candidate.queueOwner || candidate.queueHandoff !== undefined ||
732
- !isDeepStrictEqual(candidate.queueOwner, entry.queueOwner) ||
733
- !target || !sameTarget(target, binding.target);
734
- })) return { kind: "blocked", reason: "unsupported-custody" };
963
+ if (
964
+ !receiptEntries.length ||
965
+ receiptEntries.some((candidate) => {
966
+ const target = getJournalUpdateTarget(candidate.update);
967
+ return (
968
+ candidate.state !== "queued" ||
969
+ candidate.queueKind !== entry.queueKind ||
970
+ !candidate.queueOwner ||
971
+ candidate.queueHandoff !== undefined ||
972
+ !isDeepStrictEqual(candidate.queueOwner, entry.queueOwner) ||
973
+ !target ||
974
+ !sameTarget(target, binding.target)
975
+ );
976
+ })
977
+ )
978
+ return { kind: "blocked", reason: "unsupported-custody" };
735
979
  seen.add(entry.queueReceiptId);
736
980
  plans.push({
737
981
  source: source.binding,
738
982
  recovery: {
739
983
  queueKind: entry.queueKind,
740
984
  receiptId: entry.queueReceiptId,
741
- sourceUpdateIds: receiptEntries.map((candidate) => candidate.updateId).sort((a, b) => a - b),
985
+ sourceUpdateIds: receiptEntries
986
+ .map((candidate) => candidate.updateId)
987
+ .sort((a, b) => a - b),
742
988
  deadOwner: entry.queueOwner,
743
989
  recoveryOwner: deps.getRecoveryOwner(),
744
990
  },
745
991
  });
746
992
  }
747
993
  }
748
- if (!plans.length) return { kind: "blocked", reason: "protection-retained" };
994
+ if (!plans.length)
995
+ return { kind: "blocked", reason: "protection-retained" };
749
996
  for (const plan of plans) {
750
997
  let liveness: "alive" | "dead" | "unverifiable" = "unverifiable";
751
- try { liveness = deps.getQueueOwnerLiveness(plan.recovery.deadOwner); }
752
- catch { /* Unknown process evidence is never destructive authority. */ }
753
- if (liveness === "alive") return { kind: "blocked", reason: "owner-alive" };
754
- if (liveness !== "dead") return { kind: "blocked", reason: "owner-unverifiable" };
998
+ try {
999
+ liveness = deps.getQueueOwnerLiveness(plan.recovery.deadOwner);
1000
+ } catch {
1001
+ /* Unknown process evidence is never destructive authority. */
1002
+ }
1003
+ if (liveness === "alive")
1004
+ return { kind: "blocked", reason: "owner-alive" };
1005
+ if (liveness !== "dead")
1006
+ return { kind: "blocked", reason: "owner-unverifiable" };
755
1007
  }
756
1008
 
757
1009
  const recoveredIds: number[] = [];
@@ -760,7 +1012,10 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
760
1012
  return { kind: "blocked", reason: "authority-changed" };
761
1013
  }
762
1014
  const current = deps.getExternalProtection(binding);
763
- if (current.liveOwner !== "clear" || current.deliveryAuthority !== "clear") {
1015
+ if (
1016
+ current.liveOwner !== "clear" ||
1017
+ current.deliveryAuthority !== "clear"
1018
+ ) {
764
1019
  return { kind: "blocked", reason: "live-owner" };
765
1020
  }
766
1021
  if (localProtection(binding) !== "clear") {
@@ -770,10 +1025,15 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
770
1025
  try {
771
1026
  result = deps.withJournalReference
772
1027
  ? deps.withJournalReference(plan.source, () =>
773
- plan.source.journal.recoverDeadQueueOwner(plan.recovery))
1028
+ plan.source.journal.recoverDeadQueueOwner(plan.recovery),
1029
+ )
774
1030
  : plan.source.journal.recoverDeadQueueOwner(plan.recovery);
775
1031
  } catch (error) {
776
- try { deps.onMutationError?.(error); } catch { /* Diagnostics are fail-open. */ }
1032
+ try {
1033
+ deps.onMutationError?.(error);
1034
+ } catch {
1035
+ /* Diagnostics are fail-open. */
1036
+ }
777
1037
  return { kind: "blocked", reason: "mutation-refused" };
778
1038
  }
779
1039
  if (result.status === "owner-alive") {
@@ -788,8 +1048,11 @@ export function createTelegramWorkspaceDeadOwnerQueueReclaimer(deps: {
788
1048
  return { kind: "blocked", reason: "authority-changed" };
789
1049
  }
790
1050
  const after = deps.getExternalProtection(binding);
791
- if (after.liveOwner !== "clear" || after.acceptedWork !== "clear" ||
792
- after.deliveryAuthority !== "clear") {
1051
+ if (
1052
+ after.liveOwner !== "clear" ||
1053
+ after.acceptedWork !== "clear" ||
1054
+ after.deliveryAuthority !== "clear"
1055
+ ) {
793
1056
  return { kind: "blocked", reason: "protection-retained" };
794
1057
  }
795
1058
  return {
@@ -833,9 +1096,14 @@ export async function adoptTelegramWorkspaceRetirementIntent(input: {
833
1096
  const leaderEpoch = input.getLeaderEpoch();
834
1097
  const profileKey = input.getProfileKey();
835
1098
  const isCurrent = () =>
836
- leaderEpoch !== undefined && input.getLeaderEpoch() === leaderEpoch &&
837
- input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
838
- if (!isCurrent()) throw new Error("Telegram Workspace retirement adoption requires current leader authority.");
1099
+ leaderEpoch !== undefined &&
1100
+ input.getLeaderEpoch() === leaderEpoch &&
1101
+ input.getProfileKey() === profileKey &&
1102
+ input.isCurrent?.() !== false;
1103
+ if (!isCurrent())
1104
+ throw new Error(
1105
+ "Telegram Workspace retirement adoption requires current leader authority.",
1106
+ );
839
1107
  const intents = input.store.listWorkspaceRetirementIntents();
840
1108
  if (intents.length !== 1 || !isDeepStrictEqual(intents[0], input.intent)) {
841
1109
  return { kind: "blocked", reason: "intent-conflict" };
@@ -843,41 +1111,47 @@ export async function adoptTelegramWorkspaceRetirementIntent(input: {
843
1111
  if (input.intent.profileKey !== profileKey) {
844
1112
  return { kind: "blocked", reason: "profile-changed" };
845
1113
  }
846
- const binding = input.store.listWorkspaceBindings().find((candidate) =>
847
- candidate.bindingKey === input.intent.binding.bindingKey,
848
- );
1114
+ const binding = input.store
1115
+ .listWorkspaceBindings()
1116
+ .find(
1117
+ (candidate) => candidate.bindingKey === input.intent.binding.bindingKey,
1118
+ );
849
1119
  if (!binding || !isDeepStrictEqual(binding, input.intent.binding)) {
850
1120
  return { kind: "blocked", reason: "binding-changed" };
851
1121
  }
852
- const eligible = input.store.captureWorkspaceSlotOccupancy(
853
- input.getExternalProtection,
854
- { expectedRetirement: input.intent },
855
- ).bindings.find((candidate) =>
856
- candidate.bindingKey === binding.bindingKey,
857
- )?.protection === "eligible";
1122
+ const eligible =
1123
+ input.store
1124
+ .captureWorkspaceSlotOccupancy(input.getExternalProtection, {
1125
+ expectedRetirement: input.intent,
1126
+ })
1127
+ .bindings.find(
1128
+ (candidate) => candidate.bindingKey === binding.bindingKey,
1129
+ )?.protection === "eligible";
858
1130
  if (!eligible) return { kind: "blocked", reason: "protection-changed" };
859
1131
  const replacement = { ...input.intent, leaderEpoch: leaderEpoch! };
860
- if (!isCurrent()) throw new Error("Telegram Workspace retirement adoption lost leader authority.");
861
- if (!await input.store.replaceWorkspaceRetirementIntent(
862
- input.intent,
863
- replacement,
864
- isCurrent,
865
- )) return { kind: "blocked", reason: "commit-rejected" };
1132
+ if (!isCurrent())
1133
+ throw new Error(
1134
+ "Telegram Workspace retirement adoption lost leader authority.",
1135
+ );
1136
+ if (
1137
+ !(await input.store.replaceWorkspaceRetirementIntent(
1138
+ input.intent,
1139
+ replacement,
1140
+ isCurrent,
1141
+ ))
1142
+ )
1143
+ return { kind: "blocked", reason: "commit-rejected" };
866
1144
  return { kind: "adopted", intent: replacement };
867
1145
  });
868
1146
  }
869
1147
 
870
1148
  function isTelegramWorkspaceDeletionConfirmedAbsent(error: unknown): boolean {
871
1149
  if (!(error instanceof Error)) return false;
872
- const status = "status" in error && typeof error.status === "number"
873
- ? error.status : undefined;
874
- if (status !== 400) return false;
875
- const message = error.message.toLowerCase();
876
- return message.includes("topic_id_invalid") ||
877
- message.includes("message thread not found") ||
878
- message.includes("thread not found") ||
879
- message.includes("topic not found") ||
880
- message.includes("topic deleted");
1150
+ const status =
1151
+ "status" in error && typeof error.status === "number"
1152
+ ? error.status
1153
+ : undefined;
1154
+ return status === 400 && isTelegramTopicDeletedErrorMessage(error.message);
881
1155
  }
882
1156
 
883
1157
  export type TelegramWorkspaceRetirementExecution =
@@ -898,9 +1172,7 @@ export type TelegramWorkspaceRetirementExecution =
898
1172
  };
899
1173
 
900
1174
  export type TelegramWorkspaceRetirementAbsence =
901
- | "absent"
902
- | "present"
903
- | "unknown";
1175
+ "absent" | "present" | "unknown";
904
1176
 
905
1177
  function matchesTelegramWorkspaceRetirementFence(
906
1178
  fence: TelegramWorkspaceRetirementFence,
@@ -918,27 +1190,51 @@ function matchesTelegramWorkspaceRetirementFence(
918
1190
  }
919
1191
 
920
1192
  async function cancelRejectedTelegramWorkspaceRetirement(
921
- input: Pick<Parameters<typeof executeTelegramWorkspaceRetirement>[0],
922
- "store" | "admission" | "getLeaderEpoch" | "getProfileKey" | "isCurrent">,
1193
+ input: Pick<
1194
+ Parameters<typeof executeTelegramWorkspaceRetirement>[0],
1195
+ "store" | "admission" | "getLeaderEpoch" | "getProfileKey" | "isCurrent"
1196
+ >,
923
1197
  retained: TelegramWorkspaceRetirementFence,
924
1198
  ): Promise<Exclude<TelegramWorkspaceRetirementExecution, { kind: "retired" }>> {
925
1199
  const epoch = input.getLeaderEpoch();
926
- const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
927
- input.getProfileKey() === retained.profileKey && input.isCurrent?.() !== false;
1200
+ const isCurrent = () =>
1201
+ epoch !== undefined &&
1202
+ input.getLeaderEpoch() === epoch &&
1203
+ input.getProfileKey() === retained.profileKey &&
1204
+ input.isCurrent?.() !== false;
928
1205
  if (!isCurrent()) return { kind: "retained", reason: "authority-changed" };
929
- if (retained.phase !== "deletion-rejected" ||
930
- (retained.destructiveKind ?? "pressure-retirement") !== "pressure-retirement") {
1206
+ if (
1207
+ retained.phase !== "deletion-rejected" ||
1208
+ (retained.destructiveKind ?? "pressure-retirement") !==
1209
+ "pressure-retirement"
1210
+ ) {
931
1211
  return { kind: "retained", reason: "fence-conflict" };
932
1212
  }
933
1213
  const owner = input.admission.getOwner();
934
- const fence = retained.leaderEpoch === epoch && isDeepStrictEqual(retained.owner, owner)
935
- ? retained : input.admission.adoptRetirementFence(retained, { owner, leaderEpoch: epoch! });
936
- if (fence.phase !== "deletion-rejected") return { kind: "retained", reason: "fence-conflict" };
937
- const bindingRetained = () => input.store.listWorkspaceBindings().some(binding =>
938
- binding.bindingKey === fence.bindingKey && binding.slot === fence.slot && sameTarget(binding.target, fence.target));
1214
+ const fence =
1215
+ retained.leaderEpoch === epoch && isDeepStrictEqual(retained.owner, owner)
1216
+ ? retained
1217
+ : input.admission.adoptRetirementFence(retained, {
1218
+ owner,
1219
+ leaderEpoch: epoch!,
1220
+ });
1221
+ if (fence.phase !== "deletion-rejected")
1222
+ return { kind: "retained", reason: "fence-conflict" };
1223
+ const bindingRetained = () =>
1224
+ input.store
1225
+ .listWorkspaceBindings()
1226
+ .some(
1227
+ (binding) =>
1228
+ binding.bindingKey === fence.bindingKey &&
1229
+ binding.slot === fence.slot &&
1230
+ sameTarget(binding.target, fence.target),
1231
+ );
939
1232
  const intents = input.store.listWorkspaceRetirementIntents();
940
- if (!bindingRetained() || intents.length > 1 ||
941
- (intents[0] && !matchesTelegramWorkspaceRetirementFence(fence, intents[0]))) {
1233
+ if (
1234
+ !bindingRetained() ||
1235
+ intents.length > 1 ||
1236
+ (intents[0] && !matchesTelegramWorkspaceRetirementFence(fence, intents[0]))
1237
+ ) {
942
1238
  return { kind: "retained", reason: "stale-intent" };
943
1239
  }
944
1240
  if (intents[0] && !input.store.removeWorkspaceRetirementIntent(intents[0])) {
@@ -948,12 +1244,17 @@ async function cancelRejectedTelegramWorkspaceRetirement(
948
1244
  // in-memory withdrawal. The rejection fence protects both commit prefixes.
949
1245
  await input.store.persist();
950
1246
  if (!isCurrent()) return { kind: "retained", reason: "authority-changed" };
951
- if (input.store.listWorkspaceRetirementIntents().length || !bindingRetained()) {
1247
+ if (
1248
+ input.store.listWorkspaceRetirementIntents().length ||
1249
+ !bindingRetained()
1250
+ ) {
952
1251
  return { kind: "retained", reason: "commit-rejected" };
953
1252
  }
954
- try { input.admission.completeRejectedRetirementFence(fence); }
955
- catch {
956
- if (input.admission.read().fence) return { kind: "retained", reason: "fence-release-unconfirmed" };
1253
+ try {
1254
+ input.admission.completeRejectedRetirementFence(fence);
1255
+ } catch {
1256
+ if (input.admission.read().fence)
1257
+ return { kind: "retained", reason: "fence-release-unconfirmed" };
957
1258
  }
958
1259
  return { kind: "cancelled", reason: "delete-rejected" };
959
1260
  }
@@ -1005,7 +1306,10 @@ export async function executeTelegramWorkspaceRetirement(input: {
1005
1306
  input.getProfileKey() === input.intent.profileKey &&
1006
1307
  input.isCurrent?.() !== false;
1007
1308
  if (!isCurrent()) return { kind: "retained", reason: "authority-changed" };
1008
- if (!input.intent.binding.slot || !/^[A-Z]$/u.test(input.intent.binding.slot)) {
1309
+ if (
1310
+ !input.intent.binding.slot ||
1311
+ !/^[A-Z]$/u.test(input.intent.binding.slot)
1312
+ ) {
1009
1313
  return { kind: "retained", reason: "stale-intent" };
1010
1314
  }
1011
1315
  const owner = input.admission.getOwner();
@@ -1014,7 +1318,10 @@ export async function executeTelegramWorkspaceRetirement(input: {
1014
1318
  return { kind: "retained", reason: "fence-conflict" };
1015
1319
  }
1016
1320
  let fence: TelegramWorkspaceRetirementFence | undefined = storedFence;
1017
- if (fence && !matchesTelegramWorkspaceRetirementFence(fence, input.intent)) {
1321
+ if (
1322
+ fence &&
1323
+ !matchesTelegramWorkspaceRetirementFence(fence, input.intent)
1324
+ ) {
1018
1325
  return { kind: "retained", reason: "fence-conflict" };
1019
1326
  }
1020
1327
  if (
@@ -1029,15 +1336,24 @@ export async function executeTelegramWorkspaceRetirement(input: {
1029
1336
  });
1030
1337
  }
1031
1338
  if (fence?.phase === "deletion-rejected") {
1032
- return cancelRejectedTelegramWorkspaceRetirement({ ...input, isCurrent }, fence);
1339
+ return cancelRejectedTelegramWorkspaceRetirement(
1340
+ { ...input, isCurrent },
1341
+ fence,
1342
+ );
1033
1343
  }
1034
- const intent = input.store.listWorkspaceRetirementIntents().find((candidate) =>
1035
- isDeepStrictEqual(candidate, input.intent),
1036
- );
1037
- const binding = input.store.listWorkspaceBindings().find((candidate) =>
1038
- candidate.bindingKey === input.intent.binding.bindingKey,
1039
- );
1040
- if (!intent || !binding || !isDeepStrictEqual(binding, input.intent.binding)) {
1344
+ const intent = input.store
1345
+ .listWorkspaceRetirementIntents()
1346
+ .find((candidate) => isDeepStrictEqual(candidate, input.intent));
1347
+ const binding = input.store
1348
+ .listWorkspaceBindings()
1349
+ .find(
1350
+ (candidate) => candidate.bindingKey === input.intent.binding.bindingKey,
1351
+ );
1352
+ if (
1353
+ !intent ||
1354
+ !binding ||
1355
+ !isDeepStrictEqual(binding, input.intent.binding)
1356
+ ) {
1041
1357
  if (!intent && !binding && fence?.phase === "commit-ready") {
1042
1358
  try {
1043
1359
  input.admission.completeRetirementFence(fence);
@@ -1060,12 +1376,15 @@ export async function executeTelegramWorkspaceRetirement(input: {
1060
1376
  // publication while this stale local projection is removed.
1061
1377
  input.store.markStaleByTarget(binding.target, "deleted");
1062
1378
  }
1063
- const eligible = () => input.store.captureWorkspaceSlotOccupancy(
1064
- input.getExternalProtection,
1065
- { expectedRetirement: input.intent },
1066
- ).bindings.find((candidate) =>
1067
- candidate.bindingKey === input.intent.binding.bindingKey,
1068
- )?.protection === "eligible";
1379
+ const eligible = () =>
1380
+ input.store
1381
+ .captureWorkspaceSlotOccupancy(input.getExternalProtection, {
1382
+ expectedRetirement: input.intent,
1383
+ })
1384
+ .bindings.find(
1385
+ (candidate) =>
1386
+ candidate.bindingKey === input.intent.binding.bindingKey,
1387
+ )?.protection === "eligible";
1069
1388
  if (!eligible()) return { kind: "retained", reason: "protection-changed" };
1070
1389
  if (!fence) {
1071
1390
  const acquired = input.admission.acquireRetirementFence({
@@ -1112,10 +1431,20 @@ export async function executeTelegramWorkspaceRetirement(input: {
1112
1431
  } catch (error) {
1113
1432
  if (!isTelegramWorkspaceDeletionConfirmedAbsent(error)) {
1114
1433
  const target = getTelegramApiErrorRequestTarget(error);
1115
- if (target && sameTarget(target, binding.target) && isTelegramApiRequestRejected(error, "deleteForumTopic")) {
1434
+ if (
1435
+ target &&
1436
+ sameTarget(target, binding.target) &&
1437
+ isTelegramApiRequestRejected(error, "deleteForumTopic")
1438
+ ) {
1116
1439
  fence = input.admission.confirmRetirementRejection(fence);
1117
- const cancellation = await cancelRejectedTelegramWorkspaceRetirement({ ...input, isCurrent }, fence);
1118
- return cancellation.kind === "cancelled" ? { kind: "retained", reason: "delete-rejected" } : cancellation;
1440
+ const cancellation =
1441
+ await cancelRejectedTelegramWorkspaceRetirement(
1442
+ { ...input, isCurrent },
1443
+ fence,
1444
+ );
1445
+ return cancellation.kind === "cancelled"
1446
+ ? { kind: "retained", reason: "delete-rejected" }
1447
+ : cancellation;
1119
1448
  }
1120
1449
  return { kind: "retained", reason: "delete-unconfirmed" };
1121
1450
  }
@@ -1133,7 +1462,9 @@ export async function executeTelegramWorkspaceRetirement(input: {
1133
1462
  input.store.markStaleByTarget(binding.target, "deleted");
1134
1463
  }
1135
1464
  if (!eligible()) return { kind: "retained", reason: "protection-changed" };
1136
- if (!await input.store.commitWorkspaceRetirement(input.intent, isCurrent)) {
1465
+ if (
1466
+ !(await input.store.commitWorkspaceRetirement(input.intent, isCurrent))
1467
+ ) {
1137
1468
  return { kind: "retained", reason: "commit-rejected" };
1138
1469
  }
1139
1470
  try {
@@ -1213,7 +1544,10 @@ export async function prepareTelegramWorkspaceRetirement(
1213
1544
  deps.getLeaderEpoch() === leaderEpoch &&
1214
1545
  deps.getProfileKey() === profileKey &&
1215
1546
  deps.isCurrent?.() !== false;
1216
- if (!isCurrent()) throw new Error("Telegram Workspace retirement requires current leader authority.");
1547
+ if (!isCurrent())
1548
+ throw new Error(
1549
+ "Telegram Workspace retirement requires current leader authority.",
1550
+ );
1217
1551
 
1218
1552
  const existing = deps.store.listWorkspaceRetirementIntents();
1219
1553
  if (existing.length > 1) {
@@ -1221,27 +1555,30 @@ export async function prepareTelegramWorkspaceRetirement(
1221
1555
  }
1222
1556
  if (existing.length === 1) {
1223
1557
  const intent = existing[0]!;
1224
- const binding = deps.store.listWorkspaceBindings().find((candidate) =>
1225
- candidate.bindingKey === intent.binding.bindingKey,
1226
- );
1558
+ const binding = deps.store
1559
+ .listWorkspaceBindings()
1560
+ .find((candidate) => candidate.bindingKey === intent.binding.bindingKey);
1227
1561
  const snapshot = deps.store.captureWorkspaceSlotOccupancy(
1228
1562
  deps.getExternalProtection,
1229
1563
  { expectedRetirement: intent },
1230
1564
  );
1231
- const candidate = snapshot.bindings.find((entry) =>
1232
- entry.bindingKey === intent.binding.bindingKey,
1565
+ const candidate = snapshot.bindings.find(
1566
+ (entry) => entry.bindingKey === intent.binding.bindingKey,
1233
1567
  );
1234
1568
  if (
1235
1569
  intent.profileKey !== profileKey ||
1236
1570
  intent.leaderEpoch !== leaderEpoch ||
1237
- !binding || !isDeepStrictEqual(binding, intent.binding) ||
1571
+ !binding ||
1572
+ !isDeepStrictEqual(binding, intent.binding) ||
1238
1573
  candidate?.protection !== "eligible"
1239
1574
  ) {
1240
1575
  return { kind: "blocked", reason: "stale-intent" };
1241
1576
  }
1242
- if (!isCurrent()) throw new Error("Telegram Workspace retirement lost leader authority.");
1577
+ if (!isCurrent())
1578
+ throw new Error("Telegram Workspace retirement lost leader authority.");
1243
1579
  await deps.store.persist();
1244
- if (!isCurrent()) throw new Error("Telegram Workspace retirement lost leader authority.");
1580
+ if (!isCurrent())
1581
+ throw new Error("Telegram Workspace retirement lost leader authority.");
1245
1582
  return { kind: "ready", intent };
1246
1583
  }
1247
1584
 
@@ -1256,9 +1593,9 @@ export async function prepareTelegramWorkspaceRetirement(
1256
1593
  );
1257
1594
  if (selection.kind !== "candidate") return selection;
1258
1595
  const selected = selection.candidate;
1259
- const binding = deps.store.listWorkspaceBindings().find((candidate) =>
1260
- candidate.bindingKey === selected.bindingKey,
1261
- );
1596
+ const binding = deps.store
1597
+ .listWorkspaceBindings()
1598
+ .find((candidate) => candidate.bindingKey === selected.bindingKey);
1262
1599
  if (
1263
1600
  !binding?.slot ||
1264
1601
  binding.slot.toLowerCase() !== selected.slot ||
@@ -1274,21 +1611,25 @@ export async function prepareTelegramWorkspaceRetirement(
1274
1611
  leaderEpoch: leaderEpoch!,
1275
1612
  requestedAtMs: nowMs,
1276
1613
  };
1277
- if (!isCurrent()) throw new Error("Telegram Workspace retirement lost leader authority.");
1614
+ if (!isCurrent())
1615
+ throw new Error("Telegram Workspace retirement lost leader authority.");
1278
1616
  if (!deps.store.upsertWorkspaceRetirementIntent(intent)) {
1279
1617
  return { kind: "blocked", reason: "state-changed" };
1280
1618
  }
1281
- const rechecked = deps.store.captureWorkspaceSlotOccupancy(
1282
- deps.getExternalProtection,
1283
- { expectedRetirement: intent },
1284
- ).bindings.find((candidate) => candidate.bindingKey === binding.bindingKey);
1619
+ const rechecked = deps.store
1620
+ .captureWorkspaceSlotOccupancy(deps.getExternalProtection, {
1621
+ expectedRetirement: intent,
1622
+ })
1623
+ .bindings.find((candidate) => candidate.bindingKey === binding.bindingKey);
1285
1624
  if (rechecked?.protection !== "eligible" || !isCurrent()) {
1286
1625
  deps.store.removeWorkspaceRetirementIntent(intent);
1287
- if (!isCurrent()) throw new Error("Telegram Workspace retirement lost leader authority.");
1626
+ if (!isCurrent())
1627
+ throw new Error("Telegram Workspace retirement lost leader authority.");
1288
1628
  return { kind: "blocked", reason: "state-changed" };
1289
1629
  }
1290
1630
  await deps.store.persist();
1291
- if (!isCurrent()) throw new Error("Telegram Workspace retirement lost leader authority.");
1631
+ if (!isCurrent())
1632
+ throw new Error("Telegram Workspace retirement lost leader authority.");
1292
1633
  return { kind: "ready", intent };
1293
1634
  }
1294
1635
 
@@ -1302,10 +1643,13 @@ export type TelegramWorkspaceRetirementLifecycleResult =
1302
1643
  };
1303
1644
 
1304
1645
  export async function runTelegramWorkspaceRetirementLifecycle(input: {
1305
- store: TelegramWorkspaceRetirementPreparationDeps["store"] & Pick<
1306
- TelegramTopicTargetStore,
1307
- "replaceWorkspaceRetirementIntent" | "commitWorkspaceRetirement" | "markStaleByTarget"
1308
- >;
1646
+ store: TelegramWorkspaceRetirementPreparationDeps["store"] &
1647
+ Pick<
1648
+ TelegramTopicTargetStore,
1649
+ | "replaceWorkspaceRetirementIntent"
1650
+ | "commitWorkspaceRetirement"
1651
+ | "markStaleByTarget"
1652
+ >;
1309
1653
  getExternalProtection: (
1310
1654
  binding: TelegramWorkspaceThreadBinding,
1311
1655
  ) => TelegramWorkspaceExternalProtectionEvidence;
@@ -1314,7 +1658,9 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
1314
1658
  isCurrent?: () => boolean;
1315
1659
  getNowMs?: () => number;
1316
1660
  runExclusive: <T>(operation: () => Promise<T>) => Promise<T>;
1317
- admission: Parameters<typeof executeTelegramWorkspaceRetirement>[0]["admission"];
1661
+ admission: Parameters<
1662
+ typeof executeTelegramWorkspaceRetirement
1663
+ >[0]["admission"];
1318
1664
  deleteForumTopic: Parameters<
1319
1665
  typeof executeTelegramWorkspaceRetirement
1320
1666
  >[0]["deleteForumTopic"];
@@ -1324,51 +1670,101 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
1324
1670
  }): Promise<TelegramWorkspaceRetirementLifecycleResult> {
1325
1671
  let intent = input.store.listWorkspaceRetirementIntents()[0];
1326
1672
  const retainedFence = input.admission.read().fence;
1327
- if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
1328
- retainedFence.phase === "deletion-rejected") {
1329
- return input.runExclusive(() => cancelRejectedTelegramWorkspaceRetirement(input, retainedFence));
1673
+ if (
1674
+ retainedFence &&
1675
+ isTelegramWorkspaceRetirementFence(retainedFence) &&
1676
+ retainedFence.phase === "deletion-rejected"
1677
+ ) {
1678
+ return input.runExclusive(() =>
1679
+ cancelRejectedTelegramWorkspaceRetirement(input, retainedFence),
1680
+ );
1330
1681
  }
1331
- if (!intent && retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
1332
- (retainedFence.destructiveKind ?? "pressure-retirement") === "pressure-retirement" &&
1333
- retainedFence.phase === "commit-ready") {
1682
+ if (
1683
+ !intent &&
1684
+ retainedFence &&
1685
+ isTelegramWorkspaceRetirementFence(retainedFence) &&
1686
+ (retainedFence.destructiveKind ?? "pressure-retirement") ===
1687
+ "pressure-retirement" &&
1688
+ retainedFence.phase === "commit-ready"
1689
+ ) {
1334
1690
  return input.runExclusive(async () => {
1335
1691
  const epoch = input.getLeaderEpoch();
1336
- if (epoch === undefined || input.getProfileKey() !== retainedFence.profileKey ||
1337
- input.isCurrent?.() === false) return { kind: "retained", reason: "authority-changed" };
1338
- if (input.store.listWorkspaceRetirementIntents().length ||
1339
- input.store.listWorkspaceBindings().some((binding) =>
1340
- binding.bindingKey === retainedFence.bindingKey)) {
1692
+ if (
1693
+ epoch === undefined ||
1694
+ input.getProfileKey() !== retainedFence.profileKey ||
1695
+ input.isCurrent?.() === false
1696
+ )
1697
+ return { kind: "retained", reason: "authority-changed" };
1698
+ if (
1699
+ input.store.listWorkspaceRetirementIntents().length ||
1700
+ input.store
1701
+ .listWorkspaceBindings()
1702
+ .some((binding) => binding.bindingKey === retainedFence.bindingKey)
1703
+ ) {
1341
1704
  return { kind: "retained", reason: "stale-intent" };
1342
1705
  }
1343
1706
  const owner = input.admission.getOwner();
1344
- const fence = retainedFence.leaderEpoch === epoch &&
1345
- isDeepStrictEqual(retainedFence.owner, owner) ? retainedFence :
1346
- input.admission.adoptRetirementFence(retainedFence, { owner, leaderEpoch: epoch });
1707
+ const fence =
1708
+ retainedFence.leaderEpoch === epoch &&
1709
+ isDeepStrictEqual(retainedFence.owner, owner)
1710
+ ? retainedFence
1711
+ : input.admission.adoptRetirementFence(retainedFence, {
1712
+ owner,
1713
+ leaderEpoch: epoch,
1714
+ });
1347
1715
  input.admission.completeRetirementFence(fence);
1348
- return { kind: "retired", bindingKey: fence.bindingKey, slot: fence.slot };
1716
+ return {
1717
+ kind: "retired",
1718
+ bindingKey: fence.bindingKey,
1719
+ slot: fence.slot,
1720
+ };
1349
1721
  });
1350
1722
  }
1351
1723
  if (intent) {
1352
- if (retainedFence && isTelegramWorkspaceRetirementFence(retainedFence) &&
1353
- retainedFence.phase === "commit-ready" &&
1354
- matchesTelegramWorkspaceRetirementFence(retainedFence, intent)) {
1724
+ if (
1725
+ retainedFence &&
1726
+ isTelegramWorkspaceRetirementFence(retainedFence) &&
1727
+ retainedFence.phase === "commit-ready" &&
1728
+ matchesTelegramWorkspaceRetirementFence(retainedFence, intent)
1729
+ ) {
1355
1730
  const resumed = await input.runExclusive(async () => {
1356
1731
  const epoch = input.getLeaderEpoch();
1357
1732
  const profileKey = input.getProfileKey();
1358
- const isCurrent = () => epoch !== undefined && input.getLeaderEpoch() === epoch &&
1359
- input.getProfileKey() === profileKey && input.isCurrent?.() !== false;
1733
+ const isCurrent = () =>
1734
+ epoch !== undefined &&
1735
+ input.getLeaderEpoch() === epoch &&
1736
+ input.getProfileKey() === profileKey &&
1737
+ input.isCurrent?.() !== false;
1360
1738
  if (!isCurrent() || intent!.profileKey !== profileKey) return undefined;
1361
1739
  const intents = input.store.listWorkspaceRetirementIntents();
1362
- const binding = input.store.listWorkspaceBindings().find(candidate =>
1363
- candidate.bindingKey === intent!.binding.bindingKey);
1364
- if (intents.length !== 1 || !isDeepStrictEqual(intents[0], intent) ||
1365
- !binding || !isDeepStrictEqual(binding, intent.binding)) return undefined;
1740
+ const binding = input.store
1741
+ .listWorkspaceBindings()
1742
+ .find(
1743
+ (candidate) => candidate.bindingKey === intent!.binding.bindingKey,
1744
+ );
1745
+ if (
1746
+ intents.length !== 1 ||
1747
+ !isDeepStrictEqual(intents[0], intent) ||
1748
+ !binding ||
1749
+ !isDeepStrictEqual(binding, intent.binding)
1750
+ )
1751
+ return undefined;
1366
1752
  if (intent!.leaderEpoch === epoch) return intent;
1367
1753
  const replacement = { ...intent!, leaderEpoch: epoch! };
1368
- return await input.store.replaceWorkspaceRetirementIntent(intent!, replacement, isCurrent)
1369
- ? replacement : undefined;
1754
+ return (await input.store.replaceWorkspaceRetirementIntent(
1755
+ intent!,
1756
+ replacement,
1757
+ isCurrent,
1758
+ ))
1759
+ ? replacement
1760
+ : undefined;
1370
1761
  });
1371
- if (!resumed) return { kind: "blocked", stage: "adoption", reason: "commit-rejected" };
1762
+ if (!resumed)
1763
+ return {
1764
+ kind: "blocked",
1765
+ stage: "adoption",
1766
+ reason: "commit-rejected",
1767
+ };
1372
1768
  intent = resumed;
1373
1769
  } else {
1374
1770
  const adoption = await adoptTelegramWorkspaceRetirementIntent({
@@ -1386,19 +1782,21 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
1386
1782
  intent = adoption.intent;
1387
1783
  }
1388
1784
  } else {
1389
- const preparation = await prepareTelegramWorkspaceRetirement(
1390
- {
1391
- store: input.store,
1392
- getExternalProtection: input.getExternalProtection,
1393
- getLeaderEpoch: input.getLeaderEpoch,
1394
- getProfileKey: input.getProfileKey,
1395
- isCurrent: input.isCurrent,
1396
- getNowMs: input.getNowMs,
1397
- },
1398
- );
1785
+ const preparation = await prepareTelegramWorkspaceRetirement({
1786
+ store: input.store,
1787
+ getExternalProtection: input.getExternalProtection,
1788
+ getLeaderEpoch: input.getLeaderEpoch,
1789
+ getProfileKey: input.getProfileKey,
1790
+ isCurrent: input.isCurrent,
1791
+ getNowMs: input.getNowMs,
1792
+ });
1399
1793
  if (preparation.kind === "not-needed") return preparation;
1400
1794
  if (preparation.kind === "blocked") {
1401
- return { kind: "blocked", stage: "preparation", reason: preparation.reason };
1795
+ return {
1796
+ kind: "blocked",
1797
+ stage: "preparation",
1798
+ reason: preparation.reason,
1799
+ };
1402
1800
  }
1403
1801
  intent = preparation.intent;
1404
1802
  }
@@ -1416,13 +1814,17 @@ export async function runTelegramWorkspaceRetirementLifecycle(input: {
1416
1814
  });
1417
1815
  }
1418
1816
 
1419
- export type TelegramWorkspaceCapacityRunner = <T>(operation: () => Promise<T>) => Promise<T>;
1817
+ export type TelegramWorkspaceCapacityRunner = <T>(
1818
+ operation: () => Promise<T>,
1819
+ ) => Promise<T>;
1420
1820
 
1421
1821
  export interface TelegramWorkspaceSlotRotationPorts extends TelegramWorkspaceOperationGate {
1422
1822
  getAdmission: () => TelegramWorkspaceAdmissionLedger | undefined;
1423
1823
  deleteThread: TelegramWorkspaceThreadDeletionTransport;
1424
- pruneJournalEvidence?: (binding: TelegramWorkspaceThreadBinding,
1425
- isCurrent: () => boolean) => Promise<TelegramWorkspaceJournalPruneResult>;
1824
+ pruneJournalEvidence?: (
1825
+ binding: TelegramWorkspaceThreadBinding,
1826
+ isCurrent: () => boolean,
1827
+ ) => Promise<TelegramWorkspaceJournalPruneResult>;
1426
1828
  reclaimDeadOwnerQueuedWork?: (
1427
1829
  binding: TelegramWorkspaceThreadBinding,
1428
1830
  isCurrent: () => boolean,
@@ -1430,122 +1832,222 @@ export interface TelegramWorkspaceSlotRotationPorts extends TelegramWorkspaceOpe
1430
1832
  }
1431
1833
 
1432
1834
  /** Retry allocation once, only after the failed operation released all ordinary leases. */
1433
- export function createTelegramWorkspaceSlotRotation(input: TelegramWorkspaceSlotRotationPorts & {
1434
- store: TelegramTopicTargetStore;
1435
- getLeaderEpoch: () => number | string | undefined;
1436
- getExternalProtection: (binding: TelegramWorkspaceThreadBinding) => TelegramWorkspaceExternalProtectionEvidence;
1437
- recordEvent: (message: string, details: Record<string, unknown>) => void;
1438
- }): TelegramWorkspaceCapacityRunner {
1835
+ export function createTelegramWorkspaceSlotRotation(
1836
+ input: TelegramWorkspaceSlotRotationPorts & {
1837
+ store: TelegramTopicTargetStore;
1838
+ getLeaderEpoch: () => number | string | undefined;
1839
+ getExternalProtection: (
1840
+ binding: TelegramWorkspaceThreadBinding,
1841
+ ) => TelegramWorkspaceExternalProtectionEvidence;
1842
+ recordEvent: (message: string, details: Record<string, unknown>) => void;
1843
+ },
1844
+ ): TelegramWorkspaceCapacityRunner {
1439
1845
  const requests = createTelegramWorkspaceOperationGate();
1440
1846
  return async (operation) => {
1441
1847
  const admission = input.getAdmission();
1442
1848
  const epoch = input.getLeaderEpoch();
1443
1849
  if (!admission || epoch === undefined) return operation();
1444
1850
  const profileKey = admission.getProfileKey();
1445
- const isCurrent = () => input.getLeaderEpoch() === epoch &&
1851
+ const isCurrent = () =>
1852
+ input.getLeaderEpoch() === epoch &&
1446
1853
  input.getAdmission()?.getProfileKey() === profileKey;
1447
1854
  return requests.runExclusive(async () => {
1448
- if (!isCurrent()) throw new Error("Telegram Workspace allocation lost leader authority.");
1855
+ if (!isCurrent())
1856
+ throw new Error("Telegram Workspace allocation lost leader authority.");
1449
1857
  const rotate = async () => {
1450
1858
  const result = await input.runExclusive(async () => {
1451
- if (!isCurrent()) throw new Error("Telegram Workspace rotation lost leader authority.");
1859
+ if (!isCurrent())
1860
+ throw new Error(
1861
+ "Telegram Workspace rotation lost leader authority.",
1862
+ );
1452
1863
  await input.store.load();
1453
- if (!isCurrent()) throw new Error("Telegram Workspace rotation lost leader authority.");
1864
+ if (!isCurrent())
1865
+ throw new Error(
1866
+ "Telegram Workspace rotation lost leader authority.",
1867
+ );
1454
1868
  return runTelegramWorkspaceRetirementLifecycle({
1455
- store: input.store, admission, getExternalProtection: input.getExternalProtection,
1456
- getLeaderEpoch: input.getLeaderEpoch, getProfileKey: () => profileKey, isCurrent,
1869
+ store: input.store,
1870
+ admission,
1871
+ getExternalProtection: input.getExternalProtection,
1872
+ getLeaderEpoch: input.getLeaderEpoch,
1873
+ getProfileKey: () => profileKey,
1874
+ isCurrent,
1457
1875
  // This entire lifecycle already owns the shared gate, without an admission lease.
1458
1876
  runExclusive: async (action) => action(),
1459
1877
  deleteForumTopic(permit, body) {
1460
1878
  return input.deleteThread(() => {
1461
1879
  const fence = admission.read().fence;
1462
- if (!isCurrent() || !fence || !isTelegramWorkspaceRetirementFence(fence) ||
1463
- (fence.destructiveKind ?? "pressure-retirement") !== "pressure-retirement" ||
1464
- (permit.destructiveKind ?? "pressure-retirement") !== "pressure-retirement" ||
1465
- fence.phase !== "deletion-issued" ||
1466
- !isDeepStrictEqual(fence.owner, admission.getOwner()) ||
1467
- fence.profileKey !== permit.profileKey || fence.leaderEpoch !== permit.leaderEpoch ||
1468
- fence.operationId !== permit.operationId || fence.retirementIntentId !== permit.retirementIntentId ||
1469
- fence.bindingKey !== permit.bindingKey || fence.slot !== permit.slot ||
1470
- fence.deletionIssuedAtMs !== permit.issuedAtMs ||
1471
- !sameTarget(fence.target, permit.target) ||
1472
- body.chat_id !== permit.target.chatId || body.message_thread_id !== permit.target.threadId) {
1473
- throw new Error("Telegram Workspace deletion permit is stale.");
1880
+ if (
1881
+ !isCurrent() ||
1882
+ !fence ||
1883
+ !isTelegramWorkspaceRetirementFence(fence) ||
1884
+ (fence.destructiveKind ?? "pressure-retirement") !==
1885
+ "pressure-retirement" ||
1886
+ (permit.destructiveKind ?? "pressure-retirement") !==
1887
+ "pressure-retirement" ||
1888
+ fence.phase !== "deletion-issued" ||
1889
+ !isDeepStrictEqual(fence.owner, admission.getOwner()) ||
1890
+ fence.profileKey !== permit.profileKey ||
1891
+ fence.leaderEpoch !== permit.leaderEpoch ||
1892
+ fence.operationId !== permit.operationId ||
1893
+ fence.retirementIntentId !== permit.retirementIntentId ||
1894
+ fence.bindingKey !== permit.bindingKey ||
1895
+ fence.slot !== permit.slot ||
1896
+ fence.deletionIssuedAtMs !== permit.issuedAtMs ||
1897
+ !sameTarget(fence.target, permit.target) ||
1898
+ body.chat_id !== permit.target.chatId ||
1899
+ body.message_thread_id !== permit.target.threadId
1900
+ ) {
1901
+ throw new Error(
1902
+ "Telegram Workspace deletion permit is stale.",
1903
+ );
1474
1904
  }
1475
1905
  return { ...permit.target };
1476
1906
  });
1477
1907
  },
1478
1908
  });
1479
1909
  });
1480
- if (result.kind !== "retired" && result.kind !== "not-needed" && result.kind !== "cancelled") {
1481
- throw new Error(`Telegram Workspace slots A-Z are unavailable; rotation blocked (${result.reason}).`);
1910
+ if (
1911
+ result.kind !== "retired" &&
1912
+ result.kind !== "not-needed" &&
1913
+ result.kind !== "cancelled"
1914
+ ) {
1915
+ throw new Error(
1916
+ `Telegram Workspace slots A-Z are unavailable; rotation blocked (${result.reason}).`,
1917
+ );
1482
1918
  }
1483
1919
  if (result.kind === "retired") {
1484
- try { input.recordEvent("Telegram Workspace slot rotated.", { slot: result.slot }); }
1485
- catch { /* Diagnostics cannot turn completed retirement into another attempt. */ }
1920
+ try {
1921
+ input.recordEvent("Telegram Workspace slot rotated.", {
1922
+ slot: result.slot,
1923
+ });
1924
+ } catch {
1925
+ /* Diagnostics cannot turn completed retirement into another attempt. */
1926
+ }
1486
1927
  }
1487
- if (!isCurrent()) throw new Error("Telegram Workspace rotation lost leader authority.");
1928
+ if (!isCurrent())
1929
+ throw new Error("Telegram Workspace rotation lost leader authority.");
1488
1930
  return result;
1489
1931
  };
1490
1932
  const pending = await input.runExclusive(async () => {
1491
1933
  await input.store.load();
1492
- if (!isCurrent()) throw new Error("Telegram Workspace allocation lost leader authority.");
1934
+ if (!isCurrent())
1935
+ throw new Error(
1936
+ "Telegram Workspace allocation lost leader authority.",
1937
+ );
1493
1938
  return input.store.listWorkspaceRetirementIntents().length > 0;
1494
1939
  });
1495
1940
  const fence = admission.read().fence;
1496
- if (pending || (fence && isTelegramWorkspaceRetirementFence(fence) &&
1497
- (fence.destructiveKind ?? "pressure-retirement") === "pressure-retirement")) {
1941
+ if (
1942
+ pending ||
1943
+ (fence &&
1944
+ isTelegramWorkspaceRetirementFence(fence) &&
1945
+ (fence.destructiveKind ?? "pressure-retirement") ===
1946
+ "pressure-retirement")
1947
+ ) {
1498
1948
  const recovered = await rotate();
1499
1949
  if (recovered.kind !== "cancelled") return operation();
1500
1950
  }
1501
- try { return await operation(); }
1502
- catch (error) {
1503
- if (!(error instanceof TelegramWorkspaceSlotUnavailableError)) throw error;
1951
+ try {
1952
+ return await operation();
1953
+ } catch (error) {
1954
+ if (!(error instanceof TelegramWorkspaceSlotUnavailableError))
1955
+ throw error;
1504
1956
  if (input.pruneJournalEvidence || input.reclaimDeadOwnerQueuedWork) {
1505
1957
  const candidates = await input.runExclusive(async () => {
1506
1958
  await input.store.load();
1507
- if (!isCurrent()) throw new Error("Telegram Workspace reclamation lost leader authority.");
1508
- const snapshot = input.store.captureWorkspaceSlotOccupancy(input.getExternalProtection);
1509
- const allocation = planTelegramWorkspaceSlotAllocation({ ...snapshot, nowMs: Date.now() });
1510
- if (allocation.kind === "free" || (allocation.kind === "blocked" && allocation.reason !== "protected-capacity")) return [];
1511
- if (allocation.kind === "reclaim" && !input.pruneJournalEvidence) return [];
1512
- return input.store.listWorkspaceBindings().filter((binding) =>
1513
- (allocation.kind !== "reclaim" || binding.bindingKey === allocation.candidate.bindingKey) &&
1514
- typeof binding.inactiveSinceMs === "number" &&
1515
- Number.isFinite(binding.inactiveSinceMs) &&
1516
- binding.inactiveSinceMs >= 0,
1517
- ).sort((left, right) =>
1518
- left.inactiveSinceMs! - right.inactiveSinceMs! ||
1519
- (left.slot ?? "").localeCompare(right.slot ?? ""),
1959
+ if (!isCurrent())
1960
+ throw new Error(
1961
+ "Telegram Workspace reclamation lost leader authority.",
1962
+ );
1963
+ const snapshot = input.store.captureWorkspaceSlotOccupancy(
1964
+ input.getExternalProtection,
1520
1965
  );
1966
+ const allocation = planTelegramWorkspaceSlotAllocation({
1967
+ ...snapshot,
1968
+ nowMs: Date.now(),
1969
+ });
1970
+ if (
1971
+ allocation.kind === "free" ||
1972
+ (allocation.kind === "blocked" &&
1973
+ allocation.reason !== "protected-capacity")
1974
+ )
1975
+ return [];
1976
+ if (allocation.kind === "reclaim" && !input.pruneJournalEvidence)
1977
+ return [];
1978
+ return input.store
1979
+ .listWorkspaceBindings()
1980
+ .filter(
1981
+ (binding) =>
1982
+ (allocation.kind !== "reclaim" ||
1983
+ binding.bindingKey === allocation.candidate.bindingKey) &&
1984
+ typeof binding.inactiveSinceMs === "number" &&
1985
+ Number.isFinite(binding.inactiveSinceMs) &&
1986
+ binding.inactiveSinceMs >= 0,
1987
+ )
1988
+ .sort(
1989
+ (left, right) =>
1990
+ left.inactiveSinceMs! - right.inactiveSinceMs! ||
1991
+ (left.slot ?? "").localeCompare(right.slot ?? ""),
1992
+ );
1521
1993
  });
1522
1994
  for (const original of candidates) {
1523
1995
  let binding = original;
1524
- if (input.pruneJournalEvidence && ((binding.journalBindingKeys?.length ?? 0) + (binding.journalSources?.length ?? 0) > 0)) {
1525
- const pruned = await input.pruneJournalEvidence(binding, isCurrent);
1526
- if (!isCurrent()) throw new Error("Telegram Workspace pruning lost leader authority.");
1996
+ if (
1997
+ input.pruneJournalEvidence &&
1998
+ (binding.journalBindingKeys?.length ?? 0) +
1999
+ (binding.journalSources?.length ?? 0) >
2000
+ 0
2001
+ ) {
2002
+ const pruned = await input.pruneJournalEvidence(
2003
+ binding,
2004
+ isCurrent,
2005
+ );
2006
+ if (!isCurrent())
2007
+ throw new Error(
2008
+ "Telegram Workspace pruning lost leader authority.",
2009
+ );
1527
2010
  if (pruned.kind !== "committed") continue;
1528
2011
  binding = pruned.binding;
1529
- const removed = pruned.removedKeys.length + (pruned.removedSources?.length ?? 0);
2012
+ const removed =
2013
+ pruned.removedKeys.length +
2014
+ (pruned.removedSources?.length ?? 0);
1530
2015
  if (removed > 0) {
1531
- try { input.recordEvent("Telegram Workspace journal evidence pruned.", { slot: binding.slot, removed }); }
1532
- catch { /* Diagnostics cannot revoke a published metadata subset. */ }
2016
+ try {
2017
+ input.recordEvent(
2018
+ "Telegram Workspace journal evidence pruned.",
2019
+ { slot: binding.slot, removed },
2020
+ );
2021
+ } catch {
2022
+ /* Diagnostics cannot revoke a published metadata subset. */
2023
+ }
1533
2024
  }
1534
2025
  }
1535
2026
  if (!input.reclaimDeadOwnerQueuedWork) continue;
1536
2027
  const evidence = input.getExternalProtection(binding);
1537
- if (evidence.liveOwner !== "clear" ||
1538
- evidence.acceptedWork !== "protected" ||
1539
- evidence.deliveryAuthority !== "clear") continue;
1540
- const reclaimed = await input.reclaimDeadOwnerQueuedWork(binding, isCurrent);
2028
+ if (
2029
+ evidence.liveOwner !== "clear" ||
2030
+ evidence.acceptedWork !== "protected" ||
2031
+ evidence.deliveryAuthority !== "clear"
2032
+ )
2033
+ continue;
2034
+ const reclaimed = await input.reclaimDeadOwnerQueuedWork(
2035
+ binding,
2036
+ isCurrent,
2037
+ );
1541
2038
  if (reclaimed.kind === "recovered") {
1542
2039
  try {
1543
- input.recordEvent("Telegram Workspace dead-owner queue reclaimed.", {
1544
- slot: binding.slot,
1545
- receipts: reclaimed.receipts,
1546
- updateCount: reclaimed.updateIds.length,
1547
- });
1548
- } catch { /* Diagnostics cannot revoke journal-owned recovery. */ }
2040
+ input.recordEvent(
2041
+ "Telegram Workspace dead-owner queue reclaimed.",
2042
+ {
2043
+ slot: binding.slot,
2044
+ receipts: reclaimed.receipts,
2045
+ updateCount: reclaimed.updateIds.length,
2046
+ },
2047
+ );
2048
+ } catch {
2049
+ /* Diagnostics cannot revoke journal-owned recovery. */
2050
+ }
1549
2051
  break;
1550
2052
  }
1551
2053
  }