@llblab/pi-kit 0.29.1 → 0.30.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/BACKLOG.md +3 -1
- package/CHANGELOG.md +10 -0
- package/README.md +4 -4
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-state-flow/README.md +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +2 -2
- package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +17 -8
- package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +7 -3
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +2 -1
- package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +3 -2
- package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
- package/node_modules/@llblab/pi-state-flow/docs/architecture.md +3 -3
- package/node_modules/@llblab/pi-state-flow/docs/usage.md +3 -2
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +17 -8
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +6 -3
- package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +4 -2
- package/node_modules/@llblab/pi-state-flow/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/AGENTS.md +13 -10
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +23 -0
- package/node_modules/@llblab/pi-telegram/README.md +10 -11
- package/node_modules/@llblab/pi-telegram/api/commands.ts +3 -0
- package/node_modules/@llblab/pi-telegram/dist/api/commands.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.d.ts +1 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/activity-verbosity.js +15 -42
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.d.ts +9 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/activity.js +39 -10
- package/node_modules/@llblab/pi-telegram/dist/lib/agent-messages.js +7 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.d.ts +43 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/bindings.js +164 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.d.ts +12 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-api.js +181 -77
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.d.ts +111 -31
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-follower.js +1324 -359
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.d.ts +41 -91
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +969 -637
- package/node_modules/@llblab/pi-telegram/dist/lib/bus-transport.js +16 -9
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.d.ts +215 -18
- package/node_modules/@llblab/pi-telegram/dist/lib/bus.js +1048 -327
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/channel-posts.js +434 -261
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.d.ts +26 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/command-templates.js +92 -176
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.d.ts +168 -82
- package/node_modules/@llblab/pi-telegram/dist/lib/commands.js +1409 -291
- package/node_modules/@llblab/pi-telegram/dist/lib/config.d.ts +1 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/config.js +34 -76
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.d.ts +2 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/delivery.js +16 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/extension.js +642 -170
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-app-worker.mjs +19 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.d.ts +0 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/generative-apps.js +61 -49
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/inbound.js +35 -59
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.d.ts +40 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/journal.js +2102 -760
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.d.ts +2 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/lifecycle.js +75 -58
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.d.ts +34 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/locks.js +318 -119
- package/node_modules/@llblab/pi-telegram/dist/lib/logging.js +50 -14
- package/node_modules/@llblab/pi-telegram/dist/lib/media.d.ts +22 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/media.js +46 -28
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.d.ts +19 -5
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-model.js +53 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.d.ts +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-queue.js +45 -63
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.d.ts +5 -4
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-settings.js +114 -78
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-status.js +8 -25
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.d.ts +6 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/menu-thinking.js +14 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.d.ts +8 -61
- package/node_modules/@llblab/pi-telegram/dist/lib/menu.js +92 -164
- package/node_modules/@llblab/pi-telegram/dist/lib/model.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/model.js +2 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.d.ts +6 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-attachments.js +45 -38
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-buttons.js +22 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.d.ts +0 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-markup.js +51 -64
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound-voice.js +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.d.ts +6 -6
- package/node_modules/@llblab/pi-telegram/dist/lib/outbound.js +61 -51
- package/node_modules/@llblab/pi-telegram/dist/lib/ownership.js +6 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.d.ts +5 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/paths.js +37 -21
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.d.ts +2 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/pi.js +8 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.d.ts +23 -37
- package/node_modules/@llblab/pi-telegram/dist/lib/polling.js +71 -52
- package/node_modules/@llblab/pi-telegram/dist/lib/preview.js +39 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.d.ts +24 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/process-identity.js +133 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/prompts.js +2 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.d.ts +16 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/queue.js +261 -116
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.d.ts +2 -83
- package/node_modules/@llblab/pi-telegram/dist/lib/recovery.js +20 -274
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.d.ts +16 -24
- package/node_modules/@llblab/pi-telegram/dist/lib/replies.js +99 -116
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.d.ts +121 -7
- package/node_modules/@llblab/pi-telegram/dist/lib/routing.js +4750 -1161
- package/node_modules/@llblab/pi-telegram/dist/lib/runtime.d.ts +1 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.d.ts +4 -29
- package/node_modules/@llblab/pi-telegram/dist/lib/sections.js +7 -20
- package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +4 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/status.js +86 -57
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.d.ts +6 -3
- package/node_modules/@llblab/pi-telegram/dist/lib/sync.js +112 -45
- package/node_modules/@llblab/pi-telegram/dist/lib/target.d.ts +4 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/target.js +18 -0
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.d.ts +18 -11
- package/node_modules/@llblab/pi-telegram/dist/lib/telegram-api.js +165 -49
- package/node_modules/@llblab/pi-telegram/dist/lib/text-groups.js +4 -39
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.d.ts +6 -27
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-cleanup-manager.js +369 -186
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.d.ts +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-display.js +58 -38
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.d.ts +28 -8
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-naming.js +175 -66
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.d.ts +42 -16
- package/node_modules/@llblab/pi-telegram/dist/lib/thread-reconciler.js +90 -50
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.d.ts +88 -24
- package/node_modules/@llblab/pi-telegram/dist/lib/threads.js +2276 -842
- package/node_modules/@llblab/pi-telegram/dist/lib/time-injection.js +1 -2
- package/node_modules/@llblab/pi-telegram/dist/lib/turns.js +1 -19
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.d.ts +178 -82
- package/node_modules/@llblab/pi-telegram/dist/lib/updates.js +2438 -645
- package/node_modules/@llblab/pi-telegram/dist/lib/voice.d.ts +7 -22
- package/node_modules/@llblab/pi-telegram/dist/lib/voice.js +7 -22
- package/node_modules/@llblab/pi-telegram/dist/lib/wire.js +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.d.ts +3 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-admission.js +425 -211
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-identity.js +8 -13
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.d.ts +1 -1
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-retirement.js +328 -140
- package/node_modules/@llblab/pi-telegram/dist/lib/workspace-slots.js +8 -4
- package/node_modules/@llblab/pi-telegram/dist/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +43 -35
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +15 -3
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +29 -1
- package/node_modules/@llblab/pi-telegram/docs/ui-style.md +25 -8
- package/node_modules/@llblab/pi-telegram/lib/activity-verbosity.ts +59 -82
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +146 -102
- package/node_modules/@llblab/pi-telegram/lib/agent-messages.ts +12 -5
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +390 -82
- package/node_modules/@llblab/pi-telegram/lib/bus-api.ts +262 -99
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +2440 -601
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +2209 -1290
- package/node_modules/@llblab/pi-telegram/lib/bus-transport.ts +46 -12
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +1673 -444
- package/node_modules/@llblab/pi-telegram/lib/channel-posts.ts +848 -327
- package/node_modules/@llblab/pi-telegram/lib/command-templates.ts +152 -265
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +2398 -555
- package/node_modules/@llblab/pi-telegram/lib/config.ts +177 -170
- package/node_modules/@llblab/pi-telegram/lib/delivery.ts +41 -29
- package/node_modules/@llblab/pi-telegram/lib/extension.ts +1209 -431
- package/node_modules/@llblab/pi-telegram/lib/generative-app-worker.mjs +19 -7
- package/node_modules/@llblab/pi-telegram/lib/generative-apps.ts +324 -211
- package/node_modules/@llblab/pi-telegram/lib/inbound.ts +95 -97
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +5329 -1712
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +128 -78
- package/node_modules/@llblab/pi-telegram/lib/locks.ts +703 -212
- package/node_modules/@llblab/pi-telegram/lib/logging.ts +147 -58
- package/node_modules/@llblab/pi-telegram/lib/media.ts +82 -43
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +85 -18
- package/node_modules/@llblab/pi-telegram/lib/menu-queue.ts +88 -129
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +260 -155
- package/node_modules/@llblab/pi-telegram/lib/menu-status.ts +20 -60
- package/node_modules/@llblab/pi-telegram/lib/menu-thinking.ts +41 -56
- package/node_modules/@llblab/pi-telegram/lib/menu.ts +112 -285
- package/node_modules/@llblab/pi-telegram/lib/model.ts +8 -22
- package/node_modules/@llblab/pi-telegram/lib/outbound-attachments.ts +156 -125
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +76 -49
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +96 -94
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +33 -27
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +125 -125
- package/node_modules/@llblab/pi-telegram/lib/ownership.ts +19 -10
- package/node_modules/@llblab/pi-telegram/lib/paths.ts +148 -49
- package/node_modules/@llblab/pi-telegram/lib/pi.ts +35 -15
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +165 -149
- package/node_modules/@llblab/pi-telegram/lib/preview.ts +93 -40
- package/node_modules/@llblab/pi-telegram/lib/process-identity.ts +164 -0
- package/node_modules/@llblab/pi-telegram/lib/prompts.ts +4 -11
- package/node_modules/@llblab/pi-telegram/lib/queue.ts +523 -289
- package/node_modules/@llblab/pi-telegram/lib/recovery.ts +50 -454
- package/node_modules/@llblab/pi-telegram/lib/rendering.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +248 -209
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +8527 -2149
- package/node_modules/@llblab/pi-telegram/lib/runtime.ts +1 -12
- package/node_modules/@llblab/pi-telegram/lib/sections.ts +24 -89
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +1 -3
- package/node_modules/@llblab/pi-telegram/lib/skills.ts +9 -4
- package/node_modules/@llblab/pi-telegram/lib/status.ts +215 -110
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +488 -359
- package/node_modules/@llblab/pi-telegram/lib/target.ts +22 -3
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +322 -122
- package/node_modules/@llblab/pi-telegram/lib/text-groups.ts +5 -39
- package/node_modules/@llblab/pi-telegram/lib/thread-cleanup-manager.ts +833 -331
- package/node_modules/@llblab/pi-telegram/lib/thread-display.ts +213 -97
- package/node_modules/@llblab/pi-telegram/lib/thread-naming.ts +232 -87
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +155 -72
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +5329 -1817
- package/node_modules/@llblab/pi-telegram/lib/time-injection.ts +1 -4
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +15 -28
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +5194 -1600
- package/node_modules/@llblab/pi-telegram/lib/voice.ts +7 -35
- package/node_modules/@llblab/pi-telegram/lib/wire.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/workspace-admission.ts +996 -416
- package/node_modules/@llblab/pi-telegram/lib/workspace-identity.ts +14 -17
- package/node_modules/@llblab/pi-telegram/lib/workspace-retirement.ts +878 -376
- package/node_modules/@llblab/pi-telegram/lib/workspace-slots.ts +22 -13
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/@llblab/pi-telegram/scripts/check-downgrade.mjs +0 -472
- package/node_modules/@llblab/pi-telegram/scripts/measure-workspace.mjs +0 -108
|
@@ -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,52 +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
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function inspectRoutingInputExpiry(file, journalBindingKey, updateId) {
|
|
347
|
+
const disposition = file.operatorDispositions?.find((value) => value.updateId === updateId &&
|
|
348
|
+
"dispositionKind" in value &&
|
|
349
|
+
value.dispositionKind === "legacy-custody" &&
|
|
350
|
+
value.action === "discard" &&
|
|
351
|
+
value.failureId === `routing-expiry:${value.evidenceSha256}`);
|
|
352
|
+
if (!disposition ||
|
|
353
|
+
!("operatorAuthorityId" in disposition) ||
|
|
354
|
+
file.entries.some((entry) => entry.updateId === updateId))
|
|
355
|
+
return undefined;
|
|
356
|
+
return {
|
|
357
|
+
journalBindingKey,
|
|
358
|
+
updateId,
|
|
359
|
+
operatorAuthorityId: disposition.operatorAuthorityId,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function inspectRoutingGroupExpiry(file, journalBindingKey, 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])))
|
|
368
|
+
return undefined;
|
|
369
|
+
if (file.entries.some((entry) => updateIds.includes(entry.updateId)))
|
|
370
|
+
return undefined;
|
|
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))
|
|
376
|
+
return undefined;
|
|
377
|
+
// Some members may have been acknowledged earlier. Whole donor absence plus one exact expiry ends this known cohort,
|
|
378
|
+
// not recipient custody; fresh Thread protection still independently gates deletion.
|
|
379
|
+
return updateIds.map((updateId) => ({
|
|
380
|
+
journalBindingKey,
|
|
381
|
+
updateId,
|
|
382
|
+
operatorAuthorityId: expired[0].operatorAuthorityId,
|
|
383
|
+
}));
|
|
287
384
|
}
|
|
288
385
|
function createJournalError(code, path, detail, cause) {
|
|
289
386
|
return new TelegramUpdateJournalError(code, path, `Telegram update journal ${detail}: ${path}`, cause === undefined ? undefined : { cause });
|
|
@@ -418,11 +515,11 @@ function createTelegramUpdateQueueHandoffId(input) {
|
|
|
418
515
|
.slice(0, 32)}`;
|
|
419
516
|
}
|
|
420
517
|
function isTelegramInputHandoffId(value) {
|
|
421
|
-
return isBoundedString(value, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH) &&
|
|
422
|
-
/^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));
|
|
423
519
|
}
|
|
424
520
|
function createTelegramInputHandoffId(input) {
|
|
425
|
-
return `input-handoff-${createHash("sha256")
|
|
521
|
+
return `input-handoff-${createHash("sha256")
|
|
522
|
+
.update(JSON.stringify({
|
|
426
523
|
version: 1,
|
|
427
524
|
token: input.handoffToken,
|
|
428
525
|
source: input.journalBindingKey,
|
|
@@ -430,7 +527,9 @@ function createTelegramInputHandoffId(input) {
|
|
|
430
527
|
donorOwner: input.donorOwner,
|
|
431
528
|
recipientOwner: input.recipientOwner,
|
|
432
529
|
recipientBindingKey: input.recipientBindingKey,
|
|
433
|
-
}))
|
|
530
|
+
}))
|
|
531
|
+
.digest("hex")
|
|
532
|
+
.slice(0, 32)}`;
|
|
434
533
|
}
|
|
435
534
|
function validateJournalHandoff(value, path, kind) {
|
|
436
535
|
if (!isRecord(value) ||
|
|
@@ -480,38 +579,72 @@ function createTelegramUpdateTerminalFailureId(input) {
|
|
|
480
579
|
}
|
|
481
580
|
function validateJournalOperatorDisposition(value, path) {
|
|
482
581
|
if (isRecord(value) && value.dispositionKind === "legacy-custody") {
|
|
483
|
-
if (!hasOnlyKeys(value, [
|
|
484
|
-
"
|
|
582
|
+
if (!hasOnlyKeys(value, [
|
|
583
|
+
"dispositionKind",
|
|
584
|
+
"failureId",
|
|
585
|
+
"updateId",
|
|
586
|
+
"action",
|
|
587
|
+
"committedAtMs",
|
|
588
|
+
"evidenceSha256",
|
|
589
|
+
"operatorAuthorityId",
|
|
590
|
+
"authorizedAtMs",
|
|
591
|
+
]) ||
|
|
485
592
|
!isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
486
593
|
!isSafeNonNegativeInteger(value.updateId) ||
|
|
487
594
|
(value.action !== "requeue-v3" && value.action !== "discard") ||
|
|
488
595
|
!isSafeNonNegativeInteger(value.committedAtMs) ||
|
|
489
|
-
typeof value.evidenceSha256 !== "string" ||
|
|
596
|
+
typeof value.evidenceSha256 !== "string" ||
|
|
597
|
+
!/^[a-f0-9]{64}$/u.test(value.evidenceSha256) ||
|
|
490
598
|
!isBoundedString(value.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
491
|
-
!isSafeNonNegativeInteger(value.authorizedAtMs) ||
|
|
599
|
+
!isSafeNonNegativeInteger(value.authorizedAtMs) ||
|
|
600
|
+
value.committedAtMs < value.authorizedAtMs)
|
|
492
601
|
throw createJournalError("invalid", path, "contains invalid legacy custody disposition metadata");
|
|
493
|
-
return {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
+
};
|
|
497
612
|
}
|
|
498
|
-
if (!isRecord(value) ||
|
|
499
|
-
|
|
613
|
+
if (!isRecord(value) ||
|
|
614
|
+
!hasOnlyKeys(value, [
|
|
615
|
+
"failureId",
|
|
616
|
+
"updateId",
|
|
617
|
+
"action",
|
|
618
|
+
"committedAtMs",
|
|
619
|
+
"attemptCount",
|
|
620
|
+
"failureClass",
|
|
621
|
+
"terminalAtMs",
|
|
622
|
+
"terminalReason",
|
|
623
|
+
]) ||
|
|
500
624
|
!isBoundedString(value.failureId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
501
625
|
!isSafeNonNegativeInteger(value.updateId) ||
|
|
502
626
|
(value.action !== "retry" && value.action !== "discard") ||
|
|
503
|
-
!isSafeNonNegativeInteger(value.committedAtMs) ||
|
|
627
|
+
!isSafeNonNegativeInteger(value.committedAtMs) ||
|
|
628
|
+
!isSafePositiveInteger(value.attemptCount) ||
|
|
504
629
|
!isBoundedString(value.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
|
|
505
|
-
!isSafeNonNegativeInteger(value.terminalAtMs) ||
|
|
630
|
+
!isSafeNonNegativeInteger(value.terminalAtMs) ||
|
|
631
|
+
value.committedAtMs < value.terminalAtMs ||
|
|
506
632
|
!isBoundedString(value.terminalReason, TELEGRAM_UPDATE_JOURNAL_TERMINAL_REASON_MAX_LENGTH))
|
|
507
633
|
throw createJournalError("invalid", path, "contains invalid operator disposition metadata");
|
|
508
|
-
return {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
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
|
+
};
|
|
512
644
|
}
|
|
513
645
|
function journalRequiresExclusion(version) {
|
|
514
|
-
return version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
|
|
646
|
+
return (version === TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION ||
|
|
647
|
+
journalHasCustodyFields(version));
|
|
515
648
|
}
|
|
516
649
|
function journalHasCustodyFields(version) {
|
|
517
650
|
return version === TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION;
|
|
@@ -522,21 +655,33 @@ export function isTelegramUpdateJournalLegacyFamilyVersion(version) {
|
|
|
522
655
|
}
|
|
523
656
|
function validateJournalInputClaim(value, path, updateId) {
|
|
524
657
|
if (!isRecord(value) ||
|
|
525
|
-
!hasOnlyKeys(value, [
|
|
658
|
+
!hasOnlyKeys(value, [
|
|
659
|
+
"phase",
|
|
660
|
+
"owner",
|
|
661
|
+
"recipientBindingKey",
|
|
662
|
+
"handoff",
|
|
663
|
+
"executionUpdate",
|
|
664
|
+
]) ||
|
|
526
665
|
(value.phase !== "ready" && value.phase !== "running") ||
|
|
527
666
|
!isBoundedString(value.recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
|
|
528
667
|
!value.recipientBindingKey.trim()) {
|
|
529
668
|
throw createJournalError("invalid", path, "contains invalid input claim metadata");
|
|
530
669
|
}
|
|
531
670
|
const owner = validateJournalQueueOwner(value.owner, path);
|
|
532
|
-
const handoff = value.handoff === undefined
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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)))) {
|
|
536
680
|
throw createJournalError("invalid", path, "contains conflicting input handoff metadata");
|
|
537
681
|
}
|
|
538
682
|
const executionUpdate = value.executionUpdate === undefined
|
|
539
|
-
? undefined
|
|
683
|
+
? undefined
|
|
684
|
+
: validateJournaledUpdate(value.executionUpdate, path);
|
|
540
685
|
if (executionUpdate && executionUpdate.update_id !== updateId) {
|
|
541
686
|
throw createJournalError("invalid", path, "contains an input claim/update id mismatch");
|
|
542
687
|
}
|
|
@@ -556,20 +701,26 @@ function validateJournalInputProvenance(value, path, updateId) {
|
|
|
556
701
|
throw createJournalError("invalid", path, "contains invalid input queue provenance");
|
|
557
702
|
}
|
|
558
703
|
const owner = validateJournalQueueOwner(value.owner, path);
|
|
559
|
-
if (owner.handoffId !== undefined &&
|
|
704
|
+
if (owner.handoffId !== undefined &&
|
|
705
|
+
!isTelegramInputHandoffId(owner.handoffId)) {
|
|
560
706
|
throw createJournalError("invalid", path, "contains invalid input queue provenance");
|
|
561
707
|
}
|
|
562
708
|
const executionUpdate = value.executionUpdate === undefined
|
|
563
|
-
? undefined
|
|
709
|
+
? undefined
|
|
710
|
+
: validateJournaledUpdate(value.executionUpdate, path);
|
|
564
711
|
if (executionUpdate && executionUpdate.update_id !== updateId) {
|
|
565
712
|
throw createJournalError("invalid", path, "contains an input provenance/update id mismatch");
|
|
566
713
|
}
|
|
567
|
-
return {
|
|
568
|
-
|
|
714
|
+
return {
|
|
715
|
+
owner,
|
|
716
|
+
recipientBindingKey: value.recipientBindingKey,
|
|
717
|
+
...(executionUpdate ? { executionUpdate } : {}),
|
|
718
|
+
};
|
|
569
719
|
}
|
|
570
720
|
function validateJournalEntry(value, path, version) {
|
|
571
721
|
if (journalRequiresExclusion(version) &&
|
|
572
|
-
(!isRecord(value) ||
|
|
722
|
+
(!isRecord(value) ||
|
|
723
|
+
typeof value.preApprovalExcluded !== "boolean" ||
|
|
573
724
|
(value.preApprovalExcluded && value.state === "queued"))) {
|
|
574
725
|
throw createJournalError("pairing-evidence", path, "contains missing, malformed, or queued exclusion evidence");
|
|
575
726
|
}
|
|
@@ -578,7 +729,9 @@ function validateJournalEntry(value, path, version) {
|
|
|
578
729
|
"updateId",
|
|
579
730
|
"update",
|
|
580
731
|
"admittedAtMs",
|
|
581
|
-
...(isTelegramUpdateJournalLegacyFamilyVersion(version)
|
|
732
|
+
...(isTelegramUpdateJournalLegacyFamilyVersion(version)
|
|
733
|
+
? ["routingInput"]
|
|
734
|
+
: []),
|
|
582
735
|
"state",
|
|
583
736
|
"queueKind",
|
|
584
737
|
"queueReceiptId",
|
|
@@ -591,7 +744,8 @@ function validateJournalEntry(value, path, version) {
|
|
|
591
744
|
"terminalFailureId",
|
|
592
745
|
...(journalRequiresExclusion(version) ? ["preApprovalExcluded"] : []),
|
|
593
746
|
...(journalHasCustodyFields(version)
|
|
594
|
-
? ["inputClaim", "inputProvenance"]
|
|
747
|
+
? ["inputClaim", "inputProvenance"]
|
|
748
|
+
: []),
|
|
595
749
|
]) ||
|
|
596
750
|
!isSafeNonNegativeInteger(value.updateId) ||
|
|
597
751
|
!isSafeNonNegativeInteger(value.admittedAtMs) ||
|
|
@@ -608,16 +762,39 @@ function validateJournalEntry(value, path, version) {
|
|
|
608
762
|
let routingInput;
|
|
609
763
|
if (value.routingInput !== undefined) {
|
|
610
764
|
const routing = value.routingInput;
|
|
611
|
-
if (!isRecord(routing) ||
|
|
612
|
-
!
|
|
613
|
-
|
|
614
|
-
|
|
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 ||
|
|
615
781
|
(routing.phase !== "waiting" && routing.phase !== "selected") ||
|
|
616
782
|
(routing.phase === "waiting" && value.state === "queued")) {
|
|
617
783
|
throw createJournalError("invalid", path, "contains invalid routing input lifetime");
|
|
618
784
|
}
|
|
619
|
-
routingInput = {
|
|
620
|
-
|
|
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
|
+
};
|
|
621
798
|
}
|
|
622
799
|
const queueKind = value.queueKind;
|
|
623
800
|
const queueReceiptId = value.queueReceiptId;
|
|
@@ -654,11 +831,14 @@ function validateJournalEntry(value, path, version) {
|
|
|
654
831
|
throw createJournalError("pairing-evidence", path, "contains claimed exclusion evidence");
|
|
655
832
|
}
|
|
656
833
|
const inputClaim = value.inputClaim === undefined
|
|
657
|
-
? undefined
|
|
834
|
+
? undefined
|
|
835
|
+
: validateJournalInputClaim(value.inputClaim, path, value.updateId);
|
|
658
836
|
const inputProvenance = value.inputProvenance === undefined
|
|
659
|
-
? undefined
|
|
660
|
-
|
|
661
|
-
|
|
837
|
+
? undefined
|
|
838
|
+
: validateJournalInputProvenance(value.inputProvenance, path, value.updateId);
|
|
839
|
+
if ((inputClaim &&
|
|
840
|
+
(value.state === "queued" ||
|
|
841
|
+
(inputClaim.phase === "running" && value.state !== "pending"))) ||
|
|
662
842
|
(inputProvenance && (value.state !== "queued" || inputClaim))) {
|
|
663
843
|
throw createJournalError("invalid", path, "contains conflicting input claim state");
|
|
664
844
|
}
|
|
@@ -690,15 +870,14 @@ function validateJournalEntry(value, path, version) {
|
|
|
690
870
|
updateId: value.updateId,
|
|
691
871
|
update,
|
|
692
872
|
...(journalRequiresExclusion(version)
|
|
693
|
-
? { preApprovalExcluded: value.preApprovalExcluded }
|
|
873
|
+
? { preApprovalExcluded: value.preApprovalExcluded }
|
|
874
|
+
: {}),
|
|
694
875
|
...(inputClaim ? { inputClaim } : {}),
|
|
695
876
|
...(inputProvenance ? { inputProvenance } : {}),
|
|
696
877
|
admittedAtMs: value.admittedAtMs,
|
|
697
878
|
...(routingInput ? { routingInput } : {}),
|
|
698
879
|
state: value.state,
|
|
699
|
-
...(queueKind === "prompt" || queueKind === "control"
|
|
700
|
-
? { queueKind }
|
|
701
|
-
: {}),
|
|
880
|
+
...(queueKind === "prompt" || queueKind === "control" ? { queueKind } : {}),
|
|
702
881
|
...(isNonEmptyString(queueReceiptId) ? { queueReceiptId } : {}),
|
|
703
882
|
...(queueOwner ? { queueOwner } : {}),
|
|
704
883
|
...(queueHandoff ? { queueHandoff } : {}),
|
|
@@ -729,40 +908,56 @@ function validateJournalEntry(value, path, version) {
|
|
|
729
908
|
};
|
|
730
909
|
}
|
|
731
910
|
function assertSupportedJournalVersion(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
|
|
732
|
-
if (isRecord(value) &&
|
|
911
|
+
if (isRecord(value) &&
|
|
912
|
+
Number.isSafeInteger(value.version) &&
|
|
733
913
|
value.version !== version) {
|
|
734
914
|
throw createJournalError("unsupported-version", path, `uses unsupported version ${String(value.version)}`);
|
|
735
915
|
}
|
|
736
916
|
}
|
|
737
917
|
function validateJournalSourceCompletion(value, path) {
|
|
738
|
-
if (!isRecord(value) ||
|
|
739
|
-
!
|
|
740
|
-
|
|
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)) {
|
|
741
925
|
throw createJournalError("invalid", path, "has invalid source completion evidence");
|
|
742
926
|
}
|
|
743
|
-
return {
|
|
927
|
+
return {
|
|
928
|
+
updateId: value.updateId,
|
|
929
|
+
sourceSha256: value.sourceSha256,
|
|
930
|
+
completionSha256: value.completionSha256,
|
|
931
|
+
};
|
|
744
932
|
}
|
|
745
933
|
function validateJournalSourceCompletions(value, path, version, allowEmpty = false) {
|
|
746
|
-
if (!isTelegramUpdateJournalLegacyFamilyVersion(version) ||
|
|
934
|
+
if (!isTelegramUpdateJournalLegacyFamilyVersion(version) ||
|
|
935
|
+
!Array.isArray(value) ||
|
|
936
|
+
(!allowEmpty && !value.length)) {
|
|
747
937
|
throw createJournalError("invalid", path, "has invalid source completion collection");
|
|
748
938
|
}
|
|
749
|
-
const completions = value.map(item => validateJournalSourceCompletion(item, path));
|
|
939
|
+
const completions = value.map((item) => validateJournalSourceCompletion(item, path));
|
|
750
940
|
const scopes = new Set();
|
|
751
941
|
for (let index = 0; index < completions.length; index++) {
|
|
752
942
|
const completion = completions[index];
|
|
753
|
-
if ((index > 0 && completions[index - 1].updateId >= completion.updateId) ||
|
|
943
|
+
if ((index > 0 && completions[index - 1].updateId >= completion.updateId) ||
|
|
944
|
+
scopes.has(completion.completionSha256)) {
|
|
754
945
|
throw createJournalError("invalid", path, "has duplicate or unordered source completion evidence");
|
|
755
946
|
}
|
|
756
947
|
scopes.add(completion.completionSha256);
|
|
757
948
|
}
|
|
758
949
|
return completions;
|
|
759
950
|
}
|
|
760
|
-
function
|
|
761
|
-
assertSupportedJournalVersion(value, path, version);
|
|
951
|
+
function assertJournalExclusionCursor(value, path, version) {
|
|
762
952
|
if (journalRequiresExclusion(version) &&
|
|
763
|
-
(!isRecord(value) ||
|
|
953
|
+
(!isRecord(value) ||
|
|
954
|
+
!isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
|
|
764
955
|
throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
|
|
765
956
|
}
|
|
957
|
+
}
|
|
958
|
+
function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
|
|
959
|
+
assertSupportedJournalVersion(value, path, version);
|
|
960
|
+
assertJournalExclusionCursor(value, path, version);
|
|
766
961
|
if (!isRecord(value) ||
|
|
767
962
|
!hasOnlyKeys(value, [
|
|
768
963
|
"version",
|
|
@@ -798,9 +993,7 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
|
|
|
798
993
|
}
|
|
799
994
|
const queuedReceipts = new Map();
|
|
800
995
|
for (const entry of entries) {
|
|
801
|
-
if (entry.state !== "queued" ||
|
|
802
|
-
!entry.queueKind ||
|
|
803
|
-
!entry.queueReceiptId) {
|
|
996
|
+
if (entry.state !== "queued" || !entry.queueKind || !entry.queueReceiptId) {
|
|
804
997
|
continue;
|
|
805
998
|
}
|
|
806
999
|
const existing = queuedReceipts.get(entry.queueReceiptId);
|
|
@@ -831,12 +1024,18 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
|
|
|
831
1024
|
}
|
|
832
1025
|
}
|
|
833
1026
|
const operatorDispositions = (value.operatorDispositions ?? []).map((disposition) => validateJournalOperatorDisposition(disposition, path));
|
|
834
|
-
const sourceCompletions = value.sourceCompletions === undefined
|
|
1027
|
+
const sourceCompletions = value.sourceCompletions === undefined
|
|
1028
|
+
? []
|
|
1029
|
+
: validateJournalSourceCompletions(value.sourceCompletions, path, version);
|
|
835
1030
|
const botIdentity = validateBotIdentity(value.botIdentity, path);
|
|
836
|
-
const activeIds = new Set(entries.map(entry => entry.updateId));
|
|
837
|
-
const discardedIds = new Set(operatorDispositions
|
|
838
|
-
|
|
839
|
-
|
|
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))) {
|
|
840
1039
|
throw createJournalError("invalid", path, "has contradictory source completion evidence");
|
|
841
1040
|
}
|
|
842
1041
|
const dispositionFailureIds = new Set();
|
|
@@ -868,11 +1067,15 @@ function parseJournalFile(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION
|
|
|
868
1067
|
};
|
|
869
1068
|
}
|
|
870
1069
|
function assertJournalRoutingInputContinuity(file, entries, path) {
|
|
871
|
-
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]));
|
|
872
1071
|
for (const entry of entries) {
|
|
873
1072
|
const retained = previous.get(entry.updateId), next = entry.routingInput;
|
|
874
|
-
if (retained &&
|
|
875
|
-
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"))) {
|
|
876
1079
|
throw createJournalError("invalid", path, "regresses retained routing input lifetime");
|
|
877
1080
|
}
|
|
878
1081
|
}
|
|
@@ -881,14 +1084,20 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
|
|
|
881
1084
|
assertJournalRoutingInputContinuity(file, segment.upsertedEntries, path);
|
|
882
1085
|
if (segment.sourceCompletions === undefined)
|
|
883
1086
|
return;
|
|
884
|
-
const previous = new Map((file.sourceCompletions ?? []).map(completion => [
|
|
885
|
-
|
|
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
|
+
]));
|
|
886
1095
|
if ([...previous].some(([id, completion]) => !isDeepStrictEqual(completion, next.get(id)))) {
|
|
887
1096
|
throw createJournalError("invalid", path, "regresses retained source completion evidence");
|
|
888
1097
|
}
|
|
889
|
-
const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
|
|
890
|
-
const removed = new Set(segment.removedUpdateIds), upserted = new Set(segment.upsertedEntries.map(entry => entry.updateId));
|
|
891
|
-
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));
|
|
892
1101
|
// Grouped markers share one whole-receipt check rather than rescanning the group per source.
|
|
893
1102
|
const queueGroups = new Map(), queueRemovals = new Map();
|
|
894
1103
|
for (const entry of file.entries) {
|
|
@@ -905,8 +1114,15 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
|
|
|
905
1114
|
if (queueRemovals.has(receiptId))
|
|
906
1115
|
return queueRemovals.get(receiptId);
|
|
907
1116
|
const group = queueGroups.get(receiptId);
|
|
908
|
-
const valid = !!group?.length &&
|
|
909
|
-
|
|
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));
|
|
910
1126
|
queueRemovals.set(receiptId, valid);
|
|
911
1127
|
return valid;
|
|
912
1128
|
};
|
|
@@ -914,18 +1130,19 @@ function assertJournalSourceCompletionContinuity(file, segment, path) {
|
|
|
914
1130
|
if (previous.has(completion.updateId))
|
|
915
1131
|
continue;
|
|
916
1132
|
const source = entries.get(completion.updateId);
|
|
917
|
-
if (!removed.has(completion.updateId) ||
|
|
918
|
-
|
|
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) {
|
|
919
1139
|
throw createJournalError("invalid", path, "source completion does not match its removal revision");
|
|
920
1140
|
}
|
|
921
1141
|
}
|
|
922
1142
|
}
|
|
923
1143
|
function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERSION) {
|
|
924
1144
|
assertSupportedJournalVersion(value, path, version);
|
|
925
|
-
|
|
926
|
-
(!isRecord(value) || !isSafeNonNegativeInteger(value.acceptedThroughUpdateId))) {
|
|
927
|
-
throw createJournalError("pairing-evidence", path, "is missing its exclusion-mode admission cursor");
|
|
928
|
-
}
|
|
1145
|
+
assertJournalExclusionCursor(value, path, version);
|
|
929
1146
|
if (!isRecord(value) ||
|
|
930
1147
|
!hasOnlyKeys(value, [
|
|
931
1148
|
"version",
|
|
@@ -992,7 +1209,11 @@ function parseJournalSegment(value, path, version = TELEGRAM_UPDATE_JOURNAL_VERS
|
|
|
992
1209
|
...(value.operatorDispositions !== undefined
|
|
993
1210
|
? { operatorDispositions }
|
|
994
1211
|
: {}),
|
|
995
|
-
...(value.sourceCompletions !== undefined
|
|
1212
|
+
...(value.sourceCompletions !== undefined
|
|
1213
|
+
? {
|
|
1214
|
+
sourceCompletions: validateJournalSourceCompletions(value.sourceCompletions, path, version, true),
|
|
1215
|
+
}
|
|
1216
|
+
: {}),
|
|
996
1217
|
};
|
|
997
1218
|
}
|
|
998
1219
|
/**
|
|
@@ -1025,7 +1246,10 @@ export function inspectTelegramUpdateJournalFamily(input) {
|
|
|
1025
1246
|
/** Classifies private originals against exact discard tombstones; never replay or deletion authority. */
|
|
1026
1247
|
export function inspectTelegramUpdateJournalRetention(input) {
|
|
1027
1248
|
const acquired = acquireTelegramUpdateJournalFamily(input, undefined, true);
|
|
1028
|
-
return {
|
|
1249
|
+
return {
|
|
1250
|
+
evidence: acquired.evidence,
|
|
1251
|
+
retainedInputs: acquired.retainedInputs ?? [],
|
|
1252
|
+
};
|
|
1029
1253
|
}
|
|
1030
1254
|
/**
|
|
1031
1255
|
* Read-only source evidence for cooperating writers serialized by the caller through
|
|
@@ -1052,9 +1276,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1052
1276
|
const { path, limits } = input;
|
|
1053
1277
|
const acquiredSegments = [];
|
|
1054
1278
|
let snapshotRevision = 0;
|
|
1055
|
-
const fail = (message) => {
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
+
]) {
|
|
1058
1291
|
if (!isSafePositiveInteger(limits[key]))
|
|
1059
1292
|
fail("requires positive safe-integer limits");
|
|
1060
1293
|
}
|
|
@@ -1063,21 +1296,34 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1063
1296
|
fail("requires an exact profile");
|
|
1064
1297
|
const anchor = input.directory;
|
|
1065
1298
|
const contained = relative(anchor, path);
|
|
1066
|
-
if (!isAbsolute(anchor) ||
|
|
1067
|
-
resolve(
|
|
1068
|
-
|
|
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))
|
|
1069
1307
|
fail("escapes its canonical directory anchor");
|
|
1070
|
-
if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
|
|
1071
|
-
fail("platform lacks no-follow nonblocking open evidence");
|
|
1072
1308
|
let files = 0;
|
|
1073
1309
|
let bytes = 0;
|
|
1074
1310
|
let work = 0;
|
|
1075
1311
|
const observed = new Map();
|
|
1076
1312
|
const ancestors = new Set();
|
|
1077
|
-
const sameAncestor = (a, b) => b.isDirectory() &&
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
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;
|
|
1081
1327
|
const status = (target) => {
|
|
1082
1328
|
try {
|
|
1083
1329
|
return lstatSync(target, { bigint: true });
|
|
@@ -1092,7 +1338,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1092
1338
|
const value = status(target);
|
|
1093
1339
|
if (!value && !optional)
|
|
1094
1340
|
fail("has a missing path component");
|
|
1095
|
-
if (value &&
|
|
1341
|
+
if (value &&
|
|
1342
|
+
(value.isSymbolicLink() ||
|
|
1343
|
+
!(directory ? value.isDirectory() : value.isFile())))
|
|
1096
1344
|
fail("has a linked or unexpected file type");
|
|
1097
1345
|
observed.set(target, value);
|
|
1098
1346
|
return value;
|
|
@@ -1100,11 +1348,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1100
1348
|
const charge = (value) => {
|
|
1101
1349
|
if (!isRecord(value))
|
|
1102
1350
|
return;
|
|
1103
|
-
for (const key of [
|
|
1351
|
+
for (const key of [
|
|
1352
|
+
"entries",
|
|
1353
|
+
"upsertedEntries",
|
|
1354
|
+
"removedUpdateIds",
|
|
1355
|
+
"operatorDispositions",
|
|
1356
|
+
"sourceCompletions",
|
|
1357
|
+
]) {
|
|
1104
1358
|
const collection = value[key];
|
|
1105
1359
|
if (!Array.isArray(collection))
|
|
1106
1360
|
continue;
|
|
1107
|
-
if (collection.length > limits.maxEntries ||
|
|
1361
|
+
if (collection.length > limits.maxEntries ||
|
|
1362
|
+
collection.length > limits.maxWork - work)
|
|
1108
1363
|
capacity();
|
|
1109
1364
|
work += collection.length;
|
|
1110
1365
|
}
|
|
@@ -1112,7 +1367,7 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1112
1367
|
const read = (target, before) => {
|
|
1113
1368
|
if (before.size > BigInt(limits.maxBytes - bytes))
|
|
1114
1369
|
capacity();
|
|
1115
|
-
const fd = openSync(target,
|
|
1370
|
+
const fd = openSync(target, TELEGRAM_STRICT_READ_FLAGS);
|
|
1116
1371
|
try {
|
|
1117
1372
|
const opened = fstatSync(fd, { bigint: true });
|
|
1118
1373
|
if (!opened.isFile() || !same(before, opened))
|
|
@@ -1152,7 +1407,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1152
1407
|
break;
|
|
1153
1408
|
if (names.length >= limits.maxFiles - usedFiles)
|
|
1154
1409
|
capacity();
|
|
1155
|
-
if (!pattern.test(entry.name) ||
|
|
1410
|
+
if (!pattern.test(entry.name) ||
|
|
1411
|
+
!entry.isFile() ||
|
|
1412
|
+
entry.isSymbolicLink())
|
|
1156
1413
|
fail("has an unexpected journal evidence entry");
|
|
1157
1414
|
names.push(entry.name);
|
|
1158
1415
|
}
|
|
@@ -1168,7 +1425,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1168
1425
|
if (realpathSync(anchor) !== anchor)
|
|
1169
1426
|
fail("requires a canonical directory anchor");
|
|
1170
1427
|
let parent = anchor;
|
|
1171
|
-
for (const component of relative(anchor, dirname(path))
|
|
1428
|
+
for (const component of relative(anchor, dirname(path))
|
|
1429
|
+
.split(sep)
|
|
1430
|
+
.filter(Boolean)) {
|
|
1172
1431
|
parent = join(parent, component);
|
|
1173
1432
|
observe(parent, true);
|
|
1174
1433
|
ancestors.add(parent);
|
|
@@ -1182,15 +1441,19 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1182
1441
|
let file;
|
|
1183
1442
|
let knownBotId = expected.botId;
|
|
1184
1443
|
const identity = (value) => {
|
|
1185
|
-
if (value.profile !== input.profile ||
|
|
1186
|
-
|
|
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)) {
|
|
1187
1449
|
throw createJournalError("identity-mismatch", path, "belongs to another exact journal identity");
|
|
1188
1450
|
}
|
|
1189
1451
|
knownBotId ??= value.botIdentity.botId;
|
|
1190
1452
|
};
|
|
1191
1453
|
if (snapshot) {
|
|
1192
1454
|
const raw = read(path, snapshot);
|
|
1193
|
-
if (!isRecord(raw) ||
|
|
1455
|
+
if (!isRecord(raw) ||
|
|
1456
|
+
(raw.version !== 1 && raw.version !== 2 && raw.version !== 3)) {
|
|
1194
1457
|
throw createJournalError("unsupported-version", path, "has an unsupported snapshot version");
|
|
1195
1458
|
}
|
|
1196
1459
|
file = parseJournalFile(raw, path, raw.version);
|
|
@@ -1201,11 +1464,18 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1201
1464
|
const metadata = observe(target, false);
|
|
1202
1465
|
const previousWork = work;
|
|
1203
1466
|
const segment = parseJournalSegment(read(target, metadata), target, file.version);
|
|
1204
|
-
acquiredSegments.push({
|
|
1205
|
-
|
|
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
|
+
});
|
|
1206
1475
|
files += 1;
|
|
1207
1476
|
identity(segment);
|
|
1208
|
-
if (segment.revision !== Number(name.slice(0, 16)) ||
|
|
1477
|
+
if (segment.revision !== Number(name.slice(0, 16)) ||
|
|
1478
|
+
segment.previousRevision !== segment.revision - 1)
|
|
1209
1479
|
fail("has an invalid intrinsic segment revision");
|
|
1210
1480
|
const dispositionIds = new Set();
|
|
1211
1481
|
for (const disposition of segment.operatorDispositions ?? []) {
|
|
@@ -1216,19 +1486,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1216
1486
|
const revision = file.revision ?? 0;
|
|
1217
1487
|
if (segment.revision <= revision)
|
|
1218
1488
|
continue;
|
|
1219
|
-
if (segment.revision !== revision + 1 ||
|
|
1489
|
+
if (segment.revision !== revision + 1 ||
|
|
1490
|
+
segment.previousRevision !== revision)
|
|
1220
1491
|
fail("has a newer revision gap");
|
|
1221
|
-
if (segment.acceptedThroughUpdateId !== undefined &&
|
|
1492
|
+
if (segment.acceptedThroughUpdateId !== undefined &&
|
|
1493
|
+
file.acceptedThroughUpdateId !== undefined &&
|
|
1222
1494
|
segment.acceptedThroughUpdateId < file.acceptedThroughUpdateId)
|
|
1223
1495
|
fail("regresses the admission cursor");
|
|
1224
1496
|
assertJournalSourceCompletionContinuity(file, segment, target);
|
|
1225
|
-
const entries = new Map(file.entries.map(entry => [entry.updateId, entry]));
|
|
1497
|
+
const entries = new Map(file.entries.map((entry) => [entry.updateId, entry]));
|
|
1226
1498
|
for (const id of segment.removedUpdateIds)
|
|
1227
1499
|
entries.delete(id);
|
|
1228
1500
|
for (const entry of segment.upsertedEntries) {
|
|
1229
1501
|
const previous = entries.get(entry.updateId);
|
|
1230
|
-
if (journalRequiresExclusion(file.version) &&
|
|
1231
|
-
(
|
|
1502
|
+
if (journalRequiresExclusion(file.version) &&
|
|
1503
|
+
((previous &&
|
|
1504
|
+
previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
|
|
1505
|
+
(!previous &&
|
|
1506
|
+
entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
|
|
1232
1507
|
throw createJournalError("pairing-evidence", target, "changes exclusion evidence or resurrects a settled source");
|
|
1233
1508
|
}
|
|
1234
1509
|
if (!entries.has(entry.updateId) && entries.size >= limits.maxEntries)
|
|
@@ -1238,7 +1513,9 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1238
1513
|
const next = {
|
|
1239
1514
|
...file,
|
|
1240
1515
|
revision: segment.revision,
|
|
1241
|
-
...(segment.acceptedThroughUpdateId !== undefined
|
|
1516
|
+
...(segment.acceptedThroughUpdateId !== undefined
|
|
1517
|
+
? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId }
|
|
1518
|
+
: {}),
|
|
1242
1519
|
entries: [...entries.values()].sort((a, b) => a.updateId - b.updateId),
|
|
1243
1520
|
operatorDispositions: segment.operatorDispositions ?? file.operatorDispositions,
|
|
1244
1521
|
sourceCompletions: segment.sourceCompletions ?? file.sourceCompletions,
|
|
@@ -1249,16 +1526,24 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1249
1526
|
}
|
|
1250
1527
|
const retainedInputs = [];
|
|
1251
1528
|
const retentionDirectory = `${path}.retained`;
|
|
1252
|
-
const retention = inspectRetention
|
|
1529
|
+
const retention = inspectRetention
|
|
1530
|
+
? observe(retentionDirectory, true, true)
|
|
1531
|
+
: undefined;
|
|
1253
1532
|
const retentionPattern = /^abandon-[a-f0-9]{64}\.json$/u;
|
|
1254
|
-
const retentionNames = retention
|
|
1533
|
+
const retentionNames = retention
|
|
1534
|
+
? enumerate(retentionDirectory, retentionPattern)
|
|
1535
|
+
: [];
|
|
1255
1536
|
if (retentionNames.length > limits.maxEntries)
|
|
1256
1537
|
capacity();
|
|
1257
1538
|
if (retention) {
|
|
1258
1539
|
if (!file || file.version !== 1)
|
|
1259
1540
|
fail("private retention requires its exact v1 journal snapshot");
|
|
1260
1541
|
const current = file;
|
|
1261
|
-
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
|
+
})));
|
|
1262
1547
|
const updateIds = new Set();
|
|
1263
1548
|
for (const name of retentionNames) {
|
|
1264
1549
|
const target = join(retentionDirectory, name);
|
|
@@ -1272,42 +1557,70 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1272
1557
|
work += 2;
|
|
1273
1558
|
if (!isRecord(raw))
|
|
1274
1559
|
throw createJournalError("invalid", target, "private retention is not an evidence record");
|
|
1275
|
-
if (typeof raw.journalBindingKey !== "string" ||
|
|
1560
|
+
if (typeof raw.journalBindingKey !== "string" ||
|
|
1561
|
+
!bindingKeys.has(raw.journalBindingKey))
|
|
1276
1562
|
throw createJournalError("invalid", target, "has foreign private retention evidence");
|
|
1277
1563
|
const entry = validateJournalEntry(raw.entry, target, 1);
|
|
1278
1564
|
const disposition = validateJournalOperatorDisposition(raw.requestedDisposition, target);
|
|
1279
|
-
const digest = createHash("sha256")
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
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
|
+
}))
|
|
1285
1583
|
fail("has inconsistent private retention evidence");
|
|
1286
1584
|
if (updateIds.has(entry.updateId))
|
|
1287
1585
|
fail("has duplicate private retention sources");
|
|
1288
1586
|
updateIds.add(entry.updateId);
|
|
1289
|
-
const committed = current.operatorDispositions?.find(value => value.failureId === disposition.failureId ||
|
|
1290
|
-
|
|
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)))
|
|
1291
1592
|
fail("private retention contradicts its journal disposition");
|
|
1292
|
-
retainedInputs.push({
|
|
1293
|
-
|
|
1593
|
+
retainedInputs.push({
|
|
1594
|
+
path: target,
|
|
1595
|
+
journalBindingKey: raw.journalBindingKey,
|
|
1596
|
+
failureId: disposition.failureId,
|
|
1597
|
+
updateId: entry.updateId,
|
|
1598
|
+
state: committed ? "committed" : "uncommitted",
|
|
1599
|
+
});
|
|
1294
1600
|
}
|
|
1295
1601
|
}
|
|
1296
|
-
if (inspectRetention &&
|
|
1297
|
-
disposition
|
|
1298
|
-
|
|
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")))
|
|
1299
1609
|
fail("committed abandonment lost its private original");
|
|
1300
1610
|
// Re-enumeration has the same bound, rather than allocating an unbounded census.
|
|
1301
1611
|
files = snapshot ? 1 : 0;
|
|
1302
1612
|
if (segments && !isDeepStrictEqual(names, enumerate()))
|
|
1303
1613
|
fail("changed segment namespace");
|
|
1304
1614
|
files += names.length;
|
|
1305
|
-
if (retention &&
|
|
1615
|
+
if (retention &&
|
|
1616
|
+
!isDeepStrictEqual(retentionNames, enumerate(retentionDirectory, retentionPattern)))
|
|
1306
1617
|
fail("changed private retention namespace");
|
|
1307
1618
|
files += retentionNames.length;
|
|
1308
1619
|
for (const [target, before] of observed) {
|
|
1309
1620
|
const after = status(target);
|
|
1310
|
-
const compare = selectedVersion !== undefined && ancestors.has(target)
|
|
1621
|
+
const compare = selectedVersion !== undefined && ancestors.has(target)
|
|
1622
|
+
? sameAncestor
|
|
1623
|
+
: same;
|
|
1311
1624
|
if (before ? !after || !compare(before, after) : after !== undefined)
|
|
1312
1625
|
fail("changed observed namespace or file");
|
|
1313
1626
|
}
|
|
@@ -1318,14 +1631,27 @@ function acquireTelegramUpdateJournalFamily(input, selectedVersion, inspectReten
|
|
|
1318
1631
|
if (file.version !== selectedVersion) {
|
|
1319
1632
|
throw createJournalError("unsupported-version", path, "does not match the selected source version");
|
|
1320
1633
|
}
|
|
1321
|
-
if (createTelegramUpdateJournalReceiptScope({
|
|
1322
|
-
|
|
1634
|
+
if (createTelegramUpdateJournalReceiptScope({
|
|
1635
|
+
profileName: input.profile,
|
|
1636
|
+
botIdentity: expected,
|
|
1637
|
+
}) !==
|
|
1638
|
+
createTelegramUpdateJournalReceiptScope({
|
|
1639
|
+
profileName: file.profile,
|
|
1640
|
+
botIdentity: file.botIdentity,
|
|
1641
|
+
})) {
|
|
1323
1642
|
throw createJournalError("identity-mismatch", path, "does not retain the expected receipt scope");
|
|
1324
1643
|
}
|
|
1325
1644
|
}
|
|
1326
1645
|
}
|
|
1327
1646
|
return {
|
|
1328
|
-
evidence: file
|
|
1647
|
+
evidence: file
|
|
1648
|
+
? {
|
|
1649
|
+
kind: "present",
|
|
1650
|
+
file,
|
|
1651
|
+
...(knownBotId !== undefined ? { knownBotId } : {}),
|
|
1652
|
+
accounting: { files, bytes, work },
|
|
1653
|
+
}
|
|
1654
|
+
: { kind: "absent" },
|
|
1329
1655
|
snapshotRevision,
|
|
1330
1656
|
segments: acquiredSegments,
|
|
1331
1657
|
...(inspectRetention ? { retainedInputs } : {}),
|
|
@@ -1346,9 +1672,19 @@ export function inspectTelegramSessionJournalNamespace(input) {
|
|
|
1346
1672
|
}
|
|
1347
1673
|
function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
1348
1674
|
const { directory, profile, limits } = input;
|
|
1349
|
-
const fail = (message) => {
|
|
1350
|
-
|
|
1351
|
-
|
|
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
|
+
]) {
|
|
1352
1688
|
if (!isSafePositiveInteger(limits[key]))
|
|
1353
1689
|
fail("requires positive safe-integer limits");
|
|
1354
1690
|
}
|
|
@@ -1357,26 +1693,37 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1357
1693
|
const expected = validateBotIdentity(input.botIdentity, directory);
|
|
1358
1694
|
if (!isAbsolute(directory) || resolve(directory) !== directory)
|
|
1359
1695
|
fail("requires a canonical directory anchor");
|
|
1360
|
-
if (!constants.O_NOFOLLOW || !constants.O_NONBLOCK)
|
|
1361
|
-
fail("platform lacks no-follow nonblocking open evidence");
|
|
1362
1696
|
const metadata = (target) => {
|
|
1363
1697
|
const value = lstatSync(target, { bigint: true });
|
|
1364
|
-
return {
|
|
1365
|
-
|
|
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
|
+
};
|
|
1366
1707
|
};
|
|
1367
1708
|
const suffix = profile === "default" ? "" : `.${profile}`;
|
|
1368
1709
|
const pollingPath = input.pollingPath ?? join(directory, `inbox${suffix}.json`);
|
|
1369
1710
|
const polling = relative(directory, pollingPath);
|
|
1370
1711
|
{
|
|
1371
1712
|
const parts = polling.split(sep);
|
|
1372
|
-
if (!isAbsolute(pollingPath) ||
|
|
1373
|
-
|
|
1374
|
-
|
|
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)))
|
|
1375
1720
|
fail("requires a canonical polling journal path");
|
|
1376
1721
|
}
|
|
1377
1722
|
try {
|
|
1378
1723
|
const root = lstatSync(directory);
|
|
1379
|
-
if (!root.isDirectory() ||
|
|
1724
|
+
if (!root.isDirectory() ||
|
|
1725
|
+
root.isSymbolicLink() ||
|
|
1726
|
+
realpathSync(directory) !== directory)
|
|
1380
1727
|
fail("requires a canonical directory anchor");
|
|
1381
1728
|
const beforeRoot = metadata(directory);
|
|
1382
1729
|
const census = () => {
|
|
@@ -1405,7 +1752,8 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1405
1752
|
};
|
|
1406
1753
|
const requireType = (target, isDirectory) => {
|
|
1407
1754
|
const value = lstatSync(target);
|
|
1408
|
-
if (value.isSymbolicLink() ||
|
|
1755
|
+
if (value.isSymbolicLink() ||
|
|
1756
|
+
!(isDirectory ? value.isDirectory() : value.isFile()) ||
|
|
1409
1757
|
(!isDirectory && value.nlink !== 1))
|
|
1410
1758
|
fail("has a linked or unexpected journal type");
|
|
1411
1759
|
};
|
|
@@ -1451,7 +1799,9 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1451
1799
|
if (!/inbox/iu.test(name))
|
|
1452
1800
|
return;
|
|
1453
1801
|
const match = /^(inbox|follower-inbox-[a-f0-9]{16})(?:\.([a-z0-9]{1,32}))?\.json(\.(?:segments|retained))?$/u.exec(name);
|
|
1454
|
-
if (!match ||
|
|
1802
|
+
if (!match ||
|
|
1803
|
+
match[2] === "default" ||
|
|
1804
|
+
(!includeSessions && match[3] === ".retained"))
|
|
1455
1805
|
fail("has a noncanonical journal-like entry");
|
|
1456
1806
|
requireType(target, !!match[3]);
|
|
1457
1807
|
if (includeSessions && match[3])
|
|
@@ -1461,26 +1811,58 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1461
1811
|
if (familyProfile === profile && base !== pollingPath)
|
|
1462
1812
|
families.set(base, match[1] === "inbox" ? "session" : "follower");
|
|
1463
1813
|
});
|
|
1464
|
-
return {
|
|
1465
|
-
|
|
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
|
+
};
|
|
1466
1819
|
};
|
|
1467
1820
|
const before = census();
|
|
1468
|
-
const sourceInventory = [
|
|
1821
|
+
const sourceInventory = [
|
|
1822
|
+
[pollingPath, "polling"],
|
|
1823
|
+
...before.families,
|
|
1824
|
+
];
|
|
1469
1825
|
const sources = [];
|
|
1470
1826
|
const retainedInputs = [];
|
|
1471
|
-
const accounting = {
|
|
1827
|
+
const accounting = {
|
|
1828
|
+
directoryEntries: before.entries.length,
|
|
1829
|
+
files: 0,
|
|
1830
|
+
bytes: 0,
|
|
1831
|
+
work: 0,
|
|
1832
|
+
};
|
|
1472
1833
|
let knownBotId = expected.botId;
|
|
1473
1834
|
for (const [path, role] of sourceInventory) {
|
|
1474
|
-
const remaining = {
|
|
1475
|
-
|
|
1476
|
-
|
|
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)
|
|
1477
1844
|
capacity();
|
|
1478
|
-
const acquire = () => acquireTelegramUpdateJournalFamily({
|
|
1479
|
-
|
|
1480
|
-
|
|
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();
|
|
1481
1858
|
const evidence = acquired.evidence;
|
|
1482
|
-
retainedInputs.push(...(acquired.retainedInputs ?? []).map(original => ({
|
|
1483
|
-
|
|
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`))) {
|
|
1484
1866
|
fail("lost a discovered journal family");
|
|
1485
1867
|
}
|
|
1486
1868
|
if (evidence.kind === "present") {
|
|
@@ -1488,13 +1870,15 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1488
1870
|
accounting.files += evidence.accounting.files;
|
|
1489
1871
|
accounting.bytes += evidence.accounting.bytes;
|
|
1490
1872
|
}
|
|
1491
|
-
accounting.work +=
|
|
1873
|
+
accounting.work +=
|
|
1874
|
+
evidence.kind === "present" ? Math.max(1, evidence.accounting.work) : 1;
|
|
1492
1875
|
sources.push({ role, path, evidence });
|
|
1493
1876
|
}
|
|
1494
1877
|
const namespaceCurrent = () => {
|
|
1495
1878
|
try {
|
|
1496
|
-
return isDeepStrictEqual(beforeRoot, metadata(directory)) &&
|
|
1497
|
-
isDeepStrictEqual(
|
|
1879
|
+
return (isDeepStrictEqual(beforeRoot, metadata(directory)) &&
|
|
1880
|
+
isDeepStrictEqual(before, census()) &&
|
|
1881
|
+
isDeepStrictEqual(beforeRoot, metadata(directory)));
|
|
1498
1882
|
}
|
|
1499
1883
|
catch {
|
|
1500
1884
|
return false;
|
|
@@ -1502,7 +1886,12 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1502
1886
|
};
|
|
1503
1887
|
if (!namespaceCurrent())
|
|
1504
1888
|
fail("changed observed root namespace or identity");
|
|
1505
|
-
return {
|
|
1889
|
+
return {
|
|
1890
|
+
sources,
|
|
1891
|
+
accounting,
|
|
1892
|
+
...(includeSessions ? { retainedInputs } : {}),
|
|
1893
|
+
...(knownBotId !== undefined ? { knownBotId } : {}),
|
|
1894
|
+
};
|
|
1506
1895
|
}
|
|
1507
1896
|
catch (error) {
|
|
1508
1897
|
if (error instanceof TelegramUpdateJournalError)
|
|
@@ -1510,35 +1899,105 @@ function inspectTelegramJournalNamespace(input, includeSessions) {
|
|
|
1510
1899
|
throw createJournalError("io", directory, "could not establish canonical namespace evidence", error);
|
|
1511
1900
|
}
|
|
1512
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
|
+
];
|
|
1513
1931
|
/** Complete-empty protection only; caller holds source serialization through consumption. Never deletion authority. */
|
|
1514
1932
|
export function isTelegramThreadCleanupJournalNamespaceClear(input) {
|
|
1515
|
-
if (!input.requiredJournalBindingKeys.length ||
|
|
1933
|
+
if (!input.requiredJournalBindingKeys.length ||
|
|
1934
|
+
input.requiredJournalBindingKeys.length > input.limits.maxDirectoryEntries)
|
|
1516
1935
|
throw new Error("Telegram temporary cleanup requires bounded exact journal references.");
|
|
1517
1936
|
const requiredPaths = new Set();
|
|
1518
1937
|
for (const key of input.requiredJournalBindingKeys) {
|
|
1519
1938
|
const path = getTelegramUpdateJournalBindingPath(key);
|
|
1520
|
-
const identities = [
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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
|
+
})))
|
|
1524
1950
|
throw new Error("Telegram temporary cleanup has foreign or malformed journal references.");
|
|
1525
1951
|
requiredPaths.add(path);
|
|
1526
1952
|
}
|
|
1527
1953
|
const namespace = inspectTelegramSessionJournalNamespace(input);
|
|
1528
|
-
if (namespace.sources.filter(source => source.role === "polling").length !==
|
|
1529
|
-
|
|
1530
|
-
|
|
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")))
|
|
1531
1958
|
return false;
|
|
1532
|
-
|
|
1533
|
-
|
|
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)));
|
|
1534
1990
|
}
|
|
1535
1991
|
function cloneEntry(entry) {
|
|
1536
1992
|
return {
|
|
1537
1993
|
...entry,
|
|
1538
1994
|
update: structuredClone(entry.update),
|
|
1539
|
-
...(entry.inputClaim
|
|
1995
|
+
...(entry.inputClaim
|
|
1996
|
+
? { inputClaim: structuredClone(entry.inputClaim) }
|
|
1997
|
+
: {}),
|
|
1540
1998
|
...(entry.inputProvenance
|
|
1541
|
-
? { inputProvenance: structuredClone(entry.inputProvenance) }
|
|
1999
|
+
? { inputProvenance: structuredClone(entry.inputProvenance) }
|
|
2000
|
+
: {}),
|
|
1542
2001
|
...(entry.queueOwner
|
|
1543
2002
|
? { queueOwner: cloneJournalQueueOwner(entry.queueOwner) }
|
|
1544
2003
|
: {}),
|
|
@@ -1559,7 +2018,13 @@ function cloneFile(file) {
|
|
|
1559
2018
|
profile: file.profile,
|
|
1560
2019
|
botIdentity: { ...file.botIdentity },
|
|
1561
2020
|
entries: file.entries.map(cloneEntry),
|
|
1562
|
-
...(file.sourceCompletions?.length
|
|
2021
|
+
...(file.sourceCompletions?.length
|
|
2022
|
+
? {
|
|
2023
|
+
sourceCompletions: file.sourceCompletions.map((completion) => ({
|
|
2024
|
+
...completion,
|
|
2025
|
+
})),
|
|
2026
|
+
}
|
|
2027
|
+
: {}),
|
|
1563
2028
|
...(file.operatorDispositions?.length
|
|
1564
2029
|
? {
|
|
1565
2030
|
operatorDispositions: file.operatorDispositions.map((disposition) => ({ ...disposition })),
|
|
@@ -1590,7 +2055,8 @@ function publishTelegramUpdateJournalSegmentUnlocked(path, segment, onPublicatio
|
|
|
1590
2055
|
let snapshotRevision = 0;
|
|
1591
2056
|
try {
|
|
1592
2057
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
1593
|
-
snapshotRevision =
|
|
2058
|
+
snapshotRevision =
|
|
2059
|
+
parseJournalFile(parsed, path, segment.version).revision ?? 0;
|
|
1594
2060
|
}
|
|
1595
2061
|
catch (error) {
|
|
1596
2062
|
if (error?.code !== "ENOENT")
|
|
@@ -1661,7 +2127,9 @@ function mergeBotIdentity(stored, current) {
|
|
|
1661
2127
|
}
|
|
1662
2128
|
function createPendingRetentionReference(input) {
|
|
1663
2129
|
const { path, journalBindingKey, entry, maxBytes } = input;
|
|
1664
|
-
const evidenceSha256 = createHash("sha256")
|
|
2130
|
+
const evidenceSha256 = createHash("sha256")
|
|
2131
|
+
.update(JSON.stringify({ journalBindingKey, entry }))
|
|
2132
|
+
.digest("hex");
|
|
1665
2133
|
const failureId = `abandon-${evidenceSha256}`;
|
|
1666
2134
|
const retainedDirectory = `${path}.retained`;
|
|
1667
2135
|
const retainedPath = join(retainedDirectory, `${failureId}.json`);
|
|
@@ -1687,22 +2155,37 @@ function createPendingRetentionReference(input) {
|
|
|
1687
2155
|
return undefined;
|
|
1688
2156
|
throw error;
|
|
1689
2157
|
}
|
|
1690
|
-
if (!stat.isFile() ||
|
|
2158
|
+
if (!stat.isFile() ||
|
|
2159
|
+
stat.isSymbolicLink() ||
|
|
2160
|
+
stat.size > maxBytes + 4096) {
|
|
1691
2161
|
throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
|
|
1692
2162
|
}
|
|
1693
2163
|
const retained = JSON.parse(readFileSync(retainedPath, "utf8"));
|
|
1694
2164
|
if (!isRecord(retained))
|
|
1695
2165
|
throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
|
|
1696
2166
|
const disposition = validateJournalOperatorDisposition(retained.requestedDisposition, retainedPath);
|
|
1697
|
-
if (!("dispositionKind" in disposition) ||
|
|
1698
|
-
disposition.
|
|
1699
|
-
disposition.
|
|
1700
|
-
|
|
1701
|
-
|
|
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
|
+
})) {
|
|
1702
2180
|
throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
|
|
1703
2181
|
}
|
|
1704
|
-
return {
|
|
1705
|
-
|
|
2182
|
+
return {
|
|
2183
|
+
version: 1,
|
|
2184
|
+
kind: "pending-input-retention",
|
|
2185
|
+
journalBindingKey,
|
|
2186
|
+
entry,
|
|
2187
|
+
requestedDisposition: disposition,
|
|
2188
|
+
};
|
|
1706
2189
|
};
|
|
1707
2190
|
return { evidenceSha256, failureId, retainedPath, read };
|
|
1708
2191
|
}
|
|
@@ -1826,26 +2309,47 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
|
|
|
1826
2309
|
});
|
|
1827
2310
|
const path = deps.getJournalPath(configuredProfileName);
|
|
1828
2311
|
const workspaceAdmission = deps.getWorkspaceAdmission?.();
|
|
1829
|
-
const sourceAccess = {
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
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
|
+
: {}),
|
|
1835
2328
|
...(workspaceAdmission ? { workspaceAdmission } : {}),
|
|
1836
|
-
...(deps.withWriterAdmission
|
|
1837
|
-
|
|
1838
|
-
|
|
2329
|
+
...(deps.withWriterAdmission
|
|
2330
|
+
? { withWriterAdmission: deps.withWriterAdmission }
|
|
2331
|
+
: {}),
|
|
2332
|
+
...(deps.withSourceSerialization
|
|
2333
|
+
? { withSourceSerialization: deps.withSourceSerialization }
|
|
2334
|
+
: {}),
|
|
2335
|
+
...(deps.onRecovery ? { onRecovery: deps.onRecovery } : {}),
|
|
2336
|
+
};
|
|
1839
2337
|
const journal = createTelegramUpdateJournalStore(options);
|
|
1840
|
-
// Scoped completion and receipt observation use strict private handles
|
|
1841
|
-
|
|
1842
|
-
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;
|
|
1843
2340
|
const completionJournal = deps.withSourceSerialization
|
|
1844
|
-
? createTelegramUpdateJournalStore({
|
|
2341
|
+
? createTelegramUpdateJournalStore({
|
|
2342
|
+
...options,
|
|
2343
|
+
...(strict ? { sourceAccess } : {}),
|
|
2344
|
+
})
|
|
2345
|
+
: undefined;
|
|
1845
2346
|
// Queue receipt readiness gates every ordinary prompt; without strict handles it uses the ordinary journal read.
|
|
1846
2347
|
const observeQueuedReceipt = (expected) => strict
|
|
1847
2348
|
? completionJournal.inspectQueuedReceipt(expected)
|
|
1848
|
-
: inspectJournalQueuedReceipt(journal.read(), {
|
|
2349
|
+
: inspectJournalQueuedReceipt(journal.read(), {
|
|
2350
|
+
...expected,
|
|
2351
|
+
queueOwner: validateQueuedReceiptObservation(expected, path),
|
|
2352
|
+
}, path);
|
|
1849
2353
|
return {
|
|
1850
2354
|
runtimeKey: JSON.stringify({
|
|
1851
2355
|
path,
|
|
@@ -1860,25 +2364,48 @@ export function createTelegramUpdateJournalRuntimeBindingResolver(deps) {
|
|
|
1860
2364
|
readForProtection() {
|
|
1861
2365
|
// Protection must never turn corruption/recovery into empty-work evidence.
|
|
1862
2366
|
const evidence = inspectTelegramUpdateJournalFamily({
|
|
1863
|
-
...sourceAccess,
|
|
2367
|
+
...sourceAccess,
|
|
2368
|
+
path,
|
|
2369
|
+
profile: profileName,
|
|
2370
|
+
botIdentity,
|
|
1864
2371
|
});
|
|
1865
|
-
return {
|
|
2372
|
+
return {
|
|
2373
|
+
entries: evidence.kind === "present" ? evidence.file.entries : [],
|
|
2374
|
+
exists: evidence.kind === "present",
|
|
2375
|
+
};
|
|
1866
2376
|
},
|
|
1867
|
-
journal: completionJournal
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
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,
|
|
1882
2409
|
};
|
|
1883
2410
|
};
|
|
1884
2411
|
}
|
|
@@ -1899,12 +2426,13 @@ export function createTelegramUpdateJournalReferenceRegistry(input = {}) {
|
|
|
1899
2426
|
released = true;
|
|
1900
2427
|
};
|
|
1901
2428
|
},
|
|
1902
|
-
list: () => [...active.values()].map(reference => ({ ...reference })),
|
|
2429
|
+
list: () => [...active.values()].map((reference) => ({ ...reference })),
|
|
1903
2430
|
withReference(reference, operation) {
|
|
1904
2431
|
const release = this.acquire(reference);
|
|
1905
2432
|
try {
|
|
1906
2433
|
const result = operation();
|
|
1907
|
-
if (result &&
|
|
2434
|
+
if (result &&
|
|
2435
|
+
typeof result.finally === "function")
|
|
1908
2436
|
return result.finally(release);
|
|
1909
2437
|
release();
|
|
1910
2438
|
return result;
|
|
@@ -1920,15 +2448,23 @@ export function withTelegramResolvedUpdateJournalReference(input) {
|
|
|
1920
2448
|
const binding = input.resolveBinding();
|
|
1921
2449
|
if (!binding)
|
|
1922
2450
|
return undefined;
|
|
1923
|
-
return input.registry.withReference({ referenceClass: input.referenceClass,
|
|
1924
|
-
recoveryKey: binding.recoveryKey }, () => input.operation(binding));
|
|
2451
|
+
return input.registry.withReference({ referenceClass: input.referenceClass, recoveryKey: binding.recoveryKey }, () => input.operation(binding));
|
|
1925
2452
|
}
|
|
1926
2453
|
function validateQueuedReceiptObservation(expected, path) {
|
|
1927
|
-
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") ||
|
|
1928
2462
|
!isNonEmptyString(expected.receiptId) ||
|
|
1929
|
-
!Array.isArray(expected.sourceUpdateIds) ||
|
|
2463
|
+
!Array.isArray(expected.sourceUpdateIds) ||
|
|
2464
|
+
expected.sourceUpdateIds.length === 0 ||
|
|
1930
2465
|
expected.sourceUpdateIds.length > TELEGRAM_UPDATE_JOURNAL_MAX_ENTRIES ||
|
|
1931
|
-
expected.sourceUpdateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
|
|
2466
|
+
expected.sourceUpdateIds.some((id, index) => !isSafeNonNegativeInteger(id) ||
|
|
2467
|
+
(index > 0 && id <= expected.sourceUpdateIds[index - 1]))) {
|
|
1932
2468
|
throw createJournalError("invalid", path, "received invalid queue receipt observation authority");
|
|
1933
2469
|
}
|
|
1934
2470
|
return validateJournalQueueOwner(expected.queueOwner, path);
|
|
@@ -1937,19 +2473,31 @@ function inspectJournalQueuedReceipt(file, expected, path) {
|
|
|
1937
2473
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
|
|
1938
2474
|
throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
|
|
1939
2475
|
const { queueOwner } = expected;
|
|
1940
|
-
const entries = file.entries.filter(entry => entry.queueReceiptId === expected.receiptId);
|
|
1941
|
-
if (entries.length !== expected.sourceUpdateIds.length ||
|
|
1942
|
-
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)))
|
|
1943
2483
|
return undefined;
|
|
1944
|
-
return {
|
|
1945
|
-
|
|
2484
|
+
return {
|
|
2485
|
+
receipt: {
|
|
2486
|
+
queueKind: expected.queueKind,
|
|
2487
|
+
receiptId: expected.receiptId,
|
|
2488
|
+
sourceUpdateIds: [...expected.sourceUpdateIds],
|
|
2489
|
+
queueOwner: { ...queueOwner },
|
|
2490
|
+
},
|
|
1946
2491
|
sources: entries.map(createTelegramUpdateJournalEntryDigest),
|
|
1947
|
-
queueOwnerSha256: createHash("sha256")
|
|
2492
|
+
queueOwnerSha256: createHash("sha256")
|
|
2493
|
+
.update(JSON.stringify(queueOwner))
|
|
2494
|
+
.digest("hex"),
|
|
2495
|
+
};
|
|
1948
2496
|
}
|
|
1949
2497
|
function inspectJournalSourceCompletion(file, completion, path) {
|
|
1950
2498
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(file.version))
|
|
1951
2499
|
throw createJournalError("invalid", path, "source completion requires a legacy v1 source");
|
|
1952
|
-
const retained = file.sourceCompletions?.find(value => value.updateId === completion.updateId);
|
|
2500
|
+
const retained = file.sourceCompletions?.find((value) => value.updateId === completion.updateId);
|
|
1953
2501
|
if (!retained)
|
|
1954
2502
|
return undefined;
|
|
1955
2503
|
if (!isDeepStrictEqual(retained, completion))
|
|
@@ -1959,35 +2507,48 @@ function inspectJournalSourceCompletion(file, completion, path) {
|
|
|
1959
2507
|
/** Operator-distinct authority: routing never treats it as an owner cancellation. */
|
|
1960
2508
|
export const TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX = "session-successor:";
|
|
1961
2509
|
function isAdoptablePendingEntry(entry) {
|
|
1962
|
-
return entry.state === "pending" &&
|
|
1963
|
-
!entry.
|
|
1964
|
-
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);
|
|
1965
2520
|
}
|
|
1966
2521
|
export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
1967
2522
|
const resolveLeader = createTelegramUpdateJournalRuntimeBindingResolver({
|
|
1968
2523
|
...deps.base,
|
|
1969
2524
|
getJournalPath: deps.getLeaderJournalPath,
|
|
1970
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
|
+
});
|
|
1971
2543
|
const createFollowerResolver = (bindingKey, includeQueueRuntimeIdentity, sessionId, legacy = false) => {
|
|
1972
2544
|
// Session-aware composition must not fall back to a process journal before preparation supplies its ID.
|
|
1973
2545
|
if (!legacy && deps.getActiveFollowerSessionId && !sessionId)
|
|
1974
2546
|
return () => undefined;
|
|
1975
2547
|
return createTelegramUpdateJournalRuntimeBindingResolver({
|
|
1976
|
-
|
|
1977
|
-
getBotToken: deps.base.getBotToken,
|
|
1978
|
-
getBotId: deps.base.getBotId,
|
|
2548
|
+
...getSharedResolverPorts(),
|
|
1979
2549
|
...(includeQueueRuntimeIdentity && deps.base.getQueueRuntimeIdentity
|
|
1980
2550
|
? { getQueueRuntimeIdentity: deps.base.getQueueRuntimeIdentity }
|
|
1981
2551
|
: {}),
|
|
1982
|
-
...(deps.base.withWriterAdmission
|
|
1983
|
-
? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
|
|
1984
|
-
// Follower journals need the same strict exact-receipt ports as the leader; without them queue publication cannot complete.
|
|
1985
|
-
...(deps.base.withSourceSerialization
|
|
1986
|
-
? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
|
|
1987
|
-
...(deps.base.getWorkspaceAdmission
|
|
1988
|
-
? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
|
|
1989
|
-
: {}),
|
|
1990
|
-
...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
|
|
1991
2552
|
getJournalPath(profileName) {
|
|
1992
2553
|
return deps.getFollowerJournalPath(bindingKey, profileName, sessionId);
|
|
1993
2554
|
},
|
|
@@ -2001,8 +2562,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2001
2562
|
const profile = deps.base.getProfileName() ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
2002
2563
|
if (!path || !token)
|
|
2003
2564
|
return undefined;
|
|
2004
|
-
const botIdentity = createTelegramUpdateJournalBotIdentity({
|
|
2005
|
-
|
|
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
|
+
}));
|
|
2006
2574
|
if (!keys.includes(journalBindingKey))
|
|
2007
2575
|
return undefined;
|
|
2008
2576
|
const getRoot = () => deps.getRuntimeDir?.() ?? dirname(deps.getLeaderJournalPath(profile));
|
|
@@ -2011,23 +2579,43 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2011
2579
|
const suffix = escapeTelegramJournalPathPattern(getTelegramProfilePathSuffix(profile));
|
|
2012
2580
|
const flat = new RegExp(`^(?:inbox|follower-inbox-[a-f0-9]{16})${suffix}\\.json$`, "u");
|
|
2013
2581
|
const session = new RegExp(`^(?:journal\\.[a-f0-9]{16}|inbox)${suffix}\\.json$`, "u");
|
|
2014
|
-
if (!isAbsolute(root) ||
|
|
2015
|
-
|
|
2016
|
-
|
|
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]))))
|
|
2017
2591
|
return undefined;
|
|
2018
|
-
const current = () => deps.base.getBotToken() === token &&
|
|
2019
|
-
|
|
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;
|
|
2020
2597
|
const inspect = () => {
|
|
2021
2598
|
if (!current())
|
|
2022
2599
|
return undefined;
|
|
2023
|
-
const observed = inspectTelegramUpdateJournalRetention({
|
|
2024
|
-
|
|
2025
|
-
|
|
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
|
+
});
|
|
2026
2612
|
if (observed.evidence.kind !== "present" || !current())
|
|
2027
2613
|
return undefined;
|
|
2028
2614
|
return operation(observed.evidence.file, observed.retainedInputs, path);
|
|
2029
2615
|
};
|
|
2030
|
-
const result = deps.base.withSourceSerialization
|
|
2616
|
+
const result = deps.base.withSourceSerialization
|
|
2617
|
+
? deps.base.withSourceSerialization(inspect)
|
|
2618
|
+
: inspect();
|
|
2031
2619
|
return current() ? result : undefined;
|
|
2032
2620
|
};
|
|
2033
2621
|
let preparedFollower;
|
|
@@ -2042,9 +2630,15 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2042
2630
|
if (!sessionId)
|
|
2043
2631
|
return undefined;
|
|
2044
2632
|
const previous = preparedFollower;
|
|
2045
|
-
const result = previous &&
|
|
2046
|
-
|
|
2047
|
-
|
|
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
|
+
})
|
|
2048
2642
|
: undefined;
|
|
2049
2643
|
// Only a completed adoption advances the predecessor; failures retry idempotently on the next preparation.
|
|
2050
2644
|
preparedFollower = { recipientBindingKey, sessionId };
|
|
@@ -2054,16 +2648,27 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2054
2648
|
if (!isSafeNonNegativeInteger(updateId))
|
|
2055
2649
|
throw new Error("Telegram abandonment proof requires an exact update ID.");
|
|
2056
2650
|
return inspectHistoricalSource(journalBindingKey, (file, originals) => {
|
|
2057
|
-
const original = originals.find(value => value.updateId === updateId &&
|
|
2651
|
+
const original = originals.find((value) => value.updateId === updateId &&
|
|
2652
|
+
value.state === "committed" &&
|
|
2058
2653
|
value.journalBindingKey === journalBindingKey);
|
|
2059
2654
|
if (!original)
|
|
2060
2655
|
return undefined;
|
|
2061
|
-
const disposition = file.operatorDispositions?.find(value => value.failureId === original.failureId);
|
|
2062
|
-
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")
|
|
2063
2660
|
return undefined;
|
|
2064
|
-
return {
|
|
2661
|
+
return {
|
|
2662
|
+
journalBindingKey,
|
|
2663
|
+
updateId,
|
|
2664
|
+
retainedPath: original.path,
|
|
2665
|
+
operatorAuthorityId: disposition.operatorAuthorityId,
|
|
2666
|
+
};
|
|
2065
2667
|
});
|
|
2066
2668
|
},
|
|
2669
|
+
inspectSourceGroupExpiry(journalBindingKey, updateIds) {
|
|
2670
|
+
return inspectHistoricalSource(journalBindingKey, (file) => inspectRoutingGroupExpiry(file, journalBindingKey, updateIds));
|
|
2671
|
+
},
|
|
2067
2672
|
inspectSourceCompletion(journalBindingKey, expected) {
|
|
2068
2673
|
const completion = validateJournalSourceCompletion(expected, getTelegramUpdateJournalBindingPath(journalBindingKey) ?? "journal");
|
|
2069
2674
|
return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalSourceCompletion(file, completion, path));
|
|
@@ -2073,9 +2678,12 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2073
2678
|
return inspectHistoricalSource(journalBindingKey, (file, _originals, path) => inspectJournalQueuedReceipt(file, { ...expected, queueOwner }, path));
|
|
2074
2679
|
},
|
|
2075
2680
|
adoptPredecessorPending(input) {
|
|
2076
|
-
const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent } = input;
|
|
2077
|
-
if (!recipientBindingKey ||
|
|
2078
|
-
predecessorSessionId
|
|
2681
|
+
const { recipientBindingKey, predecessorSessionId, successorSessionId, isCurrent, } = input;
|
|
2682
|
+
if (!recipientBindingKey ||
|
|
2683
|
+
!predecessorSessionId ||
|
|
2684
|
+
!successorSessionId ||
|
|
2685
|
+
predecessorSessionId === successorSessionId ||
|
|
2686
|
+
typeof isCurrent !== "function") {
|
|
2079
2687
|
throw new Error("Telegram session adoption requires distinct exact session identities.");
|
|
2080
2688
|
}
|
|
2081
2689
|
const assertCurrent = () => {
|
|
@@ -2085,18 +2693,29 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2085
2693
|
assertCurrent();
|
|
2086
2694
|
const predecessor = createFollowerResolver(recipientBindingKey, false, predecessorSessionId)();
|
|
2087
2695
|
const successor = createFollowerResolver(recipientBindingKey, false, successorSessionId)();
|
|
2088
|
-
if (!predecessor?.readForProtection ||
|
|
2696
|
+
if (!predecessor?.readForProtection ||
|
|
2697
|
+
!successor ||
|
|
2698
|
+
predecessor.recoveryKey === successor.recoveryKey) {
|
|
2089
2699
|
throw new Error("Telegram session adoption requires exact predecessor and successor journals.");
|
|
2090
2700
|
}
|
|
2091
|
-
const result = {
|
|
2701
|
+
const result = {
|
|
2702
|
+
adoptedUpdateIds: [],
|
|
2703
|
+
retainedUpdateIds: [],
|
|
2704
|
+
};
|
|
2092
2705
|
// Non-repairing selection; the abandonment CAS rechecks each exact entry under the source lock.
|
|
2093
|
-
const candidates = predecessor
|
|
2706
|
+
const candidates = predecessor
|
|
2707
|
+
.readForProtection()
|
|
2708
|
+
.entries.filter(isAdoptablePendingEntry)
|
|
2094
2709
|
.sort((left, right) => left.updateId - right.updateId);
|
|
2095
2710
|
for (const entry of candidates) {
|
|
2096
2711
|
assertCurrent();
|
|
2097
2712
|
// Commit away first: a crash afterwards preserves the private original instead of duplicating execution.
|
|
2098
|
-
const committed = predecessor.journal.abandonPending({
|
|
2099
|
-
|
|
2713
|
+
const committed = predecessor.journal.abandonPending({
|
|
2714
|
+
journalBindingKey: predecessor.recoveryKey,
|
|
2715
|
+
entry,
|
|
2716
|
+
operatorAuthorityId: `${TELEGRAM_SESSION_ADOPTION_AUTHORITY_PREFIX}${successorSessionId}`,
|
|
2717
|
+
isCurrent,
|
|
2718
|
+
});
|
|
2100
2719
|
if (committed.duplicate) {
|
|
2101
2720
|
result.retainedUpdateIds.push(entry.updateId);
|
|
2102
2721
|
continue;
|
|
@@ -2110,17 +2729,7 @@ export function createTelegramUpdateJournalBindingRuntime(deps) {
|
|
|
2110
2729
|
createRecipientResolver: (bindingKey, sessionId) => createFollowerResolver(bindingKey, false, sessionId),
|
|
2111
2730
|
createLegacyRecipientResolver: (bindingKey) => createFollowerResolver(bindingKey, false, undefined, true),
|
|
2112
2731
|
createPathResolver: (path) => createTelegramUpdateJournalRuntimeBindingResolver({
|
|
2113
|
-
|
|
2114
|
-
getBotToken: deps.base.getBotToken,
|
|
2115
|
-
getBotId: deps.base.getBotId,
|
|
2116
|
-
...(deps.base.withWriterAdmission
|
|
2117
|
-
? { withWriterAdmission: deps.base.withWriterAdmission } : {}),
|
|
2118
|
-
...(deps.base.withSourceSerialization
|
|
2119
|
-
? { withSourceSerialization: deps.base.withSourceSerialization } : {}),
|
|
2120
|
-
...(deps.base.getWorkspaceAdmission
|
|
2121
|
-
? { getWorkspaceAdmission: deps.base.getWorkspaceAdmission }
|
|
2122
|
-
: {}),
|
|
2123
|
-
...(deps.base.onRecovery ? { onRecovery: deps.base.onRecovery } : {}),
|
|
2732
|
+
...getSharedResolverPorts(),
|
|
2124
2733
|
getJournalPath: () => path,
|
|
2125
2734
|
}),
|
|
2126
2735
|
};
|
|
@@ -2132,15 +2741,26 @@ export function createTelegramUpdateJournalStore(options) {
|
|
|
2132
2741
|
/** Opt-in v3 only; does not migrate old files or expose legacy unowned mutation ports. */
|
|
2133
2742
|
export function createTelegramInputJournalStore(options) {
|
|
2134
2743
|
const captured = { ...options };
|
|
2135
|
-
if (!captured.sourceAccess ||
|
|
2744
|
+
if (!captured.sourceAccess ||
|
|
2745
|
+
!captured.queueRuntimeIdentity ||
|
|
2136
2746
|
typeof captured.withSourceSerialization !== "function" ||
|
|
2137
2747
|
typeof captured.withPairingAdmission !== "function" ||
|
|
2138
2748
|
typeof captured.getInputContext !== "function" ||
|
|
2139
|
-
("withPairedAdmission" in captured &&
|
|
2749
|
+
("withPairedAdmission" in captured &&
|
|
2750
|
+
captured.withPairedAdmission !== undefined)) {
|
|
2140
2751
|
throw new Error("Telegram input custody requires strict serialized polling admission and runtime identity.");
|
|
2141
2752
|
}
|
|
2142
2753
|
return createJournalStoreCore({ ...captured, queueRuntimeIdentity: { ...captured.queueRuntimeIdentity } }, captured.getInputContext).input;
|
|
2143
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
|
+
}
|
|
2144
2764
|
function createJournalStoreCore(options, getInputContext) {
|
|
2145
2765
|
const path = options.path;
|
|
2146
2766
|
const profile = options.profileName ?? TELEGRAM_DEFAULT_PROFILE_NAME;
|
|
@@ -2174,8 +2794,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2174
2794
|
if (withPairingAdmission && withPairedAdmission) {
|
|
2175
2795
|
throw new Error("Telegram journal admission modes are mutually exclusive.");
|
|
2176
2796
|
}
|
|
2177
|
-
const version = getInputContext
|
|
2178
|
-
|
|
2797
|
+
const version = getInputContext
|
|
2798
|
+
? TELEGRAM_UPDATE_JOURNAL_CUSTODY_VERSION
|
|
2799
|
+
: withPairingAdmission
|
|
2800
|
+
? TELEGRAM_UPDATE_JOURNAL_EXCLUSION_VERSION
|
|
2801
|
+
: TELEGRAM_UPDATE_JOURNAL_VERSION;
|
|
2179
2802
|
const notifyRecovery = (event) => {
|
|
2180
2803
|
try {
|
|
2181
2804
|
options.onRecovery?.(event);
|
|
@@ -2361,7 +2984,8 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2361
2984
|
}
|
|
2362
2985
|
if (segment.profile !== storedProfile ||
|
|
2363
2986
|
!identitiesMatch(segment.botIdentity, storedIdentity) ||
|
|
2364
|
-
(withPairingAdmission &&
|
|
2987
|
+
(withPairingAdmission &&
|
|
2988
|
+
segment.botIdentity.tokenSha256 !== storedIdentity.tokenSha256)) {
|
|
2365
2989
|
throw createJournalError("identity-mismatch", segmentPath, "belongs to another journal identity");
|
|
2366
2990
|
}
|
|
2367
2991
|
assertJournalSourceCompletionContinuity(file, segment, segmentPath);
|
|
@@ -2377,8 +3001,10 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2377
3001
|
for (const entry of segment.upsertedEntries) {
|
|
2378
3002
|
const previous = entriesById.get(entry.updateId);
|
|
2379
3003
|
if (withPairingAdmission &&
|
|
2380
|
-
((previous &&
|
|
2381
|
-
|
|
3004
|
+
((previous &&
|
|
3005
|
+
previous.preApprovalExcluded !== entry.preApprovalExcluded) ||
|
|
3006
|
+
(!previous &&
|
|
3007
|
+
entry.updateId <= (file.acceptedThroughUpdateId ?? -1)))) {
|
|
2382
3008
|
throw createJournalError("pairing-evidence", segmentPath, "changes exclusion evidence or resurrects a settled source");
|
|
2383
3009
|
}
|
|
2384
3010
|
entriesById.set(entry.updateId, entry);
|
|
@@ -2401,16 +3027,23 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2401
3027
|
: file.operatorDispositions?.length
|
|
2402
3028
|
? { operatorDispositions: file.operatorDispositions }
|
|
2403
3029
|
: {}),
|
|
2404
|
-
...(segment.sourceCompletions !== undefined
|
|
2405
|
-
|
|
3030
|
+
...(segment.sourceCompletions !== undefined
|
|
3031
|
+
? { sourceCompletions: segment.sourceCompletions }
|
|
3032
|
+
: file.sourceCompletions?.length
|
|
3033
|
+
? { sourceCompletions: file.sourceCompletions }
|
|
3034
|
+
: {}),
|
|
2406
3035
|
}, segmentPath, version);
|
|
2407
3036
|
revision = segment.revision;
|
|
2408
3037
|
}
|
|
2409
3038
|
const identityChanged = storedProfile !== profile ||
|
|
2410
3039
|
!identitiesMatch(file.botIdentity, expectedIdentity) ||
|
|
2411
|
-
((withPairingAdmission || !!file.sourceCompletions?.length) &&
|
|
3040
|
+
((withPairingAdmission || !!file.sourceCompletions?.length) &&
|
|
3041
|
+
file.botIdentity.tokenSha256 !== expectedIdentity.tokenSha256);
|
|
2412
3042
|
if (identityChanged) {
|
|
2413
|
-
if (file.entries.length > 0 ||
|
|
3043
|
+
if (file.entries.length > 0 ||
|
|
3044
|
+
file.sourceCompletions?.length ||
|
|
3045
|
+
withPairingAdmission ||
|
|
3046
|
+
!allowReconciliation) {
|
|
2414
3047
|
throw createJournalError("identity-mismatch", path, storedProfile !== profile
|
|
2415
3048
|
? `belongs to profile ${storedProfile}, not ${profile}`
|
|
2416
3049
|
: "belongs to another Telegram bot identity");
|
|
@@ -2592,15 +3225,26 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2592
3225
|
try {
|
|
2593
3226
|
// Acquire before the transaction helper can create parents or staging names.
|
|
2594
3227
|
// The config continuation excludes participating writers through consumption.
|
|
2595
|
-
const source = sourceAccess
|
|
2596
|
-
|
|
2597
|
-
|
|
3228
|
+
const source = sourceAccess
|
|
3229
|
+
? acquireTelegramUpdateJournalFamily({
|
|
3230
|
+
...sourceAccess,
|
|
3231
|
+
path,
|
|
3232
|
+
profile,
|
|
3233
|
+
botIdentity: expectedIdentity,
|
|
3234
|
+
}, version)
|
|
3235
|
+
: undefined;
|
|
2598
3236
|
const readSource = () => {
|
|
2599
3237
|
if (!source)
|
|
2600
3238
|
return readCurrent();
|
|
2601
|
-
const file = source.evidence.kind === "present"
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
+
};
|
|
2604
3248
|
};
|
|
2605
3249
|
return withTelegramFileTransaction(`${path}.transaction`, () => operation(readSource));
|
|
2606
3250
|
}
|
|
@@ -2620,7 +3264,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2620
3264
|
if (!sourceAccess)
|
|
2621
3265
|
return;
|
|
2622
3266
|
const limits = sourceAccess.limits;
|
|
2623
|
-
if (files > limits.maxFiles ||
|
|
3267
|
+
if (files > limits.maxFiles ||
|
|
3268
|
+
bytes > limits.maxBytes ||
|
|
3269
|
+
work > limits.maxWork ||
|
|
2624
3270
|
collections.some((length) => length > limits.maxEntries)) {
|
|
2625
3271
|
throw createJournalError("capacity", path, "publication would exceed source inspection resource limits");
|
|
2626
3272
|
}
|
|
@@ -2628,20 +3274,31 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2628
3274
|
const inputHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH);
|
|
2629
3275
|
const inputBindingHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH);
|
|
2630
3276
|
const inputHandoffHeadroomText = "\0".repeat(TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_ID_MAX_LENGTH);
|
|
2631
|
-
const getFileWork = (file) => file.entries.length +
|
|
3277
|
+
const getFileWork = (file) => file.entries.length +
|
|
3278
|
+
(file.operatorDispositions?.length ?? 0) +
|
|
3279
|
+
(file.sourceCompletions?.length ?? 0);
|
|
2632
3280
|
const advanceInputHeadroomState = (state, entries, projectionSlack) => {
|
|
2633
3281
|
const revision = (state.file.revision ?? 0) + 1;
|
|
2634
3282
|
if (!isSafePositiveInteger(revision))
|
|
2635
3283
|
throw createJournalError("capacity", path, "exhausted input custody revisions");
|
|
2636
3284
|
return { file: { ...state.file, revision, entries }, projectionSlack };
|
|
2637
3285
|
};
|
|
2638
|
-
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]);
|
|
2639
3291
|
const createInputHeadroomClaim = (entry, phase) => ({
|
|
2640
3292
|
phase,
|
|
2641
|
-
owner: {
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
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
|
+
},
|
|
2645
3302
|
recipientBindingKey: inputBindingHeadroomText,
|
|
2646
3303
|
executionUpdate: structuredClone(entry.update),
|
|
2647
3304
|
});
|
|
@@ -2656,29 +3313,64 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2656
3313
|
const createInputHeadroomTransitionPlan = (file, entry) => {
|
|
2657
3314
|
const initial = { file, projectionSlack: 0 };
|
|
2658
3315
|
if (entry.inputClaim?.phase === "running")
|
|
2659
|
-
return [
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
+
};
|
|
2664
3331
|
delete acceptedClaim.handoff;
|
|
2665
3332
|
const acceptedEntry = { ...entry, inputClaim: acceptedClaim };
|
|
2666
3333
|
const acceptedState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, acceptedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2667
|
-
return [
|
|
3334
|
+
return [
|
|
3335
|
+
acceptedState,
|
|
3336
|
+
...createInputHeadroomTransitionPlan(acceptedState.file, acceptedEntry),
|
|
3337
|
+
];
|
|
2668
3338
|
}
|
|
2669
3339
|
if (entry.state === "pending" && entry.inputClaim?.phase === "ready") {
|
|
2670
|
-
const runningEntry = {
|
|
3340
|
+
const runningEntry = {
|
|
3341
|
+
...entry,
|
|
3342
|
+
inputClaim: { ...entry.inputClaim, phase: "running" },
|
|
3343
|
+
};
|
|
2671
3344
|
const runningState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2672
|
-
return [
|
|
3345
|
+
return [
|
|
3346
|
+
runningState,
|
|
3347
|
+
advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3348
|
+
];
|
|
2673
3349
|
}
|
|
2674
|
-
if (entry.state === "pending" &&
|
|
2675
|
-
|
|
3350
|
+
if (entry.state === "pending" &&
|
|
3351
|
+
entry.preApprovalExcluded === false &&
|
|
3352
|
+
!entry.inputClaim) {
|
|
3353
|
+
const readyEntry = {
|
|
3354
|
+
...entry,
|
|
3355
|
+
inputClaim: createInputHeadroomClaim(entry, "ready"),
|
|
3356
|
+
};
|
|
2676
3357
|
const readyState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, readyEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
|
|
2677
|
-
const runningEntry = {
|
|
3358
|
+
const runningEntry = {
|
|
3359
|
+
...readyEntry,
|
|
3360
|
+
inputClaim: { ...readyEntry.inputClaim, phase: "running" },
|
|
3361
|
+
};
|
|
2678
3362
|
const runningState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, runningEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES);
|
|
2679
|
-
return [
|
|
3363
|
+
return [
|
|
3364
|
+
readyState,
|
|
3365
|
+
runningState,
|
|
3366
|
+
advanceInputHeadroomState(runningState, replaceInputHeadroomEntry(runningState.file, entry.updateId), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3367
|
+
];
|
|
2680
3368
|
}
|
|
2681
|
-
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
|
+
: [];
|
|
2682
3374
|
};
|
|
2683
3375
|
const createInputHeadroomCancelPlan = (file, entry) => {
|
|
2684
3376
|
if (!entry.inputClaim?.handoff)
|
|
@@ -2688,40 +3380,54 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2688
3380
|
const cancelledEntry = { ...entry, inputClaim: claim };
|
|
2689
3381
|
const initial = { file, projectionSlack: 0 };
|
|
2690
3382
|
const cancelledState = advanceInputHeadroomState(initial, replaceInputHeadroomEntry(file, entry.updateId, cancelledEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2691
|
-
return [
|
|
3383
|
+
return [
|
|
3384
|
+
cancelledState,
|
|
3385
|
+
...createInputHeadroomTransitionPlan(cancelledState.file, cancelledEntry),
|
|
3386
|
+
];
|
|
2692
3387
|
};
|
|
2693
3388
|
const createInputHeadroomReleasePlan = (file, entry, allowOffered = false) => {
|
|
2694
3389
|
if (entry.inputClaim?.handoff && !allowOffered)
|
|
2695
3390
|
return [];
|
|
2696
3391
|
const normal = createInputHeadroomTransitionPlan(file, entry);
|
|
2697
|
-
const readyState = entry.inputClaim?.phase === "ready"
|
|
3392
|
+
const readyState = entry.inputClaim?.phase === "ready"
|
|
3393
|
+
? { file, projectionSlack: 0 }
|
|
3394
|
+
: normal[0];
|
|
2698
3395
|
if (!readyState)
|
|
2699
3396
|
return [];
|
|
2700
|
-
const readyEntry = readyState.file.entries.find(candidate => candidate.updateId === entry.updateId);
|
|
3397
|
+
const readyEntry = readyState.file.entries.find((candidate) => candidate.updateId === entry.updateId);
|
|
2701
3398
|
if (readyEntry?.inputClaim?.phase !== "ready")
|
|
2702
3399
|
return [];
|
|
2703
3400
|
const releasedEntry = cloneEntry(readyEntry);
|
|
2704
3401
|
delete releasedEntry.inputClaim;
|
|
2705
3402
|
const releasedState = advanceInputHeadroomState(readyState, replaceInputHeadroomEntry(readyState.file, entry.updateId, releasedEntry), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES);
|
|
2706
3403
|
const reacquisition = createInputHeadroomTransitionPlan(releasedState.file, releasedEntry);
|
|
2707
|
-
return [
|
|
3404
|
+
return [
|
|
3405
|
+
...(readyState.file === file ? [] : [readyState]),
|
|
3406
|
+
releasedState,
|
|
3407
|
+
...reacquisition,
|
|
3408
|
+
];
|
|
2708
3409
|
};
|
|
2709
3410
|
const createInputHeadroomQueueCompletionPlan = (file, entries) => {
|
|
2710
|
-
if (entries.length === 0 ||
|
|
3411
|
+
if (entries.length === 0 ||
|
|
3412
|
+
entries.some((entry) => entry.state !== "queued" || entry.queueHandoff))
|
|
2711
3413
|
return [];
|
|
2712
|
-
const requestedIds = new Set(entries.map(entry => entry.updateId));
|
|
3414
|
+
const requestedIds = new Set(entries.map((entry) => entry.updateId));
|
|
2713
3415
|
const initial = { file, projectionSlack: 0 };
|
|
2714
|
-
return [
|
|
3416
|
+
return [
|
|
3417
|
+
advanceInputHeadroomState(initial, file.entries.filter((entry) => !requestedIds.has(entry.updateId)), TELEGRAM_UPDATE_JOURNAL_INPUT_TRANSITION_HEADROOM_BYTES),
|
|
3418
|
+
];
|
|
2715
3419
|
};
|
|
2716
3420
|
const getInputHeadroomTransitionPlans = (file, updateId) => {
|
|
2717
3421
|
if (updateId !== "all") {
|
|
2718
3422
|
const requestedIds = new Set(typeof updateId === "number" ? [updateId] : [...updateId]);
|
|
2719
|
-
const requestedEntries = file.entries.filter(entry => requestedIds.has(entry.updateId));
|
|
3423
|
+
const requestedEntries = file.entries.filter((entry) => requestedIds.has(entry.updateId));
|
|
2720
3424
|
const queuedReceiptId = requestedEntries[0]?.queueReceiptId;
|
|
2721
|
-
if (requestedEntries.length === requestedIds.size &&
|
|
2722
|
-
|
|
3425
|
+
if (requestedEntries.length === requestedIds.size &&
|
|
3426
|
+
queuedReceiptId &&
|
|
3427
|
+
requestedEntries.every((entry) => entry.state === "queued" &&
|
|
2723
3428
|
entry.queueReceiptId === queuedReceiptId) &&
|
|
2724
|
-
file.entries.filter(entry => entry.queueReceiptId === queuedReceiptId)
|
|
3429
|
+
file.entries.filter((entry) => entry.queueReceiptId === queuedReceiptId)
|
|
3430
|
+
.length === requestedIds.size) {
|
|
2725
3431
|
const completion = createInputHeadroomQueueCompletionPlan(file, requestedEntries);
|
|
2726
3432
|
return completion.length > 0 ? [completion] : [];
|
|
2727
3433
|
}
|
|
@@ -2731,19 +3437,47 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2731
3437
|
const plan = entry ? createInputHeadroomTransitionPlan(file, entry) : [];
|
|
2732
3438
|
const cancel = entry ? createInputHeadroomCancelPlan(file, entry) : [];
|
|
2733
3439
|
const recover = entry?.inputClaim?.handoff
|
|
2734
|
-
? createInputHeadroomReleasePlan(file, entry, true)
|
|
2735
|
-
|
|
3440
|
+
? createInputHeadroomReleasePlan(file, entry, true)
|
|
3441
|
+
: [];
|
|
3442
|
+
return [plan, cancel, recover].filter((candidate) => candidate.length > 0);
|
|
2736
3443
|
}
|
|
2737
|
-
const running = smallestInputHeadroomEntry(file.entries.filter(entry => entry.inputClaim?.phase === "running"));
|
|
2738
|
-
const offered = largestInputHeadroomEntry(file.entries.filter(entry => entry.state === "pending" &&
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
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));
|
|
2747
3481
|
if (offered) {
|
|
2748
3482
|
plans.push(createInputHeadroomCancelPlan(file, offered));
|
|
2749
3483
|
plans.push(createInputHeadroomReleasePlan(file, offered, true));
|
|
@@ -2754,7 +3488,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2754
3488
|
}
|
|
2755
3489
|
const queuedReceipts = new Map();
|
|
2756
3490
|
for (const entry of file.entries) {
|
|
2757
|
-
if (entry.state !== "queued" ||
|
|
3491
|
+
if (entry.state !== "queued" ||
|
|
3492
|
+
!entry.queueReceiptId ||
|
|
3493
|
+
entry.queueHandoff)
|
|
2758
3494
|
continue;
|
|
2759
3495
|
const grouped = queuedReceipts.get(entry.queueReceiptId) ?? [];
|
|
2760
3496
|
grouped.push(entry);
|
|
@@ -2768,14 +3504,23 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2768
3504
|
return plans;
|
|
2769
3505
|
};
|
|
2770
3506
|
const createInputHeadroomSegment = (previous, next) => {
|
|
2771
|
-
const previousEntries = new Map(previous.entries.map(entry => [entry.updateId, entry]));
|
|
2772
|
-
const nextEntries = new Map(next.entries.map(entry => [entry.updateId, entry]));
|
|
2773
|
-
return {
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
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
|
+
};
|
|
2779
3524
|
};
|
|
2780
3525
|
const assertInputHeadroomCapacity = (file, extraBytes) => {
|
|
2781
3526
|
const bytes = assertCapacity(file);
|
|
@@ -2797,46 +3542,74 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2797
3542
|
? { ...current.source.evidence.accounting }
|
|
2798
3543
|
: { files: 1, bytes: publishedBytes, work: getFileWork(publishedFile) };
|
|
2799
3544
|
if (publishedSegment) {
|
|
2800
|
-
const segmentWork = publishedSegment.upsertedEntries.length +
|
|
2801
|
-
|
|
3545
|
+
const segmentWork = publishedSegment.upsertedEntries.length +
|
|
3546
|
+
publishedSegment.removedUpdateIds.length +
|
|
3547
|
+
(publishedSegment.operatorDispositions?.length ?? 0) +
|
|
3548
|
+
(publishedSegment.sourceCompletions?.length ?? 0);
|
|
2802
3549
|
const segmentBytes = Buffer.byteLength(`${JSON.stringify(publishedSegment, null, 2)}\n`);
|
|
2803
3550
|
baseRetainedCount += 1;
|
|
2804
3551
|
baseRetainedBytes += segmentBytes;
|
|
2805
3552
|
baseRetainedWork += segmentWork;
|
|
2806
|
-
const segmentFirst = {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
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),
|
|
2811
3565
|
bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
|
|
2812
|
-
work: Math.max(segmentFirst.work, compactResidue.work)
|
|
3566
|
+
work: Math.max(segmentFirst.work, compactResidue.work),
|
|
3567
|
+
};
|
|
2813
3568
|
}
|
|
2814
3569
|
for (const transitions of plans) {
|
|
2815
3570
|
let retainedCount = baseRetainedCount;
|
|
2816
3571
|
let retainedBytes = baseRetainedBytes;
|
|
2817
3572
|
let retainedWork = baseRetainedWork;
|
|
2818
3573
|
let accounting = { ...baseAccounting };
|
|
2819
|
-
let state = {
|
|
3574
|
+
let state = {
|
|
3575
|
+
file: publishedFile,
|
|
3576
|
+
projectionSlack: 0,
|
|
3577
|
+
};
|
|
2820
3578
|
for (const next of transitions) {
|
|
2821
3579
|
const segment = createInputHeadroomSegment(state.file, next.file);
|
|
2822
|
-
const segmentCollections = [
|
|
2823
|
-
segment.
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
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;
|
|
2827
3592
|
const nextBytes = assertInputHeadroomCapacity(next.file, next.projectionSlack);
|
|
2828
|
-
const segmentFirst = {
|
|
2829
|
-
|
|
3593
|
+
const segmentFirst = {
|
|
3594
|
+
files: accounting.files + 1,
|
|
3595
|
+
bytes: accounting.bytes + segmentBytes,
|
|
3596
|
+
work: accounting.work + segmentWork + getFileWork(next.file),
|
|
3597
|
+
};
|
|
2830
3598
|
assertSourceResources(segmentFirst.files, segmentFirst.bytes, segmentFirst.work, segmentCollections);
|
|
2831
3599
|
retainedCount += 1;
|
|
2832
3600
|
retainedBytes += segmentBytes;
|
|
2833
3601
|
retainedWork += segmentWork;
|
|
2834
|
-
const compactResidue = {
|
|
2835
|
-
|
|
3602
|
+
const compactResidue = {
|
|
3603
|
+
files: 1 + retainedCount,
|
|
3604
|
+
bytes: nextBytes + retainedBytes,
|
|
3605
|
+
work: getFileWork(next.file) + retainedWork,
|
|
3606
|
+
};
|
|
2836
3607
|
assertSourceResources(compactResidue.files, compactResidue.bytes, compactResidue.work, segmentCollections);
|
|
2837
|
-
accounting = {
|
|
3608
|
+
accounting = {
|
|
3609
|
+
files: Math.max(segmentFirst.files, compactResidue.files),
|
|
2838
3610
|
bytes: Math.max(segmentFirst.bytes, compactResidue.bytes),
|
|
2839
|
-
work: Math.max(segmentFirst.work, compactResidue.work)
|
|
3611
|
+
work: Math.max(segmentFirst.work, compactResidue.work),
|
|
3612
|
+
};
|
|
2840
3613
|
state = next;
|
|
2841
3614
|
}
|
|
2842
3615
|
}
|
|
@@ -2850,9 +3623,15 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2850
3623
|
const serializedBytes = assertCapacity(revisedFile, serialized);
|
|
2851
3624
|
const segmentText = `${JSON.stringify(segment, null, 2)}\n`;
|
|
2852
3625
|
const segmentBytes = Buffer.byteLength(segmentText);
|
|
2853
|
-
const collections = [
|
|
2854
|
-
segment.
|
|
2855
|
-
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
|
+
];
|
|
2856
3635
|
const segmentWork = collections[0] + collections[1] + collections[2] + collections[5];
|
|
2857
3636
|
const fileWork = collections[3] + collections[4] + collections[6];
|
|
2858
3637
|
const accounting = source.evidence.accounting;
|
|
@@ -2860,10 +3639,14 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2860
3639
|
assertSourceResources(accounting.files + 1, accounting.bytes + segmentBytes, accounting.work + segmentWork + fileWork, collections);
|
|
2861
3640
|
const unapplied = source.segments.filter((item) => Number(item.name.slice(0, 16)) > source.snapshotRevision);
|
|
2862
3641
|
const compact = forceCompact ||
|
|
2863
|
-
unapplied.length + 1 >=
|
|
2864
|
-
|
|
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;
|
|
2865
3646
|
if (compact) {
|
|
2866
|
-
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);
|
|
2867
3650
|
}
|
|
2868
3651
|
const segmentPath = getTelegramUpdateJournalSegmentPath(path, segment.revision);
|
|
2869
3652
|
// A crash may retain staging, so keep it outside the strictly enumerated segment directory.
|
|
@@ -2872,9 +3655,13 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2872
3655
|
writeJournalFile(path, serialized, publicationBoundary);
|
|
2873
3656
|
// Snapshot scope must not change. Preserve a validated redundant ID witness instead.
|
|
2874
3657
|
const witness = file.botIdentity.botId === undefined
|
|
2875
|
-
? source.segments.find((item) => item.botId !== undefined)?.name
|
|
3658
|
+
? source.segments.find((item) => item.botId !== undefined)?.name
|
|
3659
|
+
: undefined;
|
|
2876
3660
|
const segmentDirectory = getTelegramUpdateJournalSegmentDirectory(path);
|
|
2877
|
-
const names = [
|
|
3661
|
+
const names = [
|
|
3662
|
+
...source.segments.map((item) => item.name),
|
|
3663
|
+
basename(segmentPath),
|
|
3664
|
+
];
|
|
2878
3665
|
for (const name of names) {
|
|
2879
3666
|
if (name === witness)
|
|
2880
3667
|
continue;
|
|
@@ -2888,12 +3675,18 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2888
3675
|
try {
|
|
2889
3676
|
rmdirSync(segmentDirectory);
|
|
2890
3677
|
}
|
|
2891
|
-
catch {
|
|
3678
|
+
catch {
|
|
3679
|
+
/* Retained witness or cleanup residue. */
|
|
3680
|
+
}
|
|
2892
3681
|
}
|
|
2893
3682
|
return { file: revisedFile, serializedBytes };
|
|
2894
3683
|
};
|
|
2895
|
-
const publishMutation = (current, entries, contentChanged, operatorDispositions = current.file.operatorDispositions, acceptedThroughUpdateId = current.file.acceptedThroughUpdateId, publicationBoundary = onPublicationBoundary, inputHeadroom = journalHasCustodyFields(version)
|
|
2896
|
-
|
|
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);
|
|
2897
3690
|
if (!contentChanged &&
|
|
2898
3691
|
isDeepStrictEqual(current.file.botIdentity, botIdentity) &&
|
|
2899
3692
|
isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? []) &&
|
|
@@ -2909,9 +3702,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2909
3702
|
...(acceptedThroughUpdateId !== undefined
|
|
2910
3703
|
? { acceptedThroughUpdateId }
|
|
2911
3704
|
: {}),
|
|
2912
|
-
...(operatorDispositions?.length
|
|
2913
|
-
? { operatorDispositions }
|
|
2914
|
-
: {}),
|
|
3705
|
+
...(operatorDispositions?.length ? { operatorDispositions } : {}),
|
|
2915
3706
|
...(sourceCompletions?.length ? { sourceCompletions } : {}),
|
|
2916
3707
|
};
|
|
2917
3708
|
assertJournalRoutingInputContinuity(current.file, entries, path);
|
|
@@ -2928,7 +3719,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2928
3719
|
}
|
|
2929
3720
|
if (!current.exists) {
|
|
2930
3721
|
if (current.source) {
|
|
2931
|
-
const collections = [
|
|
3722
|
+
const collections = [
|
|
3723
|
+
file.entries.length,
|
|
3724
|
+
file.operatorDispositions?.length ?? 0,
|
|
3725
|
+
file.sourceCompletions?.length ?? 0,
|
|
3726
|
+
];
|
|
2932
3727
|
assertSourceResources(1, serializedBytes, collections[0] + collections[1] + collections[2], collections);
|
|
2933
3728
|
}
|
|
2934
3729
|
if (inputHeadroom !== false)
|
|
@@ -2937,10 +3732,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2937
3732
|
return { file, serializedBytes };
|
|
2938
3733
|
}
|
|
2939
3734
|
const previousEntries = new Map(current.file.entries.map((entry) => [entry.updateId, entry]));
|
|
2940
|
-
if (withPairingAdmission &&
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3735
|
+
if (withPairingAdmission &&
|
|
3736
|
+
entries.some((entry) => {
|
|
3737
|
+
const previous = previousEntries.get(entry.updateId);
|
|
3738
|
+
return (previous && previous.preApprovalExcluded !== entry.preApprovalExcluded);
|
|
3739
|
+
})) {
|
|
2944
3740
|
throw createJournalError("pairing-evidence", path, "changes immutable exclusion evidence");
|
|
2945
3741
|
}
|
|
2946
3742
|
const nextEntries = new Map(entries.map((entry) => [entry.updateId, entry]));
|
|
@@ -2964,7 +3760,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2964
3760
|
...(!isDeepStrictEqual(current.file.operatorDispositions ?? [], operatorDispositions ?? [])
|
|
2965
3761
|
? { operatorDispositions: operatorDispositions ?? [] }
|
|
2966
3762
|
: {}),
|
|
2967
|
-
...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? [])
|
|
3763
|
+
...(!isDeepStrictEqual(current.file.sourceCompletions ?? [], sourceCompletions ?? [])
|
|
3764
|
+
? { sourceCompletions: sourceCompletions ?? [] }
|
|
3765
|
+
: {}),
|
|
2968
3766
|
};
|
|
2969
3767
|
if (current.source) {
|
|
2970
3768
|
if (!isSafePositiveInteger(revision))
|
|
@@ -2980,7 +3778,8 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
2980
3778
|
const segmentNames = readdirSync(segmentDirectory).filter((name) => /^\d{16}\.json$/u.test(name));
|
|
2981
3779
|
let snapshotRevision = 0;
|
|
2982
3780
|
try {
|
|
2983
|
-
snapshotRevision =
|
|
3781
|
+
snapshotRevision =
|
|
3782
|
+
parseJournalFile(JSON.parse(readFileSync(path, "utf8")), path, version).revision ?? 0;
|
|
2984
3783
|
}
|
|
2985
3784
|
catch (error) {
|
|
2986
3785
|
if (error?.code !== "ENOENT")
|
|
@@ -3015,11 +3814,18 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3015
3814
|
}
|
|
3016
3815
|
return { file: revisedFile, serializedBytes };
|
|
3017
3816
|
};
|
|
3018
|
-
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();
|
|
3019
3823
|
const current = exact && !sourceAccess ? readCurrentStrict(false) : readCurrent();
|
|
3020
3824
|
if (completionInput !== undefined && !sourceAccess)
|
|
3021
3825
|
throw createJournalError("invalid", path, "source completion requires an exact source handle");
|
|
3022
|
-
const completions = completionInput === undefined
|
|
3826
|
+
const completions = completionInput === undefined
|
|
3827
|
+
? undefined
|
|
3828
|
+
: validateJournalSourceCompletions(completionInput, path, version);
|
|
3023
3829
|
if (exact && expectedSources === undefined)
|
|
3024
3830
|
throw createJournalError("invalid", path, "received no exact completion sources");
|
|
3025
3831
|
const requestedIds = new Set();
|
|
@@ -3033,133 +3839,221 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3033
3839
|
throw createJournalError("invalid", path, "received no exact completion sources");
|
|
3034
3840
|
const expectedIds = new Set();
|
|
3035
3841
|
for (const expected of expectedSources) {
|
|
3036
|
-
if (!isRecord(expected) ||
|
|
3037
|
-
|
|
3038
|
-
|
|
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)) {
|
|
3039
3849
|
throw createJournalError("invalid", path, "received an invalid exact completion source");
|
|
3040
3850
|
}
|
|
3041
3851
|
expectedIds.add(expected.updateId);
|
|
3042
|
-
const entry = current.file.entries.find(value => value.updateId === expected.updateId);
|
|
3043
|
-
if (!entry ||
|
|
3852
|
+
const entry = current.file.entries.find((value) => value.updateId === expected.updateId);
|
|
3853
|
+
if (!entry ||
|
|
3854
|
+
createTelegramUpdateJournalEntryDigest(entry).sourceSha256 !==
|
|
3855
|
+
expected.sourceSha256) {
|
|
3044
3856
|
throw createJournalError("conflict", path, "exact completion source changed or disappeared");
|
|
3045
3857
|
}
|
|
3046
3858
|
}
|
|
3047
3859
|
}
|
|
3048
3860
|
if (completions) {
|
|
3049
|
-
const guards = new Map((expectedSources ?? []).map(source => [
|
|
3050
|
-
|
|
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)) {
|
|
3051
3866
|
throw createJournalError("invalid", path, "source completion has no matching exact guard");
|
|
3052
3867
|
}
|
|
3053
3868
|
}
|
|
3054
|
-
const requestedEntries = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
|
|
3055
|
-
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");
|
|
3056
3871
|
if (protectedEntry)
|
|
3057
3872
|
throw createJournalError("conflict", path, protectedEntry.state === "failed"
|
|
3058
3873
|
? `cannot complete terminal update ${protectedEntry.updateId} without an operator disposition`
|
|
3059
3874
|
: `cannot complete queued update ${protectedEntry.updateId} without its exact owner receipt`);
|
|
3060
|
-
const removedUpdateIds = requestedEntries.map(entry => entry.updateId);
|
|
3875
|
+
const removedUpdateIds = requestedEntries.map((entry) => entry.updateId);
|
|
3061
3876
|
const sourceCompletions = completions
|
|
3062
|
-
? [...(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;
|
|
3063
3879
|
if (sourceCompletions?.length)
|
|
3064
3880
|
validateJournalSourceCompletions(sourceCompletions, path, version);
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
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
|
+
};
|
|
3068
3901
|
});
|
|
3069
3902
|
const completeQueued = (receipts, completionInput, exact = false) => runMutation((readCurrent) => {
|
|
3070
3903
|
if (exact && !sourceAccess)
|
|
3071
3904
|
throw createJournalError("invalid", path, "source completion requires an exact source handle");
|
|
3072
|
-
const completions = exact
|
|
3905
|
+
const completions = exact
|
|
3906
|
+
? validateJournalSourceCompletions(completionInput, path, version)
|
|
3907
|
+
: undefined;
|
|
3073
3908
|
if (!Array.isArray(receipts) || receipts.length === 0)
|
|
3074
3909
|
throw createJournalError("invalid", path, "received no queued receipts to complete");
|
|
3075
3910
|
const current = readCurrent();
|
|
3076
3911
|
const receiptIds = new Set(), requestedUpdateIds = new Set();
|
|
3077
3912
|
for (const receipt of receipts) {
|
|
3078
|
-
if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
|
|
3079
|
-
|
|
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) {
|
|
3080
3918
|
throw createJournalError("invalid", path, "received an invalid queued completion receipt");
|
|
3081
3919
|
}
|
|
3082
3920
|
receiptIds.add(receipt.receiptId);
|
|
3083
3921
|
const queueOwner = validateJournalQueueOwner(receipt.queueOwner, path);
|
|
3084
|
-
if (queueRuntimeIdentity &&
|
|
3085
|
-
queueOwner.
|
|
3922
|
+
if (queueRuntimeIdentity &&
|
|
3923
|
+
(queueOwner.instanceId !== queueRuntimeIdentity.instanceId ||
|
|
3924
|
+
queueOwner.processId !== queueRuntimeIdentity.processId ||
|
|
3925
|
+
queueOwner.processBirthId !== queueRuntimeIdentity.processBirthId)) {
|
|
3086
3926
|
throw createJournalError("conflict", path, `cannot complete foreign queue receipt ${receipt.receiptId}`);
|
|
3087
3927
|
}
|
|
3088
3928
|
const sourceUpdateIds = new Set();
|
|
3089
3929
|
for (const updateId of receipt.sourceUpdateIds) {
|
|
3090
|
-
if (!isSafeNonNegativeInteger(updateId) ||
|
|
3930
|
+
if (!isSafeNonNegativeInteger(updateId) ||
|
|
3931
|
+
sourceUpdateIds.has(updateId) ||
|
|
3932
|
+
requestedUpdateIds.has(updateId)) {
|
|
3091
3933
|
throw createJournalError("invalid", path, "received overlapping queued completion update ids");
|
|
3092
3934
|
}
|
|
3093
3935
|
sourceUpdateIds.add(updateId);
|
|
3094
3936
|
requestedUpdateIds.add(updateId);
|
|
3095
3937
|
}
|
|
3096
|
-
const persistedReceiptEntries = current.file.entries.filter(entry => entry.queueReceiptId === receipt.receiptId);
|
|
3097
|
-
if (persistedReceiptEntries.length !== sourceUpdateIds.size ||
|
|
3098
|
-
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))) {
|
|
3099
3946
|
throw createJournalError("conflict", path, `cannot complete stale or foreign queue receipt ${receipt.receiptId}`);
|
|
3100
3947
|
}
|
|
3101
3948
|
}
|
|
3102
|
-
const removedEntries = current.file.entries.filter(entry => requestedUpdateIds.has(entry.updateId));
|
|
3103
|
-
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);
|
|
3104
3951
|
if (removedUpdateIds.length !== requestedUpdateIds.size)
|
|
3105
3952
|
throw createJournalError("conflict", path, "queued completion did not resolve every source update");
|
|
3106
3953
|
if (completions) {
|
|
3107
|
-
const sources = new Map(removedEntries.map(entry => [
|
|
3108
|
-
|
|
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)) {
|
|
3109
3959
|
throw createJournalError("conflict", path, "queued completion source changed or belongs to another receipt");
|
|
3110
3960
|
}
|
|
3111
3961
|
}
|
|
3112
|
-
const sourceCompletions = completions
|
|
3962
|
+
const sourceCompletions = completions
|
|
3963
|
+
? [...(current.file.sourceCompletions ?? []), ...completions].sort((a, b) => a.updateId - b.updateId)
|
|
3113
3964
|
: current.file.sourceCompletions;
|
|
3114
3965
|
if (sourceCompletions?.length)
|
|
3115
3966
|
validateJournalSourceCompletions(sourceCompletions, path, version);
|
|
3116
|
-
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);
|
|
3117
|
-
return {
|
|
3118
|
-
|
|
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
|
+
};
|
|
3119
3980
|
});
|
|
3120
|
-
const mutateRoutingInputs = (input, select, publishedAtMs) => runMutation(readCurrent => {
|
|
3981
|
+
const mutateRoutingInputs = (input, select, publishedAtMs, chooser) => runMutation((readCurrent) => {
|
|
3121
3982
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(version))
|
|
3122
3983
|
throw createJournalError("unsupported-version", path, "routing input lifetime requires a private v1 source");
|
|
3123
|
-
const binding = createTelegramUpdateJournalBindingKey({
|
|
3124
|
-
|
|
3125
|
-
|
|
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) {
|
|
3126
3995
|
throw createJournalError("invalid", path, "invalid routing input lifetime authority");
|
|
3127
3996
|
}
|
|
3128
|
-
const assertCurrent = () => {
|
|
3129
|
-
|
|
3997
|
+
const assertCurrent = () => {
|
|
3998
|
+
if (!input.isCurrent())
|
|
3999
|
+
throw createJournalError("conflict", path, "routing input lifetime authority ended");
|
|
4000
|
+
};
|
|
3130
4001
|
assertCurrent();
|
|
3131
|
-
const expected = input.entries.map(entry => validateJournalEntry(entry, path, version));
|
|
3132
|
-
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)
|
|
3133
4005
|
throw createJournalError("invalid", path, "duplicate routing input sources");
|
|
3134
4006
|
const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
|
|
3135
4007
|
const nowMs = select ? getNowMs() : publishedAtMs;
|
|
3136
|
-
if (!isSafeNonNegativeInteger(nowMs) ||
|
|
4008
|
+
if (!isSafeNonNegativeInteger(nowMs) ||
|
|
4009
|
+
!Number.isSafeInteger(nowMs + TELEGRAM_ROUTING_INPUT_TTL_MS) ||
|
|
4010
|
+
(!select && nowMs > getNowMs()))
|
|
3137
4011
|
throw createJournalError("invalid", path, "invalid routing input lifetime clock");
|
|
3138
4012
|
let issued = false;
|
|
3139
4013
|
const changed = new Map();
|
|
3140
|
-
const result = expected.map(original => {
|
|
3141
|
-
const entry = current.file.entries.find(value => value.updateId === original.updateId);
|
|
3142
|
-
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) {
|
|
3143
4022
|
throw createJournalError("conflict", path, "routing input source is not unclaimed pending work");
|
|
3144
4023
|
}
|
|
3145
|
-
const withoutLifetime = (value) => {
|
|
3146
|
-
|
|
3147
|
-
|
|
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)))) {
|
|
3148
4033
|
throw createJournalError("conflict", path, "routing input source changed");
|
|
3149
4034
|
}
|
|
3150
4035
|
const lifetime = entry.routingInput;
|
|
3151
4036
|
if (lifetime && lifetime.operatorUserId !== input.operatorUserId)
|
|
3152
4037
|
throw createJournalError("conflict", path, "routing input belongs to another operator");
|
|
3153
|
-
if (createPendingRetentionReference({
|
|
4038
|
+
if (createPendingRetentionReference({
|
|
4039
|
+
path,
|
|
4040
|
+
journalBindingKey: binding,
|
|
4041
|
+
entry,
|
|
4042
|
+
maxBytes,
|
|
4043
|
+
}).read()) {
|
|
3154
4044
|
throw createJournalError("conflict", path, "routing input has protected retention");
|
|
3155
4045
|
}
|
|
3156
4046
|
if (select) {
|
|
3157
|
-
if (!lifetime ||
|
|
4047
|
+
if (!lifetime ||
|
|
4048
|
+
(lifetime.phase === "waiting" && nowMs >= lifetime.expiresAtMs))
|
|
3158
4049
|
throw createJournalError("conflict", path, "routing input choice expired or was not armed");
|
|
3159
4050
|
if (lifetime.phase === "selected")
|
|
3160
4051
|
return cloneEntry(entry);
|
|
3161
4052
|
issued = true;
|
|
3162
|
-
const selected = {
|
|
4053
|
+
const selected = {
|
|
4054
|
+
...cloneEntry(entry),
|
|
4055
|
+
routingInput: { ...lifetime, phase: "selected" },
|
|
4056
|
+
};
|
|
3163
4057
|
changed.set(entry.updateId, selected);
|
|
3164
4058
|
return cloneEntry(selected);
|
|
3165
4059
|
}
|
|
@@ -3171,16 +4065,29 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3171
4065
|
if (nowMs < entry.admittedAtMs)
|
|
3172
4066
|
throw createJournalError("conflict", path, "routing input publication predates admission");
|
|
3173
4067
|
issued = true;
|
|
3174
|
-
const armed = {
|
|
3175
|
-
|
|
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
|
+
};
|
|
3176
4078
|
changed.set(entry.updateId, armed);
|
|
3177
4079
|
return cloneEntry(armed);
|
|
3178
4080
|
});
|
|
3179
4081
|
assertCurrent();
|
|
3180
|
-
if (select &&
|
|
4082
|
+
if (select &&
|
|
4083
|
+
issued &&
|
|
4084
|
+
result.some((entry) => !changed.has(entry.updateId)))
|
|
3181
4085
|
throw createJournalError("conflict", path, "routing input selection has mixed prior phases");
|
|
3182
4086
|
if (changed.size)
|
|
3183
|
-
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
|
+
});
|
|
3184
4091
|
assertCurrent();
|
|
3185
4092
|
return { issued, entries: result };
|
|
3186
4093
|
});
|
|
@@ -3212,7 +4119,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3212
4119
|
normalizedUpdates.push(normalized);
|
|
3213
4120
|
}
|
|
3214
4121
|
const appendWithEvidence = (preApprovalExcluded) => runJournalTransaction((readCurrent) => {
|
|
3215
|
-
if (withPairingAdmission &&
|
|
4122
|
+
if (withPairingAdmission &&
|
|
4123
|
+
(typeof preApprovalExcluded !== "boolean" ||
|
|
4124
|
+
requestedAcceptedThroughUpdateId === undefined)) {
|
|
3216
4125
|
throw createJournalError("pairing-evidence", path, "requires exclusion evidence and an admission cursor");
|
|
3217
4126
|
}
|
|
3218
4127
|
if (requestedAcceptedThroughUpdateId !== undefined &&
|
|
@@ -3226,7 +4135,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3226
4135
|
...(current.file.operatorDispositions ?? [])
|
|
3227
4136
|
.filter((disposition) => disposition.action === "discard")
|
|
3228
4137
|
.map((disposition) => disposition.updateId),
|
|
3229
|
-
...(current.file.sourceCompletions ?? []).map(completion => completion.updateId),
|
|
4138
|
+
...(current.file.sourceCompletions ?? []).map((completion) => completion.updateId),
|
|
3230
4139
|
]);
|
|
3231
4140
|
let admittedAtMs;
|
|
3232
4141
|
const addedUpdateIds = [];
|
|
@@ -3244,7 +4153,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3244
4153
|
duplicateUpdateIds.push(update.update_id);
|
|
3245
4154
|
continue;
|
|
3246
4155
|
}
|
|
3247
|
-
if (withPairingAdmission &&
|
|
4156
|
+
if (withPairingAdmission &&
|
|
4157
|
+
previousAcceptedThroughUpdateId !== undefined &&
|
|
4158
|
+
update.update_id <= previousAcceptedThroughUpdateId) {
|
|
3248
4159
|
duplicateUpdateIds.push(update.update_id);
|
|
3249
4160
|
continue;
|
|
3250
4161
|
}
|
|
@@ -3257,7 +4168,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3257
4168
|
const entry = {
|
|
3258
4169
|
updateId: update.update_id,
|
|
3259
4170
|
update,
|
|
3260
|
-
...(withPairingAdmission
|
|
4171
|
+
...(withPairingAdmission
|
|
4172
|
+
? { preApprovalExcluded: preApprovalExcluded }
|
|
4173
|
+
: {}),
|
|
3261
4174
|
admittedAtMs,
|
|
3262
4175
|
state: "pending",
|
|
3263
4176
|
};
|
|
@@ -3283,7 +4196,9 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3283
4196
|
return {
|
|
3284
4197
|
nonExcludedUpdateIds: normalizedUpdates.flatMap((update) => {
|
|
3285
4198
|
const entry = entriesById.get(update.update_id);
|
|
3286
|
-
return entry && entry.preApprovalExcluded !== true
|
|
4199
|
+
return entry && entry.preApprovalExcluded !== true
|
|
4200
|
+
? [entry.updateId]
|
|
4201
|
+
: [];
|
|
3287
4202
|
}),
|
|
3288
4203
|
addedUpdateIds,
|
|
3289
4204
|
duplicateUpdateIds,
|
|
@@ -3321,12 +4236,12 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3321
4236
|
});
|
|
3322
4237
|
};
|
|
3323
4238
|
return options.withWriterAdmission
|
|
3324
|
-
? options.withWriterAdmission(admittedAppend)
|
|
4239
|
+
? options.withWriterAdmission(admittedAppend)
|
|
4240
|
+
: admittedAppend();
|
|
3325
4241
|
},
|
|
3326
4242
|
markQueued(receipt) {
|
|
3327
4243
|
return runMutation((readCurrent) => {
|
|
3328
|
-
if ((receipt.queueKind !== "prompt" &&
|
|
3329
|
-
receipt.queueKind !== "control") ||
|
|
4244
|
+
if ((receipt.queueKind !== "prompt" && receipt.queueKind !== "control") ||
|
|
3330
4245
|
!isNonEmptyString(receipt.receiptId) ||
|
|
3331
4246
|
!Array.isArray(receipt.sourceUpdateIds) ||
|
|
3332
4247
|
receipt.sourceUpdateIds.length === 0) {
|
|
@@ -3400,7 +4315,12 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3400
4315
|
updateId: entry.updateId,
|
|
3401
4316
|
update: entry.update,
|
|
3402
4317
|
admittedAtMs: entry.admittedAtMs,
|
|
3403
|
-
...(entry.preApprovalExcluded !== undefined
|
|
4318
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4319
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4320
|
+
: {}),
|
|
4321
|
+
...(entry.routingInput
|
|
4322
|
+
? { routingInput: { ...entry.routingInput } }
|
|
4323
|
+
: {}),
|
|
3404
4324
|
state: "queued",
|
|
3405
4325
|
queueKind: receipt.queueKind,
|
|
3406
4326
|
queueReceiptId: receipt.receiptId,
|
|
@@ -3430,8 +4350,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3430
4350
|
!isSafeNonNegativeInteger(input.failedAtMs) ||
|
|
3431
4351
|
!isBoundedString(input.failureClass, TELEGRAM_UPDATE_JOURNAL_FAILURE_CLASS_MAX_LENGTH) ||
|
|
3432
4352
|
!isBoundedString(input.summary, TELEGRAM_UPDATE_JOURNAL_FAILURE_SUMMARY_MAX_LENGTH) ||
|
|
3433
|
-
(input.disposition !== "retry-wait" &&
|
|
3434
|
-
input.disposition !== "failed")) {
|
|
4353
|
+
(input.disposition !== "retry-wait" && input.disposition !== "failed")) {
|
|
3435
4354
|
throw createJournalError("invalid", path, "received invalid execution failure metadata");
|
|
3436
4355
|
}
|
|
3437
4356
|
if (input.disposition === "retry-wait" &&
|
|
@@ -3477,8 +4396,12 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3477
4396
|
updateId: entry.updateId,
|
|
3478
4397
|
update: entry.update,
|
|
3479
4398
|
admittedAtMs: entry.admittedAtMs,
|
|
3480
|
-
...(entry.routingInput
|
|
3481
|
-
|
|
4399
|
+
...(entry.routingInput
|
|
4400
|
+
? { routingInput: { ...entry.routingInput } }
|
|
4401
|
+
: {}),
|
|
4402
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4403
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4404
|
+
: {}),
|
|
3482
4405
|
state: input.disposition,
|
|
3483
4406
|
failure,
|
|
3484
4407
|
...(input.disposition === "retry-wait"
|
|
@@ -3548,14 +4471,19 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3548
4471
|
updateId: entry.updateId,
|
|
3549
4472
|
update: entry.update,
|
|
3550
4473
|
admittedAtMs: entry.admittedAtMs,
|
|
3551
|
-
...(entry.preApprovalExcluded !== undefined
|
|
4474
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4475
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4476
|
+
: {}),
|
|
3552
4477
|
state: "retry-wait",
|
|
3553
4478
|
failure: entry.failure,
|
|
3554
4479
|
nextRetryAtMs: disposition.committedAtMs,
|
|
3555
4480
|
}
|
|
3556
4481
|
: candidate)
|
|
3557
4482
|
: current.file.entries.filter((candidate) => candidate.updateId !== entry.updateId);
|
|
3558
|
-
const published = publishMutation(current, nextEntries, true, [
|
|
4483
|
+
const published = publishMutation(current, nextEntries, true, [
|
|
4484
|
+
...(current.file.operatorDispositions ?? []),
|
|
4485
|
+
disposition,
|
|
4486
|
+
]);
|
|
3559
4487
|
return {
|
|
3560
4488
|
disposition: { ...disposition },
|
|
3561
4489
|
duplicate: false,
|
|
@@ -3569,30 +4497,44 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3569
4497
|
const current = readCurrent();
|
|
3570
4498
|
if (!journalHasCustodyFields(current.file.version))
|
|
3571
4499
|
throw createJournalError("conflict", path, "legacy custody disposition requires a custody journal");
|
|
3572
|
-
const existing = current.file.operatorDispositions?.find(candidate => candidate.failureId === authority.dispositionId);
|
|
4500
|
+
const existing = current.file.operatorDispositions?.find((candidate) => candidate.failureId === authority.dispositionId);
|
|
3573
4501
|
if (existing) {
|
|
3574
|
-
if (!("dispositionKind" in existing) ||
|
|
4502
|
+
if (!("dispositionKind" in existing) ||
|
|
4503
|
+
existing.dispositionKind !== "legacy-custody")
|
|
3575
4504
|
throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
|
|
3576
|
-
const normalizedDuplicate = normalizeTelegramUpdateJournalLegacyCustodyDispositionAuthority(authority, {
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
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 ||
|
|
3581
4518
|
existing.authorizedAtMs !== normalizedDuplicate.authorizedAtMs)
|
|
3582
4519
|
throw createJournalError("conflict", path, `legacy custody disposition ${authority.dispositionId} already has another authority`);
|
|
3583
4520
|
let authorized = false;
|
|
3584
4521
|
try {
|
|
3585
|
-
authorized =
|
|
4522
|
+
authorized =
|
|
4523
|
+
options.authorizeLegacyCustodyDisposition?.(normalizedDuplicate) === true;
|
|
3586
4524
|
}
|
|
3587
4525
|
catch {
|
|
3588
4526
|
authorized = false;
|
|
3589
4527
|
}
|
|
3590
4528
|
if (!authorized)
|
|
3591
4529
|
throw createJournalError("conflict", path, "legacy custody disposition is unauthorized");
|
|
3592
|
-
return {
|
|
3593
|
-
|
|
4530
|
+
return {
|
|
4531
|
+
disposition: { ...existing },
|
|
4532
|
+
duplicate: true,
|
|
4533
|
+
entryCount: current.file.entries.length,
|
|
4534
|
+
serializedBytes: current.serializedBytes,
|
|
4535
|
+
};
|
|
3594
4536
|
}
|
|
3595
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === authority.updateId);
|
|
4537
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === authority.updateId);
|
|
3596
4538
|
const evidence = entry && createTelegramUpdateJournalLegacyCustodyEvidence(entry);
|
|
3597
4539
|
if (!evidence)
|
|
3598
4540
|
throw createJournalError("conflict", path, `cannot dispose non-quarantined update ${authority.updateId}`);
|
|
@@ -3601,7 +4543,8 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3601
4543
|
throw createJournalError("conflict", path, `legacy custody evidence changed for update ${authority.updateId}`);
|
|
3602
4544
|
let authorized = false;
|
|
3603
4545
|
try {
|
|
3604
|
-
authorized =
|
|
4546
|
+
authorized =
|
|
4547
|
+
options.authorizeLegacyCustodyDisposition?.(normalized) === true;
|
|
3605
4548
|
}
|
|
3606
4549
|
catch {
|
|
3607
4550
|
authorized = false;
|
|
@@ -3612,24 +4555,38 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3612
4555
|
if (!isSafeNonNegativeInteger(nowMs))
|
|
3613
4556
|
throw createJournalError("invalid", path, "received an invalid legacy custody disposition timestamp");
|
|
3614
4557
|
const disposition = {
|
|
3615
|
-
dispositionKind: "legacy-custody",
|
|
3616
|
-
|
|
4558
|
+
dispositionKind: "legacy-custody",
|
|
4559
|
+
failureId: normalized.dispositionId,
|
|
4560
|
+
updateId: normalized.updateId,
|
|
4561
|
+
action: normalized.action,
|
|
3617
4562
|
committedAtMs: Math.max(nowMs, normalized.authorizedAtMs),
|
|
3618
4563
|
evidenceSha256: normalized.evidenceSha256,
|
|
3619
4564
|
operatorAuthorityId: normalized.operatorAuthorityId,
|
|
3620
|
-
authorizedAtMs: normalized.authorizedAtMs
|
|
4565
|
+
authorizedAtMs: normalized.authorizedAtMs,
|
|
3621
4566
|
};
|
|
3622
4567
|
const nextEntries = normalized.action === "discard"
|
|
3623
|
-
? current.file.entries.filter(candidate => candidate.updateId !== entry.updateId)
|
|
3624
|
-
: current.file.entries.map(candidate => candidate.updateId === entry.updateId
|
|
3625
|
-
? {
|
|
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,
|
|
3626
4573
|
admittedAtMs: entry.admittedAtMs,
|
|
3627
|
-
...(entry.preApprovalExcluded === undefined
|
|
3628
|
-
{
|
|
4574
|
+
...(entry.preApprovalExcluded === undefined
|
|
4575
|
+
? {}
|
|
4576
|
+
: { preApprovalExcluded: entry.preApprovalExcluded }),
|
|
4577
|
+
state: "pending",
|
|
4578
|
+
}
|
|
3629
4579
|
: candidate);
|
|
3630
|
-
const published = publishMutation(current, nextEntries, true, [
|
|
3631
|
-
|
|
3632
|
-
|
|
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
|
+
};
|
|
3633
4590
|
});
|
|
3634
4591
|
},
|
|
3635
4592
|
offerQueuedHandoff(input) {
|
|
@@ -3725,13 +4682,18 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3725
4682
|
updateId: entry.updateId,
|
|
3726
4683
|
update: entry.update,
|
|
3727
4684
|
admittedAtMs: entry.admittedAtMs,
|
|
3728
|
-
...(entry.preApprovalExcluded !== undefined
|
|
4685
|
+
...(entry.preApprovalExcluded !== undefined
|
|
4686
|
+
? { preApprovalExcluded: entry.preApprovalExcluded }
|
|
4687
|
+
: {}),
|
|
3729
4688
|
state: "queued",
|
|
3730
4689
|
queueKind: input.queueKind,
|
|
3731
4690
|
queueReceiptId: input.receiptId,
|
|
3732
4691
|
queueOwner: cloneJournalQueueOwner(queueOwner),
|
|
3733
4692
|
...(entry.inputProvenance
|
|
3734
|
-
? {
|
|
4693
|
+
? {
|
|
4694
|
+
inputProvenance: structuredClone(entry.inputProvenance),
|
|
4695
|
+
}
|
|
4696
|
+
: {}),
|
|
3735
4697
|
}
|
|
3736
4698
|
: entry), true);
|
|
3737
4699
|
return {
|
|
@@ -3777,12 +4739,15 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3777
4739
|
};
|
|
3778
4740
|
});
|
|
3779
4741
|
},
|
|
3780
|
-
completeQueued(receipts) {
|
|
3781
|
-
|
|
4742
|
+
completeQueued(receipts) {
|
|
4743
|
+
return completeQueued(receipts);
|
|
4744
|
+
},
|
|
4745
|
+
completeQueuedExact(receipts, completions) {
|
|
4746
|
+
return completeQueued(receipts, completions, true);
|
|
4747
|
+
},
|
|
3782
4748
|
discardQueued(input) {
|
|
3783
4749
|
return runMutation((readCurrent) => {
|
|
3784
|
-
if ((input.queueKind !== "prompt" &&
|
|
3785
|
-
input.queueKind !== "control") ||
|
|
4750
|
+
if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
|
|
3786
4751
|
!isNonEmptyString(input.receiptId) ||
|
|
3787
4752
|
!Array.isArray(input.sourceUpdateIds) ||
|
|
3788
4753
|
input.sourceUpdateIds.length === 0) {
|
|
@@ -3807,12 +4772,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3807
4772
|
const current = readCurrent();
|
|
3808
4773
|
const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
|
|
3809
4774
|
if (receiptEntries.length !== requestedIds.size ||
|
|
3810
|
-
receiptEntries.some((entry) => !
|
|
3811
|
-
entry.state !== "queued" ||
|
|
3812
|
-
entry.queueKind !== input.queueKind ||
|
|
3813
|
-
!entry.queueOwner ||
|
|
3814
|
-
entry.queueHandoff !== undefined ||
|
|
3815
|
-
!areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, expectedOwner))) {
|
|
4775
|
+
receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, expectedOwner))) {
|
|
3816
4776
|
throw createJournalError("conflict", path, `cannot discard stale queue receipt ${input.receiptId}`);
|
|
3817
4777
|
}
|
|
3818
4778
|
const removedUpdateIds = [...requestedIds].sort((a, b) => a - b);
|
|
@@ -3859,17 +4819,11 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3859
4819
|
}
|
|
3860
4820
|
const current = readCurrent();
|
|
3861
4821
|
const receiptEntries = current.file.entries.filter((entry) => entry.queueReceiptId === input.receiptId);
|
|
3862
|
-
if ((input.queueKind !== "prompt" &&
|
|
3863
|
-
input.queueKind !== "control") ||
|
|
4822
|
+
if ((input.queueKind !== "prompt" && input.queueKind !== "control") ||
|
|
3864
4823
|
!isNonEmptyString(input.receiptId) ||
|
|
3865
4824
|
requestedIds.size === 0 ||
|
|
3866
4825
|
receiptEntries.length !== requestedIds.size ||
|
|
3867
|
-
receiptEntries.some((entry) => !
|
|
3868
|
-
entry.state !== "queued" ||
|
|
3869
|
-
entry.queueKind !== input.queueKind ||
|
|
3870
|
-
!entry.queueOwner ||
|
|
3871
|
-
entry.queueHandoff !== undefined ||
|
|
3872
|
-
!areTelegramUpdateJournalQueueOwnersEqual(entry.queueOwner, deadOwner))) {
|
|
4826
|
+
receiptEntries.some((entry) => !isExactQueuedReceiptEntry(entry, requestedIds, input.queueKind, deadOwner))) {
|
|
3873
4827
|
throw createJournalError("conflict", path, `cannot recover stale queue receipt ${input.receiptId}`);
|
|
3874
4828
|
}
|
|
3875
4829
|
if (ownerLiveness !== "dead") {
|
|
@@ -3903,14 +4857,21 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3903
4857
|
throw createJournalError("invalid", path, "invalid abandonment update id");
|
|
3904
4858
|
// Inspection never repairs or quarantines source evidence.
|
|
3905
4859
|
const current = sourceAccess ? readCurrent() : readCurrentStrict(false);
|
|
3906
|
-
const matches = (current.file.operatorDispositions ?? []).filter(item => item.updateId === updateId);
|
|
4860
|
+
const matches = (current.file.operatorDispositions ?? []).filter((item) => item.updateId === updateId);
|
|
3907
4861
|
const disposition = matches[0];
|
|
3908
|
-
if (matches.length !== 1 ||
|
|
3909
|
-
disposition
|
|
4862
|
+
if (matches.length !== 1 ||
|
|
4863
|
+
!disposition ||
|
|
4864
|
+
!("dispositionKind" in disposition) ||
|
|
4865
|
+
disposition.dispositionKind !== "legacy-custody" ||
|
|
4866
|
+
disposition.action !== "discard" ||
|
|
3910
4867
|
disposition.failureId !== `abandon-${disposition.evidenceSha256}` ||
|
|
3911
|
-
current.file.entries.some(entry => entry.updateId === updateId))
|
|
4868
|
+
current.file.entries.some((entry) => entry.updateId === updateId))
|
|
3912
4869
|
return undefined;
|
|
3913
|
-
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4870
|
+
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4871
|
+
path,
|
|
4872
|
+
profileName: profile,
|
|
4873
|
+
botIdentity: expectedIdentity,
|
|
4874
|
+
});
|
|
3914
4875
|
const retainedPath = join(`${path}.retained`, `${disposition.failureId}.json`);
|
|
3915
4876
|
let stat;
|
|
3916
4877
|
try {
|
|
@@ -3921,20 +4882,33 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3921
4882
|
throw createJournalError("conflict", retainedPath, "committed abandonment lost its retained input");
|
|
3922
4883
|
throw error;
|
|
3923
4884
|
}
|
|
3924
|
-
if (!stat.isFile() ||
|
|
4885
|
+
if (!stat.isFile() ||
|
|
4886
|
+
stat.isSymbolicLink() ||
|
|
4887
|
+
stat.size > maxBytes + 4096) {
|
|
3925
4888
|
throw createJournalError("invalid", retainedPath, "retained input is not a bounded regular file");
|
|
3926
4889
|
}
|
|
3927
4890
|
const parsed = JSON.parse(readFileSync(retainedPath, "utf8"));
|
|
3928
4891
|
if (!isRecord(parsed))
|
|
3929
4892
|
throw createJournalError("invalid", retainedPath, "retained input is not an evidence record");
|
|
3930
4893
|
const entry = validateJournalEntry(parsed.entry, retainedPath, version);
|
|
3931
|
-
const reference = createPendingRetentionReference({
|
|
4894
|
+
const reference = createPendingRetentionReference({
|
|
4895
|
+
path,
|
|
4896
|
+
journalBindingKey,
|
|
4897
|
+
entry,
|
|
4898
|
+
maxBytes,
|
|
4899
|
+
});
|
|
3932
4900
|
const retained = reference.read();
|
|
3933
|
-
if (entry.updateId !== updateId ||
|
|
4901
|
+
if (entry.updateId !== updateId ||
|
|
4902
|
+
reference.retainedPath !== retainedPath ||
|
|
3934
4903
|
!isDeepStrictEqual(retained?.requestedDisposition, disposition)) {
|
|
3935
4904
|
throw createJournalError("conflict", retainedPath, "retained input does not match committed abandonment");
|
|
3936
4905
|
}
|
|
3937
|
-
return {
|
|
4906
|
+
return {
|
|
4907
|
+
journalBindingKey,
|
|
4908
|
+
updateId,
|
|
4909
|
+
retainedPath,
|
|
4910
|
+
operatorAuthorityId: disposition.operatorAuthorityId,
|
|
4911
|
+
};
|
|
3938
4912
|
});
|
|
3939
4913
|
},
|
|
3940
4914
|
inspectPendingRetention(entry) {
|
|
@@ -3945,11 +4919,25 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3945
4919
|
const expected = validateJournalEntry(entry, path, version);
|
|
3946
4920
|
if (expected.state !== "pending")
|
|
3947
4921
|
throw createJournalError("conflict", path, "retention inspection requires a pending source");
|
|
3948
|
-
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
3949
|
-
|
|
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
|
+
});
|
|
3950
4933
|
const retained = reference.read();
|
|
3951
|
-
return retained
|
|
3952
|
-
|
|
4934
|
+
return retained
|
|
4935
|
+
? {
|
|
4936
|
+
journalBindingKey,
|
|
4937
|
+
retainedPath: reference.retainedPath,
|
|
4938
|
+
requestedDisposition: retained.requestedDisposition,
|
|
4939
|
+
}
|
|
4940
|
+
: undefined;
|
|
3953
4941
|
});
|
|
3954
4942
|
},
|
|
3955
4943
|
abandonPending(input) {
|
|
@@ -3957,12 +4945,17 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3957
4945
|
if (!isTelegramUpdateJournalLegacyFamilyVersion(version)) {
|
|
3958
4946
|
throw createJournalError("unsupported-version", path, "pending abandonment requires the legacy v1 source");
|
|
3959
4947
|
}
|
|
3960
|
-
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4948
|
+
const journalBindingKey = createTelegramUpdateJournalBindingKey({
|
|
4949
|
+
path,
|
|
4950
|
+
profileName: profile,
|
|
4951
|
+
botIdentity: expectedIdentity,
|
|
4952
|
+
});
|
|
3961
4953
|
if (input.journalBindingKey !== journalBindingKey) {
|
|
3962
4954
|
throw createJournalError("identity-mismatch", path, "pending abandonment belongs to another source");
|
|
3963
4955
|
}
|
|
3964
4956
|
if (!isBoundedString(input.operatorAuthorityId, TELEGRAM_UPDATE_JOURNAL_FAILURE_ID_MAX_LENGTH) ||
|
|
3965
|
-
!input.operatorAuthorityId.trim() ||
|
|
4957
|
+
!input.operatorAuthorityId.trim() ||
|
|
4958
|
+
typeof input.isCurrent !== "function") {
|
|
3966
4959
|
throw createJournalError("invalid", path, "pending abandonment requires explicit operator authority");
|
|
3967
4960
|
}
|
|
3968
4961
|
const assertCurrent = () => {
|
|
@@ -3971,30 +4964,52 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
3971
4964
|
};
|
|
3972
4965
|
assertCurrent();
|
|
3973
4966
|
const expected = validateJournalEntry(input.entry, path, version);
|
|
3974
|
-
if (expected.state !== "pending" ||
|
|
3975
|
-
expected.
|
|
4967
|
+
if (expected.state !== "pending" ||
|
|
4968
|
+
expected.inputClaim ||
|
|
4969
|
+
expected.inputProvenance ||
|
|
4970
|
+
expected.queueOwner ||
|
|
4971
|
+
expected.queueHandoff ||
|
|
4972
|
+
expected.queueReceiptId ||
|
|
4973
|
+
expected.failure) {
|
|
3976
4974
|
throw createJournalError("conflict", path, "only an exact unclaimed pending source may be abandoned");
|
|
3977
4975
|
}
|
|
3978
4976
|
// Never repair/quarantine source evidence while deciding an operator cancellation.
|
|
3979
|
-
const readStrict = sourceAccess
|
|
4977
|
+
const readStrict = sourceAccess
|
|
4978
|
+
? readCurrent
|
|
4979
|
+
: () => readCurrentStrict(false);
|
|
3980
4980
|
const current = readStrict();
|
|
3981
|
-
const { evidenceSha256, failureId, retainedPath, read: readRetention } = createPendingRetentionReference({
|
|
3982
|
-
|
|
3983
|
-
|
|
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);
|
|
3984
4989
|
const assertRetention = (retained, disposition) => {
|
|
3985
|
-
if (!retained ||
|
|
4990
|
+
if (!retained ||
|
|
4991
|
+
!isDeepStrictEqual(retained.requestedDisposition, disposition)) {
|
|
3986
4992
|
throw createJournalError("conflict", retainedPath, "retained input does not match abandonment evidence");
|
|
3987
4993
|
}
|
|
3988
4994
|
};
|
|
3989
4995
|
if (existing) {
|
|
3990
|
-
if (currentEntry ||
|
|
3991
|
-
|
|
4996
|
+
if (currentEntry ||
|
|
4997
|
+
!("dispositionKind" in existing) ||
|
|
4998
|
+
existing.dispositionKind !== "legacy-custody" ||
|
|
4999
|
+
existing.failureId !== failureId ||
|
|
5000
|
+
existing.action !== "discard" ||
|
|
5001
|
+
existing.evidenceSha256 !== evidenceSha256) {
|
|
3992
5002
|
throw createJournalError("conflict", path, "pending source already has another disposition");
|
|
3993
5003
|
}
|
|
3994
5004
|
assertRetention(readRetention(), existing);
|
|
3995
5005
|
assertCurrent();
|
|
3996
|
-
return {
|
|
3997
|
-
|
|
5006
|
+
return {
|
|
5007
|
+
disposition: existing,
|
|
5008
|
+
retainedPath,
|
|
5009
|
+
duplicate: true,
|
|
5010
|
+
entryCount: current.file.entries.length,
|
|
5011
|
+
serializedBytes: current.serializedBytes,
|
|
5012
|
+
};
|
|
3998
5013
|
}
|
|
3999
5014
|
if (!currentEntry || !isDeepStrictEqual(currentEntry, expected)) {
|
|
4000
5015
|
throw createJournalError("conflict", path, "pending source changed before abandonment");
|
|
@@ -4010,11 +5025,23 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4010
5025
|
else {
|
|
4011
5026
|
// Reuse the existing legacy discard tombstone so older v1 readers also refuse replay.
|
|
4012
5027
|
// This is an operator disposition, not a fabricated execution failure or completion.
|
|
4013
|
-
disposition = {
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
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
|
+
};
|
|
4018
5045
|
writeJournalFile(retainedPath, `${JSON.stringify(copy)}\n`, (boundary, target) => {
|
|
4019
5046
|
onPublicationBoundary?.(boundary, target);
|
|
4020
5047
|
assertCurrent();
|
|
@@ -4022,12 +5049,20 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4022
5049
|
}
|
|
4023
5050
|
assertCurrent();
|
|
4024
5051
|
assertRetention(readRetention(), disposition);
|
|
4025
|
-
const committed = (snapshot) => !snapshot.file.entries.some(entry => entry.updateId === expected.updateId) &&
|
|
4026
|
-
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));
|
|
4027
5054
|
try {
|
|
4028
|
-
const published = publishMutation(current, current.file.entries.filter(entry => entry.updateId !== expected.updateId), true, [...(current.file.operatorDispositions ?? []), disposition], current.file.acceptedThroughUpdateId, (boundary, target) => {
|
|
4029
|
-
|
|
4030
|
-
|
|
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
|
+
};
|
|
4031
5066
|
}
|
|
4032
5067
|
catch (error) {
|
|
4033
5068
|
// A segment may be durable even if later snapshot compaction failed.
|
|
@@ -4042,16 +5077,26 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4042
5077
|
if (!committed(observed))
|
|
4043
5078
|
throw error;
|
|
4044
5079
|
assertRetention(readRetention(), disposition);
|
|
4045
|
-
return {
|
|
4046
|
-
|
|
5080
|
+
return {
|
|
5081
|
+
disposition,
|
|
5082
|
+
retainedPath,
|
|
5083
|
+
duplicate: false,
|
|
5084
|
+
entryCount: observed.file.entries.length,
|
|
5085
|
+
serializedBytes: observed.serializedBytes,
|
|
5086
|
+
};
|
|
4047
5087
|
}
|
|
4048
5088
|
});
|
|
4049
5089
|
},
|
|
4050
|
-
removeCompleted(updateIds) {
|
|
4051
|
-
|
|
5090
|
+
removeCompleted(updateIds) {
|
|
5091
|
+
return removeCompleted(updateIds);
|
|
5092
|
+
},
|
|
5093
|
+
removeCompletedExact(updateIds, expectedSources, completions, isCurrent) {
|
|
5094
|
+
return removeCompleted(updateIds, expectedSources, true, completions, isCurrent);
|
|
5095
|
+
},
|
|
4052
5096
|
inspectQueuedReceipt(expected) {
|
|
4053
|
-
return runMutationCore(readCurrent => {
|
|
4054
|
-
if (!sourceAccess ||
|
|
5097
|
+
return runMutationCore((readCurrent) => {
|
|
5098
|
+
if (!sourceAccess ||
|
|
5099
|
+
!isTelegramUpdateJournalLegacyFamilyVersion(version)) {
|
|
4055
5100
|
throw createJournalError("invalid", path, "queue receipt observation requires an exact legacy v1 source handle");
|
|
4056
5101
|
}
|
|
4057
5102
|
const queueOwner = validateQueuedReceiptObservation(expected, path);
|
|
@@ -4060,7 +5105,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4060
5105
|
});
|
|
4061
5106
|
},
|
|
4062
5107
|
inspectSourceCompletion(expected) {
|
|
4063
|
-
return runMutation(readCurrent => {
|
|
5108
|
+
return runMutation((readCurrent) => {
|
|
4064
5109
|
if (!sourceAccess)
|
|
4065
5110
|
throw createJournalError("invalid", path, "source completion requires an exact source handle");
|
|
4066
5111
|
const completion = validateJournalSourceCompletion(expected, path);
|
|
@@ -4069,28 +5114,128 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4069
5114
|
});
|
|
4070
5115
|
},
|
|
4071
5116
|
};
|
|
4072
|
-
const abandonRoutingPending = journal.abandonPending.bind(journal);
|
|
4073
5117
|
if (isTelegramUpdateJournalLegacyFamilyVersion(version))
|
|
4074
5118
|
journal.routingInputs = {
|
|
4075
|
-
arm(input) {
|
|
4076
|
-
|
|
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
|
+
},
|
|
5128
|
+
inspectExpiry(updateId) {
|
|
5129
|
+
return runMutation((readCurrent) => {
|
|
5130
|
+
if (!isSafeNonNegativeInteger(updateId))
|
|
5131
|
+
throw createJournalError("invalid", path, "invalid routing expiry update ID");
|
|
5132
|
+
return inspectRoutingInputExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
|
|
5133
|
+
path,
|
|
5134
|
+
profileName: profile,
|
|
5135
|
+
botIdentity: expectedIdentity,
|
|
5136
|
+
}), updateId);
|
|
5137
|
+
});
|
|
5138
|
+
},
|
|
5139
|
+
inspectGroupExpiry(updateIds) {
|
|
5140
|
+
return runMutation((readCurrent) => inspectRoutingGroupExpiry(readCurrent().file, createTelegramUpdateJournalBindingKey({
|
|
5141
|
+
path,
|
|
5142
|
+
profileName: profile,
|
|
5143
|
+
botIdentity: expectedIdentity,
|
|
5144
|
+
}), updateIds));
|
|
5145
|
+
},
|
|
4077
5146
|
expire(input) {
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
5147
|
+
return runMutation((readCurrent) => {
|
|
5148
|
+
const original = validateJournalEntry(input.entry, path, version), lifetime = original.routingInput;
|
|
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") {
|
|
5166
|
+
throw createJournalError("conflict", path, "routing expiry requires an exact unclaimed chooser source");
|
|
5167
|
+
}
|
|
5168
|
+
const assertCurrent = () => {
|
|
5169
|
+
const now = getNowMs();
|
|
5170
|
+
if (!isSafeNonNegativeInteger(now) ||
|
|
5171
|
+
now < lifetime.expiresAtMs ||
|
|
5172
|
+
!input.isCurrent())
|
|
5173
|
+
throw createJournalError("conflict", path, "routing source has not expired under current authority");
|
|
5174
|
+
};
|
|
5175
|
+
assertCurrent();
|
|
5176
|
+
const current = sourceAccess
|
|
5177
|
+
? readCurrent()
|
|
5178
|
+
: readCurrentStrict(false);
|
|
5179
|
+
const evidenceSha256 = createHash("sha256")
|
|
5180
|
+
.update(JSON.stringify(original))
|
|
5181
|
+
.digest("hex");
|
|
5182
|
+
const failureId = `routing-expiry:${evidenceSha256}`;
|
|
5183
|
+
const existing = current.file.operatorDispositions?.find((value) => value.updateId === original.updateId);
|
|
5184
|
+
const entry = current.file.entries.find((value) => value.updateId === original.updateId);
|
|
5185
|
+
if (existing) {
|
|
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}`)
|
|
5194
|
+
throw createJournalError("conflict", path, "routing source has contradictory expiry evidence");
|
|
5195
|
+
assertCurrent();
|
|
5196
|
+
return {
|
|
5197
|
+
disposition: existing,
|
|
5198
|
+
duplicate: true,
|
|
5199
|
+
entryCount: current.file.entries.length,
|
|
5200
|
+
serializedBytes: current.serializedBytes,
|
|
5201
|
+
};
|
|
5202
|
+
}
|
|
5203
|
+
if (!entry || !isDeepStrictEqual(entry, original))
|
|
5204
|
+
throw createJournalError("conflict", path, "routing source changed before expiry");
|
|
5205
|
+
// A discard tombstone prevents old readers and delayed reports from replaying; no prompt-body archive is written.
|
|
5206
|
+
const now = getNowMs();
|
|
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
|
+
});
|
|
5221
|
+
assertCurrent();
|
|
5222
|
+
return {
|
|
5223
|
+
disposition,
|
|
5224
|
+
duplicate: false,
|
|
5225
|
+
entryCount: published.file.entries.length,
|
|
5226
|
+
serializedBytes: published.serializedBytes,
|
|
5227
|
+
};
|
|
5228
|
+
});
|
|
4088
5229
|
},
|
|
4089
5230
|
};
|
|
4090
5231
|
if (!getInputContext)
|
|
4091
5232
|
return { journal };
|
|
4092
5233
|
// Strict source acquisition already requires this exact stored receipt scope.
|
|
4093
|
-
const bindingKey = createTelegramUpdateJournalBindingKey({
|
|
5234
|
+
const bindingKey = createTelegramUpdateJournalBindingKey({
|
|
5235
|
+
path,
|
|
5236
|
+
profileName: profile,
|
|
5237
|
+
botIdentity: expectedIdentity,
|
|
5238
|
+
});
|
|
4094
5239
|
const createInputSourceReference = (updateId) => ({
|
|
4095
5240
|
journalBindingKey: bindingKey,
|
|
4096
5241
|
tokenSha256: expectedIdentity.tokenSha256,
|
|
@@ -4123,10 +5268,15 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4123
5268
|
assertInputProcess(owner);
|
|
4124
5269
|
return { owner, recipientBindingKey };
|
|
4125
5270
|
};
|
|
4126
|
-
const runInputAdmission = (kind, operation) => workspaceAdmission
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
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();
|
|
4130
5280
|
const runInputMutation = (kind, operation) => runInputAdmission(kind, () => runMutation(operation));
|
|
4131
5281
|
const publishInputMutation = (current, entries, updateId, context) => publishMutation(current, entries, true, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, (boundary, publicationPath) => {
|
|
4132
5282
|
onPublicationBoundary?.(boundary, publicationPath);
|
|
@@ -4136,33 +5286,49 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4136
5286
|
}, updateId);
|
|
4137
5287
|
const publishInputSettlement = (current, entries, contentChanged) => publishMutation(current, entries, contentChanged, current.file.operatorDispositions, current.file.acceptedThroughUpdateId, onPublicationBoundary, false);
|
|
4138
5288
|
const normalizeInputSourceReference = (value, operation) => {
|
|
4139
|
-
if (!isRecord(value) ||
|
|
5289
|
+
if (!isRecord(value) ||
|
|
5290
|
+
!hasOnlyKeys(value, ["journalBindingKey", "tokenSha256", "updateId"])) {
|
|
4140
5291
|
throw createJournalError("invalid", path, `received invalid input handoff ${operation} source`);
|
|
4141
5292
|
}
|
|
4142
5293
|
const journalBindingKey = value.journalBindingKey;
|
|
4143
5294
|
const updateId = value.updateId;
|
|
4144
|
-
if (journalBindingKey !== bindingKey ||
|
|
5295
|
+
if (journalBindingKey !== bindingKey ||
|
|
5296
|
+
value.tokenSha256 !== expectedIdentity.tokenSha256 ||
|
|
4145
5297
|
!isSafeNonNegativeInteger(updateId)) {
|
|
4146
5298
|
throw createJournalError("conflict", path, "input receipt does not match its source identity");
|
|
4147
5299
|
}
|
|
4148
|
-
return {
|
|
5300
|
+
return {
|
|
5301
|
+
journalBindingKey,
|
|
5302
|
+
tokenSha256: expectedIdentity.tokenSha256,
|
|
5303
|
+
updateId,
|
|
5304
|
+
};
|
|
4149
5305
|
};
|
|
4150
5306
|
const normalizeInputReceipt = (value, requireCurrentProcess = true) => {
|
|
4151
5307
|
if (!isRecord(value) ||
|
|
4152
|
-
!hasOnlyKeys(value, [
|
|
5308
|
+
!hasOnlyKeys(value, [
|
|
5309
|
+
"journalBindingKey",
|
|
5310
|
+
"tokenSha256",
|
|
5311
|
+
"updateId",
|
|
5312
|
+
"owner",
|
|
5313
|
+
])) {
|
|
4153
5314
|
throw createJournalError("invalid", path, "received an invalid input receipt");
|
|
4154
5315
|
}
|
|
4155
|
-
const source = normalizeInputSourceReference({
|
|
4156
|
-
|
|
5316
|
+
const source = normalizeInputSourceReference({
|
|
5317
|
+
journalBindingKey: value.journalBindingKey,
|
|
5318
|
+
tokenSha256: value.tokenSha256,
|
|
5319
|
+
updateId: value.updateId,
|
|
5320
|
+
}, "receipt");
|
|
4157
5321
|
const owner = validateJournalQueueOwner(value.owner, path);
|
|
4158
5322
|
if (requireCurrentProcess)
|
|
4159
5323
|
assertInputProcess(owner);
|
|
4160
5324
|
return { ...source, owner };
|
|
4161
5325
|
};
|
|
4162
5326
|
const normalizeInputHandoffOffer = (value) => {
|
|
4163
|
-
if (!isRecord(value) ||
|
|
5327
|
+
if (!isRecord(value) ||
|
|
5328
|
+
!hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffToken"]) ||
|
|
4164
5329
|
!isBoundedString(value.handoffToken, TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MAX_LENGTH) ||
|
|
4165
|
-
value.handoffToken.length <
|
|
5330
|
+
value.handoffToken.length <
|
|
5331
|
+
TELEGRAM_UPDATE_JOURNAL_QUEUE_HANDOFF_TOKEN_MIN_LENGTH) {
|
|
4166
5332
|
throw createJournalError("invalid", path, "received invalid input handoff offer");
|
|
4167
5333
|
}
|
|
4168
5334
|
const receipt = normalizeInputReceipt(value.receipt);
|
|
@@ -4179,52 +5345,81 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4179
5345
|
return value;
|
|
4180
5346
|
};
|
|
4181
5347
|
const normalizeInputHandoffAccept = (value) => {
|
|
4182
|
-
if (!isRecord(value) ||
|
|
5348
|
+
if (!isRecord(value) ||
|
|
5349
|
+
!hasOnlyKeys(value, ["source", "recipientOwner", "handoffId"])) {
|
|
4183
5350
|
throw createJournalError("invalid", path, "received invalid input handoff accept");
|
|
4184
5351
|
}
|
|
4185
5352
|
const source = normalizeInputSourceReference(value.source, "accept");
|
|
4186
5353
|
const recipientOwner = validateJournalQueueOwnerIdentity(value.recipientOwner, path);
|
|
4187
5354
|
assertInputProcess(recipientOwner);
|
|
4188
|
-
return {
|
|
5355
|
+
return {
|
|
5356
|
+
source,
|
|
5357
|
+
recipientOwner,
|
|
5358
|
+
handoffId: normalizeInputHandoffId(value.handoffId, "accept"),
|
|
5359
|
+
};
|
|
4189
5360
|
};
|
|
4190
5361
|
const normalizeInputHandoffCancel = (value) => {
|
|
4191
|
-
if (!isRecord(value) ||
|
|
5362
|
+
if (!isRecord(value) ||
|
|
5363
|
+
!hasOnlyKeys(value, ["receipt", "recipientOwner", "handoffId"])) {
|
|
4192
5364
|
throw createJournalError("invalid", path, "received invalid input handoff cancel");
|
|
4193
5365
|
}
|
|
4194
|
-
return {
|
|
5366
|
+
return {
|
|
5367
|
+
receipt: normalizeInputReceipt(value.receipt),
|
|
4195
5368
|
recipientOwner: validateJournalQueueOwnerIdentity(value.recipientOwner, path),
|
|
4196
|
-
handoffId: normalizeInputHandoffId(value.handoffId, "cancel")
|
|
5369
|
+
handoffId: normalizeInputHandoffId(value.handoffId, "cancel"),
|
|
5370
|
+
};
|
|
4197
5371
|
};
|
|
4198
5372
|
const normalizeInputQueue = (value) => {
|
|
4199
|
-
if (!isRecord(value) ||
|
|
5373
|
+
if (!isRecord(value) ||
|
|
5374
|
+
!hasOnlyKeys(value, ["queueKind", "receiptId", "receipts"]) ||
|
|
4200
5375
|
(value.queueKind !== "prompt" && value.queueKind !== "control") ||
|
|
4201
5376
|
!isBoundedString(value.receiptId, TELEGRAM_UPDATE_JOURNAL_QUEUE_OWNER_ID_MAX_LENGTH) ||
|
|
4202
|
-
!value.receiptId.trim() ||
|
|
5377
|
+
!value.receiptId.trim() ||
|
|
5378
|
+
!Array.isArray(value.receipts) ||
|
|
5379
|
+
value.receipts.length === 0) {
|
|
4203
5380
|
throw createJournalError("invalid", path, "received invalid input queue transition");
|
|
4204
5381
|
}
|
|
4205
|
-
const receipts = value.receipts
|
|
5382
|
+
const receipts = value.receipts
|
|
5383
|
+
.map((receipt) => normalizeInputReceipt(receipt))
|
|
5384
|
+
.sort((left, right) => left.updateId - right.updateId);
|
|
4206
5385
|
if (receipts.some((receipt, index) => index > 0 && receipt.updateId === receipts[index - 1].updateId)) {
|
|
4207
5386
|
throw createJournalError("invalid", path, "received duplicate input queue transition receipts");
|
|
4208
5387
|
}
|
|
4209
5388
|
return { queueKind: value.queueKind, receiptId: value.receiptId, receipts };
|
|
4210
5389
|
};
|
|
4211
5390
|
const ownedInput = (current, receipt) => {
|
|
4212
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
|
|
4213
|
-
if (!entry?.inputClaim ||
|
|
5391
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === receipt.updateId);
|
|
5392
|
+
if (!entry?.inputClaim ||
|
|
5393
|
+
entry.state !== "pending" ||
|
|
4214
5394
|
!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner)) {
|
|
4215
5395
|
throw createJournalError("conflict", path, "input receipt lost its exact acquisition or phase");
|
|
4216
5396
|
}
|
|
4217
5397
|
return entry;
|
|
4218
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
|
+
};
|
|
4219
5411
|
const getReadyInputRelease = (current, receipt, allowOffered = false) => {
|
|
4220
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
|
|
4221
|
-
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) {
|
|
4222
5416
|
throw createJournalError("conflict", path, "input is unavailable for ready-claim release");
|
|
4223
5417
|
}
|
|
4224
5418
|
if (!entry.inputClaim)
|
|
4225
5419
|
return { entry, unclaimed: true };
|
|
4226
5420
|
if (!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
|
|
4227
|
-
entry.inputClaim.phase !== "ready" ||
|
|
5421
|
+
entry.inputClaim.phase !== "ready" ||
|
|
5422
|
+
(!allowOffered && entry.inputClaim.handoff !== undefined)) {
|
|
4228
5423
|
throw createJournalError("conflict", path, "cannot release stale, running, or offered input authority");
|
|
4229
5424
|
}
|
|
4230
5425
|
return { entry, unclaimed: false };
|
|
@@ -4232,7 +5427,7 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4232
5427
|
const publishReadyInputRelease = (current, entry) => {
|
|
4233
5428
|
const released = cloneEntry(entry);
|
|
4234
5429
|
delete released.inputClaim;
|
|
4235
|
-
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);
|
|
4236
5431
|
};
|
|
4237
5432
|
const input = {
|
|
4238
5433
|
read: journal.read,
|
|
@@ -4240,12 +5435,16 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4240
5435
|
listLegacyCustodyCandidates() {
|
|
4241
5436
|
const sourceAccess = options.sourceAccess;
|
|
4242
5437
|
const evidence = readTelegramUpdateJournalSource({
|
|
4243
|
-
directory: sourceAccess.directory,
|
|
4244
|
-
|
|
5438
|
+
directory: sourceAccess.directory,
|
|
5439
|
+
path,
|
|
5440
|
+
profile,
|
|
5441
|
+
botIdentity: expectedIdentity,
|
|
5442
|
+
limits: sourceAccess.limits,
|
|
4245
5443
|
version,
|
|
4246
5444
|
});
|
|
4247
5445
|
return evidence.kind === "present"
|
|
4248
|
-
? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file)
|
|
5446
|
+
? listTelegramUpdateJournalLegacyCustodyCandidates(evidence.file)
|
|
5447
|
+
: [];
|
|
4249
5448
|
},
|
|
4250
5449
|
applyLegacyCustodyDisposition(value) {
|
|
4251
5450
|
return runInputAdmission("legacy-custody-disposition", () => journal.applyLegacyCustodyDisposition(value));
|
|
@@ -4276,117 +5475,163 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4276
5475
|
}
|
|
4277
5476
|
requestedIds.add(updateId);
|
|
4278
5477
|
}
|
|
4279
|
-
return runInputMutation("remove-excluded", read => {
|
|
5478
|
+
return runInputMutation("remove-excluded", (read) => {
|
|
4280
5479
|
const current = read();
|
|
4281
|
-
if (!current.exists ||
|
|
5480
|
+
if (!current.exists ||
|
|
5481
|
+
[...requestedIds].some((id) => id > (current.file.acceptedThroughUpdateId ?? -1))) {
|
|
4282
5482
|
throw createJournalError("conflict", path, "exclusion removal lacks retained source admission evidence");
|
|
4283
5483
|
}
|
|
4284
|
-
const selected = current.file.entries.filter(entry => requestedIds.has(entry.updateId));
|
|
4285
|
-
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)) {
|
|
4286
5486
|
throw createJournalError("conflict", path, "cannot remove input without immutable exclusion evidence");
|
|
4287
5487
|
}
|
|
4288
5488
|
// Strict v3 decoding already excludes raw claims and queued authority on vetoed entries.
|
|
4289
|
-
const removedUpdateIds = selected.map(entry => entry.updateId);
|
|
4290
|
-
const published = publishInputSettlement(current, current.file.entries.filter(entry => !requestedIds.has(entry.updateId)), removedUpdateIds.length > 0);
|
|
4291
|
-
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
|
+
};
|
|
4292
5496
|
});
|
|
4293
5497
|
},
|
|
4294
5498
|
releaseInput(value) {
|
|
4295
5499
|
const receipt = normalizeInputReceipt(value);
|
|
4296
|
-
return runInputMutation("release", read => {
|
|
5500
|
+
return runInputMutation("release", (read) => {
|
|
4297
5501
|
const current = read();
|
|
4298
5502
|
const release = getReadyInputRelease(current, receipt);
|
|
4299
5503
|
if (release.unclaimed)
|
|
4300
|
-
return {
|
|
4301
|
-
|
|
5504
|
+
return {
|
|
5505
|
+
released: false,
|
|
5506
|
+
entryCount: current.file.entries.length,
|
|
5507
|
+
serializedBytes: current.serializedBytes,
|
|
5508
|
+
};
|
|
4302
5509
|
const published = publishReadyInputRelease(current, release.entry);
|
|
4303
|
-
return {
|
|
4304
|
-
|
|
5510
|
+
return {
|
|
5511
|
+
released: true,
|
|
5512
|
+
entryCount: published.file.entries.length,
|
|
5513
|
+
serializedBytes: published.serializedBytes,
|
|
5514
|
+
};
|
|
4305
5515
|
});
|
|
4306
5516
|
},
|
|
4307
5517
|
recoverReadyInput(value) {
|
|
4308
|
-
if (!isRecord(value) ||
|
|
5518
|
+
if (!isRecord(value) ||
|
|
5519
|
+
!hasOnlyKeys(value, ["receipt", "recoveryOwner"])) {
|
|
4309
5520
|
throw createJournalError("invalid", path, "received invalid ready-input recovery authority");
|
|
4310
5521
|
}
|
|
4311
5522
|
const receipt = normalizeInputReceipt(value.receipt, false);
|
|
4312
5523
|
const recoveryOwner = validateJournalQueueOwnerIdentity(value.recoveryOwner, path);
|
|
4313
5524
|
assertInputProcess(recoveryOwner);
|
|
4314
|
-
return runInputMutation("recover-ready", read => {
|
|
5525
|
+
return runInputMutation("recover-ready", (read) => {
|
|
4315
5526
|
const current = read();
|
|
4316
5527
|
const release = getReadyInputRelease(current, receipt, true);
|
|
4317
5528
|
if (release.unclaimed)
|
|
4318
|
-
return {
|
|
4319
|
-
|
|
5529
|
+
return {
|
|
5530
|
+
status: "unclaimed",
|
|
5531
|
+
entryCount: current.file.entries.length,
|
|
5532
|
+
serializedBytes: current.serializedBytes,
|
|
5533
|
+
};
|
|
4320
5534
|
let liveness;
|
|
4321
5535
|
try {
|
|
4322
|
-
liveness = getQueueProcessLiveness({
|
|
4323
|
-
|
|
5536
|
+
liveness = getQueueProcessLiveness({
|
|
5537
|
+
processId: receipt.owner.processId,
|
|
5538
|
+
processBirthId: receipt.owner.processBirthId,
|
|
5539
|
+
});
|
|
4324
5540
|
}
|
|
4325
5541
|
catch (error) {
|
|
4326
5542
|
throw createJournalError("io", path, "could not prove ready input owner liveness", error);
|
|
4327
5543
|
}
|
|
4328
5544
|
if (liveness !== "dead")
|
|
4329
|
-
return {
|
|
4330
|
-
|
|
5545
|
+
return {
|
|
5546
|
+
status: liveness === "alive"
|
|
5547
|
+
? "owner-alive"
|
|
5548
|
+
: "owner-unverifiable",
|
|
5549
|
+
entryCount: current.file.entries.length,
|
|
5550
|
+
serializedBytes: current.serializedBytes,
|
|
5551
|
+
};
|
|
4331
5552
|
const published = publishReadyInputRelease(current, release.entry);
|
|
4332
|
-
return {
|
|
4333
|
-
|
|
5553
|
+
return {
|
|
5554
|
+
status: "recovered",
|
|
5555
|
+
entryCount: published.file.entries.length,
|
|
5556
|
+
serializedBytes: published.serializedBytes,
|
|
5557
|
+
};
|
|
4334
5558
|
});
|
|
4335
5559
|
},
|
|
4336
5560
|
offerInputHandoff(value) {
|
|
4337
5561
|
const { receipt, recipientOwner, handoffToken } = normalizeInputHandoffOffer(value);
|
|
4338
|
-
return runInputMutation("offer", read => {
|
|
4339
|
-
const context =
|
|
4340
|
-
const current = read();
|
|
4341
|
-
const entry = ownedInput(current, receipt);
|
|
4342
|
-
const claim = entry.inputClaim;
|
|
4343
|
-
if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
|
|
4344
|
-
claim.recipientBindingKey !== context.recipientBindingKey) {
|
|
4345
|
-
throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
|
|
4346
|
-
}
|
|
5562
|
+
return runInputMutation("offer", (read) => {
|
|
5563
|
+
const { context, current, claim } = readReadyDonorInput(read(), receipt);
|
|
4347
5564
|
if (claim.handoff) {
|
|
4348
5565
|
if (!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
|
|
4349
5566
|
throw createJournalError("conflict", path, "input already has another handoff offer");
|
|
4350
5567
|
}
|
|
4351
|
-
return {
|
|
5568
|
+
return {
|
|
5569
|
+
source: createInputSourceReference(receipt.updateId),
|
|
4352
5570
|
handoff: cloneJournalQueueHandoff(claim.handoff),
|
|
4353
|
-
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
4354
|
-
|
|
5571
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5572
|
+
duplicate: true,
|
|
5573
|
+
entryCount: current.file.entries.length,
|
|
5574
|
+
serializedBytes: current.serializedBytes,
|
|
5575
|
+
};
|
|
4355
5576
|
}
|
|
4356
|
-
const handoffId = createTelegramInputHandoffId({
|
|
4357
|
-
|
|
4358
|
-
|
|
5577
|
+
const handoffId = createTelegramInputHandoffId({
|
|
5578
|
+
handoffToken,
|
|
5579
|
+
journalBindingKey: bindingKey,
|
|
5580
|
+
updateId: receipt.updateId,
|
|
5581
|
+
donorOwner: receipt.owner,
|
|
5582
|
+
recipientOwner,
|
|
5583
|
+
recipientBindingKey: claim.recipientBindingKey,
|
|
5584
|
+
});
|
|
4359
5585
|
const offeredAtMs = getNowMs();
|
|
4360
5586
|
if (!isSafeNonNegativeInteger(offeredAtMs)) {
|
|
4361
5587
|
throw createJournalError("invalid", path, "received an invalid input handoff offer timestamp");
|
|
4362
5588
|
}
|
|
4363
|
-
const handoff = {
|
|
4364
|
-
|
|
5589
|
+
const handoff = {
|
|
5590
|
+
handoffId,
|
|
5591
|
+
offeredAtMs,
|
|
5592
|
+
recipientOwner: { ...recipientOwner },
|
|
5593
|
+
};
|
|
4365
5594
|
const offeredClaim = { ...claim, handoff };
|
|
4366
|
-
const published = publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId
|
|
4367
|
-
|
|
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),
|
|
4368
5600
|
handoff: cloneJournalQueueHandoff(handoff),
|
|
4369
|
-
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
4370
|
-
|
|
5601
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5602
|
+
duplicate: false,
|
|
5603
|
+
entryCount: published.file.entries.length,
|
|
5604
|
+
serializedBytes: published.serializedBytes,
|
|
5605
|
+
};
|
|
4371
5606
|
});
|
|
4372
5607
|
},
|
|
4373
5608
|
acceptInputHandoff(value) {
|
|
4374
5609
|
const { source, recipientOwner, handoffId } = normalizeInputHandoffAccept(value);
|
|
4375
|
-
return runInputMutation("accept", read => {
|
|
5610
|
+
return runInputMutation("accept", (read) => {
|
|
4376
5611
|
const context = currentInputContext();
|
|
4377
5612
|
const current = read();
|
|
4378
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === source.updateId);
|
|
5613
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === source.updateId);
|
|
4379
5614
|
const claim = entry?.inputClaim;
|
|
4380
|
-
if (!entry ||
|
|
5615
|
+
if (!entry ||
|
|
5616
|
+
entry.state !== "pending" ||
|
|
5617
|
+
entry.preApprovalExcluded !== false ||
|
|
5618
|
+
!claim ||
|
|
4381
5619
|
!isDeepStrictEqual(context.owner, recipientOwner) ||
|
|
4382
5620
|
context.recipientBindingKey !== claim.recipientBindingKey) {
|
|
4383
5621
|
throw createJournalError("conflict", path, "input handoff recipient authority is unavailable or changed");
|
|
4384
5622
|
}
|
|
4385
|
-
if (claim.owner.handoffId === handoffId &&
|
|
4386
|
-
|
|
4387
|
-
|
|
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
|
+
};
|
|
4388
5632
|
}
|
|
4389
|
-
if (claim.phase !== "ready" ||
|
|
5633
|
+
if (claim.phase !== "ready" ||
|
|
5634
|
+
claim.handoff?.handoffId !== handoffId ||
|
|
4390
5635
|
!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
|
|
4391
5636
|
throw createJournalError("conflict", path, "cannot accept stale or unauthenticated input handoff");
|
|
4392
5637
|
}
|
|
@@ -4395,75 +5640,109 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4395
5640
|
throw createJournalError("invalid", path, "received an invalid input handoff acquisition timestamp");
|
|
4396
5641
|
}
|
|
4397
5642
|
const previousOwner = cloneJournalQueueOwner(claim.owner);
|
|
4398
|
-
const owner = {
|
|
4399
|
-
|
|
5643
|
+
const owner = {
|
|
5644
|
+
...recipientOwner,
|
|
5645
|
+
acquisitionId: randomUUID(),
|
|
5646
|
+
acquiredAtMs,
|
|
5647
|
+
handoffId,
|
|
5648
|
+
};
|
|
4400
5649
|
const acceptedClaim = structuredClone(claim);
|
|
4401
5650
|
acceptedClaim.owner = owner;
|
|
4402
5651
|
delete acceptedClaim.handoff;
|
|
4403
|
-
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);
|
|
4404
5655
|
const published = publishInputMutation(current, publishedEntries, source.updateId, context);
|
|
4405
|
-
return {
|
|
4406
|
-
|
|
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
|
+
};
|
|
4407
5664
|
});
|
|
4408
5665
|
},
|
|
4409
5666
|
cancelInputHandoff(value) {
|
|
4410
5667
|
const { receipt, recipientOwner, handoffId } = normalizeInputHandoffCancel(value);
|
|
4411
|
-
return runInputMutation("cancel", read => {
|
|
4412
|
-
const context =
|
|
4413
|
-
const current = read();
|
|
4414
|
-
const entry = ownedInput(current, receipt);
|
|
4415
|
-
const claim = entry.inputClaim;
|
|
4416
|
-
if (claim.phase !== "ready" || !inputOwnerMatchesIdentity(receipt.owner, context.owner) ||
|
|
4417
|
-
claim.recipientBindingKey !== context.recipientBindingKey) {
|
|
4418
|
-
throw createJournalError("conflict", path, "input handoff donor authority is stale or running");
|
|
4419
|
-
}
|
|
5668
|
+
return runInputMutation("cancel", (read) => {
|
|
5669
|
+
const { context, current, claim } = readReadyDonorInput(read(), receipt);
|
|
4420
5670
|
if (!claim.handoff)
|
|
4421
|
-
return {
|
|
4422
|
-
|
|
5671
|
+
return {
|
|
5672
|
+
handoffId,
|
|
5673
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5674
|
+
cancelled: false,
|
|
5675
|
+
entryCount: current.file.entries.length,
|
|
5676
|
+
serializedBytes: current.serializedBytes,
|
|
5677
|
+
};
|
|
4423
5678
|
if (claim.handoff.handoffId !== handoffId ||
|
|
4424
5679
|
!isDeepStrictEqual(claim.handoff.recipientOwner, recipientOwner)) {
|
|
4425
5680
|
throw createJournalError("conflict", path, "cannot cancel another input handoff offer");
|
|
4426
5681
|
}
|
|
4427
5682
|
const cancelledClaim = structuredClone(claim);
|
|
4428
5683
|
delete cancelledClaim.handoff;
|
|
4429
|
-
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);
|
|
4430
5687
|
const published = publishInputMutation(current, publishedEntries, receipt.updateId, context);
|
|
4431
|
-
return {
|
|
4432
|
-
|
|
5688
|
+
return {
|
|
5689
|
+
handoffId,
|
|
5690
|
+
previousOwner: cloneJournalQueueOwner(receipt.owner),
|
|
5691
|
+
cancelled: true,
|
|
5692
|
+
entryCount: published.file.entries.length,
|
|
5693
|
+
serializedBytes: published.serializedBytes,
|
|
5694
|
+
};
|
|
4433
5695
|
});
|
|
4434
5696
|
},
|
|
4435
5697
|
queueInputs(value) {
|
|
4436
5698
|
const { queueKind, receiptId, receipts } = normalizeInputQueue(value);
|
|
4437
|
-
return runInputMutation("queue", read => {
|
|
5699
|
+
return runInputMutation("queue", (read) => {
|
|
4438
5700
|
const context = currentInputContext();
|
|
4439
|
-
if (receipts.some(receipt => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
|
|
5701
|
+
if (receipts.some((receipt) => !inputOwnerMatchesIdentity(receipt.owner, context.owner))) {
|
|
4440
5702
|
throw createJournalError("conflict", path, "input queue transition belongs to another session");
|
|
4441
5703
|
}
|
|
4442
5704
|
const current = read();
|
|
4443
|
-
const receiptsById = new Map(receipts.map(receipt => [receipt.updateId, receipt]));
|
|
4444
|
-
const requestedIds = receipts.map(receipt => receipt.updateId);
|
|
4445
|
-
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);
|
|
4446
5708
|
if (existingReceiptEntries.length > 0) {
|
|
4447
5709
|
const queueOwner = existingReceiptEntries[0]?.queueOwner;
|
|
4448
|
-
if (existingReceiptEntries.length !== receipts.length ||
|
|
5710
|
+
if (existingReceiptEntries.length !== receipts.length ||
|
|
5711
|
+
!queueOwner ||
|
|
4449
5712
|
!inputOwnerMatchesIdentity(queueOwner, context.owner) ||
|
|
4450
|
-
existingReceiptEntries.some(entry => {
|
|
5713
|
+
existingReceiptEntries.some((entry) => {
|
|
4451
5714
|
const receipt = receiptsById.get(entry.updateId);
|
|
4452
|
-
return !receipt ||
|
|
4453
|
-
entry.
|
|
5715
|
+
return (!receipt ||
|
|
5716
|
+
entry.state !== "queued" ||
|
|
5717
|
+
entry.queueKind !== queueKind ||
|
|
5718
|
+
entry.queueHandoff !== undefined ||
|
|
5719
|
+
!entry.inputProvenance ||
|
|
4454
5720
|
!areTelegramUpdateJournalQueueOwnersEqual(entry.inputProvenance.owner, receipt.owner) ||
|
|
4455
|
-
entry.inputProvenance.recipientBindingKey !==
|
|
5721
|
+
entry.inputProvenance.recipientBindingKey !==
|
|
5722
|
+
context.recipientBindingKey);
|
|
4456
5723
|
})) {
|
|
4457
5724
|
throw createJournalError("conflict", path, "input queue transition conflicts with queued authority");
|
|
4458
5725
|
}
|
|
4459
|
-
return {
|
|
4460
|
-
|
|
4461
|
-
|
|
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
|
+
};
|
|
4462
5737
|
}
|
|
4463
5738
|
for (const receipt of receipts) {
|
|
4464
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === receipt.updateId);
|
|
4465
|
-
if (!entry ||
|
|
4466
|
-
|
|
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 ||
|
|
4467
5746
|
!areTelegramUpdateJournalQueueOwnersEqual(entry.inputClaim.owner, receipt.owner) ||
|
|
4468
5747
|
entry.inputClaim.recipientBindingKey !== context.recipientBindingKey) {
|
|
4469
5748
|
throw createJournalError("conflict", path, "input queue transition lost exact running authority");
|
|
@@ -4473,76 +5752,122 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4473
5752
|
if (!isSafeNonNegativeInteger(acquiredAtMs)) {
|
|
4474
5753
|
throw createJournalError("invalid", path, "received an invalid input queue acquisition timestamp");
|
|
4475
5754
|
}
|
|
4476
|
-
const queueOwner = {
|
|
4477
|
-
|
|
4478
|
-
|
|
5755
|
+
const queueOwner = {
|
|
5756
|
+
...context.owner,
|
|
5757
|
+
acquisitionId: randomUUID(),
|
|
5758
|
+
acquiredAtMs,
|
|
5759
|
+
};
|
|
5760
|
+
const queuedEntries = current.file.entries.map((entry) => {
|
|
4479
5761
|
const receipt = receiptsById.get(entry.updateId);
|
|
4480
5762
|
if (!receipt)
|
|
4481
5763
|
return entry;
|
|
4482
5764
|
const claim = entry.inputClaim;
|
|
4483
|
-
return {
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
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),
|
|
4487
5776
|
recipientBindingKey: claim.recipientBindingKey,
|
|
4488
5777
|
...(claim.executionUpdate
|
|
4489
|
-
? { executionUpdate: structuredClone(claim.executionUpdate) }
|
|
5778
|
+
? { executionUpdate: structuredClone(claim.executionUpdate) }
|
|
5779
|
+
: {}),
|
|
5780
|
+
},
|
|
5781
|
+
};
|
|
4490
5782
|
});
|
|
4491
5783
|
const published = publishInputMutation(current, queuedEntries, requestedIds, context);
|
|
4492
|
-
return {
|
|
4493
|
-
|
|
4494
|
-
|
|
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
|
+
};
|
|
4495
5795
|
});
|
|
4496
5796
|
},
|
|
4497
5797
|
acquireInput(value) {
|
|
4498
5798
|
const updateId = value.updateId;
|
|
4499
5799
|
const recipientBindingKey = value.recipientBindingKey;
|
|
4500
|
-
const executionUpdate = value.executionUpdate === undefined
|
|
5800
|
+
const executionUpdate = value.executionUpdate === undefined
|
|
5801
|
+
? undefined
|
|
5802
|
+
: normalizeIncomingJournaledUpdate(value.executionUpdate, path);
|
|
4501
5803
|
if (!isSafeNonNegativeInteger(updateId) ||
|
|
4502
5804
|
!isBoundedString(recipientBindingKey, TELEGRAM_UPDATE_JOURNAL_INPUT_BINDING_MAX_LENGTH) ||
|
|
4503
|
-
!recipientBindingKey.trim() ||
|
|
5805
|
+
!recipientBindingKey.trim() ||
|
|
5806
|
+
(executionUpdate && executionUpdate.update_id !== updateId)) {
|
|
4504
5807
|
throw createJournalError("invalid", path, "received invalid input acquisition identity");
|
|
4505
5808
|
}
|
|
4506
|
-
return runInputMutation("acquire", read => {
|
|
5809
|
+
return runInputMutation("acquire", (read) => {
|
|
4507
5810
|
const context = currentInputContext();
|
|
4508
5811
|
const identity = context.owner;
|
|
4509
5812
|
if (recipientBindingKey !== context.recipientBindingKey) {
|
|
4510
5813
|
throw createJournalError("conflict", path, "input acquisition targets another execution binding");
|
|
4511
5814
|
}
|
|
4512
5815
|
const current = read();
|
|
4513
|
-
const entry = current.file.entries.find(candidate => candidate.updateId === updateId);
|
|
4514
|
-
if (!entry ||
|
|
5816
|
+
const entry = current.file.entries.find((candidate) => candidate.updateId === updateId);
|
|
5817
|
+
if (!entry ||
|
|
5818
|
+
entry.state !== "pending" ||
|
|
5819
|
+
entry.preApprovalExcluded !== false) {
|
|
4515
5820
|
throw createJournalError("conflict", path, "input is not eligible for acquisition");
|
|
4516
5821
|
}
|
|
4517
5822
|
const projected = executionUpdate ?? entry.update;
|
|
4518
5823
|
const existing = entry.inputClaim;
|
|
4519
5824
|
if (existing) {
|
|
4520
5825
|
if (!isTelegramUpdateJournalQueueOwnerProcess(existing.owner, identity) ||
|
|
4521
|
-
existing.owner.sessionGeneration !== identity.sessionGeneration ||
|
|
5826
|
+
existing.owner.sessionGeneration !== identity.sessionGeneration ||
|
|
5827
|
+
existing.recipientBindingKey !== recipientBindingKey ||
|
|
4522
5828
|
!isDeepStrictEqual(existing.executionUpdate ?? entry.update, projected)) {
|
|
4523
5829
|
throw createJournalError("conflict", path, "input already has another owner or execution projection");
|
|
4524
5830
|
}
|
|
4525
|
-
return {
|
|
5831
|
+
return {
|
|
5832
|
+
acquired: false,
|
|
5833
|
+
receipt: createInputReceipt(updateId, existing.owner),
|
|
5834
|
+
};
|
|
4526
5835
|
}
|
|
4527
|
-
const claim = {
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
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
|
+
};
|
|
4532
5855
|
const reservedEntries = replaceInputHeadroomEntry(current.file, updateId, reservedEntry);
|
|
4533
5856
|
const actualBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: claimedEntries }));
|
|
4534
|
-
const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) +
|
|
4535
|
-
TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
|
|
5857
|
+
const reservedBytes = Buffer.byteLength(serializeJournalFile({ ...current.file, entries: reservedEntries })) + TELEGRAM_UPDATE_JOURNAL_INPUT_PROJECTION_HEADROOM_BYTES;
|
|
4536
5858
|
if (actualBytes > reservedBytes) {
|
|
4537
5859
|
throw createJournalError("capacity", path, "execution projection exceeds reserved input headroom");
|
|
4538
5860
|
}
|
|
4539
5861
|
publishInputMutation(current, claimedEntries, updateId, context);
|
|
4540
|
-
return {
|
|
5862
|
+
return {
|
|
5863
|
+
acquired: true,
|
|
5864
|
+
receipt: createInputReceipt(updateId, claim.owner),
|
|
5865
|
+
};
|
|
4541
5866
|
});
|
|
4542
5867
|
},
|
|
4543
5868
|
startInput(value) {
|
|
4544
5869
|
const receipt = normalizeInputReceipt(value);
|
|
4545
|
-
return runInputMutation("start", read => {
|
|
5870
|
+
return runInputMutation("start", (read) => {
|
|
4546
5871
|
const context = currentInputContext();
|
|
4547
5872
|
const current = read();
|
|
4548
5873
|
const entry = ownedInput(current, receipt);
|
|
@@ -4555,24 +5880,41 @@ function createJournalStoreCore(options, getInputContext) {
|
|
|
4555
5880
|
return { started: false };
|
|
4556
5881
|
if (claim.handoff)
|
|
4557
5882
|
throw createJournalError("conflict", path, "input handoff freezes donor execution");
|
|
4558
|
-
publishInputMutation(current, current.file.entries.map(candidate => candidate.updateId === receipt.updateId
|
|
4559
|
-
? {
|
|
4560
|
-
|
|
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
|
+
};
|
|
4561
5893
|
});
|
|
4562
5894
|
},
|
|
4563
5895
|
completeInput(value) {
|
|
4564
5896
|
const receipt = normalizeInputReceipt(value);
|
|
4565
|
-
return runInputMutation("complete", read => {
|
|
5897
|
+
return runInputMutation("complete", (read) => {
|
|
4566
5898
|
const current = read();
|
|
4567
|
-
const exists = current.file.entries.some(entry => entry.updateId === receipt.updateId);
|
|
4568
|
-
if (!exists &&
|
|
4569
|
-
|
|
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
|
+
};
|
|
4570
5908
|
}
|
|
4571
5909
|
const entry = ownedInput(current, receipt);
|
|
4572
5910
|
if (entry.inputClaim.phase !== "running")
|
|
4573
5911
|
throw createJournalError("conflict", path, "input has not started");
|
|
4574
|
-
const published = publishInputSettlement(current, current.file.entries.filter(candidate => candidate.updateId !== receipt.updateId), true);
|
|
4575
|
-
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
|
+
};
|
|
4576
5918
|
});
|
|
4577
5919
|
},
|
|
4578
5920
|
};
|