DeepSeek Harness stops writing sessions on drainPaused
The session looks healthy: you send a message, the model replies, tools run, yet session.v4.jsonl.zstd has not changed size in hours. This is one of the quietest failures in DeepSeek Harness (DSH): the persistence pipeline paused itself after the first failed disk write, and every later event stays in memory. Nothing crashes and nothing warns you; only a restart shows that the last rounds were never stored (source).
Three proofs that DeepSeek Harness persistence has stopped
A responsive UI, an unchanged log mtime, and lost rounds after a restart together confirm drainPaused; any single one should first be checked against disk space, permissions and session switching.
- Read the modification time of the session's
.jsonl.zstdwithls -lor a file manager. Expected: much older than the last message. - Send two more messages and read the timestamp again. Expected: still unchanged, proving nothing reached disk.
- Compare other active logs in the same directory. Expected: they keep growing, so the storage layer as a whole is alive.
- Restart DSH and reopen the session. Expected: the last two rounds are missing, which confirms the events never landed.
- Check free space and file permissions. Expected: this explains the first failure, not the lack of recovery.
| Observation | Healthy | Broken |
|---|---|---|
| Log mtime | updates every turn | frozen for hours |
| UI replies | work | work |
| History after restart | complete | last rounds missing |
The discussion's source review pins the short circuit to session-persistence-jsonl/src/storage.ts:276-277, with the flag set at :311; the only recovery entry point is flush() (called at index.ts:472 and :508-513), which nothing in the runtime schedules (#10307).
Why DeepSeek Harness drainPaused never clears itself
Failure handling pauses but never retries, and flushing only happens on an explicit call, so the paused state has no owner to clear it.
- A write hits an I/O error: a busy disk, a wrong permission, or a locked file. Expected: the failure is transient in most cases.
drain()records it and setsdrainPausedto true. Expected: the pipeline stops attempting writes.- Every later
drain()returns immediately on the flag. Expected: new events queue only in memory. - No timer is rearmed and there is no exponential backoff. Expected: nothing retries on its own.
flush()can bypass the pause, but normal operation never calls it. Expected: the pause lasts until the process exits.
One momentary error therefore costs the persistence of the entire remaining session. The dangerous part is the silence: you keep working in a session that is no longer being recorded.
Nothing counts the paused interval either, so the only evidence is a stale file. A startup check that compares the last write with the last event would surface it immediately.
Repair 1: restart DeepSeek Harness to stop the bleeding
Restarting rebuilds the storage state, clears drainPaused and resumes writes; the cost is everything that never reached disk.
- Leave the session open and copy the last few rounds somewhere else. Expected: the text survives outside the app.
- Quit DSH fully, including the tray process. Expected: no process holds the log.
- Relaunch and reopen the session, then send one message. Expected: it loads from the last durable entry.
- Watch the log timestamp after the turn. Expected: it advances, so persistence resumed.
- Note which rounds are missing and restore them manually. Expected: the record matches what you salvaged.
- Trigger one tool call. Expected: tool output is written too, not only text.
- Check disk usage. Expected: enough headroom to avoid a repeat.
A restart only clears the state. If the trigger remains, the pause returns, so fix disk space and permissions while you are here.
Do not repeat restarts hoping the log catches up. Once the process is gone, the in-memory events are gone with it.
Repair 2: flush at turn end as a DeepSeek Harness safety net
When the core cannot be changed, a DSH plugin can call flush() after turn/end, downgrading "paused forever" to "written once per turn".
- Build a small listener subscribed to
turn/endorsession/idle. Expected: it fires once per exchange. - Resolve the session's persistence service in the callback and call
flush(). Expected: the buffer is submitted. - Add a timeout and error logging so the flush cannot stall the main loop. Expected: a slow write does not freeze the turn.
- Run several turns and watch the log. Expected: the file grows every turn.
- Force one failure and see whether flush reports it. Expected: if it fails, go back to repair 1.
- Log the result of every flush. Expected: you can see which turn failed to land.
- Leave an idle session running for an hour. Expected: extra I/O is acceptable and nothing stalls.
This is a net, not a repair. It adds I/O per turn, and a plugin only sees its own lifecycle, so it cannot detect a host-side write failure.
Repair 3: back off inside DeepSeek Harness instead of pausing
The durable fix is to treat a failure as retryable: rearm the timer with backoff and expose the paused state to the interface.
- After setting the flag at
storage.ts:311, start a backoff timer at 1s, 2s, 4s, capped at 30s. Expected: the pipeline retries without user action. - On expiry, clear the pause once and retry the drain; keep the pause after the cap but log a clear error. Expected: transient errors heal quietly.
- Expose
persistence: pausedon the session so the UI can warn. Expected: the failure is no longer silent. - Add a manual retry control equivalent to calling
flush(). Expected: the user can force a write. - Test three paths: one failure that recovers, repeated failures that stay paused, and a manual flush that clears it. Expected: each behaves as designed.
- Attach reproduction steps and the version to the pull request. Expected: maintainers can compare against 0.2.1-rc.2.
- Watch the warning for a full day. Expected: no repeated pause without a logged reason.
Keep the backoff cap short. Thirty seconds covers most disk jitter; a longer cap simply restores the silent failure.
Verify and set bounds for DeepSeek Harness
Introduce a failure on purpose and confirm the pipeline recovers by itself instead of hiding the problem behind a restart.
- Make the log read-only for a moment to force a failed write. Expected: the drain pauses or reports the error.
- Restore permissions and watch the pipeline. Expected: it resumes on its own with repair 3, otherwise after a restart.
- Check whether the file grows inside the retry window. Expected: writes resume without a manual flush.
- Restart once and inspect the last rounds. Expected: nothing is missing from the durable log.
- Keep chatting for ten minutes and recheck the mtime. Expected: it keeps advancing.
- Reopen the session. Expected: no truncated record and no sequence error.
Keep a log sample from before and after the fix. Comparing mtime sequences is far more reliable than a single observation. Four limits apply: never delete the log to recover, a full disk is the most common trigger, a long conversation while paused risks every unwritten event, and no official fix version is named in the discussion. For truncated logs, see session log seq gap repair.
FAQ
DeepSeek Harness persistence is short-circuited by drainPaused. After one failed disk write, storage.ts sets the flag and stops rescheduling its timer, so later events stay in memory. A restart discards whatever was never written.
DeepSeek Harness only sets the flag and never rearms. The sole recovery entry point is flush(), and no runtime scheduler calls it, so the pause lasts until the process exits. Without backoff, a transient error becomes a permanent stop.
Check three DeepSeek Harness signals: the log mtime has not moved for hours, the interface still accepts turns, and the last rounds vanish after a restart. If only that one file is frozen, the diagnosis holds.
DeepSeek Harness exposes flush() as the forced-write entry, so calling it once pushes the buffer out. Without a management interface, a restart is the only option, or a plugin can call flush after turn/end.
Related Terms
- drainPaused
- drainPaused is the state flag in the DeepSeek Harness session persistence store that marks the write pipeline as paused. Once set, the drain loop returns immediately and no longer writes the buffer.— https://github.com/deepseek-ai/deepseek-harness/discussions/10307
- flush()
- flush() is the forced-write method exposed by the DeepSeek Harness persistence module, submitting buffered events immediately. It is the only entry point that resumes writing while drainPaused is set.— https://github.com/deepseek-ai/deepseek-harness/discussions/10307
- zombie session
- A zombie session is a DeepSeek Harness session whose interface still responds while persistence has stopped. It raises no error until a restart or crash reveals that recent history was never stored.— https://github.com/deepseek-ai/deepseek-harness/discussions/10307
Sources
- #10307 — One failed drain stops the persistence pipeline permanently and turns the session into a zombie· deepseek-ai (GitHub Discussions)
- DeepSeek Harness source repository· deepseek-ai (GitHub)