@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.
- package/AGENTS.md +9 -7
- package/BACKLOG.md +1 -18
- package/CHANGELOG.md +11 -0
- package/README.md +7 -11
- package/api/commands.ts +3 -0
- package/dist/api/commands.d.ts +1 -1
- package/dist/lib/activity-verbosity.d.ts +1 -5
- package/dist/lib/activity-verbosity.js +15 -42
- package/dist/lib/activity.d.ts +9 -0
- package/dist/lib/activity.js +39 -10
- package/dist/lib/agent-messages.js +7 -3
- package/dist/lib/bindings.d.ts +43 -5
- package/dist/lib/bindings.js +163 -20
- package/dist/lib/bus-api.d.ts +12 -0
- package/dist/lib/bus-api.js +182 -79
- package/dist/lib/bus-follower.d.ts +111 -31
- package/dist/lib/bus-follower.js +1324 -359
- package/dist/lib/bus-leader.d.ts +41 -91
- package/dist/lib/bus-leader.js +969 -637
- package/dist/lib/bus-transport.js +16 -9
- package/dist/lib/bus.d.ts +215 -18
- package/dist/lib/bus.js +1048 -327
- package/dist/lib/channel-posts.d.ts +5 -0
- package/dist/lib/channel-posts.js +434 -261
- package/dist/lib/command-templates.d.ts +26 -5
- package/dist/lib/command-templates.js +92 -176
- package/dist/lib/commands.d.ts +168 -82
- package/dist/lib/commands.js +1409 -291
- package/dist/lib/config.d.ts +1 -8
- package/dist/lib/config.js +34 -76
- package/dist/lib/delivery.d.ts +2 -2
- package/dist/lib/delivery.js +16 -16
- package/dist/lib/extension.js +639 -171
- package/dist/lib/generative-app-worker.mjs +19 -7
- package/dist/lib/generative-apps.d.ts +0 -5
- package/dist/lib/generative-apps.js +61 -49
- package/dist/lib/inbound.d.ts +5 -0
- package/dist/lib/inbound.js +35 -59
- package/dist/lib/journal.d.ts +33 -3
- package/dist/lib/journal.js +2063 -782
- package/dist/lib/lifecycle.d.ts +2 -6
- package/dist/lib/lifecycle.js +75 -58
- package/dist/lib/locks.d.ts +29 -3
- package/dist/lib/locks.js +289 -108
- package/dist/lib/logging.js +50 -14
- package/dist/lib/media.d.ts +22 -1
- package/dist/lib/media.js +46 -28
- package/dist/lib/menu-model.d.ts +19 -5
- package/dist/lib/menu-model.js +53 -11
- package/dist/lib/menu-queue.d.ts +5 -4
- package/dist/lib/menu-queue.js +45 -63
- package/dist/lib/menu-settings.d.ts +5 -4
- package/dist/lib/menu-settings.js +114 -78
- package/dist/lib/menu-status.d.ts +1 -1
- package/dist/lib/menu-status.js +8 -25
- package/dist/lib/menu-thinking.d.ts +6 -1
- package/dist/lib/menu-thinking.js +14 -21
- package/dist/lib/menu.d.ts +8 -61
- package/dist/lib/menu.js +92 -164
- package/dist/lib/model.d.ts +1 -1
- package/dist/lib/model.js +2 -16
- package/dist/lib/outbound-attachments.d.ts +6 -27
- package/dist/lib/outbound-attachments.js +45 -38
- package/dist/lib/outbound-buttons.js +22 -16
- package/dist/lib/outbound-markup.d.ts +0 -2
- package/dist/lib/outbound-markup.js +51 -64
- package/dist/lib/outbound-voice.js +6 -3
- package/dist/lib/outbound.d.ts +6 -6
- package/dist/lib/outbound.js +61 -51
- package/dist/lib/ownership.js +6 -2
- package/dist/lib/paths.d.ts +5 -0
- package/dist/lib/paths.js +37 -21
- package/dist/lib/pi.d.ts +2 -0
- package/dist/lib/pi.js +8 -1
- package/dist/lib/polling.d.ts +21 -37
- package/dist/lib/polling.js +59 -50
- package/dist/lib/preview.js +39 -19
- package/dist/lib/process-identity.d.ts +24 -0
- package/dist/lib/process-identity.js +133 -0
- package/dist/lib/prompts.js +2 -7
- package/dist/lib/queue.d.ts +14 -16
- package/dist/lib/queue.js +249 -126
- package/dist/lib/recovery.d.ts +2 -83
- package/dist/lib/recovery.js +20 -274
- package/dist/lib/replies.d.ts +16 -24
- package/dist/lib/replies.js +99 -116
- package/dist/lib/routing.d.ts +120 -7
- package/dist/lib/routing.js +4667 -1152
- package/dist/lib/runtime.d.ts +1 -11
- package/dist/lib/sections.d.ts +4 -29
- package/dist/lib/sections.js +7 -20
- package/dist/lib/skills.js +4 -3
- package/dist/lib/status.js +86 -57
- package/dist/lib/sync.d.ts +1 -3
- package/dist/lib/sync.js +67 -43
- package/dist/lib/target.d.ts +4 -0
- package/dist/lib/target.js +18 -0
- package/dist/lib/telegram-api.d.ts +18 -13
- package/dist/lib/telegram-api.js +148 -53
- package/dist/lib/text-groups.js +4 -39
- package/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/dist/lib/thread-cleanup-manager.js +369 -186
- package/dist/lib/thread-display.d.ts +1 -2
- package/dist/lib/thread-display.js +58 -38
- package/dist/lib/thread-naming.d.ts +28 -8
- package/dist/lib/thread-naming.js +175 -66
- package/dist/lib/thread-reconciler.d.ts +42 -16
- package/dist/lib/thread-reconciler.js +90 -50
- package/dist/lib/threads.d.ts +82 -20
- package/dist/lib/threads.js +2247 -855
- package/dist/lib/time-injection.js +1 -2
- package/dist/lib/turns.js +1 -19
- package/dist/lib/updates.d.ts +169 -80
- package/dist/lib/updates.js +2418 -641
- package/dist/lib/voice.d.ts +7 -22
- package/dist/lib/voice.js +7 -22
- package/dist/lib/wire.js +1 -1
- package/dist/lib/workspace-admission.d.ts +3 -1
- package/dist/lib/workspace-admission.js +425 -211
- package/dist/lib/workspace-identity.js +8 -13
- package/dist/lib/workspace-retirement.d.ts +1 -1
- package/dist/lib/workspace-retirement.js +328 -140
- package/dist/lib/workspace-slots.js +8 -4
- package/dist/package.json +1 -1
- package/docs/architecture.md +31 -29
- package/docs/inbound.md +1 -1
- package/docs/multi-instance-bus.md +15 -3
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +29 -1
- package/docs/ui-style.md +25 -8
- package/lib/activity-verbosity.ts +59 -82
- package/lib/activity.ts +146 -102
- package/lib/agent-messages.ts +12 -5
- package/lib/bindings.ts +389 -82
- package/lib/bus-api.ts +263 -102
- package/lib/bus-follower.ts +2440 -601
- package/lib/bus-leader.ts +2209 -1290
- package/lib/bus-transport.ts +46 -12
- package/lib/bus.ts +1673 -444
- package/lib/channel-posts.ts +848 -327
- package/lib/command-templates.ts +152 -265
- package/lib/commands.ts +2398 -555
- package/lib/config.ts +177 -170
- package/lib/delivery.ts +41 -29
- package/lib/extension.ts +1206 -433
- package/lib/generative-app-worker.mjs +19 -7
- package/lib/generative-apps.ts +324 -211
- package/lib/inbound.ts +95 -97
- package/lib/journal.ts +5311 -1763
- package/lib/keyboard.ts +1 -3
- package/lib/lifecycle.ts +128 -78
- package/lib/locks.ts +677 -203
- package/lib/logging.ts +147 -58
- package/lib/media.ts +82 -43
- package/lib/menu-model.ts +85 -18
- package/lib/menu-queue.ts +88 -129
- package/lib/menu-settings.ts +260 -155
- package/lib/menu-status.ts +20 -60
- package/lib/menu-thinking.ts +41 -56
- package/lib/menu.ts +112 -285
- package/lib/model.ts +8 -22
- package/lib/outbound-attachments.ts +156 -125
- package/lib/outbound-buttons.ts +76 -49
- package/lib/outbound-markup.ts +96 -94
- package/lib/outbound-voice.ts +33 -27
- package/lib/outbound.ts +125 -125
- package/lib/ownership.ts +19 -10
- package/lib/paths.ts +148 -49
- package/lib/pi.ts +35 -15
- package/lib/polling.ts +144 -147
- package/lib/preview.ts +93 -40
- package/lib/process-identity.ts +164 -0
- package/lib/prompts.ts +4 -11
- package/lib/queue.ts +511 -300
- package/lib/recovery.ts +50 -454
- package/lib/rendering.ts +1 -3
- package/lib/replies.ts +248 -209
- package/lib/routing.ts +8509 -2186
- package/lib/runtime.ts +1 -12
- package/lib/sections.ts +24 -89
- package/lib/setup.ts +1 -3
- package/lib/skills.ts +9 -4
- package/lib/status.ts +215 -110
- package/lib/sync.ts +448 -361
- package/lib/target.ts +22 -3
- package/lib/telegram-api.ts +302 -127
- package/lib/text-groups.ts +5 -39
- package/lib/thread-cleanup-manager.ts +833 -331
- package/lib/thread-display.ts +213 -97
- package/lib/thread-naming.ts +232 -87
- package/lib/thread-reconciler.ts +155 -72
- package/lib/threads.ts +5318 -1845
- package/lib/time-injection.ts +1 -4
- package/lib/turns.ts +15 -28
- package/lib/updates.ts +5184 -1612
- package/lib/voice.ts +7 -35
- package/lib/wire.ts +1 -1
- package/lib/workspace-admission.ts +996 -416
- package/lib/workspace-identity.ts +14 -17
- package/lib/workspace-retirement.ts +878 -376
- package/lib/workspace-slots.ts +22 -13
- package/package.json +1 -1
- package/scripts/check-downgrade.mjs +0 -472
- package/scripts/measure-workspace.mjs +0 -108
|
@@ -8,49 +8,86 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
8
8
|
import { chmodSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
9
9
|
import { dirname } from "node:path";
|
|
10
10
|
import { isDeepStrictEqual } from "node:util";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { areTelegramTargetsEqual as areTargetsEqual } from "./target.js";
|
|
14
|
-
import { isWireRecord as isRecord, isNonNegativeWireInteger as isSafeTimestamp } from "./wire.js";
|
|
11
|
+
import { mutateTelegramRuntimeStateSection, readTelegramRuntimeState, renameTelegramPathWithRetry, TelegramRuntimeStateError, withTelegramFileTransaction, } from "./locks.js";
|
|
12
|
+
import { getTelegramProcessLiveness, } from "./process-identity.js";
|
|
13
|
+
import { areTelegramTargetsEqual as areTargetsEqual, } from "./target.js";
|
|
14
|
+
import { isWireRecord as isRecord, isNonNegativeWireInteger as isSafeTimestamp, } from "./wire.js";
|
|
15
15
|
const TELEGRAM_WORKSPACE_ADMISSION_VERSION = 1;
|
|
16
16
|
const TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES = 4096;
|
|
17
17
|
const TELEGRAM_WORKSPACE_ADMISSION_MAX_TEXT = 512;
|
|
18
18
|
const activeTelegramWorkspaceAdmissionOperationIds = new Set();
|
|
19
19
|
export function normalizeTelegramWorkspaceJournalWriterClosureFence(value, profileKey) {
|
|
20
|
-
if (!isRecord(value) ||
|
|
21
|
-
value.
|
|
22
|
-
value.
|
|
23
|
-
!
|
|
20
|
+
if (!isRecord(value) ||
|
|
21
|
+
value.destructiveKind !== "journal-writer-closure" ||
|
|
22
|
+
value.phase !== "fenced" ||
|
|
23
|
+
!isBoundedText(value.operationId) ||
|
|
24
|
+
value.profileKey !== profileKey ||
|
|
25
|
+
!isBoundedText(value.recoveryKey) ||
|
|
26
|
+
!isSafeTimestamp(value.requestedAtMs) ||
|
|
27
|
+
!isSafeTimestamp(value.acquiredAtMs))
|
|
24
28
|
return undefined;
|
|
25
29
|
const owner = normalizeOwner(value.owner);
|
|
26
|
-
if (!owner ||
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
if (!owner ||
|
|
31
|
+
Object.keys(value).some((key) => ![
|
|
32
|
+
"destructiveKind",
|
|
33
|
+
"phase",
|
|
34
|
+
"operationId",
|
|
35
|
+
"profileKey",
|
|
36
|
+
"recoveryKey",
|
|
37
|
+
"owner",
|
|
38
|
+
"requestedAtMs",
|
|
39
|
+
"acquiredAtMs",
|
|
40
|
+
].includes(key)))
|
|
30
41
|
return undefined;
|
|
31
|
-
return {
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
return {
|
|
43
|
+
destructiveKind: "journal-writer-closure",
|
|
44
|
+
phase: "fenced",
|
|
45
|
+
operationId: value.operationId,
|
|
46
|
+
profileKey,
|
|
47
|
+
recoveryKey: value.recoveryKey,
|
|
48
|
+
owner,
|
|
49
|
+
requestedAtMs: value.requestedAtMs,
|
|
50
|
+
acquiredAtMs: value.acquiredAtMs,
|
|
51
|
+
};
|
|
34
52
|
}
|
|
35
53
|
export function normalizeTelegramWorkspaceJournalWriterProtocolMode(value, profileKey) {
|
|
36
54
|
if (!isRecord(value))
|
|
37
55
|
return undefined;
|
|
38
|
-
const keys = [
|
|
39
|
-
"
|
|
56
|
+
const keys = [
|
|
57
|
+
"version",
|
|
58
|
+
"protocol",
|
|
59
|
+
"profileKey",
|
|
60
|
+
"recoveryKey",
|
|
61
|
+
"startupAuthorityId",
|
|
62
|
+
"closureOperationId",
|
|
63
|
+
"writerInventorySha256",
|
|
64
|
+
"installedBy",
|
|
65
|
+
"installedAtMs",
|
|
66
|
+
];
|
|
40
67
|
const installedBy = normalizeOwner(value.installedBy);
|
|
41
|
-
if (Object.keys(value).some(key => !keys.includes(key)) ||
|
|
42
|
-
value.
|
|
43
|
-
|
|
68
|
+
if (Object.keys(value).some((key) => !keys.includes(key)) ||
|
|
69
|
+
value.version !== 1 ||
|
|
70
|
+
value.protocol !== "custody-v3" ||
|
|
71
|
+
value.profileKey !== profileKey ||
|
|
72
|
+
!isBoundedText(value.recoveryKey) ||
|
|
73
|
+
!isBoundedText(value.startupAuthorityId) ||
|
|
44
74
|
!isBoundedText(value.closureOperationId) ||
|
|
45
75
|
typeof value.writerInventorySha256 !== "string" ||
|
|
46
|
-
!/^[a-f0-9]{64}$/u.test(value.writerInventorySha256) ||
|
|
76
|
+
!/^[a-f0-9]{64}$/u.test(value.writerInventorySha256) ||
|
|
77
|
+
!installedBy ||
|
|
47
78
|
!isSafeTimestamp(value.installedAtMs))
|
|
48
79
|
return undefined;
|
|
49
|
-
return {
|
|
50
|
-
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
protocol: "custody-v3",
|
|
83
|
+
profileKey,
|
|
84
|
+
recoveryKey: value.recoveryKey,
|
|
85
|
+
startupAuthorityId: value.startupAuthorityId,
|
|
51
86
|
closureOperationId: value.closureOperationId,
|
|
52
87
|
writerInventorySha256: value.writerInventorySha256,
|
|
53
|
-
installedBy,
|
|
88
|
+
installedBy,
|
|
89
|
+
installedAtMs: value.installedAtMs,
|
|
90
|
+
};
|
|
54
91
|
}
|
|
55
92
|
export function isTelegramWorkspaceRetirementFence(fence) {
|
|
56
93
|
return fence.destructiveKind !== "journal-writer-closure";
|
|
@@ -166,11 +203,14 @@ function normalizeFence(value, profileKey) {
|
|
|
166
203
|
!isSafeTimestamp(value.acquiredAtMs)) {
|
|
167
204
|
return undefined;
|
|
168
205
|
}
|
|
169
|
-
if (value.destructiveKind !== undefined &&
|
|
206
|
+
if (value.destructiveKind !== undefined &&
|
|
207
|
+
value.destructiveKind !== "pressure-retirement" &&
|
|
170
208
|
value.destructiveKind !== "manual-thread-cleanup")
|
|
171
209
|
return undefined;
|
|
172
210
|
const base = {
|
|
173
|
-
...(value.destructiveKind === undefined
|
|
211
|
+
...(value.destructiveKind === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { destructiveKind: value.destructiveKind }),
|
|
174
214
|
operationId: value.operationId,
|
|
175
215
|
retirementIntentId: value.retirementIntentId,
|
|
176
216
|
profileKey,
|
|
@@ -193,11 +233,20 @@ function normalizeFence(value, profileKey) {
|
|
|
193
233
|
};
|
|
194
234
|
}
|
|
195
235
|
if (value.phase === "deletion-rejected" &&
|
|
196
|
-
(value.destructiveKind === undefined ||
|
|
197
|
-
|
|
236
|
+
(value.destructiveKind === undefined ||
|
|
237
|
+
value.destructiveKind === "pressure-retirement") &&
|
|
238
|
+
isSafeTimestamp(value.deletionIssuedAtMs) &&
|
|
239
|
+
isSafeTimestamp(value.rejectionConfirmedAtMs)) {
|
|
198
240
|
const { destructiveKind: _kind, ...pressureBase } = base;
|
|
199
|
-
return {
|
|
200
|
-
|
|
241
|
+
return {
|
|
242
|
+
...pressureBase,
|
|
243
|
+
...(value.destructiveKind === "pressure-retirement"
|
|
244
|
+
? { destructiveKind: "pressure-retirement" }
|
|
245
|
+
: {}),
|
|
246
|
+
phase: "deletion-rejected",
|
|
247
|
+
deletionIssuedAtMs: value.deletionIssuedAtMs,
|
|
248
|
+
rejectionConfirmedAtMs: value.rejectionConfirmedAtMs,
|
|
249
|
+
};
|
|
201
250
|
}
|
|
202
251
|
if (value.phase === "commit-ready" &&
|
|
203
252
|
isSafeTimestamp(value.absenceConfirmedAtMs) &&
|
|
@@ -220,7 +269,11 @@ function cloneScope(scope) {
|
|
|
220
269
|
: { ...scope };
|
|
221
270
|
}
|
|
222
271
|
function cloneLease(lease) {
|
|
223
|
-
return {
|
|
272
|
+
return {
|
|
273
|
+
...lease,
|
|
274
|
+
scope: cloneScope(lease.scope),
|
|
275
|
+
owner: { ...lease.owner },
|
|
276
|
+
};
|
|
224
277
|
}
|
|
225
278
|
export function resolveTelegramWorkspaceDestructiveFenceKind(fence) {
|
|
226
279
|
return fence.destructiveKind ?? "pressure-retirement";
|
|
@@ -259,8 +312,14 @@ function isConsolidatedAdmissionKey(value, profile) {
|
|
|
259
312
|
return false;
|
|
260
313
|
try {
|
|
261
314
|
const key = JSON.parse(value);
|
|
262
|
-
return isRecord(key) &&
|
|
263
|
-
|
|
315
|
+
return (isRecord(key) &&
|
|
316
|
+
Object.keys(key).length === 3 &&
|
|
317
|
+
key.version === TELEGRAM_WORKSPACE_ADMISSION_VERSION &&
|
|
318
|
+
key.profile === profile &&
|
|
319
|
+
isRecord(key.bot) &&
|
|
320
|
+
Object.keys(key.bot).length === 1 &&
|
|
321
|
+
typeof key.bot.tokenSha256 === "string" &&
|
|
322
|
+
/^[a-f0-9]{64}$/u.test(key.bot.tokenSha256));
|
|
264
323
|
}
|
|
265
324
|
catch {
|
|
266
325
|
return false;
|
|
@@ -270,15 +329,28 @@ function isConsolidatedAdmissionKey(value, profile) {
|
|
|
270
329
|
export function assertTelegramConsolidatedAdmissionSection(value, stateProfile) {
|
|
271
330
|
if (value === undefined)
|
|
272
331
|
return;
|
|
273
|
-
normalizeAdmissionState(value, isRecord(value) && typeof value.profileKey === "string"
|
|
332
|
+
normalizeAdmissionState(value, isRecord(value) && typeof value.profileKey === "string"
|
|
333
|
+
? value.profileKey
|
|
334
|
+
: "", stateProfile);
|
|
274
335
|
}
|
|
275
336
|
function normalizeAdmissionState(parsed, profileKey, stateProfile) {
|
|
276
337
|
if (parsed === undefined)
|
|
277
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
340
|
+
profileKey,
|
|
341
|
+
leases: [],
|
|
342
|
+
};
|
|
278
343
|
if (!isRecord(parsed))
|
|
279
344
|
invalidState("Telegram Workspace admission state is invalid.");
|
|
280
|
-
if (stateProfile !== undefined &&
|
|
281
|
-
|
|
345
|
+
if (stateProfile !== undefined &&
|
|
346
|
+
(Object.keys(parsed).some((key) => ![
|
|
347
|
+
"version",
|
|
348
|
+
"profileKey",
|
|
349
|
+
"leases",
|
|
350
|
+
"fence",
|
|
351
|
+
"writerProtocolMode",
|
|
352
|
+
].includes(key)) ||
|
|
353
|
+
!isConsolidatedAdmissionKey(parsed.profileKey, stateProfile)))
|
|
282
354
|
invalidState("Telegram consolidated admission section identity is invalid.");
|
|
283
355
|
if (parsed.version !== TELEGRAM_WORKSPACE_ADMISSION_VERSION ||
|
|
284
356
|
!Array.isArray(parsed.leases) ||
|
|
@@ -286,7 +358,8 @@ function normalizeAdmissionState(parsed, profileKey, stateProfile) {
|
|
|
286
358
|
invalidState("Telegram Workspace admission state authority is invalid.");
|
|
287
359
|
}
|
|
288
360
|
if (parsed.profileKey !== profileKey) {
|
|
289
|
-
if (parsed.leases.length === 0 &&
|
|
361
|
+
if (parsed.leases.length === 0 &&
|
|
362
|
+
parsed.fence === undefined &&
|
|
290
363
|
parsed.writerProtocolMode === undefined) {
|
|
291
364
|
return {
|
|
292
365
|
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
@@ -301,21 +374,27 @@ function normalizeAdmissionState(parsed, profileKey, stateProfile) {
|
|
|
301
374
|
invalidState("Telegram Workspace admission lease is invalid.");
|
|
302
375
|
}
|
|
303
376
|
const normalizedLeases = leases;
|
|
304
|
-
if (new Set(normalizedLeases.map((lease) => lease.operationId)).size !==
|
|
377
|
+
if (new Set(normalizedLeases.map((lease) => lease.operationId)).size !==
|
|
378
|
+
normalizedLeases.length) {
|
|
305
379
|
invalidState("Telegram Workspace admission lease identity is duplicated.");
|
|
306
380
|
}
|
|
307
|
-
const fence = parsed.fence === undefined
|
|
381
|
+
const fence = parsed.fence === undefined
|
|
382
|
+
? undefined
|
|
383
|
+
: normalizeFence(parsed.fence, profileKey);
|
|
308
384
|
if (parsed.fence !== undefined && !fence) {
|
|
309
385
|
invalidState("Telegram Workspace retirement fence is invalid.");
|
|
310
386
|
}
|
|
311
|
-
const writerProtocolMode = parsed.writerProtocolMode === undefined
|
|
312
|
-
|
|
387
|
+
const writerProtocolMode = parsed.writerProtocolMode === undefined
|
|
388
|
+
? undefined
|
|
389
|
+
: normalizeTelegramWorkspaceJournalWriterProtocolMode(parsed.writerProtocolMode, profileKey);
|
|
313
390
|
if (parsed.writerProtocolMode !== undefined && !writerProtocolMode)
|
|
314
391
|
invalidState("Telegram Workspace writer protocol mode is invalid.");
|
|
315
392
|
if (fence && writerProtocolMode)
|
|
316
393
|
invalidState("Telegram Workspace destructive fence conflicts with writer protocol mode.");
|
|
317
|
-
if (stateProfile !== undefined &&
|
|
318
|
-
!isDeepStrictEqual(parsed.
|
|
394
|
+
if (stateProfile !== undefined &&
|
|
395
|
+
(!isDeepStrictEqual(parsed.leases, normalizedLeases) ||
|
|
396
|
+
!isDeepStrictEqual(parsed.fence, fence) ||
|
|
397
|
+
!isDeepStrictEqual(parsed.writerProtocolMode, writerProtocolMode)))
|
|
319
398
|
invalidState("Telegram consolidated admission evidence cannot drop unknown fields.");
|
|
320
399
|
return {
|
|
321
400
|
version: TELEGRAM_WORKSPACE_ADMISSION_VERSION,
|
|
@@ -403,30 +482,35 @@ function areFenceBasesEqual(left, right) {
|
|
|
403
482
|
left.acquiredAtMs === right.acquiredAtMs);
|
|
404
483
|
}
|
|
405
484
|
function areWriterProtocolModesEqual(left, right) {
|
|
406
|
-
return left.protocol === right.protocol &&
|
|
485
|
+
return (left.protocol === right.protocol &&
|
|
486
|
+
left.profileKey === right.profileKey &&
|
|
407
487
|
left.recoveryKey === right.recoveryKey &&
|
|
408
488
|
left.startupAuthorityId === right.startupAuthorityId &&
|
|
409
489
|
left.closureOperationId === right.closureOperationId &&
|
|
410
490
|
left.writerInventorySha256 === right.writerInventorySha256 &&
|
|
411
|
-
areOwnersEqual(left.installedBy, right.installedBy) &&
|
|
491
|
+
areOwnersEqual(left.installedBy, right.installedBy) &&
|
|
492
|
+
left.installedAtMs === right.installedAtMs);
|
|
412
493
|
}
|
|
413
494
|
function areWriterClosureFencesEqual(left, right) {
|
|
414
|
-
return left.operationId === right.operationId &&
|
|
415
|
-
left.
|
|
416
|
-
left.
|
|
495
|
+
return (left.operationId === right.operationId &&
|
|
496
|
+
left.profileKey === right.profileKey &&
|
|
497
|
+
left.recoveryKey === right.recoveryKey &&
|
|
498
|
+
areOwnersEqual(left.owner, right.owner) &&
|
|
499
|
+
left.requestedAtMs === right.requestedAtMs &&
|
|
500
|
+
left.acquiredAtMs === right.acquiredAtMs);
|
|
417
501
|
}
|
|
418
502
|
function areFencesEqual(left, right) {
|
|
419
503
|
if (!areFenceBasesEqual(left, right) || left.phase !== right.phase)
|
|
420
504
|
return false;
|
|
421
505
|
if (left.phase === "fenced" && right.phase === "fenced")
|
|
422
506
|
return true;
|
|
423
|
-
if (left.phase === "deletion-issued" &&
|
|
424
|
-
right.phase === "deletion-issued") {
|
|
507
|
+
if (left.phase === "deletion-issued" && right.phase === "deletion-issued") {
|
|
425
508
|
return left.deletionIssuedAtMs === right.deletionIssuedAtMs;
|
|
426
509
|
}
|
|
427
|
-
if (left.phase === "deletion-rejected" &&
|
|
428
|
-
|
|
429
|
-
|
|
510
|
+
if (left.phase === "deletion-rejected" &&
|
|
511
|
+
right.phase === "deletion-rejected") {
|
|
512
|
+
return (left.deletionIssuedAtMs === right.deletionIssuedAtMs &&
|
|
513
|
+
left.rejectionConfirmedAtMs === right.rejectionConfirmedAtMs);
|
|
430
514
|
}
|
|
431
515
|
return (left.phase === "commit-ready" &&
|
|
432
516
|
right.phase === "commit-ready" &&
|
|
@@ -443,7 +527,9 @@ function validateOptions(options) {
|
|
|
443
527
|
if (!normalizeOwner(options.owner)) {
|
|
444
528
|
invalidInput("Telegram Workspace admission owner is invalid.");
|
|
445
529
|
}
|
|
446
|
-
if (options.stateProfile !== undefined &&
|
|
530
|
+
if (options.stateProfile !== undefined &&
|
|
531
|
+
(!isBoundedText(options.stateProfile) ||
|
|
532
|
+
!isConsolidatedAdmissionKey(options.profileKey, options.stateProfile)))
|
|
447
533
|
invalidInput("Telegram consolidated admission requires an exact bot/profile section identity.");
|
|
448
534
|
}
|
|
449
535
|
function validateOperationId(operationId) {
|
|
@@ -498,7 +584,9 @@ export function createTelegramWorkspaceAdmissionRuntimeBinding(input) {
|
|
|
498
584
|
});
|
|
499
585
|
return createTelegramWorkspaceAdmissionLedger({
|
|
500
586
|
path: getStatePath ? getStatePath() : getPath(configuredProfileName),
|
|
501
|
-
...(getStatePath
|
|
587
|
+
...(getStatePath
|
|
588
|
+
? { stateProfile: configuredProfileName ?? "default" }
|
|
589
|
+
: {}),
|
|
502
590
|
profileKey,
|
|
503
591
|
owner: input.owner,
|
|
504
592
|
getNowMs: input.getNowMs,
|
|
@@ -507,7 +595,8 @@ export function createTelegramWorkspaceAdmissionRuntimeBinding(input) {
|
|
|
507
595
|
},
|
|
508
596
|
};
|
|
509
597
|
}
|
|
510
|
-
|
|
598
|
+
/** Stable admission-scope identity used by leases and admission-gated journal operations. */
|
|
599
|
+
export function getTelegramWorkspaceAdmissionScopeKey(scope) {
|
|
511
600
|
if (scope.kind === "profile")
|
|
512
601
|
return "profile";
|
|
513
602
|
if (scope.kind === "chat")
|
|
@@ -523,7 +612,8 @@ function reserveTelegramWorkspaceAdmissionOperationId(operationId) {
|
|
|
523
612
|
activeTelegramWorkspaceAdmissionOperationIds.delete(operationId);
|
|
524
613
|
};
|
|
525
614
|
}
|
|
526
|
-
|
|
615
|
+
/** Validated, de-duplicated scopes in the stable key order every admitted operation acquires. */
|
|
616
|
+
function normalizeTelegramWorkspaceAdmittedScopes(input) {
|
|
527
617
|
validateOperationId(input.operationId);
|
|
528
618
|
if (!isBoundedText(input.operationKind) || input.scopes.length === 0) {
|
|
529
619
|
invalidInput("Telegram Workspace admitted operation is invalid.");
|
|
@@ -531,31 +621,38 @@ export function runWithTelegramWorkspaceAdmissions(input) {
|
|
|
531
621
|
const uniqueScopes = new Map();
|
|
532
622
|
for (const scope of input.scopes) {
|
|
533
623
|
validateScope(scope);
|
|
534
|
-
uniqueScopes.set(
|
|
624
|
+
uniqueScopes.set(getTelegramWorkspaceAdmissionScopeKey(scope), cloneScope(scope));
|
|
535
625
|
}
|
|
536
|
-
|
|
626
|
+
return Array.from(uniqueScopes.entries())
|
|
537
627
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
538
628
|
.map(([, scope]) => scope);
|
|
629
|
+
}
|
|
630
|
+
/** Appends each lease as it is acquired so a blocked or failed scope leaves earlier leases releasable. */
|
|
631
|
+
function acquireTelegramWorkspaceAdmittedScopes(input, scopes, acquired) {
|
|
632
|
+
for (const scope of scopes) {
|
|
633
|
+
const scopeOperationId = `admission:${createHash("sha256")
|
|
634
|
+
.update(input.operationId)
|
|
635
|
+
.update("\0")
|
|
636
|
+
.update(getTelegramWorkspaceAdmissionScopeKey(scope))
|
|
637
|
+
.digest("hex")}`;
|
|
638
|
+
const result = input.ledger.acquireAdmission({
|
|
639
|
+
operationId: scopeOperationId,
|
|
640
|
+
operationKind: input.operationKind,
|
|
641
|
+
scope,
|
|
642
|
+
});
|
|
643
|
+
if (result.kind === "blocked") {
|
|
644
|
+
throw new TelegramWorkspaceAdmissionError("admission-blocked", "Telegram Workspace operation is blocked by retirement.");
|
|
645
|
+
}
|
|
646
|
+
acquired.push(result.lease);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
export function runWithTelegramWorkspaceAdmissions(input) {
|
|
650
|
+
const scopes = normalizeTelegramWorkspaceAdmittedScopes(input);
|
|
539
651
|
const releaseActiveOperation = reserveTelegramWorkspaceAdmissionOperationId(input.operationId);
|
|
540
652
|
const acquired = [];
|
|
541
653
|
let outcome;
|
|
542
654
|
try {
|
|
543
|
-
|
|
544
|
-
const scopeOperationId = `admission:${createHash("sha256")
|
|
545
|
-
.update(input.operationId)
|
|
546
|
-
.update("\0")
|
|
547
|
-
.update(getAdmissionScopeKey(scope))
|
|
548
|
-
.digest("hex")}`;
|
|
549
|
-
const result = input.ledger.acquireAdmission({
|
|
550
|
-
operationId: scopeOperationId,
|
|
551
|
-
operationKind: input.operationKind,
|
|
552
|
-
scope,
|
|
553
|
-
});
|
|
554
|
-
if (result.kind === "blocked") {
|
|
555
|
-
throw new TelegramWorkspaceAdmissionError("admission-blocked", "Telegram Workspace operation is blocked by retirement.");
|
|
556
|
-
}
|
|
557
|
-
acquired.push(result.lease);
|
|
558
|
-
}
|
|
655
|
+
acquireTelegramWorkspaceAdmittedScopes(input, scopes, acquired);
|
|
559
656
|
outcome = { ok: true, value: input.operation() };
|
|
560
657
|
}
|
|
561
658
|
catch (error) {
|
|
@@ -584,14 +681,21 @@ export function createTelegramWorkspaceJournalWriterAdmission(input) {
|
|
|
584
681
|
return (operation) => {
|
|
585
682
|
if (active)
|
|
586
683
|
invalidState("Telegram Workspace journal writer admission is already active.");
|
|
587
|
-
const operationId = pendingOperationId ??
|
|
684
|
+
const operationId = pendingOperationId ??
|
|
685
|
+
input.createOperationId?.() ??
|
|
588
686
|
`journal-writer:${randomUUID()}`;
|
|
589
687
|
validateOperationId(operationId);
|
|
590
688
|
pendingOperationId = operationId;
|
|
591
689
|
const result = input.protocolAuthority
|
|
592
|
-
? input.ledger.acquireJournalWriterAdmission({
|
|
593
|
-
|
|
594
|
-
|
|
690
|
+
? input.ledger.acquireJournalWriterAdmission({
|
|
691
|
+
operationId,
|
|
692
|
+
...input.protocolAuthority,
|
|
693
|
+
})
|
|
694
|
+
: input.ledger.acquireAdmission({
|
|
695
|
+
operationId,
|
|
696
|
+
operationKind: "journal-write",
|
|
697
|
+
scope: { kind: "profile" },
|
|
698
|
+
});
|
|
595
699
|
if (result.kind === "blocked") {
|
|
596
700
|
throw new TelegramWorkspaceAdmissionError("admission-blocked", "Telegram Workspace journal writer is blocked by destructive closure.");
|
|
597
701
|
}
|
|
@@ -610,24 +714,15 @@ export function createTelegramWorkspaceJournalWriterAdmission(input) {
|
|
|
610
714
|
try {
|
|
611
715
|
input.onReleaseError?.(error);
|
|
612
716
|
}
|
|
613
|
-
catch {
|
|
717
|
+
catch {
|
|
718
|
+
/* Diagnostics cannot change the journal outcome. */
|
|
719
|
+
}
|
|
614
720
|
}
|
|
615
721
|
}
|
|
616
722
|
};
|
|
617
723
|
}
|
|
618
724
|
export async function runWithTelegramWorkspaceAdmissionsAsync(input) {
|
|
619
|
-
|
|
620
|
-
if (!isBoundedText(input.operationKind) || input.scopes.length === 0) {
|
|
621
|
-
invalidInput("Telegram Workspace admitted operation is invalid.");
|
|
622
|
-
}
|
|
623
|
-
const uniqueScopes = new Map();
|
|
624
|
-
for (const scope of input.scopes) {
|
|
625
|
-
validateScope(scope);
|
|
626
|
-
uniqueScopes.set(getAdmissionScopeKey(scope), cloneScope(scope));
|
|
627
|
-
}
|
|
628
|
-
const scopes = Array.from(uniqueScopes.entries())
|
|
629
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
630
|
-
.map(([, scope]) => scope);
|
|
725
|
+
const scopes = normalizeTelegramWorkspaceAdmittedScopes(input);
|
|
631
726
|
const releaseActiveOperation = reserveTelegramWorkspaceAdmissionOperationId(input.operationId);
|
|
632
727
|
const acquired = [];
|
|
633
728
|
const releaseAll = () => {
|
|
@@ -648,22 +743,7 @@ export async function runWithTelegramWorkspaceAdmissionsAsync(input) {
|
|
|
648
743
|
}
|
|
649
744
|
};
|
|
650
745
|
try {
|
|
651
|
-
|
|
652
|
-
const scopeOperationId = `admission:${createHash("sha256")
|
|
653
|
-
.update(input.operationId)
|
|
654
|
-
.update("\0")
|
|
655
|
-
.update(getAdmissionScopeKey(scope))
|
|
656
|
-
.digest("hex")}`;
|
|
657
|
-
const result = input.ledger.acquireAdmission({
|
|
658
|
-
operationId: scopeOperationId,
|
|
659
|
-
operationKind: input.operationKind,
|
|
660
|
-
scope,
|
|
661
|
-
});
|
|
662
|
-
if (result.kind === "blocked") {
|
|
663
|
-
throw new TelegramWorkspaceAdmissionError("admission-blocked", "Telegram Workspace operation is blocked by retirement.");
|
|
664
|
-
}
|
|
665
|
-
acquired.push(result.lease);
|
|
666
|
-
}
|
|
746
|
+
acquireTelegramWorkspaceAdmittedScopes(input, scopes, acquired);
|
|
667
747
|
}
|
|
668
748
|
catch (error) {
|
|
669
749
|
releaseAll();
|
|
@@ -688,9 +768,14 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
688
768
|
((owner) => getTelegramProcessLiveness(owner));
|
|
689
769
|
const transactionPath = `${options.path}.transaction`;
|
|
690
770
|
const path = options.path, profileKey = options.profileKey, stateProfile = options.stateProfile, boundOwner = { ...options.owner };
|
|
691
|
-
const publication = {
|
|
692
|
-
|
|
693
|
-
|
|
771
|
+
const publication = {
|
|
772
|
+
publishRename: options.publishRename,
|
|
773
|
+
onPublicationBoundary: options.onPublicationBoundary,
|
|
774
|
+
};
|
|
775
|
+
const isStorageCurrent = () => suppliedOptions.path === path &&
|
|
776
|
+
suppliedOptions.profileKey === profileKey &&
|
|
777
|
+
suppliedOptions.stateProfile === stateProfile &&
|
|
778
|
+
areOwnersEqual(suppliedOptions.owner, boundOwner);
|
|
694
779
|
const mapStateError = (error) => {
|
|
695
780
|
if (error instanceof TelegramRuntimeStateError)
|
|
696
781
|
throw new TelegramWorkspaceAdmissionError(error.code === "invalid" ? "invalid-state" : error.code, "Telegram consolidated admission state operation failed.", { cause: error });
|
|
@@ -699,17 +784,26 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
699
784
|
function transact(operation) {
|
|
700
785
|
if (stateProfile !== undefined) {
|
|
701
786
|
try {
|
|
702
|
-
return mutateTelegramRuntimeStateSection(path, stateProfile, "admission", current => {
|
|
787
|
+
return mutateTelegramRuntimeStateSection(path, stateProfile, "admission", (current) => {
|
|
703
788
|
const state = normalizeAdmissionState(current, profileKey, stateProfile);
|
|
704
|
-
const retained = state.leases.filter(lease => getProcessLiveness(lease.owner) !== "dead");
|
|
789
|
+
const retained = state.leases.filter((lease) => getProcessLiveness(lease.owner) !== "dead");
|
|
705
790
|
const pruned = retained.length !== state.leases.length;
|
|
706
791
|
state.leases = retained;
|
|
707
792
|
const outcome = operation(state);
|
|
708
|
-
return {
|
|
709
|
-
|
|
793
|
+
return {
|
|
794
|
+
value: pruned || outcome.changed
|
|
795
|
+
? normalizeAdmissionState(state, profileKey, stateProfile)
|
|
796
|
+
: current,
|
|
797
|
+
result: outcome.result,
|
|
798
|
+
};
|
|
799
|
+
}, {
|
|
800
|
+
isCurrent: isStorageCurrent,
|
|
801
|
+
publishRename: publication.publishRename,
|
|
802
|
+
onPublicationBoundary(boundary) {
|
|
710
803
|
if (boundary !== "after-rename")
|
|
711
804
|
publication.onPublicationBoundary?.(boundary, path);
|
|
712
|
-
}
|
|
805
|
+
},
|
|
806
|
+
});
|
|
713
807
|
}
|
|
714
808
|
catch (error) {
|
|
715
809
|
return mapStateError(error);
|
|
@@ -734,7 +828,9 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
734
828
|
authorityChanged("Telegram consolidated admission storage authority changed.");
|
|
735
829
|
try {
|
|
736
830
|
const file = readTelegramRuntimeState(path);
|
|
737
|
-
const value = Object.hasOwn(file.profiles, stateProfile)
|
|
831
|
+
const value = Object.hasOwn(file.profiles, stateProfile)
|
|
832
|
+
? file.profiles[stateProfile]?.admission
|
|
833
|
+
: undefined;
|
|
738
834
|
state = normalizeAdmissionState(value, profileKey, stateProfile);
|
|
739
835
|
if (!isStorageCurrent())
|
|
740
836
|
authorityChanged("Telegram consolidated admission storage authority changed.");
|
|
@@ -749,9 +845,14 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
749
845
|
profileKey: state.profileKey,
|
|
750
846
|
leases: state.leases.map(cloneLease),
|
|
751
847
|
...(state.fence ? { fence: cloneFence(state.fence) } : {}),
|
|
752
|
-
...(state.writerProtocolMode
|
|
753
|
-
|
|
754
|
-
|
|
848
|
+
...(state.writerProtocolMode
|
|
849
|
+
? {
|
|
850
|
+
writerProtocolMode: {
|
|
851
|
+
...state.writerProtocolMode,
|
|
852
|
+
installedBy: { ...state.writerProtocolMode.installedBy },
|
|
853
|
+
},
|
|
854
|
+
}
|
|
855
|
+
: {}),
|
|
755
856
|
};
|
|
756
857
|
}
|
|
757
858
|
function acquireAdmission(input) {
|
|
@@ -763,7 +864,8 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
763
864
|
return transact((state) => {
|
|
764
865
|
const journalOperation = input.operationKind === "journal-write" ||
|
|
765
866
|
input.operationKind.startsWith("journal.");
|
|
766
|
-
const protocolWriterHeld = state.leases.some(lease => lease.operationKind === "journal-write:custody-v3" &&
|
|
867
|
+
const protocolWriterHeld = state.leases.some((lease) => lease.operationKind === "journal-write:custody-v3" &&
|
|
868
|
+
lease.scope.kind === "profile" &&
|
|
767
869
|
areOwnersEqual(lease.owner, options.owner));
|
|
768
870
|
if (state.writerProtocolMode && journalOperation && !protocolWriterHeld)
|
|
769
871
|
authorityChanged("Telegram Workspace journal writer requires protocol authority.");
|
|
@@ -781,12 +883,17 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
781
883
|
authorityChanged("Telegram Workspace admission operation identity changed.");
|
|
782
884
|
}
|
|
783
885
|
return {
|
|
784
|
-
result: {
|
|
886
|
+
result: {
|
|
887
|
+
kind: "acquired",
|
|
888
|
+
lease: cloneLease(existing),
|
|
889
|
+
resumed: true,
|
|
890
|
+
},
|
|
785
891
|
changed: false,
|
|
786
892
|
};
|
|
787
893
|
}
|
|
788
|
-
if (state.fence &&
|
|
789
|
-
|
|
894
|
+
if (state.fence &&
|
|
895
|
+
(!isTelegramWorkspaceRetirementFence(state.fence) ||
|
|
896
|
+
scopesConflict(input.scope, state.fence.target))) {
|
|
790
897
|
return {
|
|
791
898
|
result: { kind: "blocked", reason: "retirement-fenced" },
|
|
792
899
|
changed: false,
|
|
@@ -830,49 +937,91 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
830
937
|
function acquireJournalWriterClosure(input) {
|
|
831
938
|
validateOperationId(input.operationId);
|
|
832
939
|
const candidate = normalizeTelegramWorkspaceJournalWriterClosureFence({
|
|
833
|
-
destructiveKind: "journal-writer-closure",
|
|
834
|
-
|
|
940
|
+
destructiveKind: "journal-writer-closure",
|
|
941
|
+
phase: "fenced",
|
|
942
|
+
...input,
|
|
943
|
+
profileKey: options.profileKey,
|
|
944
|
+
owner: options.owner,
|
|
945
|
+
acquiredAtMs: getNowMs(),
|
|
835
946
|
}, options.profileKey);
|
|
836
947
|
if (!candidate)
|
|
837
948
|
invalidInput("Telegram Workspace writer closure input is invalid.");
|
|
838
949
|
return transact((state) => {
|
|
839
950
|
if (state.writerProtocolMode) {
|
|
840
951
|
if (state.leases.length > 0)
|
|
841
|
-
return {
|
|
952
|
+
return {
|
|
953
|
+
result: { kind: "blocked", reason: "admission-active" },
|
|
954
|
+
changed: false,
|
|
955
|
+
};
|
|
842
956
|
let authorized = false;
|
|
843
957
|
if (state.writerProtocolMode.recoveryKey === candidate.recoveryKey) {
|
|
844
958
|
try {
|
|
845
|
-
authorized =
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
959
|
+
authorized =
|
|
960
|
+
options.authorizeJournalWriterProtocolClosure?.({
|
|
961
|
+
mode: {
|
|
962
|
+
...state.writerProtocolMode,
|
|
963
|
+
installedBy: { ...state.writerProtocolMode.installedBy },
|
|
964
|
+
},
|
|
965
|
+
closure: cloneFence(candidate),
|
|
966
|
+
}) === true;
|
|
850
967
|
}
|
|
851
968
|
catch {
|
|
852
969
|
authorized = false;
|
|
853
970
|
}
|
|
854
971
|
}
|
|
855
972
|
if (!authorized)
|
|
856
|
-
return {
|
|
973
|
+
return {
|
|
974
|
+
result: { kind: "blocked", reason: "retirement-active" },
|
|
975
|
+
changed: false,
|
|
976
|
+
};
|
|
857
977
|
delete state.writerProtocolMode;
|
|
858
978
|
state.fence = candidate;
|
|
859
|
-
return {
|
|
860
|
-
|
|
979
|
+
return {
|
|
980
|
+
result: {
|
|
981
|
+
kind: "acquired",
|
|
982
|
+
fence: cloneFence(candidate),
|
|
983
|
+
resumed: false,
|
|
984
|
+
},
|
|
985
|
+
changed: true,
|
|
986
|
+
};
|
|
861
987
|
}
|
|
862
988
|
if (state.fence) {
|
|
863
989
|
if (state.fence.destructiveKind === "journal-writer-closure") {
|
|
864
|
-
const resumed = {
|
|
990
|
+
const resumed = {
|
|
991
|
+
...candidate,
|
|
992
|
+
acquiredAtMs: state.fence.acquiredAtMs,
|
|
993
|
+
};
|
|
865
994
|
if (areWriterClosureFencesEqual(state.fence, resumed))
|
|
866
|
-
return {
|
|
995
|
+
return {
|
|
996
|
+
result: {
|
|
997
|
+
kind: "acquired",
|
|
998
|
+
fence: cloneFence(state.fence),
|
|
999
|
+
resumed: true,
|
|
1000
|
+
},
|
|
1001
|
+
changed: false,
|
|
1002
|
+
};
|
|
867
1003
|
if (state.fence.operationId === candidate.operationId)
|
|
868
1004
|
authorityChanged("Telegram Workspace writer closure authority changed.");
|
|
869
1005
|
}
|
|
870
|
-
return {
|
|
1006
|
+
return {
|
|
1007
|
+
result: { kind: "blocked", reason: "retirement-active" },
|
|
1008
|
+
changed: false,
|
|
1009
|
+
};
|
|
871
1010
|
}
|
|
872
1011
|
if (state.leases.length > 0)
|
|
873
|
-
return {
|
|
1012
|
+
return {
|
|
1013
|
+
result: { kind: "blocked", reason: "admission-active" },
|
|
1014
|
+
changed: false,
|
|
1015
|
+
};
|
|
874
1016
|
state.fence = candidate;
|
|
875
|
-
return {
|
|
1017
|
+
return {
|
|
1018
|
+
result: {
|
|
1019
|
+
kind: "acquired",
|
|
1020
|
+
fence: cloneFence(candidate),
|
|
1021
|
+
resumed: false,
|
|
1022
|
+
},
|
|
1023
|
+
changed: true,
|
|
1024
|
+
};
|
|
876
1025
|
});
|
|
877
1026
|
}
|
|
878
1027
|
function releaseJournalWriterClosure(expected) {
|
|
@@ -895,64 +1044,112 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
895
1044
|
const normalizedClosure = normalizeTelegramWorkspaceJournalWriterClosureFence(expected, options.profileKey);
|
|
896
1045
|
if (!normalizedClosure || !areOwnersEqual(expected.owner, options.owner))
|
|
897
1046
|
authorityChanged("Telegram Workspace writer protocol installation requires closure owner authority.");
|
|
898
|
-
const candidate = normalizeTelegramWorkspaceJournalWriterProtocolMode({
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1047
|
+
const candidate = normalizeTelegramWorkspaceJournalWriterProtocolMode({
|
|
1048
|
+
version: 1,
|
|
1049
|
+
protocol: "custody-v3",
|
|
1050
|
+
profileKey: options.profileKey,
|
|
1051
|
+
recoveryKey: expected.recoveryKey,
|
|
1052
|
+
startupAuthorityId: input.startupAuthorityId,
|
|
1053
|
+
closureOperationId: expected.operationId,
|
|
1054
|
+
writerInventorySha256: input.writerInventorySha256,
|
|
1055
|
+
installedBy: options.owner,
|
|
1056
|
+
installedAtMs: getNowMs(),
|
|
1057
|
+
}, options.profileKey);
|
|
903
1058
|
if (!candidate)
|
|
904
1059
|
invalidInput("Telegram Workspace writer protocol mode input is invalid.");
|
|
905
1060
|
return transact((state) => {
|
|
906
1061
|
if (state.writerProtocolMode) {
|
|
907
|
-
const resumed = {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1062
|
+
const resumed = {
|
|
1063
|
+
...candidate,
|
|
1064
|
+
installedAtMs: state.writerProtocolMode.installedAtMs,
|
|
1065
|
+
};
|
|
1066
|
+
if (areWriterProtocolModesEqual(state.writerProtocolMode, resumed) &&
|
|
1067
|
+
!state.fence)
|
|
1068
|
+
return {
|
|
1069
|
+
result: {
|
|
1070
|
+
mode: {
|
|
1071
|
+
...state.writerProtocolMode,
|
|
1072
|
+
installedBy: { ...state.writerProtocolMode.installedBy },
|
|
1073
|
+
},
|
|
1074
|
+
resumed: true,
|
|
1075
|
+
},
|
|
1076
|
+
changed: false,
|
|
1077
|
+
};
|
|
911
1078
|
authorityChanged("Telegram Workspace writer protocol mode authority changed.");
|
|
912
1079
|
}
|
|
913
|
-
if (!state.fence ||
|
|
914
|
-
|
|
1080
|
+
if (!state.fence ||
|
|
1081
|
+
state.fence.destructiveKind !== "journal-writer-closure" ||
|
|
1082
|
+
!areWriterClosureFencesEqual(state.fence, expected) ||
|
|
1083
|
+
state.leases.length !== 0)
|
|
915
1084
|
authorityChanged("Telegram Workspace writer closure authority changed before protocol installation.");
|
|
916
1085
|
state.writerProtocolMode = candidate;
|
|
917
1086
|
delete state.fence;
|
|
918
|
-
return {
|
|
919
|
-
|
|
1087
|
+
return {
|
|
1088
|
+
result: {
|
|
1089
|
+
mode: { ...candidate, installedBy: { ...candidate.installedBy } },
|
|
1090
|
+
resumed: false,
|
|
1091
|
+
},
|
|
1092
|
+
changed: true,
|
|
1093
|
+
};
|
|
920
1094
|
});
|
|
921
1095
|
}
|
|
922
1096
|
function acquireJournalWriterAdmission(input) {
|
|
923
1097
|
validateOperationId(input.operationId);
|
|
924
|
-
if (!isBoundedText(input.recoveryKey) ||
|
|
925
|
-
!isBoundedText(input.
|
|
1098
|
+
if (!isBoundedText(input.recoveryKey) ||
|
|
1099
|
+
!isBoundedText(input.startupAuthorityId) ||
|
|
1100
|
+
!isBoundedText(input.closureOperationId) ||
|
|
1101
|
+
!/^[a-f0-9]{64}$/u.test(input.writerInventorySha256))
|
|
926
1102
|
invalidInput("Telegram Workspace journal writer protocol authority is invalid.");
|
|
927
1103
|
return transact((state) => {
|
|
928
1104
|
const mode = state.writerProtocolMode;
|
|
929
|
-
if (!mode ||
|
|
1105
|
+
if (!mode ||
|
|
1106
|
+
mode.recoveryKey !== input.recoveryKey ||
|
|
930
1107
|
mode.startupAuthorityId !== input.startupAuthorityId ||
|
|
931
1108
|
mode.closureOperationId !== input.closureOperationId ||
|
|
932
1109
|
mode.writerInventorySha256 !== input.writerInventorySha256)
|
|
933
1110
|
authorityChanged("Telegram Workspace journal writer protocol authority changed.");
|
|
934
|
-
const existing = state.leases.find(lease => lease.operationId === input.operationId);
|
|
1111
|
+
const existing = state.leases.find((lease) => lease.operationId === input.operationId);
|
|
935
1112
|
const operationKind = "journal-write:custody-v3";
|
|
936
1113
|
if (existing) {
|
|
937
|
-
const retried = {
|
|
938
|
-
|
|
939
|
-
|
|
1114
|
+
const retried = {
|
|
1115
|
+
operationId: input.operationId,
|
|
1116
|
+
operationKind,
|
|
1117
|
+
profileKey: options.profileKey,
|
|
1118
|
+
scope: { kind: "profile" },
|
|
1119
|
+
owner: { ...options.owner },
|
|
1120
|
+
acquiredAtMs: existing.acquiredAtMs,
|
|
1121
|
+
};
|
|
940
1122
|
if (!areLeasesEqual(existing, retried))
|
|
941
1123
|
authorityChanged("Telegram Workspace journal writer operation identity changed.");
|
|
942
|
-
return {
|
|
943
|
-
|
|
1124
|
+
return {
|
|
1125
|
+
result: {
|
|
1126
|
+
kind: "acquired",
|
|
1127
|
+
lease: cloneLease(existing),
|
|
1128
|
+
resumed: true,
|
|
1129
|
+
},
|
|
1130
|
+
changed: false,
|
|
1131
|
+
};
|
|
944
1132
|
}
|
|
945
1133
|
if (state.fence)
|
|
946
|
-
return {
|
|
947
|
-
|
|
1134
|
+
return {
|
|
1135
|
+
result: { kind: "blocked", reason: "retirement-fenced" },
|
|
1136
|
+
changed: false,
|
|
1137
|
+
};
|
|
948
1138
|
if (state.leases.length >= TELEGRAM_WORKSPACE_ADMISSION_MAX_LEASES)
|
|
949
1139
|
invalidState("Telegram Workspace admission lease capacity is exhausted.");
|
|
950
|
-
const lease = {
|
|
951
|
-
|
|
952
|
-
|
|
1140
|
+
const lease = {
|
|
1141
|
+
operationId: input.operationId,
|
|
1142
|
+
operationKind,
|
|
1143
|
+
profileKey: options.profileKey,
|
|
1144
|
+
scope: { kind: "profile" },
|
|
1145
|
+
owner: { ...options.owner },
|
|
1146
|
+
acquiredAtMs: getNowMs(),
|
|
1147
|
+
};
|
|
953
1148
|
state.leases.push(lease);
|
|
954
|
-
return {
|
|
955
|
-
|
|
1149
|
+
return {
|
|
1150
|
+
result: { kind: "acquired", lease: cloneLease(lease), resumed: false },
|
|
1151
|
+
changed: true,
|
|
1152
|
+
};
|
|
956
1153
|
});
|
|
957
1154
|
}
|
|
958
1155
|
function acquireDestructiveFence(input, destructiveKind) {
|
|
@@ -971,7 +1168,10 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
971
1168
|
return transact((state) => {
|
|
972
1169
|
if (state.fence) {
|
|
973
1170
|
if (!isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
974
|
-
return {
|
|
1171
|
+
return {
|
|
1172
|
+
result: { kind: "blocked", reason: "retirement-active" },
|
|
1173
|
+
changed: false,
|
|
1174
|
+
};
|
|
975
1175
|
}
|
|
976
1176
|
if (state.fence.phase === "fenced" &&
|
|
977
1177
|
areFenceBasesEqual(state.fence, {
|
|
@@ -1003,7 +1203,11 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1003
1203
|
}
|
|
1004
1204
|
state.fence = candidate;
|
|
1005
1205
|
return {
|
|
1006
|
-
result: {
|
|
1206
|
+
result: {
|
|
1207
|
+
kind: "acquired",
|
|
1208
|
+
fence: cloneFence(candidate),
|
|
1209
|
+
resumed: false,
|
|
1210
|
+
},
|
|
1007
1211
|
changed: true,
|
|
1008
1212
|
};
|
|
1009
1213
|
});
|
|
@@ -1012,16 +1216,30 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1012
1216
|
return acquireDestructiveFence(input);
|
|
1013
1217
|
}
|
|
1014
1218
|
function acquireThreadCleanupFence(input) {
|
|
1015
|
-
return acquireDestructiveFence({
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1219
|
+
return acquireDestructiveFence({
|
|
1220
|
+
operationId: input.operationId,
|
|
1221
|
+
retirementIntentId: input.cleanupWorkSetId,
|
|
1222
|
+
bindingKey: input.bindingKey,
|
|
1223
|
+
slot: input.slot,
|
|
1224
|
+
target: input.target,
|
|
1225
|
+
leaderEpoch: input.leaderEpoch,
|
|
1226
|
+
retirementRequestedAtMs: input.cleanupRequestedAtMs,
|
|
1227
|
+
}, "manual-thread-cleanup");
|
|
1228
|
+
}
|
|
1229
|
+
// Exact profile/kind/owner preflight shared by owner-held fence transitions; the transaction still rechecks state.
|
|
1230
|
+
function assertOwnedFence(expected, expectedKind, ownerMessage) {
|
|
1231
|
+
validateExpectedFence(expected, options.profileKey);
|
|
1232
|
+
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1233
|
+
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1234
|
+
if (!areOwnersEqual(expected.owner, options.owner))
|
|
1235
|
+
authorityChanged(ownerMessage);
|
|
1019
1236
|
}
|
|
1020
1237
|
function adoptRetirementFence(expected, replacement, expectedKind = "pressure-retirement") {
|
|
1021
1238
|
validateExpectedFence(expected, options.profileKey);
|
|
1022
1239
|
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1023
1240
|
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1024
|
-
if (!normalizeOwner(replacement.owner) ||
|
|
1241
|
+
if (!normalizeOwner(replacement.owner) ||
|
|
1242
|
+
!isLeaderEpoch(replacement.leaderEpoch)) {
|
|
1025
1243
|
invalidInput("Telegram Workspace retirement successor authority is invalid.");
|
|
1026
1244
|
}
|
|
1027
1245
|
if (!areOwnersEqual(replacement.owner, options.owner)) {
|
|
@@ -1047,12 +1265,7 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1047
1265
|
});
|
|
1048
1266
|
}
|
|
1049
1267
|
function issueDeletionPermit(expected, expectedKind = "pressure-retirement") {
|
|
1050
|
-
|
|
1051
|
-
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1052
|
-
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1053
|
-
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1054
|
-
authorityChanged("Telegram Workspace deletion permit requires fence owner authority.");
|
|
1055
|
-
}
|
|
1268
|
+
assertOwnedFence(expected, expectedKind, "Telegram Workspace deletion permit requires fence owner authority.");
|
|
1056
1269
|
return transact((state) => {
|
|
1057
1270
|
if (!state.fence || !isTelegramWorkspaceRetirementFence(state.fence)) {
|
|
1058
1271
|
authorityChanged("Telegram Workspace retirement fence is absent or has another kind.");
|
|
@@ -1086,7 +1299,9 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1086
1299
|
kind: "issued",
|
|
1087
1300
|
fence: cloneFence(issued),
|
|
1088
1301
|
permit: {
|
|
1089
|
-
...(issued.destructiveKind === undefined
|
|
1302
|
+
...(issued.destructiveKind === undefined
|
|
1303
|
+
? {}
|
|
1304
|
+
: { destructiveKind: issued.destructiveKind }),
|
|
1090
1305
|
operationId: issued.operationId,
|
|
1091
1306
|
retirementIntentId: issued.retirementIntentId,
|
|
1092
1307
|
profileKey: issued.profileKey,
|
|
@@ -1102,12 +1317,7 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1102
1317
|
});
|
|
1103
1318
|
}
|
|
1104
1319
|
function confirmRetirementAbsence(expected, expectedKind = "pressure-retirement") {
|
|
1105
|
-
|
|
1106
|
-
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1107
|
-
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1108
|
-
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1109
|
-
authorityChanged("Telegram Workspace absence confirmation requires fence owner authority.");
|
|
1110
|
-
}
|
|
1320
|
+
assertOwnedFence(expected, expectedKind, "Telegram Workspace absence confirmation requires fence owner authority.");
|
|
1111
1321
|
if (expected.phase === "deletion-rejected") {
|
|
1112
1322
|
authorityChanged("Rejected Telegram Workspace deletion cannot confirm absence.");
|
|
1113
1323
|
}
|
|
@@ -1136,12 +1346,7 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1136
1346
|
});
|
|
1137
1347
|
}
|
|
1138
1348
|
function releaseUnissuedRetirementFence(expected, expectedKind = "pressure-retirement") {
|
|
1139
|
-
|
|
1140
|
-
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1141
|
-
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1142
|
-
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1143
|
-
authorityChanged("Telegram Workspace fence release requires owner authority.");
|
|
1144
|
-
}
|
|
1349
|
+
assertOwnedFence(expected, expectedKind, "Telegram Workspace fence release requires owner authority.");
|
|
1145
1350
|
return transact((state) => {
|
|
1146
1351
|
if (!state.fence)
|
|
1147
1352
|
return { result: false, changed: false };
|
|
@@ -1158,12 +1363,7 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1158
1363
|
});
|
|
1159
1364
|
}
|
|
1160
1365
|
function completeDestructiveFence(expected, expectedKind, requiredPhase) {
|
|
1161
|
-
|
|
1162
|
-
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !== expectedKind)
|
|
1163
|
-
authorityChanged("Telegram Workspace destructive fence kind changed.");
|
|
1164
|
-
if (!areOwnersEqual(expected.owner, options.owner)) {
|
|
1165
|
-
authorityChanged("Telegram Workspace fence completion requires owner authority.");
|
|
1166
|
-
}
|
|
1366
|
+
assertOwnedFence(expected, expectedKind, "Telegram Workspace fence completion requires owner authority.");
|
|
1167
1367
|
return transact((state) => {
|
|
1168
1368
|
if (!state.fence)
|
|
1169
1369
|
return { result: false, changed: false };
|
|
@@ -1183,23 +1383,33 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1183
1383
|
}
|
|
1184
1384
|
function confirmRetirementRejection(expected) {
|
|
1185
1385
|
validateExpectedFence(expected, options.profileKey);
|
|
1186
|
-
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !==
|
|
1386
|
+
if (resolveTelegramWorkspaceDestructiveFenceKind(expected) !==
|
|
1387
|
+
"pressure-retirement" ||
|
|
1187
1388
|
!areOwnersEqual(expected.owner, options.owner) ||
|
|
1188
|
-
(expected.phase !== "deletion-issued" &&
|
|
1389
|
+
(expected.phase !== "deletion-issued" &&
|
|
1390
|
+
expected.phase !== "deletion-rejected")) {
|
|
1189
1391
|
authorityChanged("Telegram Workspace rejection requires exact issued retirement authority.");
|
|
1190
1392
|
}
|
|
1191
1393
|
return transact((state) => {
|
|
1192
1394
|
const current = state.fence;
|
|
1193
|
-
if (!current ||
|
|
1194
|
-
(current
|
|
1395
|
+
if (!current ||
|
|
1396
|
+
!isTelegramWorkspaceRetirementFence(current) ||
|
|
1397
|
+
!areFenceBasesEqual(current, expected) ||
|
|
1398
|
+
(current.phase !== "deletion-issued" &&
|
|
1399
|
+
current.phase !== "deletion-rejected") ||
|
|
1195
1400
|
current.deletionIssuedAtMs !== expected.deletionIssuedAtMs ||
|
|
1196
|
-
(expected.phase === "deletion-rejected" &&
|
|
1401
|
+
(expected.phase === "deletion-rejected" &&
|
|
1402
|
+
!areFencesEqual(current, expected))) {
|
|
1197
1403
|
authorityChanged("Telegram Workspace retirement fence authority changed.");
|
|
1198
1404
|
}
|
|
1199
1405
|
if (current.phase === "deletion-rejected")
|
|
1200
1406
|
return { result: cloneFence(current), changed: false };
|
|
1201
|
-
const rejected = {
|
|
1202
|
-
|
|
1407
|
+
const rejected = {
|
|
1408
|
+
...current,
|
|
1409
|
+
destructiveKind: "pressure-retirement",
|
|
1410
|
+
phase: "deletion-rejected",
|
|
1411
|
+
rejectionConfirmedAtMs: getNowMs(),
|
|
1412
|
+
};
|
|
1203
1413
|
state.fence = rejected;
|
|
1204
1414
|
return { result: cloneFence(rejected), changed: true };
|
|
1205
1415
|
});
|
|
@@ -1227,10 +1437,14 @@ export function createTelegramWorkspaceAdmissionLedger(options) {
|
|
|
1227
1437
|
}
|
|
1228
1438
|
return {
|
|
1229
1439
|
getProfileKey: () => stateProfile === undefined ? options.profileKey : profileKey,
|
|
1230
|
-
getOwner: () => ({
|
|
1440
|
+
getOwner: () => ({
|
|
1441
|
+
...(stateProfile === undefined ? options.owner : boundOwner),
|
|
1442
|
+
}),
|
|
1231
1443
|
listReservedSlots: () => {
|
|
1232
1444
|
const fence = read().fence;
|
|
1233
|
-
return fence && isTelegramWorkspaceRetirementFence(fence)
|
|
1445
|
+
return fence && isTelegramWorkspaceRetirementFence(fence)
|
|
1446
|
+
? [fence.slot]
|
|
1447
|
+
: [];
|
|
1234
1448
|
},
|
|
1235
1449
|
read,
|
|
1236
1450
|
acquireAdmission,
|