dsh-daemon
August 29, 2026 · View on GitHub
Register the DeepSeek Harness web server (dsh web) as an auto-start,
self-healing background service.
After install, dsh web:
- starts automatically on login (LaunchAgent
RunAtLoad/ systemdWantedBy=default.target/ cron@reboot), - restarts automatically after sleep/wake,
- self-heals: a watchdog health-checks
http://127.0.0.1:<port>/healthevery 3 s (configurable) and restarts the server after 3 consecutive failures, - survives this session: the watchdog is a standalone generated script, not an in-memory plugin.
The currently running session is never touched by install/uninstall.
For the account map (npm scope / GitHub account) see CONTEXT.md.
Usage
Option A — install with dsh plugin, mount as a composition row
-
Install the package into the web profile with the official plugin manager (runs pnpm in the profile directory, so the loader can resolve it; a plain global install is not enough — see below):
dsh plugin --profile web add @chenkai114/dsh-daemon(needs
pnpmon PATH — enable it once withcorepack enable.)Why not just
npm install -g? The loader importsname:rows with Node's ESM resolution anchored at the profile directory (~/.dsh/profiles/web/); the globalnode_modulesis not on that resolution chain (andNODE_PATHdoes not apply to ESM). The profile's ownnode_modules— managed here by pnpm — is what makes the package reachable. -
Restart
dsh web. The package declares adsh.bundlemanifest, sodsh plugin addautomatically appends it todsh.profile.bundlesand it mounts as a bundle layer at boot — you do not need (and must not) also insert the same row manually into~/.dsh/profiles/web/cordis.patch.yml, or boot fails withduplicate loader entry id: dsh-daemon.The seven
dsh_daemon_*tools then become available to every agent — just ask the agent to rundsh_daemon_install.
To upgrade later: dsh plugin --profile web update @chenkai114/dsh-daemon
(plus a restart).
⚠️ Upgrading from v0.1.8 or earlier: if you previously followed the old docs and added a manual
- insert: dsh-daemonrow to~/.dsh/profiles/web/cordis.patch.yml, you must delete that row (keep anything else in the file) after upgrading — otherwise the bundle layer and the manual layer insert the sameid: dsh-daemontwice anddsh webfails to boot withduplicate loader entry id. Restart after deleting it.
Permissions: the daemon manages per-user system services (LaunchAgent plists, state files under
$DSH_HOME), so the plugin requestsdanger-full-accessfor its file and command operations. On a deployment that denies escalation the tools fail with sandbox denials.
Option B — dynamic Cordis plugin (no install)
Paste the content of lib/index.js into the code.host field of
cordis_define and run it. This is how the plugin is developed and verified
in a live session: the sandbox supplies the harness global, and the file
ends with return plugin;.
Port
Default port is the currently listening webServer port (usually 3080),
then DSH_WEB_PORT, then the explicit port tool argument. After changing
the port, run dsh_daemon_reinstall.
Architecture
The daemon is a watchdog supervisor made of three parts.
1. Platform registration
A per-user service that starts the watchdog at login and keeps it alive:
| Platform | Mechanism |
|---|---|
| macOS | LaunchAgent ~/Library/LaunchAgents/com.deepseek-ai.dsh-watchdog.plist — ProgramArguments=[node, watchdog.js], RunAtLoad, KeepAlive{SuccessfulExit:false}, ThrottleInterval=10, environment carries DSH_WEB_PORT and DSH_HOME. Loaded with launchctl load -w. |
| Linux | systemd user unit ~/.config/systemd/user/dsh-watchdog.service — Type=simple, Restart=always, RestartSec=10, StartLimitIntervalSec=0; enabled with systemctl --user enable --now. Falls back to a cron @reboot entry when systemd is unavailable. |
| Windows | VBS launcher + Task Scheduler — task DshWatchdog (XML in $DSH_HOME/daemon/dsh-watchdog-task.xml, UTF-16LE) runs wscript.exe //B dsh-watchdog.vbs at logon; the VBS sets DSH_WEB_PORT/DSH_HOME and starts node watchdog.js hidden. RestartOnFailure PT1M/999, MultipleInstancesPolicy=IgnoreNew. Registered with schtasks /Create. |
Windows support is implemented mirroring the macOS/Linux behavior (the plugin's shell layer switches to PowerShell, which is the DSH shell executor on win32) but has not yet been verified on a real Windows machine.
2. The watchdog loop
The generated standalone script $DSH_HOME/daemon/watchdog.js (dependency-free,
runs on any Node ≥ 18, no session required):
- writes its PID to
.dsh-watchdog.pid; SIGINT / SIGTERM / SIGHUP clean up and exit; a single-instance lock refuses duplicate watchers; - at startup, launches the web server (
node <dsh> web --port <port>, detached, output tologs/dsh-web.log) ifhttp://127.0.0.1:<port>/healthis not OK; - then every 30 s (configurable via
DSH_DAEMON_HEALTH_INTERVAL):- skips when
.daemon-stoppedexists (user paused monitoring) or.daemon-restart.lockis fresh (< 120 s, a restart is in progress); - restarts the server when a tick gap exceeds 90 s (sleep/wake);
- restarts the server after 3 consecutive failed health checks;
- exits when the
.daemon-installedmarker disappears (uninstalled);
- skips when
- logs to
logs/watchdog.log(5 MB × 3 rotation).
3. Daemon-aware start / stop
dsh_daemon_stopwrites.daemon-stopped(the watchdog will not restart the server) and stops the daemon-managed server if one is running.dsh_daemon_startclears the flag, makes sure the watchdog runs, and launches the server if it is unhealthy.
Tools
The plugin is Host-only and registers seven model-callable tools:
| Tool | What it does |
|---|---|
dsh_daemon_install | Generates watchdog.js + state files, writes the LaunchAgent plist (or systemd unit / cron entry, VBS + Task Scheduler on Windows), starts the watchdog now. Optional port argument. |
dsh_daemon_uninstall | Stops the watchdog, unloads and deletes the platform registration, removes all state files. |
dsh_daemon_reinstall | uninstall + install (use after upgrading dsh or changing the port; also regenerates the watchdog with the current auto-update configuration). |
dsh_daemon_status | Installed since, port, local/latest versions, update state, watchdog PID/liveness, manual-stop flag, server health, last log lines. |
dsh_daemon_start | Clears the stopped flag, ensures the watchdog runs, launches the server if unhealthy. |
dsh_daemon_stop | Writes the stopped flag (watchdog will not restart), stops the daemon-managed server if one is running. Never touches the current session. |
dsh_daemon_update | Check for a newer version (apply: false, default) or download and apply it (apply: true). Also the manual entry point for major version changes. |
Command line (dsh-daemon)
dsh_daemon_install also writes a thin dsh-daemon command into the node
bin directory (PATH), so the daemon is controllable from a terminal without
opening the GUI:
| Command | What it does |
|---|---|
dsh-daemon status | Same status as the GUI tool. |
dsh-daemon restart | Immediately restarts dsh web (kills the process on the port and launches a new one; no waiting for the health loop), verified healthy before returning. |
dsh-daemon start | Clears the stopped flag, starts the watchdog if missing, launches the web server if unhealthy. |
dsh-daemon stop | Writes the stopped flag and kills the web server (including a manually started one). |
dsh-daemon update | Check the registry (--apply to download and apply). |
dsh-daemon install / uninstall / reinstall | Registration operations, executed by the plugin through its /dsh-daemon/command route — these need dsh web to be up (the supervision commands above work standalone via the watchdog script). |
dsh-daemon help | Usage. |
restart/stop interrupt all open sessions, exactly like a manual pkill —
the watchdog relaunches the web server on the next health cycle if the direct
launch fails.
State files ($DSH_HOME/daemon/, $DSH_HOME defaults to ~/.dsh)
daemon/
├── watchdog.js # generated watchdog script (standalone, no deps)
├── .daemon-installed # install timestamp marker
├── .daemon-port # supervised port
├── .daemon-stopped # pause flag: watchdog will not restart the server
├── .daemon-restart.lock # restart-in-progress marker (TTL 120 s)
├── .dsh-watchdog.pid # watchdog PID
├── .dsh-web.pid # daemon-managed web server PID
├── .daemon-update.lock # update-in-progress lock (concurrency guard)
├── .daemon-update-pending # downloaded update awaiting a restart to activate
├── .daemon-update-check.json # last update check result (status display)
├── dsh-watchdog.vbs # Windows: hidden wscript launcher
├── dsh-watchdog-task.xml # Windows: Task Scheduler XML (UTF-16LE)
└── logs/
├── watchdog.log # watchdog log (5 MB × 3 rotation)
└── dsh-web.log # web server stdout/stderr when launched by the watchdog
Auto-update
The watchdog checks the npm registry at startup and every 6 h and updates
@chenkai114/dsh-daemon in the profile directory with pnpm:
- Version policy: same-major versions (0.1.3 → 0.1.4, 0.2.x → 0.2.y) update
automatically; a major change (0.x → 1.x, 1.x → 2.x, …) is only reported and
requires the manual
dsh_daemon_updatetool. - Update modes (
DSH_DAEMON_UPDATE_MODE):restart(default): after downloading, the watchdog polls the plugin's/dsh-daemon/activityendpoint (agent turns + background jobs) every 30 s and restartsdsh webonly after it has been idle for the quiet window — an in-progress conversation or job defers the restart until it finishes. If the endpoint is unreachable (plugin not mounted), the restart still happens afterDSH_DAEMON_DEFER_MAX. Fully unattended.download: the new package is installed in the profile and a pending marker is written; the update activates on the next naturaldsh webrestart. No session is ever interrupted — the user decides when the update takes effect.
- Failure safety: registry unreachable, pnpm failure, or a version
mismatch after update only writes a log line and the check state; the old
package stays installed (pnpm's store keeps it, so
dsh plugin --profile web add @chenkai114/dsh-daemon@<old>rolls back).
Configuration is captured at dsh_daemon_install/reinstall time and embedded
into the generated watchdog script:
| Env var | Default | Meaning |
|---|---|---|
DSH_DAEMON_AUTO_UPDATE | 1 | 0 disables the checks |
DSH_DAEMON_UPDATE_INTERVAL | 6h | check interval (ms/s/m/h/d) |
DSH_DAEMON_UPDATE_MODE | restart | restart or download |
DSH_DAEMON_QUIET_WINDOW | 5m | idle time required before a restart-mode restart |
DSH_DAEMON_DEFER_MAX | 15m | max wait for the activity endpoint before restarting anyway |
DSH_DAEMON_NPM_REGISTRY | https://registry.npmjs.org | registry used for checks and pnpm update |
DSH_DAEMON_PROFILE | web | profile directory holding the plugin |
DSH_DAEMON_HEALTH_INTERVAL | 30s | health-check interval of the watchdog loop (ms/s/m; 3 failures trigger a restart) |
DSH_DAEMON_OPEN_BROWSER | 1 | when 0, never auto-open the browser even when a new-dsh launch token is detected (the URL is still written to ~/.dsh/daemon/.web-auth-url and the watchdog log for manual access) |
DSH_DAEMON_CLI_DIR | node bin dir | directory for the generated dsh-daemon CLI (tests/sandboxed installs point it at a temp dir to avoid polluting the real PATH) |
DSH_DAEMON_NO_SYSTEM | unset | when 1, skips system-level registration (launchd/schtasks/systemd) — test/sandboxed installs never touch the host's services; the watchdog is still started directly |
DSH_DAEMON_TRUSTED_HOST | unset | comma-separated --trusted-host list (e.g. dsh.example.com,192.168.5.5:8080). When dsh web is reached through a reverse proxy (nginx) the Host header is the public hostname and the /api browser-trust fence would reject it with 403 — set this to make dsh web trust those Hosts |
The auto-update logic lives in the generated
watchdog.js; after upgrading to a version with new update logic, rundsh_daemon_reinstallonce to regenerate it.
Verification
All of the following were verified end-to-end against the real plugin code:
- install →
plutil -lintOK,launchctl listshows the agent, watchdog logswatchdog started (PID …, port 3080)/web server already healthy on port 3080; - on an empty port the watchdog launches a real
dsh web --port <port>at startup (health OK on the new port); - self-heal: after
SIGKILLof the managed server →health check failed (1/3 → 2/3 → 3/3)→failure threshold reached, restarting web server→ new process serves 200; - launchd
KeepAlive:SIGKILLof the watchdog → launchd restarts it within ~11 s; - single-instance guard: running
watchdog.jsa second time exits immediately; stopwrites the pause flag and kills only the daemon-managed server;startclears it;uninstallremoves launchd registration, plist, state files and frees the port;statusreflects every state.
Local test
node test/harness.js dsh_daemon_status # static package mode
DYNAMIC=1 node test/harness.js dsh_daemon_status # dynamic sandbox mode
The harness runs the real plugin code with real bash/fs and invokes the tool for real.
v0.1.19 — dsh web token-auth adaptation (seeding the ?token= launch token)
Since dsh ≥ 0.1.2-alpha.1 (harness commit 3e24087bfa) dsh web generates an
in-process random token at startup and prints
dsh web: http://127.0.0.1:<port>/?token=...: the browser must visit that URL
once to seed a 30-day host-only Cookie (the signing key persists across
restarts; a bare 3080 visit with no Cookie → 401). The watchdog used to launch
with --no-open, so the user's browser never got a Cookie and the daemon-managed
web was 401.
Adaptation: after launching, the watchdog extracts the current run's
token URL from dsh-web.log (the web process stdout), then:
?token=detected (new dsh) → open the default browser once to seed the Cookie (same as manualdsh web); if opening fails (headless), the URL is already on disk for manual access;- not detected (old dsh) → status quo (
--no-open, never touch the browser); - the URL is always written to
~/.dsh/daemon/.web-auth-url(0600, overwritten each launch) and shown bydsh-daemon status; DSH_DAEMON_OPEN_BROWSER=0disables the auto-popup (disk + log only);- the scan is anchored to the pre-launch file offset: the append-only POSIX log
never reuses an old run's dead token (the LAST
dsh web:line of the current run's segment wins; win32Start-Processoverwrite semantics fall back to reading the whole file, anddsh-web.log.1— the previous process — is never read); - the probe is cross-version stable:
dsh web: http://...has been printed by every dsh version, old and new differ only in whether the URL carries?token=— so?token=presence is the judge, robust to future versions; - reverse insurance: if a new dsh's token line is slow to appear the watchdog KEEPS WAITING (fast poll 15s@250ms then a slow retry phase 75s@5s) instead of dismissing it as "no token" (treating a probe failure as old dsh would miss the auth); on total timeout it only logs and the next launch retries;
- popup throttle: the token changes on every start but the 30-day cookie
outlives any single token (persistent signing key), so the browser is not
re-opened when the URL equals the last recorded one (
.web-auth-urlis still refreshed for manual access);SSH_CONNECTION/SSH_TTYnon-empty suppresses the popup entirely (never open a browser on a remote host's desktop); - the version gate mirrors the
--no-openpattern (DSH_TOKEN_AUTH_MIN = 0.1.2-alpha.1, module-scope functions inlined into the watchdog viatoString(), re-decided at every launch) and only skips the poll when the installed dsh is known to predate token auth; the URL-line probe is the authoritative detector.
v0.1.18 — Windows black-box fix: hidden console, not no console; start waits for health
On Windows the watchdog spawns dsh web, pnpm, netstat, etc. with
CP.spawn(..., { detached: true }), and Node gives detached children their
own console window by default (the watchdog itself runs hidden — VBS /
Task Scheduler — and has no console), so every launch/restart flashed black
boxes. Once v0.1.17 fixed the --no-open restart loop, the black boxes became
the visible problem.
Mechanism choice (deepseek-harness discussion #1564 / #810): dsh web
must NOT be launched with windowsHide (CREATE_NO_WINDOW) — a console-less
host forces every child it spawns to allocate a new visible console, and
CREATE_NO_WINDOW kills the Windows ACL sandbox's restricted-token children
with 0xC0000142 (DLL initialization failed). The correct approach is to give
dsh web a hidden console (STARTF_USESHOWWINDOW + SW_HIDE, keeping
dwCreationFlags=0 — on Windows implemented via Start-Process -WindowStyle Hidden, matching the dsh-daemon start direct-launch path): dsh web has no
visible window itself, and its console children inherit that hidden console,
so nothing flashes at any level.
- on win32 the watchdog now launches dsh web through
powershell.exe -Command "Start-Process -FilePath <node> -ArgumentList ... -WindowStyle Hidden -RedirectStandardOutput <web.log> -PassThru"(the powershell wrapper itself uses windowsHide — short-lived, normal token, safe); the wrapper writes the PID file and the watchdog polls for it; ⚠️ the wrapper must NOT usedetached: true— Node maps it toDETACHED_PROCESSon Windows, which hangs the Start-Process command (no PID file, child never starts; reproduced empirically). Start-Process children are independent processes anyway, so the short-lived wrapper needs no detachment; Start-Process redirects with OVERWRITE semantics, so before each launch the olddsh-web.logis rotated todsh-web.log.1(the previous run's crash output survives; the log stays bounded — current + one previous run, never unbounded); - the watchdog's other short-lived children (self-spawn, pnpm, idle-restart
waiter, netstat/lsof probes) keep
windowsHide: true— safe under a normal token, consistent with the discussion's subprocess-local treatment; dsh-daemon startnow polls for health after launching (likerestartdoes, up to ~13 s) before returning — previouslystartreturned whiledsh webwas still booting, the nextstatuslooked unhealthy, and users ranstartagain, which killed the still-booting first instance via the PID file;- template assertions: every spawn site carries
windowsHide, and the win32 web launch must go throughStart-Process -WindowStyle Hidden(regression guard).
Note: the DSH-internal layer from #1564 (two spawns in
dsh-sandbox-windows-acldwFlags:256→257+wShowWindow:0;windowsHide:trueindsh-subprocess-local) is a patch to dsh itself, not this repo; re-apply it after upgrading dsh (the community scriptCuleot/dsh-no-console-flashis idempotent).
v0.1.17 — pass --no-open only when the dsh version supports it
Since v0.1.16 the watchdog launched dsh web --port <port> --no-open, but
--no-open only exists in @deepseek-ai/dsh 0.1.0-rc.8 (dsh-web-app
0.1.0-rc.8, which also introduced default browser opening). Older CLIs reject
the flag with unknown option '--no-open' and exit immediately, so the
watchdog fell into a restart loop: launch → instant death → failed health
check → relaunch, and web never came up.
- The watchdog reads the dsh package
package.jsonversion at every launch and appends--no-openonly when it is ≥ 0.1.0-rc.8 (semver, including prerelease ordering); an unknown/unreadable version conservatively skips the flag — the server still starts, and pre-rc.8 dsh never opened a browser anyway, so nothing is lost; - the
dsh-daemon startdirect-launch commands (WindowsStart-Process/ Unixnohup) make the same version-based decision; - the gate is a single module-scope implementation; the watchdog inlines the
exact same code via
Function.prototype.toString(), so dsh upgrades or downgrades take effect at the next launch without reinstalling the daemon; - new
test/version-gate.test.jsunit tests (run bynpm test).
v0.1.16 — health check, browser pop-ups, and env forwarding
/healthroute: the watchdog health-checkshttp://127.0.0.1:<port>/healthevery 30 s, but deepseek-harness's web server has no such route (unknown paths 404), so a running web was reported unhealthy forever. The plugin now registers/healthitself, returning200 {"ok":true}— plugin up means web up, and the check is reliable.--no-open: daemon-managed web restarts (auto-update, self-heal) no longer pop a browser tab; manualdsh webstill opens by default.- Env forwarding:
dsh-daemon install/uninstall/reinstallexecute in the web process via the/dsh-daemon/commandroute, soDSH_DAEMON_*variables from the invoking shell never reached the plugin. The CLI wrapper now collects allDSH_DAEMON_*from the current shell and forwards them with the request, soDSH_DAEMON_UPDATE_INTERVAL=1m dsh-daemon reinstallconfigures the watchdog correctly.
v0.1.15 — test/sandboxed installs no longer touch the host
Test harness installs with a temp HOME used to pollute the real environment; two switches close that gap:
DSH_DAEMON_CLI_DIR: overrides where the generateddsh-daemonCLI is written (default: node bin) — tests point it at a temp dir so the real wrapper on PATH is never overwritten;DSH_DAEMON_NO_SYSTEM: when1, skips system-level registration (launchd/schtasks/systemd) so a test install cannot steal the system service label and leave the real daemon dead. The harness sets both by default.
v0.1.14 — restart is now the default auto-update mode
The default of DSH_DAEMON_UPDATE_MODE changed from download to
restart: when unset, after an update is downloaded the watchdog restarts
dsh web on its own once it is idle (fully unattended, never interrupting
an in-progress session). Set it explicitly to download when you want to
control when the update takes effect.
v0.1.13 — watchdog regenerated automatically after auto-update
Previously auto-update only refreshed the npm package; the already-generated
watchdog.js (a one-time artifact from install time) never picked up the new
generator logic — a manual dsh_daemon_reinstall was required. Since v0.1.13:
- the plugin version is embedded into the generated watchdog (
GEN_VERSION); - on every plugin boot the script's
GEN_VERSIONis compared with the installed package version; when they differ (after an auto-update, or a manual package upgrade) the plugin regenerateswatchdog.jsand the CLI wrapper and restarts the watchdog process; - so after an auto-update (user restarts web in download mode, or the
idle-aware restart does it in restart mode) or a manual upgrade + web
restart, the watchdog catches up with the new version on its own — no
manual
dsh_daemon_reinstall; - test/development loads can skip the sync with
DSH_DAEMON_AUTOREGEN=0.
v0.1.12 — Windows console-flash regression fix
v0.1.11 added windowsHide: true (Windows CREATE_NO_WINDOW) to the
watchdog's launch() and other spawns. The side effect: the dsh web
process lost its console handle, so any child it spawns afterwards (git,
tool executions, …) gets a visible console window on Windows — frequent
console flashes while the server runs (issue #1).
v0.1.12 removes all four windowsHide flags and restores the v0.1.10 model:
the watchdog is started by VBS shell.Run ..., 0 (SW_HIDE) with its own
hidden console, the web process inherits it, and web's children inherit in
turn — the whole chain stays windowless (verified on v0.1.10).
Note: if console windows still flash during plugin install/command execution (the DSH sandbox/subprocess path, not this plugin's watchdog), that is a deepseek-harness Windows console-handling issue, not this plugin — see discussion #1564 and the Culeot/dsh-no-console-flash patch.
License
MIT