DeepSeek Harness Desktop host dies with read ECONNRESET

TroubleshootingPublished 2026-10-20Author: DeepSeek Plugin Market
DeepSeek HarnessDesktopECONNRESETstdioprocess exit
DeepSeek Harness Desktop loses its host to an uncaught read ECONNRESET while Electron stays up, so every port dies. Guard the pipe or supervise the host.

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.

  1. Open the Desktop log directory (on Windows, the dsh-desktop logs under %APPDATA%) and take the newest crash-*.log. Expected: dsh: fatal uncaught exception: Error: read ECONNRESET, with WriteWrap.onWriteComplete and ChildProcess._handle.onexit in the stack.
  2. Request the local port from a terminal. Expected: connection refused, not a timeout.
  3. 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.
  4. Look for a native recovery dialog in the UI. Expected: none — only startup failures take the fatal frame path, not runtime uncaught exceptions.
  5. 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.

  1. First: observeOutputStream registers once('end'), once('close') and once('error'), and settle removes all three when the first arrives. A normal exit fires end/close first, so from that moment the pipe has no error listener.
  2. Second: the error completes only after the child exits — the ChildProcess._handle.onexit frame is the evidence. With no listener, the event reaches process level.
  3. Third: start() in apps/desktop creates a child only when none exists; with a child present it returns the original promise. Nothing creates a replacement after the host dies.
  4. 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.

  1. Install it: dsh plugin add @argszero/dsh-stdio-error-guard. Expected: the plugin appears in the installed list.
  2. Confirm it injects the subprocess service (the plugin config's inject includes it). Expected: no pending row after the host loads.
  3. Restart Desktop and use it normally for a while. Expected: no further fatal uncaught exception: Error: read ECONNRESET.
  4. 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.
  5. 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.
  6. 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.

  1. As soon as you have the child from the subprocess service, attach a listener to stdout, stderr and, where relevant, stdin. Expected: listener count is 1 right after creation.
  2. 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.
  3. Do not use a one-shot listener, which is the same mistake again; once disappears after the first event. Use on and verify no other code removes it on child close.
  4. 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.
  5. Print stdout.listenerCount('error') after the child closes as a self-check. Expected: still 1, not 0.
  6. Validate with a command that exits immediately: close the child, then trigger a write. Expected: the host keeps running.
  7. 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.

  1. Write a supervisor that starts the host child and watches its exit. Expected: it receives the exit code and the last stderr output.
  2. Configure restart backoff (exponential, for example) and a maximum restart count. Expected: repeated crashes do not become a restart storm.
  3. Release the old listening port before restarting, so the new host is not blocked. Expected: the port is usable immediately after a restart.
  4. 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.
  5. Expose host state in the UI or an external probe. Expected: users see "backend restarting" instead of guessing at a fake-live window.
  6. 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.

  1. Print stdout.listenerCount('error') after the child closes. Expected: 1.
  2. Manually emit: stdout.emit('error', new Error('ECONNRESET')). Expected: the process does not exit.
  3. Compare against a build without the guard: the same action exits the host with a non-zero code. Expected: a clear difference.
  4. 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.

PathGoalFitsCost
Plugin guardDo not crashPersonal machines, no host changesBlocks escalation only, root cause remains
Persistent listenerDo not crash (self-maintained)You own the subprocess wrapperMust ensure nothing removes it
SupervisorRecover after a crashUnattended nodes, long-runningInterrupts 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.

  1. Do not add try/catch inside the fatal handler to keep running; that fights the design.
  2. The plugin only blocks escalation and does not fix the root cause; the correct upstream fix is a permanent listener at the subprocess layer.
  3. A supervisor restart interrupts sessions and must use backoff, or the failure feels worse than the crash.
  4. Only startup failures send a fatal frame to the shell; runtime crashes produce no dialog, so "no dialog" is not "no crash".
  5. 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.
  6. 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

After an hour the host exits with read ECONNRESET. Why is the window still there?

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.

Why not catch ECONNRESET inside the fatal uncaught exception handler and keep running?

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.

Does the dsh-stdio-error-guard plugin fix the dead ports?

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.

What is the cost of having a supervisor restart the host?

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.

How do I know my own persistent error listener has not been removed?

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