dsh-autorestart 0.3.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +208 -4
- package/client.js +2764 -82
- package/cordis.patch.yml +19 -0
- package/docs/README.zh.md +122 -7
- package/index.js +1318 -72
- package/lib/checkpoint.js +38 -0
- package/lib/sleep.js +828 -0
- package/locale/en.json +113 -0
- package/locale/zh.json +113 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,11 @@ Before every restart the plugin snapshots the profile, so a restart that leaves
|
|
|
38
38
|
boot is rolled back automatically — see
|
|
39
39
|
[Pre-restart backup and automatic rollback](#pre-restart-backup-and-automatic-rollback).
|
|
40
40
|
|
|
41
|
+
The same "wait until everything is quiet" idea is also available as a **graceful shutdown**: a moon
|
|
42
|
+
button at the very bottom of the left sidebar (right next to Settings) waits for every conversation
|
|
43
|
+
to be idle, counts down on screen (a floating card with a huge number and a small whale) and then
|
|
44
|
+
switches the **real host machine** off — see [Graceful shutdown](#graceful-shutdown-收工关机).
|
|
45
|
+
|
|
41
46
|
Two safety gates protect the restart. The host checks that no conversation is busy, and the detached
|
|
42
47
|
helper checks the same gate over loopback immediately before it kills the process, so a turn that
|
|
43
48
|
starts in between is never killed.
|
|
@@ -50,8 +55,8 @@ what it adds before you install it.
|
|
|
50
55
|
| Kind | What it adds |
|
|
51
56
|
| --- | --- |
|
|
52
57
|
| Agent tools (6) | `continuum_restart`, `continuum_status`, `continuum_cancel_restart`, `continuum_park_goal`, `continuum_recover_now`, `continuum_handoff` |
|
|
53
|
-
| HTTP routes (
|
|
54
|
-
| Client slots (
|
|
58
|
+
| HTTP routes (7) | Under `<healthPath>` (default `/__dsh_autorestart`): `GET /health` (also `?state=1`, `?gate=1`, `?probeIndex=1`), `POST /park`, `POST /recover`, `POST /handoff`, `GET /sleep` and `POST /sleep` (the graceful shutdown — the desktop client and the hosted web client drive the very same route), `GET /announcements` and `POST /announcements` (the one-time new-feature announcement). Loopback or an authenticated browser only |
|
|
59
|
+
| Client slots (6) | `conversation.input.left` — the pending-restart pill (`autorestart-restart-pill`); `sidebar.session.row.leading` — the session status dot (`autorestart-restart-dot`); `conversation.input.dock` — the one-time install question (`autorestart-backup-choice`) and the one-time new-feature announcement (`autorestart-feature-announcement`); `sidebar.footer.action` — the moon button of the graceful shutdown (`autorestart-sleep-button`); `shell.overlay` — its countdown card; `settings.section` — the section "Auto-restart & shut down" in the settings page (`dsh-autorestart`) |
|
|
55
60
|
| System prompt | One "Restart policy" section that tells the agent how to ask for a restart and what the plugin will do |
|
|
56
61
|
| 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 |
|
|
57
62
|
| Profile row | One bundle row, `dsh-autorestart` |
|
|
@@ -67,6 +72,8 @@ what it adds before you install it.
|
|
|
67
72
|
|
|
68
73
|
## Install
|
|
69
74
|
|
|
75
|
+
Current version: **0.5.0**.
|
|
76
|
+
|
|
70
77
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
71
78
|
|
|
72
79
|
```sh
|
|
@@ -141,6 +148,9 @@ show up in the Plugin Manager and in the config editor.
|
|
|
141
148
|
| `backupIncludePlugins` | `true` | Include the installed plugin tree in that snapshot (about 117 MB here, ~3 s). `false` keeps only the configuration files. |
|
|
142
149
|
| `maxBackups` | `2` | How many snapshots are kept; older ones are deleted when a new one is written. |
|
|
143
150
|
| `rollbackTimeoutMs` | `90000` | How long the detached watcher waits for the relaunched host before it restores the snapshot and launches the host again. |
|
|
151
|
+
| `showSleepButton` | `true` | Whether the moon button of the graceful shutdown is shown at the foot of the left sidebar, next to Settings. |
|
|
152
|
+
| `sleepCountdownSeconds` | `20` | How many seconds the graceful shutdown counts down before the machine goes down — a dropdown: `0` / `10` / `20` / `30` / `60` seconds. `0` means "switch off right away" (still waiting for the conversations to be idle). |
|
|
153
|
+
| `sleepCommand` | `''` | A custom shutdown command for the graceful shutdown. Empty = the host is detected automatically; a value replaces the detected command (`cmd.exe /c …` on Windows, `/bin/sh -c …` everywhere else). |
|
|
144
154
|
|
|
145
155
|
The two waits are independent of each other, and both default to two seconds:
|
|
146
156
|
|
|
@@ -195,6 +205,167 @@ The rollback is deliberately conservative: it only runs when the relaunched host
|
|
|
195
205
|
own health route. A slow start inside `rollbackTimeoutMs` is never treated as a failure, and a
|
|
196
206
|
healthy host is never touched.
|
|
197
207
|
|
|
208
|
+
### The one-time install question
|
|
209
|
+
|
|
210
|
+
The first time the plugin is used, the composer dock shows ONE non-modal panel that asks whether the
|
|
211
|
+
pre-restart backup should also snapshot the installed plugin tree:
|
|
212
|
+
|
|
213
|
+
- **"Configuration + installed plugins"** — about 3 seconds and 117 MB more per restart; a plugin
|
|
214
|
+
that breaks the boot can be rolled back too.
|
|
215
|
+
- **"Configuration only"** — instant, about 66 KB; plugin problems cannot be rolled back and need a
|
|
216
|
+
manual reinstall.
|
|
217
|
+
|
|
218
|
+
Both options share the same base: the **configuration** is always backed up. The plugins are an
|
|
219
|
+
addition to it, never a rival of it.
|
|
220
|
+
|
|
221
|
+
- **It is asked exactly once.** The panel is rendered only while
|
|
222
|
+
`localStorage['dsh-autorestart:backup-choice-v1']` is missing; picking an option records the answer and the
|
|
223
|
+
panel never comes back.
|
|
224
|
+
- **Closing the panel counts as answering.** `×` and `Esc` write the very same "already answered" mark as
|
|
225
|
+
picking an option, so that browser is never asked again — and closing keeps the default (configuration *and*
|
|
226
|
+
plugins).
|
|
227
|
+
- **It never blocks you.** The panel is a `role="status"` block in the composer dock: the input box
|
|
228
|
+
stays usable, nothing is modal, and the panel does not disappear on its own. It is keyboard
|
|
229
|
+
reachable and `Esc` closes it.
|
|
230
|
+
- **Where the answer goes.** The host writes it into the profile configuration with the official
|
|
231
|
+
`configEditor` service — the same `backupIncludePlugins` key the settings section edits, so there is
|
|
232
|
+
one source of truth. If that service is unavailable the answer degrades to
|
|
233
|
+
`<DSH_HOME>/storages/dsh-autorestart/install-choice.json` and a warning is logged. The same fallback
|
|
234
|
+
happens in a hosted profile that declares no `dsh-autorestart` row of its own: there is no row for the
|
|
235
|
+
editor to write, so the answer lands in that file instead of the profile. Declare the row (see
|
|
236
|
+
*Hosted `dsh web` deployments* below) to keep the profile configuration as the single source of truth.
|
|
237
|
+
- **How to change your mind later:** Settings → Auto-restart & shut down → `backupIncludePlugins`.
|
|
238
|
+
`continuum_status` always reports the value that is in effect right now
|
|
239
|
+
(`backupIncludePlugins: true | false`).
|
|
240
|
+
|
|
241
|
+
### The one-time new-feature announcement (upgrades)
|
|
242
|
+
|
|
243
|
+
**The gap it closes.** The install question above is asked exactly once — on the *first* install. An
|
|
244
|
+
installation that already existed therefore never hears of a feature that was added later: for an
|
|
245
|
+
upgrade the new moon button would simply not exist. Since **0.5.0** every new feature is announced
|
|
246
|
+
once, in the same composer dock, as a small card:
|
|
247
|
+
|
|
248
|
+
> **New feature: graceful shutdown**
|
|
249
|
+
> There is a new 🌙 "Shut down" button at the very bottom of the left sidebar.
|
|
250
|
+
> Click it → once every conversation is done it counts down **N** seconds and powers the machine off.
|
|
251
|
+
> To change that, go to "Settings → Auto-restart & shut down".
|
|
252
|
+
> **Got it**
|
|
253
|
+
|
|
254
|
+
- **Only after an upgrade.** The host compares the version of the feature with the version it has
|
|
255
|
+
already shown (`seenFeatureVersions` in the checkpoint file). A fresh installation never sees the
|
|
256
|
+
card: the install question already explains the features, and answering it marks the announcement
|
|
257
|
+
as seen.
|
|
258
|
+
- **Once per version, for ever.** "Got it" records the version, the card disappears at once, and it
|
|
259
|
+
never comes back — not with the next restart of the host, and not with the next version of the
|
|
260
|
+
plugin (a *new* version of a feature is announced again, of course).
|
|
261
|
+
- **An unanswered card is not swallowed.** The card stays until it is answered, and it comes back with
|
|
262
|
+
the next start of DSH.
|
|
263
|
+
- **The countdown is read, not written.** The seconds in the card are the ones that are really in
|
|
264
|
+
effect (`sleepCountdownSeconds`), and when the moon button is switched off the card says so
|
|
265
|
+
("The button is currently switched off; you can turn it on in the settings").
|
|
266
|
+
- **Where the state lives.** `<DSH_HOME>/storages/dsh-autorestart/state.json`, next to the checkpoint
|
|
267
|
+
of the plugin. The host serves it on `GET /announcements` (`{ ok, pending: [{ feature, version }] }`)
|
|
268
|
+
and records it on `POST /announcements` (`{ feature }`).
|
|
269
|
+
|
|
270
|
+
## Graceful shutdown (收工关机)
|
|
271
|
+
|
|
272
|
+
**What it does.** One moon button at the very bottom of the left sidebar: press it and the plugin
|
|
273
|
+
waits until **every** conversation is idle, the screen counts down (a huge number and a small blue
|
|
274
|
+
whale, one line of text per second), and then the machine is switched off. The countdown can always
|
|
275
|
+
be cancelled — the card has a button, and the moon button itself becomes a toggle while the shutdown
|
|
276
|
+
is armed. If a conversation starts working again during the countdown, the countdown goes back to
|
|
277
|
+
"waiting" and nothing is switched off.
|
|
278
|
+
|
|
279
|
+
**Where the button is.** The moon button sits at the **very bottom of the left sidebar, right next
|
|
280
|
+
to the Settings row** (`sidebar.footer.action`). That seat is a **root** slot, so the button is
|
|
281
|
+
there even when no conversation is open — on any page. When the sidebar is collapsed into its narrow
|
|
282
|
+
rail, the button shows its icon only (the tooltip and the accessible name still say what it does),
|
|
283
|
+
so the rail keeps its width.
|
|
284
|
+
|
|
285
|
+
**The countdown does not cover the interface.** The countdown is a **floating card** in the corner,
|
|
286
|
+
not a full-screen mask: it never dims the page, never locks the scrolling and never swallows a
|
|
287
|
+
click. You can keep reading the conversation, keep scrolling and keep switching conversations while
|
|
288
|
+
it counts down; only the card's own cancel button takes clicks. Switching a conversation or a page
|
|
289
|
+
does not interrupt the countdown either — the seconds come from the host, not from the view that
|
|
290
|
+
happens to be open.
|
|
291
|
+
|
|
292
|
+
**What gets switched off: the real host.** Not a subsystem — the whole machine:
|
|
293
|
+
|
|
294
|
+
| Host | Command |
|
|
295
|
+
| --- | --- |
|
|
296
|
+
| Windows | `shutdown.exe /s /f /t 0` |
|
|
297
|
+
| WSL (the DSH itself runs inside a distribution) | `/mnt/c/Windows/System32/shutdown.exe /s /f /t 0` — the Windows binary through the mounted `C:` drive, so **Windows itself** goes down. The WSL subsystem is never shut down. |
|
|
298
|
+
| A real Linux host | `systemctl poweroff` (fallback `shutdown -h now`) |
|
|
299
|
+
| macOS | `osascript -e 'tell application "System Events" to shut down'` |
|
|
300
|
+
| Anything else | **Nothing is executed at all.** The button stays hidden and the route reports `phase: "unsupported"`. |
|
|
301
|
+
|
|
302
|
+
A WSL host is recognised through `WSL_DISTRO_NAME`, `WSL_INTEROP`, a `/proc/version` that mentions
|
|
303
|
+
Microsoft, or the mounted Windows binary. The countdown belongs to the plugin, so the operating
|
|
304
|
+
system is asked to shut down immediately (`/t 0`) instead of counting down a second time.
|
|
305
|
+
|
|
306
|
+
**Configuration.** `showSleepButton` (default `true`) decides whether the button is shown,
|
|
307
|
+
`sleepCountdownSeconds` (a **dropdown**: `0` / `10` / `20` / `30` / `60` seconds, default `20`) how
|
|
308
|
+
long the countdown runs, and `sleepCommand` (default `''`) replaces the detected command with one of
|
|
309
|
+
your own — empty means "detect the host automatically". `0` means **no countdown at all**: the machine
|
|
310
|
+
is switched off as soon as every conversation is idle (a busy conversation still blocks it), and no
|
|
311
|
+
countdown card appears. A profile that still carries a value the dropdown does not offer (`15`,
|
|
312
|
+
`45`, …) is snapped onto the nearest offered one instead of breaking.
|
|
313
|
+
|
|
314
|
+
**One click is all it takes.** The moon button arms the shutdown: the host waits for every busy
|
|
315
|
+
conversation, counts down **20 seconds** in a floating card (it never covers the interface), and then
|
|
316
|
+
switches the machine off. Clicking the button again — or the cancel button on the card — takes it
|
|
317
|
+
back. Nothing else is asked of the user: no confirmation dialog, no second click.
|
|
318
|
+
|
|
319
|
+
**The desktop client and the hosted web client behave exactly the same.** There is no ticket, no
|
|
320
|
+
extra header and no hidden mode: `POST /__dsh_autorestart/sleep` takes nothing but the action, and
|
|
321
|
+
the same route serves both. A script (or an agent) can therefore drive the shutdown as well:
|
|
322
|
+
|
|
323
|
+
```bash
|
|
324
|
+
# arm it (the machine counts down and goes off)
|
|
325
|
+
curl -s -X POST http://127.0.0.1:<port>/__dsh_autorestart/sleep \
|
|
326
|
+
-H 'content-type: application/json' -d '{"action":"arm"}'
|
|
327
|
+
# take it back
|
|
328
|
+
curl -s -X POST http://127.0.0.1:<port>/__dsh_autorestart/sleep \
|
|
329
|
+
-H 'content-type: application/json' -d '{"action":"cancel"}'
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**The one-time install question.** The first time the plugin is used, the composer dock asks once
|
|
333
|
+
more whether the button should stay. The answer is written into the profile configuration
|
|
334
|
+
(`showSleepButton`) through the same `configEditor` service the backup question uses, so the
|
|
335
|
+
settings section and the prompt always show the same value.
|
|
336
|
+
|
|
337
|
+
**The settings section.** *Settings → Auto-restart & shut down* is a section of its own in the
|
|
338
|
+
settings sidebar (right below the Agent presets). It carries the three settings that matter:
|
|
339
|
+
|
|
340
|
+
- **Countdown seconds** — the dropdown `0` / `10` / `20` / `30` / `60`. `0` means "no countdown":
|
|
341
|
+
the machine is switched off as soon as every conversation is idle.
|
|
342
|
+
- **Backup before a restart** (`backupOnRestart`) — whether the pre-restart snapshot is taken at all.
|
|
343
|
+
- **Include the installed plugins in the backup** (`backupIncludePlugins`) — whether that snapshot
|
|
344
|
+
also contains the plugin directory of the profile. It is greyed out when the backup itself is off.
|
|
345
|
+
|
|
346
|
+
A change is written into the profile configuration at once (through the plugin's own choice route)
|
|
347
|
+
and is read back from the host, so the section never shows a value the host did not take. A malformed
|
|
348
|
+
value is refused with a **400** and **nothing** is written.
|
|
349
|
+
|
|
350
|
+
**Trying it without switching anything off.** Set `DSH_AUTORESTART_SLEEP_DRYRUN=1` in the host
|
|
351
|
+
environment: the plugin then reports the command it *would* run and never runs it.
|
|
352
|
+
|
|
353
|
+
**The machine-readable state.** `GET <healthPath 前缀>/sleep` (default
|
|
354
|
+
`/__dsh_autorestart/sleep`) answers with the phase, the countdown and the host that is about to be
|
|
355
|
+
closed. A few of its fields are meant to be read by code rather than by a human: `host.kind` is the
|
|
356
|
+
kind of machine (`windows`, `wsl`, `linux`, `macos`, `unsupported`, or `custom` when the platform
|
|
357
|
+
could not be recognised but a `sleepCommand` was configured); `host.suspect` is `true` when the host
|
|
358
|
+
was only recognised as WSL because the Windows drive is mounted — a hint, not a proof;
|
|
359
|
+
`lastRun.dryRun` marks the last run as a rehearsal that switched nothing off; and
|
|
360
|
+
`backupOnRestart` / `backupIncludePlugins` report the effective scope of the pre-restart backup.
|
|
361
|
+
|
|
362
|
+
**When the shutdown command does not start.** The host waits for the real answer of the process. If
|
|
363
|
+
the command cannot start (missing binary, denied permission), the shutdown does **not** pretend it
|
|
364
|
+
worked: the phase goes back to `idle`, the reason is reported ("the shutdown command did not start
|
|
365
|
+
(…), the computer is still on, press the moon button again"), and the machine stays on. On screen
|
|
366
|
+
the "shutting down" card never locks the interface: after at most eight seconds it offers a button
|
|
367
|
+
that closes the card.
|
|
368
|
+
|
|
198
369
|
## Requirements and limits
|
|
199
370
|
|
|
200
371
|
- **Windows desktop.** `restartMode: app` restarts the DSH desktop app on Windows. Any other
|
|
@@ -217,6 +388,37 @@ healthy host is never touched.
|
|
|
217
388
|
- **The restart closes and reopens the desktop window.** It is a full app restart, not a hot reload.
|
|
218
389
|
- **The resumed round consumes API tokens**, exactly like typing "continue" by hand.
|
|
219
390
|
|
|
391
|
+
### Hosted `dsh web` deployments
|
|
392
|
+
|
|
393
|
+
| Deployment | `restartMode` | How the host is restarted |
|
|
394
|
+
| --- | --- | --- |
|
|
395
|
+
| 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 |
|
|
396
|
+
| A hosted `dsh web` deployment | `command` | There is no "restart the app" in a web deployment — you must supply the command yourself |
|
|
397
|
+
|
|
398
|
+
Minimal configuration for a hosted `dsh web` (it **replaces** the whole `config` block, it is not merged):
|
|
399
|
+
|
|
400
|
+
```yaml
|
|
401
|
+
- id: dsh-autorestart
|
|
402
|
+
config:
|
|
403
|
+
restartMode: command
|
|
404
|
+
restartCommand: systemctl --user restart dsh-web
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
`restartCommand` may use the placeholders `{pid}` (this host), `{ppid}` (its parent) and `{app}` (the desktop
|
|
408
|
+
app path).
|
|
409
|
+
|
|
410
|
+
**Without that configuration the restart fails, and it says so.** A hosted host is not the desktop app, so
|
|
411
|
+
the plugin has no way to relaunch it: the log gets
|
|
412
|
+
`no restart method available (not the desktop app and restartCommand is empty); leaving the checkpoint in place`,
|
|
413
|
+
the pending restart is rolled back (one retry with a backoff, then it is cancelled instead of retrying
|
|
414
|
+
forever), and nothing is relaunched — the checkpoint simply stays on disk, so the session is picked up again
|
|
415
|
+
when the host is started by hand. `continuum_status` keeps reporting the pending state until it is cancelled.
|
|
416
|
+
|
|
417
|
+
The same profile row is what the one-time install question writes into: a hosted profile that declares no
|
|
418
|
+
`dsh-autorestart` row has no row for the `configEditor` service to edit, so the answer falls back to
|
|
419
|
+
`<DSH_HOME>/storages/dsh-autorestart/install-choice.json` instead of the profile configuration. Declaring the
|
|
420
|
+
row keeps the profile (and therefore the settings section) as the single source of truth.
|
|
421
|
+
|
|
220
422
|
## Verify
|
|
221
423
|
|
|
222
424
|
The health route tells you whether the plugin is alive and what it has done:
|
|
@@ -224,7 +426,7 @@ The health route tells you whether the plugin is alive and what it has done:
|
|
|
224
426
|
```sh
|
|
225
427
|
curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
|
|
226
428
|
# {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
|
|
227
|
-
# "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":
|
|
429
|
+
# "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":1,…}
|
|
228
430
|
```
|
|
229
431
|
|
|
230
432
|
`bootId` and `pid` change with every new process, so a restart is visible right away.
|
|
@@ -237,9 +439,11 @@ curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
|
|
|
237
439
|
anything.
|
|
238
440
|
- `POST /__dsh_autorestart/park` disarms a goal, `POST /__dsh_autorestart/recover` re-runs the
|
|
239
441
|
recovery immediately. Both use the same authentication rules as the health route.
|
|
442
|
+
- `GET /__dsh_autorestart/sleep` reports the state of the graceful shutdown, `POST
|
|
443
|
+
/__dsh_autorestart/sleep` arms (`{ "action": "arm" }`) or cancels (`{ "action": "cancel" }`) it.
|
|
240
444
|
- Inside a session, `continuum_status` reports the same lifecycle.
|
|
241
445
|
|
|
242
|
-
The plugin ships its own test suite: **
|
|
446
|
+
The plugin ships its own test suite: **250 test cases** in `plugins/dsh-autorestart/test/`, all
|
|
243
447
|
green. Run it with the bundled Node, from the workspace root:
|
|
244
448
|
|
|
245
449
|
```sh
|