Chuyển đến nội dung chính

dsh-session-log-repair

Đã xác minh

dsh-session-log-repair · v0.2.0 · MIT · Giao diện web

DSH session-log repair plugin: scan every stored session, detect a seq collision in the committed region or a torn trailing record, and repair it from a one-click Web UI, model tools, or the /dsh-session-log-repair command. Installing it also registers it

Cài đặt

dsh plugin add dsh-session-log-repair

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Thẻ

Readme

dsh-session-log-repair

dsh-session-log-repair logo

dsh-session-log-repair 是 DeepSeek Harness (DSH) 插件,用于修复提交区 seq 冲突与 末尾半条记录的会话日志 —— 也就是 Web GUI 里 历史加载失败 / failed to observe session … corrupt session log: seq gap in committed region … 或 corrupt Zstandard session log: complete frame contains a torn JSONL record 的成因。

  • 一键修复 —— 侧边栏底部按钮打开弹框,扫描全部会话并修复,支持单个与全部
  • 安全优先 —— 拒绝本进程内活动会话、写前复检 size+mtime、自动备份、原子发布、 发布后再用宿主后端复检
  • 四条入口 —— Web GUI 弹框、3 个模型工具、/dsh-session-log-repair 命令、带围栏的 HTTP 路由
  • 自带 skill —— 装上插件即注册 dsh-session-log-repair skill(诊断知识 + 离线脚本 工具链,DSH 起不来时也能用)
  • 双语 UI —— 跟随宿主界面语言(中文 / English)
  • 无构建链 —— 宿主半边是纯 ESM,浏览器半边是手写的 __ModuleLoader__ bundle

English

它修什么

DSH 会话日志是拼接多帧 zstd 的 JSONL,加载器要求每一行的 seq 紧接上一行。 当同一会话被两个写入者持有 —— 典型场景是一个进程卡在 LLM 重试退避里,另一个 进程 resume 同一会话并写入崩溃修复事件,卡住的进程醒来后用旧计数器追加 —— 两行不同的事件会占用同一批 seq,加载器随即拒绝整个日志,会话打不开。

两种加载器报错都能修:

加载器报错 含义
corrupt session log: seq gap in committed region at line N (expected X, got Y) 撞车行,且其后有 turn/end 触发精确报错
corrupt Zstandard session log: complete frame contains a torn JSONL record 撞车行之后没有 turn/end(扫描器只记 issue 不升级),或一条记录的换行符没落盘,留在完整帧里

修复保留存活链、丢弃重叠的旧版本:

  1. 先丢掉末尾缺换行的半条记录 —— 它从未成为事件,不会丢事件;
  2. 从文件末尾反向走,找出抵达末尾的最大稠密连续段(写出日志其余部分的那个写入者);
  3. 向前延伸:结束位置恰好接上链起点的行并入,范围探入链内的行判为重复版本丢弃;
  4. 遇到真实缺口就停止,剩余部分必须是稠密前缀,否则拒绝修复而不是猜;
  5. 插件识别的合成事件指纹(turn/end 的 reason.kind: 'interrupted'、紧随的 step/end、interrupted-tool-result-*、session/end-seed resume 标记)只会 因为「与存活链重叠」被丢弃,绝不因为标签被丢弃。

判定不依赖「谁是修复版本」,因此两个版本都是真实写入时同样成立。seq 不重编号, 修复后保留存活写入者的编号。

两类情况明确拒绝:真缺行(got > expected)与结构上不完整的尾帧 (加载器自己会恢复其中的完整记录)。

功能

  • 侧边栏底部入口(sidebar.footer.action):「会话修复 / Session repair」按钮 打开修复弹框(shell.overlay),列出全部会话的 ok / corrupt / unreadable / torn / live,支持单个修复与一键修复全部。
  • 模型工具:dsh_session_log_repair_scan、dsh_session_log_repair_apply (session / all / dryRun / force)、dsh_session_log_repair_verify。
  • 命令:/dsh-session-log-repair {"op":"scan|repair|verify|status", …}。
  • HTTP 路由:POST /dsh-session-log-repair/api,围栏限定 loopback/受信主机 + 同源标记, 返回 {"ok":true,"value":…}。
  • 自带 skill:dsh-session-log-repair,启动时注册进 ctx.skills (source: bundled),详见 随包 skill。
  • 配置:可选的 backupRoot / sessionsRoot,写在本包自己的 patch 行上。

