@moonquake2004/dsh-doctor 0.5.0 → 0.5.1
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/CHANGELOG.md +21 -0
- package/dsh-doctor.mjs +58 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -16,6 +16,27 @@
|
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
+
## [0.5.1] — 2026-09-15
|
|
20
|
+
|
|
21
|
+
### Fixed — 把"读取间歇性失败"错判成"文件损坏"(社区 #6739)
|
|
22
|
+
|
|
23
|
+
#6739 报告:同一个 25MB 会话每次重读时 `corrupt Zstandard session log: frame at byte N failed validation` 里的 **N 每次不同**,
|
|
24
|
+
在 DSH 外手动重试**立即成功** —— 即那不是文件损坏,而是**偶发的读取/校验失败**(存储、内存或驱动层面的抖动)。
|
|
25
|
+
宿主不重试(0.1.6-alpha.1 的三处 throw 仍无重试),一次失败就整场历史读不出来。
|
|
26
|
+
|
|
27
|
+
**我们此前的做法同样糟**:`catch { problems: ['解压/读取失败'] }` → 直接把这种日志记成**损坏**。
|
|
28
|
+
|
|
29
|
+
现在会话读取带重试,并把三种状态分开:
|
|
30
|
+
| 尝试结果 | 判定 | 输出 |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| 首次即成功 | 正常 | 照旧 |
|
|
33
|
+
| **重试后成功** | **间歇性失败(非损坏)** | 单列一条,说明"文件没坏,是读取路径在抖",并指向存储健康排查 |
|
|
34
|
+
| 三次全失败 | 倾向损坏 | 照旧按损坏处理 |
|
|
35
|
+
|
|
36
|
+
### Changed — ⚠️ 判定变化
|
|
37
|
+
|
|
38
|
+
- 此前被判为"损坏会话"的日志,若重试能读出,**不再计入损坏**,改列为"读取间歇性失败"(不翻退出码,避免偶发性抖动导致结果跳动)。
|
|
39
|
+
|
|
19
40
|
## [0.5.0] — 2026-09-15
|
|
20
41
|
|
|
21
42
|
累积自 `0.4.6` 的全部变更(中间以 0.4.14–0.4.32 的 PATCH 形式先行发布过,此处归并到 MINOR)。
|
package/dsh-doctor.mjs
CHANGED
|
@@ -1865,7 +1865,8 @@ function scanAllSessions() {
|
|
|
1865
1865
|
// 世代感知:每个会话目录只取权威世代(v3 优先于 v0),与 store 的会话列表对齐
|
|
1866
1866
|
const files = listSessionLogs(root).map((x) => x.f);
|
|
1867
1867
|
if (files.length === 0) { report('session', 'S11', true, '未发现会话日志', undefined); return; }
|
|
1868
|
-
const corrupt = [];
|
|
1868
|
+
const corrupt = [];
|
|
1869
|
+
const unstableReads = []; const oversized = []; const clean = [];
|
|
1869
1870
|
let totalDS = 0; let totalEvents = 0;
|
|
1870
1871
|
for (const f of files) {
|
|
1871
1872
|
const cs = statSync(f).size;
|
|
@@ -1876,8 +1877,16 @@ function scanAllSessions() {
|
|
|
1876
1877
|
for (let i = 0; i <= raw.length - 4; i++) if (raw[i] === magic[0] && raw[i + 1] === magic[1] && raw[i + 2] === magic[2] && raw[i + 3] === magic[3]) frames++;
|
|
1877
1878
|
} catch { corrupt.push({ id: basename(dirname(f)), problems: ['读取失败'] }); continue; }
|
|
1878
1879
|
let text;
|
|
1879
|
-
|
|
1880
|
-
|
|
1880
|
+
{
|
|
1881
|
+
const rd = readSessionText(f);
|
|
1882
|
+
if (rd.state === 'failed') { corrupt.push({ id: basename(dirname(f)), problems: ['解压/读取失败(3 次尝试均失败 → 倾向于文件本身损坏)'] }); continue; }
|
|
1883
|
+
if (rd.state === 'intermittent') {
|
|
1884
|
+
// 重试即成功 → **不是损坏**,是读取路径不稳(社区 #6739 实测:失败帧位置每次不同)
|
|
1885
|
+
unstableReads.push({ id: basename(dirname(f)), attempts: rd.attemptLog.length, sample: rd.attemptLog.find((a) => !a.ok)?.err || '' });
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
text = rd.text;
|
|
1889
|
+
}
|
|
1881
1890
|
const ds = Buffer.byteLength(text, 'utf8');
|
|
1882
1891
|
totalDS += ds;
|
|
1883
1892
|
// 轻量损坏扫描:seq==index + end-seed 重放 + 未知类型
|
|
@@ -1935,11 +1944,18 @@ function scanAllSessions() {
|
|
|
1935
1944
|
const totalRisk = estHeapMB > heapLimit;
|
|
1936
1945
|
if (quars.length) {
|
|
1937
1946
|
report('session', 'S11', false, `全会话扫描:${corrupt.length} 个损坏会话(#1550:冷打开会拖垮服务器): ${quars.join(' | ')}`, `隔离:把这些会话目录移出 ${join(HOME, 'sessions')}(如 mv 到备份目录)`);
|
|
1938
|
-
} else if (oversized.length || totalRisk) {
|
|
1947
|
+
} else if (oversized.length || totalRisk || unstableReads.length) {
|
|
1939
1948
|
const parts = [];
|
|
1949
|
+
// 间歇性读取失败:**不是文件损坏**(重试即成功、失败帧位置每次不同,见 #6739),
|
|
1950
|
+
// 指向存储/内存/驱动层面的偶发读错误。宿主不重试,一次失败就整场读不出(0.1.6-alpha.1 仍如此);
|
|
1951
|
+
// 我们重试后能读出来,所以这里既如实报告、又明确说清"文件没坏"。
|
|
1952
|
+
if (unstableReads.length) parts.push(`${unstableReads.length} 个会话**读取间歇性失败**(重试即成功 → 非损坏,指向存储/内存/驱动的偶发读错误,社区 #6739): ${unstableReads.slice(0, 3).map((u) => `${u.id}(${u.attempts} 次尝试)`).join(' | ')}`);
|
|
1940
1953
|
if (oversized.length) parts.push(`${oversized.length} 个超大会话: ${oversized.map((o) => `${o.id}(${o.dsMB}MB/${o.events}事件)`).join(' | ')}`);
|
|
1941
1954
|
if (totalRisk) parts.push(`工作区估算物化堆 ~${estHeapMB}MB(估算= max(${totalEvents}事件×600B, ${totalMB}MB×6),跨 ${files.length} 会话累积,#1550 场景;阈值 ${heapLimit}MB,可设 DSH_DOCTOR_HEAP_MB)`);
|
|
1942
|
-
report('session', 'S11', true, `⚠ 全会话扫描:${parts.join(';')}(未损坏,可接受或归档)`,
|
|
1955
|
+
report('session', 'S11', true, `⚠ 全会话扫描:${parts.join(';')}(未损坏,可接受或归档)`,
|
|
1956
|
+
unstableReads.length
|
|
1957
|
+
? '间歇性读取失败不是日志问题:先重试读取;持续出现则排查存储健康(SMART)、内存与磁盘驱动(#6739 的证据是失败帧位置每次不同)'
|
|
1958
|
+
: '冷启动会明显变慢;必要时压缩/归档历史会话');
|
|
1943
1959
|
} else {
|
|
1944
1960
|
report('session', 'S11', true, `全会话扫描:${clean.length} 个会话均健康(损坏 0 / 超大 0 / 估算物化堆 ${estHeapMB}MB)`, undefined);
|
|
1945
1961
|
}
|
|
@@ -2485,6 +2501,43 @@ function classifyImportError(msg) {
|
|
|
2485
2501
|
}
|
|
2486
2502
|
|
|
2487
2503
|
|
|
2504
|
+
|
|
2505
|
+
/**
|
|
2506
|
+
* 会话日志读取:**带重试**,并区分"文件损坏"与"读取路径不稳"。
|
|
2507
|
+
*
|
|
2508
|
+
* 社区 #6739 的证据:同一个 25MB 会话,每次重读时 `corrupt Zstandard session log: frame at byte N
|
|
2509
|
+
* failed validation` 里的 **N 每次不同**,而在 DSH 外对同一文件手动重试**立即成功**。
|
|
2510
|
+
* 也就是说——那不是文件损坏,而是**偶发的读取/校验失败**(存储、内存或驱动层面的抖动)。
|
|
2511
|
+
* 宿主不重试,一次失败就整场历史读不出来(0.1.6-alpha.1 仍如此:persistence 的三处 throw 无重试);
|
|
2512
|
+
* 我们此前的做法同样糟糕:直接把这种日志记成"损坏"。
|
|
2513
|
+
*
|
|
2514
|
+
* 现在:失败重试若干次;只要有一次成功,就**不判为损坏**,而是单独报"读取间歇性失败"——
|
|
2515
|
+
* 这是宿主不会给出的区分,也是用户真正需要的那一句:"你的日志没坏,是你的读取路径在抖"。
|
|
2516
|
+
*/
|
|
2517
|
+
function readSessionText(file, attempts = 3) {
|
|
2518
|
+
const attemptLog = [];
|
|
2519
|
+
for (let i = 0; i < attempts; i++) {
|
|
2520
|
+
try {
|
|
2521
|
+
const text = file.endsWith('.zstd')
|
|
2522
|
+
? execFileSync('zstd', ['-dc', file], { maxBuffer: 512 * 1024 * 1024 }).toString('utf8')
|
|
2523
|
+
: readFileSync(file, 'utf8');
|
|
2524
|
+
attemptLog.push({ ok: true });
|
|
2525
|
+
return { text, attemptLog, state: attemptLog.length === 1 ? 'ok' : 'intermittent' };
|
|
2526
|
+
} catch (e) {
|
|
2527
|
+
attemptLog.push({ ok: false, err: String(e.stderr || e.message || '').split('\n').find((l) => /failed validation|error/i.test(l)) || String(e.message || '') });
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
return { text: null, attemptLog, state: 'failed' };
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
/** 由若干次尝试的结果判定状态(纯函数,便于测试)。 */
|
|
2534
|
+
function classifyReadAttempts(attemptLog) {
|
|
2535
|
+
const ok = attemptLog.filter((a) => a.ok).length;
|
|
2536
|
+
if (ok === attemptLog.length) return 'ok';
|
|
2537
|
+
if (ok === 0) return 'failed';
|
|
2538
|
+
return 'intermittent';
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2488
2541
|
/* ---- 预检增强(2026-09):快照对比 + 安全安装 ---- */
|
|
2489
2542
|
|
|
2490
2543
|
const SNAPSHOT_FILE = '.dsh-doctor-snapshot.json';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moonquake2004/dsh-doctor",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Offline diagnostic for DeepSeek Harness — 28 built-in + 5 catalog checks across env/profile/session (Layer A checks-as-data), self-update (Layer B), and a semi-automatic LLM observer (Layer C, --observe); Doctor panel in web UI settings.",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|