DeepSeek Harness 子进程 spill 文件短写丢字节怎么排查?循环写、零进度判失败与调高内存上限

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek Harness子进程输出spill 文件writeSync故障排查
DeepSeek Harness 子进程输出超过内存上限后写入 spill 文件,短写会让文件缺尾部,但路径已公布、内容被当成完整输出。根因是两处 writeSync 都不读返回值。本文给出循环写、零进度视为失败并 discardSpill、调高内存上限三条路径,附 4 字节上限复现法。

这不是磁盘写坏了,是 DeepSeek Harness 写文件时少写了字节却没人检查,文件还被继续当成完整输出公布。 排查时先别怀疑子进程本身,内存里的尾部往往完全正常,被截断的只有那份「用来兜底」的 spill 文件(来源)。

DeepSeek Harness 子进程 spill 短写的症状与 4 字节复现

典型表现是「模型看到的末尾没问题,但按提示去读完整输出文件时内容缺尾」——因为这两个数据源根本不是同一份。

  1. 找一个输出量很大的命令(例如全量构建日志、大文件 cat),让输出明显超过内存上限。预期界面提示有 spill 文件可读。
  2. 对比内存尾部的最后若干行与 spill 文件的最后若干行。预期尾部一致,但 spill 文件比预期短。
  3. 用 wc -c 量 spill 文件大小,与命令真实输出的字节数比较。预期文件偏小,且差值不是固定的截断长度。
  4. 检查进程日志里有没有写入异常。预期什么都没有——这是关键判据,短写不抛错。
  5. 若文件大小正常但内容有替换字符,那不是本文的问题,去看 子进程输出按字节截断丢 U+FFFD。
  6. 记录一次「文件应该多大」的基准:把同一命令重定向到本地文件量一次字节数,之后所有比对都以它为准。预期后面每一步都有可对账的数字。

先复现:把 DeepSeek Harness 的内存上限压到 4 字节

讨论里给出的复现方式不需要真的等磁盘写满:把上限调到极小,人为让某次写入只前进 1 字节,就能稳定命中。

  1. 准备一个能配置输出内存上限的实例,把上限设为 4 字节。预期稍大的输出立刻触发 spill。
  2. 拦截收集器自己文件描述符上的写入,让选定的调用实际只写 1 字节并返回 1,其余 I/O 交给 Node 原生实现。预期其余逻辑完全不变。
  3. 依次推入 AAAA、BBBB、CCCC。预期磁盘上应当出现 12 字节的 AAAABBBBCCCC。
  4. 读回文件比对。预期只看到 A + BBBB + CCCC,即 ABBBBCCCC,少掉的 3 个字节正是回放阶段被短写吞掉的部分。
  5. 换一种拦截点,让当前分块的写入短写。预期得到 ABC 之类的更短结果,说明两条写入路径都能独立丢字节。

根因:DeepSeek Harness 两处 writeSync 不读返回值,异常处理只认 throw

spillAll() 里有两处写入:回放已缓冲的分块,以及写入当前分块;两处都丢弃了 writeSync 的返回值,之后也没有任何「已写字节 vs 缓冲区长度」的比较。

社区核对源码的描述是:writeSync(fd, prior) 与 writeSync(spillFd, chunk) 各自独立调用,外层只有 try/catch,而短写既不抛错也不改变控制流(据讨论,社区核对源码)。于是不完整的文件被原样保留,路径继续对外公布。

还有两个细节让影响变大:

  1. spill 路径在两处写入之前就已经赋值公布,所以第一次短写发生时,上层已经拿到了这个路径。
  2. 唯一能撤销它的是 discardSpill(),而它只在写入抛错与整条流超限两处被调用,短写两条都不沾。

路径一:DeepSeek Harness 改成循环写,直到缓冲区耗尽

正确的基本原语是带偏移的循环写:每次只提交剩余部分,按返回值推进偏移,直到整个缓冲区写完为止。

  1. 把两处 writeSync(fd, buf) 改成 writeSync(fd, buf, offset, length) 形式,先算剩余长度。预期调用签名与 Node 的 fs API 对齐。骨架大致如下:
js
let written = 0;
while (written < buf.length) {
  const n = writeSync(fd, buf, written, buf.length - written);
  if (n <= 0) throw new Error('short write made no progress');
  written += n;
}
  1. 每轮把返回值累加到已写字节数,下一次从新的偏移继续。预期短写会被下一轮补齐。
  2. 循环条件写成「已写 < 总长」,不要写成「写了一次就结束」。预期不再依赖单次调用写满。
  3. 对两处写入分别实现,别只改一处——回放路径和当前分块路径都可能短写。预期两处都覆盖后,任一路径被拦截都能补齐。
  4. 在循环外保留原有的 try/catch,让真正的异常仍然走失败分支。预期异常路径行为不变。
  5. 用本文第 1 节的 4 字节复现跑一遍,预期磁盘文件为完整的 12 字节。

路径二:DeepSeek Harness 把零进度视为失败,走 discardSpill

