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 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 (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 |
54
- | Client slots (2) | `conversation.input.left` — the pending-restart pill (`autorestart-restart-pill`); `sidebar.session.row.leading` — the session status dot (`autorestart-restart-dot`) |
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":2,…}
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: **129 test cases** in `plugins/dsh-autorestart/test/`, all
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