@moonquake2004/dsh-doctor 0.4.13 → 0.4.15

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.
Files changed (2) hide show
  1. package/dsh-doctor.mjs +61 -2
  2. package/package.json +1 -1
package/dsh-doctor.mjs CHANGED
@@ -54,11 +54,11 @@
54
54
  * 退出码:0 = 全部通过;1 = 发现可修复问题(内置 + catalog severity=error);warn 级失败不改退出码。
55
55
  */
56
56
  import { execFileSync, spawnSync } from 'node:child_process';
57
- import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from 'node:fs';
57
+ import { closeSync, existsSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
58
58
  import { createRequire } from 'node:module';
59
59
  import net from 'node:net';
60
60
  import { basename, delimiter as PATH_DELIM, dirname, join, relative, resolve } from 'node:path';
61
- import { homedir } from 'node:os';
61
+ import { homedir, tmpdir } from 'node:os';
62
62
  import { pathToFileURL, fileURLToPath } from 'node:url';
63
63
 
64
64
  const HOME = process.env.DSH_HOME || join(homedir(), '.dsh');
@@ -1149,6 +1149,59 @@ function checkSession(targetPath) {
1149
1149
  } else {
1150
1150
  report('session', 'S9', true, '非 zstd 输入,跳过容器检查', undefined);
1151
1151
  }
1152
+
1153
+ // S13:会话头完整性(#6651)——首行必须是 {"type":"session",...}
1154
+ // 实测:首行被写成事件时 dsh-doctor 的其余 S 检查**全部照常通过**(它们看的是事件流),
1155
+ // S11 甚至会报"均健康";而 harness 侧会 `corrupt Zstandard session log` 直接拒绝启动 `dsh web`。
1156
+ {
1157
+ const firstLine = String(text).split('\n').find((l) => l.trim());
1158
+ let header = null;
1159
+ try { header = firstLine ? JSON.parse(firstLine) : null; } catch { header = null; }
1160
+ if (!header || header.type !== 'session') {
1161
+ report('session', 'S13', false,
1162
+ `日志首行不是会话头(type=${header && header.type ? JSON.stringify(header.type) : '缺失/无法解析'})——#6651:此类损坏会让 \`dsh web\` **整体启动失败**(corrupt Zstandard session log: first frame is not exactly …),会话列表一并损坏`,
1163
+ '优先从备份恢复该日志;无备份时把整个会话目录**移出** sessions/(隔离,勿删)后重启 dsh,再用本工具复查');
1164
+ } else {
1165
+ // 更精确的判据:harness 要求的是「**第一个 zstd 帧恰好只含一行**(即头行)」,
1166
+ // 见 dsh-session-persistence-jsonl:1891/2185 —— 首帧里若多裹了事件,同样抛
1167
+ // corrupt Zstandard session log。只查"首行是不是头"会漏掉帧边界错位这一类。
1168
+ let frameNote = '';
1169
+ let frameBad = false;
1170
+ if (target.endsWith('.zstd')) {
1171
+ try {
1172
+ const raw = readFileSync(target);
1173
+ const magic = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
1174
+ const offs = [];
1175
+ for (let i = 0; i <= raw.length - 4; i++) {
1176
+ if (raw[i] === magic[0] && raw[i + 1] === magic[1] && raw[i + 2] === magic[2] && raw[i + 3] === magic[3]) offs.push(i);
1177
+ }
1178
+ if (offs.length >= 2) {
1179
+ const tmp = join(tmpdir(), `dsh-doctor-firstframe-${process.pid}.zst`);
1180
+ writeFileSync(tmp, raw.subarray(0, offs[1]));
1181
+ let first = '';
1182
+ try { first = execFileSync('zstd', ['-dc', tmp], { maxBuffer: 16 * 1024 * 1024 }).toString('utf8'); }
1183
+ finally { try { unlinkSync(tmp); } catch { /* 清理失败不致命 */ } }
1184
+ const frameLines = first.split('\n').filter((l) => l.trim());
1185
+ if (frameLines.length !== 1) {
1186
+ frameBad = true;
1187
+ frameNote = `首帧含 ${frameLines.length} 行(应为 1 行头)——帧边界错位同样会让 harness 抛 corrupt Zstandard session log`;
1188
+ }
1189
+ } else {
1190
+ frameBad = true;
1191
+ frameNote = '仅 1 个 zstd 帧(首帧裹住了全部行)——harness 要求首帧恰好一行,与 #1043 同族';
1192
+ }
1193
+ } catch { frameNote = '(首帧边界无法确认,未据此判定)'; }
1194
+ }
1195
+ if (frameBad) {
1196
+ report('session', 'S13', false,
1197
+ `会话头存在但**首帧边界不合规**:${frameNote}`,
1198
+ '优先从备份恢复;无备份时把整个会话目录移出 sessions/(隔离,勿删)后重启 dsh,再用本工具复查');
1199
+ } else {
1200
+ report('session', 'S13', true,
1201
+ `会话头完整(type=session, version=${header.version ?? '?'}, id=${String(header.id ?? '').slice(0, 12)})${frameNote}`);
1202
+ }
1203
+ }
1204
+ }
1152
1205
  const calls = new Map(); const results2 = new Set(); let maxSeq = -1;
1153
1206
  const turnStarts = new Set(); const turnEnds = new Set();
1154
1207
  const positions = []; const endSeedSeqs = [];
@@ -1462,11 +1515,17 @@ function scanAllSessions() {
1462
1515
  totalDS += ds;
1463
1516
  // 轻量损坏扫描:seq==index + end-seed 重放 + 未知类型
1464
1517
  const problems = [];
1518
+ let firstRowSeen = false;
1465
1519
  let evIndex = 0, lastSeed = -1, seedIdx = -1, posList = [];
1466
1520
  const lines = text.split('\n');
1467
1521
  for (let li = 0; li < lines.length; li++) {
1468
1522
  const ln = lines[li]; if (!ln.trim()) continue;
1469
1523
  let d; try { d = JSON.parse(ln); } catch { problems.push(`行 ${li + 1} 无法解析`); continue; }
1524
+ // #6651:首行必须是会话头——否则 harness 拒绝启动 dsh web,而其余检查看不出问题
1525
+ if (!firstRowSeen) {
1526
+ firstRowSeen = true;
1527
+ if (d.type !== 'session') problems.push(`首行不是会话头(#6651 启动阻断,实际 type=${d.type})`);
1528
+ }
1470
1529
  if (!STORAGE_ROW_TYPES.has(d.type) && !READABLE.has(d.type) && d.ignorable !== true) problems.push(`未知类型 ${d.type}`);
1471
1530
  if (d.type === 'session/end-seed' && typeof d.seq === 'number') { lastSeed = d.seq; seedIdx = posList.length; }
1472
1531
  const t = d.type;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moonquake2004/dsh-doctor",
3
- "version": "0.4.13",
3
+ "version": "0.4.15",
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": [