只加循环还不够:如果 writeSync 对非空缓冲区返回 0,循环会原地打转。把 0 当失败并接入既有失败出口,才不会把「卡死」换成「静默截断」。

  1. 在循环里判断返回值,0 直接判定为失败,不再重试。预期不会出现死循环。
  2. 失败后调用 discardSpill(),撤销已公布的路径。预期上层拿到的是「没有 spill 文件」,而不是一份残缺文件。
  3. 同时触发收集器已有的失败回调,让这次输出被标记为不完整。预期日志或状态里能看出失败,而不是静默成功。
  4. 检查失败分支是否与整条流超限的处理复用同一出口。预期「不完整产物不作数」这条契约只有一处实现。
  5. 回归测试要覆盖两种短写:回放阶段短写、当前分块短写,以及返回 0 的情况。预期三种都走 discardSpill。
  6. 验证失败后再次写入同一命令,预期会重新创建 spill 文件,不会复用旧路径。

路径三:调高 DeepSeek Harness 的内存上限,从源头不 spill

如果你的场景输出量可控,最省事的做法是让输出压根不触发 spill:上限调大后短写路径不存在,也就不存在截断。

  1. 统计这类命令的实际最大输出字节数(取多次运行的 p95,不要取最大值)。预期得到一个可解释的量级。
  2. 把子进程输出的内存上限调到该量级之上,留出余量。预期常规命令不再产生 spill 文件。
  3. 用同一批命令重跑,确认界面不再提示 spill。预期内存占用上升但仍在可接受范围。
  4. 设一个监控:一旦重新出现 spill 就说明上限不够或命令行为变了。预期能及时发现回退。
  5. 明确这只是概率缓解——只要还有一次超限,短写仍可能发生,适合当过渡而非终局。这不是某个 DSH插件 的行为,开关插件也绕不开宿主自己的输出收集器。

DeepSeek Harness spill 修复后的验证与取舍

验收标准是可量化的:同一段输出,spill 文件字节数必须等于完整输出字节数,且失败时不能留下被公布的残缺文件。

  1. 用 4 字节上限的复现用例跑三条路径,预期前两条得到完整文件,第三条不产生文件。
  2. 用真实大输出命令跑一遍,wc -c 对账。预期字节数一致。
  3. 人为制造磁盘满或配额耗尽,预期走 discardSpill 并留下可读的失败信号。
  4. 观察内存占用:路径一与路径二几乎不增加,路径三会增加,按机器规格取舍。

这类缺陷的边界

  1. 这不是「普通写入经常短写」的抱怨——正常文件写入通常一次写满,触发条件是磁盘满、配额、信号打断与 Windows 部分完成。
  2. 短写与 UTF-8 边界截断是两个缺陷,别用同一个补丁一起改。
  3. 循环写必须有零进度保护,否则会把静默截断换成 CPU 空转。
  4. discardSpill() 的调用点目前只有两处,新增失败出口时要确认没有绕过它。
  5. 上述位置与行为来自讨论中的社区核对源码(基线为 0.2.1-rc.2),上游未修,升级后需重新确认。

常见问题

DeepSeek Harness 子进程输出写到 spill 文件后缺尾部,为什么没有任何报错?

DeepSeek Harness 的 spill 之所以静默截断,是因为 writeSync 返回的字节数没有被读取,短写不会抛异常。spill 路径在写入之前就已经公布,只有写入抛错或整条流超限才会走 discardSpill,短写两条都不命中。

DeepSeek Harness 的 spill 文件短写和按字节截断导致的乱码是同一个问题吗?

不是同一个问题:DeepSeek Harness 的 spill 短写是写文件时少写了字节,文件尾部缺失;按字节截断是内存缓冲区在解码前被切在 UTF-8 字符中间,会出现替换字符。两者都在输出收集器里,但触发点和修法不同。

DeepSeek Harness 的 spill 短写怎么稳定复现,而不是等磁盘写满?

DeepSeek Harness 的复现办法是把输出的内存上限压到极小(讨论里用 4 字节),再拦截收集器自身描述符的写入,让选定的 writeSync 调用只写 1 字节并返回 1。推入 AAAABBBBCCCC,预期磁盘上应得到完整 12 字节。

把 DeepSeek Harness 的内存上限调高,能不能直接绕过 spill 短写?

DeepSeek Harness 调高内存上限只能降低触发概率,不能修掉它。上限调高只是让 spill 更少发生,一旦输出仍然超过上限,短写路径还在。它适合当临时缓解,不适合当修复。

DeepSeek Harness 的 spill 写入改成循环写之后,为什么还必须判零进度?

DeepSeek Harness 的写循环必须判零进度,因为 writeSync 对非空缓冲区返回 0 时循环不会前进,会变成死循环。把 0 当失败并接入 discardSpill,才能既补齐短写,又不把截断换成空转。

相关术语

spill 文件
spill 文件是子进程输出超过内存上限后落盘的临时文件,用来保留完整输出,内存里只留一段有界尾部。它的路径会被公布给上层,因此内容必须完整。— https://github.com/deepseek-ai/deepseek-harness/discussions/10101
短写(short write)
短写指 write 类调用返回的正数小于请求写入的字节数,剩余字节没有落盘。常见触发条件包括磁盘满、配额耗尽、写入被信号打断,以及 Windows 上的部分完成。— https://github.com/deepseek-ai/deepseek-harness/discussions/10101
discardSpill()
discardSpill 是输出收集器里撤销 spill 路径的方法,表示「这个不完整产物不作数」。现有代码只在写入抛错和整条流超限两处调用它。— https://github.com/deepseek-ai/deepseek-harness/discussions/10101

来源