@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
package/dist/lib/journal.js
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* It does not own polling, update execution, queue admission, or follower routing.
|
|
7
7
|
*/
|
|
8
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
|
-
import { chmodSync,
|
|
10
|
-
import { basename, dirname,
|
|
9
|
+
import { chmodSync, closeSync, fstatSync, lstatSync, mkdirSync, opendirSync, openSync, readdirSync, readFileSync, readSync, realpathSync, rmdirSync, rmSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep, } from "node:path";
|
|
11
11
|
import { isDeepStrictEqual } from "node:util";
|
|
12
|
-
import { renameTelegramPathWithRetry, withTelegramFileTransaction, } from "./locks.js";
|
|
13
|
-
import { getTelegramProcessLiveness, } from "./bus.js";
|
|
12
|
+
import { TELEGRAM_STRICT_READ_FLAGS, renameTelegramPathWithRetry, withTelegramFileTransaction, } from "./locks.js";
|
|
14
13
|
import { decodeTelegramSessionDirectoryName, getTelegramProfilePathSuffix, TELEGRAM_DEFAULT_PROFILE_NAME, } from "./paths.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
14
|
+
import { getTelegramProcessLiveness, } from "./process-identity.js";
|
|
15
|
+
import { hasOnlyWireKeys as hasOnlyKeys, isNonEmptyWireString as isNonEmptyString, isWireRecord as isRecord, isNonNegativeWireInteger as isSafeNonNegativeInteger, } from "./wire.js";
|
|
16
|
+
import { getTelegramWorkspaceAdmissionScopeKey, runWithTelegramWorkspaceAdmissions, } from "./workspace-admission.js";
|
|
17
17
|
export const TELEGRAM_UPDATE_JOURNAL_VERSION = 1;
|
|
18
18
|
export const TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION = 2;
|
|
19
19
|
export const TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION = 3;
|
|
@@ -105,13 +105,17 @@ export function discoverTelegramSessionJournalPaths(input) {
|
|
|
105
105
|
return { paths: [], complete: false };
|
|
106
106
|
}
|
|
107
107
|
catch (error) {
|
|
108
|
-
return {
|
|
108
|
+
return {
|
|
109
|
+
paths: [],
|
|
110
|
+
complete: error.code === "ENOENT",
|
|
111
|
+
};
|
|
109
112
|
}
|
|
110
113
|
try {
|
|
111
114
|
for (const session of scan(root)) {
|
|
112
115
|
if (!complete)
|
|
113
116
|
break;
|
|
114
|
-
if (!session.isDirectory() ||
|
|
117
|
+
if (!session.isDirectory() ||
|
|
118
|
+
decodeTelegramSessionDirectoryName(session.name) === undefined) {
|
|
115
119
|
complete = false;
|
|
116
120
|
break;
|
|
117
121
|
}
|
|
@@ -125,7 +129,8 @@ export function discoverTelegramSessionJournalPaths(input) {
|
|
|
125
129
|
if (!complete)
|
|
126
130
|
break;
|
|
127
131
|
// The leader polling journal and legacy recovery folders are not recipient custody.
|
|
128
|
-
if (entry.name === "recovery" ||
|
|
132
|
+
if (entry.name === "recovery" ||
|
|
133
|
+
/^inbox(?:\.[a-zA-Z0-9._-]+)?\.json(?:\.segments|\.retained)?$/u.test(entry.name))
|
|
129
134
|
continue;
|
|
130
135
|
const match = /^journal\.([a-f0-9]{16})(\.[a-zA-Z0-9._-]+)?\.json(\.segments|\.retained)?$/u.exec(entry.name);
|
|
131
136
|
if (!match) {
|
|
@@ -133,7 +138,8 @@ export function discoverTelegramSessionJournalPaths(input) {
|
|
|
133
138
|
break;
|
|
134
139
|
}
|
|
135
140
|
const stat = lstatSync(join(directory, entry.name));
|
|
136
|
-
if (stat.isSymbolicLink() ||
|
|
141
|
+
if (stat.isSymbolicLink() ||
|
|
142
|
+
(match[3] ? !stat.isDirectory() : !stat.isFile())) {
|
|
137
143
|
complete = false;
|
|
138
144
|
break;
|
|
139
145
|
}
|
|
@@ -152,8 +158,10 @@ export function discoverTelegramSessionJournalPaths(input) {
|
|
|
152
158
|
export function discoverTelegramRecipientJournalPaths(input) {
|
|
153
159
|
const legacy = discoverTelegramFollowerJournalPaths(input);
|
|
154
160
|
const sessions = discoverTelegramSessionJournalPaths(input);
|
|
155
|
-
return {
|
|
156
|
-
|
|
161
|
+
return {
|
|
162
|
+
paths: Array.from(new Set([...legacy.paths, ...sessions.paths])).sort(),
|
|
163
|
+
complete: legacy.complete && sessions.complete,
|
|
164
|
+
};
|
|
157
165
|
}
|
|
158
166
|
const TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH = 256;
|
|
159
167
|
export const TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT = 256;
|
|
@@ -212,18 +220,11 @@ function getUpdateAdmissionScope(update) {
|
|
|
212
220
|
if (payloadKey === "callback_query") {
|
|
213
221
|
const query = update.callback_query;
|
|
214
222
|
return isRecord(query)
|
|
215
|
-
? getUpdateCarrierScope(query.message) ?? { kind: "profile" }
|
|
223
|
+
? (getUpdateCarrierScope(query.message) ?? { kind: "profile" })
|
|
216
224
|
: { kind: "profile" };
|
|
217
225
|
}
|
|
218
226
|
return { kind: "profile" };
|
|
219
227
|
}
|
|
220
|
-
function getWorkspaceAdmissionScopeKey(scope) {
|
|
221
|
-
if (scope.kind === "profile")
|
|
222
|
-
return "profile";
|
|
223
|
-
if (scope.kind === "chat")
|
|
224
|
-
return `chat:${scope.chatId}`;
|
|
225
|
-
return `target:${scope.target.chatId}:${scope.target.threadId}`;
|
|
226
|
-
}
|
|
227
228
|
export function getTelegramUpdateJournalAdmissionScopes(updates) {
|
|
228
229
|
if (updates.length === 0)
|
|
229
230
|
return [{ kind: "profile" }];
|
|
@@ -238,73 +239,148 @@ export function getTelegramUpdateJournalAdmissionScopes(updates) {
|
|
|
238
239
|
for (const scope of scopes) {
|
|
239
240
|
if (scope.kind === "target" && chatIds.has(scope.target.chatId))
|
|
240
241
|
continue;
|
|
241
|
-
unique.set(
|
|
242
|
+
unique.set(getTelegramWorkspaceAdmissionScopeKey(scope), scope);
|
|
242
243
|
}
|
|
243
244
|
return Array.from(unique.entries())
|
|
244
245
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
245
246
|
.map(([, scope]) => scope);
|
|
246
247
|
}
|
|
247
248
|
function listTelegramUpdateJournalLegacyCustodyCandidates(snapshot) {
|
|
248
|
-
return snapshot.entries
|
|
249
|
+
return snapshot.entries
|
|
250
|
+
.flatMap((entry) => {
|
|
249
251
|
const evidence = createTelegramUpdateJournalLegacyCustodyEvidence(entry);
|
|
250
|
-
return evidence
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
return evidence
|
|
253
|
+
? [
|
|
254
|
+
{
|
|
255
|
+
updateId: evidence.updateId,
|
|
256
|
+
state: evidence.state,
|
|
257
|
+
attemptCount: evidence.attemptCount,
|
|
258
|
+
failureClass: evidence.failureClass,
|
|
259
|
+
evidenceSha256: evidence.evidenceSha256,
|
|
260
|
+
},
|
|
261
|
+
]
|
|
262
|
+
: [];
|
|
263
|
+
})
|
|
264
|
+
.sort((left, right) => left.updateId - right.updateId);
|
|
254
265
|
}
|
|
255
266
|
export function createTelegramUpdateJournalLegacyCustodyEvidence(entry) {
|
|
256
|
-
if ((entry.state !== "retry-wait" && entry.state !== "failed") ||
|
|
257
|
-
entry.
|
|
267
|
+
if ((entry.state !== "retry-wait" && entry.state !== "failed") ||
|
|
268
|
+
!entry.failure ||
|
|
269
|
+
entry.inputClaim ||
|
|
270
|
+
entry.inputProvenance)
|
|
258
271
|
return undefined;
|
|
259
|
-
const base = {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
272
|
+
const base = {
|
|
273
|
+
updateId: entry.updateId,
|
|
274
|
+
state: entry.state,
|
|
275
|
+
attemptCount: entry.failure.attemptCount,
|
|
276
|
+
failedAtMs: entry.failure.failedAtMs,
|
|
277
|
+
failureClass: entry.failure.failureClass,
|
|
278
|
+
summary: entry.failure.summary,
|
|
279
|
+
...(entry.nextRetryAtMs === undefined
|
|
280
|
+
? {}
|
|
281
|
+
: { nextRetryAtMs: entry.nextRetryAtMs }),
|
|
282
|
+
...(entry.terminalAtMs === undefined
|
|
283
|
+
? {}
|
|
284
|
+
: { terminalAtMs: entry.terminalAtMs }),
|
|
285
|
+
...(entry.terminalReason === undefined
|
|
286
|
+
? {}
|
|
287
|
+
: { terminalReason: entry.terminalReason }),
|
|
288
|
+
...(entry.terminalFailureId === undefined
|
|
289
|
+
? {}
|
|
290
|
+
: { terminalFailureId: entry.terminalFailureId }),
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
...base,
|
|
294
|
+
evidenceSha256: createHash("sha256")
|
|
295
|
+
.update(JSON.stringify(base))
|
|
296
|
+
.digest("hex"),
|
|
297
|
+
};
|
|
267
298
|
}
|
|
268
299
|
export function normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(value, expected) {
|
|
269
300
|
if (!isRecord(value))
|
|
270
301
|
return undefined;
|
|
271
|
-
const keys = [
|
|
272
|
-
"
|
|
273
|
-
|
|
302
|
+
const keys = [
|
|
303
|
+
"version",
|
|
304
|
+
"dispositionId",
|
|
305
|
+
"updateId",
|
|
306
|
+
"evidenceSha256",
|
|
307
|
+
"action",
|
|
308
|
+
"operatorAuthorityId",
|
|
309
|
+
"authorizedAtMs",
|
|
310
|
+
];
|
|
311
|
+
if (Object.keys(value).some((key) => !keys.includes(key)) ||
|
|
312
|
+
value.version !== 1 ||
|
|
274
313
|
!isBoundedString(value.dispositionId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
275
|
-
value.updateId !== expected.updateId ||
|
|
314
|
+
value.updateId !== expected.updateId ||
|
|
315
|
+
value.evidenceSha256 !== expected.evidenceSha256 ||
|
|
276
316
|
(value.action !== "requeue-v3" && value.action !== "discard") ||
|
|
277
317
|
!isBoundedString(value.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
278
318
|
!isSafeNonNegativeInteger(value.authorizedAtMs))
|
|
279
319
|
return undefined;
|
|
280
|
-
return {
|
|
281
|
-
|
|
282
|
-
|
|
320
|
+
return {
|
|
321
|
+
version: 1,
|
|
322
|
+
dispositionId: value.dispositionId,
|
|
323
|
+
updateId: expected.updateId,
|
|
324
|
+
evidenceSha256: expected.evidenceSha256,
|
|
325
|
+
action: value.action,
|
|
326
|
+
operatorAuthorityId: value.operatorAuthorityId,
|
|
327
|
+
authorizedAtMs: value.authorizedAtMs,
|
|
328
|
+
};
|
|
283
329
|
}
|
|
284
330
|
export const TELEGRAM_ROUTING_INPUT_TTL_MS = 60 * 60 * 1000;
|
|
331
|
+
function isTelegramUpdateJournalRoutingChooser(value) {
|
|
332
|
+
return (isRecord(value) &&
|
|
333
|
+
hasOnlyKeys(value, ["chatId", "threadId", "messageId"]) &&
|
|
334
|
+
Number.isSafeInteger(value.chatId) &&
|
|
335
|
+
(value.threadId === undefined || isSafePositiveInteger(value.threadId)) &&
|
|
336
|
+
isSafePositiveInteger(value.messageId));
|
|
337
|
+
}
|
|
285
338
|
export function createTelegramUpdateJournalEntryDigest(entry) {
|
|
286
|
-
return {
|
|
339
|
+
return {
|
|
340
|
+
updateId: entry.updateId,
|
|
341
|
+
sourceSha256: createHash("sha256")
|
|
342
|
+
.update(JSON.stringify(entry))
|
|
343
|
+
.digest("hex"),
|
|
344
|
+
};
|
|
287
345
|
}
|
|
288
346
|
function inspectRoutingInputExpiry(file, journalBindingKey, updateId) {
|
|
289
|
-
const disposition = file.operatorDispositions?.find(value => value.updateId === updateId &&
|
|
290
|
-
"dispositionKind" in value &&
|
|
347
|
+
const disposition = file.operatorDispositions?.find((value) => value.updateId === updateId &&
|
|
348
|
+
"dispositionKind" in value &&
|
|
349
|
+
value.dispositionKind === "legacy-custody" &&
|
|
350
|
+
value.action === "discard" &&
|
|
291
351
|
value.failureId === `routing-expiry:${value.evidenceSha256}`);
|
|
292
|
-
if (!disposition ||
|
|
352
|
+
if (!disposition ||
|
|
353
|
+
!("operatorAuthorityId" in disposition) ||
|
|
354
|
+
file.entries.some((entry) => entry.updateId === updateId))
|
|
293
355
|
return undefined;
|
|
294
|
-
return {
|
|
356
|
+
return {
|
|
357
|
+
journalBindingKey,
|
|
358
|
+
updateId,
|
|
359
|
+
operatorAuthorityId: disposition.operatorAuthorityId,
|
|
360
|
+
};
|
|
295
361
|
}
|
|
296
362
|
function inspectRoutingGroupExpiry(file, journalBindingKey, updateIds) {
|
|
297
|
-
if (!Array.isArray(updateIds) ||
|
|
298
|
-
updateIds.
|
|
363
|
+
if (!Array.isArray(updateIds) ||
|
|
364
|
+
!updateIds.length ||
|
|
365
|
+
updateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
|
|
366
|
+
updateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
|
|
367
|
+
(index > 0 && id <= updateIds[index - 1])))
|
|
299
368
|
return undefined;
|
|
300
|
-
if (file.entries.some(entry => updateIds.includes(entry.updateId)))
|
|
369
|
+
if (file.entries.some((entry) => updateIds.includes(entry.updateId)))
|
|
301
370
|
return undefined;
|
|
302
|
-
const expired = updateIds
|
|
303
|
-
|
|
371
|
+
const expired = updateIds
|
|
372
|
+
.map((id) => inspectRoutingInputExpiry(file, journalBindingKey, id))
|
|
373
|
+
.filter((value) => value !== undefined);
|
|
374
|
+
if (!expired.length ||
|
|
375
|
+
expired.some((value) => value.operatorAuthorityId !== expired[0].operatorAuthorityId))
|
|
304
376
|
return undefined;
|
|
305
377
|
// Some members may have been acknowledged earlier. Whole donor absence plus one exact expiry ends this known cohort,
|
|
306
378
|
// not recipient custody; fresh Thread protection still independently gates deletion.
|
|
307
|
-
return updateIds.map(updateId => ({
|
|
379
|
+
return updateIds.map((updateId) => ({
|
|
380
|
+
journalBindingKey,
|
|
381
|
+
updateId,
|
|
382
|
+
operatorAuthorityId: expired[0].operatorAuthorityId,
|
|
383
|
+
}));
|
|
308
384
|
}
|
|
309
385
|
function createJournalError(code, path, detail, cause) {
|
|
310
386
|
return new TelegramUpdateJournalError(code, path, `Telegram update journal ${detail}: ${path}`, cause === undefined ? undefined : { cause });
|
|
@@ -439,11 +515,11 @@ function createTelegramUpdateQueueHandoffId(input) {
|
|
|
439
515
|
.slice(0, 32)}`;
|
|
440
516
|
}
|
|
441
517
|
function isTelegramInputHandoffId(value) {
|
|
442
|
-
return isBoundedString(value, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH) &&
|
|
443
|
-
/^input-handoff-[a-f0-9]{32}$/u.test(value);
|
|
518
|
+
return (isBoundedString(value, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH) && /^input-handoff-[a-f0-9]{32}$/u.test(value));
|
|
444
519
|
}
|
|
445
520
|
function createTelegramInputHandoffId(input) {
|
|
446
|
-
return `input-handoff-${createHash("sha256")
|
|
521
|
+
return `input-handoff-${createHash("sha256")
|
|
522
|
+
.update(JSON.stringify({
|
|
447
523
|
version: 1,
|
|
448
524
|
token: input.handoffToken,
|
|
449
525
|
source: input.journalBindingKey,
|
|
@@ -451,7 +527,9 @@ function createTelegramInputHandoffId(input) {
|
|
|
451
527
|
donorOwner: input.donorOwner,
|
|
452
528
|
recipientOwner: input.recipientOwner,
|
|
453
529
|
recipientBindingKey: input.recipientBindingKey,
|
|
454
|
-
}))
|
|
530
|
+
}))
|
|
531
|
+
.digest("hex")
|
|
532
|
+
.slice(0, 32)}`;
|
|
455
533
|
}
|
|
456
534
|
function validateJournalHandoff(value, path, kind) {
|
|
457
535
|
if (!isRecord(value) ||
|
|
@@ -501,38 +579,72 @@ function createTelegramUpdateTerminalFailureId(input) {
|
|
|
501
579
|
}
|
|
502
580
|
function validateJournalOperatorDisposition(value, path) {
|
|
503
581
|
if (isRecord(value) && value.dispositionKind === "legacy-custody") {
|
|
504
|
-
if (!hasOnlyKeys(value, [
|
|
505
|
-
"
|
|
582
|
+
if (!hasOnlyKeys(value, [
|
|
583
|
+
"dispositionKind",
|
|
584
|
+
"failureId",
|
|
585
|
+
"updateId",
|
|
586
|
+
"action",
|
|
587
|
+
"committedAtMs",
|
|
588
|
+
"evidenceSha256",
|
|
589
|
+
"operatorAuthorityId",
|
|
590
|
+
"authorizedAtMs",
|
|
591
|
+
]) ||
|
|
506
592
|
!isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
507
593
|
!isSafeNonNegativeInteger(value.updateId) ||
|
|
508
594
|
(value.action !== "requeue-v3" && value.action !== "discard") ||
|
|
509
595
|
!isSafeNonNegativeInteger(value.committedAtMs) ||
|
|
510
|
-
typeof value.evidenceSha256 !== "string" ||
|
|
596
|
+
typeof value.evidenceSha256 !== "string" ||
|
|
597
|
+
!/^[a-f0-9]{64}$/u.test(value.evidenceSha256) ||
|
|
511
598
|
!isBoundedString(value.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
512
|
-
!isSafeNonNegativeInteger(value.authorizedAtMs) ||
|
|
599
|
+
!isSafeNonNegativeInteger(value.authorizedAtMs) ||
|
|
600
|
+
value.committedAtMs < value.authorizedAtMs)
|
|
513
601
|
throw createJournalError("invalid", path, "contains invalid legacy custody disposition metadata");
|
|
514
|
-
return {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
602
|
+
return {
|
|
603
|
+
dispositionKind: "legacy-custody",
|
|
604
|
+
failureId: value.failureId,
|
|
605
|
+
updateId: value.updateId,
|
|
606
|
+
action: value.action,
|
|
607
|
+
committedAtMs: value.committedAtMs,
|
|
608
|
+
evidenceSha256: value.evidenceSha256,
|
|
609
|
+
operatorAuthorityId: value.operatorAuthorityId,
|
|
610
|
+
authorizedAtMs: value.authorizedAtMs,
|
|
611
|
+
};
|
|
518
612
|
}
|
|
519
|
-
if (!isRecord(value) ||
|
|
520
|
-
|
|
613
|
+
if (!isRecord(value) ||
|
|
614
|
+
!hasOnlyKeys(value, [
|
|
615
|
+
"failureId",
|
|
616
|
+
"updateId",
|
|
617
|
+
"action",
|
|
618
|
+
"committedAtMs",
|
|
619
|
+
"attemptCount",
|
|
620
|
+
"failureClass",
|
|
621
|
+
"terminalAtMs",
|
|
622
|
+
"terminalReason",
|
|
623
|
+
]) ||
|
|
521
624
|
!isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
522
625
|
!isSafeNonNegativeInteger(value.updateId) ||
|
|
523
626
|
(value.action !== "retry" && value.action !== "discard") ||
|
|
524
|
-
!isSafeNonNegativeInteger(value.committedAtMs) ||
|
|
627
|
+
!isSafeNonNegativeInteger(value.committedAtMs) ||
|
|
628
|
+
!isSafePositiveInteger(value.attemptCount) ||
|
|
525
629
|
!isBoundedString(value.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
|
|
526
|
-
!isSafeNonNegativeInteger(value.terminalAtMs) ||
|
|
630
|
+
!isSafeNonNegativeInteger(value.terminalAtMs) ||
|
|
631
|
+
value.committedAtMs < value.terminalAtMs ||
|
|
527
632
|
!isBoundedString(value.terminalReason, TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH))
|
|
528
633
|
throw createJournalError("invalid", path, "contains invalid operator disposition metadata");
|
|
529
|
-
return {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
634
|
+
return {
|
|
635
|
+
failureId: value.failureId,
|
|
636
|
+
updateId: value.updateId,
|
|
637
|
+
action: value.action,
|
|
638
|
+
committedAtMs: value.committedAtMs,
|
|
639
|
+
attemptCount: value.attemptCount,
|
|
640
|
+
failureClass: value.failureClass,
|
|
641
|
+
terminalAtMs: value.terminalAtMs,
|
|
642
|
+
terminalReason: value.terminalReason,
|
|
643
|
+
};
|
|
533
644
|
}
|
|
534
645
|
function journalRequiresExclusion(version) {
|
|
535
|
-
return version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
|
|
646
|
+
return (version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
|
|
647
|
+
journalHasCustodyFields(version));
|
|
536
648
|
}
|
|
537
649
|
function journalHasCustodyFields(version) {
|
|
538
650
|
return version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION;
|
|
@@ -543,21 +655,33 @@ export function isTelegramUpdateJournalLegacyFamilyVersion(version) {
|
|
|
543
655
|
}
|
|
544
656
|
function validateJournalInputClaim(value, path, updateId) {
|
|
545
657
|
if (!isRecord(value) ||
|
|
546
|
-
!hasOnlyKeys(value, [
|
|
658
|
+
!hasOnlyKeys(value, [
|
|
659
|
+
"phase",
|
|
660
|
+
"owner",
|
|
661
|
+
"recipientBindingKey",
|
|
662
|
+
"handoff",
|
|
663
|
+
"executionUpdate",
|
|
664
|
+
]) ||
|
|
547
665
|
(value.phase !== "ready" && value.phase !== "running") ||
|
|
548
666
|
!isBoundedString(value.recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
|
|
549
667
|
!value.recipientBindingKey.trim()) {
|
|
550
668
|
throw createJournalError("invalid", path, "contains invalid input claim metadata");
|
|
551
669
|
}
|
|
552
670
|
const owner = validateJournalQueueOwner(value.owner, path);
|
|
553
|
-
const handoff = value.handoff === undefined
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
671
|
+
const handoff = value.handoff === undefined
|
|
672
|
+
? undefined
|
|
673
|
+
: validateJournalHandoff(value.handoff, path, "input");
|
|
674
|
+
if ((owner.handoffId !== undefined &&
|
|
675
|
+
!isTelegramInputHandoffId(owner.handoffId)) ||
|
|
676
|
+
(handoff &&
|
|
677
|
+
(!isTelegramInputHandoffId(handoff.handoffId) ||
|
|
678
|
+
value.phase !== "ready" ||
|
|
679
|
+
isTelegramUpdateJournalQueueOwnerProcess(owner, handoff.recipientOwner)))) {
|
|
557
680
|
throw createJournalError("invalid", path, "contains conflicting input handoff metadata");
|
|
558
681
|
}
|
|
559
682
|
const executionUpdate = value.executionUpdate === undefined
|
|
560
|
-
? undefined
|
|
683
|
+
? undefined
|
|
684
|
+
: validateJournaledUpdate(value.executionUpdate, path);
|
|
561
685
|
if (executionUpdate && executionUpdate.update_id !== updateId) {
|
|
562
686
|
throw createJournalError("invalid", path, "contains an input claim/update id mismatch");
|
|
563
687
|
}
|
|
@@ -577,20 +701,26 @@ function validateJournalInputProvenance(value, path, updateId) {
|
|
|
577
701
|
throw createJournalError("invalid", path, "contains invalid input queue provenance");
|
|
578
702
|
}
|
|
579
703
|
const owner = validateJournalQueueOwner(value.owner, path);
|
|
580
|
-
if (owner.handoffId !== undefined &&
|
|
704
|
+
if (owner.handoffId !== undefined &&
|
|
705
|
+
!isTelegramInputHandoffId(owner.handoffId)) {
|
|
581
706
|
throw createJournalError("invalid", path, "contains invalid input queue provenance");
|
|
582
707
|
}
|
|
583
708
|
const executionUpdate = value.executionUpdate === undefined
|
|
584
|
-
? undefined
|
|
709
|
+
? undefined
|
|
710
|
+
: validateJournaledUpdate(value.executionUpdate, path);
|
|
585
711
|
if (executionUpdate && executionUpdate.update_id !== updateId) {
|
|
586
712
|
throw createJournalError("invalid", path, "contains an input provenance/update id mismatch");
|
|
587
713
|
}
|
|
588
|
-
return {
|
|
589
|
-
|
|
714
|
+
return {
|
|
715
|
+
owner,
|
|
716
|
+
recipientBindingKey: value.recipientBindingKey,
|
|
717
|
+
...(executionUpdate ? { executionUpdate } : {}),
|
|
718
|
+
};
|
|
590
719
|
}
|
|
591
720
|
function validateJournalEntry(value, path, version) {
|
|
592
721
|
if (journalRequiresExclusion(version) &&
|
|
593
|
-
(!isRecord(value) ||
|
|
722
|
+
(!isRecord(value) ||
|
|
723
|
+
typeof value.preApprovalExcluded !== "boolean" ||
|
|
594
724
|
(value.preApprovalExcluded && value.state === "queued"))) {
|
|
595
725
|
throw createJournalError("pairing-evidence", path, "contains missing, malformed, or queued exclusion evidence");
|
|
596
726
|
}
|
|
@@ -599,7 +729,9 @@ function validateJournalEntry(value, path, version) {
|
|
|
599
729
|
"updateId",
|
|
600
730
|
"update",
|
|
601
731
|
"admittedAtMs",
|
|
602
|
-
...(isTelegramUpdateJournalLegacyFamilyVersion(version)
|
|
732
|
+
...(isTelegramUpdateJournalLegacyFamilyVersion(version)
|
|
733
|
+
? ["routingInput"]
|
|
734
|
+
: []),
|
|
603
735
|
"state",
|
|
604
736
|
"queueKind",
|
|
605
737
|
"queueReceiptId",
|
|
@@ -612,7 +744,8 @@ function validateJournalEntry(value, path, version) {
|
|
|
612
744
|
"terminalFailureId",
|
|
613
745
|
...(journalRequiresExclusion(version) ? ["preApprovalExcluded"] : []),
|
|
614
746
|
...(journalHasCustodyFields(version)
|
|
615
|
-
? ["inputClaim", "inputProvenance"]
|
|
747
|
+
? ["inputClaim", "inputProvenance"]
|
|
748
|
+
: []),
|
|
616
749
|
]) ||
|
|
617
750
|
!isSafeNonNegativeInteger(value.updateId) ||
|
|
618
751
|
!isSafeNonNegativeInteger(value.admittedAtMs) ||
|
|
@@ -629,16 +762,39 @@ function validateJournalEntry(value, path, version) {
|
|
|
629
762
|
let routingInput;
|
|
630
763
|
if (value.routingInput !== undefined) {
|
|
631
764
|
const routing = value.routingInput;
|
|
632
|
-
if (!isRecord(routing) ||
|
|
633
|
-
!
|
|
634
|
-
|
|
635
|
-
|
|
765
|
+
if (!isRecord(routing) ||
|
|
766
|
+
!hasOnlyKeys(routing, [
|
|
767
|
+
"operatorUserId",
|
|
768
|
+
"publishedAtMs",
|
|
769
|
+
"expiresAtMs",
|
|
770
|
+
"phase",
|
|
771
|
+
"chooser",
|
|
772
|
+
]) ||
|
|
773
|
+
(routing.chooser !== undefined &&
|
|
774
|
+
!isTelegramUpdateJournalRoutingChooser(routing.chooser)) ||
|
|
775
|
+
!isSafePositiveInteger(routing.operatorUserId) ||
|
|
776
|
+
!isSafeNonNegativeInteger(routing.publishedAtMs) ||
|
|
777
|
+
routing.publishedAtMs < value.admittedAtMs ||
|
|
778
|
+
!isSafeNonNegativeInteger(routing.expiresAtMs) ||
|
|
779
|
+
routing.expiresAtMs !==
|
|
780
|
+
routing.publishedAtMs + TELEGRAM_ROUTING_INPUT_TTL_MS ||
|
|
636
781
|
(routing.phase !== "waiting" && routing.phase !== "selected") ||
|
|
637
782
|
(routing.phase === "waiting" && value.state === "queued")) {
|
|
638
783
|
throw createJournalError("invalid", path, "contains invalid routing input lifetime");
|
|
639
784
|
}
|
|
640
|
-
routingInput = {
|
|
641
|
-
|
|
785
|
+
routingInput = {
|
|
786
|
+
operatorUserId: routing.operatorUserId,
|
|
787
|
+
publishedAtMs: routing.publishedAtMs,
|
|
788
|
+
expiresAtMs: routing.expiresAtMs,
|
|
789
|
+
phase: routing.phase,
|
|
790
|
+
...(routing.chooser !== undefined
|
|
791
|
+
? {
|
|
792
|
+
chooser: {
|
|
793
|
+
...routing.chooser,
|
|
794
|
+
},
|
|
795
|
+
}
|
|
796
|
+
: {}),
|
|
797
|
+
};
|
|
642
798
|
}
|
|
643
799
|
const queueKind = value.queueKind;
|
|
644
800
|
const queueReceiptId = value.queueReceiptId;
|
|
@@ -675,11 +831,14 @@ function validateJournalEntry(value, path, version) {
|
|
|
675
831
|
throw createJournalError("pairing-evidence", path, "contains claimed exclusion evidence");
|
|
676
832
|
}
|
|
677
833
|
const inputClaim = value.inputClaim === undefined
|
|
678
|
-
? undefined
|
|
834
|
+
? undefined
|
|
835
|
+
: validateJournalInputClaim(value.inputClaim, path, value.updateId);
|
|
679
836
|
const inputProvenance = value.inputProvenance === undefined
|
|
680
|
-
? undefined
|
|
681
|
-
|
|
682
|
-
|
|
837
|
+
? undefined
|
|
838
|
+
: validateJournalInputProvenance(value.inputProvenance, path, value.updateId);
|
|
839
|
+
if ((inputClaim &&
|
|
840
|
+
(value.state === "queued" ||
|
|
841
|
+
(inputClaim.phase === "running" && value.state !== "pending"))) ||
|
|
683
842
|
(inputProvenance && (value.state !== "queued" || inputClaim))) {
|
|
684
843
|
throw createJournalError("invalid", path, "contains conflicting input claim state");
|
|
685
844
|
}
|
|
@@ -711,15 +870,14 @@ function validateJournalEntry(value, path, version) {
|
|
|
711
870
|
updateId: value.updateId,
|
|
712
871
|
update,
|
|
713
872
|
...(journalRequiresExclusion(version)
|
|
714
|
-
? { preApprovalExcluded: value.preApprovalExcluded }
|
|
873
|
+
? { preApprovalExcluded: value.preApprovalExcluded }
|
|
874
|
+
: {}),
|
|
715
875
|
...(inputClaim ? { inputClaim } : {}),
|
|
716
876
|
...(inputProvenance ? { inputProvenance } : {}),
|
|
717
877
|
admittedAtMs: value.admittedAtMs,
|
|
718
878
|
...(routingInput ? { routingInput } : {}),
|
|
719
879
|
state: value.state,
|
|
720
|
-
...(queueKind === "prompt" || queueKind === "control"
|
|
721
|
-
? { queueKind }
|
|
722
|
-
: {}),
|
|
880
|
+
...(queueKind === "prompt" || queueKind === "control" ? { queueKind } : {}),
|
|
723
881
|
...(isNonEmptyString(queueReceiptId) ? { queueReceiptId } : {}),
|
|
724
882
|
...(queueOwner ? { queueOwner } : {}),
|
|
725
883
|
...(queueHandoff ? { queueHandoff } : {}),
|
|
@@ -750,40 +908,56 @@ function validateJournalEntry(value, path, version) {
|
|
|
750
908
|
};
|
|
751
909
|
}
|
|
752
910
|
function assertSupportedJournalVersion(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
|
|
753
|
-
if (isRecord(value) &&
|
|
911
|
+
if (isRecord(value) &&
|
|
912
|
+
Number.isSafeInteger(value.version) &&
|
|
754
913
|
value.version !== version) {
|
|
755
914
|
throw createJournalError("unsupported-version", path, `uses unsupported version ${String(value.version)}`);
|
|
756
915
|
}
|
|
757
916
|
}
|
|
758
917
|
function validateJournalSourceCompletion(value, path) {
|
|
759
|
-
if (!isRecord(value) ||
|
|
760
|
-
!
|
|
761
|
-
|
|
918
|
+
if (!isRecord(value) ||
|
|
919
|
+
!hasOnlyKeys(value, ["updateId", "sourceSha256", "completionSha256"]) ||
|
|
920
|
+
!isSafeNonNegativeInteger(value.updateId) ||
|
|
921
|
+
typeof value.sourceSha256 !== "string" ||
|
|
922
|
+
!/^[a-f0-9]{64}$/u.test(value.sourceSha256) ||
|
|
923
|
+
typeof value.completionSha256 !== "string" ||
|
|
924
|
+
!/^[a-f0-9]{64}$/u.test(value.completionSha256)) {
|
|
762
925
|
throw createJournalError("invalid", path, "has invalid source completion evidence");
|
|
763
926
|
}
|
|
764
|
-
return {
|
|
927
|
+
return {
|
|
928
|
+
updateId: value.updateId,
|
|
929
|
+
sourceSha256: value.sourceSha256,
|
|
930
|
+
completionSha256: value.completionSha256,
|
|
931
|
+
};
|
|
765
932
|
}
|
|
766
933
|
function validateJournalSourceCompletions(value, path, version, allowEmpty = false) {
|
|
767
|
-
if (!isTelegramUpdateJournalLegacyFamilyVersion(version) ||
|
|
934
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(version) ||
|
|
935
|
+
!Array.isArray(value) ||
|
|
936
|
+
(!allowEmpty && !value.length)) {
|
|
768
937
|
throw createJournalError("invalid", path, "has invalid source completion collection");
|
|
769
938
|
}
|
|
770
|
-
const completions = value.map(item => validateJournalSourceCompletion(item, path));
|
|
939
|
+
const completions = value.map((item) => validateJournalSourceCompletion(item, path));
|
|
771
940
|
const scopes = new Set();
|
|
772
941
|
for (let index = 0; index < completions.length; index++) {
|
|
773
942
|
const completion = completions[index];
|
|
774
|
-
if ((index > 0 && completions[index - 1].updateId >= completion.updateId) ||
|
|
943
|
+
if ((index > 0 && completions[index - 1].updateId >= completion.updateId) ||
|
|
944
|
+
scopes.has(completion.completionSha256)) {
|
|
775
945
|
throw createJournalError("invalid", path, "has duplicate or unordered source completion evidence");
|
|
776
946
|
}
|
|
777
947
|
scopes.add(completion.completionSha256);
|
|
778
948
|
}
|
|
779
949
|
return completions;
|
|
780
950
|
}
|
|
781
|
-
function
|
|
782
|
-
assertSupportedJournalVersion(value, path, version);
|
|
951
|
+
function assertJournalExclusionCursor(value, path, version) {
|
|
783
952
|
if (journalRequiresExclusion(version) &&
|
|
784
|
-
(!isRecord(value) ||
|
|
953
|
+
(!isRecord(value) ||
|
|
954
|
+
!isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
|
|
785
955
|
throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
|
|
786
956
|
}
|
|
957
|
+
}
|
|
958
|
+
function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
|
|
959
|
+
assertSupportedJournalVersion(value, path, version);
|
|
960
|
+
assertJournalExclusionCursor(value, path, version);
|
|
787
961
|
if (!isRecord(value) ||
|
|
788
962
|
!hasOnlyKeys(value, [
|
|
789
963
|
"version",
|
|
@@ -819,9 +993,7 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
|
|
|
819
993
|
}
|
|
820
994
|
const queuedReceipts = new Map();
|
|
821
995
|
for (const entry of entries) {
|
|
822
|
-
if (entry.state !== "queued" ||
|
|
823
|
-
!entry.queueKind ||
|
|
824
|
-
!entry.queueReceiptId) {
|
|
996
|
+
if (entry.state !== "queued" || !entry.queueKind || !entry.queueReceiptId) {
|
|
825
997
|
continue;
|
|
826
998
|
}
|
|
827
999
|
const existing = queuedReceipts.get(entry.queueReceiptId);
|
|
@@ -852,12 +1024,18 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
|
|
|
852
1024
|
}
|
|
853
1025
|
}
|
|
854
1026
|
const operatorDispositions = (value.operatorDispositions ?? []).map((disposition) => validateJournalOperatorDisposition(disposition, path));
|
|
855
|
-
const sourceCompletions = value.sourceCompletions === undefined
|
|
1027
|
+
const sourceCompletions = value.sourceCompletions === undefined
|
|
1028
|
+
? []
|
|
1029
|
+
: validateJournalSourceCompletions(value.sourceCompletions, path, version);
|
|
856
1030
|
const botIdentity = validateBotIdentity(value.botIdentity, path);
|
|
857
|
-
const activeIds = new Set(entries.map(entry => entry.updateId));
|
|
858
|
-
const discardedIds = new Set(operatorDispositions
|
|
859
|
-
|
|
860
|
-
|
|
1031
|
+
const activeIds = new Set(entries.map((entry) => entry.updateId));
|
|
1032
|
+
const discardedIds = new Set(operatorDispositions
|
|
1033
|
+
.filter((disposition) => disposition.action === "discard")
|
|
1034
|
+
.map((disposition) => disposition.updateId));
|
|
1035
|
+
if (sourceCompletions.some((completion) => activeIds.has(completion.updateId) ||
|
|
1036
|
+
discardedIds.has(completion.updateId) ||
|
|
1037
|
+
(value.acceptedThroughUpdateId !== undefined &&
|
|
1038
|
+
completion.updateId > value.acceptedThroughUpdateId))) {
|
|
861
1039
|
throw createJournalError("invalid", path, "has contradictory source completion evidence");
|
|
862
1040
|
}
|
|
863
1041
|
const dispositionFailureIds = new Set();
|
|
@@ -889,11 +1067,15 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
|
|
|
889
1067
|
};
|
|
890
1068
|
}
|
|
891
1069
|
function assertJournalRoutingInputContinuity(file, entries, path) {
|
|
892
|
-
const previous = new Map(file.entries.map(entry => [entry.updateId, entry.routingInput]));
|
|
1070
|
+
const previous = new Map(file.entries.map((entry) => [entry.updateId, entry.routingInput]));
|
|
893
1071
|
for (const entry of entries) {
|
|
894
1072
|
const retained = previous.get(entry.updateId), next = entry.routingInput;
|
|
895
|
-
if (retained &&
|
|
896
|
-
next
|
|
1073
|
+
if (retained &&
|
|
1074
|
+
(!next ||
|
|
1075
|
+
next.operatorUserId !== retained.operatorUserId ||
|
|
1076
|
+
next.publishedAtMs !== retained.publishedAtMs ||
|
|
1077
|
+
next.expiresAtMs !== retained.expiresAtMs ||
|
|
1078
|
+
(retained.phase === "selected" && next.phase !== "selected"))) {
|
|
897
1079
|
throw createJournalError("invalid", path, "regresses retained routing input lifetime");
|
|
898
1080
|
}
|
|
899
1081
|
}
|
|
@@ -902,14 +1084,20 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
|
|
|
902
1084
|
assertJournalRoutingInputContinuity(file, segment.upsertedEntries, path);
|
|
903
1085
|
if (segment.sourceCompletions === undefined)
|
|
904
1086
|
return;
|
|
905
|
-
const previous = new Map((file.sourceCompletions ?? []).map(completion => [
|
|
906
|
-
|
|
1087
|
+
const previous = new Map((file.sourceCompletions ?? []).map((completion) => [
|
|
1088
|
+
completion.updateId,
|
|
1089
|
+
completion,
|
|
1090
|
+
]));
|
|
1091
|
+
const next = new Map(segment.sourceCompletions.map((completion) => [
|
|
1092
|
+
completion.updateId,
|
|
1093
|
+
completion,
|
|
1094
|
+
]));
|
|
907
1095
|
if ([...previous].some(([id, completion]) => !isDeepStrictEqual(completion, next.get(id)))) {
|
|
908
1096
|
throw createJournalError("invalid", path, "regresses retained source completion evidence");
|
|
909
1097
|
}
|
|
910
|
-
const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
|
|
911
|
-
const removed = new Set(segment.removedUpdateIds), upserted = new Set(segment.upsertedEntries.map(entry => entry.updateId));
|
|
912
|
-
const upsertedReceipts = new Set(segment.upsertedEntries.map(entry => entry.queueReceiptId));
|
|
1098
|
+
const entries = new Map(file.entries.map((entry) => [entry.updateId, entry]));
|
|
1099
|
+
const removed = new Set(segment.removedUpdateIds), upserted = new Set(segment.upsertedEntries.map((entry) => entry.updateId));
|
|
1100
|
+
const upsertedReceipts = new Set(segment.upsertedEntries.map((entry) => entry.queueReceiptId));
|
|
913
1101
|
// Grouped markers share one whole-receipt check rather than rescanning the group per source.
|
|
914
1102
|
const queueGroups = new Map(), queueRemovals = new Map();
|
|
915
1103
|
for (const entry of file.entries) {
|
|
@@ -926,8 +1114,15 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
|
|
|
926
1114
|
if (queueRemovals.has(receiptId))
|
|
927
1115
|
return queueRemovals.get(receiptId);
|
|
928
1116
|
const group = queueGroups.get(receiptId);
|
|
929
|
-
const valid = !!group?.length &&
|
|
930
|
-
|
|
1117
|
+
const valid = !!group?.length &&
|
|
1118
|
+
!upsertedReceipts.has(receiptId) &&
|
|
1119
|
+
group.every((entry) => removed.has(entry.updateId) &&
|
|
1120
|
+
!upserted.has(entry.updateId) &&
|
|
1121
|
+
entry.state === "queued" &&
|
|
1122
|
+
entry.queueKind === source.queueKind &&
|
|
1123
|
+
entry.queueHandoff === undefined &&
|
|
1124
|
+
!!entry.queueOwner &&
|
|
1125
|
+
areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, source.queueOwner));
|
|
931
1126
|
queueRemovals.set(receiptId, valid);
|
|
932
1127
|
return valid;
|
|
933
1128
|
};
|
|
@@ -935,18 +1130,19 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
|
|
|
935
1130
|
if (previous.has(completion.updateId))
|
|
936
1131
|
continue;
|
|
937
1132
|
const source = entries.get(completion.updateId);
|
|
938
|
-
if (!removed.has(completion.updateId) ||
|
|
939
|
-
|
|
1133
|
+
if (!removed.has(completion.updateId) ||
|
|
1134
|
+
!source ||
|
|
1135
|
+
(source.state === "queued" && !queueWasRemoved(source)) ||
|
|
1136
|
+
source.state === "failed" ||
|
|
1137
|
+
createTelegramUpdateJournalEntryDigest(source).sourceSha256 !==
|
|
1138
|
+
completion.sourceSha256) {
|
|
940
1139
|
throw createJournalError("invalid", path, "source completion does not match its removal revision");
|
|
941
1140
|
}
|
|
942
1141
|
}
|
|
943
1142
|
}
|
|
944
1143
|
function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
|
|
945
1144
|
assertSupportedJournalVersion(value, path, version);
|
|
946
|
-
|
|
947
|
-
(!isRecord(value) || !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
|
|
948
|
-
throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
|
|
949
|
-
}
|
|
1145
|
+
assertJournalExclusionCursor(value, path, version);
|
|
950
1146
|
if (!isRecord(value) ||
|
|
951
1147
|
!hasOnlyKeys(value, [
|
|
952
1148
|
"version",
|
|
@@ -1013,7 +1209,11 @@ function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERS
|
|
|
1013
1209
|
...(value.operatorDispositions !== undefined
|
|
1014
1210
|
? { operatorDispositions }
|
|
1015
1211
|
: {}),
|
|
1016
|
-
...(value.sourceCompletions !== undefined
|
|
1212
|
+
...(value.sourceCompletions !== undefined
|
|
1213
|
+
? {
|
|
1214
|
+
sourceCompletions: validateJournalSourceCompletions(value.sourceCompletions, path, version, true),
|
|
1215
|
+
}
|
|
1216
|
+
: {}),
|
|
1017
1217
|
};
|
|
1018
1218
|
}
|
|
1019
1219
|
/**
|
|
@@ -1046,7 +1246,10 @@ export function inspectTelegramUpdateJournalFamily(input) {
|
|
|
1046
1246
|
/** Classifies private originals against exact discard tombstones; never replay or deletion authority. */
|
|
1047
1247
|
export function inspectTelegramUpdateJournalRetention(input) {
|
|
1048
1248
|
const acquired = acquireTelegramUpdateJournalFamily(input, undefined, true);
|
|
1049
|
-
return {
|
|
1249
|
+
return {
|
|
1250
|
+
evidence: acquired.evidence,
|
|
1251
|
+
retainedInputs: acquired.retainedInputs ?? [],
|
|
1252
|
+
};
|
|
1050
1253
|
}
|
|
1051
1254
|
/**
|
|
1052
1255
|
* Read-only source evidence for cooperating writers serialized by the caller through
|
|
@@ -1073,9 +1276,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1073
1276
|
const { path, limits } = input;
|
|
1074
1277
|
const acquiredSegments = [];
|
|
1075
1278
|
let snapshotRevision = 0;
|
|
1076
|
-
const fail = (message) => {
|
|
1077
|
-
|
|
1078
|
-
|
|
1279
|
+
const fail = (message) => {
|
|
1280
|
+
throw createJournalError("invalid", path, message);
|
|
1281
|
+
};
|
|
1282
|
+
const capacity = () => {
|
|
1283
|
+
throw createJournalError("capacity", path, "exceeds inspection resource limits");
|
|
1284
|
+
};
|
|
1285
|
+
for (const key of [
|
|
1286
|
+
"maxFiles",
|
|
1287
|
+
"maxBytes",
|
|
1288
|
+
"maxEntries",
|
|
1289
|
+
"maxWork",
|
|
1290
|
+
]) {
|
|
1079
1291
|
if (!isSafePositiveInteger(limits[key]))
|
|
1080
1292
|
fail("requires positive safe-integer limits");
|
|
1081
1293
|
}
|
|
@@ -1084,21 +1296,34 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1084
1296
|
fail("requires an exact profile");
|
|
1085
1297
|
const anchor = input.directory;
|
|
1086
1298
|
const contained = relative(anchor, path);
|
|
1087
|
-
if (!isAbsolute(anchor) ||
|
|
1088
|
-
resolve(
|
|
1089
|
-
|
|
1299
|
+
if (!isAbsolute(anchor) ||
|
|
1300
|
+
resolve(anchor) !== anchor ||
|
|
1301
|
+
!isAbsolute(path) ||
|
|
1302
|
+
resolve(path) !== path ||
|
|
1303
|
+
!contained ||
|
|
1304
|
+
contained === ".." ||
|
|
1305
|
+
contained.startsWith(`..${sep}`) ||
|
|
1306
|
+
isAbsolute(contained))
|
|
1090
1307
|
fail("escapes its canonical directory anchor");
|
|
1091
|
-
if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
|
|
1092
|
-
fail("platform lacks no-follow nonblocking open evidence");
|
|
1093
1308
|
let files = 0;
|
|
1094
1309
|
let bytes = 0;
|
|
1095
1310
|
let work = 0;
|
|
1096
1311
|
const observed = new Map();
|
|
1097
1312
|
const ancestors = new Set();
|
|
1098
|
-
const sameAncestor = (a, b) => b.isDirectory() &&
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
a.
|
|
1313
|
+
const sameAncestor = (a, b) => b.isDirectory() &&
|
|
1314
|
+
!b.isSymbolicLink() &&
|
|
1315
|
+
a.dev === b.dev &&
|
|
1316
|
+
a.ino === b.ino &&
|
|
1317
|
+
a.mode === b.mode &&
|
|
1318
|
+
a.uid === b.uid &&
|
|
1319
|
+
a.gid === b.gid;
|
|
1320
|
+
const same = (a, b) => a.dev === b.dev &&
|
|
1321
|
+
a.ino === b.ino &&
|
|
1322
|
+
a.mode === b.mode &&
|
|
1323
|
+
a.nlink === b.nlink &&
|
|
1324
|
+
a.size === b.size &&
|
|
1325
|
+
a.mtimeNs === b.mtimeNs &&
|
|
1326
|
+
a.ctimeNs === b.ctimeNs;
|
|
1102
1327
|
const status = (target) => {
|
|
1103
1328
|
try {
|
|
1104
1329
|
return lstatSync(target, { bigint: true });
|
|
@@ -1113,7 +1338,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1113
1338
|
const value = status(target);
|
|
1114
1339
|
if (!value && !optional)
|
|
1115
1340
|
fail("has a missing path component");
|
|
1116
|
-
if (value &&
|
|
1341
|
+
if (value &&
|
|
1342
|
+
(value.isSymbolicLink() ||
|
|
1343
|
+
!(directory ? value.isDirectory() : value.isFile())))
|
|
1117
1344
|
fail("has a linked or unexpected file type");
|
|
1118
1345
|
observed.set(target, value);
|
|
1119
1346
|
return value;
|
|
@@ -1121,11 +1348,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1121
1348
|
const charge = (value) => {
|
|
1122
1349
|
if (!isRecord(value))
|
|
1123
1350
|
return;
|
|
1124
|
-
for (const key of [
|
|
1351
|
+
for (const key of [
|
|
1352
|
+
"entries",
|
|
1353
|
+
"upsertedEntries",
|
|
1354
|
+
"removedUpdateIds",
|
|
1355
|
+
"operatorDispositions",
|
|
1356
|
+
"sourceCompletions",
|
|
1357
|
+
]) {
|
|
1125
1358
|
const collection = value[key];
|
|
1126
1359
|
if (!Array.isArray(collection))
|
|
1127
1360
|
continue;
|
|
1128
|
-
if (collection.length > limits.maxEntries ||
|
|
1361
|
+
if (collection.length > limits.maxEntries ||
|
|
1362
|
+
collection.length > limits.maxWork - work)
|
|
1129
1363
|
capacity();
|
|
1130
1364
|
work += collection.length;
|
|
1131
1365
|
}
|
|
@@ -1133,7 +1367,7 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1133
1367
|
const read = (target, before) => {
|
|
1134
1368
|
if (before.size > BigInt(limits.maxBytes - bytes))
|
|
1135
1369
|
capacity();
|
|
1136
|
-
const fd = openSync(target,
|
|
1370
|
+
const fd = openSync(target, TELEGRAM_STRICT_READ_FLAGS);
|
|
1137
1371
|
try {
|
|
1138
1372
|
const opened = fstatSync(fd, { bigint: true });
|
|
1139
1373
|
if (!opened.isFile() || !same(before, opened))
|
|
@@ -1173,7 +1407,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1173
1407
|
break;
|
|
1174
1408
|
if (names.length >= limits.maxFiles - usedFiles)
|
|
1175
1409
|
capacity();
|
|
1176
|
-
if (!pattern.test(entry.name) ||
|
|
1410
|
+
if (!pattern.test(entry.name) ||
|
|
1411
|
+
!entry.isFile() ||
|
|
1412
|
+
entry.isSymbolicLink())
|
|
1177
1413
|
fail("has an unexpected journal evidence entry");
|
|
1178
1414
|
names.push(entry.name);
|
|
1179
1415
|
}
|
|
@@ -1189,7 +1425,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1189
1425
|
if (realpathSync(anchor) !== anchor)
|
|
1190
1426
|
fail("requires a canonical directory anchor");
|
|
1191
1427
|
let parent = anchor;
|
|
1192
|
-
for (const component of relative(anchor, dirname(path))
|
|
1428
|
+
for (const component of relative(anchor, dirname(path))
|
|
1429
|
+
.split(sep)
|
|
1430
|
+
.filter(Boolean)) {
|
|
1193
1431
|
parent = join(parent, component);
|
|
1194
1432
|
observe(parent, true);
|
|
1195
1433
|
ancestors.add(parent);
|
|
@@ -1203,15 +1441,19 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1203
1441
|
let file;
|
|
1204
1442
|
let knownBotId = expected.botId;
|
|
1205
1443
|
const identity = (value) => {
|
|
1206
|
-
if (value.profile !== input.profile ||
|
|
1207
|
-
|
|
1444
|
+
if (value.profile !== input.profile ||
|
|
1445
|
+
value.botIdentity.tokenSha256 !== expected.tokenSha256 ||
|
|
1446
|
+
(knownBotId !== undefined &&
|
|
1447
|
+
value.botIdentity.botId !== undefined &&
|
|
1448
|
+
knownBotId !== value.botIdentity.botId)) {
|
|
1208
1449
|
throw createJournalError("identity-mismatch", path, "belongs to another exact journal identity");
|
|
1209
1450
|
}
|
|
1210
1451
|
knownBotId ??= value.botIdentity.botId;
|
|
1211
1452
|
};
|
|
1212
1453
|
if (snapshot) {
|
|
1213
1454
|
const raw = read(path, snapshot);
|
|
1214
|
-
if (!isRecord(raw) ||
|
|
1455
|
+
if (!isRecord(raw) ||
|
|
1456
|
+
(raw.version !== 1 && raw.version !== 2 && raw.version !== 3)) {
|
|
1215
1457
|
throw createJournalError("unsupported-version", path, "has an unsupported snapshot version");
|
|
1216
1458
|
}
|
|
1217
1459
|
file = parseJournalFile(raw, path, raw.version);
|
|
@@ -1222,11 +1464,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1222
1464
|
const metadata = observe(target, false);
|
|
1223
1465
|
const previousWork = work;
|
|
1224
1466
|
const segment = parseJournalSegment(read(target, metadata), target, file.version);
|
|
1225
|
-
acquiredSegments.push({
|
|
1226
|
-
|
|
1467
|
+
acquiredSegments.push({
|
|
1468
|
+
name,
|
|
1469
|
+
bytes: Number(metadata.size),
|
|
1470
|
+
work: work - previousWork,
|
|
1471
|
+
...(segment.botIdentity.botId !== undefined
|
|
1472
|
+
? { botId: segment.botIdentity.botId }
|
|
1473
|
+
: {}),
|
|
1474
|
+
});
|
|
1227
1475
|
files += 1;
|
|
1228
1476
|
identity(segment);
|
|
1229
|
-
if (segment.revision !== Number(name.slice(0, 16)) ||
|
|
1477
|
+
if (segment.revision !== Number(name.slice(0, 16)) ||
|
|
1478
|
+
segment.previousRevision !== segment.revision - 1)
|
|
1230
1479
|
fail("has an invalid intrinsic segment revision");
|
|
1231
1480
|
const dispositionIds = new Set();
|
|
1232
1481
|
for (const disposition of segment.operatorDispositions ?? []) {
|
|
@@ -1237,19 +1486,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1237
1486
|
const revision = file.revision ?? 0;
|
|
1238
1487
|
if (segment.revision <= revision)
|
|
1239
1488
|
continue;
|
|
1240
|
-
if (segment.revision !== revision + 1 ||
|
|
1489
|
+
if (segment.revision !== revision + 1 ||
|
|
1490
|
+
segment.previousRevision !== revision)
|
|
1241
1491
|
fail("has a newer revision gap");
|
|
1242
|
-
if (segment.acceptedThroughUpdateId !== undefined &&
|
|
1492
|
+
if (segment.acceptedThroughUpdateId !== undefined &&
|
|
1493
|
+
file.acceptedThroughUpdateId !== undefined &&
|
|
1243
1494
|
segment.acceptedThroughUpdateId < file.acceptedThroughUpdateId)
|
|
1244
1495
|
fail("regresses the admission cursor");
|
|
1245
1496
|
assertJournalSourceCompletionContinuity(file, segment, target);
|
|
1246
|
-
const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
|
|
1497
|
+
const entries = new Map(file.entries.map((entry) => [entry.updateId, entry]));
|
|
1247
1498
|
for (const id of segment.removedUpdateIds)
|
|
1248
1499
|
entries.delete(id);
|
|
1249
1500
|
for (const entry of segment.upsertedEntries) {
|
|
1250
1501
|
const previous = entries.get(entry.updateId);
|
|
1251
|
-
if (journalRequiresExclusion(file.version) &&
|
|
1252
|
-
(
|
|
1502
|
+
if (journalRequiresExclusion(file.version) &&
|
|
1503
|
+
((previous &&
|
|
1504
|
+
previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
|
|
1505
|
+
(!previous &&
|
|
1506
|
+
entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
|
|
1253
1507
|
throw createJournalError("pairing-evidence", target, "changes exclusion evidence or resurrects a settled source");
|
|
1254
1508
|
}
|
|
1255
1509
|
if (!entries.has(entry.updateId) && entries.size >= limits.maxEntries)
|
|
@@ -1259,7 +1513,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1259
1513
|
const next = {
|
|
1260
1514
|
...file,
|
|
1261
1515
|
revision: segment.revision,
|
|
1262
|
-
...(segment.acceptedThroughUpdateId !== undefined
|
|
1516
|
+
...(segment.acceptedThroughUpdateId !== undefined
|
|
1517
|
+
? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId }
|
|
1518
|
+
: {}),
|
|
1263
1519
|
entries: [...entries.values()].sort((a, b) => a.updateId - b.updateId),
|
|
1264
1520
|
operatorDispositions: segment.operatorDispositions ?? file.operatorDispositions,
|
|
1265
1521
|
sourceCompletions: segment.sourceCompletions ?? file.sourceCompletions,
|
|
@@ -1270,16 +1526,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1270
1526
|
}
|
|
1271
1527
|
const retainedInputs = [];
|
|
1272
1528
|
const retentionDirectory = `${path}.retained`;
|
|
1273
|
-
const retention = inspectRetention
|
|
1529
|
+
const retention = inspectRetention
|
|
1530
|
+
? observe(retentionDirectory, true, true)
|
|
1531
|
+
: undefined;
|
|
1274
1532
|
const retentionPattern = /^abandon-[a-f0-9]{64}\.json$/u;
|
|
1275
|
-
const retentionNames = retention
|
|
1533
|
+
const retentionNames = retention
|
|
1534
|
+
? enumerate(retentionDirectory, retentionPattern)
|
|
1535
|
+
: [];
|
|
1276
1536
|
if (retentionNames.length > limits.maxEntries)
|
|
1277
1537
|
capacity();
|
|
1278
1538
|
if (retention) {
|
|
1279
1539
|
if (!file || file.version !== 1)
|
|
1280
1540
|
fail("private retention requires its exact v1 journal snapshot");
|
|
1281
1541
|
const current = file;
|
|
1282
|
-
const bindingKeys = new Set([expected, current.botIdentity].map(botIdentity => createTelegramUpdateJournalBindingKey({
|
|
1542
|
+
const bindingKeys = new Set([expected, current.botIdentity].map((botIdentity) => createTelegramUpdateJournalBindingKey({
|
|
1543
|
+
path,
|
|
1544
|
+
profileName: input.profile,
|
|
1545
|
+
botIdentity,
|
|
1546
|
+
})));
|
|
1283
1547
|
const updateIds = new Set();
|
|
1284
1548
|
for (const name of retentionNames) {
|
|
1285
1549
|
const target = join(retentionDirectory, name);
|
|
@@ -1293,42 +1557,70 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1293
1557
|
work += 2;
|
|
1294
1558
|
if (!isRecord(raw))
|
|
1295
1559
|
throw createJournalError("invalid", target, "private retention is not an evidence record");
|
|
1296
|
-
if (typeof raw.journalBindingKey !== "string" ||
|
|
1560
|
+
if (typeof raw.journalBindingKey !== "string" ||
|
|
1561
|
+
!bindingKeys.has(raw.journalBindingKey))
|
|
1297
1562
|
throw createJournalError("invalid", target, "has foreign private retention evidence");
|
|
1298
1563
|
const entry = validateJournalEntry(raw.entry, target, 1);
|
|
1299
1564
|
const disposition = validateJournalOperatorDisposition(raw.requestedDisposition, target);
|
|
1300
|
-
const digest = createHash("sha256")
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1565
|
+
const digest = createHash("sha256")
|
|
1566
|
+
.update(JSON.stringify({ journalBindingKey: raw.journalBindingKey, entry }))
|
|
1567
|
+
.digest("hex");
|
|
1568
|
+
if (!("dispositionKind" in disposition) ||
|
|
1569
|
+
disposition.dispositionKind !== "legacy-custody" ||
|
|
1570
|
+
disposition.action !== "discard" ||
|
|
1571
|
+
entry.state !== "pending" ||
|
|
1572
|
+
disposition.updateId !== entry.updateId ||
|
|
1573
|
+
disposition.evidenceSha256 !== digest ||
|
|
1574
|
+
disposition.failureId !== `abandon-${digest}` ||
|
|
1575
|
+
name !== `${disposition.failureId}.json` ||
|
|
1576
|
+
!isDeepStrictEqual(raw, {
|
|
1577
|
+
version: 1,
|
|
1578
|
+
kind: "pending-input-retention",
|
|
1579
|
+
journalBindingKey: raw.journalBindingKey,
|
|
1580
|
+
entry,
|
|
1581
|
+
requestedDisposition: disposition,
|
|
1582
|
+
}))
|
|
1306
1583
|
fail("has inconsistent private retention evidence");
|
|
1307
1584
|
if (updateIds.has(entry.updateId))
|
|
1308
1585
|
fail("has duplicate private retention sources");
|
|
1309
1586
|
updateIds.add(entry.updateId);
|
|
1310
|
-
const committed = current.operatorDispositions?.find(value => value.failureId === disposition.failureId ||
|
|
1311
|
-
|
|
1587
|
+
const committed = current.operatorDispositions?.find((value) => value.failureId === disposition.failureId ||
|
|
1588
|
+
value.updateId === entry.updateId);
|
|
1589
|
+
if (committed &&
|
|
1590
|
+
(!isDeepStrictEqual(committed, disposition) ||
|
|
1591
|
+
current.entries.some((value) => value.updateId === entry.updateId)))
|
|
1312
1592
|
fail("private retention contradicts its journal disposition");
|
|
1313
|
-
retainedInputs.push({
|
|
1314
|
-
|
|
1593
|
+
retainedInputs.push({
|
|
1594
|
+
path: target,
|
|
1595
|
+
journalBindingKey: raw.journalBindingKey,
|
|
1596
|
+
failureId: disposition.failureId,
|
|
1597
|
+
updateId: entry.updateId,
|
|
1598
|
+
state: committed ? "committed" : "uncommitted",
|
|
1599
|
+
});
|
|
1315
1600
|
}
|
|
1316
1601
|
}
|
|
1317
|
-
if (inspectRetention &&
|
|
1318
|
-
disposition
|
|
1319
|
-
|
|
1602
|
+
if (inspectRetention &&
|
|
1603
|
+
file?.operatorDispositions?.some((disposition) => "dispositionKind" in disposition &&
|
|
1604
|
+
disposition.dispositionKind === "legacy-custody" &&
|
|
1605
|
+
disposition.action === "discard" &&
|
|
1606
|
+
/^abandon-[a-f0-9]{64}$/u.test(disposition.failureId) &&
|
|
1607
|
+
!retainedInputs.some((original) => original.updateId === disposition.updateId &&
|
|
1608
|
+
original.state === "committed")))
|
|
1320
1609
|
fail("committed abandonment lost its private original");
|
|
1321
1610
|
// Re-enumeration has the same bound, rather than allocating an unbounded census.
|
|
1322
1611
|
files = snapshot ? 1 : 0;
|
|
1323
1612
|
if (segments && !isDeepStrictEqual(names, enumerate()))
|
|
1324
1613
|
fail("changed segment namespace");
|
|
1325
1614
|
files += names.length;
|
|
1326
|
-
if (retention &&
|
|
1615
|
+
if (retention &&
|
|
1616
|
+
!isDeepStrictEqual(retentionNames, enumerate(retentionDirectory, retentionPattern)))
|
|
1327
1617
|
fail("changed private retention namespace");
|
|
1328
1618
|
files += retentionNames.length;
|
|
1329
1619
|
for (const [target, before] of observed) {
|
|
1330
1620
|
const after = status(target);
|
|
1331
|
-
const compare = selectedVersion !== undefined && ancestors.has(target)
|
|
1621
|
+
const compare = selectedVersion !== undefined && ancestors.has(target)
|
|
1622
|
+
? sameAncestor
|
|
1623
|
+
: same;
|
|
1332
1624
|
if (before ? !after || !compare(before, after) : after !== undefined)
|
|
1333
1625
|
fail("changed observed namespace or file");
|
|
1334
1626
|
}
|
|
@@ -1339,14 +1631,27 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1339
1631
|
if (file.version !== selectedVersion) {
|
|
1340
1632
|
throw createJournalError("unsupported-version", path, "does not match the selected source version");
|
|
1341
1633
|
}
|
|
1342
|
-
if (createTelegramUpdateJournalReceiptScope({
|
|
1343
|
-
|
|
1634
|
+
if (createTelegramUpdateJournalReceiptScope({
|
|
1635
|
+
profileName: input.profile,
|
|
1636
|
+
botIdentity: expected,
|
|
1637
|
+
}) !==
|
|
1638
|
+
createTelegramUpdateJournalReceiptScope({
|
|
1639
|
+
profileName: file.profile,
|
|
1640
|
+
botIdentity: file.botIdentity,
|
|
1641
|
+
})) {
|
|
1344
1642
|
throw createJournalError("identity-mismatch", path, "does not retain the expected receipt scope");
|
|
1345
1643
|
}
|
|
1346
1644
|
}
|
|
1347
1645
|
}
|
|
1348
1646
|
return {
|
|
1349
|
-
evidence: file
|
|
1647
|
+
evidence: file
|
|
1648
|
+
? {
|
|
1649
|
+
kind: "present",
|
|
1650
|
+
file,
|
|
1651
|
+
...(knownBotId !== undefined ? { knownBotId } : {}),
|
|
1652
|
+
accounting: { files, bytes, work },
|
|
1653
|
+
}
|
|
1654
|
+
: { kind: "absent" },
|
|
1350
1655
|
snapshotRevision,
|
|
1351
1656
|
segments: acquiredSegments,
|
|
1352
1657
|
...(inspectRetention ? { retainedInputs } : {}),
|
|
@@ -1367,9 +1672,19 @@ export function inspectTelegramSessionJournalNamespace(input) {
|
|
|
1367
1672
|
}
|
|
1368
1673
|
function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
1369
1674
|
const { directory, profile, limits } = input;
|
|
1370
|
-
const fail = (message) => {
|
|
1371
|
-
|
|
1372
|
-
|
|
1675
|
+
const fail = (message) => {
|
|
1676
|
+
throw createJournalError("invalid", directory, message);
|
|
1677
|
+
};
|
|
1678
|
+
const capacity = () => {
|
|
1679
|
+
throw createJournalError("capacity", directory, "exceeds namespace inspection resource limits");
|
|
1680
|
+
};
|
|
1681
|
+
for (const key of [
|
|
1682
|
+
"maxDirectoryEntries",
|
|
1683
|
+
"maxFiles",
|
|
1684
|
+
"maxBytes",
|
|
1685
|
+
"maxEntries",
|
|
1686
|
+
"maxWork",
|
|
1687
|
+
]) {
|
|
1373
1688
|
if (!isSafePositiveInteger(limits[key]))
|
|
1374
1689
|
fail("requires positive safe-integer limits");
|
|
1375
1690
|
}
|
|
@@ -1378,26 +1693,37 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1378
1693
|
const expected = validateBotIdentity(input.botIdentity, directory);
|
|
1379
1694
|
if (!isAbsolute(directory) || resolve(directory) !== directory)
|
|
1380
1695
|
fail("requires a canonical directory anchor");
|
|
1381
|
-
if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
|
|
1382
|
-
fail("platform lacks no-follow nonblocking open evidence");
|
|
1383
1696
|
const metadata = (target) => {
|
|
1384
1697
|
const value = lstatSync(target, { bigint: true });
|
|
1385
|
-
return {
|
|
1386
|
-
|
|
1698
|
+
return {
|
|
1699
|
+
dev: value.dev,
|
|
1700
|
+
ino: value.ino,
|
|
1701
|
+
mode: value.mode,
|
|
1702
|
+
nlink: value.nlink,
|
|
1703
|
+
size: value.size,
|
|
1704
|
+
mtimeNs: value.mtimeNs,
|
|
1705
|
+
ctimeNs: value.ctimeNs,
|
|
1706
|
+
};
|
|
1387
1707
|
};
|
|
1388
1708
|
const suffix = profile === "default" ? "" : `.${profile}`;
|
|
1389
1709
|
const pollingPath = input.pollingPath ?? join(directory, `inbox${suffix}.json`);
|
|
1390
1710
|
const polling = relative(directory, pollingPath);
|
|
1391
1711
|
{
|
|
1392
1712
|
const parts = polling.split(sep);
|
|
1393
|
-
if (!isAbsolute(pollingPath) ||
|
|
1394
|
-
|
|
1395
|
-
|
|
1713
|
+
if (!isAbsolute(pollingPath) ||
|
|
1714
|
+
resolve(pollingPath) !== pollingPath ||
|
|
1715
|
+
parts.at(-1) !== `inbox${suffix}.json` ||
|
|
1716
|
+
!(parts.length === 1 ||
|
|
1717
|
+
(parts.length === 3 &&
|
|
1718
|
+
parts[0] === "sessions" &&
|
|
1719
|
+
decodeTelegramSessionDirectoryName(parts[1]) !== undefined)))
|
|
1396
1720
|
fail("requires a canonical polling journal path");
|
|
1397
1721
|
}
|
|
1398
1722
|
try {
|
|
1399
1723
|
const root = lstatSync(directory);
|
|
1400
|
-
if (!root.isDirectory() ||
|
|
1724
|
+
if (!root.isDirectory() ||
|
|
1725
|
+
root.isSymbolicLink() ||
|
|
1726
|
+
realpathSync(directory) !== directory)
|
|
1401
1727
|
fail("requires a canonical directory anchor");
|
|
1402
1728
|
const beforeRoot = metadata(directory);
|
|
1403
1729
|
const census = () => {
|
|
@@ -1426,7 +1752,8 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1426
1752
|
};
|
|
1427
1753
|
const requireType = (target, isDirectory) => {
|
|
1428
1754
|
const value = lstatSync(target);
|
|
1429
|
-
if (value.isSymbolicLink() ||
|
|
1755
|
+
if (value.isSymbolicLink() ||
|
|
1756
|
+
!(isDirectory ? value.isDirectory() : value.isFile()) ||
|
|
1430
1757
|
(!isDirectory && value.nlink !== 1))
|
|
1431
1758
|
fail("has a linked or unexpected journal type");
|
|
1432
1759
|
};
|
|
@@ -1472,7 +1799,9 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1472
1799
|
if (!/inbox/iu.test(name))
|
|
1473
1800
|
return;
|
|
1474
1801
|
const match = /^(inbox|follower-inbox-[a-f0-9]{16})(?:\.([a-z0-9]{1,32}))?\.json(\.(?:segments|retained))?$/u.exec(name);
|
|
1475
|
-
if (!match ||
|
|
1802
|
+
if (!match ||
|
|
1803
|
+
match[2] === "default" ||
|
|
1804
|
+
(!includeSessions && match[3] === ".retained"))
|
|
1476
1805
|
fail("has a noncanonical journal-like entry");
|
|
1477
1806
|
requireType(target, !!match[3]);
|
|
1478
1807
|
if (includeSessions && match[3])
|
|
@@ -1482,26 +1811,58 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1482
1811
|
if (familyProfile === profile && base !== pollingPath)
|
|
1483
1812
|
families.set(base, match[1] === "inbox" ? "session" : "follower");
|
|
1484
1813
|
});
|
|
1485
|
-
return {
|
|
1486
|
-
|
|
1814
|
+
return {
|
|
1815
|
+
entries: [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)),
|
|
1816
|
+
families: [...families].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0),
|
|
1817
|
+
profiles: [...profiles].sort(),
|
|
1818
|
+
};
|
|
1487
1819
|
};
|
|
1488
1820
|
const before = census();
|
|
1489
|
-
const sourceInventory = [
|
|
1821
|
+
const sourceInventory = [
|
|
1822
|
+
[pollingPath, "polling"],
|
|
1823
|
+
...before.families,
|
|
1824
|
+
];
|
|
1490
1825
|
const sources = [];
|
|
1491
1826
|
const retainedInputs = [];
|
|
1492
|
-
const accounting = {
|
|
1827
|
+
const accounting = {
|
|
1828
|
+
directoryEntries: before.entries.length,
|
|
1829
|
+
files: 0,
|
|
1830
|
+
bytes: 0,
|
|
1831
|
+
work: 0,
|
|
1832
|
+
};
|
|
1493
1833
|
let knownBotId = expected.botId;
|
|
1494
1834
|
for (const [path, role] of sourceInventory) {
|
|
1495
|
-
const remaining = {
|
|
1496
|
-
|
|
1497
|
-
|
|
1835
|
+
const remaining = {
|
|
1836
|
+
maxFiles: limits.maxFiles - accounting.files,
|
|
1837
|
+
maxBytes: limits.maxBytes - accounting.bytes,
|
|
1838
|
+
maxWork: limits.maxWork - accounting.work,
|
|
1839
|
+
maxEntries: limits.maxEntries,
|
|
1840
|
+
};
|
|
1841
|
+
if (remaining.maxFiles <= 0 ||
|
|
1842
|
+
remaining.maxBytes <= 0 ||
|
|
1843
|
+
remaining.maxWork <= 0)
|
|
1498
1844
|
capacity();
|
|
1499
|
-
const acquire = () => acquireTelegramUpdateJournalFamily({
|
|
1500
|
-
|
|
1501
|
-
|
|
1845
|
+
const acquire = () => acquireTelegramUpdateJournalFamily({
|
|
1846
|
+
directory,
|
|
1847
|
+
path,
|
|
1848
|
+
profile,
|
|
1849
|
+
botIdentity: {
|
|
1850
|
+
...expected,
|
|
1851
|
+
...(knownBotId !== undefined ? { botId: knownBotId } : {}),
|
|
1852
|
+
},
|
|
1853
|
+
limits: remaining,
|
|
1854
|
+
}, undefined, includeSessions);
|
|
1855
|
+
const acquired = input.withSourceReference
|
|
1856
|
+
? input.withSourceReference(path, acquire)
|
|
1857
|
+
: acquire();
|
|
1502
1858
|
const evidence = acquired.evidence;
|
|
1503
|
-
retainedInputs.push(...(acquired.retainedInputs ?? []).map(original => ({
|
|
1504
|
-
|
|
1859
|
+
retainedInputs.push(...(acquired.retainedInputs ?? []).map((original) => ({
|
|
1860
|
+
...original,
|
|
1861
|
+
journalPath: path,
|
|
1862
|
+
})));
|
|
1863
|
+
if (evidence.kind === "absent" &&
|
|
1864
|
+
(role !== "polling" ||
|
|
1865
|
+
before.entries.some(([entry]) => entry === polling || entry === `${polling}.segments`))) {
|
|
1505
1866
|
fail("lost a discovered journal family");
|
|
1506
1867
|
}
|
|
1507
1868
|
if (evidence.kind === "present") {
|
|
@@ -1509,13 +1870,15 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1509
1870
|
accounting.files += evidence.accounting.files;
|
|
1510
1871
|
accounting.bytes += evidence.accounting.bytes;
|
|
1511
1872
|
}
|
|
1512
|
-
accounting.work +=
|
|
1873
|
+
accounting.work +=
|
|
1874
|
+
evidence.kind === "present" ? Math.max(1, evidence.accounting.work) : 1;
|
|
1513
1875
|
sources.push({ role, path, evidence });
|
|
1514
1876
|
}
|
|
1515
1877
|
const namespaceCurrent = () => {
|
|
1516
1878
|
try {
|
|
1517
|
-
return isDeepStrictEqual(beforeRoot, metadata(directory)) &&
|
|
1518
|
-
isDeepStrictEqual(
|
|
1879
|
+
return (isDeepStrictEqual(beforeRoot, metadata(directory)) &&
|
|
1880
|
+
isDeepStrictEqual(before, census()) &&
|
|
1881
|
+
isDeepStrictEqual(beforeRoot, metadata(directory)));
|
|
1519
1882
|
}
|
|
1520
1883
|
catch {
|
|
1521
1884
|
return false;
|
|
@@ -1523,7 +1886,12 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1523
1886
|
};
|
|
1524
1887
|
if (!namespaceCurrent())
|
|
1525
1888
|
fail("changed observed root namespace or identity");
|
|
1526
|
-
return {
|
|
1889
|
+
return {
|
|
1890
|
+
sources,
|
|
1891
|
+
accounting,
|
|
1892
|
+
...(includeSessions ? { retainedInputs } : {}),
|
|
1893
|
+
...(knownBotId !== undefined ? { knownBotId } : {}),
|
|
1894
|
+
};
|
|
1527
1895
|
}
|
|
1528
1896
|
catch (error) {
|
|
1529
1897
|
if (error instanceof TelegramUpdateJournalError)
|
|
@@ -1531,35 +1899,105 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1531
1899
|
throw createJournalError("io", directory, "could not establish canonical namespace evidence", error);
|
|
1532
1900
|
}
|
|
1533
1901
|
}
|
|
1902
|
+
/** Whether an update addresses this private-chat Thread anywhere in its payload (message, callback, reaction, …). */
|
|
1903
|
+
export function doesTelegramJournalUpdateNameThread(update, target) {
|
|
1904
|
+
const matches = (value, depth) => {
|
|
1905
|
+
if (depth > 8 || !value || typeof value !== "object")
|
|
1906
|
+
return false;
|
|
1907
|
+
const record = value;
|
|
1908
|
+
if (record.message_thread_id === target.threadId) {
|
|
1909
|
+
const chat = record.chat;
|
|
1910
|
+
if (!chat || typeof chat !== "object" || chat.id === target.chatId)
|
|
1911
|
+
return true;
|
|
1912
|
+
}
|
|
1913
|
+
return Object.values(record).some((child) => matches(child, depth + 1));
|
|
1914
|
+
};
|
|
1915
|
+
return matches(update, 0);
|
|
1916
|
+
}
|
|
1917
|
+
/** A button tap alone carries no input, so it never keeps a tab whose inputs are resolved (the in-flight Cancel itself). */
|
|
1918
|
+
export function isTelegramJournalLoneCallbackUpdate(update) {
|
|
1919
|
+
if (!update || typeof update !== "object")
|
|
1920
|
+
return false;
|
|
1921
|
+
const kinds = Object.keys(update).filter((key) => key !== "update_id");
|
|
1922
|
+
return kinds.length === 1 && kinds[0] === "callback_query";
|
|
1923
|
+
}
|
|
1924
|
+
/** Plain inbound kinds whose address is readable; anything else stays protective. */
|
|
1925
|
+
const TELEGRAM_CLEANUP_PLAIN_UPDATE_KINDS = [
|
|
1926
|
+
"message",
|
|
1927
|
+
"edited_message",
|
|
1928
|
+
"callback_query",
|
|
1929
|
+
"message_reaction",
|
|
1930
|
+
];
|
|
1534
1931
|
/** Complete-empty protection only; caller holds source serialization through consumption. Never deletion authority. */
|
|
1535
1932
|
export function isTelegramThreadCleanupJournalNamespaceClear(input) {
|
|
1536
|
-
if (!input.requiredJournalBindingKeys.length ||
|
|
1933
|
+
if (!input.requiredJournalBindingKeys.length ||
|
|
1934
|
+
input.requiredJournalBindingKeys.length > input.limits.maxDirectoryEntries)
|
|
1537
1935
|
throw new Error("Telegram temporary cleanup requires bounded exact journal references.");
|
|
1538
1936
|
const requiredPaths = new Set();
|
|
1539
1937
|
for (const key of input.requiredJournalBindingKeys) {
|
|
1540
1938
|
const path = getTelegramUpdateJournalBindingPath(key);
|
|
1541
|
-
const identities = [
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1939
|
+
const identities = [
|
|
1940
|
+
input.botIdentity,
|
|
1941
|
+
{ tokenSha256: input.botIdentity.tokenSha256 },
|
|
1942
|
+
];
|
|
1943
|
+
if (!path ||
|
|
1944
|
+
!identities.some((botIdentity) => key ===
|
|
1945
|
+
createTelegramUpdateJournalBindingKey({
|
|
1946
|
+
path,
|
|
1947
|
+
profileName: input.profile,
|
|
1948
|
+
botIdentity,
|
|
1949
|
+
})))
|
|
1545
1950
|
throw new Error("Telegram temporary cleanup has foreign or malformed journal references.");
|
|
1546
1951
|
requiredPaths.add(path);
|
|
1547
1952
|
}
|
|
1548
1953
|
const namespace = inspectTelegramSessionJournalNamespace(input);
|
|
1549
|
-
if (namespace.sources.filter(source => source.role === "polling").length !==
|
|
1550
|
-
|
|
1551
|
-
|
|
1954
|
+
if (namespace.sources.filter((source) => source.role === "polling").length !==
|
|
1955
|
+
1 ||
|
|
1956
|
+
namespace.retainedInputs?.some((original) => original.state !== "committed") ||
|
|
1957
|
+
[...requiredPaths].some((path) => !namespace.sources.some((source) => source.path === path && source.evidence.kind === "present")))
|
|
1552
1958
|
return false;
|
|
1553
|
-
|
|
1554
|
-
|
|
1959
|
+
const cleanup = input.cleanup;
|
|
1960
|
+
const ownPaths = new Map();
|
|
1961
|
+
for (const own of cleanup?.ownInputs ?? []) {
|
|
1962
|
+
const path = getTelegramUpdateJournalBindingPath(own.journalBindingKey);
|
|
1963
|
+
if (!path)
|
|
1964
|
+
return false;
|
|
1965
|
+
const ids = ownPaths.get(path) ?? new Set();
|
|
1966
|
+
for (const id of own.updateIds)
|
|
1967
|
+
ids.add(id);
|
|
1968
|
+
ownPaths.set(path, ids);
|
|
1969
|
+
}
|
|
1970
|
+
// Custody-bearing, failed or unreadable work protects every tab: its reply target is not its message address.
|
|
1971
|
+
const isUnrelatedPlainInput = (entry, path) => {
|
|
1972
|
+
const kinds = Object.keys(entry.update).filter((key) => key !== "update_id");
|
|
1973
|
+
return (!!cleanup &&
|
|
1974
|
+
entry.state === "pending" &&
|
|
1975
|
+
!entry.queueOwner &&
|
|
1976
|
+
!entry.queueReceiptId &&
|
|
1977
|
+
!entry.queueKind &&
|
|
1978
|
+
!entry.queueHandoff &&
|
|
1979
|
+
!entry.inputClaim &&
|
|
1980
|
+
!("inputProvenance" in entry) &&
|
|
1981
|
+
!entry.failure &&
|
|
1982
|
+
kinds.length === 1 &&
|
|
1983
|
+
TELEGRAM_CLEANUP_PLAIN_UPDATE_KINDS.includes(kinds[0]) &&
|
|
1984
|
+
(isTelegramJournalLoneCallbackUpdate(entry.update) ||
|
|
1985
|
+
!doesTelegramJournalUpdateNameThread(entry.update, cleanup.target)) &&
|
|
1986
|
+
!ownPaths.get(path)?.has(entry.updateId));
|
|
1987
|
+
};
|
|
1988
|
+
return namespace.sources.every((source) => source.evidence.kind === "absent" ||
|
|
1989
|
+
source.evidence.file.entries.every((entry) => isUnrelatedPlainInput(entry, source.path)));
|
|
1555
1990
|
}
|
|
1556
1991
|
function cloneEntry(entry) {
|
|
1557
1992
|
return {
|
|
1558
1993
|
...entry,
|
|
1559
1994
|
update: structuredClone(entry.update),
|
|
1560
|
-
...(entry.inputClaim
|
|
1995
|
+
...(entry.inputClaim
|
|
1996
|
+
? { inputClaim: structuredClone(entry.inputClaim) }
|
|
1997
|
+
: {}),
|
|
1561
1998
|
...(entry.inputProvenance
|
|
1562
|
-
? { inputProvenance: structuredClone(entry.inputProvenance) }
|
|
1999
|
+
? { inputProvenance: structuredClone(entry.inputProvenance) }
|
|
2000
|
+
: {}),
|
|
1563
2001
|
...(entry.queueOwner
|
|
1564
2002
|
? { queueOwner: cloneJournalQueueOwner(entry.queueOwner) }
|
|
1565
2003
|
: {}),
|
|
@@ -1580,7 +2018,13 @@ function cloneFile(file) {
|
|
|
1580
2018
|
profile: file.profile,
|
|
1581
2019
|
botIdentity: { ...file.botIdentity },
|
|
1582
2020
|
entries: file.entries.map(cloneEntry),
|
|
1583
|
-
...(file.sourceCompletions?.length
|
|
2021
|
+
...(file.sourceCompletions?.length
|
|
2022
|
+
? {
|
|
2023
|
+
sourceCompletions: file.sourceCompletions.map((completion) => ({
|
|
2024
|
+
...completion,
|
|
2025
|
+
})),
|
|
2026
|
+
}
|
|
2027
|
+
: {}),
|
|
1584
2028
|
...(file.operatorDispositions?.length
|
|
1585
2029
|
? {
|
|
1586
2030
|
operatorDispositions: file.operatorDispositions.map((disposition) => ({ ...disposition })),
|
|
@@ -1611,7 +2055,8 @@ function publishTelegramUpdateJournalSegmentUnlocked(path, segment, onPublicatio
|
|
|
1611
2055
|
let snapshotRevision = 0;
|
|
1612
2056
|
try {
|
|
1613
2057
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1614
|
-
snapshotRevision =
|
|
2058
|
+
snapshotRevision =
|
|
2059
|
+
parseJournalFile(parsed, path, segment.version).revision ?? 0;
|
|
1615
2060
|
}
|
|
1616
2061
|
catch (error) {
|
|
1617
2062
|
if (error?.code !== "ENOENT")
|
|
@@ -1682,7 +2127,9 @@ function mergeBotIdentity(stored, current) {
|
|
|
1682
2127
|
}
|
|
1683
2128
|
function createPendingRetentionReference(input) {
|
|
1684
2129
|
const { path, journalBindingKey, entry, maxBytes } = input;
|
|
1685
|
-
const evidenceSha256 = createHash("sha256")
|
|
2130
|
+
const evidenceSha256 = createHash("sha256")
|
|
2131
|
+
.update(JSON.stringify({ journalBindingKey, entry }))
|
|
2132
|
+
.digest("hex");
|
|
1686
2133
|
const failureId = `abandon-${evidenceSha256}`;
|
|
1687
2134
|
const retainedDirectory = `${path}.retained`;
|
|
1688
2135
|
const retainedPath = join(retainedDirectory, `${failureId}.json`);
|
|
@@ -1708,22 +2155,37 @@ function createPendingRetentionReference(input) {
|
|
|
1708
2155
|
return undefined;
|
|
1709
2156
|
throw error;
|
|
1710
2157
|
}
|
|
1711
|
-
if (!stat.isFile() ||
|
|
2158
|
+
if (!stat.isFile() ||
|
|
2159
|
+
stat.isSymbolicLink() ||
|
|
2160
|
+
stat.size > maxBytes + 4096) {
|
|
1712
2161
|
throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
|
|
1713
2162
|
}
|
|
1714
2163
|
const retained = JSON.parse(readFileSync(retainedPath, "utf8"));
|
|
1715
2164
|
if (!isRecord(retained))
|
|
1716
2165
|
throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
|
|
1717
2166
|
const disposition = validateJournalOperatorDisposition(retained.requestedDisposition, retainedPath);
|
|
1718
|
-
if (!("dispositionKind" in disposition) ||
|
|
1719
|
-
disposition.
|
|
1720
|
-
disposition.
|
|
1721
|
-
|
|
1722
|
-
|
|
2167
|
+
if (!("dispositionKind" in disposition) ||
|
|
2168
|
+
disposition.dispositionKind !== "legacy-custody" ||
|
|
2169
|
+
disposition.failureId !== failureId ||
|
|
2170
|
+
disposition.updateId !== entry.updateId ||
|
|
2171
|
+
disposition.action !== "discard" ||
|
|
2172
|
+
disposition.evidenceSha256 !== evidenceSha256 ||
|
|
2173
|
+
!isDeepStrictEqual(retained, {
|
|
2174
|
+
version: 1,
|
|
2175
|
+
kind: "pending-input-retention",
|
|
2176
|
+
journalBindingKey,
|
|
2177
|
+
entry,
|
|
2178
|
+
requestedDisposition: disposition,
|
|
2179
|
+
})) {
|
|
1723
2180
|
throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
|
|
1724
2181
|
}
|
|
1725
|
-
return {
|
|
1726
|
-
|
|
2182
|
+
return {
|
|
2183
|
+
version: 1,
|
|
2184
|
+
kind: "pending-input-retention",
|
|
2185
|
+
journalBindingKey,
|
|
2186
|
+
entry,
|
|
2187
|
+
requestedDisposition: disposition,
|
|
2188
|
+
};
|
|
1727
2189
|
};
|
|
1728
2190
|
return { evidenceSha256, failureId, retainedPath, read };
|
|
1729
2191
|
}
|
|
@@ -1847,26 +2309,47 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
|
|
|
1847
2309
|
});
|
|
1848
2310
|
const path = deps.getJournalPath(configuredProfileName);
|
|
1849
2311
|
const workspaceAdmission = deps.getWorkspaceAdmission?.();
|
|
1850
|
-
const sourceAccess = {
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
2312
|
+
const sourceAccess = {
|
|
2313
|
+
directory: dirname(path),
|
|
2314
|
+
limits: {
|
|
2315
|
+
maxFiles: 1024,
|
|
2316
|
+
maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
|
|
2317
|
+
maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
|
|
2318
|
+
maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
|
|
2319
|
+
},
|
|
2320
|
+
};
|
|
2321
|
+
const options = {
|
|
2322
|
+
path,
|
|
2323
|
+
profileName,
|
|
2324
|
+
botIdentity,
|
|
2325
|
+
...(deps.getQueueRuntimeIdentity
|
|
2326
|
+
? { queueRuntimeIdentity: deps.getQueueRuntimeIdentity() }
|
|
2327
|
+
: {}),
|
|
1856
2328
|
...(workspaceAdmission ? { workspaceAdmission } : {}),
|
|
1857
|
-
...(deps.withWriterAdmission
|
|
1858
|
-
|
|
1859
|
-
|
|
2329
|
+
...(deps.withWriterAdmission
|
|
2330
|
+
? { withWriterAdmission: deps.withWriterAdmission }
|
|
2331
|
+
: {}),
|
|
2332
|
+
...(deps.withSourceSerialization
|
|
2333
|
+
? { withSourceSerialization: deps.withSourceSerialization }
|
|
2334
|
+
: {}),
|
|
2335
|
+
...(deps.onRecovery ? { onRecovery: deps.onRecovery } : {}),
|
|
2336
|
+
};
|
|
1860
2337
|
const journal = createTelegramUpdateJournalStore(options);
|
|
1861
|
-
// Scoped completion and receipt observation use strict private handles
|
|
1862
|
-
|
|
1863
|
-
const strict = deps.strictSourceAccess ?? Boolean(constants.O_NOFOLLOW && constants.O_NONBLOCK);
|
|
2338
|
+
// Scoped completion and receipt observation always use strict private handles; see TELEGRAM_STRICT_READ_FLAGS.
|
|
2339
|
+
const strict = deps.strictSourceAccess ?? true;
|
|
1864
2340
|
const completionJournal = deps.withSourceSerialization
|
|
1865
|
-
? createTelegramUpdateJournalStore({
|
|
2341
|
+
? createTelegramUpdateJournalStore({
|
|
2342
|
+
...options,
|
|
2343
|
+
...(strict ? { sourceAccess } : {}),
|
|
2344
|
+
})
|
|
2345
|
+
: undefined;
|
|
1866
2346
|
// Queue receipt readiness gates every ordinary prompt; without strict handles it uses the ordinary journal read.
|
|
1867
2347
|
const observeQueuedReceipt = (expected) => strict
|
|
1868
2348
|
? completionJournal.inspectQueuedReceipt(expected)
|
|
1869
|
-
: inspectJournalQueuedReceipt(journal.read(), {
|
|
2349
|
+
: inspectJournalQueuedReceipt(journal.read(), {
|
|
2350
|
+
...expected,
|
|
2351
|
+
queueOwner: validateQueuedReceiptObservation(expected, path),
|
|
2352
|
+
}, path);
|
|
1870
2353
|
return {
|
|
1871
2354
|
runtimeKey: JSON.stringify({
|
|
1872
2355
|
path,
|
|
@@ -1881,25 +2364,48 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
|
|
|
1881
2364
|
readForProtection() {
|
|
1882
2365
|
// Protection must never turn corruption/recovery into empty-work evidence.
|
|
1883
2366
|
const evidence = inspectTelegramUpdateJournalFamily({
|
|
1884
|
-
...sourceAccess,
|
|
2367
|
+
...sourceAccess,
|
|
2368
|
+
path,
|
|
2369
|
+
profile: profileName,
|
|
2370
|
+
botIdentity,
|
|
1885
2371
|
});
|
|
1886
|
-
return {
|
|
2372
|
+
return {
|
|
2373
|
+
entries: evidence.kind === "present" ? evidence.file.entries : [],
|
|
2374
|
+
exists: evidence.kind === "present",
|
|
2375
|
+
};
|
|
1887
2376
|
},
|
|
1888
|
-
journal: completionJournal
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
2377
|
+
journal: completionJournal
|
|
2378
|
+
? {
|
|
2379
|
+
...journal,
|
|
2380
|
+
removeCompletedExact(updateIds, expectedSources, completions, isCurrent) {
|
|
2381
|
+
return completions !== undefined
|
|
2382
|
+
? completionJournal.removeCompletedExact(updateIds, expectedSources, completions, isCurrent)
|
|
2383
|
+
: journal.removeCompletedExact(updateIds, expectedSources, undefined, isCurrent);
|
|
2384
|
+
},
|
|
2385
|
+
completeQueuedExact(receipts, completions) {
|
|
2386
|
+
return completionJournal.completeQueuedExact(receipts, completions);
|
|
2387
|
+
},
|
|
2388
|
+
inspectSourceCompletion(expected) {
|
|
2389
|
+
return completionJournal.inspectSourceCompletion(expected);
|
|
2390
|
+
},
|
|
2391
|
+
inspectQueuedReceipt: observeQueuedReceipt,
|
|
2392
|
+
isQueueReceiptCurrent(receipt, owner) {
|
|
2393
|
+
if (receipt.journalBindingKey !==
|
|
2394
|
+
createTelegramUpdateJournalBindingKey({
|
|
2395
|
+
path,
|
|
2396
|
+
profileName,
|
|
2397
|
+
botIdentity,
|
|
2398
|
+
}))
|
|
2399
|
+
return false;
|
|
2400
|
+
return (observeQueuedReceipt({
|
|
2401
|
+
queueKind: receipt.queueKind,
|
|
2402
|
+
receiptId: receipt.receiptId,
|
|
2403
|
+
sourceUpdateIds: [...receipt.sourceUpdateIds],
|
|
2404
|
+
queueOwner: { ...owner },
|
|
2405
|
+
}) !== undefined);
|
|
2406
|
+
},
|
|
2407
|
+
}
|
|
2408
|
+
: journal,
|
|
1903
2409
|
};
|
|
1904
2410
|
};
|
|
1905
2411
|
}
|
|
@@ -1920,12 +2426,13 @@ export function createTelegramUpdateJournalReferenceRegistry(input = {}) {
|
|
|
1920
2426
|
released = true;
|
|
1921
2427
|
};
|
|
1922
2428
|
},
|
|
1923
|
-
list: () => [...active.values()].map(reference => ({ ...reference })),
|
|
2429
|
+
list: () => [...active.values()].map((reference) => ({ ...reference })),
|
|
1924
2430
|
withReference(reference, operation) {
|
|
1925
2431
|
const release = this.acquire(reference);
|
|
1926
2432
|
try {
|
|
1927
2433
|
const result = operation();
|
|
1928
|
-
if (result &&
|
|
2434
|
+
if (result &&
|
|
2435
|
+
typeof result.finally === "function")
|
|
1929
2436
|
return result.finally(release);
|
|
1930
2437
|
release();
|
|
1931
2438
|
return result;
|
|
@@ -1941,15 +2448,23 @@ export function withTelegramResolvedUpdateJournalReference(input) {
|
|
|
1941
2448
|
const binding = input.resolveBinding();
|
|
1942
2449
|
if (!binding)
|
|
1943
2450
|
return undefined;
|
|
1944
|
-
return input.registry.withReference({ referenceClass: input.referenceClass,
|
|
1945
|
-
recoveryKey: binding.recoveryKey }, () => input.operation(binding));
|
|
2451
|
+
return input.registry.withReference({ referenceClass: input.referenceClass, recoveryKey: binding.recoveryKey }, () => input.operation(binding));
|
|
1946
2452
|
}
|
|
1947
2453
|
function validateQueuedReceiptObservation(expected, path) {
|
|
1948
|
-
if (!isRecord(expected) ||
|
|
2454
|
+
if (!isRecord(expected) ||
|
|
2455
|
+
!hasOnlyKeys(expected, [
|
|
2456
|
+
"queueKind",
|
|
2457
|
+
"receiptId",
|
|
2458
|
+
"sourceUpdateIds",
|
|
2459
|
+
"queueOwner",
|
|
2460
|
+
]) ||
|
|
2461
|
+
(expected.queueKind !== "prompt" && expected.queueKind !== "control") ||
|
|
1949
2462
|
!isNonEmptyString(expected.receiptId) ||
|
|
1950
|
-
!Array.isArray(expected.sourceUpdateIds) ||
|
|
2463
|
+
!Array.isArray(expected.sourceUpdateIds) ||
|
|
2464
|
+
expected.sourceUpdateIds.length === 0 ||
|
|
1951
2465
|
expected.sourceUpdateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
|
|
1952
|
-
expected.sourceUpdateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
|
|
2466
|
+
expected.sourceUpdateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
|
|
2467
|
+
(index > 0 && id <= expected.sourceUpdateIds[index - 1]))) {
|
|
1953
2468
|
throw createJournalError("invalid", path, "received invalid queue receipt observation authority");
|
|
1954
2469
|
}
|
|
1955
2470
|
return validateJournalQueueOwner(expected.queueOwner, path);
|
|
@@ -1958,19 +2473,31 @@ function inspectJournalQueuedReceipt(file, expected, path) {
|
|
|
1958
2473
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
|
|
1959
2474
|
throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
|
|
1960
2475
|
const { queueOwner } = expected;
|
|
1961
|
-
const entries = file.entries.filter(entry => entry.queueReceiptId === expected.receiptId);
|
|
1962
|
-
if (entries.length !== expected.sourceUpdateIds.length ||
|
|
1963
|
-
entry.
|
|
2476
|
+
const entries = file.entries.filter((entry) => entry.queueReceiptId === expected.receiptId);
|
|
2477
|
+
if (entries.length !== expected.sourceUpdateIds.length ||
|
|
2478
|
+
entries.some((entry, index) => entry.updateId !== expected.sourceUpdateIds[index] ||
|
|
2479
|
+
entry.state !== "queued" ||
|
|
2480
|
+
entry.queueKind !== expected.queueKind ||
|
|
2481
|
+
entry.queueHandoff !== undefined ||
|
|
2482
|
+
!isDeepStrictEqual(entry.queueOwner, queueOwner)))
|
|
1964
2483
|
return undefined;
|
|
1965
|
-
return {
|
|
1966
|
-
|
|
2484
|
+
return {
|
|
2485
|
+
receipt: {
|
|
2486
|
+
queueKind: expected.queueKind,
|
|
2487
|
+
receiptId: expected.receiptId,
|
|
2488
|
+
sourceUpdateIds: [...expected.sourceUpdateIds],
|
|
2489
|
+
queueOwner: { ...queueOwner },
|
|
2490
|
+
},
|
|
1967
2491
|
sources: entries.map(createTelegramUpdateJournalEntryDigest),
|
|
1968
|
-
queueOwnerSha256: createHash("sha256")
|
|
2492
|
+
queueOwnerSha256: createHash("sha256")
|
|
2493
|
+
.update(JSON.stringify(queueOwner))
|
|
2494
|
+
.digest("hex"),
|
|
2495
|
+
};
|
|
1969
2496
|
}
|
|
1970
2497
|
function inspectJournalSourceCompletion(file, completion, path) {
|
|
1971
2498
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
|
|
1972
2499
|
throw createJournalError("invalid", path, "source completion requires a legacy v1 source");
|
|
1973
|
-
const retained = file.sourceCompletions?.find(value => value.updateId === completion.updateId);
|
|
2500
|
+
const retained = file.sourceCompletions?.find((value) => value.updateId === completion.updateId);
|
|
1974
2501
|
if (!retained)
|
|
1975
2502
|
return undefined;
|
|
1976
2503
|
if (!isDeepStrictEqual(retained, completion))
|
|
@@ -1980,35 +2507,48 @@ function inspectJournalSourceCompletion(file, completion, path) {
|
|
|
1980
2507
|
/** Operator-distinct authority: routing never treats it as an owner cancellation. */
|
|
1981
2508
|
export const TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX = "session-successor:";
|
|
1982
2509
|
function isAdoptablePendingEntry(entry) {
|
|
1983
|
-
return entry.state === "pending" &&
|
|
1984
|
-
!entry.
|
|
1985
|
-
entry.
|
|
2510
|
+
return (entry.state === "pending" &&
|
|
2511
|
+
!entry.inputClaim &&
|
|
2512
|
+
!entry.inputProvenance &&
|
|
2513
|
+
!entry.routingInput &&
|
|
2514
|
+
!entry.queueOwner &&
|
|
2515
|
+
!entry.queueHandoff &&
|
|
2516
|
+
!entry.queueReceiptId &&
|
|
2517
|
+
!entry.queueKind &&
|
|
2518
|
+
!entry.failure &&
|
|
2519
|
+
entry.preApprovalExcluded !== true);
|
|
1986
2520
|
}
|
|
1987
2521
|
export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
1988
2522
|
const resolveLeader = createTelegramUpdateJournalRuntimeBindingResolver({
|
|
1989
2523
|
...deps.base,
|
|
1990
2524
|
getJournalPath: deps.getLeaderJournalPath,
|
|
1991
2525
|
});
|
|
2526
|
+
// Follower and path journals share identity and admission ports, never the leader's queue identity or strict source mode.
|
|
2527
|
+
const getSharedResolverPorts = () => ({
|
|
2528
|
+
getProfileName: deps.base.getProfileName,
|
|
2529
|
+
getBotToken: deps.base.getBotToken,
|
|
2530
|
+
getBotId: deps.base.getBotId,
|
|
2531
|
+
...(deps.base.withWriterAdmission
|
|
2532
|
+
? { withWriterAdmission: deps.base.withWriterAdmission }
|
|
2533
|
+
: {}),
|
|
2534
|
+
// Follower journals need the same strict exact-receipt ports as the leader; without them queue publication cannot complete.
|
|
2535
|
+
...(deps.base.withSourceSerialization
|
|
2536
|
+
? { withSourceSerialization: deps.base.withSourceSerialization }
|
|
2537
|
+
: {}),
|
|
2538
|
+
...(deps.base.getWorkspaceAdmission
|
|
2539
|
+
? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
|
|
2540
|
+
: {}),
|
|
2541
|
+
...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
|
|
2542
|
+
});
|
|
1992
2543
|
const createFollowerResolver = (bindingKey, includeQueueRuntimeIdentity, sessionId, legacy = false) => {
|
|
1993
2544
|
// Session-aware composition must not fall back to a process journal before preparation supplies its ID.
|
|
1994
2545
|
if (!legacy && deps.getActiveFollowerSessionId && !sessionId)
|
|
1995
2546
|
return () => undefined;
|
|
1996
2547
|
return createTelegramUpdateJournalRuntimeBindingResolver({
|
|
1997
|
-
|
|
1998
|
-
getBotToken: deps.base.getBotToken,
|
|
1999
|
-
getBotId: deps.base.getBotId,
|
|
2548
|
+
...getSharedResolverPorts(),
|
|
2000
2549
|
...(includeQueueRuntimeIdentity && deps.base.getQueueRuntimeIdentity
|
|
2001
2550
|
? { getQueueRuntimeIdentity: deps.base.getQueueRuntimeIdentity }
|
|
2002
2551
|
: {}),
|
|
2003
|
-
...(deps.base.withWriterAdmission
|
|
2004
|
-
? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
|
|
2005
|
-
// Follower journals need the same strict exact-receipt ports as the leader; without them queue publication cannot complete.
|
|
2006
|
-
...(deps.base.withSourceSerialization
|
|
2007
|
-
? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
|
|
2008
|
-
...(deps.base.getWorkspaceAdmission
|
|
2009
|
-
? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
|
|
2010
|
-
: {}),
|
|
2011
|
-
...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
|
|
2012
2552
|
getJournalPath(profileName) {
|
|
2013
2553
|
return deps.getFollowerJournalPath(bindingKey, profileName, sessionId);
|
|
2014
2554
|
},
|
|
@@ -2022,8 +2562,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2022
2562
|
const profile = deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
2023
2563
|
if (!path || !token)
|
|
2024
2564
|
return undefined;
|
|
2025
|
-
const botIdentity = createTelegramUpdateJournalBotIdentity({
|
|
2026
|
-
|
|
2565
|
+
const botIdentity = createTelegramUpdateJournalBotIdentity({
|
|
2566
|
+
botToken: token,
|
|
2567
|
+
botId,
|
|
2568
|
+
});
|
|
2569
|
+
const keys = [botIdentity, { tokenSha256: botIdentity.tokenSha256 }].map((identity) => createTelegramUpdateJournalBindingKey({
|
|
2570
|
+
path,
|
|
2571
|
+
profileName: profile,
|
|
2572
|
+
botIdentity: identity,
|
|
2573
|
+
}));
|
|
2027
2574
|
if (!keys.includes(journalBindingKey))
|
|
2028
2575
|
return undefined;
|
|
2029
2576
|
const getRoot = () => deps.getRuntimeDir?.() ?? dirname(deps.getLeaderJournalPath(profile));
|
|
@@ -2032,23 +2579,43 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2032
2579
|
const suffix = escapeTelegramJournalPathPattern(getTelegramProfilePathSuffix(profile));
|
|
2033
2580
|
const flat = new RegExp(`^(?:inbox|follower-inbox-[a-f0-9]{16})${suffix}\\.json$`, "u");
|
|
2034
2581
|
const session = new RegExp(`^(?:journal\\.[a-f0-9]{16}|inbox)${suffix}\\.json$`, "u");
|
|
2035
|
-
if (!isAbsolute(root) ||
|
|
2036
|
-
|
|
2037
|
-
|
|
2582
|
+
if (!isAbsolute(root) ||
|
|
2583
|
+
resolve(root) !== root ||
|
|
2584
|
+
!isAbsolute(path) ||
|
|
2585
|
+
resolve(path) !== path ||
|
|
2586
|
+
!((parts.length === 1 && flat.test(parts[0])) ||
|
|
2587
|
+
(parts.length === 3 &&
|
|
2588
|
+
parts[0] === "sessions" &&
|
|
2589
|
+
decodeTelegramSessionDirectoryName(parts[1]) !== undefined &&
|
|
2590
|
+
session.test(parts[2]))))
|
|
2038
2591
|
return undefined;
|
|
2039
|
-
const current = () => deps.base.getBotToken() === token &&
|
|
2040
|
-
|
|
2592
|
+
const current = () => deps.base.getBotToken() === token &&
|
|
2593
|
+
deps.base.getBotId() === botId &&
|
|
2594
|
+
(deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME) ===
|
|
2595
|
+
profile &&
|
|
2596
|
+
getRoot() === root;
|
|
2041
2597
|
const inspect = () => {
|
|
2042
2598
|
if (!current())
|
|
2043
2599
|
return undefined;
|
|
2044
|
-
const observed = inspectTelegramUpdateJournalRetention({
|
|
2045
|
-
|
|
2046
|
-
|
|
2600
|
+
const observed = inspectTelegramUpdateJournalRetention({
|
|
2601
|
+
directory: root,
|
|
2602
|
+
path,
|
|
2603
|
+
profile,
|
|
2604
|
+
botIdentity,
|
|
2605
|
+
limits: {
|
|
2606
|
+
maxFiles: 1024,
|
|
2607
|
+
maxBytes: TELEGRAM_UPDATE_JOURNAL_MAX_BYTES * 2,
|
|
2608
|
+
maxEntries: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES,
|
|
2609
|
+
maxWork: TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES * 1024,
|
|
2610
|
+
},
|
|
2611
|
+
});
|
|
2047
2612
|
if (observed.evidence.kind !== "present" || !current())
|
|
2048
2613
|
return undefined;
|
|
2049
2614
|
return operation(observed.evidence.file, observed.retainedInputs, path);
|
|
2050
2615
|
};
|
|
2051
|
-
const result = deps.base.withSourceSerialization
|
|
2616
|
+
const result = deps.base.withSourceSerialization
|
|
2617
|
+
? deps.base.withSourceSerialization(inspect)
|
|
2618
|
+
: inspect();
|
|
2052
2619
|
return current() ? result : undefined;
|
|
2053
2620
|
};
|
|
2054
2621
|
let preparedFollower;
|
|
@@ -2063,9 +2630,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2063
2630
|
if (!sessionId)
|
|
2064
2631
|
return undefined;
|
|
2065
2632
|
const previous = preparedFollower;
|
|
2066
|
-
const result = previous &&
|
|
2067
|
-
|
|
2068
|
-
|
|
2633
|
+
const result = previous &&
|
|
2634
|
+
previous.recipientBindingKey === recipientBindingKey &&
|
|
2635
|
+
previous.sessionId !== sessionId
|
|
2636
|
+
? runtime.adoptPredecessorPending({
|
|
2637
|
+
recipientBindingKey,
|
|
2638
|
+
predecessorSessionId: previous.sessionId,
|
|
2639
|
+
successorSessionId: sessionId,
|
|
2640
|
+
isCurrent,
|
|
2641
|
+
})
|
|
2069
2642
|
: undefined;
|
|
2070
2643
|
// Only a completed adoption advances the predecessor; failures retry idempotently on the next preparation.
|
|
2071
2644
|
preparedFollower = { recipientBindingKey, sessionId };
|
|
@@ -2075,18 +2648,26 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2075
2648
|
if (!isSafeNonNegativeInteger(updateId))
|
|
2076
2649
|
throw new Error("Telegram abandonment proof requires an exact update ID.");
|
|
2077
2650
|
return inspectHistoricalSource(journalBindingKey, (file, originals) => {
|
|
2078
|
-
const original = originals.find(value => value.updateId === updateId &&
|
|
2651
|
+
const original = originals.find((value) => value.updateId === updateId &&
|
|
2652
|
+
value.state === "committed" &&
|
|
2079
2653
|
value.journalBindingKey === journalBindingKey);
|
|
2080
2654
|
if (!original)
|
|
2081
2655
|
return undefined;
|
|
2082
|
-
const disposition = file.operatorDispositions?.find(value => value.failureId === original.failureId);
|
|
2083
|
-
if (!disposition ||
|
|
2656
|
+
const disposition = file.operatorDispositions?.find((value) => value.failureId === original.failureId);
|
|
2657
|
+
if (!disposition ||
|
|
2658
|
+
!("dispositionKind" in disposition) ||
|
|
2659
|
+
disposition.dispositionKind !== "legacy-custody")
|
|
2084
2660
|
return undefined;
|
|
2085
|
-
return {
|
|
2661
|
+
return {
|
|
2662
|
+
journalBindingKey,
|
|
2663
|
+
updateId,
|
|
2664
|
+
retainedPath: original.path,
|
|
2665
|
+
operatorAuthorityId: disposition.operatorAuthorityId,
|
|
2666
|
+
};
|
|
2086
2667
|
});
|
|
2087
2668
|
},
|
|
2088
2669
|
inspectSourceGroupExpiry(journalBindingKey, updateIds) {
|
|
2089
|
-
return inspectHistoricalSource(journalBindingKey, file => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
|
|
2670
|
+
return inspectHistoricalSource(journalBindingKey, (file) => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
|
|
2090
2671
|
},
|
|
2091
2672
|
inspectSourceCompletion(journalBindingKey, expected) {
|
|
2092
2673
|
const completion = validateJournalSourceCompletion(expected, getTelegramUpdateJournalBindingPath(journalBindingKey) ?? "journal");
|
|
@@ -2097,9 +2678,12 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2097
2678
|
return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalQueuedReceipt(file, { ...expected, queueOwner }, path));
|
|
2098
2679
|
},
|
|
2099
2680
|
adoptPredecessorPending(input) {
|
|
2100
|
-
const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent } = input;
|
|
2101
|
-
if (!recipientBindingKey ||
|
|
2102
|
-
predecessorSessionId
|
|
2681
|
+
const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent, } = input;
|
|
2682
|
+
if (!recipientBindingKey ||
|
|
2683
|
+
!predecessorSessionId ||
|
|
2684
|
+
!successorSessionId ||
|
|
2685
|
+
predecessorSessionId === successorSessionId ||
|
|
2686
|
+
typeof isCurrent !== "function") {
|
|
2103
2687
|
throw new Error("Telegram session adoption requires distinct exact session identities.");
|
|
2104
2688
|
}
|
|
2105
2689
|
const assertCurrent = () => {
|
|
@@ -2109,18 +2693,29 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2109
2693
|
assertCurrent();
|
|
2110
2694
|
const predecessor = createFollowerResolver(recipientBindingKey, false, predecessorSessionId)();
|
|
2111
2695
|
const successor = createFollowerResolver(recipientBindingKey, false, successorSessionId)();
|
|
2112
|
-
if (!predecessor?.readForProtection ||
|
|
2696
|
+
if (!predecessor?.readForProtection ||
|
|
2697
|
+
!successor ||
|
|
2698
|
+
predecessor.recoveryKey === successor.recoveryKey) {
|
|
2113
2699
|
throw new Error("Telegram session adoption requires exact predecessor and successor journals.");
|
|
2114
2700
|
}
|
|
2115
|
-
const result = {
|
|
2701
|
+
const result = {
|
|
2702
|
+
adoptedUpdateIds: [],
|
|
2703
|
+
retainedUpdateIds: [],
|
|
2704
|
+
};
|
|
2116
2705
|
// Non-repairing selection; the abandonment CAS rechecks each exact entry under the source lock.
|
|
2117
|
-
const candidates = predecessor
|
|
2706
|
+
const candidates = predecessor
|
|
2707
|
+
.readForProtection()
|
|
2708
|
+
.entries.filter(isAdoptablePendingEntry)
|
|
2118
2709
|
.sort((left, right) => left.updateId - right.updateId);
|
|
2119
2710
|
for (const entry of candidates) {
|
|
2120
2711
|
assertCurrent();
|
|
2121
2712
|
// Commit away first: a crash afterwards preserves the private original instead of duplicating execution.
|
|
2122
|
-
const committed = predecessor.journal.abandonPending({
|
|
2123
|
-
|
|
2713
|
+
const committed = predecessor.journal.abandonPending({
|
|
2714
|
+
journalBindingKey: predecessor.recoveryKey,
|
|
2715
|
+
entry,
|
|
2716
|
+
operatorAuthorityId: `${TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX}${successorSessionId}`,
|
|
2717
|
+
isCurrent,
|
|
2718
|
+
});
|
|
2124
2719
|
if (committed.duplicate) {
|
|
2125
2720
|
result.retainedUpdateIds.push(entry.updateId);
|
|
2126
2721
|
continue;
|
|
@@ -2134,17 +2729,7 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2134
2729
|
createRecipientResolver: (bindingKey, sessionId) => createFollowerResolver(bindingKey, false, sessionId),
|
|
2135
2730
|
createLegacyRecipientResolver: (bindingKey) => createFollowerResolver(bindingKey, false, undefined, true),
|
|
2136
2731
|
createPathResolver: (path) => createTelegramUpdateJournalRuntimeBindingResolver({
|
|
2137
|
-
|
|
2138
|
-
getBotToken: deps.base.getBotToken,
|
|
2139
|
-
getBotId: deps.base.getBotId,
|
|
2140
|
-
...(deps.base.withWriterAdmission
|
|
2141
|
-
? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
|
|
2142
|
-
...(deps.base.withSourceSerialization
|
|
2143
|
-
? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
|
|
2144
|
-
...(deps.base.getWorkspaceAdmission
|
|
2145
|
-
? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
|
|
2146
|
-
: {}),
|
|
2147
|
-
...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
|
|
2732
|
+
...getSharedResolverPorts(),
|
|
2148
2733
|
getJournalPath: () => path,
|
|
2149
2734
|
}),
|
|
2150
2735
|
};
|
|
@@ -2156,15 +2741,26 @@ export function createTelegramUpdateJournalStore(options) {
|
|
|
2156
2741
|
/** Opt-in v3 only; does not migrate old files or expose legacy unowned mutation ports. */
|
|
2157
2742
|
export function createTelegramInputJournalStore(options) {
|
|
2158
2743
|
const captured = { ...options };
|
|
2159
|
-
if (!captured.sourceAccess ||
|
|
2744
|
+
if (!captured.sourceAccess ||
|
|
2745
|
+
!captured.queueRuntimeIdentity ||
|
|
2160
2746
|
typeof captured.withSourceSerialization !== "function" ||
|
|
2161
2747
|
typeof captured.withPairingAdmission !== "function" ||
|
|
2162
2748
|
typeof captured.getInputContext !== "function" ||
|
|
2163
|
-
("withPairedAdmission" in captured &&
|
|
2749
|
+
("withPairedAdmission" in captured &&
|
|
2750
|
+
captured.withPairedAdmission !== undefined)) {
|
|
2164
2751
|
throw new Error("Telegram input custody requires strict serialized polling admission and runtime identity.");
|
|
2165
2752
|
}
|
|
2166
2753
|
return createJournalStoreCore({ ...captured, queueRuntimeIdentity: { ...captured.queueRuntimeIdentity } }, captured.getInputContext).input;
|
|
2167
2754
|
}
|
|
2755
|
+
/** Exact unhanded queued receipt member: requested id, queue kind and owner identity. */
|
|
2756
|
+
function isExactQueuedReceiptEntry(entry, requestedIds, queueKind, owner) {
|
|
2757
|
+
return (requestedIds.has(entry.updateId) &&
|
|
2758
|
+
entry.state === "queued" &&
|
|
2759
|
+
entry.queueKind === queueKind &&
|
|
2760
|
+
!!entry.queueOwner &&
|
|
2761
|
+
entry.queueHandoff === undefined &&
|
|
2762
|
+
areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, owner));
|
|
2763
|
+
}
|
|
2168
2764
|
function createJournalStoreCore(options, getInputContext) {
|
|
2169
2765
|
const path = options.path;
|
|
2170
2766
|
const profile = options.profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
@@ -2198,8 +2794,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2198
2794
|
if (withPairingAdmission && withPairedAdmission) {
|
|
2199
2795
|
throw new Error("Telegram journal admission modes are mutually exclusive.");
|
|
2200
2796
|
}
|
|
2201
|
-
const version = getInputContext
|
|
2202
|
-
|
|
2797
|
+
const version = getInputContext
|
|
2798
|
+
? TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION
|
|
2799
|
+
: withPairingAdmission
|
|
2800
|
+
? TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION
|
|
2801
|
+
: TELEGRAM_UPDATE_JOURNAL_VERSION;
|
|
2203
2802
|
const notifyRecovery = (event) => {
|
|
2204
2803
|
try {
|
|
2205
2804
|
options.onRecovery?.(event);
|
|
@@ -2385,7 +2984,8 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2385
2984
|
}
|
|
2386
2985
|
if (segment.profile !== storedProfile ||
|
|
2387
2986
|
!identitiesMatch(segment.botIdentity, storedIdentity) ||
|
|
2388
|
-
(withPairingAdmission &&
|
|
2987
|
+
(withPairingAdmission &&
|
|
2988
|
+
segment.botIdentity.tokenSha256 !== storedIdentity.tokenSha256)) {
|
|
2389
2989
|
throw createJournalError("identity-mismatch", segmentPath, "belongs to another journal identity");
|
|
2390
2990
|
}
|
|
2391
2991
|
assertJournalSourceCompletionContinuity(file, segment, segmentPath);
|
|
@@ -2401,8 +3001,10 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2401
3001
|
for (const entry of segment.upsertedEntries) {
|
|
2402
3002
|
const previous = entriesById.get(entry.updateId);
|
|
2403
3003
|
if (withPairingAdmission &&
|
|
2404
|
-
((previous &&
|
|
2405
|
-
|
|
3004
|
+
((previous &&
|
|
3005
|
+
previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
|
|
3006
|
+
(!previous &&
|
|
3007
|
+
entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
|
|
2406
3008
|
throw createJournalError("pairing-evidence", segmentPath, "changes exclusion evidence or resurrects a settled source");
|
|
2407
3009
|
}
|
|
2408
3010
|
entriesById.set(entry.updateId, entry);
|
|
@@ -2425,16 +3027,23 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2425
3027
|
: file.operatorDispositions?.length
|
|
2426
3028
|
? { operatorDispositions: file.operatorDispositions }
|
|
2427
3029
|
: {}),
|
|
2428
|
-
...(segment.sourceCompletions !== undefined
|
|
2429
|
-
|
|
3030
|
+
...(segment.sourceCompletions !== undefined
|
|
3031
|
+
? { sourceCompletions: segment.sourceCompletions }
|
|
3032
|
+
: file.sourceCompletions?.length
|
|
3033
|
+
? { sourceCompletions: file.sourceCompletions }
|
|
3034
|
+
: {}),
|
|
2430
3035
|
}, segmentPath, version);
|
|
2431
3036
|
revision = segment.revision;
|
|
2432
3037
|
}
|
|
2433
3038
|
const identityChanged = storedProfile !== profile ||
|
|
2434
3039
|
!identitiesMatch(file.botIdentity, expectedIdentity) ||
|
|
2435
|
-
((withPairingAdmission || !!file.sourceCompletions?.length) &&
|
|
3040
|
+
((withPairingAdmission || !!file.sourceCompletions?.length) &&
|
|
3041
|
+
file.botIdentity.tokenSha256 !== expectedIdentity.tokenSha256);
|
|
2436
3042
|
if (identityChanged) {
|
|
2437
|
-
if (file.entries.length > 0 ||
|
|
3043
|
+
if (file.entries.length > 0 ||
|
|
3044
|
+
file.sourceCompletions?.length ||
|
|
3045
|
+
withPairingAdmission ||
|
|
3046
|
+
!allowReconciliation) {
|
|
2438
3047
|
throw createJournalError("identity-mismatch", path, storedProfile !== profile
|
|
2439
3048
|
? `belongs to profile ${storedProfile}, not ${profile}`
|
|
2440
3049
|
: "belongs to another Telegram bot identity");
|
|
@@ -2616,15 +3225,26 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2616
3225
|
try {
|
|
2617
3226
|
// Acquire before the transaction helper can create parents or staging names.
|
|
2618
3227
|
// The config continuation excludes participating writers through consumption.
|
|
2619
|
-
const source = sourceAccess
|
|
2620
|
-
|
|
2621
|
-
|
|
3228
|
+
const source = sourceAccess
|
|
3229
|
+
? acquireTelegramUpdateJournalFamily({
|
|
3230
|
+
...sourceAccess,
|
|
3231
|
+
path,
|
|
3232
|
+
profile,
|
|
3233
|
+
botIdentity: expectedIdentity,
|
|
3234
|
+
}, version)
|
|
3235
|
+
: undefined;
|
|
2622
3236
|
const readSource = () => {
|
|
2623
3237
|
if (!source)
|
|
2624
3238
|
return readCurrent();
|
|
2625
|
-
const file = source.evidence.kind === "present"
|
|
2626
|
-
|
|
2627
|
-
|
|
3239
|
+
const file = source.evidence.kind === "present"
|
|
3240
|
+
? source.evidence.file
|
|
3241
|
+
: emptyFile();
|
|
3242
|
+
return {
|
|
3243
|
+
file,
|
|
3244
|
+
exists: source.evidence.kind === "present",
|
|
3245
|
+
serializedBytes: assertCapacity(file),
|
|
3246
|
+
source,
|
|
3247
|
+
};
|
|
2628
3248
|
};
|
|
2629
3249
|
return withTelegramFileTransaction(`${path}.transaction`, () => operation(readSource));
|
|
2630
3250
|
}
|
|
@@ -2644,7 +3264,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2644
3264
|
if (!sourceAccess)
|
|
2645
3265
|
return;
|
|
2646
3266
|
const limits = sourceAccess.limits;
|
|
2647
|
-
if (files > limits.maxFiles ||
|
|
3267
|
+
if (files > limits.maxFiles ||
|
|
3268
|
+
bytes > limits.maxBytes ||
|
|
3269
|
+
work > limits.maxWork ||
|
|
2648
3270
|
collections.some((length) => length > limits.maxEntries)) {
|
|
2649
3271
|
throw createJournalError("capacity", path, "publication would exceed source inspection resource limits");
|
|
2650
3272
|
}
|
|
@@ -2652,20 +3274,31 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2652
3274
|
const inputHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH);
|
|
2653
3275
|
const inputBindingHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH);
|
|
2654
3276
|
const inputHandoffHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH);
|
|
2655
|
-
const getFileWork = (file) => file.entries.length +
|
|
3277
|
+
const getFileWork = (file) => file.entries.length +
|
|
3278
|
+
(file.operatorDispositions?.length ?? 0) +
|
|
3279
|
+
(file.sourceCompletions?.length ?? 0);
|
|
2656
3280
|
const advanceInputHeadroomState = (state, entries, projectionSlack) => {
|
|
2657
3281
|
const revision = (state.file.revision ?? 0) + 1;
|
|
2658
3282
|
if (!isSafePositiveInteger(revision))
|
|
2659
3283
|
throw createJournalError("capacity", path, "exhausted input custody revisions");
|
|
2660
3284
|
return { file: { ...state.file, revision, entries }, projectionSlack };
|
|
2661
3285
|
};
|
|
2662
|
-
const replaceInputHeadroomEntry = (file, updateId, replacement) => file.entries.flatMap(entry => entry.updateId === updateId
|
|
3286
|
+
const replaceInputHeadroomEntry = (file, updateId, replacement) => file.entries.flatMap((entry) => entry.updateId === updateId
|
|
3287
|
+
? replacement
|
|
3288
|
+
? [replacement]
|
|
3289
|
+
: []
|
|
3290
|
+
: [entry]);
|
|
2663
3291
|
const createInputHeadroomClaim = (entry, phase) => ({
|
|
2664
3292
|
phase,
|
|
2665
|
-
owner: {
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3293
|
+
owner: {
|
|
3294
|
+
instanceId: inputHeadroomText,
|
|
3295
|
+
processId: Number.MAX_SAFE_INTEGER,
|
|
3296
|
+
processBirthId: inputHeadroomText,
|
|
3297
|
+
sessionGeneration: Number.MAX_SAFE_INTEGER,
|
|
3298
|
+
acquisitionId: inputHeadroomText,
|
|
3299
|
+
acquiredAtMs: Number.MAX_SAFE_INTEGER,
|
|
3300
|
+
handoffId: inputHandoffHeadroomText,
|
|
3301
|
+
},
|
|
2669
3302
|
recipientBindingKey: inputBindingHeadroomText,
|
|
2670
3303
|
executionUpdate: structuredClone(entry.update),
|
|
2671
3304
|
});
|
|
@@ -2680,29 +3313,64 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2680
3313
|
const createInputHeadroomTransitionPlan = (file, entry) => {
|
|
2681
3314
|
const initial = { file, projectionSlack: 0 };
|
|
2682
3315
|
if (entry.inputClaim?.phase === "running")
|
|
2683
|
-
return [
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
3316
|
+
return [
|
|
3317
|
+
advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3318
|
+
];
|
|
3319
|
+
if (entry.state === "pending" &&
|
|
3320
|
+
entry.inputClaim?.phase === "ready" &&
|
|
3321
|
+
entry.inputClaim.handoff) {
|
|
3322
|
+
const acceptedClaim = {
|
|
3323
|
+
...entry.inputClaim,
|
|
3324
|
+
owner: {
|
|
3325
|
+
...entry.inputClaim.handoff.recipientOwner,
|
|
3326
|
+
acquisitionId: inputHeadroomText,
|
|
3327
|
+
acquiredAtMs: Number.MAX_SAFE_INTEGER,
|
|
3328
|
+
handoffId: inputHandoffHeadroomText,
|
|
3329
|
+
},
|
|
3330
|
+
};
|
|
2688
3331
|
delete acceptedClaim.handoff;
|
|
2689
3332
|
const acceptedEntry = { ...entry, inputClaim: acceptedClaim };
|
|
2690
3333
|
const acceptedState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, acceptedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2691
|
-
return [
|
|
3334
|
+
return [
|
|
3335
|
+
acceptedState,
|
|
3336
|
+
...createInputHeadroomTransitionPlan(acceptedState.file, acceptedEntry),
|
|
3337
|
+
];
|
|
2692
3338
|
}
|
|
2693
3339
|
if (entry.state === "pending" && entry.inputClaim?.phase === "ready") {
|
|
2694
|
-
const runningEntry = {
|
|
3340
|
+
const runningEntry = {
|
|
3341
|
+
...entry,
|
|
3342
|
+
inputClaim: { ...entry.inputClaim, phase: "running" },
|
|
3343
|
+
};
|
|
2695
3344
|
const runningState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2696
|
-
return [
|
|
3345
|
+
return [
|
|
3346
|
+
runningState,
|
|
3347
|
+
advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3348
|
+
];
|
|
2697
3349
|
}
|
|
2698
|
-
if (entry.state === "pending" &&
|
|
2699
|
-
|
|
3350
|
+
if (entry.state === "pending" &&
|
|
3351
|
+
entry.preApprovalExcluded === false &&
|
|
3352
|
+
!entry.inputClaim) {
|
|
3353
|
+
const readyEntry = {
|
|
3354
|
+
...entry,
|
|
3355
|
+
inputClaim: createInputHeadroomClaim(entry, "ready"),
|
|
3356
|
+
};
|
|
2700
3357
|
const readyState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, readyEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
|
|
2701
|
-
const runningEntry = {
|
|
3358
|
+
const runningEntry = {
|
|
3359
|
+
...readyEntry,
|
|
3360
|
+
inputClaim: { ...readyEntry.inputClaim, phase: "running" },
|
|
3361
|
+
};
|
|
2702
3362
|
const runningState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
|
|
2703
|
-
return [
|
|
3363
|
+
return [
|
|
3364
|
+
readyState,
|
|
3365
|
+
runningState,
|
|
3366
|
+
advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3367
|
+
];
|
|
2704
3368
|
}
|
|
2705
|
-
return entry.preApprovalExcluded === true
|
|
3369
|
+
return entry.preApprovalExcluded === true
|
|
3370
|
+
? [
|
|
3371
|
+
advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3372
|
+
]
|
|
3373
|
+
: [];
|
|
2706
3374
|
};
|
|
2707
3375
|
const createInputHeadroomCancelPlan = (file, entry) => {
|
|
2708
3376
|
if (!entry.inputClaim?.handoff)
|
|
@@ -2712,40 +3380,54 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2712
3380
|
const cancelledEntry = { ...entry, inputClaim: claim };
|
|
2713
3381
|
const initial = { file, projectionSlack: 0 };
|
|
2714
3382
|
const cancelledState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, cancelledEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2715
|
-
return [
|
|
3383
|
+
return [
|
|
3384
|
+
cancelledState,
|
|
3385
|
+
...createInputHeadroomTransitionPlan(cancelledState.file, cancelledEntry),
|
|
3386
|
+
];
|
|
2716
3387
|
};
|
|
2717
3388
|
const createInputHeadroomReleasePlan = (file, entry, allowOffered = false) => {
|
|
2718
3389
|
if (entry.inputClaim?.handoff && !allowOffered)
|
|
2719
3390
|
return [];
|
|
2720
3391
|
const normal = createInputHeadroomTransitionPlan(file, entry);
|
|
2721
|
-
const readyState = entry.inputClaim?.phase === "ready"
|
|
3392
|
+
const readyState = entry.inputClaim?.phase === "ready"
|
|
3393
|
+
? { file, projectionSlack: 0 }
|
|
3394
|
+
: normal[0];
|
|
2722
3395
|
if (!readyState)
|
|
2723
3396
|
return [];
|
|
2724
|
-
const readyEntry = readyState.file.entries.find(candidate => candidate.updateId === entry.updateId);
|
|
3397
|
+
const readyEntry = readyState.file.entries.find((candidate) => candidate.updateId === entry.updateId);
|
|
2725
3398
|
if (readyEntry?.inputClaim?.phase !== "ready")
|
|
2726
3399
|
return [];
|
|
2727
3400
|
const releasedEntry = cloneEntry(readyEntry);
|
|
2728
3401
|
delete releasedEntry.inputClaim;
|
|
2729
3402
|
const releasedState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, releasedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2730
3403
|
const reacquisition = createInputHeadroomTransitionPlan(releasedState.file, releasedEntry);
|
|
2731
|
-
return [
|
|
3404
|
+
return [
|
|
3405
|
+
...(readyState.file === file ? [] : [readyState]),
|
|
3406
|
+
releasedState,
|
|
3407
|
+
...reacquisition,
|
|
3408
|
+
];
|
|
2732
3409
|
};
|
|
2733
3410
|
const createInputHeadroomQueueCompletionPlan = (file, entries) => {
|
|
2734
|
-
if (entries.length === 0 ||
|
|
3411
|
+
if (entries.length === 0 ||
|
|
3412
|
+
entries.some((entry) => entry.state !== "queued" || entry.queueHandoff))
|
|
2735
3413
|
return [];
|
|
2736
|
-
const requestedIds = new Set(entries.map(entry => entry.updateId));
|
|
3414
|
+
const requestedIds = new Set(entries.map((entry) => entry.updateId));
|
|
2737
3415
|
const initial = { file, projectionSlack: 0 };
|
|
2738
|
-
return [
|
|
3416
|
+
return [
|
|
3417
|
+
advanceInputHeadroomState(initial, file.entries.filter((entry) => !requestedIds.has(entry.updateId)), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3418
|
+
];
|
|
2739
3419
|
};
|
|
2740
3420
|
const getInputHeadroomTransitionPlans = (file, updateId) => {
|
|
2741
3421
|
if (updateId !== "all") {
|
|
2742
3422
|
const requestedIds = new Set(typeof updateId === "number" ? [updateId] : [...updateId]);
|
|
2743
|
-
const requestedEntries = file.entries.filter(entry => requestedIds.has(entry.updateId));
|
|
3423
|
+
const requestedEntries = file.entries.filter((entry) => requestedIds.has(entry.updateId));
|
|
2744
3424
|
const queuedReceiptId = requestedEntries[0]?.queueReceiptId;
|
|
2745
|
-
if (requestedEntries.length === requestedIds.size &&
|
|
2746
|
-
|
|
3425
|
+
if (requestedEntries.length === requestedIds.size &&
|
|
3426
|
+
queuedReceiptId &&
|
|
3427
|
+
requestedEntries.every((entry) => entry.state === "queued" &&
|
|
2747
3428
|
entry.queueReceiptId === queuedReceiptId) &&
|
|
2748
|
-
file.entries.filter(entry => entry.queueReceiptId === queuedReceiptId)
|
|
3429
|
+
file.entries.filter((entry) => entry.queueReceiptId === queuedReceiptId)
|
|
3430
|
+
.length === requestedIds.size) {
|
|
2749
3431
|
const completion = createInputHeadroomQueueCompletionPlan(file, requestedEntries);
|
|
2750
3432
|
return completion.length > 0 ? [completion] : [];
|
|
2751
3433
|
}
|
|
@@ -2755,19 +3437,47 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2755
3437
|
const plan = entry ? createInputHeadroomTransitionPlan(file, entry) : [];
|
|
2756
3438
|
const cancel = entry ? createInputHeadroomCancelPlan(file, entry) : [];
|
|
2757
3439
|
const recover = entry?.inputClaim?.handoff
|
|
2758
|
-
? createInputHeadroomReleasePlan(file, entry, true)
|
|
2759
|
-
|
|
3440
|
+
? createInputHeadroomReleasePlan(file, entry, true)
|
|
3441
|
+
: [];
|
|
3442
|
+
return [plan, cancel, recover].filter((candidate) => candidate.length > 0);
|
|
2760
3443
|
}
|
|
2761
|
-
const running = smallestInputHeadroomEntry(file.entries.filter(entry => entry.inputClaim?.phase === "running"));
|
|
2762
|
-
const offered = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" &&
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
3444
|
+
const running = smallestInputHeadroomEntry(file.entries.filter((entry) => entry.inputClaim?.phase === "running"));
|
|
3445
|
+
const offered = largestInputHeadroomEntry(file.entries.filter((entry) => entry.state === "pending" &&
|
|
3446
|
+
entry.inputClaim?.phase === "ready" &&
|
|
3447
|
+
entry.inputClaim.handoff), (entry) => ({
|
|
3448
|
+
...entry,
|
|
3449
|
+
inputClaim: {
|
|
3450
|
+
...entry.inputClaim,
|
|
3451
|
+
owner: {
|
|
3452
|
+
...entry.inputClaim.handoff.recipientOwner,
|
|
3453
|
+
acquisitionId: inputHeadroomText,
|
|
3454
|
+
acquiredAtMs: Number.MAX_SAFE_INTEGER,
|
|
3455
|
+
handoffId: inputHandoffHeadroomText,
|
|
3456
|
+
},
|
|
3457
|
+
handoff: undefined,
|
|
3458
|
+
},
|
|
3459
|
+
}));
|
|
3460
|
+
const ready = largestInputHeadroomEntry(file.entries.filter((entry) => entry.state === "pending" &&
|
|
3461
|
+
entry.inputClaim?.phase === "ready" &&
|
|
3462
|
+
!entry.inputClaim.handoff), (entry) => ({
|
|
3463
|
+
...entry,
|
|
3464
|
+
inputClaim: { ...entry.inputClaim, phase: "running" },
|
|
3465
|
+
}));
|
|
3466
|
+
const unclaimed = largestInputHeadroomEntry(file.entries.filter((entry) => entry.state === "pending" &&
|
|
3467
|
+
entry.preApprovalExcluded === false &&
|
|
3468
|
+
!entry.inputClaim), (entry) => ({
|
|
3469
|
+
...entry,
|
|
3470
|
+
inputClaim: createInputHeadroomClaim(entry, "running"),
|
|
3471
|
+
}));
|
|
3472
|
+
const excluded = smallestInputHeadroomEntry(file.entries.filter((entry) => entry.preApprovalExcluded === true));
|
|
3473
|
+
const representatives = [
|
|
3474
|
+
running,
|
|
3475
|
+
offered,
|
|
3476
|
+
ready,
|
|
3477
|
+
unclaimed,
|
|
3478
|
+
excluded,
|
|
3479
|
+
].filter((entry) => entry !== undefined);
|
|
3480
|
+
const plans = representatives.map((entry) => createInputHeadroomTransitionPlan(file, entry));
|
|
2771
3481
|
if (offered) {
|
|
2772
3482
|
plans.push(createInputHeadroomCancelPlan(file, offered));
|
|
2773
3483
|
plans.push(createInputHeadroomReleasePlan(file, offered, true));
|
|
@@ -2778,7 +3488,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2778
3488
|
}
|
|
2779
3489
|
const queuedReceipts = new Map();
|
|
2780
3490
|
for (const entry of file.entries) {
|
|
2781
|
-
if (entry.state !== "queued" ||
|
|
3491
|
+
if (entry.state !== "queued" ||
|
|
3492
|
+
!entry.queueReceiptId ||
|
|
3493
|
+
entry.queueHandoff)
|
|
2782
3494
|
continue;
|
|
2783
3495
|
const grouped = queuedReceipts.get(entry.queueReceiptId) ?? [];
|
|
2784
3496
|
grouped.push(entry);
|
|
@@ -2792,14 +3504,23 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2792
3504
|
return plans;
|
|
2793
3505
|
};
|
|
2794
3506
|
const createInputHeadroomSegment = (previous, next) => {
|
|
2795
|
-
const previousEntries = new Map(previous.entries.map(entry => [entry.updateId, entry]));
|
|
2796
|
-
const nextEntries = new Map(next.entries.map(entry => [entry.updateId, entry]));
|
|
2797
|
-
return {
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
3507
|
+
const previousEntries = new Map(previous.entries.map((entry) => [entry.updateId, entry]));
|
|
3508
|
+
const nextEntries = new Map(next.entries.map((entry) => [entry.updateId, entry]));
|
|
3509
|
+
return {
|
|
3510
|
+
version,
|
|
3511
|
+
revision: next.revision,
|
|
3512
|
+
previousRevision: next.revision - 1,
|
|
3513
|
+
profile: next.profile,
|
|
3514
|
+
botIdentity: next.botIdentity,
|
|
3515
|
+
upsertedEntries: next.entries.filter((entry) => !previousEntries.has(entry.updateId) ||
|
|
3516
|
+
!isDeepStrictEqual(previousEntries.get(entry.updateId), entry)),
|
|
3517
|
+
removedUpdateIds: previous.entries
|
|
3518
|
+
.filter((entry) => !nextEntries.has(entry.updateId))
|
|
3519
|
+
.map((entry) => entry.updateId),
|
|
3520
|
+
...(next.acceptedThroughUpdateId !== undefined
|
|
3521
|
+
? { acceptedThroughUpdateId: next.acceptedThroughUpdateId }
|
|
3522
|
+
: {}),
|
|
3523
|
+
};
|
|
2803
3524
|
};
|
|
2804
3525
|
const assertInputHeadroomCapacity = (file, extraBytes) => {
|
|
2805
3526
|
const bytes = assertCapacity(file);
|
|
@@ -2821,46 +3542,74 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2821
3542
|
? { ...current.source.evidence.accounting }
|
|
2822
3543
|
: { files: 1, bytes: publishedBytes, work: getFileWork(publishedFile) };
|
|
2823
3544
|
if (publishedSegment) {
|
|
2824
|
-
const segmentWork = publishedSegment.upsertedEntries.length +
|
|
2825
|
-
|
|
3545
|
+
const segmentWork = publishedSegment.upsertedEntries.length +
|
|
3546
|
+
publishedSegment.removedUpdateIds.length +
|
|
3547
|
+
(publishedSegment.operatorDispositions?.length ?? 0) +
|
|
3548
|
+
(publishedSegment.sourceCompletions?.length ?? 0);
|
|
2826
3549
|
const segmentBytes = Buffer.byteLength(`${JSON.stringify(publishedSegment, null, 2)}\n`);
|
|
2827
3550
|
baseRetainedCount += 1;
|
|
2828
3551
|
baseRetainedBytes += segmentBytes;
|
|
2829
3552
|
baseRetainedWork += segmentWork;
|
|
2830
|
-
const segmentFirst = {
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3553
|
+
const segmentFirst = {
|
|
3554
|
+
files: baseAccounting.files + 1,
|
|
3555
|
+
bytes: baseAccounting.bytes + segmentBytes,
|
|
3556
|
+
work: baseAccounting.work + segmentWork + getFileWork(publishedFile),
|
|
3557
|
+
};
|
|
3558
|
+
const compactResidue = {
|
|
3559
|
+
files: 1 + baseRetainedCount,
|
|
3560
|
+
bytes: publishedBytes + baseRetainedBytes,
|
|
3561
|
+
work: getFileWork(publishedFile) + baseRetainedWork,
|
|
3562
|
+
};
|
|
3563
|
+
baseAccounting = {
|
|
3564
|
+
files: Math.max(segmentFirst.files, compactResidue.files),
|
|
2835
3565
|
bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
|
|
2836
|
-
work: Math.max(segmentFirst.work, compactResidue.work)
|
|
3566
|
+
work: Math.max(segmentFirst.work, compactResidue.work),
|
|
3567
|
+
};
|
|
2837
3568
|
}
|
|
2838
3569
|
for (const transitions of plans) {
|
|
2839
3570
|
let retainedCount = baseRetainedCount;
|
|
2840
3571
|
let retainedBytes = baseRetainedBytes;
|
|
2841
3572
|
let retainedWork = baseRetainedWork;
|
|
2842
3573
|
let accounting = { ...baseAccounting };
|
|
2843
|
-
let state = {
|
|
3574
|
+
let state = {
|
|
3575
|
+
file: publishedFile,
|
|
3576
|
+
projectionSlack: 0,
|
|
3577
|
+
};
|
|
2844
3578
|
for (const next of transitions) {
|
|
2845
3579
|
const segment = createInputHeadroomSegment(state.file, next.file);
|
|
2846
|
-
const segmentCollections = [
|
|
2847
|
-
segment.
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
3580
|
+
const segmentCollections = [
|
|
3581
|
+
segment.upsertedEntries.length,
|
|
3582
|
+
segment.removedUpdateIds.length,
|
|
3583
|
+
segment.operatorDispositions?.length ?? 0,
|
|
3584
|
+
next.file.entries.length,
|
|
3585
|
+
next.file.operatorDispositions?.length ?? 0,
|
|
3586
|
+
];
|
|
3587
|
+
const segmentWork = segmentCollections[0] +
|
|
3588
|
+
segmentCollections[1] +
|
|
3589
|
+
segmentCollections[2];
|
|
3590
|
+
const segmentBytes = Buffer.byteLength(`${JSON.stringify(segment, null, 2)}\n`) +
|
|
3591
|
+
next.projectionSlack;
|
|
2851
3592
|
const nextBytes = assertInputHeadroomCapacity(next.file, next.projectionSlack);
|
|
2852
|
-
const segmentFirst = {
|
|
2853
|
-
|
|
3593
|
+
const segmentFirst = {
|
|
3594
|
+
files: accounting.files + 1,
|
|
3595
|
+
bytes: accounting.bytes + segmentBytes,
|
|
3596
|
+
work: accounting.work + segmentWork + getFileWork(next.file),
|
|
3597
|
+
};
|
|
2854
3598
|
assertSourceResources(segmentFirst.files, segmentFirst.bytes, segmentFirst.work, segmentCollections);
|
|
2855
3599
|
retainedCount += 1;
|
|
2856
3600
|
retainedBytes += segmentBytes;
|
|
2857
3601
|
retainedWork += segmentWork;
|
|
2858
|
-
const compactResidue = {
|
|
2859
|
-
|
|
3602
|
+
const compactResidue = {
|
|
3603
|
+
files: 1 + retainedCount,
|
|
3604
|
+
bytes: nextBytes + retainedBytes,
|
|
3605
|
+
work: getFileWork(next.file) + retainedWork,
|
|
3606
|
+
};
|
|
2860
3607
|
assertSourceResources(compactResidue.files, compactResidue.bytes, compactResidue.work, segmentCollections);
|
|
2861
|
-
accounting = {
|
|
3608
|
+
accounting = {
|
|
3609
|
+
files: Math.max(segmentFirst.files, compactResidue.files),
|
|
2862
3610
|
bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
|
|
2863
|
-
work: Math.max(segmentFirst.work, compactResidue.work)
|
|
3611
|
+
work: Math.max(segmentFirst.work, compactResidue.work),
|
|
3612
|
+
};
|
|
2864
3613
|
state = next;
|
|
2865
3614
|
}
|
|
2866
3615
|
}
|
|
@@ -2874,9 +3623,15 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2874
3623
|
const serializedBytes = assertCapacity(revisedFile, serialized);
|
|
2875
3624
|
const segmentText = `${JSON.stringify(segment, null, 2)}\n`;
|
|
2876
3625
|
const segmentBytes = Buffer.byteLength(segmentText);
|
|
2877
|
-
const collections = [
|
|
2878
|
-
segment.
|
|
2879
|
-
segment.
|
|
3626
|
+
const collections = [
|
|
3627
|
+
segment.upsertedEntries.length,
|
|
3628
|
+
segment.removedUpdateIds.length,
|
|
3629
|
+
segment.operatorDispositions?.length ?? 0,
|
|
3630
|
+
file.entries.length,
|
|
3631
|
+
file.operatorDispositions?.length ?? 0,
|
|
3632
|
+
segment.sourceCompletions?.length ?? 0,
|
|
3633
|
+
file.sourceCompletions?.length ?? 0,
|
|
3634
|
+
];
|
|
2880
3635
|
const segmentWork = collections[0] + collections[1] + collections[2] + collections[5];
|
|
2881
3636
|
const fileWork = collections[3] + collections[4] + collections[6];
|
|
2882
3637
|
const accounting = source.evidence.accounting;
|
|
@@ -2884,10 +3639,14 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2884
3639
|
assertSourceResources(accounting.files + 1, accounting.bytes + segmentBytes, accounting.work + segmentWork + fileWork, collections);
|
|
2885
3640
|
const unapplied = source.segments.filter((item) => Number(item.name.slice(0, 16)) > source.snapshotRevision);
|
|
2886
3641
|
const compact = forceCompact ||
|
|
2887
|
-
unapplied.length + 1 >=
|
|
2888
|
-
|
|
3642
|
+
unapplied.length + 1 >=
|
|
3643
|
+
TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_COUNT ||
|
|
3644
|
+
unapplied.reduce((sum, item) => sum + item.bytes, segmentBytes) >=
|
|
3645
|
+
TELEGRAM_UPDATE_JOURNAL_COMPACTION_SEGMENT_BYTES;
|
|
2889
3646
|
if (compact) {
|
|
2890
|
-
assertSourceResources(accounting.files + 1, serializedBytes +
|
|
3647
|
+
assertSourceResources(accounting.files + 1, serializedBytes +
|
|
3648
|
+
source.segments.reduce((sum, item) => sum + item.bytes, segmentBytes), fileWork +
|
|
3649
|
+
source.segments.reduce((sum, item) => sum + item.work, segmentWork), collections);
|
|
2891
3650
|
}
|
|
2892
3651
|
const segmentPath = getTelegramUpdateJournalSegmentPath(path, segment.revision);
|
|
2893
3652
|
// A crash may retain staging, so keep it outside the strictly enumerated segment directory.
|
|
@@ -2896,9 +3655,13 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2896
3655
|
writeJournalFile(path, serialized, publicationBoundary);
|
|
2897
3656
|
// Snapshot scope must not change. Preserve a validated redundant ID witness instead.
|
|
2898
3657
|
const witness = file.botIdentity.botId === undefined
|
|
2899
|
-
? source.segments.find((item) => item.botId !== undefined)?.name
|
|
3658
|
+
? source.segments.find((item) => item.botId !== undefined)?.name
|
|
3659
|
+
: undefined;
|
|
2900
3660
|
const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
|
|
2901
|
-
const names = [
|
|
3661
|
+
const names = [
|
|
3662
|
+
...source.segments.map((item) => item.name),
|
|
3663
|
+
basename(segmentPath),
|
|
3664
|
+
];
|
|
2902
3665
|
for (const name of names) {
|
|
2903
3666
|
if (name === witness)
|
|
2904
3667
|
continue;
|
|
@@ -2912,12 +3675,18 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2912
3675
|
try {
|
|
2913
3676
|
rmdirSync(segmentDirectory);
|
|
2914
3677
|
}
|
|
2915
|
-
catch {
|
|
3678
|
+
catch {
|
|
3679
|
+
/* Retained witness or cleanup residue. */
|
|
3680
|
+
}
|
|
2916
3681
|
}
|
|
2917
3682
|
return { file: revisedFile, serializedBytes };
|
|
2918
3683
|
};
|
|
2919
|
-
const publishMutation = (current, entries, contentChanged, operatorDispositions = current.file.operatorDispositions, acceptedThroughUpdateId = current.file.acceptedThroughUpdateId, publicationBoundary = onPublicationBoundary, inputHeadroom = journalHasCustodyFields(version)
|
|
2920
|
-
|
|
3684
|
+
const publishMutation = (current, entries, contentChanged, operatorDispositions = current.file.operatorDispositions, acceptedThroughUpdateId = current.file.acceptedThroughUpdateId, publicationBoundary = onPublicationBoundary, inputHeadroom = journalHasCustodyFields(version)
|
|
3685
|
+
? "all"
|
|
3686
|
+
: false, sourceCompletions = current.file.sourceCompletions) => {
|
|
3687
|
+
const botIdentity = current.source
|
|
3688
|
+
? current.file.botIdentity
|
|
3689
|
+
: mergeBotIdentity(current.file.botIdentity, expectedIdentity);
|
|
2921
3690
|
if (!contentChanged &&
|
|
2922
3691
|
isDeepStrictEqual(current.file.botIdentity, botIdentity) &&
|
|
2923
3692
|
isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? []) &&
|
|
@@ -2933,9 +3702,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2933
3702
|
...(acceptedThroughUpdateId !== undefined
|
|
2934
3703
|
? { acceptedThroughUpdateId }
|
|
2935
3704
|
: {}),
|
|
2936
|
-
...(operatorDispositions?.length
|
|
2937
|
-
? { operatorDispositions }
|
|
2938
|
-
: {}),
|
|
3705
|
+
...(operatorDispositions?.length ? { operatorDispositions } : {}),
|
|
2939
3706
|
...(sourceCompletions?.length ? { sourceCompletions } : {}),
|
|
2940
3707
|
};
|
|
2941
3708
|
assertJournalRoutingInputContinuity(current.file, entries, path);
|
|
@@ -2952,7 +3719,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2952
3719
|
}
|
|
2953
3720
|
if (!current.exists) {
|
|
2954
3721
|
if (current.source) {
|
|
2955
|
-
const collections = [
|
|
3722
|
+
const collections = [
|
|
3723
|
+
file.entries.length,
|
|
3724
|
+
file.operatorDispositions?.length ?? 0,
|
|
3725
|
+
file.sourceCompletions?.length ?? 0,
|
|
3726
|
+
];
|
|
2956
3727
|
assertSourceResources(1, serializedBytes, collections[0] + collections[1] + collections[2], collections);
|
|
2957
3728
|
}
|
|
2958
3729
|
if (inputHeadroom !== false)
|
|
@@ -2961,10 +3732,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2961
3732
|
return { file, serializedBytes };
|
|
2962
3733
|
}
|
|
2963
3734
|
const previousEntries = new Map(current.file.entries.map((entry) => [entry.updateId, entry]));
|
|
2964
|
-
if (withPairingAdmission &&
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
3735
|
+
if (withPairingAdmission &&
|
|
3736
|
+
entries.some((entry) => {
|
|
3737
|
+
const previous = previousEntries.get(entry.updateId);
|
|
3738
|
+
return (previous && previous.preApprovalExcluded !== entry.preApprovalExcluded);
|
|
3739
|
+
})) {
|
|
2968
3740
|
throw createJournalError("pairing-evidence", path, "changes immutable exclusion evidence");
|
|
2969
3741
|
}
|
|
2970
3742
|
const nextEntries = new Map(entries.map((entry) => [entry.updateId, entry]));
|
|
@@ -2988,7 +3760,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2988
3760
|
...(!isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? [])
|
|
2989
3761
|
? { operatorDispositions: operatorDispositions ?? [] }
|
|
2990
3762
|
: {}),
|
|
2991
|
-
...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? [])
|
|
3763
|
+
...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? [])
|
|
3764
|
+
? { sourceCompletions: sourceCompletions ?? [] }
|
|
3765
|
+
: {}),
|
|
2992
3766
|
};
|
|
2993
3767
|
if (current.source) {
|
|
2994
3768
|
if (!isSafePositiveInteger(revision))
|
|
@@ -3004,7 +3778,8 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3004
3778
|
const segmentNames = readdirSync(segmentDirectory).filter((name) => /^\d{16}\.json$/u.test(name));
|
|
3005
3779
|
let snapshotRevision = 0;
|
|
3006
3780
|
try {
|
|
3007
|
-
snapshotRevision =
|
|
3781
|
+
snapshotRevision =
|
|
3782
|
+
parseJournalFile(JSON.parse(readFileSync(path, "utf8")), path, version).revision ?? 0;
|
|
3008
3783
|
}
|
|
3009
3784
|
catch (error) {
|
|
3010
3785
|
if (error?.code !== "ENOENT")
|
|
@@ -3039,11 +3814,18 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3039
3814
|
}
|
|
3040
3815
|
return { file: revisedFile, serializedBytes };
|
|
3041
3816
|
};
|
|
3042
|
-
const removeCompleted = (updateIds, expectedSources, exact = false, completionInput) => runMutation((readCurrent) => {
|
|
3817
|
+
const removeCompleted = (updateIds, expectedSources, exact = false, completionInput, isCurrent) => runMutation((readCurrent) => {
|
|
3818
|
+
const assertCurrent = () => {
|
|
3819
|
+
if (isCurrent && !isCurrent())
|
|
3820
|
+
throw createJournalError("conflict", path, "exact removal authority ended");
|
|
3821
|
+
};
|
|
3822
|
+
assertCurrent();
|
|
3043
3823
|
const current = exact && !sourceAccess ? readCurrentStrict(false) : readCurrent();
|
|
3044
3824
|
if (completionInput !== undefined && !sourceAccess)
|
|
3045
3825
|
throw createJournalError("invalid", path, "source completion requires an exact source handle");
|
|
3046
|
-
const completions = completionInput === undefined
|
|
3826
|
+
const completions = completionInput === undefined
|
|
3827
|
+
? undefined
|
|
3828
|
+
: validateJournalSourceCompletions(completionInput, path, version);
|
|
3047
3829
|
if (exact && expectedSources === undefined)
|
|
3048
3830
|
throw createJournalError("invalid", path, "received no exact completion sources");
|
|
3049
3831
|
const requestedIds = new Set();
|
|
@@ -3057,133 +3839,221 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3057
3839
|
throw createJournalError("invalid", path, "received no exact completion sources");
|
|
3058
3840
|
const expectedIds = new Set();
|
|
3059
3841
|
for (const expected of expectedSources) {
|
|
3060
|
-
if (!isRecord(expected) ||
|
|
3061
|
-
|
|
3062
|
-
|
|
3842
|
+
if (!isRecord(expected) ||
|
|
3843
|
+
Object.keys(expected).some((key) => !["updateId", "sourceSha256"].includes(key)) ||
|
|
3844
|
+
!isSafeNonNegativeInteger(expected.updateId) ||
|
|
3845
|
+
!requestedIds.has(expected.updateId) ||
|
|
3846
|
+
expectedIds.has(expected.updateId) ||
|
|
3847
|
+
typeof expected.sourceSha256 !== "string" ||
|
|
3848
|
+
!/^[a-f0-9]{64}$/u.test(expected.sourceSha256)) {
|
|
3063
3849
|
throw createJournalError("invalid", path, "received an invalid exact completion source");
|
|
3064
3850
|
}
|
|
3065
3851
|
expectedIds.add(expected.updateId);
|
|
3066
|
-
const entry = current.file.entries.find(value => value.updateId === expected.updateId);
|
|
3067
|
-
if (!entry ||
|
|
3852
|
+
const entry = current.file.entries.find((value) => value.updateId === expected.updateId);
|
|
3853
|
+
if (!entry ||
|
|
3854
|
+
createTelegramUpdateJournalEntryDigest(entry).sourceSha256 !==
|
|
3855
|
+
expected.sourceSha256) {
|
|
3068
3856
|
throw createJournalError("conflict", path, "exact completion source changed or disappeared");
|
|
3069
3857
|
}
|
|
3070
3858
|
}
|
|
3071
3859
|
}
|
|
3072
3860
|
if (completions) {
|
|
3073
|
-
const guards = new Map((expectedSources ?? []).map(source => [
|
|
3074
|
-
|
|
3861
|
+
const guards = new Map((expectedSources ?? []).map((source) => [
|
|
3862
|
+
source.updateId,
|
|
3863
|
+
source.sourceSha256,
|
|
3864
|
+
]));
|
|
3865
|
+
if (completions.some((completion) => guards.get(completion.updateId) !== completion.sourceSha256)) {
|
|
3075
3866
|
throw createJournalError("invalid", path, "source completion has no matching exact guard");
|
|
3076
3867
|
}
|
|
3077
3868
|
}
|
|
3078
|
-
const requestedEntries = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
|
|
3079
|
-
const protectedEntry = requestedEntries.find(entry => entry.state === "failed" || entry.state === "queued");
|
|
3869
|
+
const requestedEntries = current.file.entries.filter((entry) => requestedIds.has(entry.updateId));
|
|
3870
|
+
const protectedEntry = requestedEntries.find((entry) => entry.state === "failed" || entry.state === "queued");
|
|
3080
3871
|
if (protectedEntry)
|
|
3081
3872
|
throw createJournalError("conflict", path, protectedEntry.state === "failed"
|
|
3082
3873
|
? `cannot complete terminal update ${protectedEntry.updateId} without an operator disposition`
|
|
3083
3874
|
: `cannot complete queued update ${protectedEntry.updateId} without its exact owner receipt`);
|
|
3084
|
-
const removedUpdateIds = requestedEntries.map(entry => entry.updateId);
|
|
3875
|
+
const removedUpdateIds = requestedEntries.map((entry) => entry.updateId);
|
|
3085
3876
|
const sourceCompletions = completions
|
|
3086
|
-
? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
|
|
3877
|
+
? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
|
|
3878
|
+
: current.file.sourceCompletions;
|
|
3087
3879
|
if (sourceCompletions?.length)
|
|
3088
3880
|
validateJournalSourceCompletions(sourceCompletions, path, version);
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3881
|
+
assertCurrent();
|
|
3882
|
+
const published = publishMutation(current, current.file.entries.filter((entry) => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, isCurrent
|
|
3883
|
+
? (boundary, target) => {
|
|
3884
|
+
onPublicationBoundary?.(boundary, target);
|
|
3885
|
+
assertCurrent();
|
|
3886
|
+
}
|
|
3887
|
+
: onPublicationBoundary, false, sourceCompletions);
|
|
3888
|
+
assertCurrent();
|
|
3889
|
+
return {
|
|
3890
|
+
removedUpdateIds,
|
|
3891
|
+
entryCount: published.file.entries.length,
|
|
3892
|
+
serializedBytes: published.serializedBytes,
|
|
3893
|
+
...(completions
|
|
3894
|
+
? {
|
|
3895
|
+
sourceCompletions: completions.map((completion) => ({
|
|
3896
|
+
...completion,
|
|
3897
|
+
})),
|
|
3898
|
+
}
|
|
3899
|
+
: {}),
|
|
3900
|
+
};
|
|
3092
3901
|
});
|
|
3093
3902
|
const completeQueued = (receipts, completionInput, exact = false) => runMutation((readCurrent) => {
|
|
3094
3903
|
if (exact && !sourceAccess)
|
|
3095
3904
|
throw createJournalError("invalid", path, "source completion requires an exact source handle");
|
|
3096
|
-
const completions = exact
|
|
3905
|
+
const completions = exact
|
|
3906
|
+
? validateJournalSourceCompletions(completionInput, path, version)
|
|
3907
|
+
: undefined;
|
|
3097
3908
|
if (!Array.isArray(receipts) || receipts.length === 0)
|
|
3098
3909
|
throw createJournalError("invalid", path, "received no queued receipts to complete");
|
|
3099
3910
|
const current = readCurrent();
|
|
3100
3911
|
const receiptIds = new Set(), requestedUpdateIds = new Set();
|
|
3101
3912
|
for (const receipt of receipts) {
|
|
3102
|
-
if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
|
|
3103
|
-
|
|
3913
|
+
if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
|
|
3914
|
+
!isNonEmptyString(receipt.receiptId) ||
|
|
3915
|
+
receiptIds.has(receipt.receiptId) ||
|
|
3916
|
+
!Array.isArray(receipt.sourceUpdateIds) ||
|
|
3917
|
+
receipt.sourceUpdateIds.length === 0) {
|
|
3104
3918
|
throw createJournalError("invalid", path, "received an invalid queued completion receipt");
|
|
3105
3919
|
}
|
|
3106
3920
|
receiptIds.add(receipt.receiptId);
|
|
3107
3921
|
const queueOwner = validateJournalQueueOwner(receipt.queueOwner, path);
|
|
3108
|
-
if (queueRuntimeIdentity &&
|
|
3109
|
-
queueOwner.
|
|
3922
|
+
if (queueRuntimeIdentity &&
|
|
3923
|
+
(queueOwner.instanceId !== queueRuntimeIdentity.instanceId ||
|
|
3924
|
+
queueOwner.processId !== queueRuntimeIdentity.processId ||
|
|
3925
|
+
queueOwner.processBirthId !== queueRuntimeIdentity.processBirthId)) {
|
|
3110
3926
|
throw createJournalError("conflict", path, `cannot complete foreign queue receipt ${receipt.receiptId}`);
|
|
3111
3927
|
}
|
|
3112
3928
|
const sourceUpdateIds = new Set();
|
|
3113
3929
|
for (const updateId of receipt.sourceUpdateIds) {
|
|
3114
|
-
if (!isSafeNonNegativeInteger(updateId) ||
|
|
3930
|
+
if (!isSafeNonNegativeInteger(updateId) ||
|
|
3931
|
+
sourceUpdateIds.has(updateId) ||
|
|
3932
|
+
requestedUpdateIds.has(updateId)) {
|
|
3115
3933
|
throw createJournalError("invalid", path, "received overlapping queued completion update ids");
|
|
3116
3934
|
}
|
|
3117
3935
|
sourceUpdateIds.add(updateId);
|
|
3118
3936
|
requestedUpdateIds.add(updateId);
|
|
3119
3937
|
}
|
|
3120
|
-
const persistedReceiptEntries = current.file.entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
|
|
3121
|
-
if (persistedReceiptEntries.length !== sourceUpdateIds.size ||
|
|
3122
|
-
entry
|
|
3938
|
+
const persistedReceiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === receipt.receiptId);
|
|
3939
|
+
if (persistedReceiptEntries.length !== sourceUpdateIds.size ||
|
|
3940
|
+
persistedReceiptEntries.some((entry) => !sourceUpdateIds.has(entry.updateId) ||
|
|
3941
|
+
entry.state !== "queued" ||
|
|
3942
|
+
entry.queueKind !== receipt.queueKind ||
|
|
3943
|
+
!entry.queueOwner ||
|
|
3944
|
+
entry.queueHandoff !== undefined ||
|
|
3945
|
+
!areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, queueOwner))) {
|
|
3123
3946
|
throw createJournalError("conflict", path, `cannot complete stale or foreign queue receipt ${receipt.receiptId}`);
|
|
3124
3947
|
}
|
|
3125
3948
|
}
|
|
3126
|
-
const removedEntries = current.file.entries.filter(entry => requestedUpdateIds.has(entry.updateId));
|
|
3127
|
-
const removedUpdateIds = removedEntries.map(entry => entry.updateId);
|
|
3949
|
+
const removedEntries = current.file.entries.filter((entry) => requestedUpdateIds.has(entry.updateId));
|
|
3950
|
+
const removedUpdateIds = removedEntries.map((entry) => entry.updateId);
|
|
3128
3951
|
if (removedUpdateIds.length !== requestedUpdateIds.size)
|
|
3129
3952
|
throw createJournalError("conflict", path, "queued completion did not resolve every source update");
|
|
3130
3953
|
if (completions) {
|
|
3131
|
-
const sources = new Map(removedEntries.map(entry => [
|
|
3132
|
-
|
|
3954
|
+
const sources = new Map(removedEntries.map((entry) => [
|
|
3955
|
+
entry.updateId,
|
|
3956
|
+
createTelegramUpdateJournalEntryDigest(entry).sourceSha256,
|
|
3957
|
+
]));
|
|
3958
|
+
if (completions.some((completion) => sources.get(completion.updateId) !== completion.sourceSha256)) {
|
|
3133
3959
|
throw createJournalError("conflict", path, "queued completion source changed or belongs to another receipt");
|
|
3134
3960
|
}
|
|
3135
3961
|
}
|
|
3136
|
-
const sourceCompletions = completions
|
|
3962
|
+
const sourceCompletions = completions
|
|
3963
|
+
? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
|
|
3137
3964
|
: current.file.sourceCompletions;
|
|
3138
3965
|
if (sourceCompletions?.length)
|
|
3139
3966
|
validateJournalSourceCompletions(sourceCompletions, path, version);
|
|
3140
|
-
const published = publishMutation(current, current.file.entries.filter(entry => !requestedUpdateIds.has(entry.updateId)), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, exact ? false : undefined, sourceCompletions);
|
|
3141
|
-
return {
|
|
3142
|
-
|
|
3967
|
+
const published = publishMutation(current, current.file.entries.filter((entry) => !requestedUpdateIds.has(entry.updateId)), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, exact ? false : undefined, sourceCompletions);
|
|
3968
|
+
return {
|
|
3969
|
+
removedUpdateIds,
|
|
3970
|
+
entryCount: published.file.entries.length,
|
|
3971
|
+
serializedBytes: published.serializedBytes,
|
|
3972
|
+
...(completions
|
|
3973
|
+
? {
|
|
3974
|
+
sourceCompletions: completions.map((completion) => ({
|
|
3975
|
+
...completion,
|
|
3976
|
+
})),
|
|
3977
|
+
}
|
|
3978
|
+
: {}),
|
|
3979
|
+
};
|
|
3143
3980
|
});
|
|
3144
|
-
const mutateRoutingInputs = (input, select, publishedAtMs) => runMutation(readCurrent => {
|
|
3981
|
+
const mutateRoutingInputs = (input, select, publishedAtMs, chooser) => runMutation((readCurrent) => {
|
|
3145
3982
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(version))
|
|
3146
3983
|
throw createJournalError("unsupported-version", path, "routing input lifetime requires a private v1 source");
|
|
3147
|
-
const binding = createTelegramUpdateJournalBindingKey({
|
|
3148
|
-
|
|
3149
|
-
|
|
3984
|
+
const binding = createTelegramUpdateJournalBindingKey({
|
|
3985
|
+
path,
|
|
3986
|
+
profileName: profile,
|
|
3987
|
+
botIdentity: expectedIdentity,
|
|
3988
|
+
});
|
|
3989
|
+
if (input.journalBindingKey !== binding ||
|
|
3990
|
+
!isSafePositiveInteger(input.operatorUserId) ||
|
|
3991
|
+
typeof input.isCurrent !== "function" ||
|
|
3992
|
+
!Array.isArray(input.entries) ||
|
|
3993
|
+
!input.entries.length ||
|
|
3994
|
+
input.entries.length > maxEntries) {
|
|
3150
3995
|
throw createJournalError("invalid", path, "invalid routing input lifetime authority");
|
|
3151
3996
|
}
|
|
3152
|
-
const assertCurrent = () => {
|
|
3153
|
-
|
|
3997
|
+
const assertCurrent = () => {
|
|
3998
|
+
if (!input.isCurrent())
|
|
3999
|
+
throw createJournalError("conflict", path, "routing input lifetime authority ended");
|
|
4000
|
+
};
|
|
3154
4001
|
assertCurrent();
|
|
3155
|
-
const expected = input.entries.map(entry => validateJournalEntry(entry, path, version));
|
|
3156
|
-
if (new Set(expected.map(entry => entry.updateId)).size !==
|
|
4002
|
+
const expected = input.entries.map((entry) => validateJournalEntry(entry, path, version));
|
|
4003
|
+
if (new Set(expected.map((entry) => entry.updateId)).size !==
|
|
4004
|
+
expected.length)
|
|
3157
4005
|
throw createJournalError("invalid", path, "duplicate routing input sources");
|
|
3158
4006
|
const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
|
|
3159
4007
|
const nowMs = select ? getNowMs() : publishedAtMs;
|
|
3160
|
-
if (!isSafeNonNegativeInteger(nowMs) ||
|
|
4008
|
+
if (!isSafeNonNegativeInteger(nowMs) ||
|
|
4009
|
+
!Number.isSafeInteger(nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS) ||
|
|
4010
|
+
(!select && nowMs > getNowMs()))
|
|
3161
4011
|
throw createJournalError("invalid", path, "invalid routing input lifetime clock");
|
|
3162
4012
|
let issued = false;
|
|
3163
4013
|
const changed = new Map();
|
|
3164
|
-
const result = expected.map(original => {
|
|
3165
|
-
const entry = current.file.entries.find(value => value.updateId === original.updateId);
|
|
3166
|
-
if (!entry ||
|
|
4014
|
+
const result = expected.map((original) => {
|
|
4015
|
+
const entry = current.file.entries.find((value) => value.updateId === original.updateId);
|
|
4016
|
+
if (!entry ||
|
|
4017
|
+
entry.state !== "pending" ||
|
|
4018
|
+
original.state !== "pending" ||
|
|
4019
|
+
entry.inputClaim ||
|
|
4020
|
+
entry.inputProvenance ||
|
|
4021
|
+
entry.queueOwner) {
|
|
3167
4022
|
throw createJournalError("conflict", path, "routing input source is not unclaimed pending work");
|
|
3168
4023
|
}
|
|
3169
|
-
const withoutLifetime = (value) => {
|
|
3170
|
-
|
|
3171
|
-
|
|
4024
|
+
const withoutLifetime = (value) => {
|
|
4025
|
+
const copy = cloneEntry(value);
|
|
4026
|
+
delete copy.routingInput;
|
|
4027
|
+
return copy;
|
|
4028
|
+
};
|
|
4029
|
+
if (!isDeepStrictEqual(entry, original) &&
|
|
4030
|
+
(!isDeepStrictEqual(withoutLifetime(entry), withoutLifetime(original)) ||
|
|
4031
|
+
(original.routingInput &&
|
|
4032
|
+
!isDeepStrictEqual(original.routingInput, entry.routingInput)))) {
|
|
3172
4033
|
throw createJournalError("conflict", path, "routing input source changed");
|
|
3173
4034
|
}
|
|
3174
4035
|
const lifetime = entry.routingInput;
|
|
3175
4036
|
if (lifetime && lifetime.operatorUserId !== input.operatorUserId)
|
|
3176
4037
|
throw createJournalError("conflict", path, "routing input belongs to another operator");
|
|
3177
|
-
if (createPendingRetentionReference({
|
|
4038
|
+
if (createPendingRetentionReference({
|
|
4039
|
+
path,
|
|
4040
|
+
journalBindingKey: binding,
|
|
4041
|
+
entry,
|
|
4042
|
+
maxBytes,
|
|
4043
|
+
}).read()) {
|
|
3178
4044
|
throw createJournalError("conflict", path, "routing input has protected retention");
|
|
3179
4045
|
}
|
|
3180
4046
|
if (select) {
|
|
3181
|
-
if (!lifetime ||
|
|
4047
|
+
if (!lifetime ||
|
|
4048
|
+
(lifetime.phase === "waiting" && nowMs >= lifetime.expiresAtMs))
|
|
3182
4049
|
throw createJournalError("conflict", path, "routing input choice expired or was not armed");
|
|
3183
4050
|
if (lifetime.phase === "selected")
|
|
3184
4051
|
return cloneEntry(entry);
|
|
3185
4052
|
issued = true;
|
|
3186
|
-
const selected = {
|
|
4053
|
+
const selected = {
|
|
4054
|
+
...cloneEntry(entry),
|
|
4055
|
+
routingInput: { ...lifetime, phase: "selected" },
|
|
4056
|
+
};
|
|
3187
4057
|
changed.set(entry.updateId, selected);
|
|
3188
4058
|
return cloneEntry(selected);
|
|
3189
4059
|
}
|
|
@@ -3195,16 +4065,29 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3195
4065
|
if (nowMs < entry.admittedAtMs)
|
|
3196
4066
|
throw createJournalError("conflict", path, "routing input publication predates admission");
|
|
3197
4067
|
issued = true;
|
|
3198
|
-
const armed = {
|
|
3199
|
-
|
|
4068
|
+
const armed = {
|
|
4069
|
+
...cloneEntry(entry),
|
|
4070
|
+
routingInput: {
|
|
4071
|
+
operatorUserId: input.operatorUserId,
|
|
4072
|
+
publishedAtMs: nowMs,
|
|
4073
|
+
expiresAtMs: nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS,
|
|
4074
|
+
phase: "waiting",
|
|
4075
|
+
...(chooser ? { chooser: { ...chooser } } : {}),
|
|
4076
|
+
},
|
|
4077
|
+
};
|
|
3200
4078
|
changed.set(entry.updateId, armed);
|
|
3201
4079
|
return cloneEntry(armed);
|
|
3202
4080
|
});
|
|
3203
4081
|
assertCurrent();
|
|
3204
|
-
if (select &&
|
|
4082
|
+
if (select &&
|
|
4083
|
+
issued &&
|
|
4084
|
+
result.some((entry) => !changed.has(entry.updateId)))
|
|
3205
4085
|
throw createJournalError("conflict", path, "routing input selection has mixed prior phases");
|
|
3206
4086
|
if (changed.size)
|
|
3207
|
-
publishMutation(current, current.file.entries.map(entry => changed.get(entry.updateId) ?? entry), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, target) => {
|
|
4087
|
+
publishMutation(current, current.file.entries.map((entry) => changed.get(entry.updateId) ?? entry), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, target) => {
|
|
4088
|
+
onPublicationBoundary?.(boundary, target);
|
|
4089
|
+
assertCurrent();
|
|
4090
|
+
});
|
|
3208
4091
|
assertCurrent();
|
|
3209
4092
|
return { issued, entries: result };
|
|
3210
4093
|
});
|
|
@@ -3236,7 +4119,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3236
4119
|
normalizedUpdates.push(normalized);
|
|
3237
4120
|
}
|
|
3238
4121
|
const appendWithEvidence = (preApprovalExcluded) => runJournalTransaction((readCurrent) => {
|
|
3239
|
-
if (withPairingAdmission &&
|
|
4122
|
+
if (withPairingAdmission &&
|
|
4123
|
+
(typeof preApprovalExcluded !== "boolean" ||
|
|
4124
|
+
requestedAcceptedThroughUpdateId === undefined)) {
|
|
3240
4125
|
throw createJournalError("pairing-evidence", path, "requires exclusion evidence and an admission cursor");
|
|
3241
4126
|
}
|
|
3242
4127
|
if (requestedAcceptedThroughUpdateId !== undefined &&
|
|
@@ -3250,7 +4135,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3250
4135
|
...(current.file.operatorDispositions ?? [])
|
|
3251
4136
|
.filter((disposition) => disposition.action === "discard")
|
|
3252
4137
|
.map((disposition) => disposition.updateId),
|
|
3253
|
-
...(current.file.sourceCompletions ?? []).map(completion => completion.updateId),
|
|
4138
|
+
...(current.file.sourceCompletions ?? []).map((completion) => completion.updateId),
|
|
3254
4139
|
]);
|
|
3255
4140
|
let admittedAtMs;
|
|
3256
4141
|
const addedUpdateIds = [];
|
|
@@ -3268,7 +4153,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3268
4153
|
duplicateUpdateIds.push(update.update_id);
|
|
3269
4154
|
continue;
|
|
3270
4155
|
}
|
|
3271
|
-
if (withPairingAdmission &&
|
|
4156
|
+
if (withPairingAdmission &&
|
|
4157
|
+
previousAcceptedThroughUpdateId !== undefined &&
|
|
4158
|
+
update.update_id <= previousAcceptedThroughUpdateId) {
|
|
3272
4159
|
duplicateUpdateIds.push(update.update_id);
|
|
3273
4160
|
continue;
|
|
3274
4161
|
}
|
|
@@ -3281,7 +4168,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3281
4168
|
const entry = {
|
|
3282
4169
|
updateId: update.update_id,
|
|
3283
4170
|
update,
|
|
3284
|
-
...(withPairingAdmission
|
|
4171
|
+
...(withPairingAdmission
|
|
4172
|
+
? { preApprovalExcluded: preApprovalExcluded }
|
|
4173
|
+
: {}),
|
|
3285
4174
|
admittedAtMs,
|
|
3286
4175
|
state: "pending",
|
|
3287
4176
|
};
|
|
@@ -3307,7 +4196,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3307
4196
|
return {
|
|
3308
4197
|
nonExcludedUpdateIds: normalizedUpdates.flatMap((update) => {
|
|
3309
4198
|
const entry = entriesById.get(update.update_id);
|
|
3310
|
-
return entry && entry.preApprovalExcluded !== true
|
|
4199
|
+
return entry && entry.preApprovalExcluded !== true
|
|
4200
|
+
? [entry.updateId]
|
|
4201
|
+
: [];
|
|
3311
4202
|
}),
|
|
3312
4203
|
addedUpdateIds,
|
|
3313
4204
|
duplicateUpdateIds,
|
|
@@ -3345,12 +4236,12 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3345
4236
|
});
|
|
3346
4237
|
};
|
|
3347
4238
|
return options.withWriterAdmission
|
|
3348
|
-
? options.withWriterAdmission(admittedAppend)
|
|
4239
|
+
? options.withWriterAdmission(admittedAppend)
|
|
4240
|
+
: admittedAppend();
|
|
3349
4241
|
},
|
|
3350
4242
|
markQueued(receipt) {
|
|
3351
4243
|
return runMutation((readCurrent) => {
|
|
3352
|
-
if ((receipt.queueKind !== "prompt" &&
|
|
3353
|
-
receipt.queueKind !== "control") ||
|
|
4244
|
+
if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
|
|
3354
4245
|
!isNonEmptyString(receipt.receiptId) ||
|
|
3355
4246
|
!Array.isArray(receipt.sourceUpdateIds) ||
|
|
3356
4247
|
receipt.sourceUpdateIds.length === 0) {
|
|
@@ -3424,8 +4315,12 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3424
4315
|
updateId: entry.updateId,
|
|
3425
4316
|
update: entry.update,
|
|
3426
4317
|
admittedAtMs: entry.admittedAtMs,
|
|
3427
|
-
...(entry.preApprovalExcluded !== undefined
|
|
3428
|
-
|
|
4318
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4319
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4320
|
+
: {}),
|
|
4321
|
+
...(entry.routingInput
|
|
4322
|
+
? { routingInput: { ...entry.routingInput } }
|
|
4323
|
+
: {}),
|
|
3429
4324
|
state: "queued",
|
|
3430
4325
|
queueKind: receipt.queueKind,
|
|
3431
4326
|
queueReceiptId: receipt.receiptId,
|
|
@@ -3455,8 +4350,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3455
4350
|
!isSafeNonNegativeInteger(input.failedAtMs) ||
|
|
3456
4351
|
!isBoundedString(input.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
|
|
3457
4352
|
!isBoundedString(input.summary, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH) ||
|
|
3458
|
-
(input.disposition !== "retry-wait" &&
|
|
3459
|
-
input.disposition !== "failed")) {
|
|
4353
|
+
(input.disposition !== "retry-wait" && input.disposition !== "failed")) {
|
|
3460
4354
|
throw createJournalError("invalid", path, "received invalid execution failure metadata");
|
|
3461
4355
|
}
|
|
3462
4356
|
if (input.disposition === "retry-wait" &&
|
|
@@ -3502,8 +4396,12 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3502
4396
|
updateId: entry.updateId,
|
|
3503
4397
|
update: entry.update,
|
|
3504
4398
|
admittedAtMs: entry.admittedAtMs,
|
|
3505
|
-
...(entry.routingInput
|
|
3506
|
-
|
|
4399
|
+
...(entry.routingInput
|
|
4400
|
+
? { routingInput: { ...entry.routingInput } }
|
|
4401
|
+
: {}),
|
|
4402
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4403
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4404
|
+
: {}),
|
|
3507
4405
|
state: input.disposition,
|
|
3508
4406
|
failure,
|
|
3509
4407
|
...(input.disposition === "retry-wait"
|
|
@@ -3573,14 +4471,19 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3573
4471
|
updateId: entry.updateId,
|
|
3574
4472
|
update: entry.update,
|
|
3575
4473
|
admittedAtMs: entry.admittedAtMs,
|
|
3576
|
-
...(entry.preApprovalExcluded !== undefined
|
|
4474
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4475
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4476
|
+
: {}),
|
|
3577
4477
|
state: "retry-wait",
|
|
3578
4478
|
failure: entry.failure,
|
|
3579
4479
|
nextRetryAtMs: disposition.committedAtMs,
|
|
3580
4480
|
}
|
|
3581
4481
|
: candidate)
|
|
3582
4482
|
: current.file.entries.filter((candidate) => candidate.updateId !== entry.updateId);
|
|
3583
|
-
const published = publishMutation(current, nextEntries, true, [
|
|
4483
|
+
const published = publishMutation(current, nextEntries, true, [
|
|
4484
|
+
...(current.file.operatorDispositions ?? []),
|
|
4485
|
+
disposition,
|
|
4486
|
+
]);
|
|
3584
4487
|
return {
|
|
3585
4488
|
disposition: { ...disposition },
|
|
3586
4489
|
duplicate: false,
|
|
@@ -3594,30 +4497,44 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3594
4497
|
const current = readCurrent();
|
|
3595
4498
|
if (!journalHasCustodyFields(current.file.version))
|
|
3596
4499
|
throw createJournalError("conflict", path, "legacy custody disposition requires a custody journal");
|
|
3597
|
-
const existing = current.file.operatorDispositions?.find(candidate => candidate.failureId === authority.dispositionId);
|
|
4500
|
+
const existing = current.file.operatorDispositions?.find((candidate) => candidate.failureId === authority.dispositionId);
|
|
3598
4501
|
if (existing) {
|
|
3599
|
-
if (!("dispositionKind" in existing) ||
|
|
4502
|
+
if (!("dispositionKind" in existing) ||
|
|
4503
|
+
existing.dispositionKind !== "legacy-custody")
|
|
3600
4504
|
throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
|
|
3601
|
-
const normalizedDuplicate = normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(authority, {
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
4505
|
+
const normalizedDuplicate = normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(authority, {
|
|
4506
|
+
updateId: existing.updateId,
|
|
4507
|
+
state: "retry-wait",
|
|
4508
|
+
attemptCount: 1,
|
|
4509
|
+
failedAtMs: 0,
|
|
4510
|
+
failureClass: "retained-audit",
|
|
4511
|
+
summary: "retained-audit",
|
|
4512
|
+
evidenceSha256: existing.evidenceSha256,
|
|
4513
|
+
});
|
|
4514
|
+
if (!normalizedDuplicate ||
|
|
4515
|
+
existing.action !== normalizedDuplicate.action ||
|
|
4516
|
+
existing.operatorAuthorityId !==
|
|
4517
|
+
normalizedDuplicate.operatorAuthorityId ||
|
|
3606
4518
|
existing.authorizedAtMs !== normalizedDuplicate.authorizedAtMs)
|
|
3607
4519
|
throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
|
|
3608
4520
|
let authorized = false;
|
|
3609
4521
|
try {
|
|
3610
|
-
authorized =
|
|
4522
|
+
authorized =
|
|
4523
|
+
options.authorizeLegacyCustodyDisposition?.(normalizedDuplicate) === true;
|
|
3611
4524
|
}
|
|
3612
4525
|
catch {
|
|
3613
4526
|
authorized = false;
|
|
3614
4527
|
}
|
|
3615
4528
|
if (!authorized)
|
|
3616
4529
|
throw createJournalError("conflict", path, "legacy custody disposition is unauthorized");
|
|
3617
|
-
return {
|
|
3618
|
-
|
|
4530
|
+
return {
|
|
4531
|
+
disposition: { ...existing },
|
|
4532
|
+
duplicate: true,
|
|
4533
|
+
entryCount: current.file.entries.length,
|
|
4534
|
+
serializedBytes: current.serializedBytes,
|
|
4535
|
+
};
|
|
3619
4536
|
}
|
|
3620
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === authority.updateId);
|
|
4537
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === authority.updateId);
|
|
3621
4538
|
const evidence = entry && createTelegramUpdateJournalLegacyCustodyEvidence(entry);
|
|
3622
4539
|
if (!evidence)
|
|
3623
4540
|
throw createJournalError("conflict", path, `cannot dispose non-quarantined update ${authority.updateId}`);
|
|
@@ -3626,7 +4543,8 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3626
4543
|
throw createJournalError("conflict", path, `legacy custody evidence changed for update ${authority.updateId}`);
|
|
3627
4544
|
let authorized = false;
|
|
3628
4545
|
try {
|
|
3629
|
-
authorized =
|
|
4546
|
+
authorized =
|
|
4547
|
+
options.authorizeLegacyCustodyDisposition?.(normalized) === true;
|
|
3630
4548
|
}
|
|
3631
4549
|
catch {
|
|
3632
4550
|
authorized = false;
|
|
@@ -3637,24 +4555,38 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3637
4555
|
if (!isSafeNonNegativeInteger(nowMs))
|
|
3638
4556
|
throw createJournalError("invalid", path, "received an invalid legacy custody disposition timestamp");
|
|
3639
4557
|
const disposition = {
|
|
3640
|
-
dispositionKind: "legacy-custody",
|
|
3641
|
-
|
|
4558
|
+
dispositionKind: "legacy-custody",
|
|
4559
|
+
failureId: normalized.dispositionId,
|
|
4560
|
+
updateId: normalized.updateId,
|
|
4561
|
+
action: normalized.action,
|
|
3642
4562
|
committedAtMs: Math.max(nowMs, normalized.authorizedAtMs),
|
|
3643
4563
|
evidenceSha256: normalized.evidenceSha256,
|
|
3644
4564
|
operatorAuthorityId: normalized.operatorAuthorityId,
|
|
3645
|
-
authorizedAtMs: normalized.authorizedAtMs
|
|
4565
|
+
authorizedAtMs: normalized.authorizedAtMs,
|
|
3646
4566
|
};
|
|
3647
4567
|
const nextEntries = normalized.action === "discard"
|
|
3648
|
-
? current.file.entries.filter(candidate => candidate.updateId !== entry.updateId)
|
|
3649
|
-
: current.file.entries.map(candidate => candidate.updateId === entry.updateId
|
|
3650
|
-
? {
|
|
4568
|
+
? current.file.entries.filter((candidate) => candidate.updateId !== entry.updateId)
|
|
4569
|
+
: current.file.entries.map((candidate) => candidate.updateId === entry.updateId
|
|
4570
|
+
? {
|
|
4571
|
+
updateId: entry.updateId,
|
|
4572
|
+
update: entry.update,
|
|
3651
4573
|
admittedAtMs: entry.admittedAtMs,
|
|
3652
|
-
...(entry.preApprovalExcluded === undefined
|
|
3653
|
-
{
|
|
4574
|
+
...(entry.preApprovalExcluded === undefined
|
|
4575
|
+
? {}
|
|
4576
|
+
: { preApprovalExcluded: entry.preApprovalExcluded }),
|
|
4577
|
+
state: "pending",
|
|
4578
|
+
}
|
|
3654
4579
|
: candidate);
|
|
3655
|
-
const published = publishMutation(current, nextEntries, true, [
|
|
3656
|
-
|
|
3657
|
-
|
|
4580
|
+
const published = publishMutation(current, nextEntries, true, [
|
|
4581
|
+
...(current.file.operatorDispositions ?? []),
|
|
4582
|
+
disposition,
|
|
4583
|
+
]);
|
|
4584
|
+
return {
|
|
4585
|
+
disposition: { ...disposition },
|
|
4586
|
+
duplicate: false,
|
|
4587
|
+
entryCount: published.file.entries.length,
|
|
4588
|
+
serializedBytes: published.serializedBytes,
|
|
4589
|
+
};
|
|
3658
4590
|
});
|
|
3659
4591
|
},
|
|
3660
4592
|
offerQueuedHandoff(input) {
|
|
@@ -3750,13 +4682,18 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3750
4682
|
updateId: entry.updateId,
|
|
3751
4683
|
update: entry.update,
|
|
3752
4684
|
admittedAtMs: entry.admittedAtMs,
|
|
3753
|
-
...(entry.preApprovalExcluded !== undefined
|
|
4685
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4686
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4687
|
+
: {}),
|
|
3754
4688
|
state: "queued",
|
|
3755
4689
|
queueKind: input.queueKind,
|
|
3756
4690
|
queueReceiptId: input.receiptId,
|
|
3757
4691
|
queueOwner: cloneJournalQueueOwner(queueOwner),
|
|
3758
4692
|
...(entry.inputProvenance
|
|
3759
|
-
? {
|
|
4693
|
+
? {
|
|
4694
|
+
inputProvenance: structuredClone(entry.inputProvenance),
|
|
4695
|
+
}
|
|
4696
|
+
: {}),
|
|
3760
4697
|
}
|
|
3761
4698
|
: entry), true);
|
|
3762
4699
|
return {
|
|
@@ -3802,12 +4739,15 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3802
4739
|
};
|
|
3803
4740
|
});
|
|
3804
4741
|
},
|
|
3805
|
-
completeQueued(receipts) {
|
|
3806
|
-
|
|
4742
|
+
completeQueued(receipts) {
|
|
4743
|
+
return completeQueued(receipts);
|
|
4744
|
+
},
|
|
4745
|
+
completeQueuedExact(receipts, completions) {
|
|
4746
|
+
return completeQueued(receipts, completions, true);
|
|
4747
|
+
},
|
|
3807
4748
|
discardQueued(input) {
|
|
3808
4749
|
return runMutation((readCurrent) => {
|
|
3809
|
-
if ((input.queueKind !== "prompt" &&
|
|
3810
|
-
input.queueKind !== "control") ||
|
|
4750
|
+
if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
|
|
3811
4751
|
!isNonEmptyString(input.receiptId) ||
|
|
3812
4752
|
!Array.isArray(input.sourceUpdateIds) ||
|
|
3813
4753
|
input.sourceUpdateIds.length === 0) {
|
|
@@ -3832,12 +4772,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3832
4772
|
const current = readCurrent();
|
|
3833
4773
|
const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
|
|
3834
4774
|
if (receiptEntries.length !== requestedIds.size ||
|
|
3835
|
-
receiptEntries.some((entry) => !
|
|
3836
|
-
entry.state !== "queued" ||
|
|
3837
|
-
entry.queueKind !== input.queueKind ||
|
|
3838
|
-
!entry.queueOwner ||
|
|
3839
|
-
entry.queueHandoff !== undefined ||
|
|
3840
|
-
!areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, expectedOwner))) {
|
|
4775
|
+
receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, expectedOwner))) {
|
|
3841
4776
|
throw createJournalError("conflict", path, `cannot discard stale queue receipt ${input.receiptId}`);
|
|
3842
4777
|
}
|
|
3843
4778
|
const removedUpdateIds = [...requestedIds].sort((a, b) => a - b);
|
|
@@ -3884,17 +4819,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3884
4819
|
}
|
|
3885
4820
|
const current = readCurrent();
|
|
3886
4821
|
const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
|
|
3887
|
-
if ((input.queueKind !== "prompt" &&
|
|
3888
|
-
input.queueKind !== "control") ||
|
|
4822
|
+
if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
|
|
3889
4823
|
!isNonEmptyString(input.receiptId) ||
|
|
3890
4824
|
requestedIds.size === 0 ||
|
|
3891
4825
|
receiptEntries.length !== requestedIds.size ||
|
|
3892
|
-
receiptEntries.some((entry) => !
|
|
3893
|
-
entry.state !== "queued" ||
|
|
3894
|
-
entry.queueKind !== input.queueKind ||
|
|
3895
|
-
!entry.queueOwner ||
|
|
3896
|
-
entry.queueHandoff !== undefined ||
|
|
3897
|
-
!areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, deadOwner))) {
|
|
4826
|
+
receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, deadOwner))) {
|
|
3898
4827
|
throw createJournalError("conflict", path, `cannot recover stale queue receipt ${input.receiptId}`);
|
|
3899
4828
|
}
|
|
3900
4829
|
if (ownerLiveness !== "dead") {
|
|
@@ -3928,14 +4857,21 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3928
4857
|
throw createJournalError("invalid", path, "invalid abandonment update id");
|
|
3929
4858
|
// Inspection never repairs or quarantines source evidence.
|
|
3930
4859
|
const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
|
|
3931
|
-
const matches = (current.file.operatorDispositions ?? []).filter(item => item.updateId === updateId);
|
|
4860
|
+
const matches = (current.file.operatorDispositions ?? []).filter((item) => item.updateId === updateId);
|
|
3932
4861
|
const disposition = matches[0];
|
|
3933
|
-
if (matches.length !== 1 ||
|
|
3934
|
-
disposition
|
|
4862
|
+
if (matches.length !== 1 ||
|
|
4863
|
+
!disposition ||
|
|
4864
|
+
!("dispositionKind" in disposition) ||
|
|
4865
|
+
disposition.dispositionKind !== "legacy-custody" ||
|
|
4866
|
+
disposition.action !== "discard" ||
|
|
3935
4867
|
disposition.failureId !== `abandon-${disposition.evidenceSha256}` ||
|
|
3936
|
-
current.file.entries.some(entry => entry.updateId === updateId))
|
|
4868
|
+
current.file.entries.some((entry) => entry.updateId === updateId))
|
|
3937
4869
|
return undefined;
|
|
3938
|
-
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4870
|
+
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4871
|
+
path,
|
|
4872
|
+
profileName: profile,
|
|
4873
|
+
botIdentity: expectedIdentity,
|
|
4874
|
+
});
|
|
3939
4875
|
const retainedPath = join(`${path}.retained`, `${disposition.failureId}.json`);
|
|
3940
4876
|
let stat;
|
|
3941
4877
|
try {
|
|
@@ -3946,20 +4882,33 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3946
4882
|
throw createJournalError("conflict", retainedPath, "committed abandonment lost its retained input");
|
|
3947
4883
|
throw error;
|
|
3948
4884
|
}
|
|
3949
|
-
if (!stat.isFile() ||
|
|
4885
|
+
if (!stat.isFile() ||
|
|
4886
|
+
stat.isSymbolicLink() ||
|
|
4887
|
+
stat.size > maxBytes + 4096) {
|
|
3950
4888
|
throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
|
|
3951
4889
|
}
|
|
3952
4890
|
const parsed = JSON.parse(readFileSync(retainedPath, "utf8"));
|
|
3953
4891
|
if (!isRecord(parsed))
|
|
3954
4892
|
throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
|
|
3955
4893
|
const entry = validateJournalEntry(parsed.entry, retainedPath, version);
|
|
3956
|
-
const reference = createPendingRetentionReference({
|
|
4894
|
+
const reference = createPendingRetentionReference({
|
|
4895
|
+
path,
|
|
4896
|
+
journalBindingKey,
|
|
4897
|
+
entry,
|
|
4898
|
+
maxBytes,
|
|
4899
|
+
});
|
|
3957
4900
|
const retained = reference.read();
|
|
3958
|
-
if (entry.updateId !== updateId ||
|
|
4901
|
+
if (entry.updateId !== updateId ||
|
|
4902
|
+
reference.retainedPath !== retainedPath ||
|
|
3959
4903
|
!isDeepStrictEqual(retained?.requestedDisposition, disposition)) {
|
|
3960
4904
|
throw createJournalError("conflict", retainedPath, "retained input does not match committed abandonment");
|
|
3961
4905
|
}
|
|
3962
|
-
return {
|
|
4906
|
+
return {
|
|
4907
|
+
journalBindingKey,
|
|
4908
|
+
updateId,
|
|
4909
|
+
retainedPath,
|
|
4910
|
+
operatorAuthorityId: disposition.operatorAuthorityId,
|
|
4911
|
+
};
|
|
3963
4912
|
});
|
|
3964
4913
|
},
|
|
3965
4914
|
inspectPendingRetention(entry) {
|
|
@@ -3970,11 +4919,25 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3970
4919
|
const expected = validateJournalEntry(entry, path, version);
|
|
3971
4920
|
if (expected.state !== "pending")
|
|
3972
4921
|
throw createJournalError("conflict", path, "retention inspection requires a pending source");
|
|
3973
|
-
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
3974
|
-
|
|
4922
|
+
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4923
|
+
path,
|
|
4924
|
+
profileName: profile,
|
|
4925
|
+
botIdentity: expectedIdentity,
|
|
4926
|
+
});
|
|
4927
|
+
const reference = createPendingRetentionReference({
|
|
4928
|
+
path,
|
|
4929
|
+
journalBindingKey,
|
|
4930
|
+
entry: expected,
|
|
4931
|
+
maxBytes,
|
|
4932
|
+
});
|
|
3975
4933
|
const retained = reference.read();
|
|
3976
|
-
return retained
|
|
3977
|
-
|
|
4934
|
+
return retained
|
|
4935
|
+
? {
|
|
4936
|
+
journalBindingKey,
|
|
4937
|
+
retainedPath: reference.retainedPath,
|
|
4938
|
+
requestedDisposition: retained.requestedDisposition,
|
|
4939
|
+
}
|
|
4940
|
+
: undefined;
|
|
3978
4941
|
});
|
|
3979
4942
|
},
|
|
3980
4943
|
abandonPending(input) {
|
|
@@ -3982,12 +4945,17 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3982
4945
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(version)) {
|
|
3983
4946
|
throw createJournalError("unsupported-version", path, "pending abandonment requires the legacy v1 source");
|
|
3984
4947
|
}
|
|
3985
|
-
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4948
|
+
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4949
|
+
path,
|
|
4950
|
+
profileName: profile,
|
|
4951
|
+
botIdentity: expectedIdentity,
|
|
4952
|
+
});
|
|
3986
4953
|
if (input.journalBindingKey !== journalBindingKey) {
|
|
3987
4954
|
throw createJournalError("identity-mismatch", path, "pending abandonment belongs to another source");
|
|
3988
4955
|
}
|
|
3989
4956
|
if (!isBoundedString(input.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
3990
|
-
!input.operatorAuthorityId.trim() ||
|
|
4957
|
+
!input.operatorAuthorityId.trim() ||
|
|
4958
|
+
typeof input.isCurrent !== "function") {
|
|
3991
4959
|
throw createJournalError("invalid", path, "pending abandonment requires explicit operator authority");
|
|
3992
4960
|
}
|
|
3993
4961
|
const assertCurrent = () => {
|
|
@@ -3996,30 +4964,52 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3996
4964
|
};
|
|
3997
4965
|
assertCurrent();
|
|
3998
4966
|
const expected = validateJournalEntry(input.entry, path, version);
|
|
3999
|
-
if (expected.state !== "pending" ||
|
|
4000
|
-
expected.
|
|
4967
|
+
if (expected.state !== "pending" ||
|
|
4968
|
+
expected.inputClaim ||
|
|
4969
|
+
expected.inputProvenance ||
|
|
4970
|
+
expected.queueOwner ||
|
|
4971
|
+
expected.queueHandoff ||
|
|
4972
|
+
expected.queueReceiptId ||
|
|
4973
|
+
expected.failure) {
|
|
4001
4974
|
throw createJournalError("conflict", path, "only an exact unclaimed pending source may be abandoned");
|
|
4002
4975
|
}
|
|
4003
4976
|
// Never repair/quarantine source evidence while deciding an operator cancellation.
|
|
4004
|
-
const readStrict = sourceAccess
|
|
4977
|
+
const readStrict = sourceAccess
|
|
4978
|
+
? readCurrent
|
|
4979
|
+
: () => readCurrentStrict(false);
|
|
4005
4980
|
const current = readStrict();
|
|
4006
|
-
const { evidenceSha256, failureId, retainedPath, read: readRetention } = createPendingRetentionReference({
|
|
4007
|
-
|
|
4008
|
-
|
|
4981
|
+
const { evidenceSha256, failureId, retainedPath, read: readRetention, } = createPendingRetentionReference({
|
|
4982
|
+
path,
|
|
4983
|
+
journalBindingKey,
|
|
4984
|
+
entry: expected,
|
|
4985
|
+
maxBytes,
|
|
4986
|
+
});
|
|
4987
|
+
const existing = current.file.operatorDispositions?.find((item) => item.failureId === failureId || item.updateId === expected.updateId);
|
|
4988
|
+
const currentEntry = current.file.entries.find((entry) => entry.updateId === expected.updateId);
|
|
4009
4989
|
const assertRetention = (retained, disposition) => {
|
|
4010
|
-
if (!retained ||
|
|
4990
|
+
if (!retained ||
|
|
4991
|
+
!isDeepStrictEqual(retained.requestedDisposition, disposition)) {
|
|
4011
4992
|
throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
|
|
4012
4993
|
}
|
|
4013
4994
|
};
|
|
4014
4995
|
if (existing) {
|
|
4015
|
-
if (currentEntry ||
|
|
4016
|
-
|
|
4996
|
+
if (currentEntry ||
|
|
4997
|
+
!("dispositionKind" in existing) ||
|
|
4998
|
+
existing.dispositionKind !== "legacy-custody" ||
|
|
4999
|
+
existing.failureId !== failureId ||
|
|
5000
|
+
existing.action !== "discard" ||
|
|
5001
|
+
existing.evidenceSha256 !== evidenceSha256) {
|
|
4017
5002
|
throw createJournalError("conflict", path, "pending source already has another disposition");
|
|
4018
5003
|
}
|
|
4019
5004
|
assertRetention(readRetention(), existing);
|
|
4020
5005
|
assertCurrent();
|
|
4021
|
-
return {
|
|
4022
|
-
|
|
5006
|
+
return {
|
|
5007
|
+
disposition: existing,
|
|
5008
|
+
retainedPath,
|
|
5009
|
+
duplicate: true,
|
|
5010
|
+
entryCount: current.file.entries.length,
|
|
5011
|
+
serializedBytes: current.serializedBytes,
|
|
5012
|
+
};
|
|
4023
5013
|
}
|
|
4024
5014
|
if (!currentEntry || !isDeepStrictEqual(currentEntry, expected)) {
|
|
4025
5015
|
throw createJournalError("conflict", path, "pending source changed before abandonment");
|
|
@@ -4035,11 +5025,23 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4035
5025
|
else {
|
|
4036
5026
|
// Reuse the existing legacy discard tombstone so older v1 readers also refuse replay.
|
|
4037
5027
|
// This is an operator disposition, not a fabricated execution failure or completion.
|
|
4038
|
-
disposition = {
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
5028
|
+
disposition = {
|
|
5029
|
+
dispositionKind: "legacy-custody",
|
|
5030
|
+
failureId,
|
|
5031
|
+
updateId: expected.updateId,
|
|
5032
|
+
action: "discard",
|
|
5033
|
+
committedAtMs: nowMs,
|
|
5034
|
+
evidenceSha256,
|
|
5035
|
+
operatorAuthorityId: input.operatorAuthorityId,
|
|
5036
|
+
authorizedAtMs: nowMs,
|
|
5037
|
+
};
|
|
5038
|
+
const copy = {
|
|
5039
|
+
version: 1,
|
|
5040
|
+
kind: "pending-input-retention",
|
|
5041
|
+
journalBindingKey,
|
|
5042
|
+
entry: expected,
|
|
5043
|
+
requestedDisposition: disposition,
|
|
5044
|
+
};
|
|
4043
5045
|
writeJournalFile(retainedPath, `${JSON.stringify(copy)}\n`, (boundary, target) => {
|
|
4044
5046
|
onPublicationBoundary?.(boundary, target);
|
|
4045
5047
|
assertCurrent();
|
|
@@ -4047,12 +5049,20 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4047
5049
|
}
|
|
4048
5050
|
assertCurrent();
|
|
4049
5051
|
assertRetention(readRetention(), disposition);
|
|
4050
|
-
const committed = (snapshot) => !snapshot.file.entries.some(entry => entry.updateId === expected.updateId) &&
|
|
4051
|
-
snapshot.file.operatorDispositions?.some(item => isDeepStrictEqual(item, disposition));
|
|
5052
|
+
const committed = (snapshot) => !snapshot.file.entries.some((entry) => entry.updateId === expected.updateId) &&
|
|
5053
|
+
snapshot.file.operatorDispositions?.some((item) => isDeepStrictEqual(item, disposition));
|
|
4052
5054
|
try {
|
|
4053
|
-
const published = publishMutation(current, current.file.entries.filter(entry => entry.updateId !== expected.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => {
|
|
4054
|
-
|
|
4055
|
-
|
|
5055
|
+
const published = publishMutation(current, current.file.entries.filter((entry) => entry.updateId !== expected.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => {
|
|
5056
|
+
onPublicationBoundary?.(boundary, target);
|
|
5057
|
+
assertCurrent();
|
|
5058
|
+
});
|
|
5059
|
+
return {
|
|
5060
|
+
disposition,
|
|
5061
|
+
retainedPath,
|
|
5062
|
+
duplicate: false,
|
|
5063
|
+
entryCount: published.file.entries.length,
|
|
5064
|
+
serializedBytes: published.serializedBytes,
|
|
5065
|
+
};
|
|
4056
5066
|
}
|
|
4057
5067
|
catch (error) {
|
|
4058
5068
|
// A segment may be durable even if later snapshot compaction failed.
|
|
@@ -4067,16 +5077,26 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4067
5077
|
if (!committed(observed))
|
|
4068
5078
|
throw error;
|
|
4069
5079
|
assertRetention(readRetention(), disposition);
|
|
4070
|
-
return {
|
|
4071
|
-
|
|
5080
|
+
return {
|
|
5081
|
+
disposition,
|
|
5082
|
+
retainedPath,
|
|
5083
|
+
duplicate: false,
|
|
5084
|
+
entryCount: observed.file.entries.length,
|
|
5085
|
+
serializedBytes: observed.serializedBytes,
|
|
5086
|
+
};
|
|
4072
5087
|
}
|
|
4073
5088
|
});
|
|
4074
5089
|
},
|
|
4075
|
-
removeCompleted(updateIds) {
|
|
4076
|
-
|
|
5090
|
+
removeCompleted(updateIds) {
|
|
5091
|
+
return removeCompleted(updateIds);
|
|
5092
|
+
},
|
|
5093
|
+
removeCompletedExact(updateIds, expectedSources, completions, isCurrent) {
|
|
5094
|
+
return removeCompleted(updateIds, expectedSources, true, completions, isCurrent);
|
|
5095
|
+
},
|
|
4077
5096
|
inspectQueuedReceipt(expected) {
|
|
4078
|
-
return runMutationCore(readCurrent => {
|
|
4079
|
-
if (!sourceAccess ||
|
|
5097
|
+
return runMutationCore((readCurrent) => {
|
|
5098
|
+
if (!sourceAccess ||
|
|
5099
|
+
!isTelegramUpdateJournalLegacyFamilyVersion(version)) {
|
|
4080
5100
|
throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
|
|
4081
5101
|
}
|
|
4082
5102
|
const queueOwner = validateQueuedReceiptObservation(expected, path);
|
|
@@ -4085,7 +5105,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4085
5105
|
});
|
|
4086
5106
|
},
|
|
4087
5107
|
inspectSourceCompletion(expected) {
|
|
4088
|
-
return runMutation(readCurrent => {
|
|
5108
|
+
return runMutation((readCurrent) => {
|
|
4089
5109
|
if (!sourceAccess)
|
|
4090
5110
|
throw createJournalError("invalid", path, "source completion requires an exact source handle");
|
|
4091
5111
|
const completion = validateJournalSourceCompletion(expected, path);
|
|
@@ -4096,62 +5116,126 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4096
5116
|
};
|
|
4097
5117
|
if (isTelegramUpdateJournalLegacyFamilyVersion(version))
|
|
4098
5118
|
journal.routingInputs = {
|
|
4099
|
-
arm(input) {
|
|
4100
|
-
|
|
5119
|
+
arm(input) {
|
|
5120
|
+
if (input.chooser !== undefined &&
|
|
5121
|
+
!isTelegramUpdateJournalRoutingChooser(input.chooser))
|
|
5122
|
+
throw createJournalError("invalid", path, "invalid routing input chooser location");
|
|
5123
|
+
return mutateRoutingInputs(input, false, input.publishedAtMs, input.chooser).entries;
|
|
5124
|
+
},
|
|
5125
|
+
select(input) {
|
|
5126
|
+
return mutateRoutingInputs(input, true);
|
|
5127
|
+
},
|
|
4101
5128
|
inspectExpiry(updateId) {
|
|
4102
|
-
return runMutation(readCurrent => {
|
|
5129
|
+
return runMutation((readCurrent) => {
|
|
4103
5130
|
if (!isSafeNonNegativeInteger(updateId))
|
|
4104
5131
|
throw createJournalError("invalid", path, "invalid routing expiry update ID");
|
|
4105
|
-
return inspectRoutingInputExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
|
|
5132
|
+
return inspectRoutingInputExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
|
|
5133
|
+
path,
|
|
5134
|
+
profileName: profile,
|
|
5135
|
+
botIdentity: expectedIdentity,
|
|
5136
|
+
}), updateId);
|
|
4106
5137
|
});
|
|
4107
5138
|
},
|
|
4108
5139
|
inspectGroupExpiry(updateIds) {
|
|
4109
|
-
return runMutation(readCurrent => inspectRoutingGroupExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
|
|
5140
|
+
return runMutation((readCurrent) => inspectRoutingGroupExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
|
|
5141
|
+
path,
|
|
5142
|
+
profileName: profile,
|
|
5143
|
+
botIdentity: expectedIdentity,
|
|
5144
|
+
}), updateIds));
|
|
4110
5145
|
},
|
|
4111
5146
|
expire(input) {
|
|
4112
|
-
return runMutation(readCurrent => {
|
|
5147
|
+
return runMutation((readCurrent) => {
|
|
4113
5148
|
const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
|
|
4114
|
-
const binding = createTelegramUpdateJournalBindingKey({
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
5149
|
+
const binding = createTelegramUpdateJournalBindingKey({
|
|
5150
|
+
path,
|
|
5151
|
+
profileName: profile,
|
|
5152
|
+
botIdentity: expectedIdentity,
|
|
5153
|
+
});
|
|
5154
|
+
if (!lifetime ||
|
|
5155
|
+
original.state !== "pending" ||
|
|
5156
|
+
original.inputClaim ||
|
|
5157
|
+
original.inputProvenance ||
|
|
5158
|
+
original.queueOwner ||
|
|
5159
|
+
original.queueReceiptId ||
|
|
5160
|
+
original.queueHandoff ||
|
|
5161
|
+
original.failure ||
|
|
5162
|
+
input.journalBindingKey !== binding ||
|
|
5163
|
+
lifetime.operatorUserId !== input.operatorUserId ||
|
|
5164
|
+
!isSafePositiveInteger(input.operatorUserId) ||
|
|
5165
|
+
typeof input.isCurrent !== "function") {
|
|
4118
5166
|
throw createJournalError("conflict", path, "routing expiry requires an exact unclaimed chooser source");
|
|
4119
5167
|
}
|
|
4120
5168
|
const assertCurrent = () => {
|
|
4121
5169
|
const now = getNowMs();
|
|
4122
|
-
if (!isSafeNonNegativeInteger(now) ||
|
|
5170
|
+
if (!isSafeNonNegativeInteger(now) ||
|
|
5171
|
+
now < lifetime.expiresAtMs ||
|
|
5172
|
+
!input.isCurrent())
|
|
4123
5173
|
throw createJournalError("conflict", path, "routing source has not expired under current authority");
|
|
4124
5174
|
};
|
|
4125
5175
|
assertCurrent();
|
|
4126
|
-
const current = sourceAccess
|
|
4127
|
-
|
|
5176
|
+
const current = sourceAccess
|
|
5177
|
+
? readCurrent()
|
|
5178
|
+
: readCurrentStrict(false);
|
|
5179
|
+
const evidenceSha256 = createHash("sha256")
|
|
5180
|
+
.update(JSON.stringify(original))
|
|
5181
|
+
.digest("hex");
|
|
4128
5182
|
const failureId = `routing-expiry:${evidenceSha256}`;
|
|
4129
|
-
const existing = current.file.operatorDispositions?.find(value => value.updateId === original.updateId);
|
|
4130
|
-
const entry = current.file.entries.find(value => value.updateId === original.updateId);
|
|
5183
|
+
const existing = current.file.operatorDispositions?.find((value) => value.updateId === original.updateId);
|
|
5184
|
+
const entry = current.file.entries.find((value) => value.updateId === original.updateId);
|
|
4131
5185
|
if (existing) {
|
|
4132
|
-
if (entry ||
|
|
4133
|
-
|
|
5186
|
+
if (entry ||
|
|
5187
|
+
!("dispositionKind" in existing) ||
|
|
5188
|
+
existing.dispositionKind !== "legacy-custody" ||
|
|
5189
|
+
existing.action !== "discard" ||
|
|
5190
|
+
existing.failureId !== failureId ||
|
|
5191
|
+
existing.evidenceSha256 !== evidenceSha256 ||
|
|
5192
|
+
existing.operatorAuthorityId !==
|
|
5193
|
+
`telegram-owner:${input.operatorUserId}`)
|
|
4134
5194
|
throw createJournalError("conflict", path, "routing source has contradictory expiry evidence");
|
|
4135
5195
|
assertCurrent();
|
|
4136
|
-
return {
|
|
5196
|
+
return {
|
|
5197
|
+
disposition: existing,
|
|
5198
|
+
duplicate: true,
|
|
5199
|
+
entryCount: current.file.entries.length,
|
|
5200
|
+
serializedBytes: current.serializedBytes,
|
|
5201
|
+
};
|
|
4137
5202
|
}
|
|
4138
5203
|
if (!entry || !isDeepStrictEqual(entry, original))
|
|
4139
5204
|
throw createJournalError("conflict", path, "routing source changed before expiry");
|
|
4140
5205
|
// A discard tombstone prevents old readers and delayed reports from replaying; no prompt-body archive is written.
|
|
4141
5206
|
const now = getNowMs();
|
|
4142
|
-
const disposition = {
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
5207
|
+
const disposition = {
|
|
5208
|
+
dispositionKind: "legacy-custody",
|
|
5209
|
+
failureId,
|
|
5210
|
+
updateId: original.updateId,
|
|
5211
|
+
action: "discard",
|
|
5212
|
+
committedAtMs: now,
|
|
5213
|
+
authorizedAtMs: now,
|
|
5214
|
+
evidenceSha256,
|
|
5215
|
+
operatorAuthorityId: `telegram-owner:${input.operatorUserId}`,
|
|
5216
|
+
};
|
|
5217
|
+
const published = publishMutation(current, current.file.entries.filter((value) => value.updateId !== original.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => {
|
|
5218
|
+
onPublicationBoundary?.(boundary, target);
|
|
5219
|
+
assertCurrent();
|
|
5220
|
+
});
|
|
4146
5221
|
assertCurrent();
|
|
4147
|
-
return {
|
|
5222
|
+
return {
|
|
5223
|
+
disposition,
|
|
5224
|
+
duplicate: false,
|
|
5225
|
+
entryCount: published.file.entries.length,
|
|
5226
|
+
serializedBytes: published.serializedBytes,
|
|
5227
|
+
};
|
|
4148
5228
|
});
|
|
4149
5229
|
},
|
|
4150
5230
|
};
|
|
4151
5231
|
if (!getInputContext)
|
|
4152
5232
|
return { journal };
|
|
4153
5233
|
// Strict source acquisition already requires this exact stored receipt scope.
|
|
4154
|
-
const bindingKey = createTelegramUpdateJournalBindingKey({
|
|
5234
|
+
const bindingKey = createTelegramUpdateJournalBindingKey({
|
|
5235
|
+
path,
|
|
5236
|
+
profileName: profile,
|
|
5237
|
+
botIdentity: expectedIdentity,
|
|
5238
|
+
});
|
|
4155
5239
|
const createInputSourceReference = (updateId) => ({
|
|
4156
5240
|
journalBindingKey: bindingKey,
|
|
4157
5241
|
tokenSha256: expectedIdentity.tokenSha256,
|
|
@@ -4184,10 +5268,15 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4184
5268
|
assertInputProcess(owner);
|
|
4185
5269
|
return { owner, recipientBindingKey };
|
|
4186
5270
|
};
|
|
4187
|
-
const runInputAdmission = (kind, operation) => workspaceAdmission
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
5271
|
+
const runInputAdmission = (kind, operation) => workspaceAdmission
|
|
5272
|
+
? runWithTelegramWorkspaceAdmissions({
|
|
5273
|
+
ledger: workspaceAdmission,
|
|
5274
|
+
operationId: `input:${randomUUID()}`,
|
|
5275
|
+
operationKind: `journal.input.${kind}`,
|
|
5276
|
+
scopes: [{ kind: "profile" }],
|
|
5277
|
+
operation,
|
|
5278
|
+
})
|
|
5279
|
+
: operation();
|
|
4191
5280
|
const runInputMutation = (kind, operation) => runInputAdmission(kind, () => runMutation(operation));
|
|
4192
5281
|
const publishInputMutation = (current, entries, updateId, context) => publishMutation(current, entries, true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, publicationPath) => {
|
|
4193
5282
|
onPublicationBoundary?.(boundary, publicationPath);
|
|
@@ -4197,33 +5286,49 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4197
5286
|
}, updateId);
|
|
4198
5287
|
const publishInputSettlement = (current, entries, contentChanged) => publishMutation(current, entries, contentChanged, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, false);
|
|
4199
5288
|
const normalizeInputSourceReference = (value, operation) => {
|
|
4200
|
-
if (!isRecord(value) ||
|
|
5289
|
+
if (!isRecord(value) ||
|
|
5290
|
+
!hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId"])) {
|
|
4201
5291
|
throw createJournalError("invalid", path, `received invalid input handoff ${operation} source`);
|
|
4202
5292
|
}
|
|
4203
5293
|
const journalBindingKey = value.journalBindingKey;
|
|
4204
5294
|
const updateId = value.updateId;
|
|
4205
|
-
if (journalBindingKey !== bindingKey ||
|
|
5295
|
+
if (journalBindingKey !== bindingKey ||
|
|
5296
|
+
value.tokenSha256 !== expectedIdentity.tokenSha256 ||
|
|
4206
5297
|
!isSafeNonNegativeInteger(updateId)) {
|
|
4207
5298
|
throw createJournalError("conflict", path, "input receipt does not match its source identity");
|
|
4208
5299
|
}
|
|
4209
|
-
return {
|
|
5300
|
+
return {
|
|
5301
|
+
journalBindingKey,
|
|
5302
|
+
tokenSha256: expectedIdentity.tokenSha256,
|
|
5303
|
+
updateId,
|
|
5304
|
+
};
|
|
4210
5305
|
};
|
|
4211
5306
|
const normalizeInputReceipt = (value, requireCurrentProcess = true) => {
|
|
4212
5307
|
if (!isRecord(value) ||
|
|
4213
|
-
!hasOnlyKeys(value, [
|
|
5308
|
+
!hasOnlyKeys(value, [
|
|
5309
|
+
"journalBindingKey",
|
|
5310
|
+
"tokenSha256",
|
|
5311
|
+
"updateId",
|
|
5312
|
+
"owner",
|
|
5313
|
+
])) {
|
|
4214
5314
|
throw createJournalError("invalid", path, "received an invalid input receipt");
|
|
4215
5315
|
}
|
|
4216
|
-
const source = normalizeInputSourceReference({
|
|
4217
|
-
|
|
5316
|
+
const source = normalizeInputSourceReference({
|
|
5317
|
+
journalBindingKey: value.journalBindingKey,
|
|
5318
|
+
tokenSha256: value.tokenSha256,
|
|
5319
|
+
updateId: value.updateId,
|
|
5320
|
+
}, "receipt");
|
|
4218
5321
|
const owner = validateJournalQueueOwner(value.owner, path);
|
|
4219
5322
|
if (requireCurrentProcess)
|
|
4220
5323
|
assertInputProcess(owner);
|
|
4221
5324
|
return { ...source, owner };
|
|
4222
5325
|
};
|
|
4223
5326
|
const normalizeInputHandoffOffer = (value) => {
|
|
4224
|
-
if (!isRecord(value) ||
|
|
5327
|
+
if (!isRecord(value) ||
|
|
5328
|
+
!hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffToken"]) ||
|
|
4225
5329
|
!isBoundedString(value.handoffToken, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MAX_LENGTH) ||
|
|
4226
|
-
value.handoffToken.length <
|
|
5330
|
+
value.handoffToken.length <
|
|
5331
|
+
TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH) {
|
|
4227
5332
|
throw createJournalError("invalid", path, "received invalid input handoff offer");
|
|
4228
5333
|
}
|
|
4229
5334
|
const receipt = normalizeInputReceipt(value.receipt);
|
|
@@ -4240,52 +5345,81 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4240
5345
|
return value;
|
|
4241
5346
|
};
|
|
4242
5347
|
const normalizeInputHandoffAccept = (value) => {
|
|
4243
|
-
if (!isRecord(value) ||
|
|
5348
|
+
if (!isRecord(value) ||
|
|
5349
|
+
!hasOnlyKeys(value, ["source", "recipientOwner", "handoffId"])) {
|
|
4244
5350
|
throw createJournalError("invalid", path, "received invalid input handoff accept");
|
|
4245
5351
|
}
|
|
4246
5352
|
const source = normalizeInputSourceReference(value.source, "accept");
|
|
4247
5353
|
const recipientOwner = validateJournalQueueOwnerIdentity(value.recipientOwner, path);
|
|
4248
5354
|
assertInputProcess(recipientOwner);
|
|
4249
|
-
return {
|
|
5355
|
+
return {
|
|
5356
|
+
source,
|
|
5357
|
+
recipientOwner,
|
|
5358
|
+
handoffId: normalizeInputHandoffId(value.handoffId, "accept"),
|
|
5359
|
+
};
|
|
4250
5360
|
};
|
|
4251
5361
|
const normalizeInputHandoffCancel = (value) => {
|
|
4252
|
-
if (!isRecord(value) ||
|
|
5362
|
+
if (!isRecord(value) ||
|
|
5363
|
+
!hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffId"])) {
|
|
4253
5364
|
throw createJournalError("invalid", path, "received invalid input handoff cancel");
|
|
4254
5365
|
}
|
|
4255
|
-
return {
|
|
5366
|
+
return {
|
|
5367
|
+
receipt: normalizeInputReceipt(value.receipt),
|
|
4256
5368
|
recipientOwner: validateJournalQueueOwnerIdentity(value.recipientOwner, path),
|
|
4257
|
-
handoffId: normalizeInputHandoffId(value.handoffId, "cancel")
|
|
5369
|
+
handoffId: normalizeInputHandoffId(value.handoffId, "cancel"),
|
|
5370
|
+
};
|
|
4258
5371
|
};
|
|
4259
5372
|
const normalizeInputQueue = (value) => {
|
|
4260
|
-
if (!isRecord(value) ||
|
|
5373
|
+
if (!isRecord(value) ||
|
|
5374
|
+
!hasOnlyKeys(value, ["queueKind", "receiptId", "receipts"]) ||
|
|
4261
5375
|
(value.queueKind !== "prompt" && value.queueKind !== "control") ||
|
|
4262
5376
|
!isBoundedString(value.receiptId, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH) ||
|
|
4263
|
-
!value.receiptId.trim() ||
|
|
5377
|
+
!value.receiptId.trim() ||
|
|
5378
|
+
!Array.isArray(value.receipts) ||
|
|
5379
|
+
value.receipts.length === 0) {
|
|
4264
5380
|
throw createJournalError("invalid", path, "received invalid input queue transition");
|
|
4265
5381
|
}
|
|
4266
|
-
const receipts = value.receipts
|
|
5382
|
+
const receipts = value.receipts
|
|
5383
|
+
.map((receipt) => normalizeInputReceipt(receipt))
|
|
5384
|
+
.sort((left, right) => left.updateId - right.updateId);
|
|
4267
5385
|
if (receipts.some((receipt, index) => index > 0 && receipt.updateId === receipts[index - 1].updateId)) {
|
|
4268
5386
|
throw createJournalError("invalid", path, "received duplicate input queue transition receipts");
|
|
4269
5387
|
}
|
|
4270
5388
|
return { queueKind: value.queueKind, receiptId: value.receiptId, receipts };
|
|
4271
5389
|
};
|
|
4272
5390
|
const ownedInput = (current, receipt) => {
|
|
4273
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
|
|
4274
|
-
if (!entry?.inputClaim ||
|
|
5391
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
|
|
5392
|
+
if (!entry?.inputClaim ||
|
|
5393
|
+
entry.state !== "pending" ||
|
|
4275
5394
|
!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner)) {
|
|
4276
5395
|
throw createJournalError("conflict", path, "input receipt lost its exact acquisition or phase");
|
|
4277
5396
|
}
|
|
4278
5397
|
return entry;
|
|
4279
5398
|
};
|
|
5399
|
+
// Handoff offer/cancel require an idle ready donor claim owned by this exact context and binding.
|
|
5400
|
+
const readReadyDonorInput = (current, receipt) => {
|
|
5401
|
+
const context = currentInputContext();
|
|
5402
|
+
const entry = ownedInput(current, receipt);
|
|
5403
|
+
const claim = entry.inputClaim;
|
|
5404
|
+
if (claim.phase !== "ready" ||
|
|
5405
|
+
!inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
|
|
5406
|
+
claim.recipientBindingKey !== context.recipientBindingKey) {
|
|
5407
|
+
throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
|
|
5408
|
+
}
|
|
5409
|
+
return { context, current, entry, claim };
|
|
5410
|
+
};
|
|
4280
5411
|
const getReadyInputRelease = (current, receipt, allowOffered = false) => {
|
|
4281
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
|
|
4282
|
-
if (!entry ||
|
|
5412
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
|
|
5413
|
+
if (!entry ||
|
|
5414
|
+
entry.state !== "pending" ||
|
|
5415
|
+
entry.preApprovalExcluded !== false) {
|
|
4283
5416
|
throw createJournalError("conflict", path, "input is unavailable for ready-claim release");
|
|
4284
5417
|
}
|
|
4285
5418
|
if (!entry.inputClaim)
|
|
4286
5419
|
return { entry, unclaimed: true };
|
|
4287
5420
|
if (!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
|
|
4288
|
-
entry.inputClaim.phase !== "ready" ||
|
|
5421
|
+
entry.inputClaim.phase !== "ready" ||
|
|
5422
|
+
(!allowOffered && entry.inputClaim.handoff !== undefined)) {
|
|
4289
5423
|
throw createJournalError("conflict", path, "cannot release stale, running, or offered input authority");
|
|
4290
5424
|
}
|
|
4291
5425
|
return { entry, unclaimed: false };
|
|
@@ -4293,7 +5427,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4293
5427
|
const publishReadyInputRelease = (current, entry) => {
|
|
4294
5428
|
const released = cloneEntry(entry);
|
|
4295
5429
|
delete released.inputClaim;
|
|
4296
|
-
return publishMutation(current, current.file.entries.map(candidate => candidate.updateId === entry.updateId ? released : candidate), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, entry.updateId);
|
|
5430
|
+
return publishMutation(current, current.file.entries.map((candidate) => candidate.updateId === entry.updateId ? released : candidate), true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, entry.updateId);
|
|
4297
5431
|
};
|
|
4298
5432
|
const input = {
|
|
4299
5433
|
read: journal.read,
|
|
@@ -4301,12 +5435,16 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4301
5435
|
listLegacyCustodyCandidates() {
|
|
4302
5436
|
const sourceAccess = options.sourceAccess;
|
|
4303
5437
|
const evidence = readTelegramUpdateJournalSource({
|
|
4304
|
-
directory: sourceAccess.directory,
|
|
4305
|
-
|
|
5438
|
+
directory: sourceAccess.directory,
|
|
5439
|
+
path,
|
|
5440
|
+
profile,
|
|
5441
|
+
botIdentity: expectedIdentity,
|
|
5442
|
+
limits: sourceAccess.limits,
|
|
4306
5443
|
version,
|
|
4307
5444
|
});
|
|
4308
5445
|
return evidence.kind === "present"
|
|
4309
|
-
? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file)
|
|
5446
|
+
? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file)
|
|
5447
|
+
: [];
|
|
4310
5448
|
},
|
|
4311
5449
|
applyLegacyCustodyDisposition(value) {
|
|
4312
5450
|
return runInputAdmission("legacy-custody-disposition", () => journal.applyLegacyCustodyDisposition(value));
|
|
@@ -4337,117 +5475,163 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4337
5475
|
}
|
|
4338
5476
|
requestedIds.add(updateId);
|
|
4339
5477
|
}
|
|
4340
|
-
return runInputMutation("remove-excluded", read => {
|
|
5478
|
+
return runInputMutation("remove-excluded", (read) => {
|
|
4341
5479
|
const current = read();
|
|
4342
|
-
if (!current.exists ||
|
|
5480
|
+
if (!current.exists ||
|
|
5481
|
+
[...requestedIds].some((id) => id > (current.file.acceptedThroughUpdateId ?? -1))) {
|
|
4343
5482
|
throw createJournalError("conflict", path, "exclusion removal lacks retained source admission evidence");
|
|
4344
5483
|
}
|
|
4345
|
-
const selected = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
|
|
4346
|
-
if (selected.some(entry => entry.preApprovalExcluded !== true)) {
|
|
5484
|
+
const selected = current.file.entries.filter((entry) => requestedIds.has(entry.updateId));
|
|
5485
|
+
if (selected.some((entry) => entry.preApprovalExcluded !== true)) {
|
|
4347
5486
|
throw createJournalError("conflict", path, "cannot remove input without immutable exclusion evidence");
|
|
4348
5487
|
}
|
|
4349
5488
|
// Strict v3 decoding already excludes raw claims and queued authority on vetoed entries.
|
|
4350
|
-
const removedUpdateIds = selected.map(entry => entry.updateId);
|
|
4351
|
-
const published = publishInputSettlement(current, current.file.entries.filter(entry => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0);
|
|
4352
|
-
return {
|
|
5489
|
+
const removedUpdateIds = selected.map((entry) => entry.updateId);
|
|
5490
|
+
const published = publishInputSettlement(current, current.file.entries.filter((entry) => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0);
|
|
5491
|
+
return {
|
|
5492
|
+
removedUpdateIds,
|
|
5493
|
+
entryCount: published.file.entries.length,
|
|
5494
|
+
serializedBytes: published.serializedBytes,
|
|
5495
|
+
};
|
|
4353
5496
|
});
|
|
4354
5497
|
},
|
|
4355
5498
|
releaseInput(value) {
|
|
4356
5499
|
const receipt = normalizeInputReceipt(value);
|
|
4357
|
-
return runInputMutation("release", read => {
|
|
5500
|
+
return runInputMutation("release", (read) => {
|
|
4358
5501
|
const current = read();
|
|
4359
5502
|
const release = getReadyInputRelease(current, receipt);
|
|
4360
5503
|
if (release.unclaimed)
|
|
4361
|
-
return {
|
|
4362
|
-
|
|
5504
|
+
return {
|
|
5505
|
+
released: false,
|
|
5506
|
+
entryCount: current.file.entries.length,
|
|
5507
|
+
serializedBytes: current.serializedBytes,
|
|
5508
|
+
};
|
|
4363
5509
|
const published = publishReadyInputRelease(current, release.entry);
|
|
4364
|
-
return {
|
|
4365
|
-
|
|
5510
|
+
return {
|
|
5511
|
+
released: true,
|
|
5512
|
+
entryCount: published.file.entries.length,
|
|
5513
|
+
serializedBytes: published.serializedBytes,
|
|
5514
|
+
};
|
|
4366
5515
|
});
|
|
4367
5516
|
},
|
|
4368
5517
|
recoverReadyInput(value) {
|
|
4369
|
-
if (!isRecord(value) ||
|
|
5518
|
+
if (!isRecord(value) ||
|
|
5519
|
+
!hasOnlyKeys(value, ["receipt", "recoveryOwner"])) {
|
|
4370
5520
|
throw createJournalError("invalid", path, "received invalid ready-input recovery authority");
|
|
4371
5521
|
}
|
|
4372
5522
|
const receipt = normalizeInputReceipt(value.receipt, false);
|
|
4373
5523
|
const recoveryOwner = validateJournalQueueOwnerIdentity(value.recoveryOwner, path);
|
|
4374
5524
|
assertInputProcess(recoveryOwner);
|
|
4375
|
-
return runInputMutation("recover-ready", read => {
|
|
5525
|
+
return runInputMutation("recover-ready", (read) => {
|
|
4376
5526
|
const current = read();
|
|
4377
5527
|
const release = getReadyInputRelease(current, receipt, true);
|
|
4378
5528
|
if (release.unclaimed)
|
|
4379
|
-
return {
|
|
4380
|
-
|
|
5529
|
+
return {
|
|
5530
|
+
status: "unclaimed",
|
|
5531
|
+
entryCount: current.file.entries.length,
|
|
5532
|
+
serializedBytes: current.serializedBytes,
|
|
5533
|
+
};
|
|
4381
5534
|
let liveness;
|
|
4382
5535
|
try {
|
|
4383
|
-
liveness = getQueueProcessLiveness({
|
|
4384
|
-
|
|
5536
|
+
liveness = getQueueProcessLiveness({
|
|
5537
|
+
processId: receipt.owner.processId,
|
|
5538
|
+
processBirthId: receipt.owner.processBirthId,
|
|
5539
|
+
});
|
|
4385
5540
|
}
|
|
4386
5541
|
catch (error) {
|
|
4387
5542
|
throw createJournalError("io", path, "could not prove ready input owner liveness", error);
|
|
4388
5543
|
}
|
|
4389
5544
|
if (liveness !== "dead")
|
|
4390
|
-
return {
|
|
4391
|
-
|
|
5545
|
+
return {
|
|
5546
|
+
status: liveness === "alive"
|
|
5547
|
+
? "owner-alive"
|
|
5548
|
+
: "owner-unverifiable",
|
|
5549
|
+
entryCount: current.file.entries.length,
|
|
5550
|
+
serializedBytes: current.serializedBytes,
|
|
5551
|
+
};
|
|
4392
5552
|
const published = publishReadyInputRelease(current, release.entry);
|
|
4393
|
-
return {
|
|
4394
|
-
|
|
5553
|
+
return {
|
|
5554
|
+
status: "recovered",
|
|
5555
|
+
entryCount: published.file.entries.length,
|
|
5556
|
+
serializedBytes: published.serializedBytes,
|
|
5557
|
+
};
|
|
4395
5558
|
});
|
|
4396
5559
|
},
|
|
4397
5560
|
offerInputHandoff(value) {
|
|
4398
5561
|
const { receipt, recipientOwner, handoffToken } = normalizeInputHandoffOffer(value);
|
|
4399
|
-
return runInputMutation("offer", read => {
|
|
4400
|
-
const context =
|
|
4401
|
-
const current = read();
|
|
4402
|
-
const entry = ownedInput(current, receipt);
|
|
4403
|
-
const claim = entry.inputClaim;
|
|
4404
|
-
if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
|
|
4405
|
-
claim.recipientBindingKey !== context.recipientBindingKey) {
|
|
4406
|
-
throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
|
|
4407
|
-
}
|
|
5562
|
+
return runInputMutation("offer", (read) => {
|
|
5563
|
+
const { context, current, claim } = readReadyDonorInput(read(), receipt);
|
|
4408
5564
|
if (claim.handoff) {
|
|
4409
5565
|
if (!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
|
|
4410
5566
|
throw createJournalError("conflict", path, "input already has another handoff offer");
|
|
4411
5567
|
}
|
|
4412
|
-
return {
|
|
5568
|
+
return {
|
|
5569
|
+
source: createInputSourceReference(receipt.updateId),
|
|
4413
5570
|
handoff: cloneJournalQueueHandoff(claim.handoff),
|
|
4414
|
-
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
4415
|
-
|
|
5571
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5572
|
+
duplicate: true,
|
|
5573
|
+
entryCount: current.file.entries.length,
|
|
5574
|
+
serializedBytes: current.serializedBytes,
|
|
5575
|
+
};
|
|
4416
5576
|
}
|
|
4417
|
-
const handoffId = createTelegramInputHandoffId({
|
|
4418
|
-
|
|
4419
|
-
|
|
5577
|
+
const handoffId = createTelegramInputHandoffId({
|
|
5578
|
+
handoffToken,
|
|
5579
|
+
journalBindingKey: bindingKey,
|
|
5580
|
+
updateId: receipt.updateId,
|
|
5581
|
+
donorOwner: receipt.owner,
|
|
5582
|
+
recipientOwner,
|
|
5583
|
+
recipientBindingKey: claim.recipientBindingKey,
|
|
5584
|
+
});
|
|
4420
5585
|
const offeredAtMs = getNowMs();
|
|
4421
5586
|
if (!isSafeNonNegativeInteger(offeredAtMs)) {
|
|
4422
5587
|
throw createJournalError("invalid", path, "received an invalid input handoff offer timestamp");
|
|
4423
5588
|
}
|
|
4424
|
-
const handoff = {
|
|
4425
|
-
|
|
5589
|
+
const handoff = {
|
|
5590
|
+
handoffId,
|
|
5591
|
+
offeredAtMs,
|
|
5592
|
+
recipientOwner: { ...recipientOwner },
|
|
5593
|
+
};
|
|
4426
5594
|
const offeredClaim = { ...claim, handoff };
|
|
4427
|
-
const published = publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId
|
|
4428
|
-
|
|
5595
|
+
const published = publishInputMutation(current, current.file.entries.map((candidate) => candidate.updateId === receipt.updateId
|
|
5596
|
+
? { ...candidate, inputClaim: offeredClaim }
|
|
5597
|
+
: candidate), receipt.updateId, context);
|
|
5598
|
+
return {
|
|
5599
|
+
source: createInputSourceReference(receipt.updateId),
|
|
4429
5600
|
handoff: cloneJournalQueueHandoff(handoff),
|
|
4430
|
-
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
4431
|
-
|
|
5601
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5602
|
+
duplicate: false,
|
|
5603
|
+
entryCount: published.file.entries.length,
|
|
5604
|
+
serializedBytes: published.serializedBytes,
|
|
5605
|
+
};
|
|
4432
5606
|
});
|
|
4433
5607
|
},
|
|
4434
5608
|
acceptInputHandoff(value) {
|
|
4435
5609
|
const { source, recipientOwner, handoffId } = normalizeInputHandoffAccept(value);
|
|
4436
|
-
return runInputMutation("accept", read => {
|
|
5610
|
+
return runInputMutation("accept", (read) => {
|
|
4437
5611
|
const context = currentInputContext();
|
|
4438
5612
|
const current = read();
|
|
4439
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === source.updateId);
|
|
5613
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === source.updateId);
|
|
4440
5614
|
const claim = entry?.inputClaim;
|
|
4441
|
-
if (!entry ||
|
|
5615
|
+
if (!entry ||
|
|
5616
|
+
entry.state !== "pending" ||
|
|
5617
|
+
entry.preApprovalExcluded !== false ||
|
|
5618
|
+
!claim ||
|
|
4442
5619
|
!isDeepStrictEqual(context.owner, recipientOwner) ||
|
|
4443
5620
|
context.recipientBindingKey !== claim.recipientBindingKey) {
|
|
4444
5621
|
throw createJournalError("conflict", path, "input handoff recipient authority is unavailable or changed");
|
|
4445
5622
|
}
|
|
4446
|
-
if (claim.owner.handoffId === handoffId &&
|
|
4447
|
-
|
|
4448
|
-
|
|
5623
|
+
if (claim.owner.handoffId === handoffId &&
|
|
5624
|
+
inputOwnerMatchesIdentity(claim.owner, recipientOwner)) {
|
|
5625
|
+
return {
|
|
5626
|
+
handoffId,
|
|
5627
|
+
receipt: createInputReceipt(source.updateId, claim.owner),
|
|
5628
|
+
duplicate: true,
|
|
5629
|
+
entryCount: current.file.entries.length,
|
|
5630
|
+
serializedBytes: current.serializedBytes,
|
|
5631
|
+
};
|
|
4449
5632
|
}
|
|
4450
|
-
if (claim.phase !== "ready" ||
|
|
5633
|
+
if (claim.phase !== "ready" ||
|
|
5634
|
+
claim.handoff?.handoffId !== handoffId ||
|
|
4451
5635
|
!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
|
|
4452
5636
|
throw createJournalError("conflict", path, "cannot accept stale or unauthenticated input handoff");
|
|
4453
5637
|
}
|
|
@@ -4456,75 +5640,109 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4456
5640
|
throw createJournalError("invalid", path, "received an invalid input handoff acquisition timestamp");
|
|
4457
5641
|
}
|
|
4458
5642
|
const previousOwner = cloneJournalQueueOwner(claim.owner);
|
|
4459
|
-
const owner = {
|
|
4460
|
-
|
|
5643
|
+
const owner = {
|
|
5644
|
+
...recipientOwner,
|
|
5645
|
+
acquisitionId: randomUUID(),
|
|
5646
|
+
acquiredAtMs,
|
|
5647
|
+
handoffId,
|
|
5648
|
+
};
|
|
4461
5649
|
const acceptedClaim = structuredClone(claim);
|
|
4462
5650
|
acceptedClaim.owner = owner;
|
|
4463
5651
|
delete acceptedClaim.handoff;
|
|
4464
|
-
const publishedEntries = current.file.entries.map(candidate => candidate.updateId === source.updateId
|
|
5652
|
+
const publishedEntries = current.file.entries.map((candidate) => candidate.updateId === source.updateId
|
|
5653
|
+
? { ...candidate, inputClaim: acceptedClaim }
|
|
5654
|
+
: candidate);
|
|
4465
5655
|
const published = publishInputMutation(current, publishedEntries, source.updateId, context);
|
|
4466
|
-
return {
|
|
4467
|
-
|
|
5656
|
+
return {
|
|
5657
|
+
handoffId,
|
|
5658
|
+
previousOwner,
|
|
5659
|
+
receipt: createInputReceipt(source.updateId, owner),
|
|
5660
|
+
duplicate: false,
|
|
5661
|
+
entryCount: published.file.entries.length,
|
|
5662
|
+
serializedBytes: published.serializedBytes,
|
|
5663
|
+
};
|
|
4468
5664
|
});
|
|
4469
5665
|
},
|
|
4470
5666
|
cancelInputHandoff(value) {
|
|
4471
5667
|
const { receipt, recipientOwner, handoffId } = normalizeInputHandoffCancel(value);
|
|
4472
|
-
return runInputMutation("cancel", read => {
|
|
4473
|
-
const context =
|
|
4474
|
-
const current = read();
|
|
4475
|
-
const entry = ownedInput(current, receipt);
|
|
4476
|
-
const claim = entry.inputClaim;
|
|
4477
|
-
if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
|
|
4478
|
-
claim.recipientBindingKey !== context.recipientBindingKey) {
|
|
4479
|
-
throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
|
|
4480
|
-
}
|
|
5668
|
+
return runInputMutation("cancel", (read) => {
|
|
5669
|
+
const { context, current, claim } = readReadyDonorInput(read(), receipt);
|
|
4481
5670
|
if (!claim.handoff)
|
|
4482
|
-
return {
|
|
4483
|
-
|
|
5671
|
+
return {
|
|
5672
|
+
handoffId,
|
|
5673
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5674
|
+
cancelled: false,
|
|
5675
|
+
entryCount: current.file.entries.length,
|
|
5676
|
+
serializedBytes: current.serializedBytes,
|
|
5677
|
+
};
|
|
4484
5678
|
if (claim.handoff.handoffId !== handoffId ||
|
|
4485
5679
|
!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
|
|
4486
5680
|
throw createJournalError("conflict", path, "cannot cancel another input handoff offer");
|
|
4487
5681
|
}
|
|
4488
5682
|
const cancelledClaim = structuredClone(claim);
|
|
4489
5683
|
delete cancelledClaim.handoff;
|
|
4490
|
-
const publishedEntries = current.file.entries.map(candidate => candidate.updateId === receipt.updateId
|
|
5684
|
+
const publishedEntries = current.file.entries.map((candidate) => candidate.updateId === receipt.updateId
|
|
5685
|
+
? { ...candidate, inputClaim: cancelledClaim }
|
|
5686
|
+
: candidate);
|
|
4491
5687
|
const published = publishInputMutation(current, publishedEntries, receipt.updateId, context);
|
|
4492
|
-
return {
|
|
4493
|
-
|
|
5688
|
+
return {
|
|
5689
|
+
handoffId,
|
|
5690
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5691
|
+
cancelled: true,
|
|
5692
|
+
entryCount: published.file.entries.length,
|
|
5693
|
+
serializedBytes: published.serializedBytes,
|
|
5694
|
+
};
|
|
4494
5695
|
});
|
|
4495
5696
|
},
|
|
4496
5697
|
queueInputs(value) {
|
|
4497
5698
|
const { queueKind, receiptId, receipts } = normalizeInputQueue(value);
|
|
4498
|
-
return runInputMutation("queue", read => {
|
|
5699
|
+
return runInputMutation("queue", (read) => {
|
|
4499
5700
|
const context = currentInputContext();
|
|
4500
|
-
if (receipts.some(receipt => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
|
|
5701
|
+
if (receipts.some((receipt) => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
|
|
4501
5702
|
throw createJournalError("conflict", path, "input queue transition belongs to another session");
|
|
4502
5703
|
}
|
|
4503
5704
|
const current = read();
|
|
4504
|
-
const receiptsById = new Map(receipts.map(receipt => [receipt.updateId, receipt]));
|
|
4505
|
-
const requestedIds = receipts.map(receipt => receipt.updateId);
|
|
4506
|
-
const existingReceiptEntries = current.file.entries.filter(entry => entry.queueReceiptId === receiptId);
|
|
5705
|
+
const receiptsById = new Map(receipts.map((receipt) => [receipt.updateId, receipt]));
|
|
5706
|
+
const requestedIds = receipts.map((receipt) => receipt.updateId);
|
|
5707
|
+
const existingReceiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === receiptId);
|
|
4507
5708
|
if (existingReceiptEntries.length > 0) {
|
|
4508
5709
|
const queueOwner = existingReceiptEntries[0]?.queueOwner;
|
|
4509
|
-
if (existingReceiptEntries.length !== receipts.length ||
|
|
5710
|
+
if (existingReceiptEntries.length !== receipts.length ||
|
|
5711
|
+
!queueOwner ||
|
|
4510
5712
|
!inputOwnerMatchesIdentity(queueOwner, context.owner) ||
|
|
4511
|
-
existingReceiptEntries.some(entry => {
|
|
5713
|
+
existingReceiptEntries.some((entry) => {
|
|
4512
5714
|
const receipt = receiptsById.get(entry.updateId);
|
|
4513
|
-
return !receipt ||
|
|
4514
|
-
entry.
|
|
5715
|
+
return (!receipt ||
|
|
5716
|
+
entry.state !== "queued" ||
|
|
5717
|
+
entry.queueKind !== queueKind ||
|
|
5718
|
+
entry.queueHandoff !== undefined ||
|
|
5719
|
+
!entry.inputProvenance ||
|
|
4515
5720
|
!areTelegramUpdateJournalQueueOwnersEqual(entry.inputProvenance.owner, receipt.owner) ||
|
|
4516
|
-
entry.inputProvenance.recipientBindingKey !==
|
|
5721
|
+
entry.inputProvenance.recipientBindingKey !==
|
|
5722
|
+
context.recipientBindingKey);
|
|
4517
5723
|
})) {
|
|
4518
5724
|
throw createJournalError("conflict", path, "input queue transition conflicts with queued authority");
|
|
4519
5725
|
}
|
|
4520
|
-
return {
|
|
4521
|
-
|
|
4522
|
-
|
|
5726
|
+
return {
|
|
5727
|
+
queued: false,
|
|
5728
|
+
queueReceipt: {
|
|
5729
|
+
queueKind,
|
|
5730
|
+
receiptId,
|
|
5731
|
+
sourceUpdateIds: requestedIds,
|
|
5732
|
+
queueOwner: cloneJournalQueueOwner(queueOwner),
|
|
5733
|
+
},
|
|
5734
|
+
entryCount: current.file.entries.length,
|
|
5735
|
+
serializedBytes: current.serializedBytes,
|
|
5736
|
+
};
|
|
4523
5737
|
}
|
|
4524
5738
|
for (const receipt of receipts) {
|
|
4525
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
|
|
4526
|
-
if (!entry ||
|
|
4527
|
-
|
|
5739
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
|
|
5740
|
+
if (!entry ||
|
|
5741
|
+
entry.state !== "pending" ||
|
|
5742
|
+
entry.preApprovalExcluded !== false ||
|
|
5743
|
+
!entry.inputClaim ||
|
|
5744
|
+
entry.inputClaim.phase !== "running" ||
|
|
5745
|
+
entry.inputClaim.handoff ||
|
|
4528
5746
|
!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
|
|
4529
5747
|
entry.inputClaim.recipientBindingKey !== context.recipientBindingKey) {
|
|
4530
5748
|
throw createJournalError("conflict", path, "input queue transition lost exact running authority");
|
|
@@ -4534,76 +5752,122 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4534
5752
|
if (!isSafeNonNegativeInteger(acquiredAtMs)) {
|
|
4535
5753
|
throw createJournalError("invalid", path, "received an invalid input queue acquisition timestamp");
|
|
4536
5754
|
}
|
|
4537
|
-
const queueOwner = {
|
|
4538
|
-
|
|
4539
|
-
|
|
5755
|
+
const queueOwner = {
|
|
5756
|
+
...context.owner,
|
|
5757
|
+
acquisitionId: randomUUID(),
|
|
5758
|
+
acquiredAtMs,
|
|
5759
|
+
};
|
|
5760
|
+
const queuedEntries = current.file.entries.map((entry) => {
|
|
4540
5761
|
const receipt = receiptsById.get(entry.updateId);
|
|
4541
5762
|
if (!receipt)
|
|
4542
5763
|
return entry;
|
|
4543
5764
|
const claim = entry.inputClaim;
|
|
4544
|
-
return {
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
5765
|
+
return {
|
|
5766
|
+
updateId: entry.updateId,
|
|
5767
|
+
update: entry.update,
|
|
5768
|
+
admittedAtMs: entry.admittedAtMs,
|
|
5769
|
+
preApprovalExcluded: false,
|
|
5770
|
+
state: "queued",
|
|
5771
|
+
queueKind,
|
|
5772
|
+
queueReceiptId: receiptId,
|
|
5773
|
+
queueOwner: cloneJournalQueueOwner(queueOwner),
|
|
5774
|
+
inputProvenance: {
|
|
5775
|
+
owner: cloneJournalQueueOwner(claim.owner),
|
|
4548
5776
|
recipientBindingKey: claim.recipientBindingKey,
|
|
4549
5777
|
...(claim.executionUpdate
|
|
4550
|
-
? { executionUpdate: structuredClone(claim.executionUpdate) }
|
|
5778
|
+
? { executionUpdate: structuredClone(claim.executionUpdate) }
|
|
5779
|
+
: {}),
|
|
5780
|
+
},
|
|
5781
|
+
};
|
|
4551
5782
|
});
|
|
4552
5783
|
const published = publishInputMutation(current, queuedEntries, requestedIds, context);
|
|
4553
|
-
return {
|
|
4554
|
-
|
|
4555
|
-
|
|
5784
|
+
return {
|
|
5785
|
+
queued: true,
|
|
5786
|
+
queueReceipt: {
|
|
5787
|
+
queueKind,
|
|
5788
|
+
receiptId,
|
|
5789
|
+
sourceUpdateIds: requestedIds,
|
|
5790
|
+
queueOwner: cloneJournalQueueOwner(queueOwner),
|
|
5791
|
+
},
|
|
5792
|
+
entryCount: published.file.entries.length,
|
|
5793
|
+
serializedBytes: published.serializedBytes,
|
|
5794
|
+
};
|
|
4556
5795
|
});
|
|
4557
5796
|
},
|
|
4558
5797
|
acquireInput(value) {
|
|
4559
5798
|
const updateId = value.updateId;
|
|
4560
5799
|
const recipientBindingKey = value.recipientBindingKey;
|
|
4561
|
-
const executionUpdate = value.executionUpdate === undefined
|
|
5800
|
+
const executionUpdate = value.executionUpdate === undefined
|
|
5801
|
+
? undefined
|
|
5802
|
+
: normalizeIncomingJournaledUpdate(value.executionUpdate, path);
|
|
4562
5803
|
if (!isSafeNonNegativeInteger(updateId) ||
|
|
4563
5804
|
!isBoundedString(recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
|
|
4564
|
-
!recipientBindingKey.trim() ||
|
|
5805
|
+
!recipientBindingKey.trim() ||
|
|
5806
|
+
(executionUpdate && executionUpdate.update_id !== updateId)) {
|
|
4565
5807
|
throw createJournalError("invalid", path, "received invalid input acquisition identity");
|
|
4566
5808
|
}
|
|
4567
|
-
return runInputMutation("acquire", read => {
|
|
5809
|
+
return runInputMutation("acquire", (read) => {
|
|
4568
5810
|
const context = currentInputContext();
|
|
4569
5811
|
const identity = context.owner;
|
|
4570
5812
|
if (recipientBindingKey !== context.recipientBindingKey) {
|
|
4571
5813
|
throw createJournalError("conflict", path, "input acquisition targets another execution binding");
|
|
4572
5814
|
}
|
|
4573
5815
|
const current = read();
|
|
4574
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === updateId);
|
|
4575
|
-
if (!entry ||
|
|
5816
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === updateId);
|
|
5817
|
+
if (!entry ||
|
|
5818
|
+
entry.state !== "pending" ||
|
|
5819
|
+
entry.preApprovalExcluded !== false) {
|
|
4576
5820
|
throw createJournalError("conflict", path, "input is not eligible for acquisition");
|
|
4577
5821
|
}
|
|
4578
5822
|
const projected = executionUpdate ?? entry.update;
|
|
4579
5823
|
const existing = entry.inputClaim;
|
|
4580
5824
|
if (existing) {
|
|
4581
5825
|
if (!isTelegramUpdateJournalQueueOwnerProcess(existing.owner, identity) ||
|
|
4582
|
-
existing.owner.sessionGeneration !== identity.sessionGeneration ||
|
|
5826
|
+
existing.owner.sessionGeneration !== identity.sessionGeneration ||
|
|
5827
|
+
existing.recipientBindingKey !== recipientBindingKey ||
|
|
4583
5828
|
!isDeepStrictEqual(existing.executionUpdate ?? entry.update, projected)) {
|
|
4584
5829
|
throw createJournalError("conflict", path, "input already has another owner or execution projection");
|
|
4585
5830
|
}
|
|
4586
|
-
return {
|
|
5831
|
+
return {
|
|
5832
|
+
acquired: false,
|
|
5833
|
+
receipt: createInputReceipt(updateId, existing.owner),
|
|
5834
|
+
};
|
|
4587
5835
|
}
|
|
4588
|
-
const claim = {
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
5836
|
+
const claim = {
|
|
5837
|
+
phase: "ready",
|
|
5838
|
+
recipientBindingKey,
|
|
5839
|
+
owner: {
|
|
5840
|
+
...identity,
|
|
5841
|
+
acquisitionId: randomUUID(),
|
|
5842
|
+
acquiredAtMs: getNowMs(),
|
|
5843
|
+
},
|
|
5844
|
+
...(!isDeepStrictEqual(projected, entry.update)
|
|
5845
|
+
? { executionUpdate: projected }
|
|
5846
|
+
: {}),
|
|
5847
|
+
};
|
|
5848
|
+
const claimedEntries = current.file.entries.map((candidate) => candidate.updateId === updateId
|
|
5849
|
+
? { ...candidate, inputClaim: claim }
|
|
5850
|
+
: candidate);
|
|
5851
|
+
const reservedEntry = {
|
|
5852
|
+
...entry,
|
|
5853
|
+
inputClaim: createInputHeadroomClaim(entry, "ready"),
|
|
5854
|
+
};
|
|
4593
5855
|
const reservedEntries = replaceInputHeadroomEntry(current.file, updateId, reservedEntry);
|
|
4594
5856
|
const actualBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: claimedEntries }));
|
|
4595
|
-
const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) +
|
|
4596
|
-
TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
|
|
5857
|
+
const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) + TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
|
|
4597
5858
|
if (actualBytes > reservedBytes) {
|
|
4598
5859
|
throw createJournalError("capacity", path, "execution projection exceeds reserved input headroom");
|
|
4599
5860
|
}
|
|
4600
5861
|
publishInputMutation(current, claimedEntries, updateId, context);
|
|
4601
|
-
return {
|
|
5862
|
+
return {
|
|
5863
|
+
acquired: true,
|
|
5864
|
+
receipt: createInputReceipt(updateId, claim.owner),
|
|
5865
|
+
};
|
|
4602
5866
|
});
|
|
4603
5867
|
},
|
|
4604
5868
|
startInput(value) {
|
|
4605
5869
|
const receipt = normalizeInputReceipt(value);
|
|
4606
|
-
return runInputMutation("start", read => {
|
|
5870
|
+
return runInputMutation("start", (read) => {
|
|
4607
5871
|
const context = currentInputContext();
|
|
4608
5872
|
const current = read();
|
|
4609
5873
|
const entry = ownedInput(current, receipt);
|
|
@@ -4616,24 +5880,41 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4616
5880
|
return { started: false };
|
|
4617
5881
|
if (claim.handoff)
|
|
4618
5882
|
throw createJournalError("conflict", path, "input handoff freezes donor execution");
|
|
4619
|
-
publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId
|
|
4620
|
-
? {
|
|
4621
|
-
|
|
5883
|
+
publishInputMutation(current, current.file.entries.map((candidate) => candidate.updateId === receipt.updateId
|
|
5884
|
+
? {
|
|
5885
|
+
...candidate,
|
|
5886
|
+
inputClaim: { ...claim, phase: "running" },
|
|
5887
|
+
}
|
|
5888
|
+
: candidate), receipt.updateId, context);
|
|
5889
|
+
return {
|
|
5890
|
+
started: true,
|
|
5891
|
+
update: structuredClone(claim.executionUpdate ?? entry.update),
|
|
5892
|
+
};
|
|
4622
5893
|
});
|
|
4623
5894
|
},
|
|
4624
5895
|
completeInput(value) {
|
|
4625
5896
|
const receipt = normalizeInputReceipt(value);
|
|
4626
|
-
return runInputMutation("complete", read => {
|
|
5897
|
+
return runInputMutation("complete", (read) => {
|
|
4627
5898
|
const current = read();
|
|
4628
|
-
const exists = current.file.entries.some(entry => entry.updateId === receipt.updateId);
|
|
4629
|
-
if (!exists &&
|
|
4630
|
-
|
|
5899
|
+
const exists = current.file.entries.some((entry) => entry.updateId === receipt.updateId);
|
|
5900
|
+
if (!exists &&
|
|
5901
|
+
current.exists &&
|
|
5902
|
+
receipt.updateId <= (current.file.acceptedThroughUpdateId ?? -1)) {
|
|
5903
|
+
return {
|
|
5904
|
+
removedUpdateIds: [],
|
|
5905
|
+
entryCount: current.file.entries.length,
|
|
5906
|
+
serializedBytes: current.serializedBytes,
|
|
5907
|
+
};
|
|
4631
5908
|
}
|
|
4632
5909
|
const entry = ownedInput(current, receipt);
|
|
4633
5910
|
if (entry.inputClaim.phase !== "running")
|
|
4634
5911
|
throw createJournalError("conflict", path, "input has not started");
|
|
4635
|
-
const published = publishInputSettlement(current, current.file.entries.filter(candidate => candidate.updateId !== receipt.updateId), true);
|
|
4636
|
-
return {
|
|
5912
|
+
const published = publishInputSettlement(current, current.file.entries.filter((candidate) => candidate.updateId !== receipt.updateId), true);
|
|
5913
|
+
return {
|
|
5914
|
+
removedUpdateIds: [receipt.updateId],
|
|
5915
|
+
entryCount: published.file.entries.length,
|
|
5916
|
+
serializedBytes: published.serializedBytes,
|
|
5917
|
+
};
|
|
4637
5918
|
});
|
|
4638
5919
|
},
|
|
4639
5920
|
};
|