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