dsh-autorestart 0.6.1 → 0.7.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 +457 -41
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/assets/screenshot-4.png +0 -0
- package/assets/screenshot-5.png +0 -0
- package/client.js +5973 -3222
- package/cordis.patch.yml +55 -9
- package/docs/README.zh.md +305 -27
- package/index.js +2522 -127
- package/lib/access.js +212 -0
- package/lib/activity.js +124 -0
- package/lib/autostart-advice.js +198 -0
- package/lib/backup.js +131 -4
- package/lib/blockers.js +228 -0
- package/lib/checkpoint.js +147 -0
- package/lib/diagnostics.js +180 -0
- package/lib/health.js +264 -0
- package/lib/host-kind.js +145 -0
- package/lib/lifecycle.js +17 -4
- package/lib/node-runtime.js +144 -0
- package/lib/queued.js +146 -0
- package/lib/restart-verdict.js +327 -0
- package/lib/restart.js +823 -56
- package/lib/sleep.js +46 -49
- package/lib/startup-mode.js +491 -0
- package/lib/supervisor-config.js +735 -0
- package/lib/supervisor-install.js +557 -0
- package/lib/supervisor.js +1276 -0
- package/locale/en.json +247 -60
- package/locale/zh.json +247 -60
- package/package.json +7 -2
- package/screenshots.json +6 -4
- package/scripts/emergency-disable.cmd +27 -0
- package/scripts/emergency-toggle.ps1 +302 -0
- package/scripts/run-tests.ps1 +46 -0
- package/supervisor/supervisor.js +884 -0
package/README.md
CHANGED
|
@@ -51,6 +51,95 @@ Two safety gates protect the restart. The host checks that no conversation is bu
|
|
|
51
51
|
helper checks the same gate over loopback immediately before it kills the process, so a turn that
|
|
52
52
|
starts in between is never killed.
|
|
53
53
|
|
|
54
|
+
## The whole thing in one page (ordinary users can stop here)
|
|
55
|
+
|
|
56
|
+
### Why a restart does not lose your work
|
|
57
|
+
|
|
58
|
+
The plugin turns a restart into a **resumable** event, and the order never changes:
|
|
59
|
+
|
|
60
|
+
1. **The session state is written to disk first** — what every conversation was doing and which
|
|
61
|
+
goals were still armed goes into a checkpoint on disk;
|
|
62
|
+
2. **It queues until every conversation is idle** — whoever is still running is waited for;
|
|
63
|
+
3. **The process is replaced** — the old one exits, a new one comes up (the desktop app relaunches
|
|
64
|
+
itself; the hosted web client is covered below);
|
|
65
|
+
4. **The new process checks itself** — four things: is the port up, are the routes there, did the
|
|
66
|
+
plugin load, did the sessions resume;
|
|
67
|
+
5. **If it did not come up, it rolls back** — the pre-restart snapshot (configuration + installed
|
|
68
|
+
plugins) is put back, and the verdict of that restart is written down honestly.
|
|
69
|
+
|
|
70
|
+
So "the restart lost my work" becomes "the restart carried on": you do not have to type "continue".
|
|
71
|
+
|
|
72
|
+
### Two shapes of DSH, each minding its own business
|
|
73
|
+
|
|
74
|
+
| | Desktop client | Hosted web client (`dsh web`) |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| Who brings it back | **the app itself** (it closes itself and opens itself again) | it first asks "who started me?" |
|
|
77
|
+
| Should a system service be installed | **no** — installing one makes it start **twice**: one more DSH at every login | only when *nobody* manages it |
|
|
78
|
+
| Somebody else already manages it (a supervisor script / systemd / a container / pm2 / a Windows scheduled task…) | n/a | **it only exits gracefully** and lets that supervisor bring it back (it does not start a second one) |
|
|
79
|
+
| It cannot tell who started it | n/a | **it does nothing and guesses no command** — "I cannot tell" beats starting a second copy next to the first |
|
|
80
|
+
|
|
81
|
+
### "Start at login": the plugin gives advice, it does not change your system
|
|
82
|
+
|
|
83
|
+
The plugin **never** changes your system configuration and **never** registers a startup entry for
|
|
84
|
+
you. It explains in the settings page how "start at login" works on *this* machine; the commands can
|
|
85
|
+
be copied, and whether you use them is your call.
|
|
86
|
+
|
|
87
|
+
| Your situation | What it recommends | Note |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `dsh web` on Windows | start at login: a scheduled task first (no administrator rights); if this machine refuses that, a `.cmd` in the startup folder | the record runs a **real node** + the supervisor script, not the DSH client itself (that would open a second DSH at every login) |
|
|
90
|
+
| Linux (not WSL) | a systemd user service | that is **start after login**; **start at boot** (without a login) needs the linger enabled once |
|
|
91
|
+
| WSL with systemd | the same: a systemd user unit + the linger | same |
|
|
92
|
+
| WSL without systemd | there is no "start at login" at all. Two ways out: ① turn systemd on inside WSL (change the distribution's configuration and restart the subsystem); ② let the Windows side start the distribution with a scheduled task or a startup-folder entry (much closer to a real boot autostart) | the plugin **never** changes your distribution's configuration for you, and it never recommends a container-orchestration setup inside WSL (the orchestrator of WSL is Windows itself) |
|
|
93
|
+
| container / k8s | the orchestrator does it (`restart: always` and friends) | there is no "start at login" inside a container |
|
|
94
|
+
|
|
95
|
+
**"Start after login" and "start at boot" are not the same thing**: the first waits for you to log
|
|
96
|
+
in, the second happens when the machine comes up (and needs the linger enabled once). The plugin
|
|
97
|
+
writes that sentence and the matching command into the settings page.
|
|
98
|
+
|
|
99
|
+
### If DSH does not come up
|
|
100
|
+
|
|
101
|
+
Double-click `scripts/emergency-disable.cmd` in the plugin directory — it takes this plugin out
|
|
102
|
+
safely and keeps a backup — then open DSH again.
|
|
103
|
+
|
|
104
|
+
### What has really been tested
|
|
105
|
+
|
|
106
|
+
| How it was started | Tested on a real machine? | Evidence |
|
|
107
|
+
| --- | --- | --- |
|
|
108
|
+
| started by a shell supervisor script (`supervise-*.sh` and friends) | **yes** | recognised on a real machine as external, the evidence points at that script process, and **installing a second supervisor was refused** |
|
|
109
|
+
| started by hand in a terminal (foreground) | **partly** | on a real machine it recognised itself as "nobody manages it" (mode `bare`, the evidence is the parent being bash) and handed out an installation plan; the real "install → run → uninstall" was done on an instance that was **already managed by a supervisor**, so the bare instance never had the full install/uninstall cycle |
|
|
110
|
+
| started in the background (`nohup` / `setsid`) | **partly** | on a real machine it was recognised as a bare background start; **"the re-launch command outlives its shell" is pinned by unit tests only** (the machine did not report the detached flag back then) |
|
|
111
|
+
| WSL with systemd | **yes** | really installed and really removed |
|
|
112
|
+
| a Windows login task | **partly** | on a real machine one login-triggered task was created → queried (State=Ready, the logon trigger) → deleted (no leftovers) with the system's own tools, which proves that no administrator rights are needed; **"the fallback into the startup folder when the task is refused" is pinned by unit tests only** |
|
|
113
|
+
| the desktop client | **yes** | on a real machine it comes back by itself (there is a snapshot and a self-check record, no rollback was triggered, and it does not start twice); that is the client's own business, it is not part of the hosted matrix |
|
|
114
|
+
| pm2 / forever | no, unit tests only | the recognition and the behaviour are pinned by unit tests |
|
|
115
|
+
| container / compose | no, unit tests only | same |
|
|
116
|
+
| `npx` / `pnpm dlx` wrapper | no, unit tests only | same |
|
|
117
|
+
| started by another DSH plugin | no, unit tests only | same |
|
|
118
|
+
| tmux / screen / an SSH session | no, unit tests only | same |
|
|
119
|
+
| WSL started by a Windows-side process | no, unit tests only | same (it is recognised as "cannot tell", so nothing happens) |
|
|
120
|
+
|
|
121
|
+
**Not tested on a real machine does not mean unsupported**: those paths are pinned by unit tests, and
|
|
122
|
+
"no full install/uninstall cycle" or "unit tests only" does not mean unsupported either. If your
|
|
123
|
+
environment is recognised wrongly, please send us the "startup mode + the evidence" block from the
|
|
124
|
+
settings page.
|
|
125
|
+
|
|
126
|
+
### Report a problem
|
|
127
|
+
|
|
128
|
+
The settings page has a "**Report a problem**" group at the bottom:
|
|
129
|
+
|
|
130
|
+
- The main button "**Copy the diagnostics and open a GitHub issue**": one click puts the diagnostics
|
|
131
|
+
on the clipboard and opens a **pre-filled** issue page (title and outline) — paste the clipboard
|
|
132
|
+
text into it.
|
|
133
|
+
- The diagnostics carry **no access token and no secrets**: only the plugin version, the platform,
|
|
134
|
+
whether this is the desktop client, how the host was started (with the evidence), the supervisor,
|
|
135
|
+
the restart mode, and the verdict of the last restart (with the checks that failed).
|
|
136
|
+
- When the browser refuses the clipboard (an insecure context, or no permission), the plugin shows
|
|
137
|
+
the diagnostics in a **selectable** box and says so — a report never dies because the browser said
|
|
138
|
+
no.
|
|
139
|
+
- No GitHub account? The same group carries the mailbox (252170229@qq.com), a "Copy the
|
|
140
|
+
diagnostics" button and a matching mail link (the subject is filled in, the body asks you to
|
|
141
|
+
paste).
|
|
142
|
+
|
|
54
143
|
## What it occupies in DSH
|
|
55
144
|
|
|
56
145
|
Nothing here is hidden: this is the complete list of where the plugin attaches itself, so you can see
|
|
@@ -64,7 +153,7 @@ what it adds before you install it.
|
|
|
64
153
|
| System prompt | One "Restart policy" section that tells the agent how to ask for a restart and what the plugin will do |
|
|
65
154
|
| 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 |
|
|
66
155
|
| Profile row | One bundle row, `dsh-autorestart` |
|
|
67
|
-
| Writes | Only under `<DSH_HOME>/storages/dsh-autorestart/` — `state.json` (checkpoint), `backups/` (pre-restart snapshots), `restart-<gen>.cmd` / `restart-<gen>-watch.ps1` (the detached helper), `last-rollback.json` (rollback report). The only time it writes into the profile itself is a rollback, which copies that snapshot's `cordis.patch.yml`, `package.json`, `pnpm-lock.yaml` and `node_modules` back |
|
|
156
|
+
| Writes | Only under `<DSH_HOME>/storages/dsh-autorestart/` — `state.json` (checkpoint), `backups/` (pre-restart snapshots), `restart-<gen>.cmd` / `restart-<gen>-watch.ps1` (the detached helper), `last-rollback.json` (rollback report), `restart-outcome.json` (the decision of a restart the plugin judged itself), `access.json` (the access link of the running instance, mode `0600`). The only time it writes into the profile itself is a rollback, which copies that snapshot's `cordis.patch.yml`, `package.json`, `pnpm-lock.yaml` and `node_modules` back |
|
|
68
157
|
|
|
69
158
|
## Why
|
|
70
159
|
|
|
@@ -76,7 +165,7 @@ what it adds before you install it.
|
|
|
76
165
|
|
|
77
166
|
## Install
|
|
78
167
|
|
|
79
|
-
Current version: **0.
|
|
168
|
+
Current version: **0.7.0**.
|
|
80
169
|
|
|
81
170
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
82
171
|
|
|
@@ -111,7 +200,7 @@ so you can call them yourself.
|
|
|
111
200
|
| --- | --- | --- |
|
|
112
201
|
| `continuum_restart` | Requests a graceful restart. The restart waits until every conversation is idle and the host has been quiet for `settleMs`, then the app is relaunched and this session resumes automatically. | `reason` (string, recorded in the checkpoint and the log), `resume` (boolean, default `true`), `defer_seconds` (number, extra seconds to wait before the first idle check), `parkGoals` (boolean, default `true`) — disarm a goal-driven session before the restart |
|
|
113
202
|
| `continuum_status` | Read-only snapshot of the lifecycle: whether a restart is pending, which sessions were checkpointed, why each blocking session blocks, the goals this plugin disarmed, the last recovery action per session, the handoffs, the state file path and the detected restart environment. | `include_sessions` (boolean, default `true`) — include the per-session checkpoint entries |
|
|
114
|
-
| `continuum_cancel_restart` |
|
|
203
|
+
| `continuum_cancel_restart` | Withdraws a pending restart request. By default it withdraws **only the calling session's own request** (`scope: "mine"`); the other sessions that asked for the same restart keep it. `scope: "all"` cancels the request for everyone. Does nothing when no restart is pending. | `scope` (string, `mine` (default) or `all`), `reason` (string, recorded in the log) |
|
|
115
204
|
| `continuum_park_goal` | Disarms the goal of a session so that it no longer blocks the restart. The current round still finishes and the goal is re-armed after the restart. | `sessionId` (string, optional) — defaults to the calling session |
|
|
116
205
|
| `continuum_recover_now` | Re-runs the recovery immediately instead of waiting for the next restart, and reports the action and the reason for each session. | `sessionId` (string, optional) — defaults to all sessions pending recovery |
|
|
117
206
|
| `continuum_handoff` | Hands a session whose conversation exceeded the model context window over to a fresh session, carrying the goal objective, the recorded reasons and a bounded tail of the conversation. | `sessionId` (string, optional) — defaults to the calling session, `maxChars` (number, default `20000`) — character budget for the conversation tail |
|
|
@@ -119,8 +208,66 @@ so you can call them yourself.
|
|
|
119
208
|
A goal is an enhancement, not a requirement: without one, the plugin injects a single continuation
|
|
120
209
|
message instead of re-arming a goal.
|
|
121
210
|
|
|
211
|
+
### Cancelling a restart: only your own request, or everybody's
|
|
212
|
+
|
|
213
|
+
A restart request is **coalesced**: several sessions may ask for the same restart, and the plugin
|
|
214
|
+
keeps every requester in `restart.requesters` so that all of them are resumed after that one
|
|
215
|
+
restart. That is why "cancel" has to say **whose** request it cancels:
|
|
216
|
+
|
|
217
|
+
| Scope | What happens |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `mine` (the default) | Only the **calling session's** request is withdrawn. The restart stays pending while other requesters remain, and `restart.bySessionId` is re-pointed at one of them. When the calling session was the **last** requester, the restart really goes away: the phase returns to `idle` and the session is released. |
|
|
220
|
+
| `all` | The request is cancelled for **everybody**: every requester is released and the pending restart is gone. This is "nobody restarts any more". |
|
|
221
|
+
|
|
222
|
+
Two calls are refused, and both leave the state untouched — a silent "cancel everything" is never
|
|
223
|
+
the fallback:
|
|
224
|
+
|
|
225
|
+
- the calling session is unknown (the tool was called without a session) → refused;
|
|
226
|
+
- the calling session did **not** ask for the pending restart → refused.
|
|
227
|
+
|
|
228
|
+
The tool reports what happened: `cancelled` (whether the restart request itself is gone),
|
|
229
|
+
`remainingRequesters` (how many sessions still wait for it), `scope` and `phase`.
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{ "ok": true, "cancelled": false, "scope": "mine", "remainingRequesters": 1, "sessionId": "sess-a", "phase": "deferred" }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### The capsule: click it for the details and for the way out
|
|
236
|
+
|
|
237
|
+
The capsule in the composer row is 22px high and ONE line wide, so it carries numbers only
|
|
238
|
+
(`待重启 · 已等 16 分钟 · 3 项任务在跑`) — never a name and never a hint. Clicking it (or pressing
|
|
239
|
+
Enter/Space on it) opens a small panel above it:
|
|
240
|
+
|
|
241
|
+
- how many tasks the restart is waiting for;
|
|
242
|
+
- how many OTHER sessions wait for the same restart;
|
|
243
|
+
- one button: **Cancel my restart**.
|
|
244
|
+
|
|
245
|
+
The button opens a confirmation (no timeout, nothing counts down): *Cancel your restart?* — and what
|
|
246
|
+
cancelling means right now (only you, or you while the others still get their restart). Only the
|
|
247
|
+
confirmation sends the request; `Esc`, a click outside the panel or **Never mind** closes it without
|
|
248
|
+
doing anything.
|
|
249
|
+
|
|
250
|
+
The request withdraws ONLY this session's restart request (`POST /__dsh_autorestart/cancel` with
|
|
251
|
+
`{ "sessionId": "…", "scope": "mine" }`): the other sessions keep theirs, and the restart still
|
|
252
|
+
happens for them. There is no "cancel everybody" in the panel — that stays an explicit tool call
|
|
253
|
+
(`continuum_cancel_restart` with `scope: "all"`).
|
|
254
|
+
|
|
122
255
|
## Configuration
|
|
123
256
|
|
|
257
|
+
Two blocks of the settings page are worth a word, because they show facts instead of guesses:
|
|
258
|
+
|
|
259
|
+
- **What is backed up right now** (right under the two backup switches): the HOST reads
|
|
260
|
+
`<DSH_HOME>/storages/dsh-autorestart/backups/`, so the number of copies, the **local time** of each
|
|
261
|
+
one, its size (MB), whether the installed plugins are in it and where it lives are all real. Only
|
|
262
|
+
the directory metadata is read — the contents are never walked (one copy can be 150 MB). With no
|
|
263
|
+
backup at all it says: "No backup yet; one is written before every restart."
|
|
264
|
+
- **How this host was started** (hosted web only): one plain line ("How I remember you were started:
|
|
265
|
+
by your own script (the restart is its job — I stay out of it)") plus two small actions (detect
|
|
266
|
+
again / undo); the command itself is folded away under **"Technical details"** and stays closed
|
|
267
|
+
until somebody opens it. The **desktop client does not render that block at all** — it brings
|
|
268
|
+
itself back up and must never write a `restartCommand`. When the Host cannot say which kind of
|
|
269
|
+
host this is, the whole block stays away.
|
|
270
|
+
|
|
124
271
|
Every key has a default, written in the `insert` row of `cordis.patch.yml`. To override a key, add
|
|
125
272
|
an `- id: dsh-autorestart` row with the same key to the `cordis.patch.yml` of the profile at
|
|
126
273
|
`~/.dsh/profiles/<profile>/cordis.patch.yml` (it replaces the whole config, it is not merged). The
|
|
@@ -144,7 +291,9 @@ show up in the Plugin Manager and in the config editor.
|
|
|
144
291
|
| `restartCommand` | `''` | Custom restart command. Supports the `{pid}`, `{ppid}` and `{app}` placeholders. |
|
|
145
292
|
| `relaunchDelaySeconds` | `2` | How long the detached helper waits before it kills the app (`ping -n 3`, roughly 2 seconds). |
|
|
146
293
|
| `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` | `
|
|
294
|
+
| `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*. |
|
|
295
|
+
| `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*. |
|
|
296
|
+
| `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
297
|
| `autoReloadPage` | `true` | `dsh web`: reload the page automatically when the host boot token changes. |
|
|
149
298
|
| `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
299
|
| `healthPath` | `/__dsh_autorestart/health` | Health check route. Returns the boot id, the pid, the phase and the self-check flags. |
|
|
@@ -152,9 +301,10 @@ show up in the Plugin Manager and in the config editor.
|
|
|
152
301
|
| `backupOnRestart` | `true` | Snapshot the profile before every restart, so a restart that leaves the host unable to boot can be rolled back. |
|
|
153
302
|
| `backupIncludePlugins` | `true` | Include the installed plugin tree in that snapshot (about 117 MB here, ~3 s). `false` keeps only the configuration files. |
|
|
154
303
|
| `maxBackups` | `2` | How many snapshots are kept; older ones are deleted when a new one is written. |
|
|
155
|
-
| `rollbackTimeoutMs` | `90000` | How long the detached watcher waits for the
|
|
304
|
+
| `rollbackTimeoutMs` | `90000` | How long the detached watcher waits for the new host before it restores the snapshot. |
|
|
305
|
+
| `restartCommandTimeoutMs` | `30000` | How long the restart command may run before the plugin stops waiting for it. A command that starts the host in the foreground never returns — that is reported as "still running", not as a failure. |
|
|
306
|
+
| `rollbackCopyTimeoutMs` | `120000` | How long ONE restore step (the configuration files, then the plugin tree) may take. A restore that does not finish in time is given up and the reason is written into the rollback report. |
|
|
156
307
|
| `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
308
|
| `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
309
|
|
|
160
310
|
The two waits are independent of each other, and both default to two seconds:
|
|
@@ -174,21 +324,110 @@ work other plugins queue with a timer of their own — a memory plugin that writ
|
|
|
174
324
|
seconds after the conversation ended, a summary, an MCP callback, a log flush. That work is
|
|
175
325
|
invisible here, so "every agent is idle" does not mean "nothing is happening any more".
|
|
176
326
|
|
|
177
|
-
**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
327
|
+
**One reserve window, for the restart.** A restart has no countdown of its own: the host restarts
|
|
328
|
+
as soon as the conversations are done. `restartGraceMs` is the extra wait that leaves room for the
|
|
329
|
+
delayed work of the other plugins — and it is the **only** such window:
|
|
330
|
+
|
|
331
|
+
| Window | Governs | Default | Offered |
|
|
332
|
+
| --- | --- | --- | --- |
|
|
333
|
+
| `restartGraceMs` | the **restart** | `0` (no extra wait) | `0` / `5` / `10` / `20` / `30` / `60` seconds |
|
|
334
|
+
| `sleepCountdownSeconds` | the **shutdown** (visible!) | `20` (twenty seconds) | `0` / `10` / `20` / `30` / `60` seconds |
|
|
335
|
+
|
|
336
|
+
- **The restart gate waits `max(settleMs, restartGraceMs)`** of quiet. With the default (`0`) the
|
|
337
|
+
gate waits for the settle window alone — a restart happens as soon as the conversations are done.
|
|
338
|
+
- **The graceful shutdown has no invisible reserve.** Its **visible countdown** is the wait before
|
|
339
|
+
the machine goes off: the seconds are on the screen, the user can watch them and cancel them.
|
|
340
|
+
A second, hidden window next to it would only say the same thing twice — so there is none. The
|
|
341
|
+
shutdown starts counting down as soon as every conversation is idle.
|
|
185
342
|
- **New activity restarts the timer.** If a conversation starts working again while the host is
|
|
186
|
-
waiting, the
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`
|
|
191
|
-
(
|
|
343
|
+
waiting, the reserve starts over — the wait always follows the last activity.
|
|
344
|
+
- **Where to change them.** Settings → Auto-restart & shut down, in two titled groups:
|
|
345
|
+
*Restart settings* (the wait before the restart, the two backup switches) and *Shutdown settings*
|
|
346
|
+
(the countdown of the shutdown). The status interfaces report the restart reserve:
|
|
347
|
+
`GET /health?state=1`, `GET /health?gate=1` and `continuum_status` carry `graceMs` (the restart
|
|
348
|
+
reserve) and `effectiveSettleMs` (the window the gate really uses); `GET /sleep` carries
|
|
349
|
+
`restartGraceMs` and `countdownSeconds` (the countdown that is really running). All of them carry
|
|
350
|
+
`quietForMs` (how long the host has actually been quiet).
|
|
351
|
+
|
|
352
|
+
### Why a queued message cannot block the restart for ever
|
|
353
|
+
|
|
354
|
+
**The deadlock.** A conversation whose inbox holds a message nobody claimed yet counts as "busy"
|
|
355
|
+
for the restart gate. That is right while the message is about to be picked up — the agent claims
|
|
356
|
+
it within a second or two — but it is a deadlock when nobody ever claims it. The real case was a
|
|
357
|
+
fresh, empty conversation in the UI with one unclaimed message in its inbox: the gate stayed
|
|
358
|
+
closed, the hard deadline only logged a warning, and the restart never happened.
|
|
359
|
+
|
|
360
|
+
**The upper bound.** `queuedGraceMs` (default **60000** ms) is how long an unclaimed message may
|
|
361
|
+
hold the restart back:
|
|
362
|
+
|
|
363
|
+
- **A message claimed inside the window still blocks.** The normal case is unchanged: the agent
|
|
364
|
+
picks the message up and the restart waits for it.
|
|
365
|
+
- **A message that is still unclaimed after the window is stuck.** It stops blocking the restart,
|
|
366
|
+
and the log names the session that holds the queue (`the queued message of session <id> has been
|
|
367
|
+
waiting 61s and nobody claimed it; it no longer blocks the restart`). The restart goes on.
|
|
368
|
+
- **A running conversation always blocks — under every setting.** The bound only ever judges
|
|
369
|
+
*queued* work; a live turn is never relaxed, and no configuration can make the plugin kill one.
|
|
370
|
+
- **The timer follows the work.** A session that starts working drops its queue timer, and a new
|
|
371
|
+
queue counts from zero again.
|
|
372
|
+
- **`0` means "as before".** With `queuedGraceMs: 0` an unclaimed message blocks the restart for
|
|
373
|
+
ever, exactly like it did before this bound existed.
|
|
374
|
+
- **Where to look:** `GET /health?state=1`, `GET /health?gate=1` and `continuum_status` all report
|
|
375
|
+
`queuedGraceMs`.
|
|
376
|
+
|
|
377
|
+
### What you see while the restart waits a long time
|
|
378
|
+
|
|
379
|
+
When the hard deadline (`deferHardMs`) is over and a conversation is still running, the plugin does
|
|
380
|
+
not pretend that everything is fine — and it does not give up either:
|
|
381
|
+
|
|
382
|
+
- **The request is kept.** The pending restart stays pending, the requester stays checkpointed, and
|
|
383
|
+
the restart fires as soon as the gate is quiet again. Nothing is cancelled behind your back.
|
|
384
|
+
- **The wait becomes visible.** The log says it once a minute — "the restart has been waiting for
|
|
385
|
+
17m; `<session>` still holds it — cancel the restart or wait for that work to finish" — and
|
|
386
|
+
`GET /health?state=1` carries a `longWait` object (`since`, `reason`, `blockedBy`).
|
|
387
|
+
- **The bubble says it too.** The pending-restart pill shows one more line: "waiting for 17m:
|
|
388
|
+
`<session>` keeps holding it" (「已等 17 分:`<会话>`一直占着」).
|
|
389
|
+
|
|
390
|
+
A conversation is never killed to make the restart happen: `forceRestartWhenBusy` stays off by
|
|
391
|
+
default, and this plugin never turns it on.
|
|
392
|
+
|
|
393
|
+
### What the quiet window measures
|
|
394
|
+
|
|
395
|
+
The restart gate has two layers: **no busy conversation** and **a quiet window**
|
|
396
|
+
(`max(settleMs, restartGraceMs)`).
|
|
397
|
+
|
|
398
|
+
The quiet window answers one question: *how long ago did the last real work happen?* It is measured
|
|
399
|
+
from the LATER of two markers:
|
|
400
|
+
|
|
401
|
+
- **the last real activity signal** — a turn that started or ended (`agent/status`), a message that
|
|
402
|
+
was queued (`agent/inbox/inserted`), or a session event that says work happened (a message, a tool,
|
|
403
|
+
a turn);
|
|
404
|
+
- **the last moment a conversation was seen working** — a running turn or a queued message
|
|
405
|
+
(`busyAgents()`).
|
|
406
|
+
|
|
407
|
+
**A routine session event is not activity.** The host emits session events every two to three seconds
|
|
408
|
+
while the interface polls and synchronises (a title, a projection, a context request, a job that
|
|
409
|
+
settled). Counting those as activity reset the quiet window every couple of seconds, so a window
|
|
410
|
+
longer than the event interval could never fill: the restart never fired, however long the user
|
|
411
|
+
waited. That is why a grace window of ten seconds used to make the restart impossible.
|
|
412
|
+
|
|
413
|
+
The rule is one-sided: **an event whose type cannot be read is not activity.** When in doubt the
|
|
414
|
+
plugin restarts a little early rather than never.
|
|
415
|
+
|
|
416
|
+
**A running conversation always blocks.** The quiet window only decides *when* the restart may fire;
|
|
417
|
+
the busy check is the hard guarantee, and it is never relaxed by any setting.
|
|
418
|
+
|
|
419
|
+
**Where to look when the restart does not happen.** The status interfaces report the reason of the
|
|
420
|
+
gate next to the blockers (`GET /health?state=1` carries `reason`: `conversation-active` when a
|
|
421
|
+
conversation blocks, `settling` when only the quiet window is not full yet, `quiet` when it may go),
|
|
422
|
+
and the pending restart writes one diagnostic line into the log per minute:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
restart gate: action=wait reason=agents-busy-within-soft-window busyCount=1 quietForMs=1432 effectiveSettleMs=10000
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`action` is what the restart decision did (`wait`, `restart`, `long-wait`, …), `reason` why,
|
|
429
|
+
`busyCount` how many conversations block, `quietForMs` how long the host has been quiet, and
|
|
430
|
+
`effectiveSettleMs` the window it has to fill.
|
|
192
431
|
|
|
193
432
|
### Platform note
|
|
194
433
|
|
|
@@ -216,23 +455,173 @@ it was started from, so a restart that leaves the host unable to boot can be und
|
|
|
216
455
|
like this one the full snapshot measures about 117 MB and takes under three seconds.
|
|
217
456
|
- **How many are kept:** the newest `maxBackups` snapshots (default `2`); older ones are deleted when
|
|
218
457
|
a new one is written.
|
|
219
|
-
- **How a failed boot is detected:**
|
|
220
|
-
|
|
221
|
-
|
|
458
|
+
- **How a failed boot is detected:** a small watcher runs **outside** the host process (so it survives
|
|
459
|
+
the host being killed). It waits for the **new** instance and probes **its** health route for
|
|
460
|
+
`rollbackTimeoutMs` (default `90000`). The new instance publishes its own port and its own boot id
|
|
461
|
+
in `<storage>/dsh-autorestart/access.json`, so the watcher never mistakes the old instance (or the
|
|
462
|
+
old port) for the new one. A healthy answer ends the watch and nothing is touched.
|
|
222
463
|
- **What happens when the host never answers:** the watcher copies the snapshot back over the profile
|
|
223
|
-
(configuration files, and the plugin tree when the snapshot holds one)
|
|
464
|
+
(configuration files, and the plugin tree when the snapshot holds one) and writes
|
|
224
465
|
`last-rollback.json` next to the checkpoint — including the tail of the newest crash log written
|
|
225
|
-
*after* this restart
|
|
466
|
+
*after* this restart and the reason the restore failed, if it did. It does **not** start a second
|
|
467
|
+
copy of the host: the instance that was launched is left alone, so there is never a second window
|
|
468
|
+
or a second web server.
|
|
469
|
+
- **The hosted mode has the same safety net.** With `restartMode: command` the restart command
|
|
470
|
+
usually kills the very process the plugin runs in, so the plugin cannot confirm anything itself.
|
|
471
|
+
The watcher is therefore started **before** the command runs, and it is the one that decides
|
|
472
|
+
whether the new instance came up.
|
|
473
|
+
- **The command is judged, not assumed.** A restart command that cannot be started, or that exits
|
|
474
|
+
with an error, is reported as a failure (with its exit code and output) and the snapshot is
|
|
475
|
+
restored — it is never reported as a successful restart.
|
|
226
476
|
- **How you learn about it:** the next boot reports it. `continuum_status` gains a `lastRollback`
|
|
227
|
-
object (`at`, `waitedMs`, `rolledBack`, `backupDir`, `crashLog`, `crashTail`), and the
|
|
228
|
-
clears the report so it is never mistaken for a fresh one.
|
|
477
|
+
object (`at`, `waitedMs`, `rolledBack`, `reason`, `backupDir`, `crashLog`, `crashTail`), and the
|
|
478
|
+
next restart clears the report so it is never mistaken for a fresh one.
|
|
229
479
|
- **Turning it off:** `backupOnRestart: false` skips the snapshot entirely (no rollback point, and no
|
|
230
480
|
watcher), which is what a user who keeps their own backups may prefer. `backupIncludePlugins:
|
|
231
481
|
false` keeps the snapshot but limits it to the configuration files.
|
|
232
482
|
|
|
233
483
|
The rollback is deliberately conservative: it only runs when the relaunched host never answers its
|
|
234
484
|
own health route. A slow start inside `rollbackTimeoutMs` is never treated as a failure, and a
|
|
235
|
-
healthy host is never touched.
|
|
485
|
+
healthy host is never touched. The restore itself is bounded as well: every copy step gives up after
|
|
486
|
+
`rollbackCopyTimeoutMs` (default `120000`) and says so in the report.
|
|
487
|
+
|
|
488
|
+
### The access link after a restart
|
|
489
|
+
|
|
490
|
+
The boot token of `dsh web` is created **per process**, so the link the browser holds answers `401`
|
|
491
|
+
after a restart — the user would be locked out of their own interface. The plugin therefore writes
|
|
492
|
+
the access link of the running instance into `<storage>/dsh-autorestart/access.json` (mode `0600`:
|
|
493
|
+
the file holds a secret), and reports it in `continuum_status` and in the log. The browser half
|
|
494
|
+
follows that link when the one it holds is stale, so the session comes back on its own.
|
|
495
|
+
|
|
496
|
+
### Start at login: the supervisor of this plugin
|
|
497
|
+
|
|
498
|
+
A hosted host (`dsh web`) that was started by hand does not come back when it dies — and a restart
|
|
499
|
+
that nobody brings back is a restart the user has to do themselves. The plugin can install a
|
|
500
|
+
**supervisor**: a small standalone process that starts the host, watches it, and starts it again when
|
|
501
|
+
it is gone.
|
|
502
|
+
|
|
503
|
+
- **It is a separate process.** The plugin lives inside the host, so it can never be the thing that
|
|
504
|
+
brings the host back. The supervisor is a zero-dependency Node script (`supervisor/supervisor.js`,
|
|
505
|
+
shipped with the package) that the plugin copies into `<storage>/dsh-autorestart/supervisor/` when
|
|
506
|
+
it is installed.
|
|
507
|
+
- **What it does:** start the host with the command from `supervisor.json`, wait for it to exit, back
|
|
508
|
+
off (1 s → 30 s; the delay is only reset after a start that actually worked) and start it again.
|
|
509
|
+
When the host does not come back at all, the snapshot of the last restart is restored.
|
|
510
|
+
- **How it is registered (no administrator rights):**
|
|
511
|
+
- **Windows:** a scheduled task `dsh-autorestart-web` (`/SC ONLOGON /RL LIMITED`). If the machine
|
|
512
|
+
forbids ordinary users from creating scheduled tasks, the plugin falls back to a `.cmd` in the
|
|
513
|
+
startup folder (same zero rights, same "at login" semantics).
|
|
514
|
+
- **Linux / WSL:** a systemd **user** unit
|
|
515
|
+
(`~/.config/systemd/user/dsh-autorestart-web.service`). A machine without a service manager (a
|
|
516
|
+
container, a WSL without systemd) has no "at login" at all — there the plugin says so and leaves
|
|
517
|
+
the start command to the user.
|
|
518
|
+
- **The guide of every platform.** "Start at login" means something different everywhere, so the
|
|
519
|
+
plugin hands out one piece of advice per platform (the settings page shows it, with the commands
|
|
520
|
+
you can copy):
|
|
521
|
+
|
|
522
|
+
| platform | what the plugin does |
|
|
523
|
+
|---|---|
|
|
524
|
+
| **Windows** | a scheduled task (`/SC ONLOGON /RL LIMITED`, no administrator); when the machine refuses it, a `.cmd` in the startup folder. The command names a **real node** + `supervisor.js` — never the application executable. |
|
|
525
|
+
| **Linux** | a systemd **user** service — plus the `loginctl enable-linger <user>` hint, because without the linger it only starts *after* a login, not at boot. |
|
|
526
|
+
| **WSL with systemd** | the same user unit, plus the linger hint. |
|
|
527
|
+
| **WSL without systemd** | no "at login" at all: two ways out — ① turn systemd on inside WSL (`/etc/wsl.conf` → `[boot] systemd=true` + `wsl --shutdown`; **a system configuration change**) or ② let Windows start the distribution (`wsl.exe -d <distro> -e <command>`; **needs a Windows-side action, but it is much closer to a real boot autostart**). Inside WSL the plugin **never** recommends a container-orchestration setup — the orchestrator of WSL is Windows. |
|
|
528
|
+
| **container / k8s** | no promise at all: the orchestrator brings it back (`restart: always`). |
|
|
529
|
+
| **unknown** | nothing is installed and nothing is guessed: the plugin says why and asks the user to choose. |
|
|
530
|
+
|
|
531
|
+
A host that is only **guessed** to be WSL (the mounted Windows drive is the only evidence) gets no
|
|
532
|
+
automatic anything: the plugin says "not sure, pick one yourself" and changes nothing.
|
|
533
|
+
- **It is idempotent:** installing twice leaves exactly ONE record (the task/unit name is fixed, the
|
|
534
|
+
task is created with `/F`, and the daemon takes an exclusive lock — two supervisors are impossible).
|
|
535
|
+
- **A failed installation leaves nothing behind:** when a step fails half way, the unit (or the task)
|
|
536
|
+
that was already written is taken away again and the rollback is reported in the step list — a
|
|
537
|
+
half-installed supervisor would start a stale one at the next login.
|
|
538
|
+
- **The program is never repeated:** a step carries the program in `file` and the arguments in `args`,
|
|
539
|
+
so the record runs `systemctl --user enable --now …` and never `systemctl systemctl …` (which every
|
|
540
|
+
systemd answers with "Unknown command verb systemctl").
|
|
541
|
+
- **It is reversible:** the previous `restartMode` / `restartCommand` are saved before anything
|
|
542
|
+
changes and put back by the uninstall. The settings page has one button for it ("Settings →
|
|
543
|
+
Auto-restart & shut down → Start at login"), and the manual way out is printed in the card and in
|
|
544
|
+
the log. When a step of the uninstall fails, the pending cleanup is written down instead of being
|
|
545
|
+
dropped silently — but only when something is really left, and it lives next to the supervisor
|
|
546
|
+
directory (`<stateDir>/UNINSTALL-PENDING.json`), so writing it cannot bring that directory back.
|
|
547
|
+
- **The first-run card:** a hosted host is asked ONCE whether the supervisor may be installed. The
|
|
548
|
+
card says what it would change, which rights it needs, and how to remove it again. Choosing "keep
|
|
549
|
+
the way I start it" changes nothing — the plugin only records that a supervisor of your own is
|
|
550
|
+
already there (so it never installs a second one).
|
|
551
|
+
|
|
552
|
+
The desktop app is **not** asked: it brings itself back up on its own (`restartMode: app`), so the
|
|
553
|
+
question would be noise there. It is also **refused** the installation itself — a login record that
|
|
554
|
+
starts the supervisor would start the Electron client, and that would open a **second** copy of the
|
|
555
|
+
app at every login. `plan` / `install` answer `ok: false` with `error: "desktop-manages-itself"` and
|
|
556
|
+
an empty step list there, and the settings page shows `mode: "app"` (`restartCapable: true`) instead
|
|
557
|
+
of the scary "restart not available".
|
|
558
|
+
|
|
559
|
+
The record that starts the supervisor always names a **real Node interpreter**: the process itself
|
|
560
|
+
when it is node, otherwise the runtime that ships with the installation
|
|
561
|
+
(`resources/runtime/primary-runtime/dependencies/node/bin/node.exe`), otherwise the `node` of the
|
|
562
|
+
PATH. The application executable is never used as a stand-in for node, and when no interpreter is
|
|
563
|
+
found the installation is refused with the reason.
|
|
564
|
+
|
|
565
|
+
### How this host was started (the startup matrix)
|
|
566
|
+
|
|
567
|
+
A hosted web host can be started in a dozen ways, and the plugin has to behave correctly in every
|
|
568
|
+
one of them. The classification is a **pure function** (`lib/startup-mode.js`): it takes the parent
|
|
569
|
+
chain, the environment markers, the container markers and the platform, and answers
|
|
570
|
+
`{ mode, kind, evidence, selfReplay, canInstallSupervisor, suggestion }`.
|
|
571
|
+
|
|
572
|
+
| How it was started | `mode` | What the plugin does |
|
|
573
|
+
|---|---|---|
|
|
574
|
+
| a plain terminal (`dsh web`) | `bare` | the supervisor of this plugin MAY be installed; otherwise the derived command (after your confirmation) |
|
|
575
|
+
| `nohup … &` / `setsid` | `bare` | the same — but the restart command is **detached** (`setsid nohup …`), so it does not die with the shell that started it |
|
|
576
|
+
| a supervisor script (`supervise-*.sh`) | `external` | graceful exit only (`selfReplay: refused`) — the script brings the host back |
|
|
577
|
+
| a systemd unit | `external` | graceful exit only (evidence: `INVOCATION_ID` or the parent chain) |
|
|
578
|
+
| pm2 / forever | `external` | graceful exit only |
|
|
579
|
+
| docker / compose / kubernetes | `container` | no "at login" at all: the compose fragment (`restart: always`) is the answer, and no supervisor is installed inside a container |
|
|
580
|
+
| a Windows scheduled task / an NSSM service | `external` | graceful exit only |
|
|
581
|
+
| WSL, started by a Windows process | `unknown` | nothing is written: the parent is invisible in WSL — pick the plain terminal (install your own supervisor) or write your own start command (optional) |
|
|
582
|
+
| `npx` / `pnpm dlx` | `bare` (usable) or `unknown` | usable when the resolved script path is in `argv[1]`; otherwise nothing is guessed |
|
|
583
|
+
| another DSH plugin spawned it | `external` | graceful exit only — never a second host |
|
|
584
|
+
| tmux / screen / an SSH session | `bare` | the command is replayed, but **detached from the session** (a graceful exit would break the session) |
|
|
585
|
+
|
|
586
|
+
**The two rules that matter more than the table:** only a `bare` host may get a self-replay command
|
|
587
|
+
or the supervisor of this plugin, and an `unknown` host gets `selfReplay: 'unknown'` and nothing
|
|
588
|
+
else — the plugin would rather say "I cannot tell" than start a second host next to the first.
|
|
589
|
+
|
|
590
|
+
The settings page shows the verdict **with its evidence** ("Started by … — ancestor:1234: systemd"),
|
|
591
|
+
so it is visible WHY the plugin says what it says.
|
|
592
|
+
|
|
593
|
+
### "Keep the way I start it": the derived restart command (E) and the dry run (B)
|
|
594
|
+
|
|
595
|
+
When the user keeps their own way of starting the host, the plugin can still write down a command
|
|
596
|
+
that starts it again — but only if it can be derived **without guessing**. A wrong command is worse
|
|
597
|
+
than no command: it looks like a restart and leaves the user with a dead host.
|
|
598
|
+
|
|
599
|
+
The candidate is built from what the process itself knows: `[process.execPath, ...process.execArgv,
|
|
600
|
+
...process.argv.slice(1)]` plus the working directory. The **priority** decides what happens next:
|
|
601
|
+
|
|
602
|
+
| Level | Situation | What is written |
|
|
603
|
+
|---|---|---|
|
|
604
|
+
| **E0** | The supervisor of this plugin started this instance | Nothing — the restart is asked of the supervisor over its control channel (the plugin writes the rollback request and lets the supervisor kill and start the host). |
|
|
605
|
+
| **E1** | An **external** supervisor is in charge (systemd, a user script, pm2) | **Not** a self-replay command: `restartCommand` becomes a graceful exit (`kill -TERM <pid>` / `taskkill /PID <pid>`), because the other supervisor starts the host again. A second supervisor would fight over the port. |
|
|
606
|
+
| **E2** | No supervisor and no init (a container) | The command is handed out together with a compose fragment (`restart: always`); no automatic start is promised. |
|
|
607
|
+
| **E3** | No supervisor, an init is available | The derived command — after the user confirmed the dry run. |
|
|
608
|
+
|
|
609
|
+
The **refusals** (each one has a code, and each one names the way out): the desktop app (it restarts
|
|
610
|
+
itself), a host without a web server, a command line that cannot be read back, an external
|
|
611
|
+
supervisor, a container without an init, and a port that changes on every start (the cookie of the
|
|
612
|
+
browser would die with it — the port has to be fixed).
|
|
613
|
+
|
|
614
|
+
The **dry run** (B) is the promise: the command, the working directory, the port and the health URL
|
|
615
|
+
are shown **before** anything is written. Only an explicit confirmation writes the command into the
|
|
616
|
+
profile configuration; the values that were in effect before are kept, so the settings page can put
|
|
617
|
+
them back with one click.
|
|
618
|
+
|
|
619
|
+
- The dry run: `POST /__dsh_autorestart/supervisor` with `{ "action": "dry-run" }` (or the tool
|
|
620
|
+
`continuum_supervisor` with `action: "dry-run"`) — it writes nothing.
|
|
621
|
+
- The confirmation: `{ "action": "apply", "confirmed": true }` — without `confirmed` the Host
|
|
622
|
+
answers `400 confirmation-required` and writes nothing.
|
|
623
|
+
- The undo: `{ "action": "revert", "confirmed": true }` — the previous `restartMode` /
|
|
624
|
+
`restartCommand` are written back.
|
|
236
625
|
|
|
237
626
|
### The one-time install question
|
|
238
627
|
|
|
@@ -347,7 +736,8 @@ record that cannot be read is dropped — asking once more is much better than a
|
|
|
347
736
|
itself off for ever because of a half-written file.
|
|
348
737
|
|
|
349
738
|
**For the troubleshooting.** `GET /sleep` reports `armedAt` (when the shutdown was armed) next to
|
|
350
|
-
`phase`, `blockers` and
|
|
739
|
+
`phase`, `blockers`, `countdownSeconds` (the countdown that is really running) and
|
|
740
|
+
`restartGraceMs` (the restart reserve).
|
|
351
741
|
|
|
352
742
|
### Troubleshooting: the machine did not switch off
|
|
353
743
|
|
|
@@ -378,11 +768,13 @@ waiting for something. Here is what it can be, and what to do about it:
|
|
|
378
768
|
`待执行的重启`). That is normal queueing, not a fault: the restart runs first, the sessions
|
|
379
769
|
come back with it, and the countdown continues afterwards. **What to do:** nothing — wait for
|
|
380
770
|
the restart. (You can also cancel the restart with `continuum_cancel_restart`.)
|
|
381
|
-
3. **The work just ended and the
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
771
|
+
3. **The work just ended and the countdown has not started yet.** The shutdown has **no invisible
|
|
772
|
+
reserve**: as soon as every conversation is idle the countdown starts. **What you see:**
|
|
773
|
+
`phase` is `waiting` while `blockers[]` is empty and `remainingMs` is at its full value only
|
|
774
|
+
while the conversations are still busy — with an empty list the countdown is already running.
|
|
775
|
+
**What to do:** nothing; the countdown is on the screen and it can be cancelled there. (The
|
|
776
|
+
restart has its own window — `restartGraceMs`, default none — and the two never influence each
|
|
777
|
+
other.)
|
|
386
778
|
4. **Somebody started working during the countdown.** The countdown goes back to "waiting" and
|
|
387
779
|
`remainingMs` returns to its full value. **What you see:** the message says that somebody
|
|
388
780
|
started working again, and `blockers[]` lists who. **What to do:** let them finish.
|
|
@@ -455,13 +847,19 @@ more whether the button should stay. The answer is written into the profile conf
|
|
|
455
847
|
settings section and the prompt always show the same value.
|
|
456
848
|
|
|
457
849
|
**The settings section.** *Settings → Auto-restart & shut down* is a section of its own in the
|
|
458
|
-
settings sidebar (right below the Agent presets). It
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
- **
|
|
463
|
-
|
|
464
|
-
|
|
850
|
+
settings sidebar (right below the Agent presets). It is split into two titled groups — the restart
|
|
851
|
+
settings first, the shutdown settings after them:
|
|
852
|
+
|
|
853
|
+
- **Restart settings**
|
|
854
|
+
- **Wait before the restart** (`restartGraceMs`) — the dropdown `0` / `5` / `10` / `20` / `30` /
|
|
855
|
+
`60` seconds. `0` (the default) means "no extra wait": the host restarts as soon as the
|
|
856
|
+
conversations are done.
|
|
857
|
+
- **Backup before a restart** (`backupOnRestart`) — whether the pre-restart snapshot is taken at all.
|
|
858
|
+
- **Include the installed plugins in the backup** (`backupIncludePlugins`) — whether that snapshot
|
|
859
|
+
also contains the plugin directory of the profile. It is greyed out when the backup itself is off.
|
|
860
|
+
- **Shutdown settings**
|
|
861
|
+
- **Shutdown countdown seconds** (`sleepCountdownSeconds`) — the dropdown `0` / `10` / `20` / `30` /
|
|
862
|
+
`60`. `0` means "no countdown": the machine is switched off as soon as every conversation is idle.
|
|
465
863
|
|
|
466
864
|
A change is written into the profile configuration at once (through the plugin's own choice route)
|
|
467
865
|
and is read back from the host, so the section never shows a value the host did not take. A malformed
|
|
@@ -479,6 +877,24 @@ was only recognised as WSL because the Windows drive is mounted — a hint, not
|
|
|
479
877
|
`lastRun.dryRun` marks the last run as a rehearsal that switched nothing off; and
|
|
480
878
|
`backupOnRestart` / `backupIncludePlugins` report the effective scope of the pre-restart backup.
|
|
481
879
|
|
|
880
|
+
**The record of the last restart.** `GET <healthPath>?state=1` and the `continuum_status` tool both
|
|
881
|
+
report `lastRestart`: the snapshot that was taken before the restart (`snapshot.dir`,
|
|
882
|
+
`snapshot.stamp`, `snapshot.atLocal` — the local time, because the directory name is UTC —,
|
|
883
|
+
`snapshot.durationMs`, `snapshot.pruned`) and the verdict of the instance that came up
|
|
884
|
+
(`result` is `pending`, `ok` or `failed`, plus `verifiedAt`, `durationMs` and the four boot checks
|
|
885
|
+
`listener` / `routes` / `plugin` / `recovery`). A restart whose new instance never came up stays
|
|
886
|
+
`pending` — that is visible too. The file behind it is
|
|
887
|
+
`<stateDir>/last-restart-verdict.json`.
|
|
888
|
+
|
|
889
|
+
**Who starts this host.** The `supervisor` block of the same payload says `mode`
|
|
890
|
+
(`app` / `installed` / `external` / `unknown` / `off`), `restartCapable`, and — since a second supervisor
|
|
891
|
+
would fight over the port — `selfReplay` (`allowed` / `refused` / `unknown` / `n/a`) plus the `external`
|
|
892
|
+
evidence (`kind`, `evidence`, `commandLine`) when somebody else is in charge. On Linux the parent
|
|
893
|
+
chain of the host is read (`/proc/<pid>/cmdline`), so a `supervise-*.sh` script of the user is
|
|
894
|
+
recognised. When nobody can be identified the mode is `unknown`, never a silent "bare host": the
|
|
895
|
+
plugin then refuses to replay its own command. The **desktop app** is never `unknown`: it brings
|
|
896
|
+
itself back up, so it reports `mode: "app"` with `restartCapable: true` and `selfReplay: "n/a"`.
|
|
897
|
+
|
|
482
898
|
**When the shutdown command does not start.** The host waits for the real answer of the process. If
|
|
483
899
|
the command cannot start (missing binary, denied permission), the shutdown does **not** pretend it
|
|
484
900
|
worked: the phase goes back to `idle`, the reason is reported ("the shutdown command did not start
|
package/assets/screenshot-1.png
CHANGED
|
Binary file
|
package/assets/screenshot-2.png
CHANGED
|
Binary file
|
package/assets/screenshot-3.png
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|