dsh-autorestart 0.5.0 → 0.6.1

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/docs/README.zh.md CHANGED
@@ -18,7 +18,7 @@
18
18
  - 多个会话同时要求重启时合并成一次,且每个请求者都会被唤醒;
19
19
  - 带 goal 的会话不会把重启卡死:重启前自动**解除(disarm)**它的 goal(当前那一轮照常跑完,什么都不丢),重启之后自动重新上链;
20
20
  - 重启前有两道检查:宿主侧查一次,独立 helper 在杀进程前再查一次;
21
- - 界面上能看到状态:输入框工具行左侧的"待重启 · 被 N 个会话占用"小胶囊、左侧栏会话标题旁的圆点;快重启时圆点呼吸,点胶囊看详情;
21
+ - 界面上能看到状态:输入框工具行左侧的"待重启 · 等待 N 个任务结束"小胶囊、左侧栏会话标题旁的圆点;快重启时圆点呼吸,点胶囊看详情;
22
22
  - 可配置:静默窗口(默认 2 秒)、杀进程前等待(默认 2 秒)、忙碌时是否允许强制重启等。
23
23
  - 同一套"等到大家都安静"的想法还做成了**收工关机**:左侧栏最底部(设置按钮旁边)一个月亮按钮 → 等所有会话收工 → 屏幕上倒数(一张浮动卡片:大字 + 鲸鱼娘)→ **关掉整台电脑**(见「收工关机」)。
24
24
 
@@ -98,10 +98,13 @@ session resume on boot, durable-goal re-arm, and an agent-callable idle-aware gr
98
98
  > **知道了**
99
99
 
100
100
  - **只有升级才会看到**:宿主把「这个功能的版本」和「已经展示过的版本」比一比(记在检查点文件的 `seenFeatureVersions` 里)。**新装**的用户不会看到这张卡 —— 安装提问已经把功能讲清楚了,答完就等于"已看过"。
101
- - **每个版本只弹一次**:点「知道了」→ 宿主记下版本 → 卡片立刻消失 → **以后再也不出现**(重启宿主不会,升级到下一个版本也不会 —— 除非那个新版本又改了同一个功能,那时才再公告一次)。
101
+ - **「看过」= 记录版本不低于当前功能版本,不是「等于」**:记录的版本只要**不低于**当前的功能版本就算看过 —— 功能停在 `0.5.0` 时,记录是 `0.6.0` 也算看过。**为什么不按等值判**:记录里写的是「当时公告带的版本」,而 0.6.0 之前公告带的是**包版本**,所以按等值判就会告诉一个早就点过「知道了」的用户「你还没见过」;那条记录只说明他在哪个版本上点过,不说明他没看过这个功能。记录根本不是版本号(比如坏记录)时也算看过 —— 宁可少打扰,也不要因为一条坏记录天天弹卡片。
102
+ - **每个功能版本只弹一次**:点「知道了」→ 宿主记下版本 → 卡片立刻消失 → **以后再也不出现**(重启宿主不会,升级发下一个版本也不会 —— 除非功能的版本比记录的更新,那时才再公告一次)。
103
+ - **公告跟的是「功能」,不是「发版」**:公告带的版本号是源码里的常量 `SLEEP_FEATURE_VERSION`(`index.js`),**不是** `package.json` 里的包版本 —— 所以发版不会把老用户已经点过「知道了」的卡片再弹回来。(0.6.0 之前公告带的是包版本,于是 0.5.0 → 0.6.0 每发一次版,「收工关机」就重弹一次。)
104
+ - **想主动再公告一次**:把 `SLEEP_FEATURE_VERSION` 调到**比最后记录的版本更新**的数即可 —— 卡片就再出现一次。但**只在功能本身变化大到必须再讲一遍时**才调(比如关机改成了完全不同的用法、按钮换了位置);**发版、修 bug、加小功能都不要动它** —— 那正是这个常量要防的坑。
102
105
  - **没点不会静默吞掉**:卡片一直挂在那里等你回答;不回答,下次启动还会再出现一次。
