DeepSeek Harness 插件操作卡两分钟:atomic-write 锁超时怎么清
点一下「停用」,界面开始转圈,两分钟后弹出 atomic-write: timed out waiting for the writer lock;去 profile 目录删掉 package.json.lock,重试立刻成功。 这是 DeepSeek Harness(DSH)在 Windows 上很常见的一类卡顿:锁文件残留,而锁接管逻辑只认 ESRCH,遇到 EPERM 就认定对方还活着,于是一直等到超时(来源)。
症状核对:DeepSeek Harness 的 writer lock 超时
操作卡满约 120 秒、报 writer lock 超时、删锁即恢复,三条同时成立就是本文场景;几秒就报错的是别的权限问题。
- 掐表执行一次停用或启用插件。预期:卡住接近两分钟,不是几秒。
- 记录完整报错。预期:出现
atomic-write: timed out waiting for the writer lock。 - 退出 DSH,删除锁文件后重试。预期:操作立刻完成。
- 查看 profile 目录里是否有
.lock残留。预期:存在且 mtime 很旧。 - 确认没有第二个实例在使用同一 profile。预期:只有一个 DSH 进程。
| 触发条件 | 表现 |
|---|---|
| 上次 DSH 崩溃或被强杀 | 锁文件没清 |
| 杀毒软件短暂占用 | 写入返回 EPERM |
| 多实例操作同一 profile | 互相等待 |
| 网络盘或同步盘 | 句柄释放延迟 |
据讨论里的源码核对,util/atomic-write/src/index.ts:112-125 只把 ESRCH 判为持有者已退出,:124 的注释明确把 EPERM 视为存活;:150-152 因此拒绝接管,:259-262 等满 waitMs 抛错,而 waitMs: 120000 来自插件管理器的 change()(#10097)。
根因:DeepSeek Harness 的锁为什么不能自动接管
判断「持有者是否还活着」时只用了 ESRCH,而 Windows 更常返回 EPERM,于是活锁被当成真锁。
- 写入者创建
package.json.lock,写入 PID。预期:锁文件带上持有者信息。 - 写入者崩溃,锁文件留下。预期:没有进程负责清理。
- 新写入者读取锁,尝试
kill(pid, 0)探测。预期:拿到一个错误码。 - Windows 返回
EPERM,表示进程存在但无权限,或句柄仍被占用。预期:代码无法区分两种情况。 - 代码认为持有者存活,继续等待。预期:每一轮都等满上限。
- 等到
waitMs超时,抛错。预期:插件管理界面解除转圈。
在 macOS 与 Linux 上,进程不存在通常返回 ESRCH,所以很少卡满两分钟。跨平台语义差异,是这个 bug 只在部分机器上出现的原因。
一句话概括:代码问「你还活着吗」,Windows 回答「我无权告诉你」,代码就把这当成「活着」。
修法一:删掉 DeepSeek Harness 的 package.json.lock
确认没有 DSH 正在写 profile 后,删锁是最快的恢复方式;锁不含数据,删除安全。
- 完全退出 DSH,包括托盘进程。预期:没有 DSH 进程残留。
- 打开
~/.dsh/profiles/<profile>/。预期:能看到package.json与锁文件。 - 把
package.json.lock复制到桌面备查。预期:保留一份现场。 - 删除锁文件。预期:
package.json本体没有被碰。 - 重新启动 DSH,执行一次停用或启用插件。预期:不再卡住。
- 顺手把 profile 目录加入杀毒软件白名单。预期:减少再次占用的概率。
- 确认插件列表能正常加载。预期:停用状态已保存到配置。
有三种情况不能删:还有另一个实例在使用同一 profile;锁文件 mtime 在几秒内还在变;你不确定锁属于哪个 profile。
修法二:调小 waitMs,让 DeepSeek Harness 快速失败
把等待时间从 120 秒降到 5-10 秒,能让问题从「卡两分钟」变成「快速报错」,适合先改善体验。
- 找到插件管理器里调用
change()的位置,确认waitMs来源。预期:默认值 120000 可见。 - 把默认值改为 5000 或 10000,保留可配置项。预期:等待进入秒级。
- 在超时错误里带上锁路径、PID 与锁龄。预期:报错能直接指出是哪把锁。
- 重新构建,触发一次锁残留。预期:10 秒内报错而不是 120 秒。
- 观察误报率。预期:正常写入没有被误判超时。
- 用 DSH插件 做一次批量停用。预期:每个操作都在可接受时间内返回。
调小 waitMs 只是让失败更快暴露,不解决锁残留。报错里没有 PID 和锁龄的话,用户还是得自己去翻目录。
另一个副作用是正常写入的余量变小。package.json 变大或磁盘变慢时,10 秒可能不够,所以上限要留成可配置项。
修法三:改 DeepSeek Harness 接管逻辑,按 PID 与锁龄判断
根治方向是把「EPERM 一律视为存活」改成「PID 不存在或锁龄过长则接管」,同时保留并发安全。
- 读取锁文件里的 PID 与创建时间。预期:两个字段都能拿到。
- 先探测 PID:不存在则直接接管;存在但属于其他用户时,按锁龄判断。预期:死锁被回收,活锁继续等待。
- 锁龄超过阈值,例如 10 分钟且没有心跳更新,允许接管并记录 warn。预期:陈旧锁自动清理。
- 接管时先把旧锁重命名为
.stale,再创建新锁。预期:同一时刻只有一个写入者。 - 补测试:ESRCH、EPERM 加 PID 不存在、EPERM 加锁龄超限、正常持有四种情况。预期:四种都符合设计。
- 在 PR 里说明网络盘时钟偏差的影响。预期:维护者知道锁龄判断的边界。
自动接管的风险在于两个进程同时判断锁过期。必须靠原子重命名或文件锁兜底,否则会把互斥变成竞态。
验证与边界:DeepSeek Harness 锁残留后复查
修完后要同时验证「陈旧锁能被回收」和「活锁仍能互斥」,只测一边会引入新问题。
- 造一个 PID 不存在的锁文件。预期:被接管。
- 造一个 PID 存活的锁文件。预期:写入者等待,而不是抢锁。
- 正常停用或启用插件。预期:不出现超时。
- 多开一个实例并发写。预期:互斥仍然成立。
- 连续操作十次插件。预期:没有锁残留。
- 检查 profile 目录。预期:没有遗留的
.stale文件堆积。
边界有四条:不要删 package.json 本体;不要在多个实例运行时删锁;杀毒软件是常见诱因,建议加白名单;讨论里没有官方修复版本,0.2.1-rc.2 仍可复现。插件列表打不开但 CLI 正常时,排查路径见 插件中心空白与 CLI 正常。
常见问题
DeepSeek Harness 的 atomic-write 锁文件残留了,当前进程无法确认原持有者是否退出。代码只把 ESRCH 当作持有者已死,遇到 EPERM 就认为对方还活着,于是等满 120 秒超时。删锁能让下一次写入重新建锁。
确认没有 DeepSeek Harness 进程在写 profile 时可以删。锁文件不含数据,只用于互斥,删掉后下一次写入会重建。不要删 package.json 本体,也不要在多个实例同时运行时删锁。
DeepSeek Harness 在 Windows 上更容易遇到 EPERM 而不是 ESRCH,进程崩溃或杀毒软件短暂占用都会留下这种状态。代码把 EPERM 视为存活,所以不会接管;macOS 的 errno 语义不同,触发概率低。
DeepSeek Harness 可以两条路走:把 waitMs 调小并在报错里带上 PID 与锁龄,让用户快速判断;或者改接管逻辑,对 PID 不存在或锁龄过长的锁自动清理。前者改动小,后者要谨慎处理并发。
相关术语
- atomic-write
- atomic-write 是 DeepSeek Harness 用于安全写配置文件的工具,通过写临时文件再原子替换避免半写状态。它用锁文件保证同一时刻只有一个写入者。— https://github.com/deepseek-ai/deepseek-harness/discussions/10097
- writer lock
- writer lock 是 atomic-write 的互斥锁,通常落为 package.json.lock。持有者写入期间其他写入者需要等待,超时后抛错。— https://github.com/deepseek-ai/deepseek-harness/discussions/10097
- ESRCH 与 EPERM
- ESRCH 表示目标进程不存在,EPERM 表示操作不被允许但进程可能存在。锁接管逻辑把 ESRCH 视为持有者已退出,把 EPERM 视为仍存活,因此后者会导致永久等待。— https://github.com/deepseek-ai/deepseek-harness/discussions/10097
来源
- #10097 — Windows 上 atomic-write 锁遇到 EPERM 无法自愈,插件管理卡 2 分钟· deepseek-ai(GitHub Discussions)
- DeepSeek Harness 官方仓库· deepseek-ai(GitHub)