DeepSeek Harness 插件操作卡两分钟:atomic-write 锁超时怎么清

故障排查发布于 2026-10-20作者: DeepSeek Plugin 插件市场
DeepSeek HarnessDSHatomic-writepackage.json.lockEPERM插件管理锁
DeepSeek Harness 停用或启用插件时卡满两分钟,报 atomic-write timed out waiting for the writer lock;解释 EPERM 与锁接管逻辑,给三种清理与修复方式。

点一下「停用」,界面开始转圈,两分钟后弹出 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 超时、删锁即恢复,三条同时成立就是本文场景;几秒就报错的是别的权限问题。

  1. 掐表执行一次停用或启用插件。预期:卡住接近两分钟,不是几秒。
  2. 记录完整报错。预期:出现 atomic-write: timed out waiting for the writer lock。
  3. 退出 DSH,删除锁文件后重试。预期:操作立刻完成。
  4. 查看 profile 目录里是否有 .lock 残留。预期:存在且 mtime 很旧。
  5. 确认没有第二个实例在使用同一 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,于是活锁被当成真锁。

  1. 写入者创建 package.json.lock,写入 PID。预期:锁文件带上持有者信息。
  2. 写入者崩溃,锁文件留下。预期:没有进程负责清理。
  3. 新写入者读取锁,尝试 kill(pid, 0) 探测。预期:拿到一个错误码。
  4. Windows 返回 EPERM,表示进程存在但无权限,或句柄仍被占用。预期:代码无法区分两种情况。
  5. 代码认为持有者存活,继续等待。预期:每一轮都等满上限。
  6. 等到 waitMs 超时,抛错。预期:插件管理界面解除转圈。

在 macOS 与 Linux 上,进程不存在通常返回 ESRCH,所以很少卡满两分钟。跨平台语义差异,是这个 bug 只在部分机器上出现的原因。

一句话概括:代码问「你还活着吗」,Windows 回答「我无权告诉你」,代码就把这当成「活着」。

修法一:删掉 DeepSeek Harness 的 package.json.lock

确认没有 DSH 正在写 profile 后,删锁是最快的恢复方式;锁不含数据,删除安全。

  1. 完全退出 DSH,包括托盘进程。预期:没有 DSH 进程残留。
  2. 打开 ~/.dsh/profiles/<profile>/。预期:能看到 package.json 与锁文件。
  3. 把 package.json.lock 复制到桌面备查。预期:保留一份现场。
  4. 删除锁文件。预期:package.json 本体没有被碰。
  5. 重新启动 DSH,执行一次停用或启用插件。预期:不再卡住。
  6. 顺手把 profile 目录加入杀毒软件白名单。预期:减少再次占用的概率。
  7. 确认插件列表能正常加载。预期:停用状态已保存到配置。

有三种情况不能删:还有另一个实例在使用同一 profile;锁文件 mtime 在几秒内还在变;你不确定锁属于哪个 profile。

修法二:调小 waitMs,让 DeepSeek Harness 快速失败

把等待时间从 120 秒降到 5-10 秒,能让问题从「卡两分钟」变成「快速报错」,适合先改善体验。

  1. 找到插件管理器里调用 change() 的位置,确认 waitMs 来源。预期:默认值 120000 可见。
  2. 把默认值改为 5000 或 10000,保留可配置项。预期:等待进入秒级。
  3. 在超时错误里带上锁路径、PID 与锁龄。预期:报错能直接指出是哪把锁。
  4. 重新构建,触发一次锁残留。预期:10 秒内报错而不是 120 秒。
  5. 观察误报率。预期:正常写入没有被误判超时。
  6. 用 DSH插件 做一次批量停用。预期:每个操作都在可接受时间内返回。

调小 waitMs 只是让失败更快暴露,不解决锁残留。报错里没有 PID 和锁龄的话,用户还是得自己去翻目录。

另一个副作用是正常写入的余量变小。package.json 变大或磁盘变慢时,10 秒可能不够,所以上限要留成可配置项。

修法三:改 DeepSeek Harness 接管逻辑,按 PID 与锁龄判断

根治方向是把「EPERM 一律视为存活」改成「PID 不存在或锁龄过长则接管」,同时保留并发安全。

  1. 读取锁文件里的 PID 与创建时间。预期:两个字段都能拿到。
  2. 先探测 PID:不存在则直接接管;存在但属于其他用户时,按锁龄判断。预期:死锁被回收,活锁继续等待。
  3. 锁龄超过阈值,例如 10 分钟且没有心跳更新,允许接管并记录 warn。预期:陈旧锁自动清理。
  4. 接管时先把旧锁重命名为 .stale,再创建新锁。预期:同一时刻只有一个写入者。
  5. 补测试:ESRCH、EPERM 加 PID 不存在、EPERM 加锁龄超限、正常持有四种情况。预期:四种都符合设计。
  6. 在 PR 里说明网络盘时钟偏差的影响。预期:维护者知道锁龄判断的边界。

自动接管的风险在于两个进程同时判断锁过期。必须靠原子重命名或文件锁兜底,否则会把互斥变成竞态。

验证与边界:DeepSeek Harness 锁残留后复查

修完后要同时验证「陈旧锁能被回收」和「活锁仍能互斥」,只测一边会引入新问题。

  1. 造一个 PID 不存在的锁文件。预期:被接管。
  2. 造一个 PID 存活的锁文件。预期:写入者等待,而不是抢锁。
  3. 正常停用或启用插件。预期:不出现超时。
  4. 多开一个实例并发写。预期:互斥仍然成立。
  5. 连续操作十次插件。预期:没有锁残留。
  6. 检查 profile 目录。预期:没有遗留的 .stale 文件堆积。

边界有四条:不要删 package.json 本体;不要在多个实例运行时删锁;杀毒软件是常见诱因,建议加白名单;讨论里没有官方修复版本,0.2.1-rc.2 仍可复现。插件列表打不开但 CLI 正常时,排查路径见 插件中心空白与 CLI 正常。

常见问题

DeepSeek Harness 停用插件卡两分钟,报 atomic-write 锁超时,删锁就好?

DeepSeek Harness 的 atomic-write 锁文件残留了,当前进程无法确认原持有者是否退出。代码只把 ESRCH 当作持有者已死,遇到 EPERM 就认为对方还活着,于是等满 120 秒超时。删锁能让下一次写入重新建锁。

DeepSeek Harness 的 package.json.lock 可以随便删吗,会不会损坏 profile?

确认没有 DeepSeek Harness 进程在写 profile 时可以删。锁文件不含数据,只用于互斥,删掉后下一次写入会重建。不要删 package.json 本体,也不要在多个实例同时运行时删锁。

为什么 DeepSeek Harness 的锁在 Windows 上更容易残留,macOS 上很少见?

DeepSeek Harness 在 Windows 上更容易遇到 EPERM 而不是 ESRCH,进程崩溃或杀毒软件短暂占用都会留下这种状态。代码把 EPERM 视为存活,所以不会接管;macOS 的 errno 语义不同,触发概率低。

怎么让 DeepSeek Harness 下次不再卡两分钟,而是快速报错或自动接管?

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

来源