103
106
  - **秒数是读出来的,不是写死的**:卡片里的秒数就是当前生效的 `sleepCountdownSeconds`;月亮按钮被关掉时,卡片还会补一句「按钮目前关闭,可在设置里打开。」
104
- - **状态存在哪**:`<DSH_HOME>/storages/dsh-autorestart/state.json`(和插件的检查点同一个文件)。宿主用 `GET /announcements` 报(`{ ok, pending: [{ feature, version }] }`),用 `POST /announcements`(body `{ feature }`)记。
107
+ - **状态存在哪**:`<DSH_HOME>/storages/dsh-autorestart/state.json`(和插件的检查点同一个文件)。宿主用 `GET /announcements` 报(`{ ok, pending: [{ feature, version }] }`),用 `POST /announcements`(body `{ feature }`)记。里面的 `version` 就是功能版本,跟 `SLEEP_FEATURE_VERSION` 是同一个数。
105
108
 
106
109
  ## 工作方式
107
110
 
@@ -134,7 +137,7 @@ agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autores
134
137
 
135
138
  ## 安装
136
139
 
137
- 当前版本:**0.5.0**。
140
+ 当前版本:**0.6.1**。
138
141
 
139
142
  ```powershell
140
143
  # 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
@@ -169,6 +172,7 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
169
172
  | `restartCommand` | `''` | 自定义重启命令,支持 `{pid}` `{ppid}` `{app}` 占位符 |
170
173
  | `relaunchDelaySeconds` | `2` | 脱离的 helper 在杀掉应用**之前**等几秒(`ping -n 3`,约 2 秒) |
171
174
  | `settleMs` | `2000` | 静默窗口(ms):最后一次会话活动之后,宿主要安静这么久,重启才允许开始 |
175
+ | `restartGraceMs` | `10000` | 宽限期(ms):最后一个会话收工后,再等这么久才真正重启/关机——**下拉框:0 / 3 / 5 / 10 / 20 / 30 秒**,默认 10;`0` = 不额外等 |
172
176
  | `autoReloadPage` | `true` | `dsh web`:host 重启(bootId 变化)后让页面自动 reload |
173
177
  | `blockManualRestart` | `true` | 硬拦截 agent 用 shell 命令手动杀掉/重启 DSH,并引导到 `continuum_restart` |
174
178
  | `healthPath` | `/__dsh_autorestart/health` | 健康检查路由(返回 bootId/pid/phase/自检标志) |
@@ -181,6 +185,18 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
181
185
  | `sleepCountdownSeconds` | `20` | 收工关机在真正关机前倒数几秒——**下拉框:0 / 10 / 20 / 30 / 60 秒**,默认 20;`0` 表示**不倒数、立即关机**(仍会先等所有会话收工) |
182
186
  | `sleepCommand` | `''` | 收工关机的自定义关机命令;留空 = 自动识别宿主(Windows 用 `cmd.exe /c …`,其它平台用 `/bin/sh -c …`) |
183
187
 
188
+ ### 为什么收工后还要多等几秒
189
+
190
+ **我们只看得到 DSH 的会话。** 这个插件知道的是:某个会话还在不在跑、有没有排队的 inbox 消息、最后一个是什么时候闲下来的。它**看不到别的插件用定时器排队的活** —— 比如记忆插件在会话结束后过几秒才把这次对话写进记忆、比如一次总结、一次 MCP 回调、一次日志落盘。对我们来说这些活是不存在的,所以「所有 agent 都闲了」并不等于「现在真的没有事情在发生」。
191
+
192
+ **宽限期就是为这件事留的。** `restartGraceMs`(默认 **10 秒**)的意思是:**最后一个会话收工后,再额外等这么久,才真正重启 / 关机**。这是一层普通的、可配置的保险,**不针对任何具体插件**。
193
+
194
+ - **两条路都守它**:重启闸门等的是 `max(settleMs, restartGraceMs)` 的安静;收工关机也在**开始倒数之前**先等满这个宽限期(所以宽限期不会把已经在走的倒计时顶回去一轮)。
195
+ - **等待期间又有活,重新计时**:宽限期永远跟着「最后一次活动」走 —— 会话又开始干活,就从那一刻重新等。
196
+ - **`0` 就是「和以前一样」**:`restartGraceMs: 0` 时行为与 0.4.0 完全一致。
197
+ - **在哪改**:设置 → 自动重启与关机 → 「收工后再等几秒(给记忆/总结类插件留时间)」。
198
+ - **怎么排障**:状态接口都带着它 —— `GET /health?state=1`、`GET /health?gate=1`、`continuum_status`、`GET /sleep` 都会报 `graceMs`(配了多宽限期)和 `quietForMs`(已经安静了多久)。
199
+
184
200
  ## Agent 工具与"它怎么知道该用插件"
185
201
 
186
202
  | 工具 | 作用 |
@@ -228,6 +244,45 @@ dsh plugin --profile desktop add C:\path\to\dsh-autorestart
228
244
 
229
245
  **点一下就够了。** 月亮按钮按下就武装:宿主等所有忙会话收工,在角落里倒数 **20 秒**(浮动卡片,不遮挡界面),然后关掉整台机器。**再点一下**(或点卡片上的「算了,别关」)就取消。除此之外什么都不用用户做:没有确认框、不用点第二次。
230
246
 
247
+ ### 关机和重启谁先谁后
248
+
249
+ **一句话版本。** 点月亮按钮的意思是「**等活干完了再关电脑**」,不是「现在就算还有别的在跑也关掉」。所以:
250
+
251
+ 1. **有待重启时,关机先让路。** 队列里还有一次重启没执行时,**不会关机** —— 卡片上写的是「等这次重启完成后再关机」。先重启,会话跟着回来继续干活,等它们都收工了,机器才关。
252
+ 2. **重启之后活还在继续。** 跟着重启回来的会话是在干活的:只要还有会话在忙,关机就一直等;**所有**会话都空了才关。
253
+ 3. **关机的意图跨重启保留。** 你只按了**一次**按钮。重启不会把这个请求吃掉:它被写进检查点文件,新进程启动后接着等那些活 —— **你不需要再点一次**。
254
+ 4. **随时可以反悔。** 等待卡片上有「不关了」按钮(再点一次月亮按钮也行):一按,这个意图就彻底没了。
255
+
256
+ **意图存在哪。** 检查点文件(`<DSH_HOME>/storages/dsh-autorestart/state.json`)里的 `sleep: { armed, armedAt, countdownSeconds }`。机器关掉、关机命令没跑起来、或者你点了「不关了」,它都会被清掉。**记录坏掉就直接丢弃** —— 宁可让你再点一次,也不能因为半截文件让机器永远自己关自己。
257
+
258
+ **排障用。** `GET /sleep` 会报 `armedAt`(什么时候点的关机),旁边还有 `phase`、`blockers`、`graceMs`。
259
+
260
+ ### 故障排查:点了没关机
261
+
262
+ **下面是真实宿主返回的东西**(文案是中文,字段名不是):
263
+
264
+ ```
265
+ POST /__dsh_autorestart/sleep {"action":"arm"} # 有 3 个会话在干活
266
+ → phase=waiting armed=true remainingMs=20000(满值=没开始倒数)
267
+ message: 等 3 个任务收工,收工后开始倒数。
268
+
269
+ # 制造一个待执行的重启之后:
270
+ GET /__dsh_autorestart/sleep
271
+ → phase=waiting armed=true remainingMs=20000
272
+ message: 等这次重启完成后再关机……
273
+ blockers: [{"sessionId":"restart-pending","kind":"restart-pending",
274
+ "label":"待执行的重启","name":"待执行的重启"}]
275
+ ```
276
+
277
+ **`remainingMs` 是满值 = 倒数还没开始**,也就是机器在等什么东西。下面是可能的原因,以及该怎么办:
278
+
279
+ 1. **还有任务在跑。** `blockers[]` 列的就是它们,看 `kind`:`session` 是会话,`subagent` 是某个会话派出去的子智能体。**子智能体也算在干活,这是故意的** —— 否则重启会把它掐断。**怎么办:** 等它干完,或者把那个会话停掉;列表会自己变短。
280
+ 2. **有待执行的重启。** `kind` 是 `restart-pending`(`label`/`name` 都是 `待执行的重启`)。这是正常排队,不是故障:先重启,会话跟着回来,完事之后倒数自动接着走。**怎么办:** 不用管,等重启完即可(也可以用 `continuum_cancel_restart` 把这次重启取消掉)。
281
+ 3. **刚收工,还在宽限期里**(默认 10 秒,给记忆/总结类插件留落盘时间)。**你会看到:** `graceMs` 是配的窗口,`quietForMs` 是宿主已经安静了多久。**怎么办:** 等着;等待期间又有会话开始干活,计时会重新开始(这就是这个窗口的意义)。不想要这段等待就在设置里把它调成 `0`。
282
+ 4. **倒数期间又有会话开始干活。** 倒数退回「等待」,`remainingMs` 变回满值。**你会看到:** 消息里说又有谁开始干活了,`blockers[]` 列着是谁。**怎么办:** 等他们干完。
283
+ 5. **想看「到底等谁」。** `continuum_status` 的 `blockedBy[]` 和 `GET /sleep` 的 `blockers[]` 是**同一份数据**(前者给 agent 看,后者给脚本看)。每条都有 `kind`(它是什么)、`label`(会话标题)、`name`(完整称呼,比如「写代码的会话的子智能体」)。标题解析不到时 `label` 是 `null`、`name` 是空字符串 —— 这时候等待那句话就只说「等 3 个任务收工」,不带名字列表。
284
+ 6. **彻底不想关了。** 等待卡片上的取消按钮(或者再点一次月亮按钮,或者 `POST /sleep {"action":"cancel"}`)会把这个意图彻底清掉:`armed` 变回 `false`,`armedAt` 变回 `null`。
285
+
231
286
  **桌面端和 web 端行为完全一致。** 没有票据、没有额外请求头、没有隐藏模式:`POST /__dsh_autorestart/sleep` 只带一个动作,两端走的是同一条路由。脚本(或者 agent)同样可以驱动它:
232
287
 
233
288
  ```bash
