@llblab/pi-kit 0.5.2 → 0.7.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 (141) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +5 -5
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -1
  4. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  5. package/node_modules/@llblab/pi-actors/README.md +1 -1
  6. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +3 -0
  7. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +14 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/command-templates.js +45 -3
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +1 -1
  10. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +16 -3
  11. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +92 -7
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +0 -1
  13. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +15 -24
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.d.ts +17 -0
  15. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery-lineage.js +44 -0
  16. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +4 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +102 -4
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +58 -39
  19. package/node_modules/@llblab/pi-actors/dist/lib/runtime.js +14 -6
  20. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  21. package/node_modules/@llblab/pi-actors/docs/async-runs.md +1 -1
  22. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +18 -23
  23. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +18 -1
  24. package/node_modules/@llblab/pi-actors/lib/command-templates.ts +41 -3
  25. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +1 -1
  26. package/node_modules/@llblab/pi-actors/lib/observability.ts +119 -5
  27. package/node_modules/@llblab/pi-actors/lib/pi.ts +15 -28
  28. package/node_modules/@llblab/pi-actors/lib/run-delivery-lineage.ts +68 -0
  29. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +120 -4
  30. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +69 -44
  31. package/node_modules/@llblab/pi-actors/lib/runtime.ts +17 -6
  32. package/node_modules/@llblab/pi-actors/package.json +1 -1
  33. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  34. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +2 -2
  35. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  36. package/node_modules/@llblab/pi-grow-loop/README.md +6 -6
  37. package/node_modules/@llblab/pi-grow-loop/index.ts +6 -3
  38. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  39. package/node_modules/@llblab/pi-state-flow/AGENTS.md +33 -12
  40. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +125 -2
  41. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +87 -45
  42. package/node_modules/@llblab/pi-state-flow/README.md +203 -107
  43. package/node_modules/@llblab/pi-state-flow/docs/README.md +4 -0
  44. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +188 -0
  45. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +42 -0
  46. package/node_modules/@llblab/pi-state-flow/index.ts +164 -2
  47. package/node_modules/@llblab/pi-state-flow/lib/acquisition.ts +138 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +273 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/config.ts +48 -0
  50. package/node_modules/@llblab/pi-state-flow/lib/context.ts +18 -5
  51. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +268 -0
  52. package/node_modules/@llblab/pi-state-flow/lib/discovery.ts +117 -0
  53. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +562 -0
  54. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +24 -12
  55. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +606 -70
  56. package/node_modules/@llblab/pi-state-flow/lib/git.ts +666 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/history.ts +95 -0
  58. package/node_modules/@llblab/pi-state-flow/lib/json.ts +24 -0
  59. package/node_modules/@llblab/pi-state-flow/lib/maintenance.ts +141 -0
  60. package/node_modules/@llblab/pi-state-flow/lib/memory.ts +52 -0
  61. package/node_modules/@llblab/pi-state-flow/lib/migration.ts +88 -0
  62. package/node_modules/@llblab/pi-state-flow/lib/publication.ts +296 -0
  63. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +23 -7
  64. package/node_modules/@llblab/pi-state-flow/lib/rehydration.ts +79 -0
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +264 -0
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +6 -0
  67. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +99 -7
  68. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +305 -48
  69. package/node_modules/@llblab/pi-state-flow/lib/state.ts +74 -7
  70. package/node_modules/@llblab/pi-state-flow/lib/status.ts +125 -6
  71. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +196 -0
  72. package/node_modules/@llblab/pi-state-flow/lib/temporal.ts +233 -0
  73. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +70 -24
  74. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +254 -29
  75. package/node_modules/@llblab/pi-state-flow/package.json +8 -2
  76. package/node_modules/@llblab/pi-state-flow/skills/state-flow-memory/SKILL.md +128 -0
  77. package/node_modules/@llblab/pi-telegram/AGENTS.md +14 -9
  78. package/node_modules/@llblab/pi-telegram/BACKLOG.md +20 -4
  79. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +22 -5
  80. package/node_modules/@llblab/pi-telegram/README.md +13 -9
  81. package/node_modules/@llblab/pi-telegram/docs/README.md +1 -0
  82. package/node_modules/@llblab/pi-telegram/docs/architecture.md +220 -18
  83. package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +1 -1
  84. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +70 -19
  85. package/node_modules/@llblab/pi-telegram/docs/outbound.md +13 -7
  86. package/node_modules/@llblab/pi-telegram/docs/public-api.md +13 -5
  87. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +3 -1
  88. package/node_modules/@llblab/pi-telegram/index.ts +4 -1415
  89. package/node_modules/@llblab/pi-telegram/lib/activity.ts +19 -5
  90. package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +6 -3
  91. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +37 -2
  92. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +600 -135
  93. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +962 -55
  94. package/node_modules/@llblab/pi-telegram/lib/bus.ts +350 -26
  95. package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +544 -0
  96. package/node_modules/@llblab/pi-telegram/lib/commands.ts +234 -11
  97. package/node_modules/@llblab/pi-telegram/lib/config.ts +178 -25
  98. package/node_modules/@llblab/pi-telegram/lib/delivery.ts +18 -18
  99. package/node_modules/@llblab/pi-telegram/lib/extension.ts +1792 -0
  100. package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +20 -2
  101. package/node_modules/@llblab/pi-telegram/lib/journal.ts +2184 -126
  102. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +7 -1
  103. package/node_modules/@llblab/pi-telegram/lib/locks.ts +38 -1
  104. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +154 -15
  105. package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +74 -40
  106. package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +28 -42
  107. package/node_modules/@llblab/pi-telegram/lib/outbound.ts +18 -14
  108. package/node_modules/@llblab/pi-telegram/lib/paths.ts +29 -0
  109. package/node_modules/@llblab/pi-telegram/lib/polling.ts +85 -17
  110. package/node_modules/@llblab/pi-telegram/lib/preview.ts +115 -70
  111. package/node_modules/@llblab/pi-telegram/lib/prompts.ts +5 -2
  112. package/node_modules/@llblab/pi-telegram/lib/queue.ts +66 -22
  113. package/node_modules/@llblab/pi-telegram/lib/replies.ts +47 -39
  114. package/node_modules/@llblab/pi-telegram/lib/routing.ts +305 -112
  115. package/node_modules/@llblab/pi-telegram/lib/status.ts +10 -0
  116. package/node_modules/@llblab/pi-telegram/lib/sync.ts +308 -39
  117. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +315 -7
  118. package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +664 -0
  119. package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +226 -0
  120. package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +118 -0
  121. package/node_modules/@llblab/pi-telegram/lib/threads.ts +1686 -129
  122. package/node_modules/@llblab/pi-telegram/lib/updates.ts +1319 -97
  123. package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +1643 -0
  124. package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +968 -0
  125. package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +84 -0
  126. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  127. package/node_modules/@llblab/pi-telegram/screenshot.png +0 -0
  128. package/node_modules/@llblab/pi-telegram/scripts/measure-bus.mjs +83 -0
  129. package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +101 -0
  130. package/node_modules/@llblab/{skills → pi-telegram/skills}/show-me/SKILL.md +28 -6
  131. package/node_modules/@llblab/pi-telegram/skills/show-me/references/telegram-surfaces.md +43 -0
  132. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +1 -1
  133. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +1 -0
  134. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +6 -2
  135. package/node_modules/@llblab/skills/abcd-context/SKILL.md +1 -1
  136. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +11 -5
  137. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +61 -0
  138. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +67 -0
  139. package/node_modules/@llblab/skills/package.json +2 -3
  140. package/node_modules/@llblab/skills/release-flow/SKILL.md +2 -4
  141. package/package.json +7 -6
