dsh-log-contract 0.3.3 → 0.3.4
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 +6 -4
- package/lib/index.js +1 -1
- package/lib/repair.js +73 -2
- package/package.json +1 -1
package/bin/dsh-log-contract.mjs
CHANGED
|
@@ -23,11 +23,12 @@ 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] [--neutralize] [--apply] [--backup-dir DIR] [--json]
|
|
26
|
+
dsh-log-contract fix <session-log> [--remove-markers] [--neutralize] [--clip-crossstep] [--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
|
+
--clip-crossstep 裁剪 assistant/message 的跨 step sourceEventSeqs(token-meter 不再抛 belongs to another step)
|
|
31
32
|
--neutralize 原地中和 turn-null marker(type→retrace/marker +
|
|
32
33
|
ignorable:true,删 surfaceOp/sourceEventSeqs,seq/行数不变)
|
|
33
34
|
—— token-meter 不再刷屏,会话驻留也安全(2026-08-30 事故)
|
|
@@ -172,6 +173,7 @@ function cmdFix(args) {
|
|
|
172
173
|
const json = args.includes('--json');
|
|
173
174
|
const removeMarkers = args.includes('--remove-markers');
|
|
174
175
|
const neutralize = args.includes('--neutralize');
|
|
176
|
+
const clipCrossStep = args.includes('--clip-crossstep');
|
|
175
177
|
const dropFailedTurns = args.includes('--drop-failed-turns');
|
|
176
178
|
const trimIdx = args.indexOf('--trim-last');
|
|
177
179
|
const trimLast = trimIdx >= 0 && args[trimIdx + 1] ? Number(args[trimIdx + 1]) : undefined;
|
|
@@ -183,7 +185,7 @@ function cmdFix(args) {
|
|
|
183
185
|
const file = args.find((a) => !a.startsWith('-'));
|
|
184
186
|
if (!file) fail(USAGE);
|
|
185
187
|
|
|
186
|
-
const result = repairSession(file, { removeMarkers, neutralize, dropFailedTurns, trimLast, compactLast, apply, backupDir });
|
|
188
|
+
const result = repairSession(file, { removeMarkers, neutralize, clipCrossStep, dropFailedTurns, trimLast, compactLast, apply, backupDir });
|
|
187
189
|
if (json) {
|
|
188
190
|
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
189
191
|
process.exit(result.ok ? 0 : 1);
|
|
@@ -192,7 +194,7 @@ function cmdFix(args) {
|
|
|
192
194
|
process.stdout.write(`\n🔧 dsh-log-contract fix —— ${file}\n`);
|
|
193
195
|
process.stdout.write(` 诊断:${result.issues.length === 0 ? '无问题' : result.issues.map((i) => `[${i.kind}] ${i.detail}`).join('\n ')}\n`);
|
|
194
196
|
if (result.applied) {
|
|
195
|
-
process.stdout.write(` 已应用修复:移除 ${result.removed} 项,重编号 ${result.renumbered} 行,中和 ${result.neutralized} 个 turn-null marker
|
|
197
|
+
process.stdout.write(` 已应用修复:移除 ${result.removed} 项,重编号 ${result.renumbered} 行,中和 ${result.neutralized} 个 turn-null marker,裁剪 ${result.clipped} 个跨 step 引用(seq ${(result.neutralizedSeqs ?? []).join(',')})\n`);
|
|
196
198
|
process.stdout.write(` 备份:${result.backupPath}\n`);
|
|
197
199
|
process.stdout.write(` 修复后体检:error ${result.check.summary?.bySeverity?.error ?? '?'} | surface ${result.check.summary?.surfaceNodes ?? '?'} 节点\n`);
|
|
198
200
|
} else if (apply && !result.ok) {
|
|
@@ -200,7 +202,7 @@ function cmdFix(args) {
|
|
|
200
202
|
} else if (apply) {
|
|
201
203
|
process.stdout.write(' (--apply 且无问题——无内容可修)\n');
|
|
202
204
|
} else {
|
|
203
|
-
process.stdout.write(` (干跑模式:${result.removed} 项可移除、${result.renumbered} 行待重编号、${result.neutralized} 个 turn-null marker
|
|
205
|
+
process.stdout.write(` (干跑模式:${result.removed} 项可移除、${result.renumbered} 行待重编号、${result.neutralized} 个 turn-null marker 可中和、${result.clipped} 个跨 step 引用可裁剪;加 --apply 落盘,--remove-markers / --neutralize / --clip-crossstep / --drop-failed-turns / --trim-last N 启用于对应修复)\n`);
|
|
204
206
|
}
|
|
205
207
|
process.stdout.write('\n');
|
|
206
208
|
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, neutralizeMarkersText, dropFailedTurnsText, trimLastMessagesText, compactLastMessagesText, rebuildZstdText } from './repair.js';
|
|
10
|
+
export { repairSession, strictScanText, removeMarkersText, neutralizeMarkersText, clipCrossStepSourcesText, 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,63 @@ export function strictScanText(text) {
|
|
|
119
119
|
return { failures, count };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* 裁剪 assistant/message 的跨 step sourceEventSeqs(2026-08-30 第二类事故)。
|
|
124
|
+
*
|
|
125
|
+
* 现象:DSH 的 resend/regenerate 在 agent 仍开着 step 时被触发,会把旧 step 的
|
|
126
|
+
* assistant/chunk 全部引用进新 assistant/message 的 sourceEventSeqs(526f1835
|
|
127
|
+
* seq 936047:sourceEventSeqs 覆盖 turn 54 的 step 7/8/9 三段)。token-meter
|
|
128
|
+
* 要求每个 source chunk 与消息同 turn/step(dsh-token-meter lib/index.js:645,
|
|
129
|
+
* `belongs to another step`)→ 同样刷屏压垮 host。
|
|
130
|
+
*
|
|
131
|
+
* 修复:把 sourceEventSeqs 裁剪为只含「与消息同 turn/step 的 assistant/chunk」
|
|
132
|
+
* (丢弃跨 step 的 chunk 与边界事件)。token-meter 检查全过(所有 src 同 step),
|
|
133
|
+
* token 计量仍准(保留的就是本 step 实际输出)。不动 seq/行数/其他字段。
|
|
134
|
+
*
|
|
135
|
+
* @param {string} text - JSONL 全文(含 header 行)。
|
|
136
|
+
* @returns {{ text: string, clipped: number, seqs: Array<number> }}
|
|
137
|
+
*/
|
|
138
|
+
export function clipCrossStepSourcesText(text) {
|
|
139
|
+
const parts = text.split('\n');
|
|
140
|
+
// 用 decodeLine 展开每行,建立「展开后事件 seq → 事件」映射(chunk 行会展开为
|
|
141
|
+
// 多个 assistant/chunk,其 seq 是 seq0+偏移——必须用展开后的 seq 才能对上
|
|
142
|
+
// sourceEventSeqs 引用的值)。同时记录每个展开后事件属于哪一行,便于回写。
|
|
143
|
+
const bySeq = new Map(); // 展开后 seq → { event, lineIndex }
|
|
144
|
+
for (let i = 0; i < parts.length; i++) {
|
|
145
|
+
const raw = parts[i];
|
|
146
|
+
if (!raw.trim()) continue;
|
|
147
|
+
const decoded = decodeLine(raw);
|
|
148
|
+
if (decoded === null) continue;
|
|
149
|
+
for (const ev of decoded) {
|
|
150
|
+
if (typeof ev.seq === 'number') bySeq.set(ev.seq, { event: ev, lineIndex: i });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const clipped = [];
|
|
154
|
+
let clippedCount = 0;
|
|
155
|
+
for (const { event, lineIndex } of bySeq.values()) {
|
|
156
|
+
if (event.type !== 'assistant/message' || !Array.isArray(event.sourceEventSeqs) || event.sourceEventSeqs.length === 0) continue;
|
|
157
|
+
const { turn, step } = event.data ?? {};
|
|
158
|
+
if (turn == null || step == null) continue; // turn-null marker 由 neutralize 处理
|
|
159
|
+
let dirty = false;
|
|
160
|
+
const kept = event.sourceEventSeqs.filter((s) => {
|
|
161
|
+
const src = bySeq.get(s)?.event;
|
|
162
|
+
if (!src || src.type !== 'assistant/chunk') return true; // 保留非 chunk 引用(边界事件)
|
|
163
|
+
if (src.data?.turn === turn && src.data?.step === step) return true;
|
|
164
|
+
dirty = true;
|
|
165
|
+
return false;
|
|
166
|
+
});
|
|
167
|
+
if (dirty) {
|
|
168
|
+
// 回写:只改事件所属的行。若该行是 chunk 行(多个展开事件共享一行),
|
|
169
|
+
// 此事件必是独立 assistant/message 行,直接改那一行。
|
|
170
|
+
event.sourceEventSeqs = kept;
|
|
171
|
+
parts[lineIndex] = JSON.stringify(event);
|
|
172
|
+
clipped.push(event.seq);
|
|
173
|
+
clippedCount++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { text: parts.join('\n'), clipped: clippedCount, seqs: clipped };
|
|
177
|
+
}
|
|
178
|
+
|
|
122
179
|
/**
|
|
123
180
|
* 原地中和 turn-null marker(2026-08-30 事故升级:单 marker 会让 token-meter
|
|
124
181
|
* 监听器在每条事件上抛错刷屏、压垮 host 事件循环 → 全会话连锁锁定)。
|
|
@@ -599,11 +656,12 @@ export function rebuildZstdText(text) {
|
|
|
599
656
|
* @param {string} file - .jsonl 或 .jsonl.zstd 路径。
|
|
600
657
|
* @param {{
|
|
601
658
|
* removeMarkers?: boolean, dropFailedTurns?: boolean, trimLast?: number, compactLast?: number,
|
|
602
|
-
* neutralize?: boolean, apply?: boolean, backupDir?: string
|
|
659
|
+
* neutralize?: boolean, clipCrossStep?: boolean, apply?: boolean, backupDir?: string
|
|
603
660
|
* }} opts
|
|
604
661
|
* @returns {{
|
|
605
662
|
* file, ok, issues: Array<{kind:string, detail:string}>,
|
|
606
|
-
* removed, renumbered, neutralized, neutralizedSeqs: Array<number>,
|
|
663
|
+
* removed, renumbered, neutralized, neutralizedSeqs: Array<number>,
|
|
664
|
+
* clipped, clippedSeqs: Array<number>, backupPath, applied,
|
|
607
665
|
* check: { ok, summary }
|
|
608
666
|
* }}
|
|
609
667
|
*/
|
|
@@ -665,6 +723,17 @@ export function repairSession(file, opts = {}) {
|
|
|
665
723
|
plain = r.text;
|
|
666
724
|
}
|
|
667
725
|
}
|
|
726
|
+
let clipped = 0;
|
|
727
|
+
const clippedSeqs = [];
|
|
728
|
+
if (opts.clipCrossStep) {
|
|
729
|
+
const r = clipCrossStepSourcesText(plain);
|
|
730
|
+
clipped = r.clipped;
|
|
731
|
+
clippedSeqs.push(...r.seqs);
|
|
732
|
+
if (r.clipped > 0) {
|
|
733
|
+
issues.push({ kind: 'clip-crossstep', detail: `裁剪 ${r.clipped} 个 assistant/message 的跨 step sourceEventSeqs(保留同 step chunk,token-meter 不再抛 belongs to another step)` });
|
|
734
|
+
plain = r.text;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
668
737
|
if (typeof opts.trimLast === 'number') {
|
|
669
738
|
const r = trimLastMessagesText(plain, opts.trimLast);
|
|
670
739
|
applyFix('trim', r, `裁剪到最近 ${r.kept} 条消息(丢弃 ${r.removed} 行,重编号 ${r.renumbered} 行)`);
|
|
@@ -709,6 +778,8 @@ export function repairSession(file, opts = {}) {
|
|
|
709
778
|
renumbered,
|
|
710
779
|
neutralized,
|
|
711
780
|
neutralizedSeqs,
|
|
781
|
+
clipped,
|
|
782
|
+
clippedSeqs,
|
|
712
783
|
backupPath,
|
|
713
784
|
applied,
|
|
714
785
|
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.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"bin": {
|