@@ -463,6 +518,7 @@ lib/checkpoint.js 原子 JSON 检查点,容错读取,boot/restart/session
463
518
  lib/lifecycle.js 纯决策:bootPlan / restartDecision / selectGoalDisarms / continuationPrompt / describeState
464
519
  lib/restart.js Windows 脱离重启(WMI 主、schtasks 兜底、dryRun、自定义命令)
465
520
  lib/sleep.js 收工关机:识别真宿主机(含 WSL)、关机命令、等待→倒数→关机的状态机
521
+ lib/semver.js 版本号比较(公告去重用:只看"记录版本是否低于当前功能版本")
466
522
  locale/{en,zh}.json Plugin Manager 显示用标题/描述
467
523
  icon.svg 插件图标
468
524
  LICENSE MIT 许可(发布到插件市场需要)
@@ -519,8 +575,9 @@ or by requesting the health path with ?gate=1.
519
575
 
520
576
  - 胶囊渲染在**输入框工具行左侧**(`conversation.input.left` 槽位),是一个高度约 22px 的小胶囊,**不额外占一行**(不再是输入框上方的一整行)。
521
577
  - 只有当健康路由 `/__dsh_autorestart/health?state=1` 返回 `pendingRestart: true` **且**本会话 id 出现在 `waiters` 列表里时才会出现;别的会话等待重启时你看不到它。