@@ -0,0 +1,1643 @@
1
+ /**
2
+ * Durable Telegram Workspace admission and retirement fencing
3
+ * Zones: telegram workspace identity, filesystem authority, process recovery
4
+ * Owns profile-scoped admission leases, destructive fences, and deletion permits
5
+ * Excludes journal/API operations, retirement policy, and binding persistence
6
+ */
7
+
8
+ import { createHash, randomUUID } from "node:crypto";
9
+ import {
10
+ chmodSync,
11
+ mkdirSync,
12
+ readFileSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ type PathLike,
16
+ } from "node:fs";
17
+ import { dirname } from "node:path";
18
+
19
+ import {
20
+ getTelegramProcessLiveness,
21
+ type TelegramProcessLiveness,
22
+ } from "./bus.ts";
23
+ import {
24
+ renameTelegramPathWithRetry,
25
+ withTelegramFileTransaction,
26
+ } from "./locks.ts";
27
+ import type { TelegramTarget } from "./target.ts";
28
+
29
+ const TELEGRAM_WORKSPACE_ADMISSION_VERSION = 1;
30
+ const TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES = 4096;
31
+ const TELEGRAM_WORKSPACE_ADMISSION_MAX_TEXT = 512;
32
+ const activeTelegramWorkspaceAdmissionOperationIds = new Set<string>();
33
+
34
+ export interface TelegramWorkspaceAdmissionOwner {
35
+ processId: number;
36
+ processBirthId: string;
37
+ }
38
+
39
+ export type TelegramWorkspaceAdmissionScope =
40
+ | { kind: "target"; target: TelegramTarget & { threadId: number } }
41
+ | { kind: "chat"; chatId: number }
42
+ | { kind: "profile" };
43
+
44
+ export interface TelegramWorkspaceAdmissionLease {
45
+ operationId: string;
46
+ operationKind: string;
47
+ profileKey: string;
48
+ scope: TelegramWorkspaceAdmissionScope;
49
+ owner: TelegramWorkspaceAdmissionOwner;
50
+ acquiredAtMs: number;
51
+ }
52
+
53
+ export type TelegramWorkspaceDestructiveFenceKind =
54
+ | "pressure-retirement"
55
+ | "manual-thread-cleanup"
56
+ | "journal-writer-closure";
57
+
58
+ export type TelegramWorkspaceDeletionFenceKind = Exclude<
59
+ TelegramWorkspaceDestructiveFenceKind, "journal-writer-closure">;
60
+
61
+ export interface TelegramWorkspaceJournalWriterClosureFence {
62
+ destructiveKind: "journal-writer-closure";
63
+ phase: "fenced";
64
+ operationId: string;
65
+ profileKey: string;
66
+ recoveryKey: string;
67
+ owner: TelegramWorkspaceAdmissionOwner;
68
+ requestedAtMs: number;
69
+ acquiredAtMs: number;
70
+ }
71
+
72
+ export function normalizeTelegramWorkspaceJournalWriterClosureFence(
73
+ value: unknown,
74
+ profileKey: string,
75
+ ): TelegramWorkspaceJournalWriterClosureFence | undefined {
76
+ if (!isRecord(value) || value.destructiveKind !== "journal-writer-closure" ||
77
+ value.phase !== "fenced" || !isBoundedText(value.operationId) ||
78
+ value.profileKey !== profileKey || !isBoundedText(value.recoveryKey) ||
79
+ !isSafeTimestamp(value.requestedAtMs) || !isSafeTimestamp(value.acquiredAtMs)) return undefined;
80
+ const owner = normalizeOwner(value.owner);
81
+ if (!owner || Object.keys(value).some(key => ![
82
+ "destructiveKind", "phase", "operationId", "profileKey", "recoveryKey",
83
+ "owner", "requestedAtMs", "acquiredAtMs",
84
+ ].includes(key))) return undefined;
85
+ return { destructiveKind: "journal-writer-closure", phase: "fenced",
86
+ operationId: value.operationId, profileKey, recoveryKey: value.recoveryKey,
87
+ owner, requestedAtMs: value.requestedAtMs, acquiredAtMs: value.acquiredAtMs };
88
+ }
89
+
90
+ export interface TelegramWorkspaceJournalWriterProtocolMode {
91
+ version: 1;
92
+ protocol: "custody-v3";
93
+ profileKey: string;
94
+ recoveryKey: string;
95
+ startupAuthorityId: string;
96
+ closureOperationId: string;
97
+ writerInventorySha256: string;
98
+ installedBy: TelegramWorkspaceAdmissionOwner;
99
+ installedAtMs: number;
100
+ }
101
+
102
+ export function normalizeTelegramWorkspaceJournalWriterProtocolMode(
103
+ value: unknown,
104
+ profileKey: string,
105
+ ): TelegramWorkspaceJournalWriterProtocolMode | undefined {
106
+ if (!isRecord(value)) return undefined;
107
+ const keys = ["version", "protocol", "profileKey", "recoveryKey", "startupAuthorityId",
108
+ "closureOperationId", "writerInventorySha256", "installedBy", "installedAtMs"];
109
+ const installedBy = normalizeOwner(value.installedBy);
110
+ if (Object.keys(value).some(key => !keys.includes(key)) || value.version !== 1 ||
111
+ value.protocol !== "custody-v3" || value.profileKey !== profileKey ||
112
+ !isBoundedText(value.recoveryKey) || !isBoundedText(value.startupAuthorityId) ||
113
+ !isBoundedText(value.closureOperationId) ||
114
+ typeof value.writerInventorySha256 !== "string" ||
115
+ !/^[a-f0-9]{64}$/u.test(value.writerInventorySha256) || !installedBy ||
116
+ !isSafeTimestamp(value.installedAtMs)) return undefined;
117
+ return { version: 1, protocol: "custody-v3", profileKey,
118
+ recoveryKey: value.recoveryKey, startupAuthorityId: value.startupAuthorityId,
119
+ closureOperationId: value.closureOperationId,
120
+ writerInventorySha256: value.writerInventorySha256,
121
+ installedBy, installedAtMs: value.installedAtMs };
122
+ }
123
+
124
+ interface TelegramWorkspaceRetirementFenceBase {
125
+ destructiveKind?: TelegramWorkspaceDeletionFenceKind;
126
+ operationId: string;
127
+ retirementIntentId: string;
128
+ profileKey: string;
129
+ bindingKey: string;
130
+ slot: string;
131
+ target: TelegramTarget & { threadId: number };
132
+ leaderEpoch: number | string;
133
+ retirementRequestedAtMs: number;
134
+ owner: TelegramWorkspaceAdmissionOwner;
135
+ acquiredAtMs: number;
136
+ }
137
+
138
+ export type TelegramWorkspaceRetirementFence =
139
+ | (TelegramWorkspaceRetirementFenceBase & { phase: "fenced" })
140
+ | (TelegramWorkspaceRetirementFenceBase & {
141
+ phase: "deletion-issued";
142
+ deletionIssuedAtMs: number;
143
+ })
144
+ | (TelegramWorkspaceRetirementFenceBase & {
145
+ phase: "commit-ready";
146
+ absenceConfirmedAtMs: number;
147
+ deletionIssuedAtMs?: number;
148
+ });
149
+
150
+ export type TelegramWorkspaceDestructiveFence =
151
+ | TelegramWorkspaceRetirementFence
152
+ | TelegramWorkspaceJournalWriterClosureFence;
153
+
154
+ export function isTelegramWorkspaceRetirementFence(
155
+ fence: TelegramWorkspaceDestructiveFence,
156
+ ): fence is TelegramWorkspaceRetirementFence {
157
+ return fence.destructiveKind !== "journal-writer-closure";
158
+ }
159
+
160
+ export interface TelegramWorkspaceDeletionPermit {
161
+ readonly destructiveKind?: TelegramWorkspaceDeletionFenceKind;
162
+ readonly operationId: string;
163
+ readonly retirementIntentId: string;
164
+ readonly profileKey: string;
165
+ readonly bindingKey: string;
166
+ readonly slot: string;
167
+ readonly target: TelegramTarget & { threadId: number };
168
+ readonly leaderEpoch: number | string;
169
+ readonly issuedAtMs: number;
170
+ }
171
+
172
+ interface TelegramWorkspaceAdmissionState {
173
+ version: 1;
174
+ profileKey: string;
175
+ leases: TelegramWorkspaceAdmissionLease[];
176
+ fence?: TelegramWorkspaceDestructiveFence;
177
+ writerProtocolMode?: TelegramWorkspaceJournalWriterProtocolMode;
178
+ }
179
+
180
+ export type TelegramWorkspaceAdmissionBlockReason =
181
+ | "retirement-fenced"
182
+ | "admission-active"
183
+ | "retirement-active";
184
+
185
+ export type TelegramWorkspaceAdmissionAcquireResult =
186
+ | { kind: "acquired"; lease: TelegramWorkspaceAdmissionLease; resumed: boolean }
187
+ | { kind: "blocked"; reason: "retirement-fenced" };
188
+
189
+ export type TelegramWorkspaceRetirementFenceAcquireResult =
190
+ | {
191
+ kind: "acquired";
192
+ fence: TelegramWorkspaceRetirementFence;
193
+ resumed: boolean;
194
+ }
195
+ | {
196
+ kind: "blocked";
197
+ reason: "admission-active" | "retirement-active";
198
+ };
199
+
200
+ export type TelegramWorkspaceJournalWriterProtocolInstallResult = {
201
+ mode: TelegramWorkspaceJournalWriterProtocolMode;
202
+ resumed: boolean;
203
+ };
204
+
205
+ export type TelegramWorkspaceJournalWriterClosureAcquireResult =
206
+ | { kind: "acquired"; fence: TelegramWorkspaceJournalWriterClosureFence; resumed: boolean }
207
+ | { kind: "blocked"; reason: "admission-active" | "retirement-active" };
208
+
209
+ export type TelegramWorkspaceDeletionPermitResult =
210
+ | {
211
+ kind: "issued";
212
+ fence: TelegramWorkspaceRetirementFence;
213
+ permit: TelegramWorkspaceDeletionPermit;
214
+ }
215
+ | { kind: "already-issued"; fence: TelegramWorkspaceRetirementFence };
216
+
217
+ export interface TelegramWorkspaceAdmissionLedgerSnapshot {
218
+ profileKey: string;
219
+ leases: TelegramWorkspaceAdmissionLease[];
220
+ fence?: TelegramWorkspaceDestructiveFence;
221
+ writerProtocolMode?: TelegramWorkspaceJournalWriterProtocolMode;
222
+ }
223
+
224
+ export type TelegramWorkspaceAdmissionPublicationBoundary =
225
+ | "before-write"
226
+ | "after-write-before-rename";
227
+
228
+ export interface TelegramWorkspaceAdmissionLedgerOptions {
229
+ path: string;
230
+ profileKey: string;
231
+ owner: TelegramWorkspaceAdmissionOwner;
232
+ getNowMs?: () => number;
233
+ getProcessLiveness?: (
234
+ owner: TelegramWorkspaceAdmissionOwner,
235
+ ) => TelegramProcessLiveness;
236
+ publishRename?: (sourcePath: PathLike, destinationPath: PathLike) => void;
237
+ onPublicationBoundary?: (
238
+ boundary: TelegramWorkspaceAdmissionPublicationBoundary,
239
+ path: string,
240
+ ) => void;
241
+ authorizeJournalWriterProtocolClosure?: (input: {
242
+ mode: TelegramWorkspaceJournalWriterProtocolMode;
243
+ closure: TelegramWorkspaceJournalWriterClosureFence;
244
+ }) => boolean;
245
+ }
246
+
247
+ export class TelegramWorkspaceAdmissionError extends Error {
248
+ readonly code:
249
+ | "invalid-input"
250
+ | "invalid-state"
251
+ | "state-unavailable"
252
+ | "authority-changed"
253
+ | "admission-blocked"
254
+ | "publication-unknown";
255
+
256
+ constructor(
257
+ code: TelegramWorkspaceAdmissionError["code"],
258
+ message: string,
259
+ options?: ErrorOptions,
260
+ ) {
261
+ super(message, options);
262
+ this.name = "TelegramWorkspaceAdmissionError";
263
+ this.code = code;
264
+ }
265
+ }
266
+
267
+ export interface TelegramWorkspaceAdmissionLedger {
268
+ getProfileKey: () => string;
269
+ getOwner: () => TelegramWorkspaceAdmissionOwner;
270
+ listReservedSlots: () => string[];
271
+ read: () => TelegramWorkspaceAdmissionLedgerSnapshot;
272
+ acquireAdmission: (input: {
273
+ operationId: string;
274
+ operationKind: string;
275
+ scope: TelegramWorkspaceAdmissionScope;
276
+ }) => TelegramWorkspaceAdmissionAcquireResult;
277
+ releaseAdmission: (expected: TelegramWorkspaceAdmissionLease) => boolean;
278
+ acquireJournalWriterClosure: (input: { operationId: string; recoveryKey: string;
279
+ requestedAtMs: number }) => TelegramWorkspaceJournalWriterClosureAcquireResult;
280
+ releaseJournalWriterClosure: (expected: TelegramWorkspaceJournalWriterClosureFence) => boolean;
281
+ installJournalWriterProtocolMode: (expected: TelegramWorkspaceJournalWriterClosureFence,
282
+ input: { startupAuthorityId: string; writerInventorySha256: string }) =>
283
+ TelegramWorkspaceJournalWriterProtocolInstallResult;
284
+ acquireJournalWriterAdmission: (input: { operationId: string; recoveryKey: string;
285
+ startupAuthorityId: string; closureOperationId: string; writerInventorySha256: string }) =>
286
+ TelegramWorkspaceAdmissionAcquireResult;
287
+ acquireRetirementFence: (input: {
288
+ operationId: string;
289
+ retirementIntentId: string;
290
+ bindingKey: string;
291
+ slot: string;
292
+ target: TelegramTarget & { threadId: number };
293
+ leaderEpoch: number | string;
294
+ retirementRequestedAtMs: number;
295
+ }) => TelegramWorkspaceRetirementFenceAcquireResult;
296
+ acquireThreadCleanupFence: (input: {
297
+ operationId: string;
298
+ cleanupWorkSetId: string;
299
+ bindingKey: string;
300
+ slot: string;
301
+ target: TelegramTarget & { threadId: number };
302
+ leaderEpoch: number | string;
303
+ cleanupRequestedAtMs: number;
304
+ }) => TelegramWorkspaceRetirementFenceAcquireResult;
305
+ adoptRetirementFence: (
306
+ expected: TelegramWorkspaceRetirementFence,
307
+ replacement: {
308
+ owner: TelegramWorkspaceAdmissionOwner;
309
+ leaderEpoch: number | string;
310
+ },
311
+ ) => TelegramWorkspaceRetirementFence;
312
+ adoptThreadCleanupFence: TelegramWorkspaceAdmissionLedger["adoptRetirementFence"];
313
+ issueDeletionPermit: (
314
+ expected: TelegramWorkspaceRetirementFence,
315
+ ) => TelegramWorkspaceDeletionPermitResult;
316
+ issueThreadCleanupDeletionPermit: TelegramWorkspaceAdmissionLedger["issueDeletionPermit"];
317
+ confirmRetirementAbsence: (
318
+ expected: TelegramWorkspaceRetirementFence,
319
+ ) => TelegramWorkspaceRetirementFence;
320
+ confirmThreadCleanupAbsence: TelegramWorkspaceAdmissionLedger["confirmRetirementAbsence"];
321
+ releaseUnissuedRetirementFence: (
322
+ expected: TelegramWorkspaceRetirementFence,
323
+ ) => boolean;
324
+ releaseUnissuedThreadCleanupFence: TelegramWorkspaceAdmissionLedger["releaseUnissuedRetirementFence"];
325
+ completeRetirementFence: (
326
+ expected: TelegramWorkspaceRetirementFence,
327
+ ) => boolean;
328
+ completeThreadCleanupFence: TelegramWorkspaceAdmissionLedger["completeRetirementFence"];
329
+ }
330
+
331
+ function invalidInput(message: string): never {
332
+ throw new TelegramWorkspaceAdmissionError("invalid-input", message);
333
+ }
334
+
335
+ function invalidState(message: string, cause?: unknown): never {
336
+ throw new TelegramWorkspaceAdmissionError("invalid-state", message, {
337
+ cause,
338
+ });
339
+ }
340
+
341
+ function authorityChanged(message: string): never {
342
+ throw new TelegramWorkspaceAdmissionError("authority-changed", message);
343
+ }
344
+
345
+ function isRecord(value: unknown): value is Record<string, unknown> {
346
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
347
+ }
348
+
349
+ function isSafeTimestamp(value: unknown): value is number {
350
+ return Number.isSafeInteger(value) && (value as number) >= 0;
351
+ }
352
+
353
+ function isBoundedText(value: unknown): value is string {
354
+ return (
355
+ typeof value === "string" &&
356
+ value.length > 0 &&
357
+ value.length <= TELEGRAM_WORKSPACE_ADMISSION_MAX_TEXT
358
+ );
359
+ }
360
+
361
+ function isLeaderEpoch(value: unknown): value is number | string {
362
+ return (
363
+ (typeof value === "number" && Number.isSafeInteger(value)) ||
364
+ isBoundedText(value)
365
+ );
366
+ }
367
+
368
+ function normalizeTarget(
369
+ value: unknown,
370
+ ): (TelegramTarget & { threadId: number }) | undefined {
371
+ if (!isRecord(value)) return undefined;
372
+ if (
373
+ !Number.isSafeInteger(value.chatId) ||
374
+ value.chatId === 0 ||
375
+ !Number.isSafeInteger(value.threadId) ||
376
+ (value.threadId as number) <= 0
377
+ ) {
378
+ return undefined;
379
+ }
380
+ return { chatId: value.chatId as number, threadId: value.threadId as number };
381
+ }
382
+
383
+ function normalizeOwner(
384
+ value: unknown,
385
+ ): TelegramWorkspaceAdmissionOwner | undefined {
386
+ if (!isRecord(value)) return undefined;
387
+ if (
388
+ !Number.isSafeInteger(value.processId) ||
389
+ (value.processId as number) <= 0 ||
390
+ !isBoundedText(value.processBirthId) ||
391
+ !value.processBirthId.startsWith(`${value.processId}:`)
392
+ ) {
393
+ return undefined;
394
+ }
395
+ return {
396
+ processId: value.processId as number,
397
+ processBirthId: value.processBirthId,
398
+ };
399
+ }
400
+
401
+ function normalizeScope(
402
+ value: unknown,
403
+ ): TelegramWorkspaceAdmissionScope | undefined {
404
+ if (!isRecord(value)) return undefined;
405
+ if (value.kind === "profile") return { kind: "profile" };
406
+ if (value.kind === "chat") {
407
+ if (!Number.isSafeInteger(value.chatId) || value.chatId === 0) return undefined;
408
+ return { kind: "chat", chatId: value.chatId as number };
409
+ }
410
+ if (value.kind === "target") {
411
+ const target = normalizeTarget(value.target);
412
+ return target ? { kind: "target", target } : undefined;
413
+ }
414
+ return undefined;
415
+ }
416
+
417
+ function normalizeLease(
418
+ value: unknown,
419
+ profileKey: string,
420
+ ): TelegramWorkspaceAdmissionLease | undefined {
421
+ if (!isRecord(value)) return undefined;
422
+ const scope = normalizeScope(value.scope);
423
+ const owner = normalizeOwner(value.owner);
424
+ if (
425
+ !isBoundedText(value.operationId) ||
426
+ !isBoundedText(value.operationKind) ||
427
+ value.profileKey !== profileKey ||
428
+ !scope ||
429
+ !owner ||
430
+ !isSafeTimestamp(value.acquiredAtMs)
431
+ ) {
432
+ return undefined;
433
+ }
434
+ return {
435
+ operationId: value.operationId,
436
+ operationKind: value.operationKind,
437
+ profileKey,
438
+ scope,
439
+ owner,
440
+ acquiredAtMs: value.acquiredAtMs,
441
+ };
442
+ }
443
+
444
+ function normalizeFence(
445
+ value: unknown,
446
+ profileKey: string,
447
+ ): TelegramWorkspaceDestructiveFence | undefined {
448
+ if (!isRecord(value)) return undefined;
449
+ if (value.destructiveKind === "journal-writer-closure")
450
+ return normalizeTelegramWorkspaceJournalWriterClosureFence(value, profileKey);
451
+ const target = normalizeTarget(value.target);
452
+ const owner = normalizeOwner(value.owner);
453
+ if (
454
+ !isBoundedText(value.operationId) ||
455
+ !isBoundedText(value.retirementIntentId) ||
456
+ value.profileKey !== profileKey ||
457
+ !isBoundedText(value.bindingKey) ||
458
+ typeof value.slot !== "string" ||
459
+ !/^[A-Z]$/u.test(value.slot) ||
460
+ !target ||
461
+ !isLeaderEpoch(value.leaderEpoch) ||
462
+ !isSafeTimestamp(value.retirementRequestedAtMs) ||
463
+ !owner ||
464
+ !isSafeTimestamp(value.acquiredAtMs)
465
+ ) {
466
+ return undefined;
467
+ }
468
+ if (value.destructiveKind !== undefined && value.destructiveKind !== "pressure-retirement" &&
469
+ value.destructiveKind !== "manual-thread-cleanup") return undefined;
470
+ const base: TelegramWorkspaceRetirementFenceBase = {
471
+ ...(value.destructiveKind === undefined ? {} : { destructiveKind: value.destructiveKind }),
472
+ operationId: value.operationId,
473
+ retirementIntentId: value.retirementIntentId,
474
+ profileKey,
475
+ bindingKey: value.bindingKey,
476
+ slot: value.slot,
477
+ target,
478
+ leaderEpoch: value.leaderEpoch,
479
+ retirementRequestedAtMs: value.retirementRequestedAtMs,
480
+ owner,
481
+ acquiredAtMs: value.acquiredAtMs,
482
+ };
483
+ if (value.phase === "fenced") return { ...base, phase: "fenced" };
484
+ if (
485
+ value.phase === "deletion-issued" &&
486
+ isSafeTimestamp(value.deletionIssuedAtMs)
487
+ ) {
488
+ return {
489
+ ...base,
490
+ phase: "deletion-issued",
491
+ deletionIssuedAtMs: value.deletionIssuedAtMs,
492
+ };
493
+ }
494
+ if (
495
+ value.phase === "commit-ready" &&
496
+ isSafeTimestamp(value.absenceConfirmedAtMs) &&
497
+ (value.deletionIssuedAtMs === undefined ||
498
+ isSafeTimestamp(value.deletionIssuedAtMs))
499
+ ) {
500
+ return {
501
+ ...base,
502
+ phase: "commit-ready",
503
+ absenceConfirmedAtMs: value.absenceConfirmedAtMs,
504
+ ...(value.deletionIssuedAtMs === undefined
505
+ ? {}
506
+ : { deletionIssuedAtMs: value.deletionIssuedAtMs }),
507
+ };
508
+ }
509
+ return undefined;
510
+ }
511
+
512
+ function cloneScope(
513
+ scope: TelegramWorkspaceAdmissionScope,
514
+ ): TelegramWorkspaceAdmissionScope {
515
+ return scope.kind === "target"
516
+ ? { kind: "target", target: { ...scope.target } }
517
+ : { ...scope };
518
+ }
519
+
520
+ function cloneLease(
521
+ lease: TelegramWorkspaceAdmissionLease,
522
+ ): TelegramWorkspaceAdmissionLease {
523
+ return { ...lease, scope: cloneScope(lease.scope), owner: { ...lease.owner } };
524
+ }
525
+
526
+ export function resolveTelegramWorkspaceDestructiveFenceKind(
527
+ fence: { destructiveKind?: TelegramWorkspaceDestructiveFenceKind },
528
+ ): TelegramWorkspaceDestructiveFenceKind {
529
+ return fence.destructiveKind ?? "pressure-retirement";
530
+ }
531
+
532
+ function cloneFence(fence: TelegramWorkspaceRetirementFence): TelegramWorkspaceRetirementFence;
533
+ function cloneFence(fence: TelegramWorkspaceJournalWriterClosureFence): TelegramWorkspaceJournalWriterClosureFence;
534
+ function cloneFence(fence: TelegramWorkspaceDestructiveFence): TelegramWorkspaceDestructiveFence;
535
+ function cloneFence(
536
+ fence: TelegramWorkspaceDestructiveFence,
537
+ ): TelegramWorkspaceDestructiveFence {
538
+ return isTelegramWorkspaceRetirementFence(fence)
539
+ ? { ...fence, target: { ...fence.target }, owner: { ...fence.owner } }
540
+ : { ...fence, owner: { ...fence.owner } };
541
+ }
542
+
543
+ function readState(
544
+ path: string,
545
+ profileKey: string,
546
+ ): TelegramWorkspaceAdmissionState {
547
+ let raw: string;
548
+ try {
549
+ raw = readFileSync(path, "utf8");
550
+ } catch (error) {
551
+ if ((error as { code?: unknown })?.code === "ENOENT") {
552
+ return {
553
+ version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
554
+ profileKey,
555
+ leases: [],
556
+ };
557
+ }
558
+ throw new TelegramWorkspaceAdmissionError(
559
+ "state-unavailable",
560
+ "Telegram Workspace admission state is unavailable.",
561
+ { cause: error },
562
+ );
563
+ }
564
+ let parsed: unknown;
565
+ try {
566
+ parsed = JSON.parse(raw);
567
+ } catch (error) {
568
+ invalidState("Telegram Workspace admission state is malformed.", error);
569
+ }
570
+ if (!isRecord(parsed)) {
571
+ invalidState("Telegram Workspace admission state is invalid.");
572
+ }
573
+ if (
574
+ parsed.version !== TELEGRAM_WORKSPACE_ADMISSION_VERSION ||
575
+ !Array.isArray(parsed.leases) ||
576
+ parsed.leases.length > TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES
577
+ ) {
578
+ invalidState("Telegram Workspace admission state authority is invalid.");
579
+ }
580
+ if (parsed.profileKey !== profileKey) {
581
+ if (parsed.leases.length === 0 && parsed.fence === undefined &&
582
+ parsed.writerProtocolMode === undefined) {
583
+ return {
584
+ version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
585
+ profileKey,
586
+ leases: [],
587
+ };
588
+ }
589
+ invalidState("Telegram Workspace admission profile authority changed.");
590
+ }
591
+ const leases = parsed.leases.map((value) => normalizeLease(value, profileKey));
592
+ if (leases.some((lease) => lease === undefined)) {
593
+ invalidState("Telegram Workspace admission lease is invalid.");
594
+ }
595
+ const normalizedLeases = leases as TelegramWorkspaceAdmissionLease[];
596
+ if (new Set(normalizedLeases.map((lease) => lease.operationId)).size !== normalizedLeases.length) {
597
+ invalidState("Telegram Workspace admission lease identity is duplicated.");
598
+ }
599
+ const fence =
600
+ parsed.fence === undefined ? undefined : normalizeFence(parsed.fence, profileKey);
601
+ if (parsed.fence !== undefined && !fence) {
602
+ invalidState("Telegram Workspace retirement fence is invalid.");
603
+ }
604
+ const writerProtocolMode = parsed.writerProtocolMode === undefined ? undefined :
605
+ normalizeTelegramWorkspaceJournalWriterProtocolMode(parsed.writerProtocolMode, profileKey);
606
+ if (parsed.writerProtocolMode !== undefined && !writerProtocolMode)
607
+ invalidState("Telegram Workspace writer protocol mode is invalid.");
608
+ if (fence && writerProtocolMode)
609
+ invalidState("Telegram Workspace destructive fence conflicts with writer protocol mode.");
610
+ return {
611
+ version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
612
+ profileKey,
613
+ leases: normalizedLeases,
614
+ ...(fence ? { fence } : {}),
615
+ ...(writerProtocolMode ? { writerProtocolMode } : {}),
616
+ };
617
+ }
618
+
619
+ function writeState(
620
+ path: string,
621
+ state: TelegramWorkspaceAdmissionState,
622
+ options: Pick<
623
+ TelegramWorkspaceAdmissionLedgerOptions,
624
+ "publishRename" | "onPublicationBoundary"
625
+ >,
626
+ ): void {
627
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
628
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
629
+ try {
630
+ options.onPublicationBoundary?.("before-write", path);
631
+ writeFileSync(tempPath, `${JSON.stringify(state, null, 2)}\n`, {
632
+ encoding: "utf8",
633
+ mode: 0o600,
634
+ });
635
+ chmodSync(tempPath, 0o600);
636
+ options.onPublicationBoundary?.("after-write-before-rename", path);
637
+ try {
638
+ if (
639
+ !renameTelegramPathWithRetry(tempPath, path, {
640
+ ...(options.publishRename ? { rename: options.publishRename } : {}),
641
+ })
642
+ ) {
643
+ throw new Error("Temporary admission state disappeared before publication.");
644
+ }
645
+ chmodSync(path, 0o600);
646
+ } catch (error) {
647
+ throw new TelegramWorkspaceAdmissionError(
648
+ "publication-unknown",
649
+ "Telegram Workspace admission publication outcome is unknown.",
650
+ { cause: error },
651
+ );
652
+ }
653
+ } finally {
654
+ try {
655
+ unlinkSync(tempPath);
656
+ } catch {
657
+ // A successful atomic rename consumes the temporary path.
658
+ }
659
+ }
660
+ }
661
+
662
+ function areTargetsEqual(
663
+ left: TelegramTarget & { threadId: number },
664
+ right: TelegramTarget & { threadId: number },
665
+ ): boolean {
666
+ return left.chatId === right.chatId && left.threadId === right.threadId;
667
+ }
668
+
669
+ function scopesConflict(
670
+ scope: TelegramWorkspaceAdmissionScope,
671
+ target: TelegramTarget & { threadId: number },
672
+ ): boolean {
673
+ if (scope.kind === "profile") return true;
674
+ if (scope.kind === "chat") return scope.chatId === target.chatId;
675
+ return areTargetsEqual(scope.target, target);
676
+ }
677
+
678
+ function areOwnersEqual(
679
+ left: TelegramWorkspaceAdmissionOwner,
680
+ right: TelegramWorkspaceAdmissionOwner,
681
+ ): boolean {
682
+ return (
683
+ left.processId === right.processId &&
684
+ left.processBirthId === right.processBirthId
685
+ );
686
+ }
687
+
688
+ function areScopesEqual(
689
+ left: TelegramWorkspaceAdmissionScope,
690
+ right: TelegramWorkspaceAdmissionScope,
691
+ ): boolean {
692
+ if (left.kind !== right.kind) return false;
693
+ if (left.kind === "profile" && right.kind === "profile") return true;
694
+ if (left.kind === "chat" && right.kind === "chat") {
695
+ return left.chatId === right.chatId;
696
+ }
697
+ return (
698
+ left.kind === "target" &&
699
+ right.kind === "target" &&
700
+ areTargetsEqual(left.target, right.target)
701
+ );
702
+ }
703
+
704
+ function areLeasesEqual(
705
+ left: TelegramWorkspaceAdmissionLease,
706
+ right: TelegramWorkspaceAdmissionLease,
707
+ ): boolean {
708
+ return (
709
+ left.operationId === right.operationId &&
710
+ left.operationKind === right.operationKind &&
711
+ left.profileKey === right.profileKey &&
712
+ areScopesEqual(left.scope, right.scope) &&
713
+ areOwnersEqual(left.owner, right.owner) &&
714
+ left.acquiredAtMs === right.acquiredAtMs
715
+ );
716
+ }
717
+
718
+ function areFenceBasesEqual(
719
+ left: TelegramWorkspaceRetirementFence,
720
+ right: TelegramWorkspaceRetirementFence,
721
+ ): boolean {
722
+ return (
723
+ resolveTelegramWorkspaceDestructiveFenceKind(left) ===
724
+ resolveTelegramWorkspaceDestructiveFenceKind(right) &&
725
+ left.operationId === right.operationId &&
726
+ left.retirementIntentId === right.retirementIntentId &&
727
+ left.profileKey === right.profileKey &&
728
+ left.bindingKey === right.bindingKey &&
729
+ left.slot === right.slot &&
730
+ areTargetsEqual(left.target, right.target) &&
731
+ left.leaderEpoch === right.leaderEpoch &&
732
+ left.retirementRequestedAtMs === right.retirementRequestedAtMs &&
733
+ areOwnersEqual(left.owner, right.owner) &&
734
+ left.acquiredAtMs === right.acquiredAtMs
735
+ );
736
+ }
737
+
738
+ function areWriterProtocolModesEqual(
739
+ left: TelegramWorkspaceJournalWriterProtocolMode,
740
+ right: TelegramWorkspaceJournalWriterProtocolMode,
741
+ ): boolean {
742
+ return left.protocol === right.protocol && left.profileKey === right.profileKey &&
743
+ left.recoveryKey === right.recoveryKey &&
744
+ left.startupAuthorityId === right.startupAuthorityId &&
745
+ left.closureOperationId === right.closureOperationId &&
746
+ left.writerInventorySha256 === right.writerInventorySha256 &&
747
+ areOwnersEqual(left.installedBy, right.installedBy) && left.installedAtMs === right.installedAtMs;
748
+ }
749
+
750
+ function areWriterClosureFencesEqual(
751
+ left: TelegramWorkspaceJournalWriterClosureFence,
752
+ right: TelegramWorkspaceJournalWriterClosureFence,
753
+ ): boolean {
754
+ return left.operationId === right.operationId && left.profileKey === right.profileKey &&
755
+ left.recoveryKey === right.recoveryKey && areOwnersEqual(left.owner, right.owner) &&
756
+ left.requestedAtMs === right.requestedAtMs && left.acquiredAtMs === right.acquiredAtMs;
757
+ }
758
+
759
+ function areFencesEqual(
760
+ left: TelegramWorkspaceRetirementFence,
761
+ right: TelegramWorkspaceRetirementFence,
762
+ ): boolean {
763
+ if (!areFenceBasesEqual(left, right) || left.phase !== right.phase) return false;
764
+ if (left.phase === "fenced" && right.phase === "fenced") return true;
765
+ if (
766
+ left.phase === "deletion-issued" &&
767
+ right.phase === "deletion-issued"
768
+ ) {
769
+ return left.deletionIssuedAtMs === right.deletionIssuedAtMs;
770
+ }
771
+ return (
772
+ left.phase === "commit-ready" &&
773
+ right.phase === "commit-ready" &&
774
+ left.absenceConfirmedAtMs === right.absenceConfirmedAtMs &&
775
+ left.deletionIssuedAtMs === right.deletionIssuedAtMs
776
+ );
777
+ }
778
+
779
+ function validateOptions(options: TelegramWorkspaceAdmissionLedgerOptions): void {
780
+ if (!isBoundedText(options.path)) {
781
+ invalidInput("Telegram Workspace admission path is required.");
782
+ }
783
+ if (!isBoundedText(options.profileKey)) {
784
+ invalidInput("Telegram Workspace admission profile authority is required.");
785
+ }
786
+ if (!normalizeOwner(options.owner)) {
787
+ invalidInput("Telegram Workspace admission owner is invalid.");
788
+ }
789
+ }
790
+
791
+ function validateOperationId(operationId: string): void {
792
+ if (!isBoundedText(operationId)) {
793
+ invalidInput("Telegram Workspace admission operation identity is invalid.");
794
+ }
795
+ }
796
+
797
+ function validateScope(scope: TelegramWorkspaceAdmissionScope): void {
798
+ if (!normalizeScope(scope)) {
799
+ invalidInput("Telegram Workspace admission scope is invalid.");
800
+ }
801
+ }
802
+
803
+ function validateExpectedLease(
804
+ lease: TelegramWorkspaceAdmissionLease,
805
+ profileKey: string,
806
+ ): void {
807
+ if (!normalizeLease(lease, profileKey)) {
808
+ invalidInput("Telegram Workspace admission lease authority is invalid.");
809
+ }
810
+ }
811
+
812
+ function validateExpectedFence(
813
+ fence: TelegramWorkspaceRetirementFence,
814
+ profileKey: string,
815
+ ): void {
816
+ if (!normalizeFence(fence, profileKey)) {
817
+ invalidInput("Telegram Workspace retirement fence authority is invalid.");
818
+ }
819
+ }
820
+
821
+ export function createTelegramWorkspaceAdmissionOperationId(): string {
822
+ return randomUUID();
823
+ }
824
+
825
+ export function createTelegramWorkspaceAdmissionProfileKey(input: {
826
+ profileName?: string;
827
+ botToken: string;
828
+ }): string {
829
+ const profileName = (input.profileName ?? "default").trim();
830
+ if (!isBoundedText(profileName) || !isBoundedText(input.botToken)) {
831
+ invalidInput("Telegram Workspace admission bot/profile identity is invalid.");
832
+ }
833
+ return JSON.stringify({
834
+ version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
835
+ profile: profileName,
836
+ bot: {
837
+ tokenSha256: createHash("sha256").update(input.botToken).digest("hex"),
838
+ },
839
+ });
840
+ }
841
+
842
+ export interface TelegramWorkspaceAdmissionRuntimeBinding {
843
+ resolve: () => TelegramWorkspaceAdmissionLedger | undefined;
844
+ }
845
+
846
+ export function createTelegramWorkspaceAdmissionRuntimeBinding(input: {
847
+ getProfileName: () => string | undefined;
848
+ getBotToken: () => string | undefined;
849
+ getPath: (profileName?: string) => string;
850
+ owner: TelegramWorkspaceAdmissionOwner;
851
+ getNowMs?: () => number;
852
+ getProcessLiveness?: (
853
+ owner: TelegramWorkspaceAdmissionOwner,
854
+ ) => TelegramProcessLiveness;
855
+ }): TelegramWorkspaceAdmissionRuntimeBinding {
856
+ return {
857
+ resolve() {
858
+ const botToken = input.getBotToken();
859
+ if (!botToken) return undefined;
860
+ const configuredProfileName = input.getProfileName();
861
+ const profileKey = createTelegramWorkspaceAdmissionProfileKey({
862
+ profileName: configuredProfileName ?? "default",
863
+ botToken,
864
+ });
865
+ return createTelegramWorkspaceAdmissionLedger({
866
+ path: input.getPath(configuredProfileName),
867
+ profileKey,
868
+ owner: input.owner,
869
+ getNowMs: input.getNowMs,
870
+ getProcessLiveness: input.getProcessLiveness,
871
+ });
872
+ },
873
+ };
874
+ }
875
+
876
+ function getAdmissionScopeKey(scope: TelegramWorkspaceAdmissionScope): string {
877
+ if (scope.kind === "profile") return "profile";
878
+ if (scope.kind === "chat") return `chat:${scope.chatId}`;
879
+ return `target:${scope.target.chatId}:${scope.target.threadId}`;
880
+ }
881
+
882
+ function reserveTelegramWorkspaceAdmissionOperationId(
883
+ operationId: string,
884
+ ): () => void {
885
+ if (activeTelegramWorkspaceAdmissionOperationIds.has(operationId)) {
886
+ invalidState("Telegram Workspace admission operation is already active.");
887
+ }
888
+ activeTelegramWorkspaceAdmissionOperationIds.add(operationId);
889
+ return () => {
890
+ activeTelegramWorkspaceAdmissionOperationIds.delete(operationId);
891
+ };
892
+ }
893
+
894
+ export function runWithTelegramWorkspaceAdmissions<T>(input: {
895
+ ledger: Pick<
896
+ TelegramWorkspaceAdmissionLedger,
897
+ "acquireAdmission" | "releaseAdmission"
898
+ >;
899
+ operationId: string;
900
+ operationKind: string;
901
+ scopes: readonly TelegramWorkspaceAdmissionScope[];
902
+ operation: () => T;
903
+ }): T {
904
+ validateOperationId(input.operationId);
905
+ if (!isBoundedText(input.operationKind) || input.scopes.length === 0) {
906
+ invalidInput("Telegram Workspace admitted operation is invalid.");
907
+ }
908
+ const uniqueScopes = new Map<string, TelegramWorkspaceAdmissionScope>();
909
+ for (const scope of input.scopes) {
910
+ validateScope(scope);
911
+ uniqueScopes.set(getAdmissionScopeKey(scope), cloneScope(scope));
912
+ }
913
+ const scopes = Array.from(uniqueScopes.entries())
914
+ .sort(([left], [right]) => left.localeCompare(right))
915
+ .map(([, scope]) => scope);
916
+ const releaseActiveOperation =
917
+ reserveTelegramWorkspaceAdmissionOperationId(input.operationId);
918
+ const acquired: TelegramWorkspaceAdmissionLease[] = [];
919
+ let outcome: { ok: true; value: T } | { ok: false; error: unknown };
920
+ try {
921
+ for (const scope of scopes) {
922
+ const scopeOperationId = `admission:${createHash("sha256")
923
+ .update(input.operationId)
924
+ .update("\0")
925
+ .update(getAdmissionScopeKey(scope))
926
+ .digest("hex")}`;
927
+ const result = input.ledger.acquireAdmission({
928
+ operationId: scopeOperationId,
929
+ operationKind: input.operationKind,
930
+ scope,
931
+ });
932
+ if (result.kind === "blocked") {
933
+ throw new TelegramWorkspaceAdmissionError(
934
+ "admission-blocked",
935
+ "Telegram Workspace operation is blocked by retirement.",
936
+ );
937
+ }
938
+ acquired.push(result.lease);
939
+ }
940
+ outcome = { ok: true, value: input.operation() };
941
+ } catch (error) {
942
+ outcome = { ok: false, error };
943
+ }
944
+ const releaseErrors: unknown[] = [];
945
+ for (const lease of acquired.reverse()) {
946
+ try {
947
+ input.ledger.releaseAdmission(lease);
948
+ } catch (error) {
949
+ releaseErrors.push(error);
950
+ }
951
+ }
952
+ releaseActiveOperation();
953
+ if (releaseErrors.length > 0) {
954
+ throw new AggregateError(
955
+ outcome.ok ? releaseErrors : [outcome.error, ...releaseErrors],
956
+ "Telegram Workspace admission release failed.",
957
+ );
958
+ }
959
+ if (!outcome.ok) throw outcome.error;
960
+ return outcome.value;
961
+ }
962
+
963
+ export function createTelegramWorkspaceJournalWriterAdmission(input: {
964
+ ledger: Pick<TelegramWorkspaceAdmissionLedger,
965
+ "acquireAdmission" | "acquireJournalWriterAdmission" | "releaseAdmission">;
966
+ protocolAuthority?: Omit<Parameters<TelegramWorkspaceAdmissionLedger["acquireJournalWriterAdmission"]>[0],
967
+ "operationId">;
968
+ createOperationId?: () => string;
969
+ onReleaseError?: (error: unknown) => void;
970
+ }): <T>(operation: () => T) => T {
971
+ let pendingOperationId: string | undefined;
972
+ let active = false;
973
+ return <T>(operation: () => T): T => {
974
+ if (active) invalidState("Telegram Workspace journal writer admission is already active.");
975
+ const operationId = pendingOperationId ?? input.createOperationId?.() ??
976
+ `journal-writer:${randomUUID()}`;
977
+ validateOperationId(operationId);
978
+ pendingOperationId = operationId;
979
+ const result = input.protocolAuthority
980
+ ? input.ledger.acquireJournalWriterAdmission({ operationId, ...input.protocolAuthority })
981
+ : input.ledger.acquireAdmission({ operationId,
982
+ operationKind: "journal-write", scope: { kind: "profile" } });
983
+ if (result.kind === "blocked") {
984
+ throw new TelegramWorkspaceAdmissionError("admission-blocked",
985
+ "Telegram Workspace journal writer is blocked by destructive closure.");
986
+ }
987
+ pendingOperationId = undefined;
988
+ active = true;
989
+ try {
990
+ return operation();
991
+ } finally {
992
+ active = false;
993
+ try {
994
+ if (!input.ledger.releaseAdmission(result.lease))
995
+ authorityChanged("Telegram Workspace journal writer lease disappeared before release.");
996
+ } catch (error) {
997
+ try { input.onReleaseError?.(error); } catch { /* Diagnostics cannot change the journal outcome. */ }
998
+ }
999
+ }
1000
+ };
1001
+ }
1002
+
1003
+ export async function runWithTelegramWorkspaceAdmissionsAsync<T>(input: {
1004
+ ledger: Pick<
1005
+ TelegramWorkspaceAdmissionLedger,
1006
+ "acquireAdmission" | "releaseAdmission"
1007
+ >;
1008
+ operationId: string;
1009
+ operationKind: string;
1010
+ scopes: readonly TelegramWorkspaceAdmissionScope[];
1011
+ operation: () => Promise<T>;
1012
+ onReleaseError?: (error: unknown) => void;
1013
+ }): Promise<T> {
1014
+ validateOperationId(input.operationId);
1015
+ if (!isBoundedText(input.operationKind) || input.scopes.length === 0) {
1016
+ invalidInput("Telegram Workspace admitted operation is invalid.");
1017
+ }
1018
+ const uniqueScopes = new Map<string, TelegramWorkspaceAdmissionScope>();
1019
+ for (const scope of input.scopes) {
1020
+ validateScope(scope);
1021
+ uniqueScopes.set(getAdmissionScopeKey(scope), cloneScope(scope));
1022
+ }
1023
+ const scopes = Array.from(uniqueScopes.entries())
1024
+ .sort(([left], [right]) => left.localeCompare(right))
1025
+ .map(([, scope]) => scope);
1026
+ const releaseActiveOperation =
1027
+ reserveTelegramWorkspaceAdmissionOperationId(input.operationId);
1028
+ const acquired: TelegramWorkspaceAdmissionLease[] = [];
1029
+ const releaseAll = (): void => {
1030
+ for (const lease of acquired.reverse()) {
1031
+ try {
1032
+ if (!input.ledger.releaseAdmission(lease)) {
1033
+ throw new TelegramWorkspaceAdmissionError(
1034
+ "authority-changed",
1035
+ "Telegram Workspace admission lease disappeared before release.",
1036
+ );
1037
+ }
1038
+ } catch (error) {
1039
+ try {
1040
+ input.onReleaseError?.(error);
1041
+ } catch {
1042
+ // Diagnostics cannot change the settled admitted operation.
1043
+ }
1044
+ }
1045
+ }
1046
+ };
1047
+ try {
1048
+ for (const scope of scopes) {
1049
+ const scopeOperationId = `admission:${createHash("sha256")
1050
+ .update(input.operationId)
1051
+ .update("\0")
1052
+ .update(getAdmissionScopeKey(scope))
1053
+ .digest("hex")}`;
1054
+ const result = input.ledger.acquireAdmission({
1055
+ operationId: scopeOperationId,
1056
+ operationKind: input.operationKind,
1057
+ scope,
1058
+ });
1059
+ if (result.kind === "blocked") {
1060
+ throw new TelegramWorkspaceAdmissionError(
1061
+ "admission-blocked",
1062
+ "Telegram Workspace operation is blocked by retirement.",
1063
+ );
1064
+ }
1065
+ acquired.push(result.lease);
1066
+ }
1067
+ } catch (error) {
1068
+ releaseAll();
1069
+ releaseActiveOperation();
1070
+ throw error;
1071
+ }
1072
+ try {
1073
+ return await input.operation();
1074
+ } finally {
1075
+ releaseAll();
1076
+ releaseActiveOperation();
1077
+ }
1078
+ }
1079
+
1080
+ export function createTelegramWorkspaceAdmissionLedger(
1081
+ options: TelegramWorkspaceAdmissionLedgerOptions,
1082
+ ): TelegramWorkspaceAdmissionLedger {
1083
+ validateOptions(options);
1084
+ const getNowMs = options.getNowMs ?? Date.now;
1085
+ const getProcessLiveness =
1086
+ options.getProcessLiveness ??
1087
+ ((owner: TelegramWorkspaceAdmissionOwner) =>
1088
+ getTelegramProcessLiveness(owner));
1089
+ const transactionPath = `${options.path}.transaction`;
1090
+
1091
+ function transact<T>(
1092
+ operation: (state: TelegramWorkspaceAdmissionState) => {
1093
+ result: T;
1094
+ changed: boolean;
1095
+ },
1096
+ ): T {
1097
+ return withTelegramFileTransaction(transactionPath, () => {
1098
+ const state = readState(options.path, options.profileKey);
1099
+ const retainedLeases = state.leases.filter(
1100
+ (lease) => getProcessLiveness(lease.owner) !== "dead",
1101
+ );
1102
+ const pruned = retainedLeases.length !== state.leases.length;
1103
+ if (pruned) state.leases = retainedLeases;
1104
+ const outcome = operation(state);
1105
+ if (pruned || outcome.changed) writeState(options.path, state, options);
1106
+ return outcome.result;
1107
+ });
1108
+ }
1109
+
1110
+ function read(): TelegramWorkspaceAdmissionLedgerSnapshot {
1111
+ const state = readState(options.path, options.profileKey);
1112
+ return {
1113
+ profileKey: state.profileKey,
1114
+ leases: state.leases.map(cloneLease),
1115
+ ...(state.fence ? { fence: cloneFence(state.fence) } : {}),
1116
+ ...(state.writerProtocolMode ? { writerProtocolMode: {
1117
+ ...state.writerProtocolMode, installedBy: { ...state.writerProtocolMode.installedBy },
1118
+ } } : {}),
1119
+ };
1120
+ }
1121
+
1122
+ function acquireAdmission(input: {
1123
+ operationId: string;
1124
+ operationKind: string;
1125
+ scope: TelegramWorkspaceAdmissionScope;
1126
+ }): TelegramWorkspaceAdmissionAcquireResult {
1127
+ validateOperationId(input.operationId);
1128
+ if (!isBoundedText(input.operationKind)) {
1129
+ invalidInput("Telegram Workspace admission operation kind is invalid.");
1130
+ }
1131
+ validateScope(input.scope);
1132
+ return transact<TelegramWorkspaceAdmissionAcquireResult>((state) => {
1133
+ const journalOperation = input.operationKind === "journal-write" ||
1134
+ input.operationKind.startsWith("journal.");
1135
+ const protocolWriterHeld = state.leases.some(lease =>
1136
+ lease.operationKind === "journal-write:custody-v3" && lease.scope.kind === "profile" &&
1137
+ areOwnersEqual(lease.owner, options.owner));
1138
+ if (state.writerProtocolMode && journalOperation && !protocolWriterHeld)
1139
+ authorityChanged("Telegram Workspace journal writer requires protocol authority.");
1140
+ const existing = state.leases.find(
1141
+ (lease) => lease.operationId === input.operationId,
1142
+ );
1143
+ if (existing) {
1144
+ const retried: TelegramWorkspaceAdmissionLease = {
1145
+ operationId: input.operationId,
1146
+ operationKind: input.operationKind,
1147
+ profileKey: options.profileKey,
1148
+ scope: cloneScope(input.scope),
1149
+ owner: { ...options.owner },
1150
+ acquiredAtMs: existing.acquiredAtMs,
1151
+ };
1152
+ if (!areLeasesEqual(existing, retried)) {
1153
+ authorityChanged("Telegram Workspace admission operation identity changed.");
1154
+ }
1155
+ return {
1156
+ result: { kind: "acquired", lease: cloneLease(existing), resumed: true },
1157
+ changed: false,
1158
+ };
1159
+ }
1160
+ if (state.fence && (!isTelegramWorkspaceRetirementFence(state.fence) ||
1161
+ scopesConflict(input.scope, state.fence.target))) {
1162
+ return {
1163
+ result: { kind: "blocked", reason: "retirement-fenced" },
1164
+ changed: false,
1165
+ };
1166
+ }
1167
+ if (state.leases.length >= TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES) {
1168
+ invalidState("Telegram Workspace admission lease capacity is exhausted.");
1169
+ }
1170
+ const lease: TelegramWorkspaceAdmissionLease = {
1171
+ operationId: input.operationId,
1172
+ operationKind: input.operationKind,
1173
+ profileKey: options.profileKey,
1174
+ scope: cloneScope(input.scope),
1175
+ owner: { ...options.owner },
1176
+ acquiredAtMs: getNowMs(),
1177
+ };
1178
+ state.leases.push(lease);
1179
+ return {
1180
+ result: { kind: "acquired", lease: cloneLease(lease), resumed: false },
1181
+ changed: true,
1182
+ };
1183
+ });
1184
+ }
1185
+
1186
+ function releaseAdmission(expected: TelegramWorkspaceAdmissionLease): boolean {
1187
+ validateExpectedLease(expected, options.profileKey);
1188
+ if (!areOwnersEqual(expected.owner, options.owner)) {
1189
+ authorityChanged("Telegram Workspace admission release requires lease owner authority.");
1190
+ }
1191
+ return transact((state) => {
1192
+ const index = state.leases.findIndex(
1193
+ (lease) => lease.operationId === expected.operationId,
1194
+ );
1195
+ if (index < 0) return { result: false, changed: false };
1196
+ const current = state.leases[index];
1197
+ if (!areLeasesEqual(current, expected)) {
1198
+ authorityChanged("Telegram Workspace admission lease authority changed.");
1199
+ }
1200
+ state.leases.splice(index, 1);
1201
+ return { result: true, changed: true };
1202
+ });
1203
+ }
1204
+
1205
+ function acquireJournalWriterClosure(input: { operationId: string; recoveryKey: string;
1206
+ requestedAtMs: number }): TelegramWorkspaceJournalWriterClosureAcquireResult {
1207
+ validateOperationId(input.operationId);
1208
+ const candidate = normalizeTelegramWorkspaceJournalWriterClosureFence({
1209
+ destructiveKind: "journal-writer-closure", phase: "fenced", ...input,
1210
+ profileKey: options.profileKey, owner: options.owner, acquiredAtMs: getNowMs(),
1211
+ }, options.profileKey);
1212
+ if (!candidate) invalidInput("Telegram Workspace writer closure input is invalid.");
1213
+ return transact<TelegramWorkspaceJournalWriterClosureAcquireResult>((state) => {
1214
+ if (state.writerProtocolMode) {
1215
+ if (state.leases.length > 0)
1216
+ return { result: { kind: "blocked", reason: "admission-active" }, changed: false };
1217
+ let authorized = false;
1218
+ if (state.writerProtocolMode.recoveryKey === candidate.recoveryKey) {
1219
+ try { authorized = options.authorizeJournalWriterProtocolClosure?.({
1220
+ mode: { ...state.writerProtocolMode,
1221
+ installedBy: { ...state.writerProtocolMode.installedBy } },
1222
+ closure: cloneFence(candidate),
1223
+ }) === true; } catch { authorized = false; }
1224
+ }
1225
+ if (!authorized)
1226
+ return { result: { kind: "blocked", reason: "retirement-active" }, changed: false };
1227
+ delete state.writerProtocolMode;
1228
+ state.fence = candidate;
1229
+ return { result: { kind: "acquired", fence: cloneFence(candidate), resumed: false },
1230
+ changed: true };
1231
+ }
1232
+ if (state.fence) {
1233
+ if (state.fence.destructiveKind === "journal-writer-closure") {
1234
+ const resumed = { ...candidate, acquiredAtMs: state.fence.acquiredAtMs };
1235
+ if (areWriterClosureFencesEqual(state.fence, resumed))
1236
+ return { result: { kind: "acquired", fence: cloneFence(state.fence), resumed: true }, changed: false };
1237
+ if (state.fence.operationId === candidate.operationId)
1238
+ authorityChanged("Telegram Workspace writer closure authority changed.");
1239
+ }
1240
+ return { result: { kind: "blocked", reason: "retirement-active" }, changed: false };
1241
+ }
1242
+ if (state.leases.length > 0)
1243
+ return { result: { kind: "blocked", reason: "admission-active" }, changed: false };
1244
+ state.fence = candidate;
1245
+ return { result: { kind: "acquired", fence: cloneFence(candidate), resumed: false }, changed: true };
1246
+ });
1247
+ }
1248
+
1249
+ function releaseJournalWriterClosure(expected: TelegramWorkspaceJournalWriterClosureFence): boolean {
1250
+ const normalized = normalizeTelegramWorkspaceJournalWriterClosureFence(expected, options.profileKey);
1251
+ if (!normalized) invalidInput("Telegram Workspace writer closure authority is invalid.");
1252
+ if (!areOwnersEqual(expected.owner, options.owner))
1253
+ authorityChanged("Telegram Workspace writer closure release requires owner authority.");
1254
+ return transact((state) => {
1255
+ if (!state.fence) return { result: false, changed: false };
1256
+ if (state.fence.destructiveKind !== "journal-writer-closure" ||
1257
+ !areWriterClosureFencesEqual(state.fence, expected))
1258
+ authorityChanged("Telegram Workspace writer closure authority changed.");
1259
+ delete state.fence;
1260
+ return { result: true, changed: true };
1261
+ });
1262
+ }
1263
+
1264
+ function installJournalWriterProtocolMode(
1265
+ expected: TelegramWorkspaceJournalWriterClosureFence,
1266
+ input: { startupAuthorityId: string; writerInventorySha256: string },
1267
+ ): TelegramWorkspaceJournalWriterProtocolInstallResult {
1268
+ const normalizedClosure = normalizeTelegramWorkspaceJournalWriterClosureFence(expected,
1269
+ options.profileKey);
1270
+ if (!normalizedClosure || !areOwnersEqual(expected.owner, options.owner))
1271
+ authorityChanged("Telegram Workspace writer protocol installation requires closure owner authority.");
1272
+ const candidate = normalizeTelegramWorkspaceJournalWriterProtocolMode({ version: 1,
1273
+ protocol: "custody-v3", profileKey: options.profileKey, recoveryKey: expected.recoveryKey,
1274
+ startupAuthorityId: input.startupAuthorityId, closureOperationId: expected.operationId,
1275
+ writerInventorySha256: input.writerInventorySha256, installedBy: options.owner,
1276
+ installedAtMs: getNowMs() }, options.profileKey);
1277
+ if (!candidate) invalidInput("Telegram Workspace writer protocol mode input is invalid.");
1278
+ return transact<TelegramWorkspaceJournalWriterProtocolInstallResult>((state) => {
1279
+ if (state.writerProtocolMode) {
1280
+ const resumed = { ...candidate, installedAtMs: state.writerProtocolMode.installedAtMs };
1281
+ if (areWriterProtocolModesEqual(state.writerProtocolMode, resumed) && !state.fence)
1282
+ return { result: { mode: { ...state.writerProtocolMode,
1283
+ installedBy: { ...state.writerProtocolMode.installedBy } }, resumed: true }, changed: false };
1284
+ authorityChanged("Telegram Workspace writer protocol mode authority changed.");
1285
+ }
1286
+ if (!state.fence || state.fence.destructiveKind !== "journal-writer-closure" ||
1287
+ !areWriterClosureFencesEqual(state.fence, expected) || state.leases.length !== 0)
1288
+ authorityChanged("Telegram Workspace writer closure authority changed before protocol installation.");
1289
+ state.writerProtocolMode = candidate;
1290
+ delete state.fence;
1291
+ return { result: { mode: { ...candidate, installedBy: { ...candidate.installedBy } },
1292
+ resumed: false }, changed: true };
1293
+ });
1294
+ }
1295
+
1296
+ function acquireJournalWriterAdmission(input: { operationId: string; recoveryKey: string;
1297
+ startupAuthorityId: string; closureOperationId: string; writerInventorySha256: string }):
1298
+ TelegramWorkspaceAdmissionAcquireResult {
1299
+ validateOperationId(input.operationId);
1300
+ if (!isBoundedText(input.recoveryKey) || !isBoundedText(input.startupAuthorityId) ||
1301
+ !isBoundedText(input.closureOperationId) || !/^[a-f0-9]{64}$/u.test(input.writerInventorySha256))
1302
+ invalidInput("Telegram Workspace journal writer protocol authority is invalid.");
1303
+ return transact<TelegramWorkspaceAdmissionAcquireResult>((state) => {
1304
+ const mode = state.writerProtocolMode;
1305
+ if (!mode || mode.recoveryKey !== input.recoveryKey ||
1306
+ mode.startupAuthorityId !== input.startupAuthorityId ||
1307
+ mode.closureOperationId !== input.closureOperationId ||
1308
+ mode.writerInventorySha256 !== input.writerInventorySha256)
1309
+ authorityChanged("Telegram Workspace journal writer protocol authority changed.");
1310
+ const existing = state.leases.find(lease => lease.operationId === input.operationId);
1311
+ const operationKind = "journal-write:custody-v3";
1312
+ if (existing) {
1313
+ const retried: TelegramWorkspaceAdmissionLease = { operationId: input.operationId,
1314
+ operationKind, profileKey: options.profileKey, scope: { kind: "profile" },
1315
+ owner: { ...options.owner }, acquiredAtMs: existing.acquiredAtMs };
1316
+ if (!areLeasesEqual(existing, retried))
1317
+ authorityChanged("Telegram Workspace journal writer operation identity changed.");
1318
+ return { result: { kind: "acquired", lease: cloneLease(existing), resumed: true },
1319
+ changed: false };
1320
+ }
1321
+ if (state.fence) return { result: { kind: "blocked", reason: "retirement-fenced" },
1322
+ changed: false };
1323
+ if (state.leases.length >= TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES)
1324
+ invalidState("Telegram Workspace admission lease capacity is exhausted.");
1325
+ const lease: TelegramWorkspaceAdmissionLease = { operationId: input.operationId,
1326
+ operationKind, profileKey: options.profileKey, scope: { kind: "profile" },
1327
+ owner: { ...options.owner }, acquiredAtMs: getNowMs() };
1328
+ state.leases.push(lease);
1329
+ return { result: { kind: "acquired", lease: cloneLease(lease), resumed: false },
1330
+ changed: true };
1331
+ });
1332
+ }
1333
+
1334
+ function acquireDestructiveFence(input: {
1335
+ operationId: string;
1336
+ retirementIntentId: string;
1337
+ bindingKey: string;
1338
+ slot: string;
1339
+ target: TelegramTarget & { threadId: number };
1340
+ leaderEpoch: number | string;
1341
+ retirementRequestedAtMs: number;
1342
+ }, destructiveKind?: TelegramWorkspaceDeletionFenceKind): TelegramWorkspaceRetirementFenceAcquireResult {
1343
+ validateOperationId(input.operationId);
1344
+ const candidate = normalizeFence(
1345
+ {
1346
+ ...input,
1347
+ ...(destructiveKind === undefined ? {} : { destructiveKind }),
1348
+ profileKey: options.profileKey,
1349
+ owner: options.owner,
1350
+ acquiredAtMs: getNowMs(),
1351
+ phase: "fenced",
1352
+ },
1353
+ options.profileKey,
1354
+ );
1355
+ if (!candidate || !isTelegramWorkspaceRetirementFence(candidate)) {
1356
+ invalidInput("Telegram Workspace retirement fence input is invalid.");
1357
+ }
1358
+ return transact<TelegramWorkspaceRetirementFenceAcquireResult>((state) => {
1359
+ if (state.fence) {
1360
+ if (!isTelegramWorkspaceRetirementFence(state.fence)) {
1361
+ return { result: { kind: "blocked", reason: "retirement-active" }, changed: false };
1362
+ }
1363
+ if (
1364
+ state.fence.phase === "fenced" &&
1365
+ areFenceBasesEqual(state.fence, {
1366
+ ...candidate,
1367
+ acquiredAtMs: state.fence.acquiredAtMs,
1368
+ })
1369
+ ) {
1370
+ return {
1371
+ result: {
1372
+ kind: "acquired",
1373
+ fence: cloneFence(state.fence),
1374
+ resumed: true,
1375
+ },
1376
+ changed: false,
1377
+ };
1378
+ }
1379
+ if (state.fence.operationId === candidate.operationId) {
1380
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1381
+ }
1382
+ return {
1383
+ result: { kind: "blocked", reason: "retirement-active" },
1384
+ changed: false,
1385
+ };
1386
+ }
1387
+ if (state.leases.some((lease) => scopesConflict(lease.scope, candidate.target))) {
1388
+ return {
1389
+ result: { kind: "blocked", reason: "admission-active" },
1390
+ changed: false,
1391
+ };
1392
+ }
1393
+ state.fence = candidate;
1394
+ return {
1395
+ result: { kind: "acquired", fence: cloneFence(candidate), resumed: false },
1396
+ changed: true,
1397
+ };
1398
+ });
1399
+ }
1400
+
1401
+ function acquireRetirementFence(input: Parameters<TelegramWorkspaceAdmissionLedger["acquireRetirementFence"]>[0]) {
1402
+ return acquireDestructiveFence(input);
1403
+ }
1404
+
1405
+ function acquireThreadCleanupFence(
1406
+ input: Parameters<TelegramWorkspaceAdmissionLedger["acquireThreadCleanupFence"]>[0],
1407
+ ): TelegramWorkspaceRetirementFenceAcquireResult {
1408
+ return acquireDestructiveFence({ operationId: input.operationId,
1409
+ retirementIntentId: input.cleanupWorkSetId, bindingKey: input.bindingKey,
1410
+ slot: input.slot, target: input.target, leaderEpoch: input.leaderEpoch,
1411
+ retirementRequestedAtMs: input.cleanupRequestedAtMs }, "manual-thread-cleanup");
1412
+ }
1413
+
1414
+ function adoptRetirementFence(
1415
+ expected: TelegramWorkspaceRetirementFence,
1416
+ replacement: {
1417
+ owner: TelegramWorkspaceAdmissionOwner;
1418
+ leaderEpoch: number | string;
1419
+ },
1420
+ expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
1421
+ ): TelegramWorkspaceRetirementFence {
1422
+ validateExpectedFence(expected, options.profileKey);
1423
+ if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
1424
+ authorityChanged("Telegram Workspace destructive fence kind changed.");
1425
+ if (!normalizeOwner(replacement.owner) || !isLeaderEpoch(replacement.leaderEpoch)) {
1426
+ invalidInput("Telegram Workspace retirement successor authority is invalid.");
1427
+ }
1428
+ if (!areOwnersEqual(replacement.owner, options.owner)) {
1429
+ authorityChanged("Telegram Workspace retirement adoption requires successor owner authority.");
1430
+ }
1431
+ const adopted = {
1432
+ ...cloneFence(expected),
1433
+ owner: { ...replacement.owner },
1434
+ leaderEpoch: replacement.leaderEpoch,
1435
+ } satisfies TelegramWorkspaceRetirementFence;
1436
+ return transact((state) => {
1437
+ if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
1438
+ authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
1439
+ }
1440
+ if (areFencesEqual(state.fence, adopted)) {
1441
+ return { result: cloneFence(state.fence), changed: false };
1442
+ }
1443
+ if (!areFencesEqual(state.fence, expected)) {
1444
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1445
+ }
1446
+ state.fence = adopted;
1447
+ return { result: cloneFence(adopted), changed: true };
1448
+ });
1449
+ }
1450
+
1451
+ function issueDeletionPermit(
1452
+ expected: TelegramWorkspaceRetirementFence,
1453
+ expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
1454
+ ): TelegramWorkspaceDeletionPermitResult {
1455
+ validateExpectedFence(expected, options.profileKey);
1456
+ if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
1457
+ authorityChanged("Telegram Workspace destructive fence kind changed.");
1458
+ if (!areOwnersEqual(expected.owner, options.owner)) {
1459
+ authorityChanged("Telegram Workspace deletion permit requires fence owner authority.");
1460
+ }
1461
+ return transact<TelegramWorkspaceDeletionPermitResult>((state) => {
1462
+ if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
1463
+ authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
1464
+ }
1465
+ if (
1466
+ state.fence.operationId === expected.operationId &&
1467
+ areFenceBasesEqual(state.fence, expected) &&
1468
+ state.fence.phase !== "fenced"
1469
+ ) {
1470
+ return {
1471
+ result: { kind: "already-issued", fence: cloneFence(state.fence) },
1472
+ changed: false,
1473
+ };
1474
+ }
1475
+ if (!areFencesEqual(state.fence, expected)) {
1476
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1477
+ }
1478
+ if (state.fence.phase !== "fenced") {
1479
+ return {
1480
+ result: { kind: "already-issued", fence: cloneFence(state.fence) },
1481
+ changed: false,
1482
+ };
1483
+ }
1484
+ const deletionIssuedAtMs = getNowMs();
1485
+ const issued: TelegramWorkspaceRetirementFence = {
1486
+ ...state.fence,
1487
+ phase: "deletion-issued",
1488
+ deletionIssuedAtMs,
1489
+ };
1490
+ state.fence = issued;
1491
+ return {
1492
+ result: {
1493
+ kind: "issued",
1494
+ fence: cloneFence(issued),
1495
+ permit: {
1496
+ ...(issued.destructiveKind === undefined ? {} : { destructiveKind: issued.destructiveKind }),
1497
+ operationId: issued.operationId,
1498
+ retirementIntentId: issued.retirementIntentId,
1499
+ profileKey: issued.profileKey,
1500
+ bindingKey: issued.bindingKey,
1501
+ slot: issued.slot,
1502
+ target: { ...issued.target },
1503
+ leaderEpoch: issued.leaderEpoch,
1504
+ issuedAtMs: deletionIssuedAtMs,
1505
+ },
1506
+ },
1507
+ changed: true,
1508
+ };
1509
+ });
1510
+ }
1511
+
1512
+ function confirmRetirementAbsence(
1513
+ expected: TelegramWorkspaceRetirementFence,
1514
+ expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
1515
+ ): TelegramWorkspaceRetirementFence {
1516
+ validateExpectedFence(expected, options.profileKey);
1517
+ if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
1518
+ authorityChanged("Telegram Workspace destructive fence kind changed.");
1519
+ if (!areOwnersEqual(expected.owner, options.owner)) {
1520
+ authorityChanged("Telegram Workspace absence confirmation requires fence owner authority.");
1521
+ }
1522
+ return transact((state) => {
1523
+ if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
1524
+ authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
1525
+ }
1526
+ if (
1527
+ state.fence.operationId === expected.operationId &&
1528
+ areFenceBasesEqual(state.fence, expected) &&
1529
+ state.fence.phase === "commit-ready"
1530
+ ) {
1531
+ return { result: cloneFence(state.fence), changed: false };
1532
+ }
1533
+ if (!areFencesEqual(state.fence, expected)) {
1534
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1535
+ }
1536
+ const ready: TelegramWorkspaceRetirementFence = {
1537
+ ...state.fence,
1538
+ phase: "commit-ready",
1539
+ absenceConfirmedAtMs: getNowMs(),
1540
+ ...(state.fence.phase === "deletion-issued"
1541
+ ? { deletionIssuedAtMs: state.fence.deletionIssuedAtMs }
1542
+ : {}),
1543
+ };
1544
+ state.fence = ready;
1545
+ return { result: cloneFence(ready), changed: true };
1546
+ });
1547
+ }
1548
+
1549
+ function releaseUnissuedRetirementFence(
1550
+ expected: TelegramWorkspaceRetirementFence,
1551
+ expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
1552
+ ): boolean {
1553
+ validateExpectedFence(expected, options.profileKey);
1554
+ if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
1555
+ authorityChanged("Telegram Workspace destructive fence kind changed.");
1556
+ if (!areOwnersEqual(expected.owner, options.owner)) {
1557
+ authorityChanged("Telegram Workspace fence release requires owner authority.");
1558
+ }
1559
+ return transact((state) => {
1560
+ if (!state.fence) return { result: false, changed: false };
1561
+ if (!isTelegramWorkspaceRetirementFence(state.fence))
1562
+ authorityChanged("Telegram Workspace retirement fence has another kind.");
1563
+ if (!areFencesEqual(state.fence, expected)) {
1564
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1565
+ }
1566
+ if (state.fence.phase !== "fenced") {
1567
+ authorityChanged("Issued Telegram Workspace retirement fence cannot be released.");
1568
+ }
1569
+ delete state.fence;
1570
+ return { result: true, changed: true };
1571
+ });
1572
+ }
1573
+
1574
+ function completeRetirementFence(
1575
+ expected: TelegramWorkspaceRetirementFence,
1576
+ expectedKind: TelegramWorkspaceDeletionFenceKind = "pressure-retirement",
1577
+ ): boolean {
1578
+ validateExpectedFence(expected, options.profileKey);
1579
+ if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
1580
+ authorityChanged("Telegram Workspace destructive fence kind changed.");
1581
+ if (!areOwnersEqual(expected.owner, options.owner)) {
1582
+ authorityChanged("Telegram Workspace fence completion requires owner authority.");
1583
+ }
1584
+ return transact((state) => {
1585
+ if (!state.fence) return { result: false, changed: false };
1586
+ if (!isTelegramWorkspaceRetirementFence(state.fence))
1587
+ authorityChanged("Telegram Workspace retirement fence has another kind.");
1588
+ if (!areFencesEqual(state.fence, expected)) {
1589
+ authorityChanged("Telegram Workspace retirement fence authority changed.");
1590
+ }
1591
+ if (state.fence.phase !== "commit-ready") {
1592
+ authorityChanged("Telegram Workspace retirement commit is not ready.");
1593
+ }
1594
+ delete state.fence;
1595
+ return { result: true, changed: true };
1596
+ });
1597
+ }
1598
+
1599
+ function adoptThreadCleanupFence(expected: TelegramWorkspaceRetirementFence,
1600
+ replacement: { owner: TelegramWorkspaceAdmissionOwner; leaderEpoch: number | string }) {
1601
+ return adoptRetirementFence(expected, replacement, "manual-thread-cleanup");
1602
+ }
1603
+ function issueThreadCleanupDeletionPermit(expected: TelegramWorkspaceRetirementFence) {
1604
+ return issueDeletionPermit(expected, "manual-thread-cleanup");
1605
+ }
1606
+ function confirmThreadCleanupAbsence(expected: TelegramWorkspaceRetirementFence) {
1607
+ return confirmRetirementAbsence(expected, "manual-thread-cleanup");
1608
+ }
1609
+ function releaseUnissuedThreadCleanupFence(expected: TelegramWorkspaceRetirementFence) {
1610
+ return releaseUnissuedRetirementFence(expected, "manual-thread-cleanup");
1611
+ }
1612
+ function completeThreadCleanupFence(expected: TelegramWorkspaceRetirementFence) {
1613
+ return completeRetirementFence(expected, "manual-thread-cleanup");
1614
+ }
1615
+
1616
+ return {
1617
+ getProfileKey: () => options.profileKey,
1618
+ getOwner: () => ({ ...options.owner }),
1619
+ listReservedSlots: () => {
1620
+ const fence = read().fence;
1621
+ return fence && isTelegramWorkspaceRetirementFence(fence) ? [fence.slot] : [];
1622
+ },
1623
+ read,
1624
+ acquireAdmission,
1625
+ releaseAdmission,
1626
+ acquireJournalWriterClosure,
1627
+ releaseJournalWriterClosure,
1628
+ installJournalWriterProtocolMode,
1629
+ acquireJournalWriterAdmission,
1630
+ acquireRetirementFence,
1631
+ acquireThreadCleanupFence,
1632
+ adoptRetirementFence,
1633
+ adoptThreadCleanupFence,
1634
+ issueDeletionPermit,
1635
+ issueThreadCleanupDeletionPermit,
1636
+ confirmRetirementAbsence,
1637
+ confirmThreadCleanupAbsence,
1638
+ releaseUnissuedRetirementFence,
1639
+ releaseUnissuedThreadCleanupFence,
1640
+ completeRetirementFence,
1641
+ completeThreadCleanupFence,
1642
+ };
1643
+ }