Repository navigation
Releases
v0.2.17 - unattended mode: nudge on any non-completed turn
What changed
User feedback: only reason.kind === 'error' triggered the
failure-nudge. aborted / interrupted / blocked / max-tokens
were silently treated as "agent stopped" and the task stalled.
This breaks unattended operation.
Behavior now
Every turn/end event with reason.kind !== 'completed' now:
- Sets a synthetic
lastFailureon the session
(e.g.TURN_ABORTED,TURN_INTERRUPTED,TURN_MAX-TOKENS,
TURN_BLOCKED). - Triggers
runIdleCheck()synchronously, so the failure-nudge
fires within milliseconds — not afteridleThresholdMs.
Only turn/end:completed is treated as a normal exit: no
failure, no nudge, nudge counter resets.
Why this matters for unattended use
Scenario: bash tool gets SIGTERM during a long command. dsh aborts
the turn. Without this fix, the conversation sits idle until the
user (or the 3-min idle timer) sends 继续. With this fix, the
doctor injects 继续 immediately and the agent resumes the next
turn.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.17- Restart dsh web (or wait for the next auto-reload).
- Trigger any non-completion: kill a long-running bash command,
trigger a 429 / 5xx, or simulate a max-tokens truncation. - Watch
~/.dsh/doctor/logs/doctor.log:
session watch: nudged ... (failure=TURN_ABORTED, ...)
within milliseconds of the turn ending.
110/110 unit tests pass.
v0.2.16 - retry PI_AI_ERROR and tool-orchestration errors
What changed
Two common failure shapes were aborting the agent loop without the
doctor re-injecting a continuation:
-
PI_AI_ERROR / Provider returned error — upstream API hiccup.
Was classified as 'business' (the default fallback) so the
doctor did NOT defer a followup. -
edit requires reading file first / ToolCallError / inject
error — the agent's own tool call hit a recoverable
orchestration error. The fix is for the agent to re-do the
tool call after reading the file.
Behavior now
Both classes are now:
PI_AI_ERROR / Provider returned error→ TRANSIENT
(record + log, retry-safe)requires reading X first/ToolCallError/
cannot get property X without inject→ AGENT
(record + log + defer followup so the agent retries)
The agent's failure-nudge path (session-watch) fires on every
turn/end:error regardless of classification, so the user-visible
behavior is: any tool error that bubbles up as turn/end:error
gets a 继续 followup immediately.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.16- Trigger a real PI_AI_ERROR (or stage a fake "requires reading X
first" error) and watch~/.dsh/doctor/logs/doctor.logfor:
tool error captured: ... → transient (recorded)
session watch: nudged session-... (failure=..., ...)
102/102 unit tests pass.
v0.2.15 - 7 new patterns actually ship in the watchdog
What changed
v0.2.14 claimed 6 new patterns but the standalone watchdog (the
process that actually saves your dsh web boot) still had only 5
because it has its own inline PATTERNS table separate from
src/triage.ts. This release syncs the two.
Patterns now in the standalone watchdog
| Pattern | Priority | Action |
|---|---|---|
eaddrinuse |
100 | kill-pid-and-restart |
node-version-mismatch |
95 | notify-user |
disk-full |
90 | notify-user |
duplicate-loader-entry |
89 | disable-row |
corrupt-patch-yaml |
88 | cleanup-and-restart |
cordis-schema-validate |
85 | safe-mode |
schema-parse-error |
80 | disable-row |
pnpm-peer-conflict |
78 | disable-row |
plugin-export-missing |
75 | disable-row |
plugin-file-missing |
72 | disable-row |
cannot-find-module |
70 | disable-row |
plugin-load-error |
60 | disable-row |
12 patterns in total.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.15launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor- Confirm watchdog reloaded:
grep "id: '" ~/.dsh/doctor/watchdog.js | wc -l→ should be 12 - Stage a fake bad plugin that says "Requires Node ^999":
~/.dsh/doctor/.doctor-user-action-requiredwill be written
by the watchdog once it sees that line in the dsh-web log.
100/100 unit tests pass.
v0.2.14 - 6 new triage patterns
What changed
v0.2.13's triage engine recognized 5 patterns. v0.2.14 adds 6 more,
covering failures we could only stare at before.
New patterns
| Pattern | Priority | Action |
|---|---|---|
node-version-mismatch |
95 | notify-user |
disk-full (ENOSPC) |
90 | notify-user |
corrupt-patch-yaml |
88 | cleanup-and-restart |
cordis-schema-validate |
85 | safe-mode |
pnpm-peer-conflict (ERESOLVE) |
78 | disable-row |
plugin-export-missing |
75 | disable-row |
plugin-file-missing (ENOENT on .js) |
72 | disable-row |
New action kinds
notify-user: doctor cannot fix, human must. Writes to
~/.dsh/doctor/.doctor-user-action-required.cleanup-and-restart: doctor moves broken local state aside
(e.g. corruptcordis.patch.yml) and waits for the platform
service to restart dsh web.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.14launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor- Simulate a node version error by adding a bad plugin
(e.g. one that says "Requires Node ^999") and watch the log:
tail -f ~/.dsh/doctor/logs/watchdog.log cat ~/.dsh/doctor/.doctor-user-action-requiredwill show the
doctor staged a user-action-required entry.
100/100 unit tests pass.
v0.2.13 - 2s probe, first-failure triage
What changed
User feedback: the previous 30s probe + 90s 3-failure threshold
meant a stalled GUI for 2 minutes. The user explicitly said
"争分夺秒" (every second counts).
- healthIntervalMs: 30s -> 2s. Watchdog polls dsh web's
/health every 2 seconds. - No 3-failure wait. The first failure gives launchd one
probe (2s) to recover on its own. If port 3080 is still empty
on the next probe, triage fires immediately. If dsh web is
alive but the health endpoint fails on the first probe, triage
also fires immediately.
Triage action (unchanged)
Read dsh web's log, identify the broken plugin, write a cordis
patch row that disables it. The platform service's next restart
picks up the fix. dsh-doctor never touches launchd / systemd /
Task Scheduler.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.13launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor- Tail the log:
tail -f ~/.dsh/doctor/logs/watchdog.log pkill -f 'dsh web'- Within 4s (2 probes), expect:
health probe failed, port 3080 empty - giving platform service 1 probe to recover
then (if launchd hasn't recovered yet):
platform service failed to recover dsh web after 2 probes - entering crash-loop triage - dsh-doctor writes
cordis.patch.yml.doctor-disabled-<pluginId>
and waits. launchd eventually pulls dsh web back, this time
without the broken plugin.
94/94 unit tests pass.
v0.2.12 - doctor diagnoses, launchd/systemd restarts
What changed
v0.2.11 wrongly had the watchdog call launchctl kickstart /
systemctl restart to relaunch dsh web. That raced with the
platform service which already owns dsh web's lifecycle.
v0.2.12 separates the responsibilities:
- Platform service (LaunchAgent / systemd / Task Scheduler):
restarts dsh web. The doctor never touches it. - dsh-doctor: when dsh web is broken, identify the offending
plugin and write a disable row. The platform service's next
restart attempt then runs without the broken plugin.
Flow
tick()
probe()
OK → log heartbeat, return
FAIL → count += 1
port empty? (dsh web process is gone)
< 3 fails: do nothing, launchd will pull it back
>= 3 fails: launchd is in a crash loop, triage
port has listener? (dsh web alive but broken)
< 3 fails: log only, wait for richer log
>= 3 fails: triage
triage(lines):
matched disable-row? → stageDisableRow(pluginId)
matched safe-mode? → activateSafeMode(allowList)
nothing matched? → activateSafeMode (worst case)
budget exhausted? → activateSafeMode
After triage: reset failure tracking. The next probe (30s later)
will see whether dsh web came back up cleanly.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.12launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor- To simulate a plugin crash: stage a disable for a real plugin
(e.g.dsh-doctor-disable-dsh-vision), thenpkill -f 'dsh web'. - Watch
~/.dsh/doctor/logs/watchdog.log:health probe failed AND port 3080 is empty — letting platform service handle the restart- (dsh web comes back via launchd)
health probe OK (heartbeat) uptime=...
94/94 unit tests pass.
v0.2.11 - 30s response to dsh web death (was 90s)
What changed
Previous v0.2.10 still waited 3 consecutive probe failures (90s)
before checking the port. v0.2.11 checks the port on the very first
failure:
- If the port is empty, dsh web is GONE. Ask the platform service
to relaunch it immediately. Worst case 30s before doctor acts. - If the port has a listener, dsh web is alive but broken. Fall
through to the 3-failure triage path (give the doctor 90s to
read the log and pick the right plugin to disable).
New flow
tick()
probe()
if OK: consecutiveFailures = 0, return
if FAIL: consecutiveFailures += 1
if first failure:
if port empty: relaunchViaPlatform() ← new
else: fall through (plugin crash suspected)
if < 3: log warn, return
else: triage (disable-row / safe-mode)
Trade-off
A single false positive (dsh web alive but health probe fails AND
the port briefly looks empty) costs one launchctl kickstart.
That's a cheap, idempotent operation, so the recovery speed is
worth it.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.11launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctorto
regenerate the watchdog with the new code.pkill -f 'dsh web'- Within 30s,
~/.dsh/doctor/logs/watchdog.logshould show:
first probe failure AND port 3080 is empty - dsh web process is gone
thenlaunchctl kickstart -k gui/<uid>/io.deepseek.dsh issued
and within 30s more:health probe OK (heartbeat) uptime=...
94/94 unit tests pass.
v0.2.10 - launchd handles dsh web, doctor just triages
What changed
Previous v0.2.9 had the watchdog spawn a fresh dsh web from itself
when port 3080 was empty. That raced with launchd / systemd / Task
Scheduler — which already own dsh web's lifecycle.
v0.2.10 reverses the design: dsh-doctor is a diagnostic and
repair layer that runs BEFORE the platform service relaunches dsh
web. It does not replace launchd.
New flow
- dsh web dies, port 3080 is empty.
- Watchdog reads
~/.dsh/doctor/.doctor-web-label(written at
install time after scanning the platform service registry). - Watchdog runs:
- macOS:
launchctl kickstart -k gui/<uid>/<label> - Linux:
systemctl --user restart <unit> - Win:
schtasks /Run /TN <label>
- macOS:
- The platform service pulls dsh web back up. If dsh web crashed
because of a broken plugin, the watchdog's triage path
(disable-row / safe-mode) has already removed that plugin, so
the relaunch will succeed. - If no platform label is found (naked install), the watchdog
falls back to directcp.spawn('dsh', ['web', ...])for
backward compatibility.
Auto-label detection
auto-install-helper.ts now includes detectDshWebLabel() which
scans the platform service registry for the dsh web label. Filters
out anything containing 'doctor' so the watchdog never kickstarts
itself.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.10- Wait for auto-install to write
.doctor-web-label. Or manually:
cat ~/.dsh/doctor/.doctor-web-labelshould showio.deepseek.dsh. pkill -f 'dsh web'- Watch
~/.dsh/doctor/logs/watchdog.logfor:
no process listening on port 3080 - asking platform service to relaunch
thenlaunchctl kickstart -k gui/<uid>/io.deepseek.dsh issued
94/94 unit tests pass.
v0.2.9 - watchdog respawns dsh web on its own
What changed
Previously: when dsh web died, the watchdog saw SIGTERM at the same
time (because dsh web is a launchd GUI app) and either re-launched
itself in a tight loop, or sat idle waiting for 3 consecutive probe
failures that never came (launchd pulled dsh web back up too fast).
v0.2.9: after 3 consecutive health probe failures, the watchdog now
checks whether anything is listening on port 3080. If the port is
empty, it spawns a fresh dsh web directly via startWeb() -
bypassing the plugin-triage path (theres no plugin to blame yet, the
process itself is gone).
If the port is in use (e.g. launchd already pulled dsh web back up),
the watchdog proceeds to the existing triage path.
How to verify
dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.9launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctorto
regenerate the watchdog with the new code.killthe dsh web process (orpkill -f "dsh web").- Watch
~/.dsh/doctor/logs/watchdog.logfor:
no process listening on port 3080 - assuming dsh web died; respawning
thenrespawned dsh web pid=NNNNN.
94/94 unit tests pass.
v0.2.8 — auto-nudge on LLM failure
What changed
A turn ended with `reason.kind === 'error'` (e.g. dsh-agent got a
402 / "Insufficient balance" from the model provider) used to wait
the full `idleThresholdMs` (3 min) before the session watch tried
to nudge. That defeated unattended operation — the operator had to
type 继续 manually.
v0.2.8 splits the nudge trigger into two independent paths:
- FAILURE nudge: `lastFailure !== null` → nudge immediately.
- IDLE nudge: `turnRunning && (now - lastEventAt) >= idleThresholdMs`.
Both honor `maxNudgesPerSession` + `nudgeCooldownMs`. The watch also
runs the idle check synchronously on `turn/end:error` so the recovery
nudge fires in the same event-loop turn, not 30s later on the next
tick.
How to verify
- `dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.8\`
- Trigger any LLM call that fails with QUOTA / 402 / 429.
- Within 1 second, the dsh web GUI should show a new "继续" user
message injected by dsh-doctor, and the turn should resume. - `dsh_doctor_watch_list` will show `nudgesSent` go from 0 to 1
on that session.
94/94 unit tests pass (up from 92 — added 2 regression tests).
Verified live on dsh 0.1.0-rc.8 + macOS LaunchAgent + dsh web PID 72176.