@llblab/pi-telegram 0.27.11 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +150 -258
- package/BACKLOG.md +1 -169
- package/CHANGELOG.md +396 -441
- package/README.md +9 -6
- package/api/updates.ts +5 -0
- package/docs/architecture.md +71 -26
- package/docs/multi-instance-bus.md +23 -5
- package/docs/public-api.md +7 -6
- package/docs/ui-style.md +6 -6
- package/docs/updates.md +29 -11
- package/index.ts +356 -246
- package/lib/activity-verbosity.ts +26 -0
- package/lib/bindings.ts +240 -2
- package/lib/bus-follower.ts +436 -238
- package/lib/bus-leader.ts +395 -42
- package/lib/bus.ts +994 -153
- package/lib/commands.ts +184 -30
- package/lib/config.ts +23 -2
- package/lib/journal.ts +3140 -0
- package/lib/lifecycle.ts +4 -0
- package/lib/locks.ts +21 -21
- package/lib/media.ts +71 -32
- package/lib/menu-queue.ts +31 -17
- package/lib/menu.ts +5 -3
- package/lib/model.ts +51 -24
- package/lib/ownership.ts +42 -7
- package/lib/paths.ts +35 -0
- package/lib/polling.ts +591 -106
- package/lib/prompts.ts +17 -0
- package/lib/queue.ts +732 -143
- package/lib/routing.ts +291 -64
- package/lib/runtime.ts +26 -10
- package/lib/status.ts +257 -18
- package/lib/sync.ts +131 -5
- package/lib/telegram-api.ts +41 -11
- package/lib/text-groups.ts +75 -35
- package/lib/threads.ts +112 -4
- package/lib/turns.ts +79 -14
- package/lib/updates.ts +3771 -223
- package/package.json +3 -3
- package/scripts/check-downgrade.mjs +435 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"node": ">=22.19.0"
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
|
-
"test": "node --experimental-strip-types --test --test-reporter=dot tests/*.test.ts",
|
|
30
|
+
"test": "node --experimental-strip-types --test --test-concurrency=4 --test-reporter=dot tests/*.test.ts",
|
|
31
31
|
"test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"audit": "npm audit --omit=peer",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"extensions": [
|
|
65
65
|
"./index.ts"
|
|
66
66
|
],
|
|
67
|
-
"image": "https://
|
|
67
|
+
"image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@earendil-works/pi-agent-core": ">=0.80.6",
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Conservative pre-downgrade journal authority check.
|
|
5
|
+
* Usage: node scripts/check-downgrade.mjs [agent-dir]
|
|
6
|
+
*
|
|
7
|
+
* This is an intentionally simpler schema validator than `lib/journal.ts`.
|
|
8
|
+
* It only needs to prove whether unresolved journal authority remains, so it
|
|
9
|
+
* errs toward BLOCKED and does not cross-check the rules the runtime owns:
|
|
10
|
+
*
|
|
11
|
+
* - Operator dispositions are not reconciled against surviving entries
|
|
12
|
+
* (`lib/journal.ts` rejects a "discard" disposition whose entry is still
|
|
13
|
+
* present).
|
|
14
|
+
* - `queueOwner` and `failure` are presence-checked, not deep-validated
|
|
15
|
+
* (acquisition shape, retry/terminal derivation, and disposition id
|
|
16
|
+
* generation stay in `lib/journal.ts`).
|
|
17
|
+
*
|
|
18
|
+
* The shared entry rules (keys, state enum, update_id agreement, and
|
|
19
|
+
* queue/failure metadata presence) MUST stay in sync with
|
|
20
|
+
* `lib/journal.ts` `validateJournalEntry`; `tests/journal-downgrade.test.ts`
|
|
21
|
+
* reconciles the two surfaces on those rules.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
existsSync,
|
|
26
|
+
lstatSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
readdirSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { homedir } from "node:os";
|
|
31
|
+
import { basename, join, resolve } from "node:path";
|
|
32
|
+
|
|
33
|
+
const SNAPSHOT_PATTERN = /^(?:inbox|follower-inbox-[a-f0-9]{16})(?:\.[a-zA-Z0-9._-]+)?\.json$/u;
|
|
34
|
+
const SEGMENT_NAME_PATTERN = /^\d{16}\.json$/u;
|
|
35
|
+
const AUTHORITY_STATES = new Set(["pending", "retry-wait", "queued", "failed"]);
|
|
36
|
+
const SNAPSHOT_KEYS = new Set([
|
|
37
|
+
"version",
|
|
38
|
+
"revision",
|
|
39
|
+
"profile",
|
|
40
|
+
"botIdentity",
|
|
41
|
+
"entries",
|
|
42
|
+
"operatorDispositions",
|
|
43
|
+
]);
|
|
44
|
+
const SEGMENT_KEYS = new Set([
|
|
45
|
+
"version",
|
|
46
|
+
"revision",
|
|
47
|
+
"previousRevision",
|
|
48
|
+
"profile",
|
|
49
|
+
"botIdentity",
|
|
50
|
+
"upsertedEntries",
|
|
51
|
+
"removedUpdateIds",
|
|
52
|
+
"operatorDispositions",
|
|
53
|
+
]);
|
|
54
|
+
const ENTRY_KEYS = new Set([
|
|
55
|
+
"updateId",
|
|
56
|
+
"update",
|
|
57
|
+
"admittedAtMs",
|
|
58
|
+
"state",
|
|
59
|
+
"queueKind",
|
|
60
|
+
"queueReceiptId",
|
|
61
|
+
"queueOwner",
|
|
62
|
+
"queueHandoff",
|
|
63
|
+
"failure",
|
|
64
|
+
"nextRetryAtMs",
|
|
65
|
+
"terminalAtMs",
|
|
66
|
+
"terminalReason",
|
|
67
|
+
"terminalFailureId",
|
|
68
|
+
]);
|
|
69
|
+
const DISPOSITION_KEYS = new Set([
|
|
70
|
+
"failureId",
|
|
71
|
+
"updateId",
|
|
72
|
+
"action",
|
|
73
|
+
"committedAtMs",
|
|
74
|
+
"attemptCount",
|
|
75
|
+
"failureClass",
|
|
76
|
+
"terminalAtMs",
|
|
77
|
+
"terminalReason",
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
function resolveAgentDir() {
|
|
81
|
+
if (process.argv[2]) return resolve(process.argv[2]);
|
|
82
|
+
if (process.env.PI_CODING_AGENT_DIR) {
|
|
83
|
+
return resolve(process.env.PI_CODING_AGENT_DIR);
|
|
84
|
+
}
|
|
85
|
+
const executableName = basename(process.execPath).toLowerCase();
|
|
86
|
+
const invokedName = basename(process.argv[1] ?? "").toLowerCase();
|
|
87
|
+
return join(
|
|
88
|
+
homedir(),
|
|
89
|
+
executableName.startsWith("omp") || invokedName.startsWith("omp")
|
|
90
|
+
? ".omp"
|
|
91
|
+
: ".pi",
|
|
92
|
+
"agent",
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function block(message) {
|
|
97
|
+
console.error(`BLOCKED: ${message}`);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function hasOnlyKeys(value, allowed) {
|
|
102
|
+
return Object.keys(value).every((key) => allowed.has(key));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isRecord(value) {
|
|
106
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPositiveInteger(value) {
|
|
110
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isNonNegativeInteger(value) {
|
|
114
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readJson(path) {
|
|
118
|
+
try {
|
|
119
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
block(
|
|
122
|
+
`cannot verify ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
123
|
+
);
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function validateBotIdentity(value) {
|
|
129
|
+
return (
|
|
130
|
+
isRecord(value) &&
|
|
131
|
+
hasOnlyKeys(value, new Set(["botId", "tokenSha256"])) &&
|
|
132
|
+
(value.botId === undefined || isPositiveInteger(value.botId)) &&
|
|
133
|
+
typeof value.tokenSha256 === "string" &&
|
|
134
|
+
/^[a-f0-9]{64}$/u.test(value.tokenSha256)
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function identitiesMatch(left, right) {
|
|
139
|
+
if (
|
|
140
|
+
left.botId !== undefined &&
|
|
141
|
+
right.botId !== undefined &&
|
|
142
|
+
left.botId !== right.botId
|
|
143
|
+
) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
(left.botId !== undefined && left.botId === right.botId) ||
|
|
148
|
+
left.tokenSha256 === right.tokenSha256
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function mergeBotIdentity(stored, current) {
|
|
153
|
+
return {
|
|
154
|
+
...(current.botId !== undefined
|
|
155
|
+
? { botId: current.botId }
|
|
156
|
+
: stored.botId !== undefined
|
|
157
|
+
? { botId: stored.botId }
|
|
158
|
+
: {}),
|
|
159
|
+
tokenSha256: current.tokenSha256,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validateEntries(value) {
|
|
164
|
+
if (!Array.isArray(value)) return undefined;
|
|
165
|
+
const entries = new Map();
|
|
166
|
+
let previousId = -1;
|
|
167
|
+
for (const entry of value) {
|
|
168
|
+
if (
|
|
169
|
+
!isRecord(entry) ||
|
|
170
|
+
!hasOnlyKeys(entry, ENTRY_KEYS) ||
|
|
171
|
+
!isNonNegativeInteger(entry.updateId) ||
|
|
172
|
+
entry.updateId <= previousId ||
|
|
173
|
+
!AUTHORITY_STATES.has(entry.state) ||
|
|
174
|
+
!isRecord(entry.update) ||
|
|
175
|
+
entry.update.update_id !== entry.updateId ||
|
|
176
|
+
!isNonNegativeInteger(entry.admittedAtMs)
|
|
177
|
+
) {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
const hasQueueMetadata =
|
|
181
|
+
entry.queueKind !== undefined ||
|
|
182
|
+
entry.queueReceiptId !== undefined ||
|
|
183
|
+
entry.queueOwner !== undefined ||
|
|
184
|
+
entry.queueHandoff !== undefined;
|
|
185
|
+
const hasFailureMetadata =
|
|
186
|
+
entry.failure !== undefined ||
|
|
187
|
+
entry.nextRetryAtMs !== undefined ||
|
|
188
|
+
entry.terminalAtMs !== undefined ||
|
|
189
|
+
entry.terminalReason !== undefined ||
|
|
190
|
+
entry.terminalFailureId !== undefined;
|
|
191
|
+
if (
|
|
192
|
+
(entry.state === "pending" && (hasQueueMetadata || hasFailureMetadata)) ||
|
|
193
|
+
(entry.state === "queued" &&
|
|
194
|
+
((entry.queueKind !== "prompt" && entry.queueKind !== "control") ||
|
|
195
|
+
typeof entry.queueReceiptId !== "string" ||
|
|
196
|
+
entry.queueReceiptId.length === 0 ||
|
|
197
|
+
entry.queueOwner === undefined ||
|
|
198
|
+
hasFailureMetadata)) ||
|
|
199
|
+
((entry.state === "retry-wait" || entry.state === "failed") &&
|
|
200
|
+
(hasQueueMetadata || !isRecord(entry.failure)))
|
|
201
|
+
) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
previousId = entry.updateId;
|
|
205
|
+
entries.set(entry.updateId, entry);
|
|
206
|
+
}
|
|
207
|
+
return entries;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateOperatorDispositions(value) {
|
|
211
|
+
if (value === undefined) return true;
|
|
212
|
+
if (!Array.isArray(value)) return false;
|
|
213
|
+
const failureIds = new Set();
|
|
214
|
+
for (const disposition of value) {
|
|
215
|
+
if (
|
|
216
|
+
!isRecord(disposition) ||
|
|
217
|
+
!hasOnlyKeys(disposition, DISPOSITION_KEYS) ||
|
|
218
|
+
typeof disposition.failureId !== "string" ||
|
|
219
|
+
disposition.failureId.length === 0 ||
|
|
220
|
+
failureIds.has(disposition.failureId) ||
|
|
221
|
+
!isNonNegativeInteger(disposition.updateId) ||
|
|
222
|
+
(disposition.action !== "retry" && disposition.action !== "discard") ||
|
|
223
|
+
!isNonNegativeInteger(disposition.committedAtMs) ||
|
|
224
|
+
!isPositiveInteger(disposition.attemptCount) ||
|
|
225
|
+
typeof disposition.failureClass !== "string" ||
|
|
226
|
+
disposition.failureClass.length === 0 ||
|
|
227
|
+
!isNonNegativeInteger(disposition.terminalAtMs) ||
|
|
228
|
+
disposition.committedAtMs < disposition.terminalAtMs ||
|
|
229
|
+
typeof disposition.terminalReason !== "string" ||
|
|
230
|
+
disposition.terminalReason.length === 0
|
|
231
|
+
) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
failureIds.add(disposition.failureId);
|
|
235
|
+
}
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function validateSnapshot(value) {
|
|
240
|
+
if (
|
|
241
|
+
!isRecord(value) ||
|
|
242
|
+
!hasOnlyKeys(value, SNAPSHOT_KEYS) ||
|
|
243
|
+
value.version !== 1 ||
|
|
244
|
+
(value.revision !== undefined && !isPositiveInteger(value.revision)) ||
|
|
245
|
+
typeof value.profile !== "string" ||
|
|
246
|
+
value.profile.length === 0 ||
|
|
247
|
+
!validateBotIdentity(value.botIdentity) ||
|
|
248
|
+
!validateOperatorDispositions(value.operatorDispositions)
|
|
249
|
+
) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
const entries = validateEntries(value.entries);
|
|
253
|
+
if (!entries) return undefined;
|
|
254
|
+
return {
|
|
255
|
+
revision: value.revision ?? 0,
|
|
256
|
+
profile: value.profile,
|
|
257
|
+
botIdentity: value.botIdentity,
|
|
258
|
+
entries,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function validateSegment(value) {
|
|
263
|
+
if (
|
|
264
|
+
!isRecord(value) ||
|
|
265
|
+
!hasOnlyKeys(value, SEGMENT_KEYS) ||
|
|
266
|
+
value.version !== 1 ||
|
|
267
|
+
!isPositiveInteger(value.revision) ||
|
|
268
|
+
!isNonNegativeInteger(value.previousRevision) ||
|
|
269
|
+
value.previousRevision !== value.revision - 1 ||
|
|
270
|
+
typeof value.profile !== "string" ||
|
|
271
|
+
value.profile.length === 0 ||
|
|
272
|
+
!validateBotIdentity(value.botIdentity) ||
|
|
273
|
+
!Array.isArray(value.upsertedEntries) ||
|
|
274
|
+
!Array.isArray(value.removedUpdateIds) ||
|
|
275
|
+
!validateOperatorDispositions(value.operatorDispositions)
|
|
276
|
+
) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
const upsertedEntries = validateEntries(
|
|
280
|
+
[...value.upsertedEntries].sort((left, right) =>
|
|
281
|
+
Number(left?.updateId) - Number(right?.updateId),
|
|
282
|
+
),
|
|
283
|
+
);
|
|
284
|
+
if (!upsertedEntries || upsertedEntries.size !== value.upsertedEntries.length) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const removedUpdateIds = new Set();
|
|
288
|
+
for (const updateId of value.removedUpdateIds) {
|
|
289
|
+
if (
|
|
290
|
+
!isNonNegativeInteger(updateId) ||
|
|
291
|
+
removedUpdateIds.has(updateId) ||
|
|
292
|
+
upsertedEntries.has(updateId)
|
|
293
|
+
) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
removedUpdateIds.add(updateId);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
revision: value.revision,
|
|
300
|
+
previousRevision: value.previousRevision,
|
|
301
|
+
profile: value.profile,
|
|
302
|
+
botIdentity: value.botIdentity,
|
|
303
|
+
upsertedEntries,
|
|
304
|
+
removedUpdateIds,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const agentDir = resolveAgentDir();
|
|
309
|
+
const runtimeDir = join(agentDir, "tmp", "telegram");
|
|
310
|
+
if (!existsSync(runtimeDir)) {
|
|
311
|
+
console.log("SAFE: no Telegram runtime directory exists.");
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
let runtimeNames;
|
|
316
|
+
try {
|
|
317
|
+
runtimeNames = readdirSync(runtimeDir);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
block(
|
|
320
|
+
`cannot verify Telegram runtime directory ${runtimeDir}: ${error instanceof Error ? error.message : String(error)}`,
|
|
321
|
+
);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const snapshotNames = runtimeNames.filter((name) => SNAPSHOT_PATTERN.test(name));
|
|
326
|
+
const snapshotSet = new Set(snapshotNames);
|
|
327
|
+
for (const name of runtimeNames) {
|
|
328
|
+
const snapshotName = name.endsWith(".segments")
|
|
329
|
+
? name.slice(0, -".segments".length)
|
|
330
|
+
: undefined;
|
|
331
|
+
if (snapshotName && SNAPSHOT_PATTERN.test(snapshotName)) {
|
|
332
|
+
if (!snapshotSet.has(snapshotName)) {
|
|
333
|
+
block(
|
|
334
|
+
`cannot verify orphan journal segments without ${join(runtimeDir, snapshotName)}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (
|
|
340
|
+
(name.startsWith("inbox") || name.startsWith("follower-inbox")) &&
|
|
341
|
+
!SNAPSHOT_PATTERN.test(name)
|
|
342
|
+
) {
|
|
343
|
+
block(`cannot verify unrecognized journal-like artifact ${join(runtimeDir, name)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
for (const name of snapshotNames) {
|
|
348
|
+
const path = join(runtimeDir, name);
|
|
349
|
+
let pathStat;
|
|
350
|
+
try {
|
|
351
|
+
pathStat = lstatSync(path);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
block(`cannot verify journal snapshot ${path}: ${String(error)}`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (!pathStat.isFile()) {
|
|
357
|
+
block(`cannot verify non-file journal snapshot ${path}`);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const snapshot = validateSnapshot(readJson(path));
|
|
361
|
+
if (!snapshot) {
|
|
362
|
+
block(`cannot verify malformed journal snapshot ${path}`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
let revision = snapshot.revision;
|
|
367
|
+
let botIdentity = snapshot.botIdentity;
|
|
368
|
+
const entries = snapshot.entries;
|
|
369
|
+
const segmentDir = `${path}.segments`;
|
|
370
|
+
if (existsSync(segmentDir)) {
|
|
371
|
+
let segmentNames;
|
|
372
|
+
try {
|
|
373
|
+
if (!lstatSync(segmentDir).isDirectory()) {
|
|
374
|
+
block(`cannot verify non-directory journal segments ${segmentDir}`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
segmentNames = readdirSync(segmentDir);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
block(`cannot verify journal segments ${segmentDir}: ${String(error)}`);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
const unrecognized = segmentNames.filter(
|
|
383
|
+
(segmentName) => !SEGMENT_NAME_PATTERN.test(segmentName),
|
|
384
|
+
);
|
|
385
|
+
for (const segmentName of unrecognized) {
|
|
386
|
+
block(`cannot verify unrecognized journal segment ${join(segmentDir, segmentName)}`);
|
|
387
|
+
}
|
|
388
|
+
for (const segmentName of segmentNames.filter((candidate) =>
|
|
389
|
+
SEGMENT_NAME_PATTERN.test(candidate),
|
|
390
|
+
).sort()) {
|
|
391
|
+
const nameRevision = Number(segmentName.slice(0, 16));
|
|
392
|
+
if (nameRevision <= revision) continue;
|
|
393
|
+
const segmentPath = join(segmentDir, segmentName);
|
|
394
|
+
let segmentStat;
|
|
395
|
+
try {
|
|
396
|
+
segmentStat = lstatSync(segmentPath);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
block(`cannot verify journal segment ${segmentPath}: ${String(error)}`);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const segment = segmentStat.isFile()
|
|
402
|
+
? validateSegment(readJson(segmentPath))
|
|
403
|
+
: undefined;
|
|
404
|
+
if (
|
|
405
|
+
!segment ||
|
|
406
|
+
segment.revision !== nameRevision ||
|
|
407
|
+
segment.previousRevision !== revision
|
|
408
|
+
) {
|
|
409
|
+
block(`cannot verify malformed or gapped journal segment ${segmentPath}`);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (
|
|
413
|
+
segment.profile !== snapshot.profile ||
|
|
414
|
+
!identitiesMatch(segment.botIdentity, botIdentity)
|
|
415
|
+
) {
|
|
416
|
+
block(`cannot verify foreign journal segment identity ${segmentPath}`);
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
for (const updateId of segment.removedUpdateIds) entries.delete(updateId);
|
|
420
|
+
for (const [updateId, entry] of segment.upsertedEntries) {
|
|
421
|
+
entries.set(updateId, entry);
|
|
422
|
+
}
|
|
423
|
+
revision = segment.revision;
|
|
424
|
+
botIdentity = mergeBotIdentity(botIdentity, segment.botIdentity);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (entries.size > 0) {
|
|
428
|
+
block(
|
|
429
|
+
`${path} retains ${entries.size} unresolved update(s); drain with 0.28.x before downgrade.`,
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (process.exitCode) process.exit(1);
|
|
435
|
+
console.log(`SAFE: ${snapshotNames.length} Telegram journal(s) contain no unresolved updates.`);
|