DeepSeek Harness 工具调用后崩溃 "reading 'prepare'"?核心包版本漂移排查
DeepSeek Harness 会话中途工具调用后崩溃、报 Cannot read properties of undefined (reading 'prepare'),且纯文本回复一切正常——根因通常是版本漂移:第三方插件把 @deepseek-ai/dsh-tools 声明为直接依赖,装进 profile 时解析到旧版本(实测 0.1.0-rc.8 vs 全局 0.1.1-rc.2),工具调度器的 Symbol() 键跨份不相等,agent loop 取到 undefined 直接崩。 对比 profile 与全局的核心包版本、二分禁用插件即可定位并恢复;这与「重复核心包」同源但触发路径不同,本文单独排查。
DeepSeek Harness 工具调用后崩溃长什么样
报错固定为 Cannot read properties of undefined (reading 'prepare')、code UNKNOWN,出现在工具调用之后,且纯文本回合完全正常。 报告者实跑复现(讨论原文),现象如下:
- 会话日志序列:
tool/call发出后紧跟step/end,从未写出tool/result,随后turn/end报reason.error "Cannot read properties of undefined (reading 'prepare')"; - 连续多轮复现:原帖 3 连崩(turn 14/15/16),后续复现扩展到 10 个连续 turn 崩溃;
- 纯文本 turn 正常(无工具时
turn/end completed)——说明模型/provider 链路健康,故障集中在工具分发层; - 重启/resume 后会话即恢复(
reason:"resume"),服务层从未 crash(curl :3080长期 HTTP 200)。
这条证据链有个关键判别点:会话早期(turn 1–13)每个 tool/call 都有配对 tool/result,说明调度器当时是活的;如果 profile 里静态躺着重复核心包,第一个工具调用就该崩——中途才开始崩,指向"进程生命周期内模块图被改动"或"另一条求值边"。
版本漂移与重复核心包:同源不同路径
两者共享同一机制——TOOL_RUNTIME_SCHEDULER 是普通 Symbol() 而非 Symbol.for()(定义处,实测 lib/index.js:2416),每次调用生成唯一实例,两份副本的键永远不相等,跨份读取 registry[TOOL_RUNTIME_SCHEDULER] 就是 undefined(#4529 根因确认)。 但触发载体不同:
- 静态重复(#4640):profile 里躺着一棵残留的
node_modules/@deepseek-ai/树,启动后第一个工具调用就崩; - 版本漂移(本文):插件把核心包当直接依赖装进 profile,解析到旧版本(
0.1.0-rc.8≠ 全局0.1.1-rc.2),会话中途开始崩(turn 14 起),重启/resume 或禁用该插件后恢复。
报告者实测确认:profile node_modules/@deepseek-ai/dsh-tools 是实体目录、非 symlink、版本 0.1.0-rc.8,与全局 CLI 内 0.1.1-rc.2 的 lib/index.js:2416 字符串相同但 Symbol 实例不同——两条物理文件构成两条求值边。
谁把旧版核心包拉进 profile:直接依赖名单
把 @deepseek-ai/dsh-tools 声明为 dependencies(而非 peerDependencies)的第三方插件,就是注入源。 报告者逐包核对(来源):
dsh-obsidian:"@deepseek-ai/dsh-tools": "*"(通配符最危险,永远装最新但可能漂移);dsh-better-sidebar:"^0.1.0-rc.8";dsh-chat-import、dsh-recall:"^0.1.0-rc.8"系;实测名单里是"^0.1.0-rc.6";dsh-univer-office:"0.1.0-rc.8"(固定旧版本,直接把旧核心包钉进 profile)。
pnpm 解析到 0.1.0-rc.8(旧于全局 0.1.1-rc.2)就构成版本漂移的第二条求值边。另有旁路:npm install <plugin>(npm ≥7 会自动安装插件的 peer set)也会复制核心包,而 dsh plugin(pnpm、autoInstallPeers: false)不会——同一批插件,安装通道不同结果不同。
排查步骤:对比核心包版本 + 二分禁用插件
定位思路两条:先对比 profile 与全局的核心包版本,再用禁用插件做二分。 按步骤操作:
- 确认模型/provider 健康:让 Agent 发一段纯文本回复,正常即排除 provider 问题;再用 curl 确认服务层 200:
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:3080/
- 排除静态重复:开一个全新会话跑第一个工具调用——首轮就崩走重复核心包排查,中途崩继续本文;
- 对比版本:先看全局 CLI 版本(
dsh --version),再列出 profile 里的核心包,逐个比对:
dsh --version
ls ~/.dsh/profiles/<name>/node_modules/@deepseek-ai/
profile 里是旧版本(如 0.1.0-rc.8、全局 0.1.1-rc.2)即漂移;
4. 找物理副本:用 find 确认 profile 内是否有实体目录:
find ~/.dsh ~/.npm-global -type d -name dsh-tools 2>/dev/null
- 二分禁用插件:列出已装插件,把可疑第三方插件在 profile 配置里设为
disabled: true:
dsh plugin --profile <name> list
重启 dsh web,新会话工具全部正常(bash/read/glob 均返回结果)即锁定罪魁;
6. 彻底恢复:卸载该插件 + 移走 profile 内版本漂移的核心包副本,重启后新会话工具恢复;已遗留 tool_calls 的旧会话放弃。
报告者实测:禁用第三方视觉插件并重启后,新会话工具全部恢复正常——该插件的 enable/disable 就是"能否稳定复现"的开关,可据此做 A/B 验证。
修复与预防:Symbol.for 与插件依赖规范
修复方向分两层:官方把调度器键改成跨副本一致的 Symbol.for(),插件作者把核心包放 peerDependencies。 社区给出的建议(来源):
- 键改
Symbol.for('@deepseek-ai/dsh-tools.scheduler'):两份副本算出同一个键,崩溃降级为"仅冗余安装"; - 读取处显式判空:
registry[TOOL_RUNTIME_SCHEDULER]命中 undefined 时抛可行动诊断(点名本机可能的重复/漂移来源),而不是裸UNKNOWNTypeError——本次排查正是因为错误被统一抹平成 UNKNOWN 才花了大量人时; - 插件作者契约:
@deepseek-ai/*一律放peerDependencies,严禁写进dependencies;"*"通配符和固定旧版本尤其要避免; - 诊断配方:崩溃在 web 会话 log 内、不落 stderr,解析
session.jsonl找turn/end error,配合turn_summary.py逐 turn 统计tool/call与tool/result配对即可快速定位。
在官方修复落地前,按本文步骤禁用/卸载注入源即可恢复。这类问题说明插件依赖声明规范比功能更重要——如果你不想逐个核对依赖树,安装目录里经过人工验证的插件会少很多这类打包问题。DeepSeek Harness 桌面端内置的 DSH Plugin Hub(dsh-plugin.org)集中展示插件来源与已装清单,禁用、卸载一目了然,避免手改 profile 依赖。
注意事项
- 崩溃 turn 后遗留
tool_calls无结果,该会话可能被模型提供方以 400INVALID_REQUEST拒绝(#4549 恢复缺口),别在同一会话里反复重试。 - 禁用插件 + 重启是"两个变量同时变",若要严谨归因,先只重启不重启插件、再只禁用不重启,分开验证。
- 崩溃当时 profile 是否已含漂移副本,报告者承认无法回溯确认——若你的场景需要铁证,在崩溃前/后的干净环境做 A/B。
- 同类插件报错还汇总在《DeepSeek Harness 插件报错合集:DSH plugin 不加载、Web UI 异常与会话缓存修复》里。
来源:Discussion #4601、#4529(根因确认)、packages/core/tools、packages/core/agent-loop
常见问题
第三方插件把 @deepseek-ai/dsh-tools 声明为直接依赖(dependencies 而非 peerDependencies),安装进 profile 时解析到旧版本(实测 0.1.0-rc.8,晚于全局 CLI 的 0.1.1-rc.2)。两份核心包各自用普通 Symbol() 做调度器键、互不相等,agent loop 跨份读取时取到 undefined(来源)。
机制同源(Symbol() 对重复加载不健壮),但触发路径不同:静态重复是 profile 里残留整棵副本、启动后第一个工具调用就崩;版本漂移是插件把旧版核心包当直接依赖装进 profile,会话中途(如第 14 轮)才开始崩,且重启/resume 或禁用该插件后即恢复。
三步:开全新会话看第一个工具调用是否崩(崩=静态重复,参考另一篇排查);对比 profile 与全局核心包版本(dsh --version 看全局,ls ~/.dsh/profiles/<name>/node_modules/@deepseek-ai/ 看 profile);再跑 find ~/.dsh ~/.npm-global -type d -name dsh-tools 2>/dev/null 找物理副本。
把 @deepseek-ai/dsh-tools 写进 dependencies 的插件都会。实测名单:dsh-obsidian("*")、dsh-better-sidebar("^0.1.0-rc.8")、dsh-chat-import / dsh-recall("^0.1.0-rc.6")、dsh-univer-office(固定 "0.1.0-rc.8" 旧版)。npm ≥7 还会自动安装插件的 peer set 复制核心包,dsh plugin(pnpm,autoInstallPeers: false)不会。
先禁用可疑第三方插件(profile 里 disabled: true)重启 dsh web 验证工具恢复,再彻底卸载该插件并清理 profile 内版本漂移的核心包副本;崩溃 turn 遗留 tool_calls 无结果时,该会话可能被模型提供方以 400 INVALID_REQUEST 拒绝,只能开新会话。
来源
- deepseek-harness Discussion #4601:bash 工具调用后 agent loop 崩溃,3 个连续 turn 报 reading 'prepare'· deepseek-ai(GitHub Discussions)
- deepseek-harness Discussion #4529:--profile headless 任何工具调用都崩,根因确认(TOOL_RUNTIME_SCHEDULER 为普通 Symbol())· deepseek-ai(GitHub Discussions)
- deepseek-harness packages/core/tools(TOOL_RUNTIME_SCHEDULER 定义处)· deepseek-ai
- deepseek-harness packages/core/agent-loop/src/tool-calls.ts(调度器读取处)· deepseek-ai