dsh-autorestart
DSH 插件:重启不打断工作——等所有对话空闲后优雅重启,重启后自动接着干。AI 生成、未人工审核,谨慎使用。
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 22, 2026
- Updated
- Oct 26, 2026
Introduction
dsh-autorestart
Restart DeepSeek Harness without losing the work that is currently running.
Chinese version: README.zh.md
What it does
dsh-autorestart turns a DeepSeek Harness restart into a resumable event instead of a dead end.
An agent can request a graceful restart. The plugin waits until every conversation is idle and the host has been quiet for the settle window, then it restarts the host. Once the host is back, the interrupted sessions resume automatically: a session that owns a durable goal gets that goal re-armed so the goal round driver starts the next round, and a session without a goal receives one continuation message that asks it to pick up where it left off.
Several sessions can request a restart at the same time. Those requests are coalesced into one restart, and every requester is woken afterwards — not only the last one.
A goal-driven session would otherwise keep the restart waiting forever, because its goal starts the next round as soon as the agent goes idle. The plugin therefore disarms such a goal before the restart: the round that is running right now still finishes normally and nothing is lost, but no new round starts, so the session goes idle and the restart can proceed. The goal is re-armed after the restart and continues.
The browser shows the state: a compact "pending restart" pill on the left side of the input toolbar and a small dot next to the session title in the sidebar. The pill's text reports how many conversations are still in the way (counted once per conversation, and the tooltip explains that a conversation with queued-but-not-started work counts too); it does not open a details panel. The dot breathes when the restart is about to fire.
Before every restart the plugin snapshots the profile, so a restart that leaves the host unable to boot is rolled back automatically — see Pre-restart backup and automatic rollback.
Two safety gates protect the restart. The host checks that no conversation is busy, and the detached helper checks the same gate over loopback immediately before it kills the process, so a turn that starts in between is never killed.
What it occupies in DSH
Nothing here is hidden: this is the complete list of where the plugin attaches itself, so you can see what it adds before you install it.
| Kind | What it adds |
|---|---|
| Agent tools (6) | continuum_restart, continuum_status, continuum_cancel_restart, continuum_park_goal, continuum_recover_now, continuum_handoff |
| HTTP routes (4) | Under <healthPath> (default /__dsh_autorestart): GET /health (also ?state=1, ?gate=1, ?probeIndex=1), POST /park, POST /recover, POST /handoff. Loopback or an authenticated browser only |
| Client slots (2) | conversation.input.left — the pending-restart pill (autorestart-restart-pill); sidebar.session.row.leading — the session status dot (autorestart-restart-dot) |
| System prompt | One "Restart policy" section that tells the agent how to ask for a restart and what the plugin will do |
| Harness hooks | agent/status (tracks which conversations are busy), tools/pre-execute (blocks a shell command that would kill or relaunch DSH by hand), and the boot pass that resumes interrupted sessions |
| Profile row | One bundle row, dsh-autorestart |
| Writes | Only under <DSH_HOME>/storages/dsh-autorestart/ — state.json (checkpoint), backups/ (pre-restart snapshots), restart-<gen>.cmd / restart-<gen>-watch.ps1 (the detached helper), last-rollback.json (rollback report). The only time it writes into the profile itself is a rollback, which copies that snapshot's cordis.patch.yml, package.json, pnpm-lock.yaml and node_modules back |
Why
| Situation | Without it | With it |
|---|---|---|
| An agent is running a long task and DSH is restarted or crashes | The round is lost and you have to type "continue" by hand | The new process resumes the session and injects a continuation message, so the work continues without you |
An agent owns a durable goal (create_goal) that should survive a restart | The goal activation lives in the process, so after the restart it is disarmed and the automatic continuation stops | The plugin re-arms the goal at boot and the goal round driver starts the next round right away |
| An agent wants to install or upgrade a plugin, but a restart would cut the work short | You either wait or take the risk | Call continuum_restart: it waits until every agent is idle and the session continues by itself afterwards |
Install
From npm — the package is published under the name dsh-autorestart:
dsh plugin install dsh-autorestart
From source:
git clone https://github.com/Eason-22/dsh-autorestart.git
cd dsh-autorestart
dsh plugin --profile desktop add /absolute/path/to/dsh-autorestart
Inside DSH the equivalent is the plugin_manager tool:
plugin_manager install_bundle target = <absolute path to the package>
Installing into a profile adds a row with the id dsh-autorestart to the plugin list. The host half
of the plugin takes effect immediately, but the client half (the pending restart pill and the
sidebar dot) is loaded by the browser, so refresh the page or restart DSH once after the install.
Usage
The plugin registers six tools for the agent. All of them are also visible to you in the session, so you can call them yourself.
| Tool | What it does | Parameters |
|---|---|---|
continuum_restart | Requests a graceful restart. The restart waits until every conversation is idle and the host has been quiet for settleMs, then the app is relaunched and this session resumes automatically. | reason (string, recorded in the checkpoint and the log), resume (boolean, default true), defer_seconds (number, extra seconds to wait before the first idle check), parkGoals (boolean, default true) — disarm a goal-driven session before the restart |
continuum_status | Read-only snapshot of the lifecycle: whether a restart is pending, which sessions were checkpointed, why each blocking session blocks, the goals this plugin disarmed, the last recovery action per session, the handoffs, the state file path and the detected restart environment. | include_sessions (boolean, default true) — include the per-session checkpoint entries |
continuum_cancel_restart | Cancels a pending restart and releases the continuation request of every requester. Does nothing when no restart is pending. | none |
continuum_park_goal | Disarms the goal of a session so that it no longer blocks the restart. The current round still finishes and the goal is re-armed after the restart. | sessionId (string, optional) — defaults to the calling session |
continuum_recover_now | Re-runs the recovery immediately instead of waiting for the next restart, and reports the action and the reason for each session. | sessionId (string, optional) — defaults to all sessions pending recovery |
continuum_handoff | Hands a session whose conversation exceeded the model context window over to a fresh session, carrying the goal objective, the recorded reasons and a bounded tail of the conversation. | sessionId (string, optional) — defaults to the calling session, maxChars (number, default 20000) — character budget for the conversation tail |
A goal is an enhancement, not a requirement: without one, the plugin injects a single continuation message instead of re-arming a goal.
Configuration
Every key has a default, written in the insert row of cordis.patch.yml. To override a key, add
an - id: dsh-autorestart row with the same key to the cordis.patch.yml of the profile at
~/.dsh/profiles/<profile>/cordis.patch.yml (it replaces the whole config, it is not merged). The
plugin also exports a real schemastery Config schema, so the keys, their types and their defaults
show up in the Plugin Manager and in the config editor.
| Key | Default | Meaning |
|---|---|---|
enabled | true | Turns the whole plugin off. |
stateFile | '' | Path of the checkpoint file. Empty means <DSH_HOME>/storages/dsh-autorestart/state.json. |
resumeOnBoot | true | Whether interrupted sessions are resumed automatically at boot. |
recoveryDelayMs | 6000 | How long to wait after boot before resuming, so the session store and the model are ready. |
maxSessions | 4 | Maximum number of sessions resumed per boot. |
maxResumeAttempts | 2 | Maximum resume attempts per session, counted across boots, so a bad session cannot loop forever. |
deferPollMs | 3000 | Poll interval while waiting for the conversations to go idle. |
deferSoftMs | 300000 | Soft deadline: warn when the conversations are still busy after this long. |
deferHardMs | 900000 | Hard deadline: warn after this long. The restart is only forced when forceRestartWhenBusy is enabled. |
deferPolicy | auto | auto = after the hard deadline, follow forceRestartWhenBusy (by default keep waiting). wait = wait forever. |
forceRestartWhenBusy | false | Whether a restart may be forced while a conversation is still running. The default false never interrupts a session: the plugin waits until the other conversations finish. |
restartMode | app | app = restart the DeepSeek Harness desktop app. command = run restartCommand. |
restartCommand | '' | Custom restart command. Supports the {pid}, {ppid} and {app} placeholders. |
relaunchDelaySeconds | 2 | How long the detached helper waits before it kills the app (ping -n 3, roughly 2 seconds). |
settleMs | 2000 | Quiet window in milliseconds: the host must stay silent this long after the last conversation activity before a restart may start. |
autoReloadPage | true | dsh web: reload the page automatically when the host boot token changes. |
blockManualRestart | true | Hard-block an agent that tries to kill or restart DSH with a shell command, and point it at continuum_restart instead. |
healthPath | /__dsh_autorestart/health | Health check route. Returns the boot id, the pid, the phase and the self-check flags. |
continuationText | '' | Overrides the default continuation prompt text. |
backupOnRestart | true | Snapshot the profile before every restart, so a restart that leaves the host unable to boot can be rolled back. |
backupIncludePlugins | true | Include the installed plugin tree in that snapshot (about 117 MB here, ~3 s). false keeps only the configuration files. |
maxBackups | 2 | How many snapshots are kept; older ones are deleted when a new one is written. |
rollbackTimeoutMs | 90000 | How long the detached watcher waits for the relaunched host before it restores the snapshot and launches the host again. |
The two waits are independent of each other, and both default to two seconds:
settleMs(2000ms) is the quiet window after the last conversation activity during which the host has to stay silent before a restart is allowed to start.relaunchDelaySeconds(2seconds) is how long the detached helper waits before it kills the app.
Both can be overridden separately in the profile config. A larger settleMs is the more
conservative choice, because it is less likely to cut a session short that has just started.
Platform note
restartMode: app only applies to the DeepSeek Harness desktop app. A hosted dsh web
deployment has no desktop app to relaunch, so it must set restartMode: command and provide a
restart command of its own:
- id: dsh-autorestart
config:
restartMode: command
restartCommand: systemctl --user restart dsh-web
Remember that this replaces the whole config instead of merging into it.
Pre-restart backup and automatic rollback
A restart is only safe if the host comes back. Before every restart the plugin snapshots the profile it was started from, so a restart that leaves the host unable to boot can be undone automatically.
- What is backed up:
cordis.patch.yml,package.jsonandpnpm-lock.yaml, plus — unlessbackupIncludePluginsis turned off — the installed plugin tree (node_modules). Snapshots live in<storage>/dsh-autorestart/backups/<timestamp>/with amanifest.jsonnext to them. On a profile like this one the full snapshot measures about 117 MB and takes under three seconds. - How many are kept: the newest
maxBackupssnapshots (default2); older ones are deleted when a new one is written. - How a failed boot is detected: the detached helper that kills and relaunches the host also runs
a small PowerShell watcher. It polls the health route for
rollbackTimeoutMs(default90000). A healthy answer ends the watch and nothing is touched. - What happens when the host never answers: the watcher copies the snapshot back over the profile
(configuration files, and the plugin tree when the snapshot holds one), writes
last-rollback.jsonnext to the checkpoint — including the tail of the newest crash log written after this restart — and launches the host again with the restored configuration. - How you learn about it: the next boot reports it.
continuum_statusgains alastRollbackobject (at,waitedMs,rolledBack,backupDir,crashLog,crashTail), and the next restart clears the report so it is never mistaken for a fresh one. - Turning it off:
backupOnRestart: falseskips the snapshot entirely (no rollback point, and no watcher), which is what a user who keeps their own backups may prefer.backupIncludePlugins: falsekeeps the snapshot but limits it to the configuration files.
The rollback is deliberately conservative: it only runs when the relaunched host never answers its
own health route. A slow start inside rollbackTimeoutMs is never treated as a failure, and a
healthy host is never touched.
The one-time install question
The first time the plugin is used, the composer dock shows ONE non-modal panel that asks whether the pre-restart backup should also snapshot the installed plugin tree:
- "Configuration + installed plugins" — about 3 seconds and 117 MB more per restart; a plugin that breaks the boot can be rolled back too.
- "Configuration only" — instant, about 66 KB; plugin problems cannot be rolled back and need a manual reinstall.
Both options share the same base: the configuration is always backed up. The plugins are an addition to it, never a rival of it.
- It is asked exactly once. The panel is rendered only while
localStorage['dsh-autorestart:backup-choice-v1']is missing; picking an option records the answer and the panel never comes back. - Closing the panel counts as answering.
×andEscwrite the very same "already answered" mark as picking an option, so that browser is never asked again — and closing keeps the default (configuration and plugins). - It never blocks you. The panel is a
role="status"block in the composer dock: the input box stays usable, nothing is modal, and the panel does not disappear on its own. It is keyboard reachable andEsccloses it. - Where the answer goes. The host writes it into the profile configuration with the official
configEditorservice — the samebackupIncludePluginskey the settings page edits, so there is one source of truth. If that service is unavailable the answer degrades to<DSH_HOME>/storages/dsh-autorestart/install-choice.jsonand a warning is logged. The same fallback happens in a hosted profile that declares nodsh-autorestartrow of its own: there is no row for the editor to write, so the answer lands in that file instead of the profile. Declare the row (see Hosteddsh webdeployments below) to keep the profile configuration as the single source of truth. - How to change your mind later: Settings → Plugins → dsh-autorestart →
backupIncludePlugins.continuum_statusalways reports the value that is in effect right now (backupIncludePlugins: true | false).
Requirements and limits
- Windows desktop.
restartMode: apprestarts the DSH desktop app on Windows. Any other platform, and every hosteddsh webdeployment, has to userestartMode: commandwith its own restart command. - The manual-restart guard is pattern matching, not a sandbox. It looks at the command text for kill and restart verbs together with DSH process names or the pid of this host. Encoded commands, killing by image name, and routes around the guard — a GUI menu, an external script, a system reboot — are not covered. It exists to stop an agent from casually killing itself, not to contain a hostile command.
- A session past the model context window cannot continue. Once a conversation has grown beyond
the model context window, the provider rejects every request with
invalid_request_error, and the host's own compaction cannot rescue it because the compaction request needs the same oversized context. Hand such a session over withcontinuum_handoff; the plugin deliberately refuses to auto-resume it. - A hosted deployment must let the browser read the state route. The pending restart pill reads
GET <healthPath>?state=1. The route accepts loopback callers and requests that the host connection service considers authenticated browsers. A hosted deployment that blocks it will never show the pill. - The restart closes and reopens the desktop window. It is a full app restart, not a hot reload.
- The resumed round consumes API tokens, exactly like typing "continue" by hand.
Hosted dsh web deployments
| Deployment | restartMode | How the host is restarted |
|---|---|---|
| DeepSeek Harness desktop app (Windows) | app (the default) | The host's own app restart: the old process tree is killed and the desktop app is relaunched |
A hosted dsh web deployment | command | There is no "restart the app" in a web deployment — you must supply the command yourself |
Minimal configuration for a hosted dsh web (it replaces the whole config block, it is not merged):
- id: dsh-autorestart
config:
restartMode: command
restartCommand: systemctl --user restart dsh-web
restartCommand may use the placeholders {pid} (this host), {ppid} (its parent) and {app} (the desktop
app path).
Without that configuration the restart fails, and it says so. A hosted host is not the desktop app, so
the plugin has no way to relaunch it: the log gets
no restart method available (not the desktop app and restartCommand is empty); leaving the checkpoint in place,
the pending restart is rolled back (one retry with a backoff, then it is cancelled instead of retrying
forever), and nothing is relaunched — the checkpoint simply stays on disk, so the session is picked up again
when the host is started by hand. continuum_status keeps reporting the pending state until it is cancelled.
The same profile row is what the one-time install question writes into: a hosted profile that declares no
dsh-autorestart row has no row for the configEditor service to edit, so the answer falls back to
<DSH_HOME>/storages/dsh-autorestart/install-choice.json instead of the profile configuration. Declaring the
row keeps the profile (and therefore the settings page) as the single source of truth.
Verify
The health route tells you whether the plugin is alive and what it has done:
curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
# {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
# "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":2,…}
bootId and pid change with every new process, so a restart is visible right away.
?state=1returns the pending restart state:pendingRestart,waiters,blockedBy,allowRestart,quietForMs,settleMs,parkedGoals,recoveryandhandoffs.?probeIndex=1applies the page heartbeat injection to a probe HTML page, so the client half can be checked without an authenticated page.?gate=1returns the live idle gate verdict that the detached helper uses before it kills anything.POST /__dsh_autorestart/parkdisarms a goal,POST /__dsh_autorestart/recoverre-runs the recovery immediately. Both use the same authentication rules as the health route.- Inside a session,
continuum_statusreports the same lifecycle.
The plugin ships its own test suite: 129 test cases in plugins/dsh-autorestart/test/, all
green. Run it with the bundled Node, from the workspace root:
node --test "plugins/dsh-autorestart/test/*.test.mjs"
Node 24 treats a positional argument as a glob, so keep the quotes. The same suite also runs with
a bare node --test from the package directory. Passing the directory itself (node --test test/) does not work: the argument is read as a glob, and a glob does not match directories.
Screenshots
![]()