目录结构

├── lib/index.js          # 宿主半边(纯 ESM):编解码、规划、操作、工具、路由、skill
├── lib/client.js         # 浏览器半边:__ModuleLoader__ 工厂(底部按钮 + 弹框)
├── skill/SKILL.md        # 随包 skill 正文:症状分型、容器契约、算法
├── skill/scripts/        # 离线工具链(无需宿主运行):scan / repair / verify
├── scripts/host-smoke.mjs    # 宿主半边集成测试(真实后端 + 合成损坏日志)
├── scripts/client-smoke.mjs  # 浏览器半边渲染测试(模块加载器 face + 两个插槽)
├── scripts/install-smoke.mjs # 安装脚本回归测试(重复 entry id、幂等)
├── scripts/install.mjs       # 安装 / 卸载到 DSH profile
├── scripts/host-resolve.mjs  # 测试共用的宿主包解析
├── cordis.patch.yml      # bundle patch:插入本包的加载行
├── package.json          # dsh.bundle + dsh.client(web) 清单 + peer/dev 依赖
├── .github/workflows/    # ci.yml(测试)+ release.yml(打 tag 发 Release)
├── README.md             # English
└── README.zh.md          # 本文件

安装

# 本地检出(本仓库的用法)
dsh plugin --profile web add ./dsh-session-log-repair

# 重启宿主让 bundle 层挂载
dsh --profile web --dump-config   # 验证:只有一行 dsh-session-log-repair
dsh --profile web                 # 启动

本仓库的安装脚本做同样的三处改动,并用宿主自己的 loader 组合 profile 来验证结果:

node scripts/install.mjs --profile web            # 安装 + 验证
node scripts/install.mjs --profile web --uninstall

一个插件只能有一条启用路径。 不要再往 profile 的 cordis.patch.yml 里加同样的 insert 行:bundle 层已经插入了同一个 entry id,两行同 id 会让启动直接失败并报 duplicate loader entry id: dsh-session-log-repair。安装脚本会删掉发现的这类行 (并保证用户 patch 层仍是合法的 YAML 数组),再用宿主 loader 组合一遍确认只剩 1 行。

bundle 层的行在树加载早期就 apply,此时 webserver 服务还没注册,所以插件通过 ctx.inject([…]) 等待 webServer / commands / skills,而不是一次性 ctx.get——否则 每次启动都会静默跳过路由注册。

重启宿主让 bundle 层挂载,并刷新浏览器页面加载客户端 bundle。

用法

Web GUI —— 点侧边栏底部的「会话修复」。弹框会扫描全部会话,列出无法加载的项, 支持单个「修复」和「一键修复全部」。

模型工具

工具 作用
dsh_session_log_repair_scan 扫描全部日志,输出 ok / corrupt / unreadable / torn / live
dsh_session_log_repair_apply 修一个会话(session)、修全部(all: true)、或只预览(dryRun: true)
dsh_session_log_repair_verify 用宿主真实加载路径复检

命令 —— /dsh-session-log-repair {"op":"scan"},repair / verify / status 用同样的 JSON 字段。

HTTP —— POST /dsh-session-log-repair/api,body {"op":"…"},返回 {"ok":true,"value":…}; 围栏限定 loopback/受信主机 + 同源标记。

安全边界

  • 读取走运行中的后端(sessionPersistence.readRaw),字节来自宿主自己的帧解码器; 行展开用宿主的 decodeStorageRecord。
  • 本进程内活动会话直接拒绝(scan 跳过);force: true 可显式覆盖。
  • 规划后、发布前复检文件(size + mtime),并发写入会让本次修复中止。
  • 写盘前把原文件复制到 $DSH_HOME/session-repair-backups/<id>-<时间戳>/<id>.jsonl.zstd.orig。
  • 修复结果写到同目录临时文件 → fsync → rename 覆盖(原子发布)。
  • 发布后再用后端自身重新加载;不一致会报错而不是默认成功。
  • 幂等:干净的日志永远不会被重写。

随包 skill

