dsh-session-surgeon 实现合同(多智能体共享)
September 2, 2026 · View on GitHub
兼容:@deepseek-ai/dsh@0.1.0-rc.6
对照源码(只读,禁止复制进 dependencies;路径随本机 DSH 安装位置变化):
@deepseek-ai/dsh-session/lib/types/chunk-rows.js@deepseek-ai/dsh-session/lib/types/repair.js@deepseek-ai/dsh-session/lib/types/known-event-types.js@deepseek-ai/dsh-session-persistence-jsonl/lib/index.js(scanZstdFrames / SessionLogScanner)
硬约束
- ESM only(
"type":"module"),Node^22.19.0 || >=24。 - 零 runtime dependencies。
@deepseek-ai/*只允许出现在 peerDependencies / optional 注释,禁止放进 dependencies。 - 单文件 ≤300 行;已有文件 >200 行先拆再加功能。
- 默认只读。写路径必须
--apply,先写session.jsonl.zstd.bak.<utc>。 - 禁止把
~/.dsh/sessions原文提交进 git。 - 测试用 Node 内置
node:test+node:assert/strict,不要加 vitest/jest。 - 不要改官方 dsh 源码。插件热插拔。
- 用
write/edit工具写文件,不要用 cat/heredoc 覆盖大文件。 - 只写自己被分配的文件。不要改别人的文件。
- 所有用户可见字符串:中英均可,CLI help 中英各一行。
文件所有权(禁止越界)
| 文件 | owner |
|---|---|
src/zstd-frames.mjs | zstd |
src/header.mjs | header |
src/packed.mjs | packed |
src/known-types.mjs | packed |
src/closers.mjs | closers |
src/decode.mjs | decode |
src/scanner.mjs | decode |
src/encode.mjs | encode |
src/scan.mjs | scan |
src/inspect.mjs | inspect |
src/repair.mjs | repair |
src/compact.mjs | compact |
src/redact.mjs | export |
src/export.mjs | export |
src/format.mjs | cli |
src/find.mjs | scan |
bin/dsh-session-surgeon.mjs | cli |
plugin/index.mjs | plugin-host |
plugin/client.mjs | plugin-client |
plugin/settings-card.mjs | plugin-client |
cordis.patch.yml | plugin-host |
package.json | cli(可加 scripts / dsh / files / exports;不要加 dependencies) |
fixtures/synthetic/build.mjs | fixtures |
fixtures/synthetic/*.session.jsonl.zstd | fixtures |
fixtures/synthetic/orphan-tmp/** | fixtures |
test/*.test.mjs | 对应 tester |
.github/workflows/ci.yml | fixtures |
现有 src/zstd-frames.mjs / src/scan.mjs / bin/dsh-session-surgeon.mjs 由对应 owner 整文件重写(先 read 再 write)。
语义(必须对齐官方,不是猜测)
Zstd
- Magic LE uint32 =
4247762216(字节28 B5 2F FD)。 scanZstdFrames(buf)必须按官方结构扫描:descriptor、content size、dict、blocks、checksum。- 中间帧 magic 非法 / reserved bit / reserved block type → throw。
- 最后一帧结构不完整 →
{ frames, tornStart },不要 throw。 - 完整帧用
zstdDecompressSync;失败 = 该帧 corrupt。 - torn 前缀用
zstdDecompress+finishFlush: zlib.constants.ZSTD_e_flush尽量救出完整 JSONL 行。 - 写回:
zstdCompress+params: { [ZSTD_c_checksumFlag]: 1 }。
Header
isHeaderLine:
- object,
type==="session" versionnumber,idstring,createdAt非负安全整数(拒绝 -0)delegationDepth非负安全整数(拒绝 -0)origin缺省或"subagent"agentPreset缺省或 string- 有
sandboxMode/approvalPolicy→ 当作退役字段,拒载
version !== 0 → foreign-version,不修,提示升级。
Packed
移植官方 decodeStorageRecord / packChunkRuns:
- tags:
text-chunks/reasoning-chunks/tool-call-chunks - 精确 key 集合,畸形行 必须 throw(不能当普通事件)
MIN_RUN = 3- 展开:
seq = seq0+k,time = time0 + sum(dt[0..k))
扫描器(decode)
对齐 SessionLogScanner.consumeEventLine:
- JSON.parse / decodeStorageRecord 失败 → issue
unparsable-line - 展开后
event.seq !== events.length:若本行是 packed 行、从已提交 seq 往回重叠但连续接到当前游标,丢掉已提交前缀、收下后缀(#5151)。否则回滚本行已 push 的事件。本行或后续行出现turn/end→ issueseq-gap-committed;否则 issueseq-gap-tail - issue 之后若出现
turn/end→ committed 缺陷,记录并停止接受(tail 升级为 committed) - issue 之后再无
turn/end→ 只保留 committed prefix,尾巴当脏尾 - 未知 type(不在 KNOWN 且无
ignorable)→ 标unknown-type,保留
Closers
对齐 interruptedTurnClosers:
- 扫 openTurn / openStep / pendingCalls
- assistant/message 的 tool-call block 登记 pending
- tool/call 补 callSeq
- tool/result 删除 pending
- 对每个 pending:合成 tool/result(started → TOOL_OUTCOME_UNKNOWN,否则 TOOL_NOT_STARTED)
- 再 step/end(若 open),再 turn/end reason=interrupted
- seq 从 last.seq+1,time 复用 last.time
- 合成 message:role=user, source.kind=tool, 一个 tool-result block
Repair 顺序(每步可关,默认全开)
- torn-tail:保留不完整帧里已解出的完整行
- seq-overlap:后写 seq ≤ lastAccepted → 丢掉从这条起的尾巴 2b. packed 行重叠已提交前缀、后缀连续:丢掉前缀成员,收下后缀
- seq-gap-committed:回退到 gap 前最后一个 turn/end(含)
- 若 gap 后没有 turn/end:丢掉 i 及之后,走 closer
- lone-surrogate:字符串里孤立代理 → U+FFFD
5b. newer-format-ranges:把 sourceEventSeqs 的 [start,end] 展开成包含端点的密集整数
5c. forward-event-shim:仅对结构校验通过的官方 Alpha
model/selection加ignorable: true,不删行、不改 seq/data - 合成 closer
- 重跑 decode,seq 必须连续,否则拒绝 --apply
- header 解不出 → 只报告不写
- 中间完整帧解压失败 → 不自动修,报告帧号
Compact
--keep-last-turns N(N≥1)- v0.1 走更稳妥路径:前面完整 turn 整段删除,从保留的第一个 turn/start 起重排 seq 从 0,
header.seedLength = 0。不插入compaction/summary - 中间坏帧 / header 非 header-ok / seq 不连续时 refuse(先 repair)
- 产出必须是合法独立 session 文件(自己的 header + 连续 seq)
Export
- 默认 redact:
sk-[A-Za-z0-9]{10,}、PEM 头、/home/<user>换成~ --no-redact必须显式- 输出 JSONL 文本到 stdout 或
--out
CLI
dsh-session-surgeon scan [root] [--format json|text]
dsh-session-surgeon inspect <id> [root] [--format json|text]
dsh-session-surgeon repair <id> [root] [--dry-run|--apply] [--format json|text]
dsh-session-surgeon compact <id> [root] --keep-last-turns N [--dry-run|--apply]
dsh-session-surgeon export <id> [root] [--no-redact] [--out file]
dsh-session-surgeon index [root] [--format json|text]
- repair 默认 dry-run
index= scan + parent/depth/preset/turns/goal/health 表- 退出码:成功 0,用法错 2,找不到/不可修 1
插件
形态对齐 @linxin666/dsh-live-stats / dsh-ssh:
package.json增加:"exports": { ".": "./plugin/index.mjs", "./client": "./plugin/client.js", "./package.json": "./package.json" }, "dsh": { "bundle": { "patch": "./cordis.patch.yml" }, "client": { "inject": ["@deepseek-ai/dsh-client-runtime","@deepseek-ai/dsh-client-ui-settings"], "platform": "web" } }cordis.patch.yml:- insert: - id: session-surgeon name: dsh-session-surgeon- host:
defineTool注册session_scan/session_inspect/session_repair(repair 默认 dryRun=true,apply 必须显式) @deepseek-ai/dsh-tools只放 peerDependencies- 若 host 环境没有 defineTool(单元测试),plugin 仍应能被 import(把 defineTool 包在 try 或延迟注册)
- host 在
ctx.webServer可用时注册/api/session-surgeon/{scan,inspect,repair,compact,export}(仅 loopback) - client:侧栏「会话医生」面板承载 scan/inspect/repair/compact/export;会话 ⋯ 菜单注入复制 ID / 检查 / 预览修复。不抢 aionui details 右栏。
- 插件不监听端口、不改 profile、不访问外网
测试黄金样例
| fixture | 期望 |
|---|---|
| torn-tail | inspect 报 torn-tail;repair --apply 后 decode seq 连续且有合成 turn/end |
| seq-gap-committed | 停在 gap 前最后一个 turn/end |
| seq-gap-tail | 保留空洞前连续前缀,不截到上一 turn/end |
| lone-surrogate | 不再含孤立代理;seq 不变 |
| orphan-tmp | scan 列出来,repair 不把它当正本 |
| healthy packed | inspect 展开后 seq 连续,dry-run 0 处必须修改 |
对照(官方包可动态 import,失败则 skip、不 fail CI):
- closer 与官方
interruptedTurnClosers逐条 type/seq/data.reason 相等 packChunkRuns/decodeStorageRecord与官方 chunk-rows 对同一事件数组深相等KNOWN_SESSION_EVENT_TYPES与官方 catalog 集合相等
package.json scripts
"test": "node --test test/*.test.mjs",
"scan": "node bin/dsh-session-surgeon.mjs scan",
"inspect": "node bin/dsh-session-surgeon.mjs inspect",
"build-fixtures": "node fixtures/synthetic/build.mjs"
不要加 dependencies 字段。