License
MIT — see LICENSE.
Submitting to the curated plugin list
The curated list lives in the awesome-dsh-plugin/awesome-dsh-plugin repository, and one pull
request that adds a single file is the whole submission: data/plugins/<owner>__<repo>.yml. For
this plugin that file is data/plugins/Eason-22__dsh-autorestart.yml:
url: https://github.com/Eason-22/dsh-autorestart
name: Eason-22/dsh-autorestart
category: session
description:
en: 'DeepSeek Harness plugin: restart without losing work, with an idle-aware graceful restart and automatic session resume after the host comes back.'
zh: 'DeepSeek Harness 插件:重启不丢工作——等所有对话空闲后优雅重启,重启后自动接续被打断的会话。'
icon: https://raw.githubusercontent.com/Eason-22/dsh-autorestart/main/assets/icon-180.png
The list's CI checks that the repository declares a dsh.bundle manifest, that the repository is
at least one day old (the check re-runs by itself, so no need to resubmit), that the repository
exists and is not archived, and that a pull request adds at most three entries. Descriptions are
read as claims about the plugin and are checked against the code, so keep them factual and free of
marketing. The marketplace icon must be a GitHub-hosted raster image (png, jpg, jpeg or webp; SVG
is refused) — this repository ships assets/icon-180.png — and the optional screenshots.json next
to package.json lets the storefront show the images listed in assets/. The two READMEs of the
list are generated from the entry files; do not edit them by hand.
AI-generated notice
This plugin was generated by an AI and has not been reviewed by a human. It restarts your DSH process and writes local state files, so it can interrupt a running session. Try it in a test profile first, evaluate the risk yourself, and only then use it in your daily profile.