dsh-autorestart 0.8.3 → 0.8.5
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/CHANGELOG.md +61 -0
- package/README.md +78 -18
- package/client.js +581 -9
- package/docs/README.zh.md +23 -8
- package/index.js +727 -47
- package/lib/backup.js +219 -4
- package/lib/checkpoint.js +93 -3
- package/lib/restart-phase.js +5 -0
- package/locale/en.json +21 -1
- package/locale/zh.json +21 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.5 — 2026-10-30
|
|
4
|
+
|
|
5
|
+
### 备份
|
|
6
|
+
|
|
7
|
+
- **每份备份都有身份,只剩「安全份」能被自动回滚**(用户决定,2026-10-30):备份目录里多了一张 `meta.json`,把每份拷贝标成 `known-good`(安全份)/ `snapshot`(本次快照)/ `unknown`(旧备份,未验证)。第一次安装时种子一份安全份;每次启动成功(健康检查通过)后,把这次重启前拍的那份提升为安全份;重启失败时只回到最新的安全份——**本次快照永远不会被当成回滚目标**,因为它可能正是坏掉的那一份(改坏插件再点重启就是这个场景)。
|
|
8
|
+
- **没有安全份就不动任何文件**:回滚前先找 newest known-good,找不到就零还原,报告里写 `no-known-good: <原因>`,看门狗也不 arm(没有可还原的目标,挂上去只会再写一遍坏配置)。
|
|
9
|
+
- **保留 3 份,安全份受保护**:默认 `maxBackups = 3`,超出删最旧的那份,但 `known-good` 不会被 prune 掉。
|
|
10
|
+
- **旧备份不会被自动回滚**:没有 `meta.json` 的拷贝(身份层之前的版本)标成 `unknown`,只展示、不参与自动回滚。
|
|
11
|
+
- **设置页看得见身份**:备份列表每行先写「身份 · 版本」(安全份 / 本次快照 / 旧备份(未验证)),下面是原有的时间与大小;列表上方加一段中文说明,讲清两种身份、看门狗只回滚安全份、第一次安装会生成安全份、最多保留 3 份、旧备份不会被自动回滚、手动回退会先二次确认再重启。`?state=1` 与 `health?state=1` 的 `backups.items[]` 因此多了 `kind` 与 `version` 两个字段。
|
|
12
|
+
- **可以手动回退到任意一份备份**(含更早、未验证的那份):列表每行多一个「回退到这一份」按钮,点了先弹二次确认(写清还原哪一份、当前状态会被覆盖、还原后会自动重启),**确认后才动**;取消则什么都不发。服务端新增 `POST /__dsh_autorestart/rollback-to`,只接受**备份目录名**(必须是本机列表里的那一份,路径 / 路径穿越直接拒绝),顺序是:①先把当前状态存一份快照(`reason: before-manual-rollback`,所以这次回退本身还能再退回来)②还原选中的那份 ③写 `last-manual-rollback.json` 回执(何时 / 哪一份 / 结果 / 那份安全快照)④走既有重启流程请求一次重启(插件自己绝不 kill 进程)。当前状态存不下来、或看门狗正持有回滚锁时直接拒绝(409),不会出现「还原了一半」。
|
|
13
|
+
|
|
14
|
+
### 现场
|
|
15
|
+
|
|
16
|
+
三次真机反馈说的是同一件事:**浮层说的不是真话**。先是重启被打断、备份早就结束了,「DSH 重启前备份中」却一直挂着不消失;修完这个,又出现备份做完后**整整 25 秒**界面(浮层和底部胶囊)一起说「正在重启…」而实际什么都没在重启——宿主把重启交给了 detached helper,helper 还在等自己的空闲复查,被拒之后才把阶段改回等待;再后来是「被打断之后我还有大活要干,一直悬浮很烦人」。
|
|
17
|
+
|
|
18
|
+
### 修复
|
|
19
|
+
|
|
20
|
+
- **阶段必须有去有回**:新增 `deferRestartStage()`,把「重启被推迟」的每一条路径都写成 `waiting`:闸门回收(又有会话忙)、detached helper 拒绝杀进程、重排重试、helper 调度失败;记录里带 `reason`(`deferred: conversation-active` 之类),事后能看出是推迟而不是在跑。放弃重启时一并清掉阶段。
|
|
21
|
+
- **`snapshotting` 只在真拷贝期间存在**:备份函数一返回(拷完 / 取消 / 抛错),阶段立刻离开 `snapshotting`——拷完就标 `restarting`,取消就清空,抛错就走推迟。以前从拷贝结束到真正执行重启之间那一整段(arm watchdog、写判定记录、调度 helper)都还挂着 `snapshotting`,一旦推迟就再也回不来。
|
|
22
|
+
- **`restarting` 只表示「即将 / 正在杀进程」**:备份一结束、标记它**之前**先当场复查一次空闲门,被挡就在同一次 tick 退回等待(`deferred: conversation-active after snapshot: …`),命令行不跑、watchdog 不 arm;进入 `restarting` 之后(等 helper / 等调度)**每 350ms** 复查一次,一有会话忙立刻降级回等待(`deferred: conversation-active while restarting`)并重新排上——几十秒的假 `restarting` 窗口没有了。
|
|
23
|
+
- **残留中间态不许跨进程继承**:启动时读到 `snapshotting` / `restarting` 一律改写——新进程起来是 `back`,同一进程里插件重载则是 `waiting`(`reason: boot: …`)。
|
|
24
|
+
- **同一次重启请求不重复备份**:快照是这次重启的回滚点,不因为重启要等待就作废。请求里记下本次已完成快照(`state.restart.snapshot`:请求时刻 + 目录 + 体积/文件数),重试时三个条件都满足就直接复用——**同一请求**、**目录还在**、**manifest 体积对得上**;任一不满足才重新拷贝。请求被取消 / 完成,标记随请求对象一起消失,下次重新备份。用户那次 205 MB 白拷一遍的事故不会再有。
|
|
25
|
+
- 新增 `restartPhase.reason`(状态路由与 health 简报都带)。
|
|
26
|
+
|
|
27
|
+
### 界面
|
|
28
|
+
|
|
29
|
+
- **被暂缓就说被暂缓,绝不说「正在重启」**:`waiting` 且 `reason` 以 `deferred:` 开头时,浮层文案是「**有新会话在跑,重启已暂缓**」+ 小字「等它结束会自动继续 · 已等 N 个会话」(英文 `A conversation just started — restart postponed` / `It will retry automatically when idle`);渲染优先级最高,同一次渲染里阶段还写着 `restarting` 也直接画暂缓文案,旧词一帧不留。「正在重启…」只在真的进入杀进程阶段才出现。
|
|
30
|
+
- **暂缓卡片是通知,不是进度条**:显示 **4 秒**后自动收起(胶囊继续显示「待重启 · 等待 N 个会话」),新的暂缓(新的阶段时间戳)会再通知 4 秒;「备份中」「正在重启…」不受影响,活儿还在就一直显示。
|
|
31
|
+
- **陈旧保护**:`snapshotting` 必须 10 秒内有过更新(拷贝期间每个进度 tick 都刷新 `at`),任何中间阶段 15 秒内 `at` 没变就当过期,**直接不画**。
|
|
32
|
+
- **状态轮询跟着状态提速**:重启进行中(`pendingRestart` 且阶段是等待 / 备份中 / 正在重启)时,共享轮询从 2 秒临时降到 **500ms**,被新会话打断这件事一秒内就能反映到浮层;重启结束立刻恢复 2 秒。页面隐藏时不轮询,可见性恢复立刻补查一次;同一时刻只允许一个在途请求。
|
|
33
|
+
|
|
34
|
+
### 文档
|
|
35
|
+
|
|
36
|
+
- **npm 一句话描述换成新口径**(`package.json` 的 `description`):原来只讲「等空闲→重启→续跑」,现在把「重启前快照 + 起不来自动还原 + 倒计时关机 + Windows 桌面 / 托管 dsh web(Windows / WSL / Linux)都可用」一起讲清;这句是 npm 搜索结果那一行,**必须随新版本发布才生效**。
|
|
37
|
+
- **README(英)/ docs/README.zh.md 补行为说明**:被打断时显示「重启已暂缓」而不是「正在重启」、暂缓卡片 4 秒后收起、阶段超过 15 秒无更新视为过期不再显示(胶囊仍显示等待数)。
|
|
38
|
+
|
|
39
|
+
### 验证
|
|
40
|
+
|
|
41
|
+
- `node --test plugins/dsh-autorestart/test/*.test.mjs` → **全绿**(细节见本次提交说明)。
|
|
42
|
+
- 宿主侧新增用例:拷完被拒的重启最终是 `waiting` 且 arm watchdog 那一刻已是 `restarting`;helper 门禁拒绝后阶段回到 `waiting`;另一进程残留的 `snapshotting` 不带进新进程;同进程重载残留的 `restarting` 被重置;备份完成后会话立刻变忙 → 同一次 tick 就是等待;`restarting` 期间会话变忙 → 守卫回调一触发即降级(复查周期断言落在 300–500ms);备份完成 → 暂缓 → 重试**不产生第二个快照目录**且 `snapshotBytes` 不变;取消后重新请求 → 重新备份;快照目录被删 → 重新备份。
|
|
43
|
+
- 界面侧新增用例:15 秒 / 10 秒陈旧边界;`snapshotting` 15 秒无更新不渲染、3 秒内有更新正常渲染;`waiting` + `deferred:` 渲染「重启已暂缓」且不含「正在重启」(中英各一条);真 `restarting` 仍「正在重启…」;降级后下一次渲染即改口;暂缓卡片 4 秒后收起、再暂缓再出现;运行中的卡片不会被收起;轮询 500ms / 2000ms 切换、隐藏页不发请求、慢响应单飞。
|
|
44
|
+
- 真机复现路径由主会话验证(打断一次重启,看浮层是否立刻改口、是否 4 秒后收起、重试是否不再拷一遍)。
|
|
45
|
+
|
|
46
|
+
## 0.8.4 — 2026-10-29
|
|
47
|
+
|
|
48
|
+
**只改文档与版本号,行为没有变化。**
|
|
49
|
+
|
|
50
|
+
### 文档
|
|
51
|
+
|
|
52
|
+
- **README(英)/ docs/README.zh.md 补齐 0.8.3 的真实界面行为**(npm 页面上原来还是 0.8.2 时代的旧描述):
|
|
53
|
+
- 重启阶段显示在**主界面居中浮层**(centered overlay card),**只排队等空闲时不出现**,只在真的备份 / 重启时出现,回来显示 1.5 秒后消失;
|
|
54
|
+
- 胶囊只有**一句短话**(≤12 字),长句降级为 tooltip,详情在点开的面板里;
|
|
55
|
+
- 备份进度是**真进度**(MB + 百分比),单位自适应(`92 KB` / `156 MB`),**备份快就一闪而过、不人为拉长**;
|
|
56
|
+
- 设置改动有回执:保存成功后**设置在面板内部顶部**(面板自己的 sticky 条)显示绿色「已生效 / Saved」+ 时间,3 秒淡出;失败红色「没保存成功 / Not saved」并保留宿主给的原因;
|
|
57
|
+
- 备份列表改成两行:`备份时间 / Backup time 2026-10-29 22:08`、`备份大小 / Backup size 156 MB(含已装插件 / plugins included)`,**不显示时区偏移**。
|
|
58
|
+
- 槽位表补上 `shell.overlay` 里的重启阶段浮层,并说明保存回执渲染在设置面板内部(不占窗口级浮层)。
|
|
59
|
+
|
|
60
|
+
### 版本号
|
|
61
|
+
|
|
62
|
+
- `package.json` 与 `index.js` 的 `PLUGIN_VERSION_FALLBACK` 同步到 **0.8.4**,两份 README 的「当前版本」同步。
|
|
63
|
+
|
|
3
64
|
## 0.8.3 — 2026-10-29
|
|
4
65
|
|
|
5
66
|
**重启阶段搬到界面正中:不再挤在输入框那一行被截断,靠数字说话,也不是常驻。**
|
package/README.md
CHANGED
|
@@ -36,11 +36,13 @@ round starts, so the session goes idle and the restart can proceed. The goal is
|
|
|
36
36
|
restart and continues.
|
|
37
37
|
|
|
38
38
|
The browser shows the state: a compact "pending restart" pill on the left side of the input toolbar
|
|
39
|
-
and a small dot next to the session title in the sidebar. The pill
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
and a small dot next to the session title in the sidebar. The pill carries ONE short line (12
|
|
40
|
+
characters at most: `待重启 · 等待 2 个会话` / `pending restart · waiting for 2 session(s)`) — a
|
|
41
|
+
conversation and its own subagents count as one. Everything else lives one layer deeper: the long
|
|
42
|
+
sentence is the tooltip, the count and the way out are in the floating panel the pill opens, and the
|
|
43
|
+
**stage** of a real restart (backing up / restarting / back) is shown on a **centered overlay card in
|
|
44
|
+
the middle of the shell** — never in the composer row, where a longer sentence used to be cut off.
|
|
45
|
+
The dot breathes when the restart is about to fire.
|
|
44
46
|
|
|
45
47
|
Every blocker says **what it is**: a conversation is named by its own title, and a **subagent** is
|
|
46
48
|
named after the conversation that created it ("Subagent of *that* conversation"). A raw session id is
|
|
@@ -183,7 +185,7 @@ what it adds before you install it.
|
|
|
183
185
|
| --- | --- |
|
|
184
186
|
| Agent tools (6) | `continuum_restart`, `continuum_status`, `continuum_cancel_restart`, `continuum_park_goal`, `continuum_recover_now`, `continuum_handoff` |
|
|
185
187
|
| 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 |
|
|
186
|
-
| 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` —
|
|
188
|
+
| 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` — the shutdown countdown card and the **centered restart-stage overlay** (`autorestart-sleep-overlay`, `autorestart-restart-overlay`); `settings.section` — the section "Auto-restart & shut down" in the settings page (`dsh-autorestart`), which also carries the save receipt inside the panel itself (no overlay slot of its own) |
|
|
187
189
|
| System prompt | One "Restart policy" section that tells the agent how to ask for a restart and what the plugin will do |
|
|
188
190
|
| 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 |
|
|
189
191
|
| Profile row | One bundle row, `dsh-autorestart` |
|
|
@@ -199,7 +201,7 @@ what it adds before you install it.
|
|
|
199
201
|
|
|
200
202
|
## Install
|
|
201
203
|
|
|
202
|
-
Current version: **0.8.
|
|
204
|
+
Current version: **0.8.5**.
|
|
203
205
|
|
|
204
206
|
From npm — the package is published under the name `dsh-autorestart`:
|
|
205
207
|
|
|
@@ -268,9 +270,12 @@ The tool reports what happened: `cancelled` (whether the restart request itself
|
|
|
268
270
|
|
|
269
271
|
### The capsule: click it for the details and for the way out
|
|
270
272
|
|
|
271
|
-
The capsule in the composer row is 22px high and ONE line wide, so it carries
|
|
272
|
-
(`待重启 ·
|
|
273
|
-
|
|
273
|
+
The capsule in the composer row is 22px high and ONE line wide, so it carries ONE short line and
|
|
274
|
+
nothing else (`待重启 · 空闲后自动重启` / `待重启 · 等待 2 个会话`, 12 characters at most — never a
|
|
275
|
+
name and never a long sentence, because that is exactly what got cut off before). The details live
|
|
276
|
+
one layer down: the long sentence is the tooltip, the **stage** (backing up / restarting / back) is on
|
|
277
|
+
the **centered overlay card in the middle of the shell**, and the count with the way out is in the
|
|
278
|
+
panel below. Clicking it (or pressing Enter/Space on it) opens a small panel above it:
|
|
274
279
|
|
|
275
280
|
- how many conversations the restart is waiting for (the same number the capsule shows);
|
|
276
281
|
- how many OTHER sessions wait for the same restart;
|
|
@@ -292,9 +297,39 @@ Two blocks of the settings page are worth a word, because they show facts instea
|
|
|
292
297
|
|
|
293
298
|
- **What is backed up right now** (right under the two backup switches): the HOST reads
|
|
294
299
|
`<DSH_HOME>/storages/dsh-autorestart/backups/`, so the number of copies, the **local time** of each
|
|
295
|
-
one, its size
|
|
300
|
+
one, its size, whether the installed plugins are in it and where it lives are all real. Only
|
|
296
301
|
the directory metadata is read — the contents are never walked (one copy can be 150 MB). With no
|
|
297
302
|
backup at all it says: "No backup yet; one is written before every restart."
|
|
303
|
+
Every copy leads with **what it is** — its identity — so the list answers the only question that
|
|
304
|
+
matters ("may this one be restored?"), then two labelled rows, so nothing runs into anything else:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
Identity Known good · version 0.8.5
|
|
308
|
+
Backup time 2026-10-29 22:08
|
|
309
|
+
Backup size 156 MB (plugins included)
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The identity is one of **known good** (a copy a boot has already proven — the only kind an automatic
|
|
313
|
+
rollback may restore), **this restart's snapshot** (taken right before a restart and not proven yet)
|
|
314
|
+
or **old backup (unverified)** (written before the identity layer existed, or without an identity
|
|
315
|
+
file — shown, never rolled back automatically). A short paragraph above the list says the same in
|
|
316
|
+
plain words, plus the rules behind it (only a known-good copy is rolled back, the first install
|
|
317
|
+
seeds one, at most three copies are kept, a manual rollback asks for confirmation and then
|
|
318
|
+
restarts). The identity and the version it was proven with come from the Host
|
|
319
|
+
(`backups.items[].kind` / `backups.items[].version`) — the browser cannot read `meta.json`.
|
|
320
|
+
|
|
321
|
+
The time is the local clock of the machine that took the copy, **to the minute and with no
|
|
322
|
+
timezone offset** (a `+08:00` only adds noise — the reader is looking at their own clock). The
|
|
323
|
+
size picks its own unit (`92 KB` / `156 MB`), keeps the tenth only when it says something
|
|
324
|
+
(`117.7 MB`, never `156.0 MB`), and the plugin scope sits in brackets right after it, because it
|
|
325
|
+
is the reason the copy is that big. Two copies are two groups with air between them.
|
|
326
|
+
- **A setting that was saved says so.** Every control writes itself the moment it is changed, so the
|
|
327
|
+
panel has to say whether the write landed: once the HOST answers `ok: true`, a green strip at the
|
|
328
|
+
**top of the settings panel itself** (inside the panel — a `sticky` row, so it stays visible while
|
|
329
|
+
the page scrolls) says **`已生效` / `Saved`** with the time (`Saved 21:40:12`) and fades after 3
|
|
330
|
+
seconds. A refused write says **`没保存成功` / `Not saved`** in red, with the reason the Host gave,
|
|
331
|
+
and it stays until the next change works or you close it. Nothing is ever announced before the
|
|
332
|
+
Host's answer arrived — rendering the page, polling it and clicking a control stay silent.
|
|
298
333
|
- **How this host was started** (hosted web only): one plain line ("How I remember you were started:
|
|
299
334
|
by your own script (the restart is its job — I stay out of it)") plus two small actions (detect
|
|
300
335
|
again / undo); the command itself is folded away under **"Technical details"** and stays closed
|
|
@@ -334,7 +369,7 @@ show up in the Plugin Manager and in the config editor.
|
|
|
334
369
|
| `continuationText` | `''` | Overrides the default continuation prompt text. |
|
|
335
370
|
| `backupOnRestart` | `true` | Snapshot the profile before every restart, so a restart that leaves the host unable to boot can be rolled back. |
|
|
336
371
|
| `backupIncludePlugins` | `true` | Include the installed plugin tree in that snapshot (about 117 MB here, ~3 s). `false` keeps only the configuration files. |
|
|
337
|
-
| `maxBackups` | `
|
|
372
|
+
| `maxBackups` | `3` | How many snapshots are kept (the current one plus the two before it); older ones are deleted when a new one is written — **except the newest known-good copy, which is never deleted**. |
|
|
338
373
|
| `rollbackTimeoutMs` | `90000` | How long the detached watcher waits for the new host before it restores the snapshot. |
|
|
339
374
|
| `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. |
|
|
340
375
|
| `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. |
|
|
@@ -416,10 +451,15 @@ with its plugins is ~150 MB). The button looked dead. It is not — and now it s
|
|
|
416
451
|
|
|
417
452
|
| Stage | What the interface shows | Where the data comes from |
|
|
418
453
|
|---|---|---|
|
|
419
|
-
| `waiting` | "
|
|
420
|
-
| `snapshotting` | The
|
|
421
|
-
| `restarting` | "Restarting
|
|
422
|
-
| `back` | "Back — carrying on where we left off" | the new host, once it boots |
|
|
454
|
+
| `waiting` | Nothing on the overlay — the **centered overlay card does not appear at all** while the gate only waits (no backup is running, nothing happens but waiting). The capsule says it in one short line ("pending restart · waiting for N sessions"). **One exception:** when a restart was interrupted by a new conversation, the card stays and says so — "A conversation just started — restart postponed" + "It will retry automatically when idle" — instead of claiming "Restarting…". | the idle gate's blockers |
|
|
455
|
+
| `snapshotting` | The **centered overlay card in the middle of the shell**: "Backing up before DSH restart" + a real progress bar, "12.3 MB / 40 MB" and a percentage; the settings page and the tooltip keep the long line, in the same words ("Backing up before DSH restart (about 12s, 78 MB done)…"). The card has NO minimum shown time: it lives exactly as long as the Host reports the stage (a 92 KB copy is gone in about 5 ms, a slow one stays as long as it runs); the size picks its own unit (`92 KB` / `156 MB`) so a small backup never reads "0 MB" | the REAL copy loop: files and bytes written, measured total |
|
|
456
|
+
| `restarting` | "Restarting…" — and ONLY while the process is really being killed: the idle gate is re-checked every 350 ms, so a conversation that starts here turns the card into the postponed one above instead of leaving a false "Restarting…" on screen | the host is on its way out |
|
|
457
|
+
| `back` | "Back — carrying on where we left off", for **1.5 seconds**, then the card is gone | the new host, once it boots |
|
|
458
|
+
|
|
459
|
+
**The overlay never hangs.** When a restart is postponed back to waiting, the stage is reset and the
|
|
460
|
+
overlay disappears at once; and a stage that has not been updated for more than **15 seconds** is
|
|
461
|
+
treated as stale and is not drawn at all (the capsule keeps showing the waiting count). A stale stage
|
|
462
|
+
can therefore never sit on top of the interface.
|
|
423
463
|
|
|
424
464
|
Three things make this honest rather than decorative:
|
|
425
465
|
|
|
@@ -523,8 +563,20 @@ it was started from, so a restart that leaves the host unable to boot can be und
|
|
|
523
563
|
`backupIncludePlugins` is turned off — the installed plugin tree (`node_modules`). Snapshots live in
|
|
524
564
|
`<storage>/dsh-autorestart/backups/<timestamp>/` with a `manifest.json` next to them. On a profile
|
|
525
565
|
like this one the full snapshot measures about 117 MB and takes under three seconds.
|
|
526
|
-
- **
|
|
527
|
-
a
|
|
566
|
+
- **The rollback target is the newest PROVEN copy, not the newest copy** (user decision,
|
|
567
|
+
2026-10-30). Every copy carries its identity in a `meta.json`: `known-good` (a copy a boot has
|
|
568
|
+
already survived), `snapshot` (taken right before a restart, not proven yet) or `unknown` (written
|
|
569
|
+
before the identity layer). The snapshot taken right before THIS restart is never the rollback
|
|
570
|
+
target — it may be the broken state itself (that is exactly the "I edited a plugin and clicked
|
|
571
|
+
restart" case). The first install seeds one known-good copy, and every boot that passes its health
|
|
572
|
+
check promotes the snapshot of that restart to known-good.
|
|
573
|
+
- **No known-good copy, no restore at all.** When nothing has been proven yet, the plugin restores
|
|
574
|
+
nothing and says why: the report carries `no-known-good: <reason>`, the profile is left exactly as
|
|
575
|
+
the failed restart left it, and no watcher is armed (a watcher with nothing to restore would only
|
|
576
|
+
write the broken state back).
|
|
577
|
+
- **How many are kept:** the newest `maxBackups` snapshots (default `3`: the current one plus the two
|
|
578
|
+
before it); older ones are deleted when a new one is written. The newest **known-good** copy is
|
|
579
|
+
never deleted, whatever the limit says — it is the only thing a failed boot can roll back to.
|
|
528
580
|
- **How a failed boot is detected:** a small watcher runs **outside** the host process (so it survives
|
|
529
581
|
the host being killed). It waits for the **new** instance and probes **its** health route for
|
|
530
582
|
`rollbackTimeoutMs` (default `90000`). The new instance publishes its own port and its own boot id
|
|
@@ -540,6 +592,14 @@ it was started from, so a restart that leaves the host unable to boot can be und
|
|
|
540
592
|
usually kills the very process the plugin runs in, so the plugin cannot confirm anything itself.
|
|
541
593
|
The watcher is therefore started **before** the command runs, and it is the one that decides
|
|
542
594
|
whether the new instance came up.
|
|
595
|
+
- **A manual rollback to ANY copy** (user decision, 2026-10-30): the settings page offers
|
|
596
|
+
"roll back to this copy" on every row, so a copy from last week — even an unverified one — can be
|
|
597
|
+
restored on purpose. It asks first (which copy, that the current state is replaced, that a restart
|
|
598
|
+
follows); the Host saves the current state as a fresh snapshot BEFORE it restores anything, writes
|
|
599
|
+
`last-manual-rollback.json`, and only then requests a normal restart (the plugin never kills the
|
|
600
|
+
host itself). The route (`POST /__dsh_autorestart/rollback-to`) takes a backup NAME only — a path
|
|
601
|
+
or a traversal is refused before anything is read — and it stands down when the watchdog already
|
|
602
|
+
holds the rollback lock.
|
|
543
603
|
- **The command is judged, not assumed.** A restart command that cannot be started, or that exits
|
|
544
604
|
with an error, is reported as a failure (with its exit code and output) and the snapshot is
|
|
545
605
|
restored — it is never reported as a successful restart.
|