dsh-log-contract 0.3.4 → 0.3.6

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/README.md CHANGED
@@ -1,57 +1,143 @@
1
- # dsh-log-contract · 日志契约守护
1
+ <div align="center">
2
2
 
3
- > DSH(DeepSeek Harness)会话日志的**结构契约保险丝**:离线体检 + 写前校验。
4
- > 原名 `log-contract-validator`(候选二号),按 Offer快 三件套规划定名 **`dsh-log-contract`**。
3
+ # 🔒 dsh-log-contract
5
4
 
6
- DSH 会话日志(`*.jsonl` / `*.jsonl.zstd`)装一条保险丝:人眼看不出、程序解析会崩的日志格式漂移,在它这里被拦下并告警。它不判断日志**内容**对不对,只守护日志**结构**是否破坏了下游消费者(Harness 读路径、客户端引擎、插件 marker 语义)的预期。
5
+ **Log Contract Guard** the structural fuse for DeepSeek Harness session logs:
6
+ offline health check + pre-write validation. The business layer's **doctor**.
7
7
 
8
- - **`check <session-log>`** —— 离线体检:官方解码器全量解码 + 契约逐条校验 + foldSurface 终验,产出违规报告。
9
- - **`prewrite <edit-file> --log <session-log>`** —— ★ 写前校验:任何写入(追加 / 帧级手术)在落盘之前先过三层契约,违约即拦。
10
- - **`contracts`** —— 列出内置契约规则目录(每条附官方源码出处)。
8
+ [![npm version](https://img.shields.io/npm/v/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
9
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
10
+ [![License: MIT](https://img.shields.io/npm/l/dsh-log-contract)](https://github.com/yamingmou/dsh-log-contract/blob/main/LICENSE)
11
+ [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4A90D9)](https://github.com/topics/dsh-plugin)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/yamingmou/dsh-log-contract/pulls)
13
+
14
+ **English** · [简体中文](./README.zh.md)
15
+
16
+ </div>
17
+
18
+ Formerly `log-contract-validator` (candidate #2); named **`dsh-log-contract`**
19
+ per the OfferKuai three-piece plan.
20
+
21
+ A fuse for DSH session logs (`*.jsonl` / `*.jsonl.zstd`): format drift that humans
22
+ cannot see but parsers crash on is caught and reported here. It does **not** judge
23
+ whether log *content* is right — only whether log *structure* breaks the
24
+ expectations of downstream consumers (the Harness read path, the client engine,
25
+ plugin marker semantics).
26
+
27
+ - **`check <session-log>`** — offline health check: official decoder full decode +
28
+ per-rule contract validation + foldSurface final verification, with a violation
29
+ report.
30
+ - **`prewrite <edit-file> --log <session-log>`** — ★ pre-write validation: any
31
+ write (append / frame-level surgery) passes the three-layer contract before it
32
+ lands; violations are blocked.
33
+ - **`contracts`** — list the built-in contract rule catalog (each with its
34
+ official source reference).
35
+
36
+ ---
37
+
38
+ ## Where it sits in the business layer
39
+
40
+ > **dsh-log-contract is the core capability component of
41
+ > [dsh-retrace](https://github.com/yamingmou/dsh-retrace)** — the "doctor" module
42
+ > of the business layer: session-log **check & repair**, so every recall/edit/rewind
43
+ > lands on a legal log and `/compact` never breaks.
44
+
45
+ | Layer | What it is | Components |
46
+ |---|---|---|
47
+ | **Agent business layer (production-grade)** | Framework-agnostic core: session hygiene / retraceability / auditability / recoverability | Four modules: governance / retrospect / archaeology / **doctor** |
48
+ | **dsh-retrace** | The business layer's DeepSeek Harness implementation | recall/edit/version/rollback/watchdog |
49
+ | **dsh-log-contract** | dsh-retrace's core component = the business layer's **doctor** (check & repair) | check / prewrite / fix / extract / audit |
50
+
51
+ **Meaning**: dsh-log-contract is published standalone (for direct use or
52
+ re-implementation), but it is first the "check & repair" capability of dsh-retrace —
53
+ together they form the **Agent business layer (production-grade guarantees)** on
54
+ DSH (see the [dsh-retrace roadmap](https://github.com/yamingmou/dsh-retrace/blob/main/docs/ROADMAP.md)).
11
55
 
12
56
  ---
13
57
 
14
- ## 为什么需要它
58
+ ## Why it exists
15
59
 
16
- **#3632one log, two consumers, two verdicts」**:一条日志同时被人类与自动化程序消费,人眼容忍格式微调,程序解析依赖严格契约;格式一旦漂移,人看不出问题,程序直接崩溃或误报。
60
+ **#3632 "one log, two consumers, two verdicts"**: one log is consumed by both
61
+ humans and automated programs. Humans tolerate format drift; programs depend on
62
+ strict contracts. Once the format drifts, humans see nothing wrong while programs
63
+ crash or misreport.
17
64
 
18
- **2026-08-25 会话修复事故(真实回归用例)**:一次"恢复被隐藏内容"的修复,第 1 轮清空 marker `sourceEventSeqs` 直接写盘 会话加载抛 `SessionPersistenceCorruptionError`;第 2 轮把 marker 改成 `append` → 客户端引擎崩溃。两次都是**违约写入没被拦**。如果有写前校验,会话根本不会被改坏。本工具把这次事故沉淀为两条核心规则(S5、M1)与回归测试。
65
+ **Real incidents shaped every rule here** see the [Incident log](#-incident-log)
66
+ below. Each incident is a regression fixture: a corrupted session that this tool
67
+ must flag, and a fixed session that it must pass.
19
68
 
20
69
  ---
21
70
 
22
- ## 三层契约(判定模型)
71
+ ## Three-layer contract (the model)
23
72
 
24
- | | 契约 | 本工具 |
73
+ > 30+ rules across the layers below (`contracts` lists them all, each with its
74
+ > official source reference).
75
+
76
+ | Layer | Rules | What it guards |
25
77
  |---|---|---|
26
- | **持久化层** | seq 严格连续;type 在已知词汇表内;surface 事件携带合法 `surfaceOp`;replace `sourceEventSeqs` 必须**完整覆盖被替换节点**;官方 `foldSurface` 不抛 = 通过 | 规则 H/R/E/S(含 S5 核心) |
27
- | **客户端引擎层** | `data.turn/step` null `assistant/message` 只能以 **replace** 承载(插件 marker 定义),append 会触发引擎崩溃 | 规则 M1 |
28
- | **插件语义层** | marker id 前缀必须可识别(改名登记遗留前缀);marker 自身 seq 不得进入自身 shadowed | 规则 P1/P2 |
78
+ | **Persistence** | H/R/E/S (incl. **S5**) + **S9** | seq contiguous, known types, legal `surfaceOp`, `sourceEventSeqs` fully covers shadowed nodes, file-physical seq monotonic, `foldSurface` not throwing |
79
+ | **Client engine** | **M1** + **T1 / T2 / I1** | turn-null markers only as replace; token-meter pairing; cross-step source refs; inbox seed-relative replay |
80
+ | **Wire message flow** | **W1 / W2** | tool messages follow an assistant with tool_calls; no user text between tool_calls and results |
81
+ | **Plugin semantics** | P1/P2 | marker id prefixes recognizable; a marker's seq not in its own shadowed set |
29
82
 
30
- > 校验哲学:先用与官方同语义的增量重放做**逐事件归因**(定位到 seq/行号),再跑官方 `foldSurface` 做**终验**(不抛才算过)——两套都绿才过。
83
+ > Philosophy: first an incremental replay with official-equivalent semantics for
84
+ > **per-event attribution** (pinpoint seq/line), then the official `foldSurface` as
85
+ > the **final verdict** (not throwing = pass) — both green to pass.
31
86
 
32
87
  ---
33
88
 
34
- ## 安装
89
+ ## ⚡ Incident log — why "production-grade" is not a slogan
90
+
91
+ Every rule below was born from a **real incident** in our workspace. These are the
92
+ sessions that made us build this tool. Dates and shapes are real; session ids are
93
+ omitted for privacy.
94
+
95
+ | # | Date | What happened | The rule / fix it produced |
96
+ |---|---|---|---|
97
+ | 1 | 2026-08-25 | A "restore hidden content" repair wrote a replace marker with **emptied `sourceEventSeqs`** → the session refused to load (`SessionPersistenceCorruptionError`); a second attempt changed the marker to **append** → the client engine crashed. Both were **violating writes that nothing caught**. | **S5** (sourceEventSeqs must cover shadowed nodes), **M1** (turn-null assistant/message can only be replace), pre-write validation |
98
+ | 2 | 2026-08-27~28 | Interrupted/restarted turns replayed with a **stale in-memory cursor**, re-appending old seqs to the file tail (tail regression, duplicate batches); two writers interleaved → **file-physical order non-monotonic** (`734056 → 733539 → 735470`). Sessions failed to load with `seq gap`. | **S9** (physical-order monotonic), fix `--tail-renumber` |
99
+ | 3 | 2026-08-27~28 | **Fork-boundary orphan splice**: the fork's "remove parent's pending prompt" splice assumed the parent's inbox; the child's seed-relative replay has an empty inbox → `resume failed: invalid persisted inbox splice`. | **I1** (inbox seed-relative replay), fix `--neutralize-orphan` |
100
+ | 4 | 2026-08-28 | An oversized session (**1,052,557 tokens** vs the 1M window) could neither continue nor `/compact`; the trim budget estimator underpriced CJK by ~3.7×. | T1 (token-meter pairing) for compactability, `fix --trim` budget guidance |
101
+ | 5 | 2026-08-29 | **W1/W2 wire violations**: markers shadowed an assistant with tool_calls but left the tool results dangling → strict endpoints (`INVALID_REQUEST`) reject the session's request stream. | **W1 / W2** (wire message flow) |
102
+ | 6 | 2026-08-30 | A single **turn-null marker** made the token-meter listener throw on **every** appended event (`consumedEvents` never advanced → full-prefix re-fold per event) → **30s / 10,008 log lines**, host event loop crushed, all sessions locked. Same session also had a **cross-step sourceEventSeqs** (steps 7/8/9 mixed in one assistant message) — offline checks were green, the live meter crashed. | **T1** (turn/step pairing), **T2** (cross-step source refs), `fix --neutralize`, `fix --clip-crossstep` |
103
+
104
+ > **Takeaway**: every rule in this tool is a scar from a real session — validated
105
+ > against the actual corrupted-session fixtures, not synthetic theory. That is what
106
+ > "production-grade" means here.
107
+
108
+ ---
109
+
110
+ ## Installation
35
111
 
36
112
  ```bash
37
- pnpm add -D dsh-log-contract # npm install
113
+ pnpm add -D dsh-log-contract # or npm install
38
114
  pnpm dlx dsh-log-contract --help
39
115
  ```
40
116
 
41
- 依赖:Node 22(`node:zlib` 内置 zstd)、`@deepseek-ai/dsh-session`(peer,校验/解码复用官方实现,保证与 Harness 读路径同源)。
117
+ > **Using dsh-retrace?** No separate install needed — `dsh-retrace` declares
118
+ > `dsh-log-contract` as a dependency, so the contract guard (check / pre-write /
119
+ > repair primitives) comes with the plugin automatically. This package is published
120
+ > standalone for direct use or re-implementation.
121
+ >
122
+ > **Downloaded the repo as a ZIP?** `cd dsh-log-contract && npm install && npm run build`,
123
+ > then `node bin/dsh-log-contract.mjs check <session-log>` — no global install needed.
124
+
125
+ Dependencies: Node ≥ 22 (`node:zlib` has built-in zstd), `@deepseek-ai/dsh-session`
126
+ (peer; validation/decode reuse the official implementation, so it stays in sync
127
+ with the Harness read path).
42
128
 
43
129
  ---
44
130
 
45
- ## CLI 用法
131
+ ## CLI
46
132
 
47
- ### 1. 离线体检
133
+ ### 1. Offline health check
48
134
 
49
135
  ```bash
50
136
  dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd
51
- dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd --json # 机器可读
137
+ dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd --json # machine-readable
52
138
  ```
53
139
 
54
- 输出示例:
140
+ Sample output (the CLI reports in Chinese — it is the tool's UI language):
55
141
 
56
142
  ```
57
143
  📋 dsh-log-contract check —— backup-session-xxxx.jsonl.zstd
@@ -64,139 +150,153 @@ dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd --json # 机器可读
64
150
  ❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
65
151
  ```
66
152
 
67
- 退出码:0 = 通过(无 error 级违规);1 = 存在 error 级违规。
153
+ Exit code: 0 = pass (no error-level violations); 1 = error-level violations exist.
68
154
 
69
- `check` 0.2.0 起新增 **W1/W2 wire 级检查**:按 surface 顺序展开模型请求消息流,
70
- 捕获"悬空 tool 消息"tool 结果没有前置 assistant tool_calls)与"user 文本插在
71
- tool_calls 与其结果之间"——这类问题 DeepSeek 曾容忍,但 MiMo 等严格端点会直接
72
- `INVALID_REQUEST`(2026-08-27 实锤)。
155
+ `check` adds **W1/W2 wire-level checks** since 0.2.0: expand the model request
156
+ stream in surface order and catch "dangling tool messages" (a tool result with no
157
+ preceding assistant tool_calls) and "user text between tool_calls and their
158
+ results" — tolerated by some endpoints, `INVALID_REQUEST` on strict ones
159
+ (MiMo, verified 2026-08-27).
73
160
 
74
- ### 1.5. 修复(2026-08 事故固化方案)
161
+ ### 2. Repair (`fix`)
75
162
 
76
163
  ```bash
77
- # 干跑(只报告):严格 seq 连续扫描 + 全契约体检(含 W1/W2)+ 可移除 marker
164
+ # Dry run (report only): strict seq scan + full contract check + removable-marker count
78
165
  dsh-log-contract fix ~/.dsh/sessions/<id>.jsonl.zstd --remove-markers
79
166
 
80
- # 应用:备份后落盘(.zstd 走官方帧格式重建:帧1=header、帧2=其余、checksum、单个结尾换行)
167
+ # Apply: backup first, then write (.zstd rebuilt in official frame format: frame1=header,
168
+ # frame2=rest, checksum, single trailing newline)
81
169
  dsh-log-contract fix ~/.dsh/sessions/<id>.jsonl.zstd --remove-markers --apply
82
170
  ```
83
171
 
84
- - `--remove-markers`:移除 retrace/message-editor marker 并全量重编号
85
- seq/seq0/sourceEventSeqs/surfaceOp 同步)——用于大范围 marker 遮蔽历史、
86
- marker 漏盖 tool/result 导致的悬空 tool。
87
- - 手术安全协议:改前备份、改后全量复检(strictScan + check + foldSurface)、
88
- marker 只能遮蔽其之前的节点、marker 绝不能改成 append(M1 客户端崩溃)。
89
- - ⚠️ 若会话已被运行中的应用驻留内存,修复文件后需**重启应用**(强杀避免脏状态刷回)。
172
+ - `--remove-markers`: remove retrace/message-editor markers and renumber everything
173
+ (seq/seq0/sourceEventSeqs/surfaceOp in sync) — for large marker-shadowed history
174
+ or markers that left dangling tools.
175
+ - `--neutralize`: in-place neutralization of turn-null markers (incident #6) —
176
+ type → `retrace/marker` + `ignorable:true`, drops surfaceOp/sourceEventSeqs,
177
+ seq/line count unchanged (safe while the session is resident).
178
+ - `--clip-crossstep`: trim cross-step sourceEventSeqs (incident #6) — keep only
179
+ same-turn/step chunk references.
180
+ - Surgery safety protocol: back up first, re-verify after (strictScan + check +
181
+ foldSurface); markers may only shadow earlier nodes; a marker must never become
182
+ append (M1 crashes the client engine).
183
+ - ⚠️ If the session is resident in a running app, **restart the app** after fixing
184
+ the file (hard-kill to avoid dirty state flushing back).
90
185
 
91
- ### 2. 写前校验(本次事故的直接解药)
186
+ ### 3. Pre-write validation (`prewrite`)
92
187
 
93
- `edit-file` JSON,两种形状:
188
+ `edit-file` is JSON with two shapes:
94
189
 
95
190
  ```jsonc
96
- // 拟追加一个事件到日志尾部(seq 缺省 = 自动按 nextSeq 赋值)
191
+ // Append one event to the log tail (seq omitted = auto-assigned as nextSeq)
97
192
  { "append": { "type": "assistant/message", "surfaceOp": { "op": "replace", "start": 121774, "end": 156421 }, "sourceEventSeqs": [121774, 121779, "…"], "data": { "turn": null, "step": null, "message": { "…": "…" }, "editor": { "targetSeq": 156430, "text": "…" } } } }
98
193
 
99
- // 帧级手术后的完整事件列表(改后确认,与改前基线双绿才允许落盘)
100
- { "edit": [ "…完整事件列表…" ] }
194
+ // Frame-level surgery: the complete event list after the edit (both baseline and
195
+ // result must be green before it may land)
196
+ { "edit": [ "…full event list…" ] }
101
197
  ```
102
198
 
103
199
  ```bash
104
200
  dsh-log-contract prewrite marker-write.json --log ~/.dsh/sessions/<id>.jsonl.zstd
105
201
  ```
106
202
 
107
- - 基线本身有 error 级违规时直接拒绝校验(安全修复协议第 2 步:**改前基线必须绿**)。
108
- - 判定通过才允许落盘——**validate first, commit later**(与官方 `SurfaceManager.validateNext` 同思路)。
203
+ - A baseline with error-level violations is rejected outright (safety protocol
204
+ step 2: **the pre-surgery baseline must be green**).
205
+ - Only a pass may land — **validate first, commit later** (same idea as the
206
+ official `SurfaceManager.validateNext`).
109
207
 
110
- ### 3. 契约目录
208
+ ### 4. Contract catalog
111
209
 
112
210
  ```bash
113
211
  dsh-log-contract contracts
114
212
  ```
115
213
 
116
- 完整契约清单见 [docs/CONTRACTS.md](docs/CONTRACTS.md)
214
+ Full catalog in [docs/CONTRACTS.md](docs/CONTRACTS.md).
215
+
216
+ ### 5. Session archaeology (`extract` / `audit-report`)
217
+
218
+ Every tool call's full input/output is persisted in the session log — a data and
219
+ audit asset. Read-only archaeology:
220
+
221
+ ```sh
222
+ # Export tool outputs matching a command regex (original text preserved)
223
+ dsh-log-contract extract <session-log> --pattern "seed-scale" --min-size 50 --out ./found
224
+
225
+ # Archaeology audit report: call count / pairing rate / orphans / command distribution
226
+ dsh-log-contract audit-report <session-log>
227
+ ```
228
+
229
+ Contract rules P3 (tool/call↔tool/result pairing integrity) and P4 (output
230
+ structure parseable) keep the dig working: orphan calls and abnormal `text` fields
231
+ are flagged in `check`.
117
232
 
118
233
  ---
119
234
 
120
- ## Node API(写前校验嵌入你的脚本)
235
+ ## Node API (embed pre-write validation in your script)
121
236
 
122
237
  ```js
123
238
  import { loadSessionLog, validateSessionLog, createPreWriter } from 'dsh-log-contract';
124
239
 
125
- // ① 基线体检(改前基线必须绿)
240
+ // ① Baseline check (the pre-surgery baseline must be green)
126
241
  const log = loadSessionLog('session.jsonl.zstd');
127
242
  const baseline = validateSessionLog(log);
128
- if (!baseline.ok) throw new Error('基线已坏,先修基线');
243
+ if (!baseline.ok) throw new Error('baseline is broken; repair it first');
129
244
 
130
- // ② 写前校验:拟写入一个 marker replace
245
+ // ② Pre-write validation: about to write a marker replace
131
246
  const prewriter = createPreWriter({ events: log.events.map((e) => e.event) });
132
247
  const verdict = prewriter.validateAppend({
133
248
  type: 'assistant/message',
134
249
  surfaceOp: { op: 'replace', start: 121774, end: 156421 },
135
- sourceEventSeqs: [121774, 121779 /* …必须完整覆盖被替换节点… */],
250
+ sourceEventSeqs: [121774, 121779 /* …must fully cover shadowed nodes… */],
136
251
  data: { turn: null, step: null, message: { /* … */ } },
137
252
  });
138
253
  if (!verdict.ok) {
139
254
  for (const v of verdict.violations) console.error(v.id, v.message);
140
- process.exit(1); // 不落盘
255
+ process.exit(1); // do not land
141
256
  }
142
- // ③ 通过后才写
257
+ // ③ Only a pass writes
143
258
  ```
144
259
 
145
260
  ---
146
261
 
147
- ## 测试
262
+ ## Tests
148
263
 
149
264
  ```bash
150
- pnpm check && pnpm test # 语法检查 + 40 个单测(含事故回归用例)
265
+ pnpm check && pnpm test # syntax check + 79 unit tests (incl. incident regressions)
151
266
  ```
152
267
 
153
- - **合成夹具**(入库):合法会话 / seq 缺口 / 空 sourceEventSeqs / turn=null append / 未知 type / 坏 chunk 行 / 撕裂尾帧 / 未知 marker 前缀 / 自指 shadowed 等。
154
- - **真实化石**(不入库,含用户隐私):本地跑
268
+ - **Synthetic fixtures** (in-repo): legal session / seq gap / empty sourceEventSeqs
269
+ / turn-null append / unknown type / bad chunk row / torn tail frame / unknown
270
+ marker prefix / self-shadowing etc.
271
+ - **Real fossils** (not in-repo, contain user data): run locally
155
272
 
156
273
  ```bash
157
- node scripts/check-local-fossils.mjs # 扫描 ../ backup-session-*.jsonl.zstd
274
+ node scripts/check-local-fossils.mjs # scans ../ for backup-session-*.jsonl.zstd
158
275
  ```
159
276
 
160
- 已知真值表:事故修复后会话 PASS;`seqgap`/`corrupt`/`rewritten-230542` FAIL;`spliced-orphan` PASS(持久化层合法——#3632 的"消费路径判不可读"属于另一类契约,本工具只守护持久化契约层,见 [docs/CONTRACTS.md](docs/CONTRACTS.md) 边界说明)。
161
-
162
- ---
163
-
164
- ## 与三件套的关系
165
-
166
- | 工具 | 象限 | 状态 |
167
- |---|---|---|
168
- | [workbuddy-session-fork](https://github.com/yamingmou/workbuddy-session-fork) | 会话分叉 · 状态管理 | ✅ 已发布 v1.2.0 |
169
- | **dsh-log-contract**(本仓库) | 日志契约 · 接口稳定性 | ✅ Phase 1(check/prewrite)+ Phase 1.5(fix)0.2.0 |
170
- | dsh-turn-guard(规划中) | 中断回合 · 异常韧性 | 待立项 |
171
-
172
- 三者共享同一份 DSH 日志事件契约认知(59 条审计发现 = spec,aborted/corrupt/seqgap 化石 = 测试集)。dsh-retrace(回溯时间线)可把本工具的违规标记渲染到时间线上;本工具是 retrace 投影源健康度的**前置保险**。
277
+ Known truth table: incident-repaired sessions PASS; `seqgap`/`corrupt`/
278
+ `rewritten-230542` FAIL; `spliced-orphan` PASS (legal for the persistence layer —
279
+ #3632's "consumer path deems it unreadable" is a different contract; this tool only
280
+ guards the persistence contract layer, see the boundary note in
281
+ [docs/CONTRACTS.md](docs/CONTRACTS.md)).
173
282
 
174
283
  ---
175
284
 
176
285
  ## Roadmap
177
286
 
178
- - [x] **Phase 10.1.0)**:CLI 离线体检 + 写前校验 + 契约目录
179
- - [x] **Phase 1.50.2.0)**:`fix` 子命令(严格 seq 扫描 + W1/W2 wire 检查 + 移除 marker 重编号 + 官方帧格式重建);CI 集成(`dsh-log-contract check` 作为 Harness 会话目录的定时守护)
180
- - [ ] Phase 2:运行时守护(订阅 session append 事件流实时校验,断裂即标记 `dsh/contract-violation` 事件,策略可配 告警/拦截)——DSH 插件形态
181
- - [ ] Phase 3:与 dsh-turn-guard / dsh-retrace 时间线联动
182
-
183
- ## 许可
287
+ - [x] **Phase 1 (0.1.0)**: CLI offline check + pre-write validation + contract catalog
288
+ - [x] **Phase 1.5 (0.2.0)**: `fix` subcommand (strict seq scan + W1/W2 wire checks +
289
+ marker removal with renumbering + official frame rebuild); CI integration
290
+ (`dsh-log-contract check` as a scheduled guard over the Harness session dir)
291
+ - [x] **0.3.x (2026-08-30 incident hardening)**: T1 token-meter pairing → 0.3.1 W1/W2
292
+ fold-position fix → 0.3.2 `tailSeq` → 0.3.3 `fix --neutralize` (in-place
293
+ turn-null neutralization) → 0.3.4 `fix --clip-crossstep` (cross-step clipping) →
294
+ 0.3.5 **T2/S9/I1 rules** (cross-step source refs / physical order / inbox replay)
295
+ - [ ] Phase 2: runtime guard (subscribe to the session append stream, validate live,
296
+ mark violations as `dsh/contract-violation`, policy configurable alert/block) —
297
+ DSH plugin form
298
+ - [ ] Phase 3: link with dsh-turn-guard / dsh-retrace timeline
299
+
300
+ ## License
184
301
 
185
302
  MIT © OfferKuai Team
186
-
187
-
188
- ## 🧭 会话考古(extract / audit-report)
189
-
190
- DSH 会话日志持久化了每次工具调用的完整输入输出——数据资产与审计资产。
191
- 本工具提供只读考古能力:
192
-
193
- ```sh
194
- # 按命令正则导出工具输出(保留原始文本)
195
- dsh-log-contract extract <session-log> --pattern "seed-scale" --min-size 50 --out ./found
196
-
197
- # 考古审计报告:调用数 / 配对率 / 孤儿数 / 命令分布
198
- dsh-log-contract audit-report <session-log>
199
- ```
200
-
201
- 契约规则 P3(tool/call↔tool/result 配对完整性)与 P4(输出结构可解析)
202
- 守护"挖得动":孤儿调用、text 字段异常在 check 中告警。
package/README.zh.md ADDED
@@ -0,0 +1,253 @@
1
+ <div align="center">
2
+
3
+ # 🔒 dsh-log-contract
4
+
5
+ **日志契约守护** —— DSH 会话日志的结构契约保险丝:离线体检 + 写前校验。业务层的**医生**。
6
+
7
+ [![npm version](https://img.shields.io/npm/v/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
8
+ [![npm downloads](https://img.shields.io/npm/dm/dsh-log-contract)](https://www.npmjs.com/package/dsh-log-contract)
9
+ [![License: MIT](https://img.shields.io/npm/l/dsh-log-contract)](https://github.com/yamingmou/dsh-log-contract/blob/main/LICENSE)
10
+ [![DSH plugin](https://img.shields.io/badge/DSH-plugin-4A90D9)](https://github.com/topics/dsh-plugin)
11
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](https://github.com/yamingmou/dsh-log-contract/pulls)
12
+
13
+ [English](./README.md) · **简体中文**
14
+
15
+ </div>
16
+
17
+ # dsh-log-contract · 日志契约守护
18
+
19
+
20
+ > DSH(DeepSeek Harness)会话日志的**结构契约保险丝**:离线体检 + 写前校验。
21
+ > 原名 `log-contract-validator`(候选二号),按 Offer快 三件套规划定名 **`dsh-log-contract`**。
22
+
23
+ 给 DSH 会话日志(`*.jsonl` / `*.jsonl.zstd`)装一条保险丝:人眼看不出、程序解析会崩的日志格式漂移,在它这里被拦下并告警。它不判断日志**内容**对不对,只守护日志**结构**是否破坏了下游消费者(Harness 读路径、客户端引擎、插件 marker 语义)的预期。
24
+
25
+ - **`check <session-log>`** —— 离线体检:官方解码器全量解码 + 契约逐条校验 + foldSurface 终验,产出违规报告。
26
+ - **`prewrite <edit-file> --log <session-log>`** —— ★ 写前校验:任何写入(追加 / 帧级手术)在落盘之前先过三层契约,违约即拦。
27
+ - **`contracts`** —— 列出内置契约规则目录(每条附官方源码出处)。
28
+
29
+ ---
30
+
31
+ ## 它在业务层里的位置
32
+
33
+ > **dsh-log-contract 是 [dsh-retrace](https://github.com/yamingmou/dsh-retrace) 的核心能力组件**(业务层的「医生」模块):负责会话日志的**体检与修复**——让每一次撤回/编辑/回退都落在合法日志上,让 /compact 永不失效。
34
+
35
+ | 层 | 是什么 | 组件 |
36
+ |---|---|---|
37
+ | **Agent 业务层(生产级保证)** | 抽象核心能力:会话卫生 / 可回溯 / 可审计 / 可恢复,与平台无关 | 四模块:治理 / 看 / 考古 / **医生** |
38
+ | **dsh-retrace** | 业务层在 DeepSeek Harness 上的实现(生产级业务插件) | 撤回/编辑/版本/回退/看门狗 |
39
+ | **dsh-log-contract** | dsh-retrace 的核心能力组件 = 业务层的**医生**(体检/修复) | check / prewrite / fix / extract / audit |
40
+
41
+ **含义**:dsh-log-contract 独立发布(供单独使用或二次开发),但它首先是
42
+ dsh-retrace 的「日志体检与修复」能力——与 dsh-retrace 一起构成
43
+ **Agent 业务层(生产级保证)** 在 DSH 上的落地(详见 [dsh-retrace 路线图](https://github.com/yamingmou/dsh-retrace/blob/main/docs/ROADMAP.md))。
44
+
45
+ ---
46
+
47
+ ## 为什么需要它
48
+
49
+ **#3632「one log, two consumers, two verdicts」**:一条日志同时被人类与自动化程序消费,人眼容忍格式微调,程序解析依赖严格契约;格式一旦漂移,人看不出问题,程序直接崩溃或误报。
50
+
51
+ **这里每一条规则都来自真实事故**——见下方 [⚡ 事故记录](#-事故记录)。每起事故都是一个回归夹具:损坏的会话本工具必须报出,修复后的会话必须通过。
52
+
53
+ ---
54
+
55
+ ## 三层契约(判定模型)
56
+
57
+ > 30+ 条规则,覆盖以下三层(`contracts` 列出全部,每条附官方源码出处)。
58
+
59
+ | 层 | 规则 | 守护什么 |
60
+ |---|---|---|
61
+ | **持久化层** | H/R/E/S(含 **S5**)+ **S9** | seq 连续、type 已知、surfaceOp 合法、`sourceEventSeqs` 完整覆盖被替换节点、文件物理序单调、`foldSurface` 不抛 |
62
+ | **客户端引擎层** | **M1** + **T1 / T2 / I1** | turn-null marker 只能 replace;token-meter 配对;跨 step 源引用;inbox seed 相对重放 |
63
+ | **wire 消息流** | **W1 / W2** | tool 消息跟在带 tool_calls 的 assistant 之后;user 文本不插在 tool_calls 与结果之间 |
64
+ | **插件语义层** | P1/P2 | marker 前缀可识别;marker 自身 seq 不进自身 shadowed 集 |
65
+
66
+ > 校验哲学:先用与官方同语义的增量重放做**逐事件归因**(定位到 seq/行号),再跑官方 `foldSurface` 做**终验**(不抛才算过)——两套都绿才过。
67
+
68
+ ---
69
+
70
+ ## ⚡ 事故记录 ——「生产级保证」不是口号
71
+
72
+ 下面的每一条规则都来自我们工作区的一起**真实事故**。日期与形态真实,会话 id 为隐私省略。
73
+
74
+ | # | 日期 | 发生了什么 | 产出的规则/修复 |
75
+ |---|---|---|---|
76
+ | 1 | 2026-08-25 | 一次「恢复被隐藏内容」的修复写了**清空 `sourceEventSeqs`** 的 replace marker → 会话加载被拒(`SessionPersistenceCorruptionError`);第二次尝试把 marker 改成 **append** → 客户端引擎崩溃。两次都是**违约写入没被拦**。 | **S5**(sourceEventSeqs 必须覆盖被替换节点)、**M1**(turn-null 的 assistant/message 只能 replace)、写前校验 |
77
+ | 2 | 2026-08-27~28 | 中断/暂停的轮次恢复时按**过期内存光标**重放,把旧 seq 追加到文件尾(尾部回归、重复批次);两个写入者交织 → **文件物理序非单调**(`734056 → 733539 → 735470`)。会话 `seq gap` 加载失败。 | **S9**(物理序单调)、fix `--tail-renumber` |
78
+ | 3 | 2026-08-27~28 | **fork 边界孤儿 spliced**:fork 的「移除父待处理提示词」splice 假设父会话 inbox;子会话 seed 相对重放里 inbox 为空 → `resume failed: invalid persisted inbox splice`。 | **I1**(inbox seed 相对重放)、fix `--neutralize-orphan` |
79
+ | 4 | 2026-08-28 | 超限会话(**1,052,557 tokens** vs 1M 窗口)既无法继续也无法 `/compact`;裁剪预算估算对中文低估 ~3.7×。 | T1(token-meter 配对,保障可压缩)、`fix --trim` 预算指引 |
80
+ | 5 | 2026-08-29 | **W1/W2 wire 违规**:marker 遮蔽了带 tool_calls 的 assistant 但漏盖 tool 结果 → 悬空 tool,严格端点 `INVALID_REQUEST` 拒绝请求流。 | **W1 / W2**(wire 消息流) |
81
+ | 6 | 2026-08-30 | 单个 **turn-null marker** 让 token-meter 监听器在**每条**追加事件上抛错(`consumedEvents` 不前进 → 每事件全前缀重折)→ **30 秒 / 10,008 行日志**、host 事件循环被压垮、全部会话锁定。同会话还有**跨 step sourceEventSeqs**(step 7/8/9 混进一条 assistant 消息)——离线 check 全绿、实机 meter 崩溃。 | **T1**(turn/step 配对)、**T2**(跨 step 源引用)、`fix --neutralize`、`fix --clip-crossstep` |
82
+
83
+ > **结论**:这个工具的每条规则都是一次真实会话留下的疤——用真实的损坏会话夹具验证过,不是合成理论。这就是这里「生产级」的含义。
84
+
85
+ ---
86
+
87
+ ## 安装
88
+
89
+ ```bash
90
+ pnpm add -D dsh-log-contract # 或 npm install
91
+ pnpm dlx dsh-log-contract --help
92
+ ```
93
+
94
+ > **你是 dsh-retrace 用户?** 无需单独安装——`dsh-retrace` 已把 `dsh-log-contract`
95
+ > 声明为依赖,装 retrace 时自动带好契约守护(体检/写前校验/修复原语全部随插件生效)。
96
+ > 本包独立发布,供愿意单独使用或二次开发的用户直接引入。
97
+ >
98
+ > **从 GitHub 下载了 ZIP?** 解压后 `cd dsh-log-contract && npm install && npm run build`,
99
+ > 然后 `node bin/dsh-log-contract.mjs check <session-log>` 即可使用(无需全局安装)。
100
+
101
+ 依赖:Node ≥ 22(`node:zlib` 内置 zstd)、`@deepseek-ai/dsh-session`(peer,校验/解码复用官方实现,保证与 Harness 读路径同源)。
102
+
103
+ ---
104
+
105
+ ## CLI 用法
106
+
107
+ ### 1. 离线体检
108
+
109
+ ```bash
110
+ dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd
111
+ dsh-log-contract check ~/.dsh/sessions/<id>.jsonl.zstd --json # 机器可读
112
+ ```
113
+
114
+ 输出示例:
115
+
116
+ ```
117
+ 📋 dsh-log-contract check —— backup-session-xxxx.jsonl.zstd
118
+ 事件 204754 | surface 节点 16 | replace 代数 5 | 帧 8620(3439.5KiB → 8191.3KiB)
119
+ 违规 1(error 1 / warning 0)
120
+
121
+ [error] S5 @ seq 156425 / line 778 (assistant/message)
122
+ surface replace: sourceEventSeqs 必须覆盖每个被替换节点;缺失 121774, 121779(共 2 个)
123
+
124
+ ❌ 未通过:见上方违规明细(error 级 = 会话不可读/不可写)
125
+ ```
126
+
127
+ 退出码:0 = 通过(无 error 级违规);1 = 存在 error 级违规。
128
+
129
+ `check` 自 0.2.0 起新增 **W1/W2 wire 级检查**:按 surface 顺序展开模型请求消息流,
130
+ 捕获"悬空 tool 消息"(tool 结果没有前置 assistant tool_calls)与"user 文本插在
131
+ tool_calls 与其结果之间"——这类问题 DeepSeek 曾容忍,但 MiMo 等严格端点会直接
132
+ `INVALID_REQUEST`(2026-08-27 实锤)。
133
+
134
+ ### 2. 修复(`fix`)
135
+
136
+ ```bash
137
+ # 干跑(只报告):严格 seq 连续扫描 + 全契约体检(含 W1/W2)+ 可移除 marker 数
138
+ dsh-log-contract fix ~/.dsh/sessions/<id>.jsonl.zstd --remove-markers
139
+
140
+ # 应用:备份后落盘(.zstd 走官方帧格式重建:帧1=header、帧2=其余、checksum、单个结尾换行)
141
+ dsh-log-contract fix ~/.dsh/sessions/<id>.jsonl.zstd --remove-markers --apply
142
+ ```
143
+
144
+ - `--remove-markers`:移除 retrace/message-editor marker 并全量重编号
145
+ (seq/seq0/sourceEventSeqs/surfaceOp 同步)——用于大范围 marker 遮蔽历史、
146
+ marker 漏盖 tool/result 导致的悬空 tool。
147
+ - `--neutralize`:原地中和 turn-null marker(事故 #6)——type → `retrace/marker` +
148
+ `ignorable:true`,删 surfaceOp/sourceEventSeqs,seq/行数不变(会话驻留也安全)。
149
+ - `--clip-crossstep`:裁剪跨 step sourceEventSeqs(事故 #6)——只保留同 turn/step 的 chunk 引用。
150
+ - 手术安全协议:改前备份、改后全量复检(strictScan + check + foldSurface)、
151
+ marker 只能遮蔽其之前的节点、marker 绝不能改成 append(M1 客户端崩溃)。
152
+ - ⚠️ 若会话已被运行中的应用驻留内存,修复文件后需**重启应用**(强杀避免脏状态刷回)。
153
+
154
+ ### 3. 写前校验(`prewrite`)
155
+
156
+ `edit-file` 为 JSON,两种形状:
157
+
158
+ ```jsonc
159
+ // 拟追加一个事件到日志尾部(seq 缺省 = 自动按 nextSeq 赋值)
160
+ { "append": { "type": "assistant/message", "surfaceOp": { "op": "replace", "start": 121774, "end": 156421 }, "sourceEventSeqs": [121774, 121779, "…"], "data": { "turn": null, "step": null, "message": { "…": "…" }, "editor": { "targetSeq": 156430, "text": "…" } } } }
161
+
162
+ // 帧级手术后的完整事件列表(改后确认,与改前基线双绿才允许落盘)
163
+ { "edit": [ "…完整事件列表…" ] }
164
+ ```
165
+
166
+ ```bash
167
+ dsh-log-contract prewrite marker-write.json --log ~/.dsh/sessions/<id>.jsonl.zstd
168
+ ```
169
+
170
+ - 基线本身有 error 级违规时直接拒绝校验(安全修复协议第 2 步:**改前基线必须绿**)。
171
+ - 判定通过才允许落盘——**validate first, commit later**(与官方 `SurfaceManager.validateNext` 同思路)。
172
+
173
+ ### 4. 契约目录
174
+
175
+ ```bash
176
+ dsh-log-contract contracts
177
+ ```
178
+
179
+ 完整契约清单见 [docs/CONTRACTS.md](docs/CONTRACTS.md)。
180
+
181
+ ### 5. 会话考古(`extract` / `audit-report`)
182
+
183
+ DSH 会话日志持久化了每次工具调用的完整输入输出——数据资产与审计资产。
184
+ 只读考古能力:
185
+
186
+ ```sh
187
+ # 按命令正则导出工具输出(保留原始文本)
188
+ dsh-log-contract extract <session-log> --pattern "seed-scale" --min-size 50 --out ./found
189
+
190
+ # 考古审计报告:调用数 / 配对率 / 孤儿数 / 命令分布
191
+ dsh-log-contract audit-report <session-log>
192
+ ```
193
+
194
+ 契约规则 P3(tool/call↔tool/result 配对完整性)与 P4(输出结构可解析)
195
+ 守护"挖得动":孤儿调用、text 字段异常在 check 中告警。
196
+
197
+ ---
198
+
199
+ ## Node API(写前校验嵌入你的脚本)
200
+
201
+ ```js
202
+ import { loadSessionLog, validateSessionLog, createPreWriter } from 'dsh-log-contract';
203
+
204
+ // ① 基线体检(改前基线必须绿)
205
+ const log = loadSessionLog('session.jsonl.zstd');
206
+ const baseline = validateSessionLog(log);
207
+ if (!baseline.ok) throw new Error('基线已坏,先修基线');
208
+
209
+ // ② 写前校验:拟写入一个 marker replace
210
+ const prewriter = createPreWriter({ events: log.events.map((e) => e.event) });
211
+ const verdict = prewriter.validateAppend({
212
+ type: 'assistant/message',
213
+ surfaceOp: { op: 'replace', start: 121774, end: 156421 },
214
+ sourceEventSeqs: [121774, 121779 /* …必须完整覆盖被替换节点… */],
215
+ data: { turn: null, step: null, message: { /* … */ } },
216
+ });
217
+ if (!verdict.ok) {
218
+ for (const v of verdict.violations) console.error(v.id, v.message);
219
+ process.exit(1); // 不落盘
220
+ }
221
+ // ③ 通过后才写
222
+ ```
223
+
224
+ ---
225
+
226
+ ## 测试
227
+
228
+ ```bash
229
+ pnpm check && pnpm test # 语法检查 + 79 个单测(含事故回归用例)
230
+ ```
231
+
232
+ - **合成夹具**(入库):合法会话 / seq 缺口 / 空 sourceEventSeqs / turn=null append / 未知 type / 坏 chunk 行 / 撕裂尾帧 / 未知 marker 前缀 / 自指 shadowed 等。
233
+ - **真实化石**(不入库,含用户隐私):本地跑
234
+
235
+ ```bash
236
+ node scripts/check-local-fossils.mjs # 扫描 ../ 下 backup-session-*.jsonl.zstd
237
+ ```
238
+
239
+ 已知真值表:事故修复后会话 PASS;`seqgap`/`corrupt`/`rewritten-230542` FAIL;`spliced-orphan` PASS(持久化层合法——#3632 的"消费路径判不可读"属于另一类契约,本工具只守护持久化契约层,见 [docs/CONTRACTS.md](docs/CONTRACTS.md) 边界说明)。
240
+
241
+ ---
242
+
243
+ ## Roadmap
244
+
245
+ - [x] **Phase 1(0.1.0)**:CLI 离线体检 + 写前校验 + 契约目录
246
+ - [x] **Phase 1.5(0.2.0)**:`fix` 子命令(严格 seq 扫描 + W1/W2 wire 检查 + 移除 marker 重编号 + 官方帧格式重建);CI 集成(`dsh-log-contract check` 作为 Harness 会话目录的定时守护)
247
+ - [x] **0.3.x(2026-08-30 事故固化)**:T1 token-meter 配对 → 0.3.1 W1/W2 折叠位置修复 → 0.3.2 `tailSeq` → 0.3.3 `fix --neutralize`(turn-null marker 原地中和)→ 0.3.4 `fix --clip-crossstep`(跨 step 引用裁剪)→ 0.3.5 **T2/S9/I1 规则**(跨 step 源引用 / 物理序单调 / inbox 重放)
248
+ - [ ] Phase 2:运行时守护(订阅 session append 事件流实时校验,断裂即标记 `dsh/contract-violation` 事件,策略可配 告警/拦截)——DSH 插件形态
249
+ - [ ] Phase 3:与 dsh-turn-guard / dsh-retrace 时间线联动
250
+
251
+ ## 许可
252
+
253
+ MIT © OfferKuai Team
package/lib/checks.js CHANGED
@@ -122,6 +122,37 @@ export function isReplaceOp(op) {
122
122
  );
123
123
  }
124
124
 
125
+ /**
126
+ * S9 —— 文件物理序 seq 单调(2026-08-30 事故固化;交接书 L2)。
127
+ *
128
+ * 按**文件物理行序**(非 seq 排序)要求展开后的事件 seq 严格单调递增。
129
+ * 单进程 append 不可能写出非单调物理序(appendCore 断言 seq==cursor+i 且按
130
+ * id 串行化)——非单调 = 多写入者/旧光标回放交织的现场特征(526f1835 物理序
131
+ * 734056→733539→735470)。E2 只查「排序后连续」,排序会掩盖物理序倒退;
132
+ * S9 补「物理序单调」盲区。
133
+ *
134
+ * @param rows - loadSessionLog 的 rows(物理行序,每行含 decoded 数组)。
135
+ * @returns S9 违规列表(error 级)。
136
+ */
137
+ export function physicalOrderViolations(rows) {
138
+ const out = [];
139
+ let prevSeq = -1;
140
+ let prevLineNo = null;
141
+ for (const row of rows) {
142
+ if (!Array.isArray(row.decoded) || row.decoded.length === 0) continue;
143
+ for (const event of row.decoded) {
144
+ if (typeof event.seq !== 'number' || !Number.isSafeInteger(event.seq) || event.seq < 0) continue; // E1 处理
145
+ if (event.seq < prevSeq) {
146
+ out.push(violation('S9', { seq: event.seq, lineNo: row.lineNo, eventType: event.type }, `文件物理序 seq 倒退:${prevSeq}(line ${prevLineNo})→ ${event.seq}(line ${row.lineNo})——非单调 = 多写入者/旧光标回放交织(单进程 append 不可能写出),会话加载会被拒`));
147
+ return out; // 首个倒退即现场特征,报一次足够(后续乱序都源自此)
148
+ }
149
+ prevSeq = event.seq;
150
+ prevLineNo = row.lineNo;
151
+ }
152
+ }
153
+ return out;
154
+ }
155
+
125
156
  /**
126
157
  * 与官方同语义的 surface 增量重放,逐事件归因 S1–S7。
127
158
  * @param {Array<{event:object, lineNo?:number}>} events 按日志顺序的事件(带 loc 包装)
@@ -395,6 +426,114 @@ export function tokenMeterViolations(events) {
395
426
  return out;
396
427
  }
397
428
 
429
+ /**
430
+ * T2 —— token-meter 的 sourceEventSeqs 引用必须同 turn/step(2026-08-30 第二类
431
+ * 刷屏事故固化)。
432
+ *
433
+ * 镜像官方 `_estimateProviderAssistant`(dsh-token-meter lib/index.js:634-650):
434
+ * `assistant/message` 的每个 `sourceEventSeqs` 若指向 `assistant/chunk`,其
435
+ * turn/step 必须与消息自身一致;跨 step 引用 → 官方抛
436
+ * `token meter: assistant/message at seq N source seq M belongs to another step`
437
+ * (lib/index.js:645)。
438
+ *
439
+ * 事故现场:DSH resend/regenerate 在 agent 仍开着 step 时被触发,会把旧 step 的
440
+ * chunk 全部引用进新 assistant/message(526f1835 seq 936047 跨 step 7/8/9)→
441
+ * 离线 check(T1)全绿但实机 token-meter 崩溃 → 同样刷屏压垮 host。
442
+ *
443
+ * @param events - 行序事件流(`{event, lineNo}`)。
444
+ * @returns T2 违规列表(error 级,token-meter 实机必崩)。
445
+ */
446
+ export function tokenMeterSourceViolations(events) {
447
+ const out = [];
448
+ const bySeq = new Map();
449
+ for (const { event, lineNo } of events) bySeq.set(event.seq, { event, lineNo });
450
+ for (const { event, lineNo } of events) {
451
+ if (event.type !== 'assistant/message' || !Array.isArray(event.sourceEventSeqs) || event.sourceEventSeqs.length === 0) continue;
452
+ const turn = event.data?.turn;
453
+ const step = event.data?.step;
454
+ if (turn == null || step == null) continue; // turn-null marker 由 T1 覆盖
455
+ if (event.data?.usage === void 0) continue; // 无 usage 不触发 _estimateProviderAssistant(官方 :592 前提)——replace marker(S5 遮蔽语义,sourceEventSeqs 含非 chunk 节点)合法
456
+ const seen = new Set();
457
+ for (const s of event.sourceEventSeqs) {
458
+ if (s >= event.seq) {
459
+ out.push(violation('T2', { seq: event.seq, lineNo, eventType: event.type }, `assistant/message at seq ${event.seq} source seq ${s} is not earlier——token meter 折叠会抛错(_estimateProviderAssistant)`));
460
+ break;
461
+ }
462
+ if (seen.has(s)) {
463
+ out.push(violation('T2', { seq: event.seq, lineNo, eventType: event.type }, `assistant/message at seq ${event.seq} repeats source seq ${s}——token meter 折叠会抛错`));
464
+ break;
465
+ }
466
+ seen.add(s);
467
+ const src = bySeq.get(s);
468
+ if (!src || src.event.type !== 'assistant/chunk') {
469
+ out.push(violation('T2', { seq: event.seq, lineNo, eventType: event.type }, `assistant/message at seq ${event.seq} source seq ${s} is not assistant/chunk(实际 ${src ? src.event.type : 'MISSING'})——token meter 折叠会抛错(_estimateProviderAssistant :644 对非 chunk 引用直接 throw),/compact 与压力测量永久失败`));
470
+ break; // 官方抛一次即停(consumedEvents 不前进),只报首条
471
+ }
472
+ if (src.event.data?.turn !== turn || src.event.data?.step !== step) {
473
+ out.push(violation('T2', { seq: event.seq, lineNo, eventType: event.type }, `assistant/message at seq ${event.seq} source seq ${s} belongs to another step(消息 turn ${turn}/step ${step},源 turn ${String(src.event.data?.turn)}/step ${String(src.event.data?.step)})——token meter 折叠会抛错,/compact 与压力测量永久失败(DSH resend 在 step 未关时跨 step 引用)`));
474
+ break; // 官方抛一次即停(consumedEvents 不前进),只报首条
475
+ }
476
+ }
477
+ }
478
+ return out;
479
+ }
480
+
481
+ /**
482
+ * I1 —— inbox seed 相对重放(交接书 L1;镜像 dsh-agent lib/types/inbox.js)。
483
+ *
484
+ * 复刻官方 Inbox:从 `header.seedLength` 起重放 `agent/inbox/spliced`,
485
+ * next-turn/next-step 双队列;每条 splice 校验
486
+ * `start + removedCount <= 队列长` 且不产生重复 message id。违反 =
487
+ * `resume failed: invalid persisted inbox splice at seq N`(fork 边界孤儿:
488
+ * fork 时"移除父待处理提示词"的 removedCount=1 在子会话 seed 相对空 inbox 上非法)。
489
+ *
490
+ * @param events - 行序事件流(`{event, lineNo}`)。
491
+ * @param header - 日志 header(取 seedLength)。
492
+ * @returns I1 违规列表(error 级,resume 会被拒)。
493
+ */
494
+ export function inboxReplayViolations(events, header) {
495
+ const out = [];
496
+ const seedLength = header?.seedLength;
497
+ if (typeof seedLength !== 'number' || !Number.isSafeInteger(seedLength) || seedLength < 0) {
498
+ // 无 seedLength(非 fork 会话)→ Inbox 从 0 重放,语义等同全量;仍做队列校验
499
+ }
500
+ const state = { 'next-turn': [], 'next-step': [] };
501
+ const startSeq = seedLength ?? 0;
502
+ for (const { event, lineNo } of events) {
503
+ if (event.seq < startSeq) continue;
504
+ if (event.type !== 'agent/inbox/spliced') continue;
505
+ const loc = { seq: event.seq, lineNo, eventType: event.type };
506
+ const splice = event.data;
507
+ if (!splice || typeof splice.target !== 'string' || !['next-turn', 'next-step'].includes(splice.target)) {
508
+ out.push(violation('I1', loc, `spliced 缺合法 target(next-turn/next-step):${JSON.stringify(splice)?.slice(0, 80)}`));
509
+ continue;
510
+ }
511
+ const inbox = state[splice.target];
512
+ const removedCount = splice.removedCount ?? 0;
513
+ if (!Number.isSafeInteger(splice.start) || splice.start < 0 || splice.start > inbox.length
514
+ || !Number.isSafeInteger(removedCount) || removedCount < 0
515
+ || splice.start + removedCount > inbox.length) {
516
+ out.push(violation('I1', loc, `invalid inbox splice @seq ${event.seq}:target=${splice.target} start=${splice.start} removedCount=${removedCount} 但队列长 ${inbox.length}(seedLength=${seedLength})——resume 会被拒(fork 边界孤儿 spliced 即此形态,removedCount 指向 seed 相对空 inbox)`));
517
+ continue;
518
+ }
519
+ const inserted = Array.isArray(splice.inserted) ? splice.inserted : [];
520
+ const candidate = inbox.slice(0, splice.start).concat(inserted, inbox.slice(splice.start + removedCount));
521
+ const ids = new Set();
522
+ const other = splice.target === 'next-turn' ? state['next-step'] : state['next-turn'];
523
+ for (const message of [...candidate, ...other]) {
524
+ const id = message?.id ?? message?.message?.id;
525
+ if (id === undefined) continue;
526
+ if (ids.has(id)) {
527
+ out.push(violation('I1', loc, `message "${id}" 已在待处理队列中(target=${splice.target})——resume 会被拒(重复 pending id)`));
528
+ break;
529
+ }
530
+ ids.add(id);
531
+ }
532
+ inbox.splice(splice.start, removedCount, ...inserted);
533
+ }
534
+ return out;
535
+ }
536
+
398
537
  /** 从事件推导 wire 消息(与 dsh-session deriveEventMessage 同语义)。 */
399
538
  export function deriveWireMessage(event) {
400
539
  if (event.type === 'user/message') {
package/lib/contracts.js CHANGED
@@ -98,6 +98,22 @@ export const CONTRACT_RULES = [
98
98
  source: '@deepseek-ai/dsh-session lib/index.js:398 (planSurfaceEvent "not contiguous");审计 S2/N6',
99
99
  description: 'seq 必须从 0(或窗口 baseSeq)严格连续递增。缺口/倒退 = 违反单写入者假设(多实例共享存储并发写的痕迹),加载时直接 throw。',
100
100
  },
101
+ {
102
+ id: 'S9',
103
+ title: '文件物理序 seq 单调(多写入者交织现场特征)',
104
+ layer: LAYER.PERSISTENCE,
105
+ severity: SEVERITY.ERROR,
106
+ source: '2026-08-28 实锤:526f1835 文件物理序 734056→733539→735470;单进程 appendCore 断言 seq==cursor+i 且按 id 串行化不可能写出',
107
+ description: '按文件物理行序要求展开后事件 seq 严格单调递增。E2 在排序后检查(loadSessionLog 会 sort),物理序倒退被掩盖;S9 在排序前按行序检查,非单调 = 多写入者/旧光标回放交织的直接现场证据,加载会被拒。',
108
+ },
109
+ {
110
+ id: 'I1',
111
+ title: 'inbox seed 相对重放(fork 边界孤儿 spliced)',
112
+ layer: LAYER.ENGINE,
113
+ severity: SEVERITY.ERROR,
114
+ source: '@deepseek-ai/dsh-agent lib/types/inbox.js:155-178 (apply/validate);2026-08-28 实锤:62c5b531/73ed35d8 fork 边界 removedCount=1 孤儿',
115
+ description: '从 header.seedLength 起重放 agent/inbox/spliced,next-turn/next-step 双队列;start+removedCount 不得超过队列长、不得产生重复 pending id。fork 时"移除父待处理提示词"的 splice 假设父会话 inbox,子会话 seed 相对空 inbox 上非法 → resume 被拒(invalid persisted inbox splice)。',
116
+ },
101
117
  {
102
118
  id: 'E3',
103
119
  title: 'type 必须在已知词汇表内(或带 ignorable 标记)',
@@ -205,6 +221,14 @@ export const CONTRACT_RULES = [
205
221
  source: '@deepseek-ai/dsh-token-meter lib/index.js:566-625 (_foldEvent)',
206
222
  description: 'token meter 折叠要求 assistant/message 与 step/end 与打开的 step/start(turn/step 完全一致)匹配;违反即 /compact 与压力测量永久失败。retrace 的 turn-null 编辑/撤回 marker(空 assistant/message replace)命中此条——foldSurface 认可其合法性但 token meter 崩溃(M1 只约束 append 形态的盲区),压缩前需清理。',
207
223
  },
224
+ {
225
+ id: 'T2',
226
+ title: 'token-meter 源引用:assistant/message 的 sourceEventSeqs 引用的 chunk 必须同 turn/step',
227
+ layer: LAYER.ENGINE,
228
+ severity: SEVERITY.ERROR,
229
+ source: '@deepseek-ai/dsh-token-meter lib/index.js:634-650 (_estimateProviderAssistant,:645 belongs to another step)',
230
+ description: 'token meter 重建 provider 输出时,逐条检查 assistant/message 的 sourceEventSeqs:指向 assistant/chunk 的引用必须与消息同 turn/step,且 seq 更早、不重复;跨 step 引用 → 官方抛 belongs to another step → 每次事件追加都重抛(consumedEvents 不前进)→ 刷屏压垮 host(2026-08-30 实测 526f1835 seq 936047 跨 step 7/8/9)。T1 只查 step 配对不查源引用,此条补盲区;修复用 fix --clip-crossstep。',
231
+ },
208
232
  {
209
233
  id: 'P3',
210
234
  title: 'tool/call ↔ tool/result 配对完整性(考古 B1)',
package/lib/index.js CHANGED
@@ -9,5 +9,5 @@ export { validateSessionLog } from './validate.js';
9
9
  export { createPreWriter, preWriterFromLog } from './prewrite.js';
10
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
- export { tokenMeterViolations } from './checks.js';
12
+ export { tokenMeterViolations, tokenMeterSourceViolations, physicalOrderViolations, inboxReplayViolations } from './checks.js';
13
13
  export { auditToolCalls, extractText, extractToolOutputs, indexToolCalls, toolCommandOf } from './archaeology.js';
package/lib/repair.js CHANGED
@@ -156,10 +156,11 @@ export function clipCrossStepSourcesText(text) {
156
156
  if (event.type !== 'assistant/message' || !Array.isArray(event.sourceEventSeqs) || event.sourceEventSeqs.length === 0) continue;
157
157
  const { turn, step } = event.data ?? {};
158
158
  if (turn == null || step == null) continue; // turn-null marker 由 neutralize 处理
159
+ if (event.data?.usage === void 0) continue; // 无 usage 不触发 _estimateProviderAssistant(官方 :592 前提)——replace marker 的 sourceEventSeqs 是 S5 遮蔽语义,非 chunk 引用合法,不可裁
159
160
  let dirty = false;
160
161
  const kept = event.sourceEventSeqs.filter((s) => {
161
162
  const src = bySeq.get(s)?.event;
162
- if (!src || src.type !== 'assistant/chunk') return true; // 保留非 chunk 引用(边界事件)
163
+ if (!src || src.type !== 'assistant/chunk') { dirty = true; return false; } // chunk/缺失引用官方 :644 直接 throw → 裁剪
163
164
  if (src.data?.turn === turn && src.data?.step === step) return true;
164
165
  dirty = true;
165
166
  return false;
package/lib/validate.js CHANGED
@@ -13,11 +13,14 @@ import {
13
13
  envelopeViolations,
14
14
  engineViolations,
15
15
  finalFold,
16
+ inboxReplayViolations,
16
17
  isSafeInt,
18
+ physicalOrderViolations,
17
19
  pluginViolations,
18
20
  replaySurface,
19
21
  violation,
20
22
  tokenMeterViolations,
23
+ tokenMeterSourceViolations,
21
24
  toolPairingViolations,
22
25
  toolResultStructureViolations,
23
26
  wireViolations,
@@ -99,14 +102,21 @@ export function validateSessionLog(log, opts = {}) {
99
102
  }
100
103
  }
101
104
 
105
+ // ── S9 · 文件物理序 seq 单调(多写入者交织现场;E2 排序后检查看不到)──
106
+ violations.push(...physicalOrderViolations(rows));
107
+
102
108
  // ── S · surface 增量重放(归因)+ 官方 foldSurface 终验 ────────────────
103
109
  const replay = replaySurface(events);
104
110
  violations.push(...replay.violations);
105
111
 
106
112
  const folded = finalFold(events.map((e) => e.event));
107
113
 
108
- // ── T · token meter 配对(事故根因 3)──
114
+ // ── T · token meter 配对(事故根因 3 + 2026-08-30 两类刷屏)──
109
115
  violations.push(...tokenMeterViolations(events));
116
+ violations.push(...tokenMeterSourceViolations(events));
117
+
118
+ // ── I1 · inbox seed 相对重放(fork 边界孤儿;交接书 L1)──────────────
119
+ violations.push(...inboxReplayViolations(events, header));
110
120
 
111
121
  // ── P3/P4 · 考古契约(工具配对 + 输出结构)──
112
122
  violations.push(...toolPairingViolations(events));
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",
4
+ "version": "0.3.6",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "bin": {
@@ -24,7 +24,8 @@
24
24
  "bin/**/*.mjs",
25
25
  "docs/**/*.md",
26
26
  "README.md",
27
- "LICENSE"
27
+ "LICENSE",
28
+ "README.zh.md"
28
29
  ],
29
30
  "keywords": [
30
31
  "dsh",