@zhengjunyao/dsh-updater
English | 中文
Track DeepSeek Harness's own releases and update it in one click. It follows the npm
dist-tags (latest / next / alpha) and the GitHub releases (tag dsh-v*, bilingual
bodies), shows the changelog of every version between the running build and the target
plus a graded risk report, then hands the install to a detached helper: move the current
install aside with mv → npm install --prefix <prefix> → verify what landed → relaunch
with the exact same command → roll back automatically when the new build does not boot.
Why
DSH ships often (there are several 0.1.x-rc.x channels) and dsh has no self-update of
its own.
- After
npm i -g @deepseek-ai/dsh you still have to restart it yourself, and the restart
command has to be exactly the one it was started with (port, profile, cwd and node
path may all have changed).
- A machine can hold more than one DSH install. The one identified from
process.argv[1], the first one on PATH, and the one npm config get prefix points at
can be three different directories — a naive npm i -g updates the last of them and
changes nothing.
- npm publishes frequently, but only GitHub releases say what changed, and a version on npm
does not necessarily have a release (and vice versa: a version released only on GitHub
cannot be installed at all).
This plugin chains "see the version → read the changelog → grade the risk → install →
verify → restart → roll back on failure" and makes the install that is actually running
the only update target.
Four capabilities
1. Track the latest version
- Three channels: the npm dist-tags
latest / next / alpha, selected by channel.
- Background checks:
checkIntervalMinutes (default 180, 0 disables) plus checkOnBoot
(one check 8s after boot). Their point is not to nag but to keep the cached registry /
release snapshots warm, so opening the panel or asking the agent answers instantly
instead of waiting on two networks.
- Cache: results younger than
cacheTtlMinutes (default 30) are reused, stored in
cache/registry.json and cache/releases.json; force bypasses the cache.
- Upstream failures are never fatal: when registry or GitHub cannot be reached, the last
good answer is reused with a warning saying it may be stale — "cannot reach GitHub" is
never displayed as "no update available".
- A missing or lagging dist-tag falls back down the channel ladder: with no
next/alpha
tag, the newest prerelease at or above latest is offered instead of silently reporting
"no update".
- The panel also shows
newerElsewhere: whether a newer version exists outside the current
channel.
2. Show what changed
- Which versions exist comes from npm (what can be installed); what changed comes from
GitHub releases (the changelog).
- The notes cover every published version in the half-open interval
(current, target], not just a diff — running latest usually jumps several publishes.
- Each version is rendered as sections taken from the release body's
<h3> / # headings.
The bodies are bilingual (Chinese section, ---, English section): the Chinese half is
preferred and the English half is the fallback.
- A version with no matching release is explicitly marked "no release notes found for this
version" (
notesMissing) rather than pretending nothing changed.
- Versions released on GitHub but absent from npm are excluded from the selectable
targets (
installMissing) with an explanation.
- Unauthenticated GitHub is limited to 60 requests/hour;
githubToken raises it, and the
panel shows the remaining budget and reset time.
3. One-click update
The entry point is the DSH-update settings card, the sidebar entry, or the dsh_update
tool. The install itself belongs to the detached helper helper/update-helper.mjs, because
a process cannot safely replace the code it is running:
- the host writes
pending-spec.json, spawns the detached helper, answers 202, and only
then exits;
- the helper waits for the old host to release its port (SIGKILLing it as a backstop);
- it moves the current install aside with
mv (rename) — not a copy of 30 000 files.
A same-directory rename is instant and atomic, so rollback never depends on free space;
npm install -g --prefix <running prefix> --registry <registry> --no-audit --no-fund @deepseek-ai/dsh@<version> (--prefix is always explicit, never
npm config get prefix), with npm's output streamed into the log;
- it verifies what landed: the
package.json version must equal the target, the entry
script <root>/lib/bin.js must exist, and node_modules/@deepseek-ai must be populated
(npm can exit 0 and still leave a half-written tree);
- it relaunches with the exact same invocation (
process.execArgv + argv[1:] + the
original cwd and executable), so dsh web --port 3080, a custom profile and custom paths
survive unchanged;
- a failed boot renames the backup back and relaunches the old version;
- a recovery console is available throughout (default
http://127.0.0.1:3098, trying
+1…+9 when busy).
4. Update risk report
The version number alone cannot answer "should I press this button?". The risk engine turns
what is known about the target, this machine's install topology, the loaded plugins'
contracts and the feasibility of the attempt into one ordered list, in three levels:
high — do not do this blindly; blocks the one-click path until acknowledged;
warn — read this first;
info — context.
Every check below comes from the source, not from guesswork:
Preflight checks (preflight, all from src/installs.ts, returned whether they pass or not):
Grading and gating:
level is the worst level present; score is a 0–100 weighted total (45 per high, 12
per warn, 2 per info, capped at 100) that drives the panel's meter.
vetted: false means a high finding still stands — the update has not been vetted.
confirmOn sets the one-click gate: high (default) blocks only high; warn also
blocks warn; never disables the gate entirely (the report is still shown).
- When blocked, the HTTP route answers 428 (panel) and the tool returns
requiresRiskConfirmation: true (agent) along with the top reasons; retry with
confirmRisk: true once acknowledged.
- The check is re-run at the moment the update is requested: the report the user
acknowledged must be the report for what is about to happen.
Install
# from GitHub (the repo carries the dsh-plugin topic)
dsh plugin --profile web add github:zhengjy01/dsh-updater
# local development: link or file
dsh plugin --profile web add link:/path/to/dsh-updater
dsh plugin --profile web add file:/path/to/dsh-updater
# once published to npm
dsh plugin --profile web add @zhengjunyao/dsh-updater
A dsh web restart is required before the host half loads.
Where it lives in the GUI
- A settings card (
settings.section): running version and the directory it is actually
loaded from, how many installs this machine has, the target version per channel, the
changelog, the risk report, the one-click update button, history, the last failure report
and the plugin settings.
- A sidebar entry, controlled by
entry: sidebar (default, inline in the sidebar),
ball (a fixed corner ball), both, or off (no entry at all).
- While an update runs, a full-screen overlay is shown (
showOverlay, default on) and the
page reloads itself once the new host answers (autoReload, default on).
The sidebar tooltip / aria-label and the overlay titles use the same update wording, matching
the description above (settings.section, id updater, order 339).
Screenshots
Three screenshots from a real machine (macOS, DSH 0.1.5-rc.1, window about 800px wide).
Click any of them for full size.
1. The default latest channel: already up to date
Note the amber install diagnostics block. Under latest there is nothing to update, and it
is shown anyway — because "this machine has two installs and PATH does not agree with the
running one" has nothing to do with whether an update exists. That is this machine's real
state, not demo data.
2. The collapsible sections: history, log, failure report, backups, plugin settings
3. Switched to next: an available version with its changelog
The badge goes from "already up to date" to "0.1.5-rc.2 available", the target version in the
header follows the channel, and the changelog expands as 0.1.5-rc.1 → 0.1.5-rc.2 · 1 version:
one collapsible block per version, with a prerelease badge, publish date and a GitHub release
link, and the body grouped under headings such as 体验优化.
HTTP routes
All registered under /api/dsh-updater/* and loopback-only (the peer address must be
127.0.0.1 / ::1 / ::ffff:127.0.0.1, the Host header must be 127.0.0.1 /
localhost / [::1], Sec-Fetch-Site: cross-site is rejected, and a present Origin must
be same-origin):
/update deliberately replies first and exits later: commit() runs only after the response
is on the wire, so the browser always gets a definite answer.
Agent tools
The gates on dsh_update:
confirm: true is required — never update without the user's consent. The local
standing rule applies: DSH is never restarted or stopped on your own initiative. Without
confirm, the tool only returns an explanation and does nothing.
- When the risk list contains a
high finding, confirmRisk: true is required as well
(the blocked call returns requiresRiskConfirmation: true and the top findings).
- Recommended flow: call
dsh_update_check first, present the changelog and the risks to
the user, get explicit consent, then call dsh_update.
- The update interrupts the current turn and this session's connection; the web page
reconnects and refreshes on its own.
Configuration
File: DSH_HOME/dsh-updater/config.json (default ~/.dsh/dsh-updater/config.json, mode
0600). It is seeded once from the plugin's composition row; after that the file wins
(it is re-read before every update).
Every field is clamped to a sane range (maxAttempts 1–5, backupCount 0–20,
installTimeoutMs 30000–3600000, …); illegal values silently fall back to the defaults, so
the config file can never make the plugin throw.
Update flow
panel button / dsh_update tool
│ POST /api/dsh-updater/update (re-runs the check and the risk grading first)
▼
host (old process)
├─ writes pending-spec.json (0600) + appends a history row (outcome: pending)
├─ spawns the detached helper: helper/update-helper.mjs --spec <spec>
├─ answers 202 (helperPid / logFile / consoleUrl / backupDir / exitInMs)
└─ SIGTERMs itself only after the reply is on the wire (~700ms; hard exit after 15s)
▼
helper (dependency-free Node ESM, outlives the host)
├─ starts the recovery console (3098, then +1…+9 when busy)
├─ waits for 127.0.0.1:<port> to free (SIGKILLs the old pid on timeout)
├─ mv the current install → .dsh-updater-backup-<version>-<stamp> next to it (rename, atomic)
├─ npm install -g --prefix <prefix> --registry <registry> --no-audit --no-fund @deepseek-ai/dsh@<version>
├─ verifies: package.json version == target; lib/bin.js exists; node_modules/@deepseek-ai non-empty
├─ relaunches the new host with the identical file/argv/cwd/env (stdout+stderr → logs/<stamp>-<pid>.log)
├─ triple readiness: port answers + process alive + no fatal boot line in the output,
│ held for readyConfirmMs (4s) before it counts
├─ after ready, keeps watching for bootWatchMs (30s): a drop or a fatal line re-marks it failed
├─ failed → rename the backup back → relaunch the old version (fresh attempt budget)
├─ still failing → stops in `failed`, console stays up for a human
└─ success → prunes backups beyond backupCount → lingers lingerMs → exits
▼
page polls /api/dsh-updater/probe → new host answers → location.reload()
Host and helper exchange state through status.json. The host cannot read a result written
after it died, so on every boot it reconciles the history: it adopts
outcome / landedVersion only when the target and source versions in status.json match
the newest record.
Files and directories
Everything the plugin owns lives under DSH_HOME/dsh-updater/ (override with
DSH_UPDATER_HOME; the config file additionally honours DSH_UPDATER_CONFIG):
Pre-update backups deliberately live outside that directory, next to the install they
protect:
<install root>/../.dsh-updater-backup-<version>-<stamp>/
e.g. /opt/homebrew/lib/node_modules/@deepseek-ai/.dsh-updater-backup-0.1.1-20260913-205812/
Only a same-directory sibling makes the rename work, so taking the backup and putting it
back are single syscalls; npm has been observed to leave such directories alone. Backups
beyond backupCount are removed after a successful update.
Security and limits
Stated plainly:
- An update interrupts the current turn, drops the browser connection and restarts the
host. That is inherent to replacing the host process, not an option.
- A rollback restores the install as it was, i.e. the old code. It does not guarantee
compatibility with data or configuration written by the newer version. "Rolled back"
means the old version starts, not that everything is as it was.
confirmOn: 'never' disables the risk gate — the report is still shown, but a high
finding no longer blocks the one-click path. That is a choice for the user's own machine;
the default is high.
- Prerelease builds (
-rc.* / -alpha.* / -beta.*) are not marked stable by upstream;
interfaces and plugin contracts may change again at any time, and a rollback does not
guarantee plug