DeepSeek Harness Desktop host dies with read ECONNRESET
This is not a network fault; it is a DeepSeek Harness pipe error with nobody listening. The host child exits under the fail-loud policy because of an uncaught read ECONNRESET, the Electron shell does not restart it, and the UI never explains the failure — so the symptom is a live window with every port down (source).
Three signals, and why the DeepSeek Harness Desktop host exits
Bottom line: three facts at once are enough — a fatal uncaught exception in the host log, a refused connection on the local port, and a still-running Electron main process.
- Open the Desktop log directory (on Windows, the dsh-desktop logs under
%APPDATA%) and take the newestcrash-*.log. Expected:dsh: fatal uncaught exception: Error: read ECONNRESET, withWriteWrap.onWriteCompleteandChildProcess._handle.onexitin the stack. - Request the local port from a terminal. Expected: connection refused, not a timeout.
- Check Task Manager or Activity Monitor: the Electron main process is present, the dsh host child is gone. Expected: the "shell alive, backend dead" shape.
- Look for a native recovery dialog in the UI. Expected: none — only startup failures take the
fatalframe path, not runtime uncaught exceptions. - Confirm the timing: it usually happens during active use with sessions or plugins running, not while idle. Expected: that explains why it looks intermittent but repeats.
If the window vanishes with no log at all, start with silent host exit diagnosis.
Why you cannot patch the fatal handler
Bottom line: installFailLoud is deliberate — an uncaught exception means the state at the throw site is untrustworthy, so continuing would replace a crash with wrong output. The fix belongs where the exception is raised, not where it is reported.
The same tree already contains the right pattern: one experimental runtime attaches an empty error listener to child stdio and documents that ECONNRESET/EPIPE occur when a child dies early, that they should be swallowed, and that close is the authoritative failure signal. The repository therefore already accepts that these errors belong at the point of origin — the subprocess stdout/stderr collector just has not caught up (per the discussion, community source review).
So the direction is: leave the exit policy alone, change how long the listener lives.
Root cause: settle removes the listeners, and start() does not spawn again
Bottom line: two defects stack — observeOutputStream removes all three listeners after the first end/close/error, and Desktop's start() returns the existing promise instead of launching a new child.
- First:
observeOutputStreamregistersonce('end'),once('close')andonce('error'), andsettleremoves all three when the first arrives. A normal exit fires end/close first, so from that moment the pipe has no error listener. - Second: the error completes only after the child exits — the
ChildProcess._handle.onexitframe is the evidence. With no listener, the event reaches process level. - Third:
start()inapps/desktopcreates a child only when none exists; with a child present it returns the original promise. Nothing creates a replacement after the host dies. - Result: one pipe error → process exit → every service down → manual app restart.
Fix 1: install the stdio-error-guard DSH plugin
Bottom line: the smallest change — the plugin wraps the subprocess service and keeps a permanent error listener on every child, consuming the error in place without touching the exit policy. It is one guard in the DSH plugin ecosystem and cannot take over the host's exit policy.
- Install it:
dsh plugin add @argszero/dsh-stdio-error-guard. Expected: the plugin appears in the installed list. - Confirm it injects the
subprocessservice (the plugin config'sinjectincludes it). Expected: no pending row after the host loads. - Restart Desktop and use it normally for a while. Expected: no further
fatal uncaught exception: Error: read ECONNRESET. - Force a child to die early: run a command that exits immediately, then trigger one more output read. Expected: the pipe error is swallowed and the host stays up.
- Remember the boundary: it consumes errors, it does not make failures louder and does not restart a host that already died. Expected: you will not rely on it to bring the service back after a crash.
- If the host reports a missing injection after installing, check that the subprocess service still has the same name in this version before deciding between reinstall and waiting for a plugin update. Expected: you can tell a stale plugin from a wrong package.
Fix 2: keep your own persistent error listener in DeepSeek Harness
Bottom line: without a third-party package, do the same thing in your subprocess wrapper — attach on('error') to each child's stdout and stderr and make sure nothing removes it for the lifetime of the process.
- As soon as you have the child from the subprocess service, attach a listener to
stdout,stderrand, where relevant,stdin. Expected: listener count is 1 right after creation. - Keep the listener body to logging only; do not throw and do not do heavy work inside it. Expected: the error no longer escalates to an uncaught exception.
- Do not use a one-shot listener, which is the same mistake again;
oncedisappears after the first event. Useonand verify no other code removes it on child close. - Keep
child.on('close')as the authoritative failure signal, with exit code and captured output judged as before. Expected: swallowing pipe errors does not hide real failures. - Print
stdout.listenerCount('error')after the child closes as a self-check. Expected: still 1, not 0. - Validate with a command that exits immediately: close the child, then trigger a write. Expected: the host keeps running.
- Leave one trace line for each swallowed error (pid, code, timestamp). Expected: nothing escalates, and nothing is buried beyond reach either.
Fix 3: put a supervisor in front of the DeepSeek Harness host
Bottom line: the first two paths reduce the chance of a crash but cannot guarantee zero; "nobody is watching when it dies" needs a keeper outside the host, with backoff and visible status. For a manual restart button in the UI rather than a daemon, dsh-process-panel covers that narrow case but not the root cause.
- Write a supervisor that starts the host child and watches its
exit. Expected: it receives the exit code and the last stderr output. - Configure restart backoff (exponential, for example) and a maximum restart count. Expected: repeated crashes do not become a restart storm.
- Release the old listening port before restarting, so the new host is not blocked. Expected: the port is usable immediately after a restart.
- Write each restart to a durable log (time, exit code, last 64 KiB of stderr). Expected: afterwards you can tell a crash from a clean exit.
- Expose host state in the UI or an external probe. Expected: users see "backend restarting" instead of guessing at a fake-live window.
- Stress-test by killing the host. Expected: the supervisor brings it back within the backoff window, the port recovers, and the mobile relay follows.
Verify the DeepSeek Harness guard and choose a path
Bottom line: do not settle for "it did not crash this time" — measure whether a listener still exists on the pipe, which is the only evidence that separates luck from a fix.
- Print
stdout.listenerCount('error')after the child closes. Expected: 1. - Manually emit:
stdout.emit('error', new Error('ECONNRESET')). Expected: the process does not exit. - Compare against a build without the guard: the same action exits the host with a non-zero code. Expected: a clear difference.
- Run one to two hours of normal use and check the logs again. Expected: no new fatal records.
Decide between "do not crash" and "comes back"
Bottom line: the three paths do not solve the same problem — the first two lower crash probability, the third guarantees service recovery, and they cost differently.
| Path | Goal | Fits | Cost |
|---|---|---|---|
| Plugin guard | Do not crash | Personal machines, no host changes | Blocks escalation only, root cause remains |
| Persistent listener | Do not crash (self-maintained) | You own the subprocess wrapper | Must ensure nothing removes it |
| Supervisor | Recover after a crash | Unattended nodes, long-running | Interrupts sessions, needs backoff |
What this approach does not promise
Bottom line: none of the three paths replaces the upstream fix, and each has a failure mode you have to accept explicitly.
- Do not add try/catch inside the fatal handler to keep running; that fights the design.
- The plugin only blocks escalation and does not fix the root cause; the correct upstream fix is a permanent listener at the subprocess layer.
- A supervisor restart interrupts sessions and must use backoff, or the failure feels worse than the crash.
- Only startup failures send a
fatalframe to the shell; runtime crashes produce no dialog, so "no dialog" is not "no crash". - The locations and mechanics above come from community source review and measurement in the discussion; upstream has not fixed it, so re-confirm after an upgrade.
- The error can also appear on the write direction (
write ECONNRESET); handle it the same way rather than writing a second listener.
DeepSeek Harness FAQ
Bottom line: these are the points where people usually misread the symptom as a network or UI problem.
Q: Could a proxy or VPN be causing the reset?
A: Not here. The reset is on the stdio pipe to a child that already exited, which is local by definition. A network fault would not leave an onexit frame in the stack.
Q: Restarting the app always fixes it. Is that good enough? A: It restores service but loses the sessions that were running and repeats on the next crash. It is the manual version of Fix 3 without the backoff or the logs.
Q: Does the guard hide genuine pipe failures?
A: It swallows the error event, not the close event. Exit code and captured output still decide success or failure.
Q: Should I apply all three? A: They are compatible. The guard or your own listener stops the common case, and the supervisor covers whatever still gets through.
FAQ
The DeepSeek Harness host child died; the Electron main process did not. Once the stdio pipe's error listener has been removed, the error falls into Node's uncaught path and the fail-loud handler exits by design. The shell has no respawn, so you are left with a window that cannot reach its backend.
DeepSeek Harness treats that handler as a deliberate policy: an uncaught exception means state at the throw site cannot be trusted, and continuing turns a visible crash into silently wrong output. The correct level is one layer down — keep the pipe error listened to so it never escalates to process level.
It stops the DeepSeek Harness ECONNRESET from escalating into a fatal exception, so the host stays alive and the ports stay up. It cannot rebuild a host that already died, and it does not persist failures beyond stderr; that part needs an outer supervisor.
A DeepSeek Harness host restart interrupts running sessions and tool calls, and a deterministic crash turns into a restart loop. Use it on unattended nodes with backoff and a restart cap; it does not replace fixing the source of the uncaught exception.
DeepSeek Harness gives you the check: print stdout.listenerCount('error') after the child closes. It should be 1, not 0. A zero means something used once or was removed by settle, and you need an on listener that lives for the process lifetime.
Related Terms
- fail-loud
- DeepSeek Harness's existing policy for uncaught exceptions: record diagnostics, release resources, exit with a non-zero code, and never try to recover the failed operation. It is installed by installFailLoud and is intentional, not a defect.— https://github.com/deepseek-ai/deepseek-harness/discussions/9904
- ECONNRESET
- The peer reset the connection. In a subprocess context it commonly means the child already exited when a pipe read or write completed. It is an ordinary I/O error, but with no listener it becomes an uncaught exception.— https://nodejs.org/api/errors.html#common-system-errors
- observeOutputStream
- The subprocess-layer function that attaches one-shot listeners to stdout and stderr for end, close and error. Whichever fires first makes settle remove all three listeners.— https://github.com/deepseek-ai/deepseek-harness/discussions/9904
Sources
- Desktop host exits on an uncaught ECONNRESET and the web service does not recover· deepseek-ai/deepseek-harness Discussions
- @argszero/dsh-stdio-error-guard· argszero