You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Uncaught ENOENT in OutputCollector.spillAll kills the dsh web server; spill roots also grow unbounded (13 GB measured, volume driven to 197 MB free)
#8330
Channel:deepseek-ai/deepseek-harness has GitHub Issues disabled (the API returns 410 Issues has been disabled in this repository). The project README routes feedback and bug
reports to GitHub Discussions, so this one was posted there, under Q&A. Packages:@deepseek-ai/dsh 0.1.7-rc.2 observed; crash stack from @deepseek-ai/dsh-subprocess-local 0.1.2-rc.1 Environment: Windows 11, Node v22.17.0 Severity: Critical — one deleted temp dir kills the entire dsh web server and every live session
Title
Uncaught ENOENT in OutputCollector.spillAll terminates the whole dsh web server when its spill dir is deleted mid-session
Observed crash (verbatim)
Error: ENOENT: no such file or directory, open 'C:\Users\Prime\AppData\Local\Temp\dsh-subprocess-Kp72eM\dsh-subprocess-18596-1-8ee8c7f6420e-stdout.log'
at openSync (node:fs:561:18)
at OutputCollector.spillAll (file:///C:/Users/Prime/AppData/Local/npm-cache/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai/dsh-subprocess-local/lib/index.js:717:19)
at OutputCollector.push (file:///C:/Users/Prime/AppData/Local/npm-cache/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai/dsh-subprocess-local/lib/index.js:693:75)
at Socket.<anonymous> (file:///C:/Users/Prime/AppData/Local/npm-cache/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai/dsh-subprocess-local/lib/index.js:862:14)
at Socket.emit (node:events:518:28)
...
Node.js v22.17.0
The process hosting the web GUI (all sessions) died. The filename's spill counter (-1-) shows this was the server's first spill, ~83 minutes into the run — any subprocess whose output first exceeds the in-memory cap after the dir disappears triggers it.
Root cause
lib/index.js:648-651 — the spill dir is created once per process via mkdtempSync(join(tmpdir(), "dsh-subprocess-")) and cached in defaultSpillDir. The module imports no mkdir, so it can never be recreated.
lib/index.js:710-721 — spillAll() calls openSync(this.spillFile, "wx", 384) and writeSync(...) with no error handling, synchronously inside a stream data handler (push() at :693, wired at :862). Any failure escapes as an uncaught exception and takes down the whole server, not just the affected subprocess.
Nothing in any @deepseek-ai/* package deletes these dirs — a source-wide grep finds only the mkdtemp line, the per-file name template, and dependency pins. So an external %TEMP% cleaner (Storage Sense, AV, manual sweep) removing the live dir mid-session guarantees this crash on the next spill.
Adjacent observation: stale dsh-subprocess-* dirs are never pruned; six accumulated on this machine over 10 days of normal use.
Measured volume impact (Windows 11, 2026-09-29) — separate from the crash above
The same never-pruned behaviour was measured on a second, larger spill root on the same machine
(%LOCALAPPDATA%\dsh-temp, alongside the %TEMP%\dsh-subprocess-* dirs described above):
Downstream effects while the volume sat at 197 MB — all on the host, none of them inside the
spill code:
another long-running agent process on the machine failed every write with OSError: [Errno 28] No space left on device; its cron persistence, its rotating log handler
and its gateway loop all broke and retried in a loop;
the CodeGraph MCP daemon for a neighbouring project began failing keepalives and reconnecting,
and a full index rebuild could not take its database lock at all (EPERM ... database file is in use) — which is what a full disk looks like from SQLite's side;
operations that spill large output became the least reliable thing on the box.
One observation recorded but not re-verified, because our local remedy reclaimed the files
before it could be checked: a depth-limited listing showed the same 599.9 MB file name three
times in that tree, which may mean duplicate spills of one object rather than three distinct ones.
What we did locally (not a substitute for upstream pruning)
deploy/dsh-temp-reaper.ps1: reports by default, deletes only files older than a threshold
(default 24 h) inside a directory whose leaf is exactly dsh-temp, skips locked files without
forcing them, prints denominators, appends one line per run; installed as a daily 03:00 task
ahead of the 03:30 reindex so space is reclaimed before anything needs to write.
Ask
Prune stale spills on startup and/or on a timer, bounded by age and total size, like any
other cache.
Document the spill roots (%TEMP%\dsh-subprocess-* and %LOCALAPPDATA%\dsh-temp) so an
operator can bound them without reverse-engineering.
Fix the uncaught-ENOENT path described above — that is what turns any external cleanup,
ours included, into a server-wide crash.
Repro
npx @deepseek-ai/dsh web, open a session (the spill dir is created lazily once a spill-capped subprocess spawns).
Delete %TEMP%\dsh-subprocess-* while the server runs.
Produce any subprocess output exceeding the in-memory collection cap (e.g. a long-running session agent or an MCP child with chatty stdout). The server exits with the stack above.
Suggested fix (applied and verified locally)
Wrap the spill path in try/catch; on failure degrade to the existing tail-only path via discardSpill() (which already sets spillDisabled = true), recreate the dir on ENOENT so later collectors can spill again, and warn once per process:
--- a/lib/index.js (spillAll)+import { ..., mkdirSync, ... } from "node:fs";
spillAll(chunk) {
if (this.maxSpillBytes !== void 0 && this.total > this.maxSpillBytes) {
this.discardSpill();
return;
}
- if (this.spillFd === void 0) {- this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);- this.spillFd = openSync(this.spillFile, "wx", 384);- for (const prior of this.chunks) writeSync(this.spillFd, prior);- }- writeSync(this.spillFd, chunk);+ try {+ if (this.spillFd === void 0) {+ this.spillFile = join(this.spillDir, `dsh-subprocess-${process.pid}-${++spillCounter}-${randomBytes(6).toString("hex")}-${this.label}.log`);+ this.spillFd = openSync(this.spillFile, "wx", 384);+ for (const prior of this.chunks) writeSync(this.spillFd, prior);+ }+ writeSync(this.spillFd, chunk);+ } catch (error) {+ this.discardSpill();+ if (error?.code === "ENOENT") {+ try { mkdirSync(this.spillDir, { recursive: true }); } catch {}+ }+ if (OutputCollector._spillWarned !== true) {+ OutputCollector._spillWarned = true;+ console.warn(`dsh-subprocess-local: output spill to ${this.spillDir} failed (${error?.code ?? error?.message ?? String(error)}); degraded to in-memory tail${error?.code === "ENOENT" ? " and recreated the spill dir" : ""}`);+ }+ }
}
Optional hardening: prune stale dsh-subprocess-* dirs at startup (they leak forever today), and consider a process-level uncaughtException guard for the web server so one subsystem can't take down every session.
Spilling is best-effort: when the spill file cannot be opened or appended (the per-process directory removed by a temporary-file cleaner, EACCES, EMFILE, ENOSPC), the collector discards the spill, logs one error through the plugin logger, and keeps collecting the in-memory tail, so the result is truncated with no spill path.
⇒ 所以"spillAll 里的未捕获 ENOENT 打死 web 服务"违背了这份契约——按文档,它应当只损失一条完整输出,而不是整个服务。
2. 文档甚至说明了"为什么不重建目录"
同文件 :157:
A removed spill directory is not recreated — the private per-process directory is created once; after an external cleaner removes it, every later spill in that process degrades to the in-memory tail with an error log until the host restarts. Recreating a fresh random directory on ENOENT is deferred work.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Upstream report — @deepseek-ai/dsh
Channel:
deepseek-ai/deepseek-harnesshas GitHub Issues disabled (the API returns410 Issues has been disabled in this repository). The project README routes feedback and bugreports to GitHub Discussions, so this one was posted there, under Q&A.
Packages:
@deepseek-ai/dsh0.1.7-rc.2 observed; crash stack from@deepseek-ai/dsh-subprocess-local0.1.2-rc.1Environment: Windows 11, Node v22.17.0
Severity: Critical — one deleted temp dir kills the entire
dsh webserver and every live sessionTitle
Uncaught
ENOENTinOutputCollector.spillAllterminates the wholedsh webserver when its spill dir is deleted mid-sessionObserved crash (verbatim)
The process hosting the web GUI (all sessions) died. The filename's spill counter (
-1-) shows this was the server's first spill, ~83 minutes into the run — any subprocess whose output first exceeds the in-memory cap after the dir disappears triggers it.Root cause
lib/index.js:648-651— the spill dir is created once per process viamkdtempSync(join(tmpdir(), "dsh-subprocess-"))and cached indefaultSpillDir. The module imports nomkdir, so it can never be recreated.lib/index.js:710-721—spillAll()callsopenSync(this.spillFile, "wx", 384)andwriteSync(...)with no error handling, synchronously inside a streamdatahandler (push()at:693, wired at:862). Any failure escapes as an uncaught exception and takes down the whole server, not just the affected subprocess.@deepseek-ai/*package deletes these dirs — a source-wide grep finds only themkdtempline, the per-file name template, and dependency pins. So an external%TEMP%cleaner (Storage Sense, AV, manual sweep) removing the live dir mid-session guarantees this crash on the next spill.dsh-subprocess-*dirs are never pruned; six accumulated on this machine over 10 days of normal use.Measured volume impact (Windows 11, 2026-09-29) — separate from the crash above
The same never-pruned behaviour was measured on a second, larger spill root on the same machine
(
%LOCALAPPDATA%\dsh-temp, alongside the%TEMP%\dsh-subprocess-*dirs described above):Downstream effects while the volume sat at 197 MB — all on the host, none of them inside the
spill code:
OSError: [Errno 28] No space left on device; its cron persistence, its rotating log handlerand its gateway loop all broke and retried in a loop;
and a full index rebuild could not take its database lock at all (
EPERM ... database file is in use) — which is what a full disk looks like from SQLite's side;One observation recorded but not re-verified, because our local remedy reclaimed the files
before it could be checked: a depth-limited listing showed the same 599.9 MB file name three
times in that tree, which may mean duplicate spills of one object rather than three distinct ones.
What we did locally (not a substitute for upstream pruning)
deploy/dsh-temp-reaper.ps1: reports by default, deletes only files older than a threshold(default 24 h) inside a directory whose leaf is exactly
dsh-temp, skips locked files withoutforcing them, prints denominators, appends one line per run; installed as a daily 03:00 task
ahead of the 03:30 reindex so space is reclaimed before anything needs to write.
Ask
other cache.
%TEMP%\dsh-subprocess-*and%LOCALAPPDATA%\dsh-temp) so anoperator can bound them without reverse-engineering.
ENOENTpath described above — that is what turns any external cleanup,ours included, into a server-wide crash.
Repro
npx @deepseek-ai/dsh web, open a session (the spill dir is created lazily once a spill-capped subprocess spawns).%TEMP%\dsh-subprocess-*while the server runs.Suggested fix (applied and verified locally)
Wrap the spill path in try/catch; on failure degrade to the existing tail-only path via
discardSpill()(which already setsspillDisabled = true), recreate the dir onENOENTso later collectors can spill again, and warn once per process:Optional hardening: prune stale
dsh-subprocess-*dirs at startup (they leak forever today), and consider a process-leveluncaughtExceptionguard for the web server so one subsystem can't take down every session.All reactions