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 +254 -11
- package/client.js +2500 -147
- package/cordis.patch.yml +24 -0
- package/docs/README.zh.md +160 -15
- package/index.js +1336 -80
- package/lib/checkpoint.js +116 -0
- package/lib/sleep.js +1012 -0
- package/locale/en.json +108 -1
- package/locale/zh.json +108 -1
- package/package.json +1 -1
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
|
-
|
|
34
|
-
|
|
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 (
|
|
54
|
-
| Client slots (
|
|
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
|
|
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 →
|
|
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
|
|
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":
|
|
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: **
|
|
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
|