dsh-messages-sanitizer:自动修复 DeepSeek Harness 会话里的孤儿 tool_calls

前言

在 DeepSeek Harness 里开发或加载插件时,一次工具调度崩溃(例如 Cannot read properties of undefined (reading 'prepare'))就会在会话日志里留下孤儿 tool_calls。之后每一轮请求都被 400 INVALID_REQUEST 拒绝,重试无效,会话卡死,只能放弃这个会话。

下面介绍的 dsh-messages-sanitizer 就是为这个问题准备的:它在运行时自动把 messages 数组修回合法,让对话继续。

问题是怎么发生的

OpenAI 兼容协议要求工具调用成对出现,且 tool 消息必须紧邻其 assistant tool_calls 消息:

assistant  { content: ..., tool_calls: [{ id: "call_A", ... }] }
tool       { tool_call_id: "call_A", ... }   ← 必须紧跟,覆盖每个 id

当一次工具调度在「记录 assistant tool_calls / tool/call 之后、产出 tool 结果之前」崩溃时,会话日志会留下一个没有 tool 消息响应的孤儿 tool_calls。下一轮请求拼出的历史是这样的:

[..., assistant{tool_calls:[write]}, user{...}]        ← 非法

API 直接返回 400 INVALID_REQUEST,且重试时历史原封不动,反复被拒。如果崩溃后还有多次失败重试,日志里还会留下多条重复的 user 消息,横在孤儿 assistant 与注入点之间——这时连「补插 tool 消息」也无法满足紧邻约束。

这是什么

dsh-messages-sanitizer 是一个 DeepSeek Harness 插件,作者 Leeminjing,MIT 许可证,当前版本 0.1.1。一句话定位:自动修复 messages 数组中无效的 tool_calls/tool 消息配对(孤儿 tool_calls),阻止 400 INVALID_REQUEST 导致的会话卡死。

同类故障在 DeepSeek Harness 官方仓库也有跟踪:Discussion #4843 描述了会话历史中存在无配对 result、或 id/name/arguments 残缺的 tool_calls 时 chat-completions 接口返回 400 的问题,官方从 harness 源码层给出根因修复。本插件与那个修复是互补而非替代:它不修改 DSH 源码,只在运行时自动矫正 messages 数组,相当于一个安全网。已污染的旧会话、或跑在仍带该 bug / 工具调度崩溃的 harness 版本上的用户,都能被自动救回并继续对话,而不必等待 harness 发版。

四层修复机制

插件按处理时机分四层工作。

1、自动续跑(agent/status,主路径)

工具调度崩溃后,趁 agent 回到 idle 时,把合成 error tool-result 送回报文箱并唤醒它。错误会原样上报给 LLM,下一步换工具、重试还是向用户报告,由 LLM 自己决定。这保证 AI 消息始终是最后一条,对话不卡死。

2、预防(agent/pre-step)

追踪每个会话中「已声明但从未被 tool/result 响应」的调用;下一轮请求构建前,把合成 error tool-result 消息插到该步消息最前面(覆盖重启后恢复的旧孤儿)。合成消息随 decision.messages 以 user/message 事件落盘,deriveMessages() 从根上恢复合法,从源头杜绝 400。

3、治愈(agent/request-error)

如果 API 仍因 tool_calls 配对/紧邻违规返回 400(例如旧版本已污染的会话、或孤儿 assistant 后面已横着过期消息),插件用 surface 替换完成修复,然后强制重试一次(重试基于修复后的日志重建请求)。具体动作:

  1. 把悬空 assistant 消息改写成不含 tool_calls 的版本(剥离无响应的调用);
  2. 把孤儿 tool 消息中和成纯文本 user 消息;
  3. 恢复被误剥但结果仍紧邻的 assistant(还原 tool_calls,保留历史工具上下文);
  4. 折叠崩溃重试留下的重复 user 消息。

修复是幂等的:第二次遇到同一违规时无事可做,自然回退下游策略,不会无限重试。

4、兜底(llm/stream)

对每个请求做纯数组矫正,包括配对、紧邻重排、孤儿/重复丢弃、空 assistant 丢弃。循环构建的请求是冻结的,只告警不改写;compaction、session-title 等自建 messages 的非冻结请求则直接原地替换。

安装与启用

先执行安装命令,再重启 harness 即生效(插件随 profile 层栈自动加载):

dsh plugin --profile web add github:Leeminjing/dsh-messages-sanitizer

更新到最新版:

dsh plugin --profile web update dsh-messages-sanitizer

配置项只有一个:enabled(boolean,默认 true),是总开关。停用时,删除 cordis.patch.yml 中的插入行,或改为:

- insert:
    - id: messages-sanitizer
      name: 'dsh-messages-sanitizer'
      disabled: true

验证

插件自带测试,覆盖纯函数矫正、会话追踪、请求失败修复和真实 cordis/Session 集成:

cd dsh-messages-sanitizer
node --test        # 共 40 个用例

测试均用真实 @deepseek-ai/dsh-session 的 foldSurface / Session 校验,包括真实崩溃序列的端到端模拟、污染日志修复后的幂等验证、surface 替换在真实 Session 上执行,以及请求失败修复只在 tool_calls 配对 400 时干预、修复后强制重试一次且不会无限重试。

适用场景与注意事项

适合两类人:一是在 DeepSeek Harness 上开发插件、遇到过工具调度崩溃导致会话卡死的开发者;二是手上还有已污染的旧会话想救回的用户。后者不需要等 harness 发版,治愈路径会在第一次请求失败时自动剥离悬空调用并重试成功。

安装和使用前注意几点:

  1. 插件以当前 dsh 进程权限运行,安装前应检查源码与许可证(MIT)。
  2. 要求 Node >= 18。插件是零构建的纯 ESM,修改插件代码后无需重新构建,重启 harness(或让 cordis HMR 重载)即生效。
  3. 运行期 peer 依赖 @deepseek-ai/cordis@deepseek-ai/dsh-agent@deepseek-ai/dsh-llm@deepseek-ai/dsh-session@deepseek-ai/schemastery,与 harness 运行时同源。
  4. 仓库目录下的 node_modules 是指向 harness 运行时 ~/.dsh/profiles/node_modules 的 junction,仅为本地 node --test 提供依赖解析,harness 运行期不依赖它。
  5. 它与 DeepSeek Harness 官方 Discussion #4843 的源码层修复互补而非替代:官方修复在 DSH 源码里根治,本插件不修改 DSH 源码、在运行时自动矫正 messages 数组,可救回已污染的旧会话。

结尾

dsh-messages-sanitizer 解决的是一个很具体的死局:一次工具调度崩溃,就让整个会话再也无法继续。它通过续跑、预防、治愈、兜底四层机制在运行时把 messages 数组修回合法,且修复幂等、不会引入新的循环问题。插件目录页在 https://www.skillhub.cn/plugins/Leeminjing/dsh-messages-sanitizer ,源码与 README 见 https://github.com/Leeminjing/dsh-messages-sanitizer 。

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye