dsh-autorestart 0.3.6 → 0.6.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
@@ -29,15 +29,24 @@ round starts, so the session goes idle and the restart can proceed. The goal is
29
29
  restart and continues.
30
30
 
31
31
  The browser shows the state: a compact "pending restart" pill on the left side of the input toolbar
32
- and a small dot next to the session title in the sidebar. The pill's text reports how many
33
- conversations are still in the way (counted once per conversation, and the tooltip explains that a
34
- conversation with queued-but-not-started work counts too); it does not open a details panel. The dot
35
- breathes when the restart is about to fire.
32
+ and a small dot next to the session title in the sidebar. The pill's text reports how many **tasks**
33
+ are still in the way — a conversation, a subagent or a teammate counts as one task each, and the
34
+ tooltip explains that queued-but-not-started work counts too; it does not open a details panel. The
35
+ dot breathes when the restart is about to fire.
36
+
37
+ Every blocker says **what it is**: a conversation is named by its own title, and a **subagent** is
38
+ named after the conversation that created it ("Subagent of *that* conversation"). A raw session id is
39
+ never shown to the user.
36
40
 
37
41
  Before every restart the plugin snapshots the profile, so a restart that leaves the host unable to
38
42
  boot is rolled back automatically — see
39
43
  [Pre-restart backup and automatic rollback](#pre-restart-backup-and-automatic-rollback).
40
44
 
45
+ The same "wait until everything is quiet" idea is also available as a **graceful shutdown**: a moon
46
+ button at the very bottom of the left sidebar (right next to Settings) waits for every conversation
47
+ to be idle, counts down on screen (a floating card with a huge number and a small whale) and then
48
+ switches the **real host machine** off — see [Graceful shutdown](#graceful-shutdown-收工关机).
49
+
41
50
  Two safety gates protect the restart. The host checks that no conversation is busy, and the detached
42
51
  helper checks the same gate over loopback immediately before it kills the process, so a turn that
43
52
  starts in between is never killed.
@@ -50,8 +59,8 @@ what it adds before you install it.
50
59
  | Kind | What it adds |
51
60
  | --- | --- |
52
61
  | 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`) |
62
+ | 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 |
63
+ | 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
64
  | System prompt | One "Restart policy" section that tells the agent how to ask for a restart and what the plugin will do |
56
65
  | 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
66
  | Profile row | One bundle row, `dsh-autorestart` |
@@ -67,6 +76,8 @@ what it adds before you install it.
67
76
 
68
77
  ## Install
69
78
 
79
+ Current version: **0.6.0**.
80
+
70
81
  From npm — the package is published under the name `dsh-autorestart`:
71
82
 
72
83
  ```sh
@@ -133,6 +144,7 @@ show up in the Plugin Manager and in the config editor.
133
144
  | `restartCommand` | `''` | Custom restart command. Supports the `{pid}`, `{ppid}` and `{app}` placeholders. |
134
145
  | `relaunchDelaySeconds` | `2` | How long the detached helper waits before it kills the app (`ping -n 3`, roughly 2 seconds). |
135
146
  | `settleMs` | `2000` | Quiet window in milliseconds: the host must stay silent this long after the last conversation activity before a restart may start. |
147
+ | `restartGraceMs` | `10000` | Grace window in milliseconds: how long the host waits after the last conversation finished before it is really restarted or switched off — a dropdown: `0` / `3` / `5` / `10` / `20` / `30` seconds. `0` means "no extra wait". |
136
148
  | `autoReloadPage` | `true` | `dsh web`: reload the page automatically when the host boot token changes. |
137
149
  | `blockManualRestart` | `true` | Hard-block an agent that tries to kill or restart DSH with a shell command, and point it at `continuum_restart` instead. |
138
150
  | `healthPath` | `/__dsh_autorestart/health` | Health check route. Returns the boot id, the pid, the phase and the self-check flags. |
@@ -141,6 +153,9 @@ show up in the Plugin Manager and in the config editor.
141
153
  | `backupIncludePlugins` | `true` | Include the installed plugin tree in that snapshot (about 117 MB here, ~3 s). `false` keeps only the configuration files. |
142
154
  | `maxBackups` | `2` | How many snapshots are kept; older ones are deleted when a new one is written. |
143
155
  | `rollbackTimeoutMs` | `90000` | How long the detached watcher waits for the relaunched host before it restores the snapshot and launches the host again. |
156
+ | `showSleepButton` | `true` | Whether the moon button of the graceful shutdown is shown at the foot of the left sidebar, next to Settings. |
157
+ | `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). |
158
+ | `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
159
 
145
160
  The two waits are independent of each other, and both default to two seconds:
146
161
 
@@ -151,6 +166,30 @@ The two waits are independent of each other, and both default to two seconds:
151
166
  Both can be overridden separately in the profile config. A larger `settleMs` is the more
152
167
  conservative choice, because it is less likely to cut a session short that has just started.
153
168
 
169
+ ### Why the host waits a few more seconds after the work is done
170
+
171
+ **The blind spot.** This plugin can only see the **DSH agents**: it knows whether a conversation is
172
+ running, whether it has queued inbox work, and when the last of them went idle. It cannot see the
173
+ work other plugins queue with a timer of their own — a memory plugin that writes its notes a few
174
+ seconds after the conversation ended, a summary, an MCP callback, a log flush. That work is
175
+ invisible here, so "every agent is idle" does not mean "nothing is happening any more".
176
+
177
+ **The grace window.** `restartGraceMs` (default **10 seconds**) is the extra wait for exactly
178
+ that: after the LAST conversation finished, the host waits this long before it is really restarted
179
+ or switched off. It is a plain, configurable insurance — not a special case for any particular
180
+ plugin.
181
+
182
+ - **Both paths use it.** The restart gate waits `max(settleMs, restartGraceMs)` of quiet, and the
183
+ graceful shutdown waits the same grace **before** its countdown starts (so the grace never pushes
184
+ a countdown that is already running back by a round).
185
+ - **New activity restarts the timer.** If a conversation starts working again while the host is
186
+ waiting, the grace starts over — the wait always follows the last activity.
187
+ - **`0` means "as before".** With `restartGraceMs: 0` the behaviour is exactly the one of 0.4.0.
188
+ - **Where to change it.** Settings → Auto-restart & shut down → *Wait a few more seconds after the
189
+ work is done*. The status interfaces report it too: `GET /health?state=1`, `GET /health?gate=1`,
190
+ `continuum_status` and `GET /sleep` all carry `graceMs` (the configured window) and `quietForMs`
191
+ (how long the host has actually been quiet).
192
+
154
193
  ### Platform note
155
194
 
156
195
  `restartMode: app` only applies to the DeepSeek Harness desktop app. A hosted `dsh web`
@@ -218,16 +257,218 @@ addition to it, never a rival of it.
218
257
  stays usable, nothing is modal, and the panel does not disappear on its own. It is keyboard
219
258
  reachable and `Esc` closes it.
220
259
  - **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
260
+ `configEditor` service — the same `backupIncludePlugins` key the settings section edits, so there is
222
261
  one source of truth. If that service is unavailable the answer degrades to
223
262
  `<DSH_HOME>/storages/dsh-autorestart/install-choice.json` and a warning is logged. The same fallback
224
263
  happens in a hosted profile that declares no `dsh-autorestart` row of its own: there is no row for the
225
264
  editor to write, so the answer lands in that file instead of the profile. Declare the row (see
226
265
  *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`.
266
+ - **How to change your mind later:** Settings → Auto-restart & shut down → `backupIncludePlugins`.
228
267
  `continuum_status` always reports the value that is in effect right now
229
268
  (`backupIncludePlugins: true | false`).
230
269
 
270
+ ### The one-time new-feature announcement (upgrades)
271
+
272
+ **The gap it closes.** The install question above is asked exactly once — on the *first* install. An
273
+ installation that already existed therefore never hears of a feature that was added later: for an
274
+ upgrade the new moon button would simply not exist. Since **0.5.0** every new feature is announced
275
+ once, in the same composer dock, as a small card:
276
+
277
+ > **New feature: graceful shutdown**
278
+ > There is a new 🌙 "Shut down" button at the very bottom of the left sidebar.
279
+ > Click it → once every conversation is done it counts down **N** seconds and powers the machine off.
280
+ > To change that, go to "Settings → Auto-restart & shut down".
281
+ > **Got it**
282
+
283
+ - **Only after an upgrade.** The host compares the version of the feature with the version it has
284
+ already shown (`seenFeatureVersions` in the checkpoint file). A fresh installation never sees the
285
+ card: the install question already explains the features, and answering it marks the announcement
286
+ as seen.
287
+ - **Once per version, for ever.** "Got it" records the version, the card disappears at once, and it
288
+ never comes back — not with the next restart of the host, and not with the next version of the
289
+ plugin (a *new* version of a feature is announced again, of course).
290
+ - **An unanswered card is not swallowed.** The card stays until it is answered, and it comes back with
291
+ the next start of DSH.
292
+ - **The countdown is read, not written.** The seconds in the card are the ones that are really in
293
+ effect (`sleepCountdownSeconds`), and when the moon button is switched off the card says so
294
+ ("The button is currently switched off; you can turn it on in the settings").
295
+ - **Where the state lives.** `<DSH_HOME>/storages/dsh-autorestart/state.json`, next to the checkpoint
296
+ of the plugin. The host serves it on `GET /announcements` (`{ ok, pending: [{ feature, version }] }`)
297
+ and records it on `POST /announcements` (`{ feature }`).
298
+
299
+ ## Graceful shutdown (收工关机)
300
+
301
+ **What it does.** One moon button at the very bottom of the left sidebar: press it and the plugin
302
+ waits until **every** conversation is idle, the screen counts down (a huge number and a small blue
303
+ whale, one line of text per second), and then the machine is switched off. The countdown can always
304
+ be cancelled — the card has a button, and the moon button itself becomes a toggle while the shutdown
305
+ is armed. If a conversation starts working again during the countdown, the countdown goes back to
306
+ "waiting" and nothing is switched off.
307
+
308
+ ### Shutdown and restart: who goes first
309
+
310
+ **The short version.** Pressing the moon button means "switch the machine off when the work is
311
+ done" — **not** "switch it off right now, whatever else is running". So:
312
+
313
+ 1. **A pending restart wins.** If a restart is still on the list, the shutdown does **not** happen.
314
+ The card says *"Waiting for the pending restart to finish before shutting down"*. The restart
315
+ goes first, the sessions come back with it and keep working, and only then does the machine
316
+ go off.
317
+ 2. **The work keeps running after the restart.** The sessions that resume with the restart are
318
+ running work: while any of them is busy, the shutdown waits. The machine only goes off when
319
+ **every** session is idle again.
320
+ 3. **The shutdown intent survives the restart.** You pressed the button **once**. The restart does
321
+ not swallow that request: it is written into the checkpoint file, and the new boot picks it up
322
+ and keeps waiting for the work. You do **not** have to press the button a second time.
323
+ 4. **You can cancel at any time.** The waiting card has a *"Keep it on"* button; pressing it (or the
324
+ moon button again) forgets the intent for good.
325
+
326
+ **Where the intent lives.** In the checkpoint file
327
+ (`<DSH_HOME>/storages/dsh-autorestart/state.json`) as `sleep: { armed, armedAt, countdownSeconds }`.
328
+ It is cleared as soon as the machine went off, when the shutdown failed, and when you cancel. A
329
+ record that cannot be read is dropped — asking once more is much better than a machine that switches
330
+ itself off for ever because of a half-written file.
331
+
332
+ **For the troubleshooting.** `GET /sleep` reports `armedAt` (when the shutdown was armed) next to
333
+ `phase`, `blockers` and `graceMs`.
334
+
335
+ ### Troubleshooting: the machine did not switch off
336
+
337
+ **These are the real answers of a running host** (the copy of the host is Chinese, the fields are
338
+ not):
339
+
340
+ ```
341
+ POST /__dsh_autorestart/sleep {"action":"arm"} # three conversations are working
342
+ → phase=waiting armed=true remainingMs=20000 (the full value: the countdown has NOT started)
343
+ message: 等 3 个任务收工,收工后开始倒数。
344
+
345
+ # after a restart was requested:
346
+ GET /__dsh_autorestart/sleep
347
+ → phase=waiting armed=true remainingMs=20000
348
+ message: 等这次重启完成后再关机……
349
+ blockers: [{"sessionId":"restart-pending","kind":"restart-pending",
350
+ "label":"待执行的重启","name":"待执行的重启"}]
351
+ ```
352
+
353
+ `remainingMs` at its full value is the tell: **the countdown has not started**, so the machine is
354
+ waiting for something. Here is what it can be, and what to do about it:
355
+
356
+ 1. **Some tasks are still working.** `blockers[]` lists them; look at `kind`:
357
+ `session` is a conversation, `subagent` is a child agent of one. A subagent counts as work on
358
+ purpose — otherwise a restart would cut it off. **What to do:** let it finish, or stop that
359
+ conversation. The list shrinks on its own.
360
+ 2. **A restart is still pending.** `kind` is `restart-pending` (`label`/`name` are
361
+ `待执行的重启`). That is normal queueing, not a fault: the restart runs first, the sessions
362
+ come back with it, and the countdown continues afterwards. **What to do:** nothing — wait for
363
+ the restart. (You can also cancel the restart with `continuum_cancel_restart`.)
364
+ 3. **The work just ended and the grace window is still running** (default 10 seconds, so that a
365
+ memory or summary plugin can still write its notes). **What you see:** `graceMs` is the
366
+ configured window, `quietForMs` is how long the host has actually been quiet. **What to do:**
367
+ wait; if another conversation starts working during the wait, the timer starts over (that is
368
+ the point of the window). Set it to `0` in the settings if you do not want the extra wait.
369
+ 4. **Somebody started working during the countdown.** The countdown goes back to "waiting" and
370
+ `remainingMs` returns to its full value. **What you see:** the message says that somebody
371
+ started working again, and `blockers[]` lists who. **What to do:** let them finish.
372
+ 5. **You want to know who it is waiting for.** `continuum_status` → `blockedBy[]` and
373
+ `GET /sleep` → `blockers[]` are the **same data**: the first is for agents, the second for
374
+ scripts. Each entry has `kind` (what it is), `label` (the title of the session) and `name`
375
+ (the whole name, e.g. "the subagent of the coding session"). A session whose title could not
376
+ be resolved reports `label: null` and an empty `name` — that is why the waiting line then
377
+ only says "等 3 个任务收工" without a list of names.
378
+ 6. **You do not want it at all.** The cancel button on the waiting card (or the moon button
379
+ again, or `POST /sleep {"action":"cancel"}`) clears the intent for good: `armed` becomes
380
+ `false` and `armedAt` becomes `null`.
381
+
382
+ **Where the button is.** The moon button sits at the **very bottom of the left sidebar, right next
383
+ to the Settings row** (`sidebar.footer.action`). That seat is a **root** slot, so the button is
384
+ there even when no conversation is open — on any page. When the sidebar is collapsed into its narrow
385
+ rail, the button shows its icon only (the tooltip and the accessible name still say what it does),
386
+ so the rail keeps its width.
387
+
388
+ **The countdown does not cover the interface.** The countdown is a **floating card** in the corner,
389
+ not a full-screen mask: it never dims the page, never locks the scrolling and never swallows a
390
+ click. You can keep reading the conversation, keep scrolling and keep switching conversations while
391
+ it counts down; only the card's own cancel button takes clicks. Switching a conversation or a page
392
+ does not interrupt the countdown either — the seconds come from the host, not from the view that
393
+ happens to be open.
394
+
395
+ **What gets switched off: the real host.** Not a subsystem — the whole machine:
396
+
397
+ | Host | Command |
398
+ | --- | --- |
399
+ | Windows | `shutdown.exe /s /f /t 0` |
400
+ | 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. |
401
+ | A real Linux host | `systemctl poweroff` (fallback `shutdown -h now`) |
402
+ | macOS | `osascript -e 'tell application "System Events" to shut down'` |
403
+ | Anything else | **Nothing is executed at all.** The button stays hidden and the route reports `phase: "unsupported"`. |
404
+
405
+ A WSL host is recognised through `WSL_DISTRO_NAME`, `WSL_INTEROP`, a `/proc/version` that mentions
406
+ Microsoft, or the mounted Windows binary. The countdown belongs to the plugin, so the operating
407
+ system is asked to shut down immediately (`/t 0`) instead of counting down a second time.
408
+
409
+ **Configuration.** `showSleepButton` (default `true`) decides whether the button is shown,
410
+ `sleepCountdownSeconds` (a **dropdown**: `0` / `10` / `20` / `30` / `60` seconds, default `20`) how
411
+ long the countdown runs, and `sleepCommand` (default `''`) replaces the detected command with one of
412
+ your own — empty means "detect the host automatically". `0` means **no countdown at all**: the machine
413
+ is switched off as soon as every conversation is idle (a busy conversation still blocks it), and no
414
+ countdown card appears. A profile that still carries a value the dropdown does not offer (`15`,
415
+ `45`, …) is snapped onto the nearest offered one instead of breaking.
416
+
417
+ **One click is all it takes.** The moon button arms the shutdown: the host waits for every busy
418
+ conversation, counts down **20 seconds** in a floating card (it never covers the interface), and then
419
+ switches the machine off. Clicking the button again — or the cancel button on the card — takes it
420
+ back. Nothing else is asked of the user: no confirmation dialog, no second click.
421
+
422
+ **The desktop client and the hosted web client behave exactly the same.** There is no ticket, no
423
+ extra header and no hidden mode: `POST /__dsh_autorestart/sleep` takes nothing but the action, and
424
+ the same route serves both. A script (or an agent) can therefore drive the shutdown as well:
425
+
426
+ ```bash
427
+ # arm it (the machine counts down and goes off)
428
+ curl -s -X POST http://127.0.0.1:<port>/__dsh_autorestart/sleep \
429
+ -H 'content-type: application/json' -d '{"action":"arm"}'
430
+ # take it back
431
+ curl -s -X POST http://127.0.0.1:<port>/__dsh_autorestart/sleep \
432
+ -H 'content-type: application/json' -d '{"action":"cancel"}'
433
+ ```
434
+
435
+ **The one-time install question.** The first time the plugin is used, the composer dock asks once
436
+ more whether the button should stay. The answer is written into the profile configuration
437
+ (`showSleepButton`) through the same `configEditor` service the backup question uses, so the
438
+ settings section and the prompt always show the same value.
439
+
440
+ **The settings section.** *Settings → Auto-restart & shut down* is a section of its own in the
441
+ settings sidebar (right below the Agent presets). It carries the three settings that matter:
442
+
443
+ - **Countdown seconds** — the dropdown `0` / `10` / `20` / `30` / `60`. `0` means "no countdown":
444
+ the machine is switched off as soon as every conversation is idle.
445
+ - **Backup before a restart** (`backupOnRestart`) — whether the pre-restart snapshot is taken at all.
446
+ - **Include the installed plugins in the backup** (`backupIncludePlugins`) — whether that snapshot
447
+ also contains the plugin directory of the profile. It is greyed out when the backup itself is off.
448
+
449
+ A change is written into the profile configuration at once (through the plugin's own choice route)
450
+ and is read back from the host, so the section never shows a value the host did not take. A malformed
451
+ value is refused with a **400** and **nothing** is written.
452
+
453
+ **Trying it without switching anything off.** Set `DSH_AUTORESTART_SLEEP_DRYRUN=1` in the host
454
+ environment: the plugin then reports the command it *would* run and never runs it.
455
+
456
+ **The machine-readable state.** `GET <healthPath 前缀>/sleep` (default
457
+ `/__dsh_autorestart/sleep`) answers with the phase, the countdown and the host that is about to be
458
+ closed. A few of its fields are meant to be read by code rather than by a human: `host.kind` is the
459
+ kind of machine (`windows`, `wsl`, `linux`, `macos`, `unsupported`, or `custom` when the platform
460
+ could not be recognised but a `sleepCommand` was configured); `host.suspect` is `true` when the host
461
+ was only recognised as WSL because the Windows drive is mounted — a hint, not a proof;
462
+ `lastRun.dryRun` marks the last run as a rehearsal that switched nothing off; and
463
+ `backupOnRestart` / `backupIncludePlugins` report the effective scope of the pre-restart backup.
464
+
465
+ **When the shutdown command does not start.** The host waits for the real answer of the process. If
466
+ the command cannot start (missing binary, denied permission), the shutdown does **not** pretend it
467
+ worked: the phase goes back to `idle`, the reason is reported ("the shutdown command did not start
468
+ (…), the computer is still on, press the moon button again"), and the machine stays on. On screen
469
+ the "shutting down" card never locks the interface: after at most eight seconds it offers a button
470
+ that closes the card.
471
+
231
472
  ## Requirements and limits
232
473
 
233
474
  - **Windows desktop.** `restartMode: app` restarts the DSH desktop app on Windows. Any other
@@ -279,7 +520,7 @@ when the host is started by hand. `continuum_status` keeps reporting the pending
279
520
  The same profile row is what the one-time install question writes into: a hosted profile that declares no
280
521
  `dsh-autorestart` row has no row for the `configEditor` service to edit, so the answer falls back to
281
522
  `<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.
523
+ row keeps the profile (and therefore the settings section) as the single source of truth.
283
524
 
284
525
  ## Verify
285
526
 
@@ -288,7 +529,7 @@ The health route tells you whether the plugin is alive and what it has done:
288
529
  ```sh
289
530
  curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
290
531
  # {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
291
- # "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":2,…}
532
+ # "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":1,…}
292
533
  ```
293
534
 
294
535
  `bootId` and `pid` change with every new process, so a restart is visible right away.
@@ -301,9 +542,11 @@ curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
301
542
  anything.
302
543
  - `POST /__dsh_autorestart/park` disarms a goal, `POST /__dsh_autorestart/recover` re-runs the
303
544
  recovery immediately. Both use the same authentication rules as the health route.
545
+ - `GET /__dsh_autorestart/sleep` reports the state of the graceful shutdown, `POST
546
+ /__dsh_autorestart/sleep` arms (`{ "action": "arm" }`) or cancels (`{ "action": "cancel" }`) it.
304
547
  - Inside a session, `continuum_status` reports the same lifecycle.
305
548
 
306
- The plugin ships its own test suite: **129 test cases** in `plugins/dsh-autorestart/test/`, all
549
+ The plugin ships its own test suite: **250 test cases** in `plugins/dsh-autorestart/test/`, all
307
550
  green. Run it with the bundled Node, from the workspace root:
308
551
 
309
552
  ```sh