dsh-log-contract 0.3.2 → 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.
@@ -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} 行\n`);
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} 行待重编号;加 --apply 落盘,--remove-markers / --drop-failed-turns / --trim-last N 启用于对应修复)\n`);
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
@@ -7,7 +7,7 @@
7
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/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.2",
4
+ "version": "0.3.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "bin": {