DeepSeek Harness 会话日志停止写入:drainPaused 僵尸会话怎么修
会话看着一切正常:你发消息,模型回,工具照跑,但 session.v4.jsonl.zstd 的文件大小已经几个小时没变。 这是 DeepSeek Harness(DSH)里最难察觉的一类故障——持久化流水线在第一次写盘失败后把自己暂停了,之后的事件全部留在内存。进程不崩、界面不报错,直到重启才发现最近几轮根本没落盘(来源)。
三条证据:确认 DeepSeek Harness 会话持久化已停
界面正常、日志 mtime 不动、重启丢最近几轮,三条同时成立就是 drainPaused;只出现一条时先排除磁盘满、权限和会话切换。
- 用
ls -l或资源管理器看目标会话的.jsonl.zstd修改时间。预期:比最后一条消息早很多。 - 在会话里继续发两条消息,再看一次 mtime。预期:仍然不动,说明没有写入。
- 对比同目录其他活跃会话的日志。预期:它们在增长,说明不是整层存储都停了。
- 重启 DSH,检查最近两轮是否消失。预期:消失即确认事件从未落盘。
- 查看磁盘剩余空间与文件权限。预期:能解释第一次失败,但不解释为什么没有恢复。
| 观测点 | 正常 | 故障 |
|---|---|---|
| 日志 mtime | 每轮更新 | 长时间不动 |
| 界面回复 | 正常 | 正常 |
| 重启后历史 | 完整 | 缺最近几轮 |
据讨论里的源码核对,session-persistence-jsonl/src/storage.ts:276-277 在 drainPaused 为真时直接短路,:311 负责置位;唯一恢复入口是 flush()(index.ts:472、:508-513),运行时没有调度器调用它(#10307)。
根因:DeepSeek Harness 的 drainPaused 为什么不自动恢复
失败处理只有「暂停」没有「退避重试」,而 flush 只在显式调用时执行,于是暂停状态没有人解除。
- 写入时遇到一次 I/O 错误,例如磁盘忙、权限不对、文件被占用。预期:多数是瞬时错误。
drain()捕获后把drainPaused置为 true。预期:流水线停止尝试写入。- 后续每次
drain()看到 true 就直接返回。预期:新事件只排队在内存。 - 计时器没有被重新挂上,也没有指数退避。预期:没有任何自动重试。
flush()能绕过暂停,但正常运行时没人调用。预期:暂停持续到进程退出。
结果就是:一次瞬时错误,换来整段会话的持久化静默停止。危险的地方在于它不报错,你会继续在同一个会话里干活。
更麻烦的是,暂停后没有任何计数器或日志持续提示。只有下次启动读到旧文件时,你才会发现丢了一段。
修法一:重启 DeepSeek Harness,先止血
重启会重新初始化存储状态,drainPaused 被清掉,后续事件恢复写盘;代价是未落盘的那部分丢失。
- 先不要关闭会话窗口,复制最近几轮的关键内容。预期:文本离开应用,落到别处。
- 完全退出 DSH(托盘也要退出),确认进程结束。预期:没有残留进程占着日志。
- 重新启动,打开该会话,发一条消息。预期:会话从最后一条已落盘记录加载。
- 观察日志文件 mtime 是否更新。预期:更新即恢复写盘。
- 检查重启前的最后几轮是否缺失,必要时手工补记。预期:记录与复制内容一致。
- 发一条会触发工具的消息。预期:工具结果也写入日志,不只是文本。
- 查看磁盘占用。预期:有足够空间,避免再次触发。
重启只是把状态清空,触发条件还在的话还会复发。磁盘空间和权限要顺手处理掉。
修法二:在回合结束主动 flush,给 DeepSeek Harness 兜底
不改核心时,可以用 DSH插件 监听 turn/end 后调用 flush(),把「永久暂停」降级为「每轮兜底写一次」。
- 写一个轻量监听器,订阅
turn/end或session/idle。预期:每轮结束都能触发。 - 在回调里拿到会话的持久化服务,调用
flush()。预期:缓冲区被写出。 - 给 flush 加超时与错误日志,避免它拖住主流程。预期:慢写入不会卡住回合。
- 连续跑几轮,确认日志文件每轮都在增长。预期:增长节奏与回合一致。
- 人为制造一次失败,观察 flush 是否报错。预期:报错就回到修法一。
- 把每次 flush 的结果写进日志。预期:能看出哪一轮没有落盘。
- 在空闲会话上跑一小时。预期:I/O 增长可接受,没有明显卡顿。
这是兜底,不是修复。每轮 flush 会增加 I/O,长会话频繁触发时要注意磁盘压力;flush 本身失败时仍然只能重启。
插件只能看到自己的生命周期,宿主内部的写入失败它感知不到。所以别把它当成监控,它只是多争取一次落盘机会。
修法三:改 DeepSeek Harness 的 drain,失败后退避重挂
根治方向是失败后不永久暂停,而是按指数退避重挂计时器,并把 paused 状态暴露给界面。
- 在
storage.ts:311置位drainPaused后,启动退避计时器(1s、2s、4s,上限 30s)。预期:流水线会自动重试。 - 计时器到期时清一次 paused 并重试 drain;连续失败到上限后保持 paused,但记录明确错误。预期:瞬时错误静默自愈。
- 在会话状态里暴露
persistence: paused,让 UI 显示警告。预期:故障不再静默。 - 提供手动「重试写入」入口,等价于调用 flush。预期:用户能自己触发一次写出。
- 补三条测试:一次失败后恢复、连续失败进入 paused、手动 flush 清除 paused。预期:三条都符合设计。
- 在 PR 里附上复现步骤与版本。预期:维护者能对照 0.2.1-rc.2 验证。
退避上限别设得太长。30 秒已经能覆盖大多数磁盘抖动,再长就等于把静默故障又拉回来。
验证与边界:DeepSeek Harness 会话写入复查
人为制造一次失败,确认流水线能自己恢复,而不是靠重启掩盖问题。
- 临时把日志文件设为只读,制造一次写盘失败。预期:drain 暂停或报错。
- 恢复权限,观察是否自动恢复。预期:修法三下自动恢复,否则重启后恢复。
- 看日志在重试窗口内是否重新增长。预期:不需要手动 flush 也能写入。
- 重启一次,检查最近几轮。预期:持久化日志里一条不少。
- 继续对话十分钟后再看 mtime。预期:仍在持续更新。
- 检查会话能否正常重开。预期:没有截断记录,也没有 seq 报错。
修复前后各留一份日志片段。对比 mtime 序列,比看单次结果可靠得多。
边界有四条:不要用删日志文件的方式恢复,会直接损坏会话;磁盘满是常见触发原因;paused 状态下长时间对话风险很高;讨论里没有官方修复版本,0.2.1-rc.2 仍可复现。日志被截断的通用处理见 会话日志 seq gap 修复。
常见问题
这是 DeepSeek Harness 持久化流水线的 drainPaused 短路。一次写盘失败后,storage.ts 置位并停止重挂计时器,后续事件只进内存。重启会丢掉未落盘的部分,需要恢复 drain 或调用 flush 强制写出。
DeepSeek Harness 当前实现只置位、不重挂。失败后唯一恢复入口是 flush(),而运行时没有调度器定期调用它,于是暂停状态保持到进程重启。缺少退避重试,实际效果就是永久停摆。
看 DeepSeek Harness 的三个信号:日志文件 mtime 长时间不变、界面还能继续对话、重启后最近几轮消失。再对比同目录其他会话的日志是否在增长,只有这一个不动就基本确认。
DeepSeek Harness 有可调用的 flush() 时,触发一次就能写出缓冲区。没有管理接口就只能重启;也可以在插件里监听 turn/end 后主动 flush,作为临时兜底。
相关术语
- drainPaused
- drainPaused 是 DeepSeek Harness 会话持久化存储里的状态位,表示写盘流水线已暂停。置位后 drain 循环直接返回,不再尝试写出缓冲区。— https://github.com/deepseek-ai/deepseek-harness/discussions/10307
- flush()
- flush() 是 DeepSeek Harness 会话持久化模块提供的强制写出方法,会把内存缓冲区里的待写事件立即提交。它是 drainPaused 状态下唯一能恢复写盘的入口。— https://github.com/deepseek-ai/deepseek-harness/discussions/10307
- zombie session
- zombie session 指界面仍可交互、进程仍能应答,但持久化已经停止的 DeepSeek Harness 会话。它不会立刻报错,直到重启或崩溃后才发现历史丢失。— https://github.com/deepseek-ai/deepseek-harness/discussions/10307
来源
- #10307 — 一次 drain 失败后持久化流水线永久停摆,会话变僵尸· deepseek-ai(GitHub Discussions)
- DeepSeek Harness 官方仓库· deepseek-ai(GitHub)