@ddtcorex/dsh-maestro-supervisor 0.7.2 → 0.7.3
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/lib/debug-agent.d.ts +0 -2
- package/lib/debug-agent.js +4 -340
- package/lib/plugin.d.ts +93 -0
- package/lib/plugin.js +233 -2
- package/lib/resume-tools.d.ts +101 -0
- package/lib/resume-tools.js +248 -0
- package/lib/session-health.d.ts +83 -0
- package/lib/session-health.js +268 -0
- package/package.json +3 -2
- package/skills/dsh-safe-restart/scripts/restart-dsh-web.sh +19 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-log health scan.
|
|
3
|
+
*
|
|
4
|
+
* Deeper resilience for dsh-safe-restart pre-flight: after (or before) a
|
|
5
|
+
* restart, individual session logs under a project session dir can end up in
|
|
6
|
+
* three distinct shapes:
|
|
7
|
+
*
|
|
8
|
+
* 1. healthy multi-frame — one frame per line (a header-only first frame,
|
|
9
|
+
* then per-event frames). The DSH reader depends on
|
|
10
|
+
* this shape to fast-seek the session header.
|
|
11
|
+
* 2. single-frame whole-log — the entire log was written as ONE zstd frame.
|
|
12
|
+
* Data is fully intact (whole-file decode works) but
|
|
13
|
+
* the header cannot be located by a frame walk.
|
|
14
|
+
* 3. corrupt first frame — the first frame cannot be decoded at all.
|
|
15
|
+
*
|
|
16
|
+
* Classification uses fzstd's whole-file decode + streaming Decompress and
|
|
17
|
+
* avoids a hand-rolled zstd frame-header walker (empirically wrong on both
|
|
18
|
+
* real file shapes; fzstd whole-file decode already handles single and
|
|
19
|
+
* concatenated frames). The first-frame probe only ever feeds the streaming
|
|
20
|
+
* decoder a bounded 64 KiB head (log2 scans of ≤256-byte-prefix steps), so it
|
|
21
|
+
* is stack-safe even on long-running many-frame logs — feeding a whole file
|
|
22
|
+
* into the streaming decoder recurses per frame and overflows the V8 stack.
|
|
23
|
+
*
|
|
24
|
+
* Repair re-encodes a single-frame whole-log into the canonical multi-frame
|
|
25
|
+
* shape (frame #1 = exactly the header line, trailing line-batched frames),
|
|
26
|
+
* byte-preserving every event; the original file is kept as a sidecar backup
|
|
27
|
+
* and the replace is atomic (write temp + rename).
|
|
28
|
+
*
|
|
29
|
+
* NOTE: fzstd is a decode-only pure-JS zstd library; re-encoding uses Node's
|
|
30
|
+
* native zlib zstd encoder, whose frames fzstd (and the zstd CLI / DSH's
|
|
31
|
+
* reader) decode as standard concatenated frames.
|
|
32
|
+
*/
|
|
33
|
+
export type SessionLogClass = 'ok' | 'single-frame-whole-log' | 'corrupt-first-frame' | 'not-a-session-log';
|
|
34
|
+
export interface SessionHealthEntry {
|
|
35
|
+
path: string;
|
|
36
|
+
klass: SessionLogClass;
|
|
37
|
+
remark?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Classify a single session log file per the validated algorithm.
|
|
41
|
+
*
|
|
42
|
+
* Fast path (dominant case): classify a healthy multi-frame log from its first
|
|
43
|
+
* frame alone — the bounded 64 KiB head probe finds the first-frame boundary,
|
|
44
|
+
* decodes just that frame, and a header-only first frame means `ok` WITHOUT
|
|
45
|
+
* ever decoding the whole file.
|
|
46
|
+
*
|
|
47
|
+
* Whole-file decode is ONLY the fallback corrupt gate:
|
|
48
|
+
* - first frame decodes but is multi-line (or the probe found no boundary
|
|
49
|
+
* within 64 KiB — a first frame bigger than the cap is never a canonical
|
|
50
|
+
* healthy log): whole decode throws → 'corrupt-first-frame', succeeds →
|
|
51
|
+
* 'single-frame-whole-log' (whole decodes to exactly one line → 'ok').
|
|
52
|
+
*
|
|
53
|
+
* Empty files are not session logs.
|
|
54
|
+
*/
|
|
55
|
+
export declare function classifySessionLog(path: string): Promise<SessionHealthEntry>;
|
|
56
|
+
/**
|
|
57
|
+
* Re-encode a single-frame-whole-log into canonical multi-frame form:
|
|
58
|
+
* frame #1 = exactly the header line, then line-batched trailing frames.
|
|
59
|
+
* The original file is preserved as `path + backupSuffix` (first time only)
|
|
60
|
+
* and the replace is atomic (temp file + rename).
|
|
61
|
+
*/
|
|
62
|
+
export declare function repairSingleFrameLog(path: string, backupSuffix?: string): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Walk `root` recursively and classify every `session.jsonl.zstd` found.
|
|
65
|
+
* The process-write layout is <dshHome>/sessions/<project>/<session>/, but we
|
|
66
|
+
* recurse generically so tests and future layouts work unchanged.
|
|
67
|
+
*/
|
|
68
|
+
export declare function scanSessionLogs(root: string): Promise<SessionHealthEntry[]>;
|
|
69
|
+
/**
|
|
70
|
+
* Health check over a session-log root: repair single-frame logs (atomic
|
|
71
|
+
* re-encode), optionally quarantine corrupt-first-frame logs aside, and
|
|
72
|
+
* report counts. `remaining` counts unhealthy entries left untouched
|
|
73
|
+
* (not-a-session-log, corrupt frames when quarantine is off, and
|
|
74
|
+
* single-frame logs when repair is off).
|
|
75
|
+
*/
|
|
76
|
+
export declare function runSessionHealthCheck(root: string, opts?: {
|
|
77
|
+
repair?: boolean;
|
|
78
|
+
quarantine?: boolean;
|
|
79
|
+
}): Promise<{
|
|
80
|
+
fixed: number;
|
|
81
|
+
quarantined: number;
|
|
82
|
+
remaining: number;
|
|
83
|
+
}>;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-log health scan.
|
|
3
|
+
*
|
|
4
|
+
* Deeper resilience for dsh-safe-restart pre-flight: after (or before) a
|
|
5
|
+
* restart, individual session logs under a project session dir can end up in
|
|
6
|
+
* three distinct shapes:
|
|
7
|
+
*
|
|
8
|
+
* 1. healthy multi-frame — one frame per line (a header-only first frame,
|
|
9
|
+
* then per-event frames). The DSH reader depends on
|
|
10
|
+
* this shape to fast-seek the session header.
|
|
11
|
+
* 2. single-frame whole-log — the entire log was written as ONE zstd frame.
|
|
12
|
+
* Data is fully intact (whole-file decode works) but
|
|
13
|
+
* the header cannot be located by a frame walk.
|
|
14
|
+
* 3. corrupt first frame — the first frame cannot be decoded at all.
|
|
15
|
+
*
|
|
16
|
+
* Classification uses fzstd's whole-file decode + streaming Decompress and
|
|
17
|
+
* avoids a hand-rolled zstd frame-header walker (empirically wrong on both
|
|
18
|
+
* real file shapes; fzstd whole-file decode already handles single and
|
|
19
|
+
* concatenated frames). The first-frame probe only ever feeds the streaming
|
|
20
|
+
* decoder a bounded 64 KiB head (log2 scans of ≤256-byte-prefix steps), so it
|
|
21
|
+
* is stack-safe even on long-running many-frame logs — feeding a whole file
|
|
22
|
+
* into the streaming decoder recurses per frame and overflows the V8 stack.
|
|
23
|
+
*
|
|
24
|
+
* Repair re-encodes a single-frame whole-log into the canonical multi-frame
|
|
25
|
+
* shape (frame #1 = exactly the header line, trailing line-batched frames),
|
|
26
|
+
* byte-preserving every event; the original file is kept as a sidecar backup
|
|
27
|
+
* and the replace is atomic (write temp + rename).
|
|
28
|
+
*
|
|
29
|
+
* NOTE: fzstd is a decode-only pure-JS zstd library; re-encoding uses Node's
|
|
30
|
+
* native zlib zstd encoder, whose frames fzstd (and the zstd CLI / DSH's
|
|
31
|
+
* reader) decode as standard concatenated frames.
|
|
32
|
+
*/
|
|
33
|
+
import { decompress, Decompress } from 'fzstd';
|
|
34
|
+
import { copyFileSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
35
|
+
import { zstdCompressSync } from 'node:zlib';
|
|
36
|
+
import { join, basename } from 'node:path';
|
|
37
|
+
/**
|
|
38
|
+
* Cap on the file head probed through the streaming decoder. Feeding a whole
|
|
39
|
+
* long-running log (thousands of concatenated zstd frames) into fzstd's
|
|
40
|
+
* streaming `Decompress` in one push recurses once per frame and overflows the
|
|
41
|
+
* V8 stack (RangeError: Maximum call stack size exceeded, seen on a real
|
|
42
|
+
* ~1.8 MB session log). Only the first frame matters for classification, so we
|
|
43
|
+
* never probe past 64 KiB — a canonical log's first frame is a few hundred
|
|
44
|
+
* bytes, and a frame can never legitimately straddle this cap.
|
|
45
|
+
*/
|
|
46
|
+
const FIRST_FRAME_CAP = 64 * 1024;
|
|
47
|
+
/** Standard zstd frame encoder (node:zlib) — decode-compatible with fzstd. */
|
|
48
|
+
function compress(data) {
|
|
49
|
+
return zstdCompressSync(data);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Smallest head prefix at which the streaming decoder first emits output
|
|
53
|
+
* (≈ first-frame completion), searched only within FIRST_FRAME_CAP. Never
|
|
54
|
+
* pushes more than the cap into the decoder, so the probe is stack-safe on
|
|
55
|
+
* arbitrarily large many-frame logs. A push that throws on invalid bytes past
|
|
56
|
+
* the first decoded frame is harmless to the boundary search — once the first
|
|
57
|
+
* frame has emitted (`fired`), the boundary is already known, so the throw is
|
|
58
|
+
* swallowed. Only garbage that decodes nothing yields `{ found: false }`.
|
|
59
|
+
*/
|
|
60
|
+
function firstFrameHead(buf) {
|
|
61
|
+
const emits = (len) => {
|
|
62
|
+
let fired = false;
|
|
63
|
+
const d = new Decompress((chunk) => { if (chunk.length > 0)
|
|
64
|
+
fired = true; });
|
|
65
|
+
try {
|
|
66
|
+
d.push(buf.subarray(0, len));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
// Invalid bytes past the first frame (corrupt tail): the decoder had
|
|
70
|
+
// already emitted the first frame's output → keep `fired` as-is.
|
|
71
|
+
}
|
|
72
|
+
return fired;
|
|
73
|
+
};
|
|
74
|
+
const cap = Math.min(buf.length, FIRST_FRAME_CAP);
|
|
75
|
+
let hi = cap;
|
|
76
|
+
for (let l = 256; l <= cap; l += 256)
|
|
77
|
+
if (emits(l)) {
|
|
78
|
+
hi = l;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (!emits(cap))
|
|
82
|
+
return { found: false }; // no frame boundary within the cap
|
|
83
|
+
let lo = 0;
|
|
84
|
+
while (lo + 1 < hi) {
|
|
85
|
+
const mid = (lo + hi) >> 1;
|
|
86
|
+
emits(mid) ? hi = mid : lo = mid;
|
|
87
|
+
}
|
|
88
|
+
return { found: true, end: hi };
|
|
89
|
+
}
|
|
90
|
+
function isSingleHeaderLine(plain) {
|
|
91
|
+
if (plain.length === 0)
|
|
92
|
+
return false;
|
|
93
|
+
const nl = plain.indexOf(0x0a);
|
|
94
|
+
return nl === plain.length - 1; // exactly one line, terminated by '\n'
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Classify a single session log file per the validated algorithm.
|
|
98
|
+
*
|
|
99
|
+
* Fast path (dominant case): classify a healthy multi-frame log from its first
|
|
100
|
+
* frame alone — the bounded 64 KiB head probe finds the first-frame boundary,
|
|
101
|
+
* decodes just that frame, and a header-only first frame means `ok` WITHOUT
|
|
102
|
+
* ever decoding the whole file.
|
|
103
|
+
*
|
|
104
|
+
* Whole-file decode is ONLY the fallback corrupt gate:
|
|
105
|
+
* - first frame decodes but is multi-line (or the probe found no boundary
|
|
106
|
+
* within 64 KiB — a first frame bigger than the cap is never a canonical
|
|
107
|
+
* healthy log): whole decode throws → 'corrupt-first-frame', succeeds →
|
|
108
|
+
* 'single-frame-whole-log' (whole decodes to exactly one line → 'ok').
|
|
109
|
+
*
|
|
110
|
+
* Empty files are not session logs.
|
|
111
|
+
*/
|
|
112
|
+
export async function classifySessionLog(path) {
|
|
113
|
+
let buf;
|
|
114
|
+
try {
|
|
115
|
+
buf = readFileSync(path);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return { path, klass: 'not-a-session-log' };
|
|
119
|
+
}
|
|
120
|
+
if (buf.length === 0)
|
|
121
|
+
return { path, klass: 'not-a-session-log' };
|
|
122
|
+
const head = firstFrameHead(buf);
|
|
123
|
+
if (head.found) {
|
|
124
|
+
let first;
|
|
125
|
+
try {
|
|
126
|
+
first = Buffer.from(decompress(buf.subarray(0, head.end)));
|
|
127
|
+
}
|
|
128
|
+
catch { /* prefix not a complete frame */ }
|
|
129
|
+
if (first?.byteLength !== undefined && isSingleHeaderLine(first))
|
|
130
|
+
return { path, klass: 'ok' };
|
|
131
|
+
// First frame decodes but is multi-line (or the prefix is not a complete
|
|
132
|
+
// frame): the whole file must still decode for this to be recoverable.
|
|
133
|
+
try {
|
|
134
|
+
decompress(buf);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { path, klass: 'corrupt-first-frame' };
|
|
138
|
+
}
|
|
139
|
+
return { path, klass: 'single-frame-whole-log' };
|
|
140
|
+
}
|
|
141
|
+
// No frame boundary within the cap — first frame is bigger than 64 KiB, never
|
|
142
|
+
// a canonical healthy log. The whole-file decode is authoritative.
|
|
143
|
+
let whole;
|
|
144
|
+
try {
|
|
145
|
+
whole = Buffer.from(decompress(buf));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return { path, klass: 'corrupt-first-frame' };
|
|
149
|
+
}
|
|
150
|
+
if (isSingleHeaderLine(whole))
|
|
151
|
+
return { path, klass: 'ok' };
|
|
152
|
+
return { path, klass: 'single-frame-whole-log' }; // whole-file decodes; first frame isn't header-only → recoverable
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Re-encode a single-frame-whole-log into canonical multi-frame form:
|
|
156
|
+
* frame #1 = exactly the header line, then line-batched trailing frames.
|
|
157
|
+
* The original file is preserved as `path + backupSuffix` (first time only)
|
|
158
|
+
* and the replace is atomic (temp file + rename).
|
|
159
|
+
*/
|
|
160
|
+
export async function repairSingleFrameLog(path, backupSuffix = '.corrupt-singleframe.bak') {
|
|
161
|
+
const buf = readFileSync(path);
|
|
162
|
+
const whole = Buffer.from(decompress(buf));
|
|
163
|
+
const nl = whole.indexOf(0x0a);
|
|
164
|
+
const header = whole.subarray(0, nl + 1);
|
|
165
|
+
const rest = whole.subarray(nl + 1);
|
|
166
|
+
if (!existsSync(path + backupSuffix))
|
|
167
|
+
copyFileSync(path, path + backupSuffix);
|
|
168
|
+
const frames = [compress(header)];
|
|
169
|
+
let start = 0;
|
|
170
|
+
for (let i = 0; i < rest.length; i++) {
|
|
171
|
+
if (rest[i] === 0x0a && i - start + 1 >= 64 * 1024) {
|
|
172
|
+
frames.push(compress(rest.subarray(start, i + 1)));
|
|
173
|
+
start = i + 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (start < rest.length)
|
|
177
|
+
frames.push(compress(rest.subarray(start)));
|
|
178
|
+
const tmp = path + '.repair.tmp';
|
|
179
|
+
writeFileSync(tmp, Buffer.concat(frames));
|
|
180
|
+
renameSync(tmp, path);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Walk `root` recursively and classify every `session.jsonl.zstd` found.
|
|
184
|
+
* The process-write layout is <dshHome>/sessions/<project>/<session>/, but we
|
|
185
|
+
* recurse generically so tests and future layouts work unchanged.
|
|
186
|
+
*/
|
|
187
|
+
export async function scanSessionLogs(root) {
|
|
188
|
+
const found = [];
|
|
189
|
+
const walk = (dir) => {
|
|
190
|
+
let entries;
|
|
191
|
+
try {
|
|
192
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
for (const e of entries) {
|
|
198
|
+
const fp = join(dir, e.name);
|
|
199
|
+
if (e.isDirectory())
|
|
200
|
+
walk(fp);
|
|
201
|
+
else if (e.isFile() && basename(e.name) === 'session.jsonl.zstd')
|
|
202
|
+
found.push(fp);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
walk(root);
|
|
206
|
+
const out = [];
|
|
207
|
+
for (const p of found) {
|
|
208
|
+
try {
|
|
209
|
+
out.push(await classifySessionLog(p));
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
// One unreadable / unclassifiable file must never abort the whole scan.
|
|
213
|
+
out.push({ path: p, klass: 'corrupt-first-frame', remark: err instanceof Error ? err.message : String(err) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Health check over a session-log root: repair single-frame logs (atomic
|
|
220
|
+
* re-encode), optionally quarantine corrupt-first-frame logs aside, and
|
|
221
|
+
* report counts. `remaining` counts unhealthy entries left untouched
|
|
222
|
+
* (not-a-session-log, corrupt frames when quarantine is off, and
|
|
223
|
+
* single-frame logs when repair is off).
|
|
224
|
+
*/
|
|
225
|
+
export async function runSessionHealthCheck(root, opts = {}) {
|
|
226
|
+
const repair = opts.repair ?? true;
|
|
227
|
+
const quarantine = opts.quarantine ?? false;
|
|
228
|
+
let fixed = 0;
|
|
229
|
+
let quarantined = 0;
|
|
230
|
+
let remaining = 0;
|
|
231
|
+
for (const entry of await scanSessionLogs(root)) {
|
|
232
|
+
try {
|
|
233
|
+
switch (entry.klass) {
|
|
234
|
+
case 'single-frame-whole-log':
|
|
235
|
+
if (repair) {
|
|
236
|
+
await repairSingleFrameLog(entry.path);
|
|
237
|
+
fixed++;
|
|
238
|
+
}
|
|
239
|
+
else
|
|
240
|
+
remaining++;
|
|
241
|
+
break;
|
|
242
|
+
case 'corrupt-first-frame':
|
|
243
|
+
if (quarantine) {
|
|
244
|
+
let aside = entry.path + '.corrupt-' + Date.now() + '.bak';
|
|
245
|
+
let n = 0;
|
|
246
|
+
while (existsSync(aside))
|
|
247
|
+
aside = entry.path + '.corrupt-' + Date.now() + '-' + (++n) + '.bak';
|
|
248
|
+
renameSync(entry.path, aside);
|
|
249
|
+
quarantined++;
|
|
250
|
+
}
|
|
251
|
+
else
|
|
252
|
+
remaining++;
|
|
253
|
+
break;
|
|
254
|
+
case 'not-a-session-log':
|
|
255
|
+
remaining++;
|
|
256
|
+
break;
|
|
257
|
+
default:
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// Repair/quarantine of a single entry failed — it stays unhealthy and
|
|
263
|
+
// can never abort the whole pass. Counts always reconcile.
|
|
264
|
+
remaining++;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { fixed, quarantined, remaining };
|
|
268
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddtcorex/dsh-maestro-supervisor",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "Supervisor daemon for DSH Web resilience — auto-detect crashes, rollback to LKG, report",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -38,7 +38,8 @@
|
|
|
38
38
|
"cordis.patch.yml"
|
|
39
39
|
],
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@ddtcorex/dsh-maestro-config-lib": "^0.1.2"
|
|
41
|
+
"@ddtcorex/dsh-maestro-config-lib": "^0.1.2",
|
|
42
|
+
"fzstd": "0.1.1"
|
|
42
43
|
},
|
|
43
44
|
"devDependencies": {
|
|
44
45
|
"@types/node": "^26.3.0",
|
|
@@ -9,6 +9,13 @@ log="${DSH_RESTART_LOG:-/tmp/dsh-web-restart.log}"
|
|
|
9
9
|
# down poll as a crash unless this marker is fresh, so it never races this
|
|
10
10
|
# script's own kill -> dry-boot -> relaunch sequence with its own rollback.
|
|
11
11
|
marker="${DSH_SUPERVISOR_MARKER:-$HOME/.dsh/.supervisor/planned-restart}"
|
|
12
|
+
# Session-log pre-flight env (see lib/session-health.ts): SESSIONS_ROOT is the
|
|
13
|
+
# operator DSH store's sessions dir (default <dsh home>/sessions) and may be
|
|
14
|
+
# overridden per invocation. PLUGIN_DIR is this package's root, derived from
|
|
15
|
+
# the script's own path — never a hard-coded location.
|
|
16
|
+
dsh_home="${DSH_HOME:-$HOME/.dsh}"
|
|
17
|
+
export SESSIONS_ROOT="${SESSIONS_ROOT:-$dsh_home/sessions}"
|
|
18
|
+
PLUGIN_DIR="${DSH_SUPERVISOR_PLUGIN_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
|
|
12
19
|
confirmed=false
|
|
13
20
|
dry_run=false
|
|
14
21
|
auto_mode=false
|
|
@@ -249,6 +256,18 @@ for port in 3000 3080; do
|
|
|
249
256
|
fi
|
|
250
257
|
done
|
|
251
258
|
|
|
259
|
+
# Pre-flight: never let a corrupt session log brick the boot
|
|
260
|
+
# Runs only AFTER the old tree is stopped and the ports are free (above) and
|
|
261
|
+
# BEFORE the fresh boot below, so a live writer can never race the repair.
|
|
262
|
+
# Deliberately NON-FATAL (the block's exit status is dropped): a transient
|
|
263
|
+
# scan error must never block a legitimate restart.
|
|
264
|
+
if [ -n "$SESSIONS_ROOT" ] && [ -d "$SESSIONS_ROOT" ]; then
|
|
265
|
+
node --input-type=module -e "import('${PLUGIN_DIR}/lib/session-health.js').then(async m => {
|
|
266
|
+
const r = await m.runSessionHealthCheck(process.env.SESSIONS_ROOT, { repair: true, quarantine: false });
|
|
267
|
+
console.log('[session-health] fixed=' + r.fixed + ' quarantined=' + r.quarantined + ' remaining=' + r.remaining);
|
|
268
|
+
})" || true
|
|
269
|
+
fi
|
|
270
|
+
|
|
252
271
|
command -v curl >/dev/null 2>&1 || fail 'required command is unavailable: curl'
|
|
253
272
|
|
|
254
273
|
if [[ "$systemd_managed" == true ]]; then
|