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