Skip to content

[Bug] 0.10.0 ⇄ 0.9.2:升级后新会话首轮必失败,回退后迁移过的会话被静默移出工作区 #621

Description

@HACC-svg

摘要

在 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 下每个新会话第一轮必定失败

复现步骤

  1. 从 0.9.0 通过客户端内更新升级到 0.10.0,重启客户端
  2. 打开客户端 → 点「新对话」
  3. 任意发一句话

实际结果

处理失败
● 本轮运行失败  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\。)


建议

按优先级:

  1. 代际选择应回退到「最老的可用且受支持」而不是「版本号最大」(故障 B 第 1 步)。当最大代际被 SessionFormatUnsupportedError 拒绝、而目录里还存在可读的旧代际时,应使用旧代际并把新代际标记为"由更新版本写入",而不是把整条会话判为 "header is missing"。这是本次最直接的修复点。
  2. 工作区成员剔除不该静默发生。mutate() 里那行 filter 会把"读不到头的会话"直接从 sessionIds 里删掉并落盘。建议:改为移入显式的"未归类/孤儿会话"集合(UI 可见、可恢复),或至少在 UI 上给出"该会话由更新版本创建,需升级客户端"的提示。
  3. 提供一个不依赖 initialized 标志的恢复入口(故障 B 第 6 步)。例如启动时或设置里提供"按磁盘会话头重建工作区成员",或对 bootstrap 增加一个显式的强制重跑参数。
  4. 让 v4 迁移对 kind: "plugin" 的消息做兼容映射,或让所有 producer 补齐 kind,修掉"新会话首轮必然失败"(故障 A)。
  5. 把 @deepseek-ai/dsh-workflow-worker-thread 补进构建产物(或从 preset 里移除该行,并给 preset 加版本号,避免旧 preset 引用新包里不存在的插件)。
  6. 升级时重建 profile 里的 @deepseek-ai\* 链接。fix(startup): make recovery independent of profile links (#533) 已在 0.9.2 里做过类似修复,0.10.0 疑似回归。
  7. 写 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 机器上实测并定位,日志与代码行号均为本机原始输出。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions