cg-shell-janitor
A DeepSeek Harness plugin for Windows that closes the runner processes DSH leaves behind after a shell command finished. Each one holds about 140 MB of RAM.
The problem
On Windows every dsh shell call starts a private runner process:
"DeepSeek Harness.exe" "...\dsh-subprocess-local\lib\runner.js" -- ...\bash.exe -c "<your command>"
That runner owns the Win32 Job of the command. When the command ends the runner is supposed to
exit on its own — its own poll() closes the empty Job and calls finish(0). In practice it
often stays alive: measured on DSH Desktop 0.2.x, eight of them had accumulated in a single
working session, 1127 MB, the oldest one four and a half hours old and completely idle.
Neither @deepseek-ai/dsh-subprocess-local nor dsh-ops exposes a shell lifetime setting, so
there is nothing to configure. This plugin is the workaround.
What it does
Every intervalSeconds it looks for DSH runner processes and closes the ones that are
provably doing nothing. Closing a runner releases its Win32 Job, which is the semantics DSH
already uses for its own children.
It only acts when all of these hold:
- The runner's parent is still the DSH host process.
- The runner has no living child — its
bash.exealready exited. - None of the children it was ever seen with is still alive, so it is not babysitting a
background process. The tree is re-learned on every sample, so a command that left a server
running behind
&keeps its runner protected. - It is older than
idleMinutes. - There are no unattributable daemon-like orphans around. If there are, the plugin enters a conservative mode and closes nothing that cycle.
The first sample after startup only observes, so the plugin learns the tree before deciding anything. Nothing is deleted, and no process is touched other than the leaked runner itself.
Measured effect
Same machine, same session, right after the first real cycle:
| Metric | Before | After |
|---|---|---|
| Live runners | 8 (1127 MB) | 2 (288 MB, both with a live shell) |
| Free RAM | 3.37 GB | 5.78 GB |
| Total processes | 298 | 240 |
It closed three leaked runners and left the two that were running commands alone.
Configuration
In cordis.patch.yml:
| Key | Default | Meaning |
|---|---|---|
enabled |
true |
turn the cycle off without uninstalling |
dryRun |
false |
only log what it would close |
intervalSeconds |
180 |
how often it samples (30–3600) |
idleMinutes |
5 |
minimum age before closing (1–1440) |
reapOnStartup |
true |
first sample 20 s after startup |
A cycle costs about 2 seconds of one core every three minutes: it runs one Get-CimInstance
query through PowerShell. Raise intervalSeconds if that matters to you.
Log: ~/.dsh/shell-janitor.log, rotated at 256 KB.
Install
From the dsh-market catalog (one click), or from a local checkout:
dsh plugin --profile <your-profile> add <path-to-this-directory>
Then restart the harness.
Remove
dsh plugin --profile <your-profile> remove cg-shell-janitor
Scope and limitations
- Windows only. The leak is in the Win32 Job-owning runner. On other platforms the plugin logs one line and does nothing.
- It is a heuristic, not a fix. The real fix belongs upstream: the runner should exit by itself.
- A runner created before the plugin loaded whose shell already exited has no children on
record. It is still closed once it passes
idleMinutes, because condition 5 only guards against orphans the plugin cannot attribute at all. - If you deliberately keep a daemon alive from a shell command and the plugin never observed that command, the daemon is protected only by the conservative mode, not by an exact match.
License
MIT.
Español
Plugin para DeepSeek Harness en Windows que cierra los procesos runner que DSH deja vivos después de que su comando terminó. Cada uno ocupa unos 140 MB.
En Windows cada llamada a dsh crea un proceso runner que es dueño del Job de Win32 del
comando. Cuando el comando termina debería salir solo, pero muchas veces queda vivo: medido en
DSH Desktop 0.2.x, en una sola sesión de trabajo se acumularon ocho, 1127 MB, el más viejo con
cuatro horas y media de antigüedad y sin hacer nada.
Ni @deepseek-ai/dsh-subprocess-local ni dsh-ops exponen un ajuste de vida del shell, así
que no hay nada que configurar. Este plugin es el parche.
Cada intervalSeconds busca runners y cierra los que no están haciendo nada. Solo actúa si el
runner no tiene hijo vivo, no supervisa ningún proceso que se le haya conocido, es más viejo
que idleMinutes y no hay huérfanos de tipo demonio que no sepa atribuir (si los hay, entra en
modo conservador y no cierra nada). La primera muestra después de arrancar solo observa.
Log en ~/.dsh/shell-janitor.log. Configuración en cordis.patch.yml. Solo Windows.
Licencia MIT.
No comments yet. Be the first to write one.