装上插件就同时激活 dsh-session-log-repair skill:宿主半边把它注册进 ctx.skills (source: bundled,resourceBase 指向 skill/),因此它会出现在模型技能目录里、可用 skill 工具加载——不需要再单独装到 ~/.agents/skills。

  • skill/SKILL.md:诊断与成因知识(症状分型、日志容器契约、成因指纹、存活链规则、宿主侧 加固建议、源码索引)。
  • skill/scripts/:离线工具链,不需要 DSH 运行——DSH 起不来时走这条路:
cd <deepseek-harness 检出目录>
node --import tsx/esm <插件目录>/skill/scripts/repair-all-sessions.mjs --dry-run
脚本 作用
skill/scripts/scan-sessions.mjs 只读扫描 ~/.dsh/sessions
skill/scripts/repair-all-sessions.mjs 扫全库并修复所有未被占用的会话(--dry-run、--backup-dir)
skill/scripts/repair-session-log.mjs 单会话诊断;--apply 才写盘
skill/scripts/verify-repaired-session.mjs 用真实 loader + 投影复检
skill/scripts/host-resolver.mjs pnpm 布局下的裸依赖解析兜底

配置

可选插件配置,写进本包自己的 cordis.patch.yml(bundle 层),不要写进 profile 的用户 patch:

- insert:
    - id: dsh-session-log-repair
      name: dsh-session-log-repair
      config:
        backupRoot: D:/backups/session-repair   # 默认 $DSH_HOME/session-repair-backups/<id>-<时间戳>
        sessionsRoot: D:/other/sessions         # 默认取后端配置的 root

开发

要求:Node ≥ 22.15 + pnpm 10(packageManager 字段固定 pnpm 版本)。测试脚本需要 宿主包,解析顺序是「DSH profile → 本包 devDependencies」,因此干净检出也能直接跑:

pnpm install       # devDependencies:@deepseek-ai/* 宿主包、react、react-dom
npm test           # 语法检查 + 三个冒烟测试
npm run test:host      # 真实后端 + 临时目录:工具、路由、围栏、skill 注册、fiber 回收
npm run test:client    # 模块加载器 face、插槽注册、渲染
npm run test:install   # 安装脚本:重复 entry id 迁移 + 幂等 + 卸载
npm run check          # 对每个源文件跑 node --check
npm run publish:npm    # npm test 之后 npm publish --access public

node scripts/host-smoke.mjs <真实损坏日志.jsonl.zstd> 可以改用真实损坏日志(默认用内置 合成日志)。插件自己的 scripts/ 与 skill 的 skill/scripts/ 是两个目录:前者驱动本包, 后者是 skill 正文引用的离线工具链。

自动化发布

工作流 触发 内容
ci.yml push 到 master、PR、手动 Node 26 → pnpm install --frozen-lockfile → npm test
release.yml 打 v* tag 同样的检查 → npm pack → 创建正式 GitHub Release 并附 tarball → 通过 npm Trusted Publishing(OIDC)执行 npm publish --provenance
npm run release    # npm test && npm version patch && git push --follow-tags

tag 会触发 release.yml:既创建 GitHub Release,也发布 npm 包。npm 走 Trusted Publishing(OIDC,id-token: write),不存长期 token,与 dsh-jenkins 一致。

首次发布需一次性 bootstrap:npm 的 Trusted Publishing 设置页只对「已存在的包」开放, 所以第一个版本要先在本机执行 npm run publish:npm(或 npm login && npm publish --access public)发布,再到 npmjs.com 该包 → Settings → Trusted Publishing 填仓库 jsoncode/dsh-session-log-repair 与工作流文件名 release.yml。此后每次打 tag 都会自动发版。

实现说明

  • 宿主半边是纯 ESM(lib/index.js),由宿主用原生 Node ESM 加载——无打包器、无构建产物。
  • 浏览器半边(lib/client.js)是单个 window.__ModuleLoader__.load 工厂,导出 { name, inject, apply };react / react-dom 保持外部依赖,运行时从宿主模块表解析。
  • peer 依赖(@deepseek-ai/dsh-session、@deepseek-ai/dsh-tools)是可选的:缺失也能工作, 后端不是 JSONL 时状态会报 unsupported。
  • 不修改官方 deepseek-harness:全部使用既有服务(tools、commands、skills、 webServer)与插槽(sidebar.footer.action、shell.overlay)。

许可证

MIT © jsoncode