dsh-log-contract 0.3.1 → 0.3.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/bin/dsh-log-contract.mjs +8 -4
- package/lib/index.js +2 -2
- package/lib/log-reader.js +43 -0
- package/lib/repair.js +59 -2
- package/package.json +1 -1
package/bin/dsh-log-contract.mjs
CHANGED
|
@@ -23,11 +23,14 @@ const USAGE = `dsh-log-contract —— 日志契约守护(DSH session log cont
|
|
|
23
23
|
--json 输出机器可读 JSON 报告
|
|
24
24
|
--max-details N 每条违规最多列 N 个缺失 seq(默认 8,--json 忽略)
|
|
25
25
|
|
|
26
|
-
dsh-log-contract fix <session-log> [--remove-markers] [--apply] [--backup-dir DIR] [--json]
|
|
26
|
+
dsh-log-contract fix <session-log> [--remove-markers] [--neutralize] [--apply] [--backup-dir DIR] [--json]
|
|
27
27
|
诊断 + 修复(2026-08 事故固化方案)。先做严格 seq 连续扫描 + 契约体检
|
|
28
28
|
(含 W1/W2 wire 级悬空 tool 检查),再按需修复:
|
|
29
29
|
--remove-markers 移除 retrace/message-editor marker 并全量重编号
|
|
30
30
|
(用于大范围遮蔽历史 / marker 漏盖 tool/result)
|
|
31
|
+
--neutralize 原地中和 turn-null marker(type→retrace/marker +
|
|
32
|
+
ignorable:true,删 surfaceOp/sourceEventSeqs,seq/行数不变)
|
|
33
|
+
—— token-meter 不再刷屏,会话驻留也安全(2026-08-30 事故)
|
|
31
34
|
--apply 备份后落盘(.zstd 走官方帧格式重建:帧1=header、
|
|
32
35
|
帧2=其余、带 checksum、单个结尾换行)
|
|
33
36
|
不传 --apply 为干跑(只报告)。
|
|
@@ -168,6 +171,7 @@ function cmdContracts() {
|
|
|
168
171
|
function cmdFix(args) {
|
|
169
172
|
const json = args.includes('--json');
|
|
170
173
|
const removeMarkers = args.includes('--remove-markers');
|
|
174
|
+
const neutralize = args.includes('--neutralize');
|
|
171
175
|
const dropFailedTurns = args.includes('--drop-failed-turns');
|
|
172
176
|
const trimIdx = args.indexOf('--trim-last');
|
|
173
177
|
const trimLast = trimIdx >= 0 && args[trimIdx + 1] ? Number(args[trimIdx + 1]) : undefined;
|
|
@@ -179,7 +183,7 @@ function cmdFix(args) {
|
|
|
179
183
|
const file = args.find((a) => !a.startsWith('-'));
|
|
180
184
|
if (!file) fail(USAGE);
|
|
181
185
|
|
|
182
|
-
const result = repairSession(file, { removeMarkers, dropFailedTurns, trimLast, compactLast, apply, backupDir });
|
|
186
|
+
const result = repairSession(file, { removeMarkers, neutralize, dropFailedTurns, trimLast, compactLast, apply, backupDir });
|
|
183
187
|
if (json) {
|
|
184
188
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
185
189
|
process.exit(result.ok ? 0 : 1);
|
|
@@ -188,7 +192,7 @@ function cmdFix(args) {
|
|
|
188
192
|
process.stdout.write(`\n🔧 dsh-log-contract fix —— ${file}\n`);
|
|
189
193
|
process.stdout.write(` 诊断:${result.issues.length === 0 ? '无问题' : result.issues.map((i) => `[${i.kind}] ${i.detail}`).join('\n ')}\n`);
|
|
190
194
|
if (result.applied) {
|
|
191
|
-
process.stdout.write(` 已应用修复:移除 ${result.removed} 项,重编号 ${result.renumbered}
|
|
195
|
+
process.stdout.write(` 已应用修复:移除 ${result.removed} 项,重编号 ${result.renumbered} 行,中和 ${result.neutralized} 个 turn-null marker(seq ${(result.neutralizedSeqs ?? []).join(',')})\n`);
|
|
192
196
|
process.stdout.write(` 备份:${result.backupPath}\n`);
|
|
193
197
|
process.stdout.write(` 修复后体检:error ${result.check.summary?.bySeverity?.error ?? '?'} | surface ${result.check.summary?.surfaceNodes ?? '?'} 节点\n`);
|
|
194
198
|
} else if (apply && !result.ok) {
|
|
@@ -196,7 +200,7 @@ function cmdFix(args) {
|
|
|
196
200
|
} else if (apply) {
|
|
197
201
|
process.stdout.write(' (--apply 且无问题——无内容可修)\n');
|
|
198
202
|
} else {
|
|
199
|
-
process.stdout.write(` (干跑模式:${result.removed} 项可移除、${result.renumbered}
|
|
203
|
+
process.stdout.write(` (干跑模式:${result.removed} 项可移除、${result.renumbered} 行待重编号、${result.neutralized} 个 turn-null marker 可中和;加 --apply 落盘,--remove-markers / --neutralize / --drop-failed-turns / --trim-last N 启用于对应修复)\n`);
|
|
200
204
|
}
|
|
201
205
|
process.stdout.write('\n');
|
|
202
206
|
process.exit(result.ok ? 0 : 1);
|
package/lib/index.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* 日志契约守护(DSH session log contract guard)。
|
|
5
5
|
* 公开 API:离线体检 + 写前校验 + 修复 + 契约目录。
|
|
6
6
|
*/
|
|
7
|
-
export { loadSessionLog } from './log-reader.js';
|
|
7
|
+
export { loadSessionLog, tailSeq } from './log-reader.js';
|
|
8
8
|
export { validateSessionLog } from './validate.js';
|
|
9
9
|
export { createPreWriter, preWriterFromLog } from './prewrite.js';
|
|
10
|
-
export { repairSession, strictScanText, removeMarkersText, dropFailedTurnsText, trimLastMessagesText, compactLastMessagesText, rebuildZstdText } from './repair.js';
|
|
10
|
+
export { repairSession, strictScanText, removeMarkersText, neutralizeMarkersText, dropFailedTurnsText, trimLastMessagesText, compactLastMessagesText, rebuildZstdText } from './repair.js';
|
|
11
11
|
export { CONTRACT_RULES, LAYER, SEVERITY, ruleById } from './contracts.js';
|
|
12
12
|
export { tokenMeterViolations } from './checks.js';
|
|
13
13
|
export { auditToolCalls, extractText, extractToolOutputs, indexToolCalls, toolCommandOf } from './archaeology.js';
|
package/lib/log-reader.js
CHANGED
|
@@ -92,6 +92,49 @@ export function decompressZstd(buf) {
|
|
|
92
92
|
return Buffer.concat(parts);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* 读取文件尾部最后一条事件的 seq(看门狗 R1 用,只解最后一帧,O(帧数) 内)。
|
|
97
|
+
*
|
|
98
|
+
* 语义:zstd 多帧文件取最后一个**完整**帧解压(撕裂尾帧跳过——可能正在写入),
|
|
99
|
+
* 从帧内末尾向前找第一条合法 JSON 行,用 `decodeStorageRecord` 展开取末事件 seq
|
|
100
|
+
* (chunk 行展开后末成员即该 run 的最新 seq)。明文文件直接扫末尾。
|
|
101
|
+
*
|
|
102
|
+
* @param {string} path 日志文件路径
|
|
103
|
+
* @returns {number|null} 尾部 seq;读取失败/无可解析行返回 null(调用方降级不检查)。
|
|
104
|
+
*/
|
|
105
|
+
export function tailSeq(path) {
|
|
106
|
+
let buf;
|
|
107
|
+
try {
|
|
108
|
+
buf = fs.readFileSync(path);
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const isZstd = buf.length >= 4 && buf.readUInt32LE(0) === ZSTD_MAGIC;
|
|
113
|
+
let plain;
|
|
114
|
+
if (isZstd) {
|
|
115
|
+
const { frames } = scanZstdFrames(buf);
|
|
116
|
+
if (frames.length === 0) return null;
|
|
117
|
+
const [s, e] = frames[frames.length - 1];
|
|
118
|
+
try {
|
|
119
|
+
plain = zstdDecompressSync(buf.subarray(s, e));
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
plain = buf;
|
|
125
|
+
}
|
|
126
|
+
const lines = plain.toString('utf8').split('\n');
|
|
127
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
128
|
+
const line = lines[i];
|
|
129
|
+
if (line.trim().length === 0) continue; // 帧边界产物(空行)跳过
|
|
130
|
+
try {
|
|
131
|
+
const decoded = decodeStorageRecord(JSON.parse(line));
|
|
132
|
+
if (decoded.length > 0) return decoded[decoded.length - 1].seq;
|
|
133
|
+
} catch { /* 损坏行/不可解析:继续向前找 */ }
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
95
138
|
/**
|
|
96
139
|
* 读取并解码一个会话日志文件(.jsonl / .jsonl.zstd / 任意带 zstd 魔数的文件)。
|
|
97
140
|
*
|
package/lib/repair.js
CHANGED
|
@@ -119,6 +119,50 @@ export function strictScanText(text) {
|
|
|
119
119
|
return { failures, count };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* 原地中和 turn-null marker(2026-08-30 事故升级:单 marker 会让 token-meter
|
|
124
|
+
* 监听器在每条事件上抛错刷屏、压垮 host 事件循环 → 全会话连锁锁定)。
|
|
125
|
+
*
|
|
126
|
+
* 机制(自检单 §3.3):把 turn-null `assistant/message` replace marker 改写为
|
|
127
|
+
* `retrace/marker` + `ignorable: true`(dsh-session lib/index.js:1203/1209 认可
|
|
128
|
+
* envelope 级 ignorable),**删除 surfaceOp/sourceEventSeqs,不动行数/seq/时间**。
|
|
129
|
+
* 改写后该事件不再属于 SURFACE_EVENT_TYPES(dsh-session:219-223)→ foldSurface
|
|
130
|
+
* 跳过、token-meter 不做 step 配对(不再抛错刷屏);会话驻留时也安全(seq 不变,
|
|
131
|
+
* 退出 flush 不撞车)。代价:被遮蔽的旧消息回到 surface(编辑外观回退为原文)。
|
|
132
|
+
*
|
|
133
|
+
* @param {string} text - JSONL 全文(含 header 行)。
|
|
134
|
+
* @returns {{ text: string, neutralized: number, seqs: Array<number> }}
|
|
135
|
+
*/
|
|
136
|
+
export function neutralizeMarkersText(text) {
|
|
137
|
+
const parts = text.split('\n');
|
|
138
|
+
const seqs = [];
|
|
139
|
+
let neutralized = 0;
|
|
140
|
+
for (let i = 0; i < parts.length; i++) {
|
|
141
|
+
const raw = parts[i];
|
|
142
|
+
if (!raw.trim()) continue;
|
|
143
|
+
let v;
|
|
144
|
+
try {
|
|
145
|
+
v = JSON.parse(raw);
|
|
146
|
+
} catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!v || typeof v !== 'object' || v.type !== 'assistant/message') continue;
|
|
150
|
+
// 只中和 turn-null 的 retrace marker(data.turn/step 为 null 且 id 带 marker 前缀)
|
|
151
|
+
if (v.data?.turn != null || v.data?.step != null) continue;
|
|
152
|
+
const id = v.data?.message?.id;
|
|
153
|
+
if (typeof id !== 'string' || !MARKER_PREFIXES.some((p) => id.startsWith(`${p}-`))) continue;
|
|
154
|
+
const seq = v.seq;
|
|
155
|
+
delete v.surfaceOp;
|
|
156
|
+
delete v.sourceEventSeqs;
|
|
157
|
+
v.type = 'retrace/marker';
|
|
158
|
+
v.ignorable = true;
|
|
159
|
+
parts[i] = JSON.stringify(v);
|
|
160
|
+
seqs.push(seq);
|
|
161
|
+
neutralized++;
|
|
162
|
+
}
|
|
163
|
+
return { text: parts.join('\n'), neutralized, seqs };
|
|
164
|
+
}
|
|
165
|
+
|
|
122
166
|
/**
|
|
123
167
|
* 移除 retrace/message-editor marker 并全量重编号。
|
|
124
168
|
* @param {string} text - JSONL 全文。
|
|
@@ -555,11 +599,11 @@ export function rebuildZstdText(text) {
|
|
|
555
599
|
* @param {string} file - .jsonl 或 .jsonl.zstd 路径。
|
|
556
600
|
* @param {{
|
|
557
601
|
* removeMarkers?: boolean, dropFailedTurns?: boolean, trimLast?: number, compactLast?: number,
|
|
558
|
-
* apply?: boolean, backupDir?: string
|
|
602
|
+
* neutralize?: boolean, apply?: boolean, backupDir?: string
|
|
559
603
|
* }} opts
|
|
560
604
|
* @returns {{
|
|
561
605
|
* file, ok, issues: Array<{kind:string, detail:string}>,
|
|
562
|
-
* removed, renumbered, backupPath, applied,
|
|
606
|
+
* removed, renumbered, neutralized, neutralizedSeqs: Array<number>, backupPath, applied,
|
|
563
607
|
* check: { ok, summary }
|
|
564
608
|
* }}
|
|
565
609
|
*/
|
|
@@ -610,6 +654,17 @@ export function repairSession(file, opts = {}) {
|
|
|
610
654
|
const r = removeMarkersText(plain);
|
|
611
655
|
applyFix('markers', r, `移除 ${r.removed} 个 retrace/message-editor marker(重编号 ${r.renumbered} 行)`);
|
|
612
656
|
}
|
|
657
|
+
let neutralized = 0;
|
|
658
|
+
const neutralizedSeqs = [];
|
|
659
|
+
if (opts.neutralize) {
|
|
660
|
+
const r = neutralizeMarkersText(plain);
|
|
661
|
+
neutralized = r.neutralized;
|
|
662
|
+
neutralizedSeqs.push(...r.seqs);
|
|
663
|
+
if (r.neutralized > 0) {
|
|
664
|
+
issues.push({ kind: 'neutralize', detail: `原地中和 ${r.neutralized} 个 turn-null marker(type→retrace/marker + ignorable:true,删除 surfaceOp/sourceEventSeqs,seq/行数不变)→ token-meter 不再刷屏` });
|
|
665
|
+
plain = r.text;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
613
668
|
if (typeof opts.trimLast === 'number') {
|
|
614
669
|
const r = trimLastMessagesText(plain, opts.trimLast);
|
|
615
670
|
applyFix('trim', r, `裁剪到最近 ${r.kept} 条消息(丢弃 ${r.removed} 行,重编号 ${r.renumbered} 行)`);
|
|
@@ -652,6 +707,8 @@ export function repairSession(file, opts = {}) {
|
|
|
652
707
|
issues,
|
|
653
708
|
removed,
|
|
654
709
|
renumbered,
|
|
710
|
+
neutralized,
|
|
711
|
+
neutralizedSeqs,
|
|
655
712
|
backupPath,
|
|
656
713
|
applied,
|
|
657
714
|
check,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-log-contract",
|
|
3
3
|
"description": "日志契约守护 — DSH session log contract guard: offline health check (CLI) + pre-write validation for DeepSeek Harness session logs",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"bin": {
|