Skip to content

Releases

v0.2.17 - unattended mode: nudge on any non-completed turn

Choose a tag to compare

@d86e d86e released this 30 Aug 00:54

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:

  1. Sets a synthetic lastFailure on the session
    (e.g. TURN_ABORTED, TURN_INTERRUPTED, TURN_MAX-TOKENS,
    TURN_BLOCKED).
  2. Triggers runIdleCheck() synchronously, so the failure-nudge
    fires within milliseconds — not after idleThresholdMs.

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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.17
  2. Restart dsh web (or wait for the next auto-reload).
  3. Trigger any non-completion: kill a long-running bash command,
    trigger a 429 / 5xx, or simulate a max-tokens truncation.
  4. 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

Choose a tag to compare

@d86e d86e released this 28 Aug 09:51

What changed

Two common failure shapes were aborting the agent loop without the
doctor re-injecting a continuation:

  1. PI_AI_ERROR / Provider returned error — upstream API hiccup.
    Was classified as 'business' (the default fallback) so the
    doctor did NOT defer a followup.

  2. 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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.16
  2. Trigger a real PI_AI_ERROR (or stage a fake "requires reading X
    first" error) and watch ~/.dsh/doctor/logs/doctor.log for:
    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

Choose a tag to compare

@d86e d86e released this 28 Aug 07:28

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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.15
  2. launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor
  3. Confirm watchdog reloaded:
    grep "id: '" ~/.dsh/doctor/watchdog.js | wc -l → should be 12
  4. Stage a fake bad plugin that says "Requires Node ^999":
    ~/.dsh/doctor/.doctor-user-action-required will 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

Choose a tag to compare

@d86e d86e released this 28 Aug 07:12

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. corrupt cordis.patch.yml) and waits for the platform
    service to restart dsh web.

How to verify

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.14
  2. launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor
  3. 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
  4. cat ~/.dsh/doctor/.doctor-user-action-required will show the
    doctor staged a user-action-required entry.

100/100 unit tests pass.

v0.2.13 - 2s probe, first-failure triage

Choose a tag to compare

@d86e d86e released this 28 Aug 06:31

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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.13
  2. launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor
  3. Tail the log:
    tail -f ~/.dsh/doctor/logs/watchdog.log
  4. pkill -f 'dsh web'
  5. 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
  6. 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

Choose a tag to compare

@d86e d86e released this 28 Aug 06:25

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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.12
  2. launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor
  3. To simulate a plugin crash: stage a disable for a real plugin
    (e.g. dsh-doctor-disable-dsh-vision), then pkill -f 'dsh web'.
  4. 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)

Choose a tag to compare

@d86e d86e released this 28 Aug 06:23

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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.11
  2. launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor to
    regenerate the watchdog with the new code.
  3. pkill -f 'dsh web'
  4. Within 30s, ~/.dsh/doctor/logs/watchdog.log should show:
    first probe failure AND port 3080 is empty - dsh web process is gone
    then launchctl 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

Choose a tag to compare

@d86e d86e released this 28 Aug 06:20

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

  1. dsh web dies, port 3080 is empty.
  2. Watchdog reads ~/.dsh/doctor/.doctor-web-label (written at
    install time after scanning the platform service registry).
  3. Watchdog runs:
    • macOS: launchctl kickstart -k gui/<uid>/<label>
    • Linux: systemctl --user restart <unit>
    • Win: schtasks /Run /TN <label>
  4. 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.
  5. If no platform label is found (naked install), the watchdog
    falls back to direct cp.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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.10
  2. Wait for auto-install to write .doctor-web-label. Or manually:
    cat ~/.dsh/doctor/.doctor-web-label should show io.deepseek.dsh.
  3. pkill -f 'dsh web'
  4. Watch ~/.dsh/doctor/logs/watchdog.log for:
    no process listening on port 3080 - asking platform service to relaunch
    then launchctl kickstart -k gui/<uid>/io.deepseek.dsh issued

94/94 unit tests pass.

v0.2.9 - watchdog respawns dsh web on its own

Choose a tag to compare

@d86e d86e released this 28 Aug 06:07

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

  1. dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.9
  2. launchctl kickstart -k gui/501/com.deepseek-ai.dsh-doctor to
    regenerate the watchdog with the new code.
  3. kill the dsh web process (or pkill -f "dsh web").
  4. Watch ~/.dsh/doctor/logs/watchdog.log for:
    no process listening on port 3080 - assuming dsh web died; respawning
    then respawned dsh web pid=NNNNN.

94/94 unit tests pass.

v0.2.8 — auto-nudge on LLM failure

Choose a tag to compare

@d86e d86e released this 28 Aug 05:55

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

  1. `dsh plugin --profile web add https://github.com/d86e/dsh-doctor.git#v0.2.8\`
  2. Trigger any LLM call that fails with QUOTA / 402 / 429.
  3. Within 1 second, the dsh web GUI should show a new "继续" user
    message injected by dsh-doctor, and the turn should resume.
  4. `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.