522
- - 文案有多种状态:还有别的会话占着时显示「待重启 · 被 N 个会话占用」;都空闲时显示「待重启 · 空闲后自动重启」;被某个会话**自动续跑的 goal** 占着时显示「待重启 · 被某个会话自动续跑的 goal 占着」(见下面的升级 3)。
523
- - 悬停提示(原生 `title`):「等待其他会话空闲后自动重启;重启后会自动继续本会话进度」。
578
+ - 文案有多种状态:还有别的**任务**占着时显示「待重启 · 等待 N 个任务结束」;都空闲时显示「待重启 · 空闲后自动重启」;被某个**自动续跑的 goal** 占着时显示「待重启 · 被某个自动续跑的 goal 占着」(见下面的升级 3)。
579
+ - 悬停提示(原生 `title`):「等待其他任务结束(计数含"排队但还没开始"的任务)后自动重启;重启后会自动继续本会话进度」。
580
+ - **每个占用者都说清自己是什么**:普通会话用它自己的标题;**子智能体**用"创建它的那个会话"的标题来称呼(显示成「子智能体(那个会话)」)。**原始的 `session-xxxx` 永远不会推给界面** —— 取不到标题时用一个通用说法。
524
581
  - 颜色走**品牌色(蓝紫)** `var(--dsw-alias-brand-primary, #5b8def)`,**不再用告警黄(warning)色**:胶囊里的小圆点和侧边栏圆点都是品牌色,胶囊边框保持中性(`--dsw-alias-border-l1`),文字保持中性。
