dsh-autorestart 0.6.0 → 0.6.2

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
@@ -76,7 +76,7 @@ what it adds before you install it.
76
76
 
77
77
  ## Install
78
78
 
79
- Current version: **0.6.0**.
79
+ Current version: **0.6.2**.
80
80
 
81
81
  From npm — the package is published under the name `dsh-autorestart`:
82
82
 
@@ -144,7 +144,9 @@ show up in the Plugin Manager and in the config editor.
144
144
  | `restartCommand` | `''` | Custom restart command. Supports the `{pid}`, `{ppid}` and `{app}` placeholders. |
145
145
  | `relaunchDelaySeconds` | `2` | How long the detached helper waits before it kills the app (`ping -n 3`, roughly 2 seconds). |
146
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". |
147
+ | `restartGraceMs` | `0` | The **restart reserve** in milliseconds: how long the host waits after the last conversation finished before it is really RESTARTED — a dropdown: `0` / `5` / `10` / `20` / `30` / `60` seconds. `0` means "no extra wait". A restart is a daily operation, so the default is none. The settings page calls it *Wait before the restart*. |
148
+ | `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). **This visible countdown IS the wait before the machine goes off** — the shutdown has no second, invisible reserve. The settings page calls it *Shutdown countdown seconds*. |
149
+ | `queuedGraceMs` | `60000` | Upper bound in milliseconds for a **queued** (unclaimed) inbox message: how long it may hold the restart back before it is treated as stuck. `0` means "no upper bound" (the behaviour before 0.6.2). A **running** conversation always blocks, under every setting. |
148
150
  | `autoReloadPage` | `true` | `dsh web`: reload the page automatically when the host boot token changes. |
149
151
  | `blockManualRestart` | `true` | Hard-block an agent that tries to kill or restart DSH with a shell command, and point it at `continuum_restart` instead. |
150
152
  | `healthPath` | `/__dsh_autorestart/health` | Health check route. Returns the boot id, the pid, the phase and the self-check flags. |
@@ -154,7 +156,6 @@ show up in the Plugin Manager and in the config editor.
154
156
  | `maxBackups` | `2` | How many snapshots are kept; older ones are deleted when a new one is written. |
155
157
  | `rollbackTimeoutMs` | `90000` | How long the detached watcher waits for the relaunched host before it restores the snapshot and launches the host again. |
156
158
  | `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
