摘要
在 0.10.0 → 0.9.2 的一次升级/回退往返中,踩到两个互相独立、但都足以让客户端"不可用或像丢了数据"的硬故障:
| # |
故障 |
触发版本 |
严重度 |
已定位位置 |
| A |
每个新建会话第一轮必定失败,报 format v4 message requires a producer-owned source kind |
0.10.0 |
阻断(无法开新对话) |
dsh-session-format-v3-to-v4/lib/index.js:126 |
| B |
回退到 0.9.2 后,0.10.0 迁移过的会话从侧栏消失(数据没丢,但被静默移出工作区,用户以为丢了) |
0.9.2 读取 v4 日志 |
严重(数据"看不见") |
dsh-session-persistence-jsonl + dsh-workspace 共 5 处,见下 |
故障 B 是本次报告的重点:它有完整代码链和日志证据,且用户侧没有任何提示,很容易被误判为"聊天记录丢了"。
环境
| 项目 |
值 |
| OS |
Windows 11 家庭中文版 25H2 (build 26200.x) |
| DSH Desktop |
0.10.0(2026-09-26 发布,从 0.9.0 客户端内更新升级);后回退至 0.9.2 |
| 捆绑 Harness |
0.10.0 内置 @deepseek-ai/dsh 0.1.7-rc.2;0.9.2 内置 0.1.5-rc.2 |
| 安装位置 |
自定义目录 D:\harness\DSH Desktop(非默认路径) |
| Profile |
web(%APPDATA%\dsh-desktop\harness\profiles\web) |
| 模式 |
标准模式 |
故障 A:0.10.0 下每个新会话第一轮必定失败
复现步骤
- 从 0.9.0 通过客户端内更新升级到 0.10.0,重启客户端
- 打开客户端 → 点「新对话」
- 任意发一句话
实际结果
处理失败
● 本轮运行失败 format v4 message requires a producer-owned source kind
每一个新会话都如此,100% 复现;而升级前就存在、已完成 v3→v4 迁移的旧会话可以正常对话。
期望结果
新会话第一轮正常工作。
日志证据
%APPDATA%\dsh-desktop\logs\harness.log:
[harness-log] session-error session-fba24d93-1755-418b-8356-b2fbe9577c55:
format v4 message requires a producer-owned source kind
[harness-log] warn session-title-service: session "session-fba24d93-...":
automatic title generation failed: SessionFormatError: format v4 message requires a producer-owned source kind
[harness-log] warn session-projection-cache: session projection cache:
turn/end write for "session-fba24d93-..." failed (cache stays stale):
SessionFormatError: format v4 message requires a producer-owned source kind
出错位置
D:\harness\DSH Desktop\resources\app.asar.unpacked\node_modules\@deepseek-ai\dsh-session-format-v3-to-v4\lib\index.js:126
if (!isSessionFormatJsonObject(value) || typeof value["kind"] !== "string"
|| value["kind"].length === 0 || value["kind"] === "plugin")
throw new SessionFormatError("format v4 message requires a producer-owned source kind");
即:v4 明确拒绝 kind === "plugin" 的消息,但仍有某个来源在写这种消息 —— 生产方与校验方的契约不一致。
故障 B:回退到 0.9.2 后,迁移过的会话被静默移出工作区
现象
回退到 0.9.2 并重启客户端后,侧栏里只剩回退之后新建的会话;升级前就在、且被 0.10.0 迁移过的那几条长会话完全不见(不是报错,是不显示)。日志目录里同时存在 session.v3.jsonl.zstd 与 session.v4.jsonl.zstd。
%APPDATA%\dsh-desktop\harness\storages\workspace.json 里,这几条会话的 id 已经被从未参与过任何用户操作地删掉了。
日志证据
每次启动都会出现(本次为 3 条,与磁盘上存在 v4 日志的会话完全一致):
[harness-log] warn workspace-registry: workspace 'd5681dab-…' filtered session 'session-fba24d93-…' from membership: session header is missing
[harness-log] warn workspace-registry: workspace 'd5681dab-…' filtered session 'session-a2673f31-…' from membership: session header is missing
[harness-log] warn workspace-registry: workspace 'd5681dab-…' filtered session 'session-50432086-…' from membership: session header is missing
同一时刻,其余 10 条会话(只有 v3 日志)全部正常。
根因链
| 步骤 |
位置(0.9.2 内置 dsh-session-persistence-jsonl@0.1.5-rc.2 / dsh-workspace) |
行为 |
| 1 |
dsh-session-persistence-jsonl/lib/index.js:3233 |
选版本号最大的那一代日志:
const latest = generations.sort((left, right) => right.version - left.version)[0]; |
| 2 |
同上 :10519 |
选中文件的头 version !== 3 → 抛 SessionFormatUnsupportedError(0.9.2 只装了 dsh-session-format-v0-to-v1 / v1-to-v2 / v2-to-v3,没有 v4 编解码) |
| 3 |
同上 :5649 |
拒绝文案:session "…" uses log format v4, but this harness reads only v3: the log was written by a newer harness — upgrade the harness to open it |
| 4 |
dsh-workspace/lib/index.js:763 |
头读不到 → 归类为 "session header is missing" |
| 5 |
dsh-workspace/lib/index.js:178 |
mutate() 的唯一写路径按「id + 规范化 cwd」过滤成员:
const sessionIds = changed.sessionIds.filter((id) => this.host.sessionPath(id) === changed.path) → 写回 workspace.json 时把这几条会话剔除 |
| 6 |
dsh-workspace/lib/index.js:344 |
历史重建 bootstrap(headers) 只在 state.initialized === false 时执行,而该用户的 workspace.json 是 true → 永远不会自动恢复 |
关键点在第 1 步:同一目录里明明有可读的 v3,却因为"v4 更新"而整条会话被判为"头缺失",既没有回退到更老的可用代际,也没有给用户任何可见提示。
影响
- 用户看到的是"我的聊天记录没了",极易误判为数据丢失(实际数据仍完整躺在磁盘上)
workspace.json 被静默改写,且是不可见的副作用(没有 UI 提示、没有备份)
- 也无法通过重启自愈(第 6 步)
用户侧可用的恢复步骤(本机已验证有效)
# 1) 让 0.9.2 回落到可读的 v3:把 v4 日志改成一个非规范名(两个压缩分支都不匹配,等于移出代际选择)
$p = "$env:APPDATA\dsh-desktop\harness\sessions\--D-DeepSeek~0020Harness~0020Work~0020Area--"
Rename-Item "$p\session-a2673f31-…\session.v4.jsonl.zstd" session.v4.jsonl.zstd.bak
Rename-Item "$p\session-50432086-…\session.v4.jsonl.zstd" session.v4.jsonl.zstd.bak
# 2) 手工把会话 id 挂回 %APPDATA%\dsh-desktop\harness\storages\workspace.json 的 sessionIds
# (bootstrap 不会重跑,必须手改;改完重启客户端)
.bak 后缀可被 parseSessionFormatLogFilename 正确忽略 —— 判据是
dsh-session-format/lib/index.js:464:
const CANONICAL_LOG_FILENAME = /^session(?:\.v([1-9][0-9]*))?\.jsonl$/u;
该正则对 session.v4.jsonl.zstd.bak 在 zstd 与明文两个分支下都返回 undefined。
另外两处可疑点(可能相关,单独列出)
1) 0.10.0 安装包里缺少 @deepseek-ai/dsh-workflow-worker-thread
agent-presets\workbench\agent.cordis.yml 第 222-228 行要求它:
- id: workflow-worker-thread
name: '@deepseek-ai/dsh-workflow-worker-thread'
config:
provider: spawn
- id: tool-workflow
name: '@deepseek-ai/dsh-tool-workflow'
但:
resources\app.asar.unpacked\node_modules\@deepseek-ai\ 下没有 dsh-workflow-worker-thread
resources\app.asar(6.2 MB)里也没有
- npm 上的
@deepseek-ai/dsh@0.1.7-rc.2 依赖树里同样没有
启动日志因此报:
error group: Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@deepseek-ai/dsh-workflow-worker-thread'
imported from ...\app.asar.unpacked\node_modules\@deepseek-ai\dsh\lib\bin.js
host fallback ... also failed
warn agent-preset-registry: agent preset workbench: workflow-worker-thread
(@deepseek-ai/dsh-workflow-worker-thread): never started
2) 升级后 profile 里 node_modules\@deepseek-ai\* 全是失效目录链接(junction)
0.10.0 把应用代码从 resources\app\ 改成 resources\app.asar,但 profile 里的链接仍指向旧路径:
<profile>\node_modules\@deepseek-ai\dsh-base
LinkType = Junction
Target = D:\harness\DSH Desktop\resources\app\node_modules\@deepseek-ai\dsh-base ← 该目录已不存在
(该 profile 下共有数百个这样的链接,全部指向已删除的 resources\app\。)
建议
按优先级:
- 代际选择应回退到「最老的可用且受支持」而不是「版本号最大」(故障 B 第 1 步)。当最大代际被
SessionFormatUnsupportedError 拒绝、而目录里还存在可读的旧代际时,应使用旧代际并把新代际标记为"由更新版本写入",而不是把整条会话判为 "header is missing"。这是本次最直接的修复点。
- 工作区成员剔除不该静默发生。
mutate() 里那行 filter 会把"读不到头的会话"直接从 sessionIds 里删掉并落盘。建议:改为移入显式的"未归类/孤儿会话"集合(UI 可见、可恢复),或至少在 UI 上给出"该会话由更新版本创建,需升级客户端"的提示。
- 提供一个不依赖
initialized 标志的恢复入口(故障 B 第 6 步)。例如启动时或设置里提供"按磁盘会话头重建工作区成员",或对 bootstrap 增加一个显式的强制重跑参数。
- 让 v4 迁移对
kind: "plugin" 的消息做兼容映射,或让所有 producer 补齐 kind,修掉"新会话首轮必然失败"(故障 A)。
- 把
@deepseek-ai/dsh-workflow-worker-thread 补进构建产物(或从 preset 里移除该行,并给 preset 加版本号,避免旧 preset 引用新包里不存在的插件)。
- 升级时重建 profile 里的
@deepseek-ai\* 链接。fix(startup): make recovery independent of profile links (#533) 已在 0.9.2 里做过类似修复,0.10.0 疑似回归。
- 写
workspace.json 前做一次自动备份(例如 workspace.json.bak-<timestamp>),让这类静默改写可回滚。
附:复现与自查脚本
故障 B 可以用一个 30 行脚本在不启动客户端的情况下复现「哪一代日志会被选中、会不会被拒」——直接复用应用内的正则:
// 与 dsh-session-persistence-jsonl/lib/index.js:771-775 完全一致
import { parseSessionFormatLogFilename } from '@deepseek-ai/dsh-session-format'
const compressionSuffix = (c) => (c === 'zstd' ? '.zstd' : '')
function parseGenerationLogFilename(filename, compression) {
const suffix = compressionSuffix(compression)
if (!filename.endsWith(suffix)) return void 0
return parseSessionFormatLogFilename(filename.slice(0, filename.length - suffix.length))
}
// 然后每次取 generations.sort((l, r) => r.version - l.version)[0],读其首行 header.version 是否 === 3
本机跑出来的结果与日志完全吻合:
✗ session-50432086-… 候选: v4, v3 选中: session.v4.jsonl.zstd 头版本: 4 → 会被丢弃
✗ session-a2673f31-… 候选: v4, v3 选中: session.v4.jsonl.zstd 头版本: 4 → 会被丢弃
✗ session-fba24d93-… 候选: v4 选中: session.v4.jsonl.zstd 头版本: 4 → 会被丢弃
汇总: 可读 10 个 / 会被丢弃 3 个
临时规避(用户侧已验证)
- 故障 A:继续使用升级前已存在的旧会话;或在终端
npx @deepseek-ai/dsh web 用浏览器打开
- 故障 B:见上文「用户侧可用的恢复步骤」
本报告由用户在其 Windows 机器上实测并定位,日志与代码行号均为本机原始输出。
摘要
在 0.10.0 → 0.9.2 的一次升级/回退往返中,踩到两个互相独立、但都足以让客户端"不可用或像丢了数据"的硬故障:
format v4 message requires a producer-owned source kinddsh-session-format-v3-to-v4/lib/index.js:126dsh-session-persistence-jsonl+dsh-workspace共 5 处,见下故障 B 是本次报告的重点:它有完整代码链和日志证据,且用户侧没有任何提示,很容易被误判为"聊天记录丢了"。
环境
@deepseek-ai/dsh0.1.7-rc.2;0.9.2 内置 0.1.5-rc.2D:\harness\DSH Desktop(非默认路径)web(%APPDATA%\dsh-desktop\harness\profiles\web)故障 A:0.10.0 下每个新会话第一轮必定失败
复现步骤
实际结果
每一个新会话都如此,100% 复现;而升级前就存在、已完成 v3→v4 迁移的旧会话可以正常对话。
期望结果
新会话第一轮正常工作。
日志证据
%APPDATA%\dsh-desktop\logs\harness.log:出错位置
即:v4 明确拒绝
kind === "plugin"的消息,但仍有某个来源在写这种消息 —— 生产方与校验方的契约不一致。故障 B:回退到 0.9.2 后,迁移过的会话被静默移出工作区
现象
回退到 0.9.2 并重启客户端后,侧栏里只剩回退之后新建的会话;升级前就在、且被 0.10.0 迁移过的那几条长会话完全不见(不是报错,是不显示)。日志目录里同时存在
session.v3.jsonl.zstd与session.v4.jsonl.zstd。%APPDATA%\dsh-desktop\harness\storages\workspace.json里,这几条会话的 id 已经被从未参与过任何用户操作地删掉了。日志证据
每次启动都会出现(本次为 3 条,与磁盘上存在 v4 日志的会话完全一致):
同一时刻,其余 10 条会话(只有 v3 日志)全部正常。
根因链
dsh-session-persistence-jsonl@0.1.5-rc.2/dsh-workspace)dsh-session-persistence-jsonl/lib/index.js:3233const latest = generations.sort((left, right) => right.version - left.version)[0];:10519version !== 3→ 抛SessionFormatUnsupportedError(0.9.2 只装了dsh-session-format-v0-to-v1 / v1-to-v2 / v2-to-v3,没有 v4 编解码):5649session "…" uses log format v4, but this harness reads only v3: the log was written by a newer harness — upgrade the harness to open itdsh-workspace/lib/index.js:763"session header is missing"dsh-workspace/lib/index.js:178mutate()的唯一写路径按「id + 规范化 cwd」过滤成员:const sessionIds = changed.sessionIds.filter((id) => this.host.sessionPath(id) === changed.path)→ 写回 workspace.json 时把这几条会话剔除
dsh-workspace/lib/index.js:344bootstrap(headers)只在state.initialized === false时执行,而该用户的workspace.json是true→ 永远不会自动恢复关键点在第 1 步:同一目录里明明有可读的 v3,却因为"v4 更新"而整条会话被判为"头缺失",既没有回退到更老的可用代际,也没有给用户任何可见提示。
影响
workspace.json被静默改写,且是不可见的副作用(没有 UI 提示、没有备份)用户侧可用的恢复步骤(本机已验证有效)
另外两处可疑点(可能相关,单独列出)
1) 0.10.0 安装包里缺少
@deepseek-ai/dsh-workflow-worker-threadagent-presets\workbench\agent.cordis.yml第 222-228 行要求它:但:
resources\app.asar.unpacked\node_modules\@deepseek-ai\下没有dsh-workflow-worker-threadresources\app.asar(6.2 MB)里也没有@deepseek-ai/dsh@0.1.7-rc.2依赖树里同样没有启动日志因此报:
2) 升级后 profile 里
node_modules\@deepseek-ai\*全是失效目录链接(junction)0.10.0 把应用代码从
resources\app\改成resources\app.asar,但 profile 里的链接仍指向旧路径:(该 profile 下共有数百个这样的链接,全部指向已删除的
resources\app\。)建议
按优先级:
SessionFormatUnsupportedError拒绝、而目录里还存在可读的旧代际时,应使用旧代际并把新代际标记为"由更新版本写入",而不是把整条会话判为 "header is missing"。这是本次最直接的修复点。mutate()里那行 filter 会把"读不到头的会话"直接从sessionIds里删掉并落盘。建议:改为移入显式的"未归类/孤儿会话"集合(UI 可见、可恢复),或至少在 UI 上给出"该会话由更新版本创建,需升级客户端"的提示。initialized标志的恢复入口(故障 B 第 6 步)。例如启动时或设置里提供"按磁盘会话头重建工作区成员",或对bootstrap增加一个显式的强制重跑参数。kind: "plugin"的消息做兼容映射,或让所有 producer 补齐kind,修掉"新会话首轮必然失败"(故障 A)。@deepseek-ai/dsh-workflow-worker-thread补进构建产物(或从 preset 里移除该行,并给 preset 加版本号,避免旧 preset 引用新包里不存在的插件)。@deepseek-ai\*链接。fix(startup): make recovery independent of profile links (#533)已在 0.9.2 里做过类似修复,0.10.0 疑似回归。workspace.json前做一次自动备份(例如workspace.json.bak-<timestamp>),让这类静默改写可回滚。附:复现与自查脚本
故障 B 可以用一个 30 行脚本在不启动客户端的情况下复现「哪一代日志会被选中、会不会被拒」——直接复用应用内的正则:
本机跑出来的结果与日志完全吻合:
临时规避(用户侧已验证)
npx @deepseek-ai/dsh web用浏览器打开本报告由用户在其 Windows 机器上实测并定位,日志与代码行号均为本机原始输出。