dsh-autorestart 0.3.6 → 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
 
@@ -218,16 +228,144 @@ addition to it, never a rival of it.
218
228
  stays usable, nothing is modal, and the panel does not disappear on its own. It is keyboard
219
229
  reachable and `Esc` closes it.
220
230
  - **Where the answer goes.** The host writes it into the profile configuration with the official
221
- `configEditor` service — the same `backupIncludePlugins` key the settings page edits, so there is
231
+ `configEditor` service — the same `backupIncludePlugins` key the settings section edits, so there is
222
232
  one source of truth. If that service is unavailable the answer degrades to
223
233
  `<DSH_HOME>/storages/dsh-autorestart/install-choice.json` and a warning is logged. The same fallback
224
234
  happens in a hosted profile that declares no `dsh-autorestart` row of its own: there is no row for the
225
235
  editor to write, so the answer lands in that file instead of the profile. Declare the row (see
226
236
  *Hosted `dsh web` deployments* below) to keep the profile configuration as the single source of truth.
227
- - **How to change your mind later:** Settings → Plugins → dsh-autorestart → `backupIncludePlugins`.
237
+ - **How to change your mind later:** Settings → Auto-restart & shut down → `backupIncludePlugins`.
228
238
  `continuum_status` always reports the value that is in effect right now
229
239
  (`backupIncludePlugins: true | false`).
230
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
+
231
369
  ## Requirements and limits
232
370
 
233
371
  - **Windows desktop.** `restartMode: app` restarts the DSH desktop app on Windows. Any other
@@ -279,7 +417,7 @@ when the host is started by hand. `continuum_status` keeps reporting the pending
279
417
  The same profile row is what the one-time install question writes into: a hosted profile that declares no
280
418
  `dsh-autorestart` row has no row for the `configEditor` service to edit, so the answer falls back to
281
419
  `<DSH_HOME>/storages/dsh-autorestart/install-choice.json` instead of the profile configuration. Declaring the
282
- row keeps the profile (and therefore the settings page) as the single source of truth.
420
+ row keeps the profile (and therefore the settings section) as the single source of truth.
283
421
 
284
422
  ## Verify
285
423
 
@@ -288,7 +426,7 @@ The health route tells you whether the plugin is alive and what it has done:
288
426
  ```sh
289
427
  curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
290
428
  # {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
291
- # "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":2,…}
429
+ # "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":1,…}
292
430
  ```
293
431
 
294
432
  `bootId` and `pid` change with every new process, so a restart is visible right away.
@@ -301,9 +439,11 @@ curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
301
439
  anything.
302
440
  - `POST /__dsh_autorestart/park` disarms a goal, `POST /__dsh_autorestart/recover` re-runs the
303
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.
304
444
  - Inside a session, `continuum_status` reports the same lifecycle.
305
445
 
306
- 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
307
447
  green. Run it with the bundled Node, from the workspace root:
308
448
 
309
449
  ```sh