525
582
  - 左侧边栏会话行的**行首 16px 状态格**里也会出现一个同款 8px 圆点(`sidebar.session.row.leading` 槽位,id `autorestart-restart-dot`,order 20):纯图标,完整说明放在悬停 `title` 里,aria-label 为「待重启」/「Pending restart」。
526
583
  - 那个 16px 状态格会被**优先级更高的行状态**(ask / 未读 / 运行中)替换,所以圆点只在该行**空闲**时出现。
@@ -572,8 +629,12 @@ or by requesting the health path with ?gate=1.
572
629
  }
573
630
  ```
574
631
 
575
- - `blockedBy[]` 的每一项在原有字段(`sessionId`、`reason`)之外,可能多一个**可选的 `title`**:Host 会尽力从会话控制器(`ctx.get('sessionController').list({})`)里读出该会话的标题。
576
- - 这是**尽力而为**:查不到标题就**不带 `title` 字段**,原有字段和行为完全不变;会话控制器缺失、抛错或没有标题都**不会**让这条路由失败。
632
+ - `blockedBy[]` 的每一项在原有字段(`sessionId`、`reason`)之外,还带两个字段:
633
+ - `kind`:`"session"` 或 `"subagent"` —— 这个占用者是普通会话还是**子智能体**(子智能体用 `agent.origin === 'subagent'` 判定;分叉会话的 `origin` 为空,仍然是普通会话)。
634
+ - `label`:**它该被怎么称呼** —— 普通会话用自己的标题;子智能体用「创建它的那个会话」的标题(所以界面显示成「子智能体(那个会话)」)。取不到就是 `null`,界面改用通用说法。
635
+ - 另外还有 `name`(完整的称呼,如「写代码的会话的子智能体」)、`parentTitle` / `parentSessionId`(子智能体的父会话)、`title`(`label` 的旧名字,保留给老客户端)。
636
+ - 这些字段是**尽力而为**:会话控制器缺失、抛错或没有标题都**不会**让这条路由失败,只是 `label` 为 `null`;**原始的 `session-xxxx` 永远不会被当成名字推给界面**。
637
+ - 标题查询结果会缓存约 **5 秒**(客户端每 2 秒轮询一次,不会把会话列表打成风暴)。
577
638
  - 标题查询结果会缓存约 **5 秒**(客户端每 2 秒轮询一次,不会把会话列表打成风暴)。
578
639
  - `blockedBy[]` 里的 `reason` 可能是 **goal 相关**的:`goal-active`(持有一个会自动续跑的已武装 goal)、`goal-exhausted`(goal 的轮次额度用完,无法继续)等;此时原本的占用原因保留在 `busyReason` 里。
579
640
  - `parkUrl` 是停泊路由的绝对路径(由配置的 `healthPath` 前缀推导),客户端优先用它,而不是自己猜路径。