@xmoon76/dsh-pi-tui 0.1.2 → 0.1.4
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/README.md +1 -1
- package/dist/index.d.mts +47 -1
- package/dist/index.mjs +4151 -748
- package/package.json +5 -3
- package/scripts/bench.mts +261 -0
- package/scripts/naming-gate.mjs +102 -0
- package/scripts/repair-core.mjs +363 -49
- package/scripts/repair-session.mjs +249 -52
- package/scripts/tarball-smoke.mjs +325 -0
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* `session.jsonl.zstd` (concatenated Zstandard frames of JSONL). A session
|
|
8
8
|
* written concurrently by two dsh processes can end up with duplicate or
|
|
9
9
|
* missing seq numbers and become unreadable (`corrupt session log: seq gap
|
|
10
|
-
* in committed region`)
|
|
11
|
-
*
|
|
10
|
+
* in committed region`); a crash mid-flush can leave a torn (truncated)
|
|
11
|
+
* Zstandard tail that most readers silently ignore. This script repairs the
|
|
12
|
+
* artifact and verifies the result with the same storage-format scan the
|
|
13
|
+
* reader performs. A torn tail is truncated at the last COMPLETE frame
|
|
14
|
+
* boundary — the salvaged prefix is re-framed, and the loss count is
|
|
15
|
+
* reported as unknown (the torn frame's content is unknowable).
|
|
12
16
|
*
|
|
13
17
|
* Usage:
|
|
14
18
|
* node repair-session.mjs <session-id> [--yes] [--dsh-dir <path>] [--dsh-home <path>]
|
|
@@ -19,7 +23,9 @@
|
|
|
19
23
|
* - Before writing, the original artifact is ALWAYS backed up to
|
|
20
24
|
* `<artifact>.bak-<timestamp>`; a failed backup aborts the write.
|
|
21
25
|
* - `--scan` read-only scans every persisted session and lists the damaged
|
|
22
|
-
* ids (one line each, `CORRUPT <id>: <reason>`).
|
|
26
|
+
* ids (one line each, `CORRUPT <id>: <reason>`). A torn (truncated)
|
|
27
|
+
* Zstandard tail is reported with its byte accounting instead of being
|
|
28
|
+
* silently treated as healthy.
|
|
23
29
|
*
|
|
24
30
|
* Dependencies: resolves `decodeStorageRecord`/`packChunkRuns` from the
|
|
25
31
|
* installed dsh (`@deepseek-ai/dsh-session`). Resolution order:
|
|
@@ -27,20 +33,27 @@
|
|
|
27
33
|
* the owning package), then `$DSH_HOME`/`DSH_DIR`.
|
|
28
34
|
*/
|
|
29
35
|
|
|
30
|
-
import {
|
|
36
|
+
import { randomUUID } from 'node:crypto'
|
|
37
|
+
import { copyFileSync, existsSync, openSync, fsyncSync, closeSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
31
38
|
import { homedir } from 'node:os'
|
|
32
39
|
import { basename, dirname, join, resolve } from 'node:path'
|
|
40
|
+
import { pathToFileURL } from 'node:url'
|
|
33
41
|
import { createRequire } from 'node:module'
|
|
34
42
|
import { spawnSync } from 'node:child_process'
|
|
35
43
|
import { zstdCompressSync, zstdDecompressSync } from 'node:zlib'
|
|
36
|
-
import { compressLog, decompressFrames, encodeLog, repairEvents, scanEvents,
|
|
44
|
+
import { compressLog, decompressFrames, DUPLICATE_REFERENCE_STRATEGIES, encodeLog, frameLineRanges, repairEvents, scanEvents, scanFrameLayout } from './repair-core.mjs'
|
|
37
45
|
|
|
38
46
|
const USAGE = `usage:
|
|
39
|
-
node repair-session.mjs <session-id> [--yes] [--dsh-dir <path>] [--dsh-home <path>]
|
|
47
|
+
node repair-session.mjs <session-id> [--yes] [--duplicate-reference first|last|segment] [--dsh-dir <path>] [--dsh-home <path>]
|
|
40
48
|
node repair-session.mjs --scan [--dsh-dir <path>] [--dsh-home <path>]
|
|
41
49
|
|
|
42
50
|
repair one session log (dry run by default; --yes applies with a backup),
|
|
43
|
-
or --scan all persisted sessions read-only and list damaged ids
|
|
51
|
+
or --scan all persisted sessions read-only and list damaged ids.
|
|
52
|
+
|
|
53
|
+
--duplicate-reference resolves references to a duplicated seq (a seq that
|
|
54
|
+
appears more than once because two writers collided): first = the earlier
|
|
55
|
+
event, last = the later event, segment = an event in the same flush frame
|
|
56
|
+
when one exists. Without it, an ambiguous log is refused, never rewritten.`
|
|
44
57
|
|
|
45
58
|
/** Locate the dsh package directory (the one whose manifest is @deepseek-ai/dsh). */
|
|
46
59
|
function findDshPackageDir(from) {
|
|
@@ -110,31 +123,110 @@ function findSessionArtifact(home, id) {
|
|
|
110
123
|
return undefined
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* Read one artifact into decompressed text plus the full frame-layout
|
|
128
|
+
* diagnosis. Only COMPLETE frames contribute to the text, so a torn tail is
|
|
129
|
+
* inherently excluded from the salvageable prefix; the layout result carries
|
|
130
|
+
* the byte accounting (`tornStart`/`garbageStart`, `totalBytes`,
|
|
131
|
+
* `completeBytes`) for reporting.
|
|
132
|
+
*/
|
|
114
133
|
function readArtifact(path, compression) {
|
|
115
|
-
if (compression === 'none')
|
|
134
|
+
if (compression === 'none') {
|
|
135
|
+
return {
|
|
136
|
+
text: readFileSync(path, 'utf8'),
|
|
137
|
+
layout: { status: 'healthy', frames: [], totalBytes: 0, completeBytes: 0 },
|
|
138
|
+
lineRanges: undefined,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
116
141
|
const buffer = readFileSync(path)
|
|
117
|
-
const layout =
|
|
118
|
-
|
|
142
|
+
const layout = scanFrameLayout(buffer, zstdDecompressSync)
|
|
143
|
+
// Only complete frames contribute; a frame-less prefix (torn from byte 0)
|
|
144
|
+
// yields an empty salvageable text and is refused by the caller.
|
|
145
|
+
return {
|
|
146
|
+
text: layout.frames.length === 0 ? '' : decompressFrames(buffer, zstdDecompressSync).text,
|
|
147
|
+
layout,
|
|
148
|
+
// Per-frame JSONL line ranges: writer segments for duplicate-seq
|
|
149
|
+
// reference resolution (--duplicate-reference=segment).
|
|
150
|
+
lineRanges: frameLineRanges(buffer, zstdDecompressSync),
|
|
151
|
+
}
|
|
119
152
|
}
|
|
120
153
|
|
|
121
|
-
/**
|
|
122
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Validate one artifact FILE with the same checks the reader performs:
|
|
156
|
+
* frame layout (healthy) and event contiguity. Returns the issue message,
|
|
157
|
+
* or undefined when the file is fully readable.
|
|
158
|
+
*/
|
|
159
|
+
function verifyArtifactFile(path, compression, decodeStorageRecord) {
|
|
160
|
+
const { text, layout } = readArtifact(path, compression)
|
|
161
|
+
if (layout.status !== 'healthy') return layout.issue
|
|
162
|
+
const { issue } = scanEvents(text, decodeStorageRecord)
|
|
163
|
+
return issue?.message
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Write one artifact back SAFELY: durable fsynced backup first, then a
|
|
168
|
+
* UNIQUE tmp file (0600, fsynced), VERIFIED with the reader's own checks
|
|
169
|
+
* BEFORE it becomes the active file, then an atomic rename over the target
|
|
170
|
+
* (with a best-effort parent-dir fsync). Any failure — backup, tmp write,
|
|
171
|
+
* verification, rename — deletes the tmp and leaves the original target
|
|
172
|
+
* untouched: a verification failure can never damage the active log, and
|
|
173
|
+
* the CLI's post-rename verify is no longer the only gate.
|
|
174
|
+
* @returns the backup path.
|
|
175
|
+
*/
|
|
176
|
+
function writeArtifact(path, compression, text, decodeStorageRecord) {
|
|
123
177
|
const backup = `${path}.bak-${new Date().toISOString().replace(/[:.]/g, '-')}`
|
|
124
178
|
try {
|
|
125
179
|
copyFileSync(path, backup)
|
|
180
|
+
// The backup must be durable BEFORE the original is replaced: fsync the
|
|
181
|
+
// copied bytes so a crash cannot lose both copies.
|
|
182
|
+
const fd = openSync(backup, 'r')
|
|
183
|
+
try {
|
|
184
|
+
fsyncSync(fd)
|
|
185
|
+
} finally {
|
|
186
|
+
closeSync(fd)
|
|
187
|
+
}
|
|
126
188
|
} catch (error) {
|
|
127
189
|
throw new Error(`backup failed (${error instanceof Error ? error.message : String(error)}); refusing to write`)
|
|
128
190
|
}
|
|
129
|
-
|
|
191
|
+
// A random suffix keeps concurrent repairs and stale PIDs from colliding.
|
|
192
|
+
const tmp = `${path}.repair-tmp-${process.pid}-${randomUUID()}`
|
|
130
193
|
const bytes = compression === 'none' ? Buffer.from(text, 'utf8') : compressLog(text, zstdCompressSync)
|
|
194
|
+
let fd
|
|
131
195
|
try {
|
|
132
|
-
|
|
196
|
+
fd = openSync(tmp, 'w', 0o600)
|
|
197
|
+
writeFileSync(fd, bytes)
|
|
198
|
+
fsyncSync(fd)
|
|
199
|
+
closeSync(fd)
|
|
200
|
+
fd = undefined
|
|
201
|
+
// VERIFY THE TMP BEFORE IT BECOMES THE ACTIVE FILE: the original
|
|
202
|
+
// target stays in place when the bytes are not readable.
|
|
203
|
+
const verifyIssue = verifyArtifactFile(tmp, compression, decodeStorageRecord)
|
|
204
|
+
if (verifyIssue !== undefined) {
|
|
205
|
+
throw new Error(`verification failed before replace: ${verifyIssue}`)
|
|
206
|
+
}
|
|
133
207
|
renameSync(tmp, path)
|
|
208
|
+
// Persist the rename itself (best effort: some platforms cannot fsync
|
|
209
|
+
// directories, and the tmp+rename is already atomic without it).
|
|
210
|
+
try {
|
|
211
|
+
const dirFd = openSync(dirname(path), 'r')
|
|
212
|
+
try {
|
|
213
|
+
fsyncSync(dirFd)
|
|
214
|
+
} finally {
|
|
215
|
+
closeSync(dirFd)
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
// Best effort.
|
|
219
|
+
}
|
|
134
220
|
} catch (error) {
|
|
221
|
+
if (fd !== undefined) {
|
|
222
|
+
try {
|
|
223
|
+
closeSync(fd)
|
|
224
|
+
} catch {
|
|
225
|
+
// Best effort.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
135
228
|
try {
|
|
136
|
-
|
|
137
|
-
copyFileSync(backup, path)
|
|
229
|
+
rmSync(tmp, { force: true })
|
|
138
230
|
} catch {
|
|
139
231
|
// Best effort.
|
|
140
232
|
}
|
|
@@ -143,6 +235,18 @@ function writeArtifact(path, compression, text) {
|
|
|
143
235
|
return backup
|
|
144
236
|
}
|
|
145
237
|
|
|
238
|
+
/** A one-line damage report for --scan output. */
|
|
239
|
+
function damageLine(layout, lastEvent) {
|
|
240
|
+
if (layout.status === 'torn-tail') {
|
|
241
|
+
const at = layout.tornStart ?? layout.garbageStart
|
|
242
|
+
const last = lastEvent === undefined
|
|
243
|
+
? 'no decodable event in the complete prefix'
|
|
244
|
+
: `last decodable event: ${lastEvent.seq}:${lastEvent.type}`
|
|
245
|
+
return `torn tail — damage starts at byte ${at} of ${layout.totalBytes} (${layout.completeBytes} complete bytes; ${last})`
|
|
246
|
+
}
|
|
247
|
+
return layout.issue
|
|
248
|
+
}
|
|
249
|
+
|
|
146
250
|
/** Scan every persisted session read-only; returns damaged entries. */
|
|
147
251
|
function scanAll(home, decodeStorageRecord) {
|
|
148
252
|
const damaged = []
|
|
@@ -159,13 +263,19 @@ function scanAll(home, decodeStorageRecord) {
|
|
|
159
263
|
const compression = name.endsWith('.zstd') ? 'zstd' : 'none'
|
|
160
264
|
const id = basename(dir)
|
|
161
265
|
try {
|
|
162
|
-
const { text,
|
|
163
|
-
const { issue } = scanEvents(text, decodeStorageRecord)
|
|
164
|
-
if (
|
|
266
|
+
const { text, layout, lineRanges } = readArtifact(path, compression)
|
|
267
|
+
const { issue, events } = scanEvents(text, decodeStorageRecord, lineRanges)
|
|
268
|
+
if (layout.status !== 'healthy' || issue !== undefined) {
|
|
269
|
+
const lastEvent = events.length === 0 ? undefined : events[events.length - 1]
|
|
165
270
|
damaged.push({
|
|
166
271
|
id,
|
|
167
272
|
path,
|
|
168
|
-
|
|
273
|
+
layout,
|
|
274
|
+
lastEvent,
|
|
275
|
+
issue: {
|
|
276
|
+
kind: layout.status !== 'healthy' ? layout.status : issue.kind,
|
|
277
|
+
message: layout.status !== 'healthy' ? damageLine(layout, lastEvent) : issue.message,
|
|
278
|
+
},
|
|
169
279
|
})
|
|
170
280
|
}
|
|
171
281
|
} catch (error) {
|
|
@@ -177,22 +287,83 @@ function scanAll(home, decodeStorageRecord) {
|
|
|
177
287
|
return damaged
|
|
178
288
|
}
|
|
179
289
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Parse CLI arguments with NO side effects (no console output, no
|
|
292
|
+
* process.exit, no env mutation), so the module is safe to import: the
|
|
293
|
+
* caller decides what to do with the parsed result. Missing flag values
|
|
294
|
+
* throw a clear error instead of silently consuming the next token.
|
|
295
|
+
* @param args - the argument vector (e.g. `process.argv.slice(2)`).
|
|
296
|
+
* @returns the flags plus the positional ids.
|
|
297
|
+
*/
|
|
298
|
+
export function parseArgs(args) {
|
|
299
|
+
const flags = {
|
|
300
|
+
scan: false,
|
|
301
|
+
yes: false,
|
|
302
|
+
duplicateReference: undefined,
|
|
303
|
+
dshDir: undefined,
|
|
304
|
+
dshHome: undefined,
|
|
305
|
+
help: false,
|
|
306
|
+
}
|
|
307
|
+
const positional = []
|
|
308
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
309
|
+
const arg = args[i]
|
|
310
|
+
if (arg === '--scan') flags.scan = true
|
|
311
|
+
else if (arg === '--yes') flags.yes = true
|
|
312
|
+
else if (arg === '--duplicate-reference') {
|
|
313
|
+
if (i + 1 >= args.length) throw new Error('--duplicate-reference requires a value (first|last|segment)')
|
|
314
|
+
const value = args[++i]
|
|
315
|
+
if (value.startsWith('--')) throw new Error(`--duplicate-reference requires a value (first|last|segment), got a flag: ${value}`)
|
|
316
|
+
if (!DUPLICATE_REFERENCE_STRATEGIES.includes(value)) {
|
|
317
|
+
throw new Error(`--duplicate-reference must be one of ${DUPLICATE_REFERENCE_STRATEGIES.join('|')}, got "${value}"`)
|
|
318
|
+
}
|
|
319
|
+
flags.duplicateReference = value
|
|
320
|
+
} else if (arg.startsWith('--duplicate-reference=')) {
|
|
321
|
+
// The README's = form: --duplicate-reference=first.
|
|
322
|
+
const value = arg.slice('--duplicate-reference='.length)
|
|
323
|
+
if (!DUPLICATE_REFERENCE_STRATEGIES.includes(value)) {
|
|
324
|
+
throw new Error(`--duplicate-reference must be one of ${DUPLICATE_REFERENCE_STRATEGIES.join('|')}, got "${value}"`)
|
|
325
|
+
}
|
|
326
|
+
flags.duplicateReference = value
|
|
327
|
+
} else if (arg === '--dsh-dir') {
|
|
328
|
+
if (i + 1 >= args.length) throw new Error('--dsh-dir requires a value')
|
|
329
|
+
const value = args[++i]
|
|
330
|
+
if (value.startsWith('--')) throw new Error(`--dsh-dir requires a value, got a flag: ${value}`)
|
|
331
|
+
flags.dshDir = value
|
|
332
|
+
} else if (arg === '--dsh-home') {
|
|
333
|
+
if (i + 1 >= args.length) throw new Error('--dsh-home requires a value')
|
|
334
|
+
const value = args[++i]
|
|
335
|
+
if (value.startsWith('--')) throw new Error(`--dsh-home requires a value, got a flag: ${value}`)
|
|
336
|
+
flags.dshHome = value
|
|
337
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
338
|
+
flags.help = true
|
|
339
|
+
} else {
|
|
340
|
+
positional.push(arg)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return { flags, positional }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Print the ambiguity report for a refused duplicate-seq repair. */
|
|
347
|
+
function printAmbiguity(id, plan, events) {
|
|
348
|
+
console.log(`session ${id}: ${plan.message}`)
|
|
349
|
+
for (const entry of plan.ambiguous) {
|
|
350
|
+
const frame = entry.frame >= 0 ? `, frame ${entry.frame}` : ''
|
|
351
|
+
const conflict = entry.sameFrameConflict === true ? ' (same-frame conflict)' : ''
|
|
352
|
+
console.log(` seq ${entry.seq} referenced via ${entry.key} by event ${entry.eventIndex} (line ${entry.line}${frame})${conflict}`)
|
|
353
|
+
console.log(` candidates: ${entry.candidates.map(index => `event ${index} (seq ${events[index].seq})`).join(', ')}`)
|
|
354
|
+
}
|
|
355
|
+
console.log(' strategies (--duplicate-reference): first = bind to the first occurrence; last = bind to the last;')
|
|
356
|
+
console.log(' segment = bind to the UNIQUE occurrence in the same flush frame, else first. Risk: a wrong binding')
|
|
357
|
+
console.log(' rewrites cross-references to the other writer\'s event — review the candidates before choosing.')
|
|
193
358
|
}
|
|
194
359
|
|
|
195
|
-
function main() {
|
|
360
|
+
function main(args) {
|
|
361
|
+
const { flags, positional } = parseArgs(args)
|
|
362
|
+
if (flags.help) {
|
|
363
|
+
console.log(USAGE)
|
|
364
|
+
process.exit(0)
|
|
365
|
+
}
|
|
366
|
+
if (flags.dshHome !== undefined) process.env.DSH_HOME = flags.dshHome
|
|
196
367
|
const home = dshHome()
|
|
197
368
|
const sessionsRoot = join(home, 'sessions')
|
|
198
369
|
if (!existsSync(sessionsRoot)) {
|
|
@@ -226,24 +397,44 @@ function main() {
|
|
|
226
397
|
process.exit(2)
|
|
227
398
|
}
|
|
228
399
|
|
|
229
|
-
const { text,
|
|
230
|
-
|
|
231
|
-
|
|
400
|
+
const { text, layout, lineRanges } = readArtifact(artifact.path, artifact.compression)
|
|
401
|
+
// Frame line ranges feed duplicate-seq reference resolution (writer segments).
|
|
402
|
+
const { header, events, issue } = scanEvents(text, decodeStorageRecord, lineRanges)
|
|
403
|
+
if (layout.status === 'healthy' && issue === undefined) {
|
|
232
404
|
console.log(`session ${id}: log is contiguous (${events.length} events) and the frame layout is valid; nothing to repair`)
|
|
233
405
|
process.exit(0)
|
|
234
406
|
}
|
|
407
|
+
// A torn tail with zero salvageable bytes (damage at byte 0) cannot be
|
|
408
|
+
// repaired by truncation: the complete prefix holds nothing to keep.
|
|
409
|
+
if (layout.status === 'torn-tail' && layout.completeBytes === 0) {
|
|
410
|
+
console.error(`session ${id}: ${layout.issue}; no salvageable prefix — nothing to repair, original left untouched`)
|
|
411
|
+
process.exit(1)
|
|
412
|
+
}
|
|
235
413
|
// Content repair (renumber/truncate) applies when the event stream is
|
|
236
|
-
// damaged; a layout-damaged log (e.g. whole-log single frame) is re-framed
|
|
237
|
-
|
|
238
|
-
|
|
414
|
+
// damaged; a layout-damaged log (e.g. whole-log single frame) is re-framed;
|
|
415
|
+
// a torn tail is dropped by construction (the text only holds complete
|
|
416
|
+
// frames) and re-framed together with the rest.
|
|
417
|
+
const plan = issue === undefined ? undefined : repairEvents(events, issue, {
|
|
418
|
+
duplicateReference: flags.duplicateReference,
|
|
419
|
+
})
|
|
420
|
+
if (plan !== undefined && plan.action === 'refuse') {
|
|
421
|
+
printAmbiguity(id, plan, events)
|
|
422
|
+
console.log(' no write was performed; re-run with a strategy or inspect the log manually')
|
|
423
|
+
process.exit(1)
|
|
424
|
+
}
|
|
425
|
+
if (layout.status !== 'healthy') console.log(`session ${id}: ${damageLine(layout, events.length === 0 ? undefined : events[events.length - 1])}`)
|
|
239
426
|
if (issue !== undefined) console.log(`session ${id}: ${issue.message}`)
|
|
240
427
|
console.log(` file: ${artifact.path}`)
|
|
241
|
-
|
|
428
|
+
const storageRows = text.split('\n').filter(line => line !== '').length - 1
|
|
429
|
+
if (layout.status === 'torn-tail') {
|
|
430
|
+
console.log(` plan: truncate the torn tail — keep ${layout.completeBytes} of ${layout.totalBytes} bytes (${storageRows} storage row(s), ${events.length} expanded event(s)); events lost in the torn tail: unknown`)
|
|
431
|
+
} else if (layout.status !== 'healthy') {
|
|
242
432
|
console.log(' plan: re-frame the log (dsh requires the header line alone in the first frame, one frame per flush batch)')
|
|
243
433
|
}
|
|
244
434
|
if (plan !== undefined) {
|
|
245
435
|
if (plan.action === 'renumber') {
|
|
246
|
-
|
|
436
|
+
const resolved = plan.resolvedStrategy === undefined ? '' : `, ${plan.ambiguous.length} ambiguous reference(s) resolved as ${plan.resolvedStrategy}`
|
|
437
|
+
console.log(` plan: renumber ${plan.changed} event(s) from seq ${issue.got} by +${plan.delta} (${plan.refsChanged} reference(s) remapped${resolved})`)
|
|
247
438
|
} else if (plan.action === 'truncate') {
|
|
248
439
|
console.log(` plan: truncate ${plan.changed} trailing event(s) after index ${plan.fromIndex} (missing/undecodable events cannot be recreated)`)
|
|
249
440
|
}
|
|
@@ -255,23 +446,29 @@ function main() {
|
|
|
255
446
|
}
|
|
256
447
|
|
|
257
448
|
const output = encodeLog(header, plan === undefined ? events : plan.events)
|
|
258
|
-
const backup = writeArtifact(artifact.path, artifact.compression, output)
|
|
449
|
+
const backup = writeArtifact(artifact.path, artifact.compression, output, decodeStorageRecord)
|
|
259
450
|
console.log(` backup: ${backup}`)
|
|
260
451
|
console.log(` wrote: ${artifact.path}`)
|
|
261
452
|
|
|
262
|
-
//
|
|
453
|
+
// Final confirmation on the ACTIVE path (the tmp was already verified
|
|
454
|
+
// before the rename; this re-checks the same bytes in place).
|
|
263
455
|
const verifyArtifact = readArtifact(artifact.path, artifact.compression)
|
|
264
456
|
const verifyEvents = scanEvents(verifyArtifact.text, decodeStorageRecord)
|
|
265
|
-
if (verifyArtifact.
|
|
266
|
-
console.error(` VERIFY FAILED: ${verifyArtifact.
|
|
457
|
+
if (verifyArtifact.layout.status !== 'healthy' || verifyEvents.issue !== undefined) {
|
|
458
|
+
console.error(` VERIFY FAILED: ${verifyArtifact.layout.status !== 'healthy' ? verifyArtifact.layout.issue : verifyEvents.issue.message} — original preserved in ${backup}`)
|
|
267
459
|
process.exit(1)
|
|
268
460
|
}
|
|
269
461
|
console.log(` verify: valid frame layout, contiguous, ${verifyEvents.events.length} event(s)`)
|
|
270
462
|
}
|
|
271
463
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
464
|
+
const isMain = process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
465
|
+
if (isMain) {
|
|
466
|
+
try {
|
|
467
|
+
main(process.argv.slice(2))
|
|
468
|
+
} catch (error) {
|
|
469
|
+
console.error(`repair-session: ${error instanceof Error ? error.message : String(error)}`)
|
|
470
|
+
process.exit(1)
|
|
471
|
+
}
|
|
277
472
|
}
|
|
473
|
+
|
|
474
|
+
export { writeArtifact, verifyArtifactFile }
|