Introduction¶
On Windows, running DSH causes every shell call to spawn a private runner process. Once the command finishes, this runner should exit on its own, but in practice, it often stays behind: according to the README on a DSH Desktop 0.2.x machine, one work session accumulated 8 runners, occupying 1127 MB, with the oldest one running idle for four and a half hours. Neither @deepseek-ai/dsh-subprocess-local nor dsh-ops expose shell lifecycle settings, meaning there is nothing to configure. cg-shell-janitor is a workaround for this issue, maintained by DonGeeo87, categorized under admin-security.
Below is an introduction to how it works, its configuration options, and installation methods.
Where the Problem Lies¶
On Windows, every dsh shell call spawns a private runner:
"DeepSeek Harness.exe" "...\dsh-subprocess-local\lib\runner.js" -- ...\bash.exe -c "<your command>"
This runner holds the command’s Win32 Job. When the command ends, it should close the empty Job via its own poll() and call finish(0), but it often continues to survive, constantly occupying memory. This is precisely the object that cg-shell-janitor aims to clean.
Core Functionality¶
The plugin samples every intervalSeconds, looking for DSH runner processes, and shuts down those that are truly idle. Closing a runner releases the Win32 Job it holds, which aligns with the semantics of DSH managing child processes itself. It only takes action when all of the following conditions are met:
- The runner’s parent process is still the DSH host process.
- The runner has no living child processes, meaning
bash.exehas exited. - It also has no previously observed children still alive, indicating it isn’t watching background processes. The tree is re-learned on every sample, so a command that leaves a server running with
&will still have its runner protected. - It has existed for more than
idleMinutes. - There are no unattributable daemon-like orphan processes nearby. If there are, the plugin enters conservative mode and closes nothing in this round.
The first sample after startup only observes and does not decide, to first understand the current process tree. The plugin does not delete any files except for the leaked runners themselves, and it does not touch any other processes.
The real-world data provided in the README is from the same machine and session, after the first real cycle: surviving runners dropped from 8 (1127 MB) to 2 (288 MB, both with running shells), free memory rose from 3.37 GB to 5.78 GB, and total process count dropped from 298 to 240.
Installation¶
The README mentions two methods: one-click installation from the dsh-market directory, or installation from a local checkout. The local installation command is:
dsh plugin --profile <your-profile> add <path-to-this-directory>
Restart the harness after completion.
Uninstall command:
dsh plugin --profile <your-profile> remove cg-shell-janitor
Configuration¶
Configuration is written in cordis.patch.yml:
| Key | Default Value | Meaning |
|---|---|---|
enabled |
true |
Disable the cycle without uninstalling the plugin |
dryRun |
false |
Only log the processes to be closed, do not perform actual operations |
intervalSeconds |
180 |
Sampling interval (values 30–3600) |
idleMinutes |
5 |
Minimum survival time before closing (values 1–1440) |
reapOnStartup |
true |
Perform the first sample 20 seconds after startup |
A cycle consumes approximately 2 seconds of single-core CPU every 3 minutes, as it runs a single Get-CimInstance query via PowerShell. If this affects you, simply increase intervalSeconds.
Logs are written to ~/.dsh/shell-janitor.log, with a 256 KB rotation.
Applicable Scenarios and Notes¶
Targeted at developers who use DSH for long periods on Windows and are troubled by accumulated memory usage from leaked runners. Pay attention to a few points:
- Windows only. The leak occurs on runners holding Win32 Jobs; on other platforms, the plugin only logs one line and does nothing.
- This is a heuristic solution, not a fix. The real fix is upstream: the runner should exit on its own.
- Runners created before the plugin loads and whose shells have exited have no child process record on the plugin side. They will still be closed if they exceed
idleMinutes, because condition 5 only blocks those orphan processes that the plugin cannot attribute at all. - If you deliberately keep a daemon alive from a shell command and the plugin has never observed that command, this daemon is only protected by conservative mode, not precise matching.
The plugin runs with the permissions of the current dsh process. It is recommended to review the source code (README and cordis.patch.yml) and confirm the license before installation: MIT.
Summary¶
cg-shell-janitor has a narrow scope: periodically finding DSH runners on Windows that have nothing to do but haven’t exited and shutting them down, recovering about 140 MB each. It cannot replace an upstream fix, but it provides a usable cleanup mechanism when configuration options are lacking.
- Community directory page: https://www.skillhub.cn/plugins/DonGeeo87/cg-shell-janitor
- GitHub: https://github.com/DonGeeo87/cg-shell-janitor