159
  | `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). |
159
160
 
160
161
  The two waits are independent of each other, and both default to two seconds:
@@ -174,21 +175,110 @@ work other plugins queue with a timer of their own — a memory plugin that writ
174
175
  seconds after the conversation ended, a summary, an MCP callback, a log flush. That work is
175
176
  invisible here, so "every agent is idle" does not mean "nothing is happening any more".
176
177
 
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).
178
+ **One reserve window, for the restart.** A restart has no countdown of its own: the host restarts
179
+ as soon as the conversations are done. `restartGraceMs` is the extra wait that leaves room for the
180
+ delayed work of the other plugins — and it is the **only** such window:
181
+
182
+ | Window | Governs | Default | Offered |
183
+ | --- | --- | --- | --- |
184
+ | `restartGraceMs` | the **restart** | `0` (no extra wait) | `0` / `5` / `10` / `20` / `30` / `60` seconds |
185
+ | `sleepCountdownSeconds` | the **shutdown** (visible!) | `20` (twenty seconds) | `0` / `10` / `20` / `30` / `60` seconds |
186
+
187
+ - **The restart gate waits `max(settleMs, restartGraceMs)`** of quiet. With the default (`0`) the
188
+ gate waits for the settle window alone — a restart happens as soon as the conversations are done.
189
+ - **The graceful shutdown has no invisible reserve.** Its **visible countdown** is the wait before
190
+ the machine goes off: the seconds are on the screen, the user can watch them and cancel them.
191
+ A second, hidden window next to it would only say the same thing twice — so there is none. The
192
+ shutdown starts counting down as soon as every conversation is idle.
185
193
  - **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).
194
+ waiting, the reserve starts over — the wait always follows the last activity.
195
+ - **Where to change them.** Settings → Auto-restart & shut down, in two titled groups:
196
+ *Restart settings* (the wait before the restart, the two backup switches) and *Shutdown settings*
197
+ (the countdown of the shutdown). The status interfaces report the restart reserve:
198
+ `GET /health?state=1`, `GET /health?gate=1` and `continuum_status` carry `graceMs` (the restart
199
+ reserve) and `effectiveSettleMs` (the window the gate really uses); `GET /sleep` carries
200
+ `restartGraceMs` and `countdownSeconds` (the countdown that is really running). All of them carry
201
+ `quietForMs` (how long the host has actually been quiet).
202
+
203
+ ### Why a queued message cannot block the restart for ever
204
+
205
+ **The deadlock.** A conversation whose inbox holds a message nobody claimed yet counts as "busy"
206
+ for the restart gate. That is right while the message is about to be picked up — the agent claims
207
+ it within a second or two — but it is a deadlock when nobody ever claims it. The real case was a
208
+ fresh, empty conversation in the UI with one unclaimed message in its inbox: the gate stayed
209
+ closed, the hard deadline only logged a warning, and the restart never happened.
210
+
211
+ **The upper bound.** `queuedGraceMs` (default **60000** ms) is how long an unclaimed message may
212
+ hold the restart back:
213
+
214
+ - **A message claimed inside the window still blocks.** The normal case is unchanged: the agent
215
+ picks the message up and the restart waits for it.
216
+ - **A message that is still unclaimed after the window is stuck.** It stops blocking the restart,
217
+ and the log names the session that holds the queue (`the queued message of session <id> has been
218
+ waiting 61s and nobody claimed it; it no longer blocks the restart`). The restart goes on.
219
+ - **A running conversation always blocks — under every setting.** The bound only ever judges
220
+ *queued* work; a live turn is never relaxed, and no configuration can make the plugin kill one.
221
+ - **The timer follows the work.** A session that starts working drops its queue timer, and a new
222
+ queue counts from zero again.
223
+ - **`0` means "as before".** With `queuedGraceMs: 0` an unclaimed message blocks the restart for
224
+ ever, exactly like it did before this bound existed.
225
+ - **Where to look:** `GET /health?state=1`, `GET /health?gate=1` and `continuum_status` all report
226
+ `queuedGraceMs`.
227
+
228
+ ### What you see while the restart waits a long time
229
+
230
+ When the hard deadline (`deferHardMs`) is over and a conversation is still running, the plugin does
231
+ not pretend that everything is fine — and it does not give up either:
232
+
233
+ - **The request is kept.** The pending restart stays pending, the requester stays checkpointed, and
234
+ the restart fires as soon as the gate is quiet again. Nothing is cancelled behind your back.
235
+ - **The wait becomes visible.** The log says it once a minute — "the restart has been waiting for
236
+ 17m; `<session>` still holds it — cancel the restart or wait for that work to finish" — and
237
+ `GET /health?state=1` carries a `longWait` object (`since`, `reason`, `blockedBy`).
238
+ - **The bubble says it too.** The pending-restart pill shows one more line: "waiting for 17m:
239
+ `<session>` keeps holding it" (「已等 17 分:`<会话>`一直占着」).
240
+
241
+ A conversation is never killed to make the restart happen: `forceRestartWhenBusy` stays off by
242
+ default, and this plugin never turns it on.
243
+
244
+ ### What the quiet window measures
245
+
246
+ The restart gate has two layers: **no busy conversation** and **a quiet window**
247
+ (`max(settleMs, restartGraceMs)`).
248
+
249
+ The quiet window answers one question: *how long ago did the last real work happen?* It is measured
250
+ from the LATER of two markers:
251
+
252
+ - **the last real activity signal** — a turn that started or ended (`agent/status`), a message that
253
+ was queued (`agent/inbox/inserted`), or a session event that says work happened (a message, a tool,
254
+ a turn);
255
+ - **the last moment a conversation was seen working** — a running turn or a queued message
256
+ (`busyAgents()`).
257
+
258
+ **A routine session event is not activity.** The host emits session events every two to three seconds
259
+ while the interface polls and synchronises (a title, a projection, a context request, a job that
260
+ settled). Counting those as activity reset the quiet window every couple of seconds, so a window
261
+ longer than the event interval could never fill: the restart never fired, however long the user
262
+ waited. That is why a grace window of ten seconds used to make the restart impossible.
263
+
264
+ The rule is one-sided: **an event whose type cannot be read is not activity.** When in doubt the
265
+ plugin restarts a little early rather than never.
266
+
267
+ **A running conversation always blocks.** The quiet window only decides *when* the restart may fire;
268
+ the busy check is the hard guarantee, and it is never relaxed by any setting.
269
+
270
+ **Where to look when the restart does not happen.** The status interfaces report the reason of the
271
+ gate next to the blockers (`GET /health?state=1` carries `reason`: `conversation-active` when a
272
+ conversation blocks, `settling` when only the quiet window is not full yet, `quiet` when it may go),
273
+ and the pending restart writes one diagnostic line into the log per minute:
274
+
275
+ ```
276
+ restart gate: action=wait reason=agents-busy-within-soft-window busyCount=1 quietForMs=1432 effectiveSettleMs=10000
277
+ ```
278
+
279
+ `action` is what the restart decision did (`wait`, `restart`, `long-wait`, …), `reason` why,
280
+ `busyCount` how many conversations block, `quietForMs` how long the host has been quiet, and
281
+ `effectiveSettleMs` the window it has to fill.
192
282
 
193
283
  ### Platform note
194
284
 
@@ -284,9 +374,25 @@ once, in the same composer dock, as a small card:
284
374
  already shown (`seenFeatureVersions` in the checkpoint file). A fresh installation never sees the
285
375
  card: the install question already explains the features, and answering it marks the announcement
286
376
  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).
377
+ - **"Seen" means "not older", not "equal".** The card counts as answered as soon as the recorded
378
+ version is **not older** than the current feature version — a record of `0.6.0` counts as seen
379
+ while the feature itself stands at `0.5.0`. Why not equality? Because the record does not say
380
+ *which* release the user answered on (until 0.6.0 the card carried the version of the package),
381
+ so an equality check would tell a user who already dismissed the card that they never saw it. A
382
+ record that is not a version at all counts as seen as well: a broken record must not make the
383
+ card come back for ever.
384
+ - **Once per feature version, for ever.** "Got it" records the version, the card disappears at once,
385
+ and it never comes back — not with the next restart of the host, and not with the next release of
386
+ the plugin (a *newer version of the feature* is announced again, of course).
387
+ - **The announcement follows the FEATURE, not the release.** The version it carries is a constant in
388
+ the source (`SLEEP_FEATURE_VERSION` in `index.js`), **not** the version of the package: a release
389
+ never brings the card back. Until 0.6.0 the card carried the package version, and every release
390
+ (0.5.0 → 0.6.0) brought "graceful shutdown" back to the users who had already answered it.
391
+ - **How to announce it again on purpose.** Raise `SLEEP_FEATURE_VERSION` to a version **newer** than
392
+ the one that was recorded last: the card is then shown exactly once more. Do that **only** when
393
+ the feature itself changed so much that the user has to hear about it again (the shutdown became a
394
+ completely different thing, the button moved somewhere else, …). A release, a bug fix or a small
395
+ addition must **not** touch it — that is exactly the bug this constant exists to prevent.
290
396
  - **An unanswered card is not swallowed.** The card stays until it is answered, and it comes back with
291
397
  the next start of DSH.
292
398
  - **The countdown is read, not written.** The seconds in the card are the ones that are really in
@@ -294,7 +400,8 @@ once, in the same composer dock, as a small card:
294
400
  ("The button is currently switched off; you can turn it on in the settings").
295
401
  - **Where the state lives.** `<DSH_HOME>/storages/dsh-autorestart/state.json`, next to the checkpoint
296
402
  of the plugin. The host serves it on `GET /announcements` (`{ ok, pending: [{ feature, version }] }`)
297
- and records it on `POST /announcements` (`{ feature }`).
403
+ and records it on `POST /announcements` (`{ feature }`). The `version` in that answer is the feature
404
+ version — the same number that `SLEEP_FEATURE_VERSION` holds.
298
405
 
299
406
  ## Graceful shutdown (收工关机)
300
407
 
@@ -330,7 +437,8 @@ record that cannot be read is dropped — asking once more is much better than a
330
437
  itself off for ever because of a half-written file.
331
438
 
332
439
  **For the troubleshooting.** `GET /sleep` reports `armedAt` (when the shutdown was armed) next to
333
- `phase`, `blockers` and `graceMs`.
440
+ `phase`, `blockers`, `countdownSeconds` (the countdown that is really running) and
441
+ `restartGraceMs` (the restart reserve).
334
442
 
335
443
  ### Troubleshooting: the machine did not switch off
336
444
 
@@ -361,11 +469,13 @@ waiting for something. Here is what it can be, and what to do about it:
361
469
  `待执行的重启`). That is normal queueing, not a fault: the restart runs first, the sessions
362
470
  come back with it, and the countdown continues afterwards. **What to do:** nothing — wait for
363
471
  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.
472
+ 3. **The work just ended and the countdown has not started yet.** The shutdown has **no invisible
473
+ reserve**: as soon as every conversation is idle the countdown starts. **What you see:**
474
+ `phase` is `waiting` while `blockers[]` is empty and `remainingMs` is at its full value only
475
+ while the conversations are still busy — with an empty list the countdown is already running.
476
+ **What to do:** nothing; the countdown is on the screen and it can be cancelled there. (The
477
+ restart has its own window — `restartGraceMs`, default none — and the two never influence each
478
+ other.)
369
479
  4. **Somebody started working during the countdown.** The countdown goes back to "waiting" and
370
480
  `remainingMs` returns to its full value. **What you see:** the message says that somebody
371
481
  started working again, and `blockers[]` lists who. **What to do:** let them finish.
@@ -438,13 +548,19 @@ more whether the button should stay. The answer is written into the profile conf
438
548
  settings section and the prompt always show the same value.
439
549
 
440
550
  **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.
551
+ settings sidebar (right below the Agent presets). It is split into two titled groups — the restart
552
+ settings first, the shutdown settings after them:
553
+
554
+ - **Restart settings**
555
+ - **Wait before the restart** (`restartGraceMs`) — the dropdown `0` / `5` / `10` / `20` / `30` /
556
+ `60` seconds. `0` (the default) means "no extra wait": the host restarts as soon as the
557
+ conversations are done.
558
+ - **Backup before a restart** (`backupOnRestart`) — whether the pre-restart snapshot is taken at all.
559
+ - **Include the installed plugins in the backup** (`backupIncludePlugins`) — whether that snapshot
560
+ also contains the plugin directory of the profile. It is greyed out when the backup itself is off.
561
+ - **Shutdown settings**
562
+ - **Shutdown countdown seconds** (`sleepCountdownSeconds`) — the dropdown `0` / `10` / `20` / `30` /
563
+ `60`. `0` means "no countdown": the machine is switched off as soon as every conversation is idle.
448
564
 
449
565
  A change is written into the profile configuration at once (through the plugin's own choice route)
450
566
  and is read back from the host, so the section never shows a value the host did not take. A malformed