dsh-autorestart 0.2.1 → 0.3.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/LICENSE CHANGED
File without changes
package/README.md CHANGED
@@ -1,317 +1,136 @@
1
1
  # dsh-autorestart
2
2
 
3
- ## 它解决什么问题
3
+ Restart DeepSeek Harness without losing the work that is currently running.
4
4
 
5
- - DeepSeek Harness 在装插件、升级、崩溃、或关掉重开时,正在跑的任务会被打断。这个插件把"重启"变成一件可恢复的事,不再需要你事后补一句"继续":
6
- - 等所有对话都空闲了才重启:不会在你或别的会话正在跑的时候把应用关掉;
7
- - 重启后自动接着干:被打断的会话自动恢复,目标(goal)会重新武装继续跑;
8
- - 多个会话同时要求重启时合并成一次,且每个请求者都会被唤醒;
9
- - 重启前有两道检查:宿主侧查一次,独立 helper 在杀进程前再查一次;
10
- - 界面上能看到状态:输入框工具行左侧的"待重启 · 被 N 个会话占用"小胶囊、左侧栏会话标题旁的圆点;快重启时圆点呼吸,点胶囊看详情;
11
- - 可配置:静默窗口(默认 2 秒)、杀进程前等待(默认 2 秒)、忙碌时是否允许强制重启等。
5
+ [![npm version](https://img.shields.io/npm/v/dsh-autorestart?style=flat-square)](https://www.npmjs.com/package/dsh-autorestart)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue?style=flat-square)](./LICENSE)
7
+ [![dsh-plugin](https://img.shields.io/github/topics/dsh-plugin?style=flat-square)](https://github.com/topics/dsh-plugin)
8
+ [![language](https://img.shields.io/badge/language-JavaScript-yellow?style=flat-square)](https://github.com/Eason-22/dsh-autorestart)
12
9
 
13
- 场景对照:
10
+ Chinese version: [README.zh.md](README.zh.md)
14
11
 
15
- | 场景 | 没有它 | 有它 |
16
- | --- | --- | --- |
17
- | Agent 正在跑长任务,DSH 被重启/崩溃 | 这一轮丢失,必须人工催"继续" | 新进程启动后自动 resume 该会话并注入续接提示,继续干活 |
18
- | Agent 有一个持久 goal(`create_goal`)跨重启 | goal 的 activation 是进程内的,重启后变回 `disarmed`,自动续跑停止 | 启动时重新 `goals.resume()`,goal round driver 立刻开始下一轮 |
19
- | Agent 想装/升级插件但"重启必打断" | 只能等或赌 | 调 `continuum_restart`:等所有 agent idle(当前这一轮跑完)再重启,回来自己接着做 |
12
+ ## What it does
20
13
 
21
- ## 适合谁 / 不适合谁
14
+ `dsh-autorestart` turns a DeepSeek Harness restart into a resumable event instead of a dead end.
22
15
 
23
- 适合:经常因为装插件、升级、应用崩溃而丢掉正在进行的长任务,希望重启后自动接续的人。
16
+ An agent can request a graceful restart. The plugin waits until every conversation is idle and the
17
+ host has been quiet for the settle window, then it restarts the host. Once the host is back, the
18
+ interrupted sessions resume automatically: a session that owns a durable goal gets that goal
19
+ re-armed so the goal round driver starts the next round, and a session without a goal receives one
20
+ continuation message that asks it to pick up where it left off.
24
21
 
25
- 不适合:无法接受应用窗口关闭再重开的人;非 Windows 桌面且未配置 restartMode 为 command 的 web 部署(重启功能不可用);正在跑关键任务的 profile(建议先在测试 profile 验证)。
22
+ Several sessions can request a restart at the same time. Those requests are coalesced into **one**
23
+ restart, and every requester is woken afterwards — not only the last one.
26
24
 
27
- > **包名说明**:原先的包名在公共 npm registry(npmjs.com)上已被其他作者占用,因此本包更名为 **dsh-autorestart**。
25
+ A goal-driven session would otherwise keep the restart waiting forever, because its goal starts the
26
+ next round as soon as the agent goes idle. The plugin therefore **disarms** such a goal before the
27
+ restart: the round that is running right now still finishes normally and nothing is lost, but no new
28
+ round starts, so the session goes idle and the restart can proceed. The goal is re-armed after the
29
+ restart and continues.
28
30
 
29
- **重启不断线 / Cross-restart continuation for DeepSeek Harness (dsh).**
31
+ The browser shows the state: a compact "pending restart" pill on the left side of the input toolbar
32
+ and a small dot next to the session title in the sidebar. The pill can be clicked for details, and
33
+ the dot breathes when the restart is about to fire.
30
34
 
31
- Agent 在 DSH 重启(插件升级、应用崩溃、用户关闭再打开)之后,可以**自动接续重启前没做完的工作**,不需要人再发一条"继续"。
32
- This Host plugin makes a DSH restart a *resumable* event instead of a dead end: a durable checkpoint, automatic
33
- session resume on boot, durable-goal re-arm, and an agent-callable idle-aware graceful restart.
35
+ Two safety gates protect the restart. The host checks that no conversation is busy, and the detached
36
+ helper checks the same gate over loopback immediately before it kills the process, so a turn that
37
+ starts in between is never killed.
34
38
 
35
- > 目标与社区插件 [dsh-phoenix](https://github.com/StvLi/dsh-phoenix) 相同,但这一版为 **Windows + DSH Desktop** 原生实现:
36
- > 不依赖 systemd,使用 WMI/schtasks 脱离将要被销毁的 Electron 进程树来重启应用。
39
+ ## Why
37
40
 
38
- ---
41
+ | Situation | Without it | With it |
42
+ | --- | --- | --- |
43
+ | An agent is running a long task and DSH is restarted or crashes | The round is lost and you have to type "continue" by hand | The new process resumes the session and injects a continuation message, so the work continues without you |
44
+ | An agent owns a durable goal (`create_goal`) that should survive a restart | The goal activation lives in the process, so after the restart it is `disarmed` and the automatic continuation stops | The plugin re-arms the goal at boot and the goal round driver starts the next round right away |
45
+ | An agent wants to install or upgrade a plugin, but a restart would cut the work short | You either wait or take the risk | Call `continuum_restart`: it waits until every agent is idle and the session continues by itself afterwards |
39
46
 
40
- ## 工作方式
47
+ ## Install
41
48
 
42
- ```
43
- agent/status(running) ──► 写 checkpoint(<DSH_HOME>\storages\dsh-autorestart\state.json,原子写)
44
- │
45
- ├─ continuum_restart 工具:checkpoint 标记 pendingResume + generation+1,轮询等 idle
46
- │ └─ 全空闲 ──► WMI 创建脱离进程树的 cmd:ping 延迟 → taskkill 整个应用 → 重新启动 exe
47
- │
48
- └─ 进程意外死亡 ──► 下一次启动时,旧 bootPid 记录里 running=true 的会话被判定为“中断”
49
- │
50
- 新进程 boot ──► apply():beginBoot()(新 pid ⇒ 新 bootId)──► recoveryDelayMs 后恢复
51
- │
52
- ┌──────────────┴──────────────┐
53
- session 有 active+disarmed 的 goal 无 goal(或 goal 不可续)
54
- │ │
55
- ctx.goals.resume(agent, ref) agent.followup(<续接消息>)
56
- │ │
57
- goal round driver 自动开下一轮 一条带 <continuum_resume> 标记的续接消息
58
- ```
49
+ From npm — the package is published under the name `dsh-autorestart`:
59
50
 
60
- - **恢复是"至多一次/每次启动"的**:恢复尝试先原子落盘 `resumeAttempts`,再执行副作用;同一 bootId 内的热重载(HMR)不会重复恢复;
61
- 未变化的 pid 不会被误判为重启(`bootPid` + `bootId`)。
62
- - **冷会话用官方入口恢复**:`ctx.sessionController.resolveAgent(sessionId)`(会做 agent preset 组装);
63
- 续接消息通过 `agent.followup(...)` 进入该会话的 next-turn 队列 —— 不用 `sessionController.prompt`,
64
- 因为它的进程内签名是 `prompt(request, signal)` 且会硬编码 human `user` source。
65
- - **续接消息是普通 user message**,`source.kind='continuum'`(非 human authority)、内容明确标记
66
- `<continuum_resume>`,模型能看到完整历史后自行判断中断点。
67
-
68
- ## 安装
69
-
70
- ```powershell
71
- # 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
72
- # plugin_manager install_bundle target = <本目录绝对路径>
73
- # 等价命令行(需要 dsh CLI):
74
- dsh plugin --profile desktop add C:\path\to\dsh-autorestart
51
+ ```sh
52
+ dsh plugin install dsh-autorestart
75
53
  ```
76
54
 
77
- 安装后行 id 为 `dsh-autorestart`。本机已经装好并处于 `active`。
78
-
79
- ## 配置
80
-
81
- 所有键都有默认值,写在 `cordis.patch.yml` 的 insert 行里;要覆盖就在 profile 的
82
- `~/.dsh/profiles/<profile>/cordis.patch.yml` 里加同名 `- id: dsh-autorestart` 行(**整段替换 config**)。
83
- 插件同时导出真实的 `Config` schema(schemastery),所以 Plugin Manager / 配置编辑器里能看到键名、类型与默认值
84
- (见 `GET /__dsh_autorestart/health` 的 `configSchema: true`)。
85
-
86
- | 键 | 默认 | 说明 |
87
- | --- | --- | --- |
88
- | `enabled` | `true` | 关掉整个插件 |
89
- | `stateFile` | `''` | 检查点路径;空 = `<DSH_HOME>/storages/dsh-autorestart/state.json` |
90
- | `resumeOnBoot` | `true` | 启动时是否自动恢复 |
91
- | `recoveryDelayMs` | `6000` | 启动后等多久再恢复(给 session store / model 就绪留时间) |
92
- | `maxSessions` | `4` | 一次启动最多恢复几个会话 |
93
- | `maxResumeAttempts` | `2` | 每个会话最多尝试恢复几次(跨启动累计,防重启循环) |
94
- | `deferPollMs` | `3000` | 等 idle 的轮询间隔 |
95
- | `deferSoftMs` | `300000` | 软期限:还在忙就告警 |
96
- | `deferHardMs` | `900000` | 硬期限:超过后告警;只有 `forceRestartWhenBusy` 打开时才强制重启 |
97
- | `deferPolicy` | `auto` | `auto` = 硬期限后按 `forceRestartWhenBusy` 决定(默认继续等);`wait` = 一直等 |
98
- | `forceRestartWhenBusy` | `false` | 硬期限到了但还有会话在跑时是否强制重启;默认 `false` = **不打断任何会话**,一直等到其他会话结束 |
99
- | `restartMode` | `app` | `app` = 重启 DeepSeek Harness 桌面应用;`command` = 跑 `restartCommand` |
100
- | `restartCommand` | `''` | 自定义重启命令,支持 `{pid}` `{ppid}` `{app}` 占位符 |
101
- | `relaunchDelaySeconds` | `2` | 脱离的 helper 在杀掉应用**之前**等几秒(`ping -n 3`,约 2 秒) |
102
- | `settleMs` | `2000` | 静默窗口(ms):最后一次会话活动之后,宿主要安静这么久,重启才允许开始 |
103
- | `autoReloadPage` | `true` | `dsh web`:host 重启(bootId 变化)后让页面自动 reload |
104
- | `blockManualRestart` | `true` | 硬拦截 agent 用 shell 命令手动杀掉/重启 DSH,并引导到 `continuum_restart` |
105
- | `healthPath` | `/__dsh_autorestart/health` | 健康检查路由(返回 bootId/pid/phase/自检标志) |
106
- | `continuationText` | `''` | 覆盖默认续接提示文本 |
107
-
108
- ## Agent 工具与"它怎么知道该用插件"
109
-
110
- | 工具 | 作用 |
111
- | --- | --- |
112
- | `continuum_restart` | 请求**优雅重启**:等所有 agent idle 后重启应用,回来自动续接本会话。参数 `reason` / `resume` / `defer_seconds` |
113
- | `continuum_status` | 只读:phase、generation、是否有待重启、检查点路径、识别的重启环境、被记录的会话 |
114
- | `continuum_cancel_restart` | 取消待重启,并释放该会话的续接请求 |
115
-
116
- 除了工具本身(模型在工具表里能看到名字/描述),插件还有两层保证:
117
-
118
- 1. **system prompt 段落**(`systemPrompt.section('dsh-autorestart:restart-policy')`,全局、随插件上下线):
119
- 要重启 DSH 就调用 `continuum_restart`,不要自己 `taskkill`/`Stop-Process`;重启前把进度写进工作区/todo/goal;
120
- 用 `continuum_status` / `continuum_cancel_restart` 查看与取消;**不要求 goal**。
121
- 2. **硬拦截工具守卫**(`ctx.tools.guard(...)`,`blockManualRestart` 默认开启):当 agent 试图通过命令类工具
122
- (`pwsh` / `bash` / 终端 / `run_code` 等)执行"杀掉或重启 DeepSeek Harness"的命令时,**在 dispatch 之前直接拒绝**,
123
- 返回一段说明并引导改用 `continuum_restart`。命中规则包括:
124
- - 杀进程类动词:`taskkill` / `Stop-Process` / `pkill` / `kill` / `terminate` / `wmic … delete|terminate` / `kill-port`,
125
- 且命令文本里出现 `DeepSeek Harness`、`DeepSeek*`、`Deep*Harness*` 等进程选择形式;
126
- - 针对**本 host 进程 pid / 父进程(Electron 主进程)pid** 的 kill;
127
- - 针对**当前 web 端口**的 `kill-port`(端口从 `webServer.getOrigin()` 读取);
128
- - `schtasks /run|/create` 且指向 dsh/deepseek/continuum;
129
- - **系统级关机/重启**:`shutdown /r|/s|/p|-r|-s`、`Restart-Computer`、`Stop-Computer`(一定会连带杀掉 DSH)。
130
- 普通命令(如 `echo`、`git status`、`git commit -m "…taskkill…"` 不含 DSH 字样、`kill 777`、`npx kill-port 3000`、
131
- 写文件/读文件/网页抓取里提到这些词)不受影响。拒绝次数可在健康路由的 `manualRestartBlocks` 看到。
132
-
133
- **残差(诚实说明)**:这是**字符串模式匹配**,不是安全沙箱。编码混淆(`-EncodedCommand`)、按 `node.exe` 镜像名杀进程、
134
- 以及命令文本超过 200KB / 嵌套超过 4 层的参数扫描不到;用别的路径(GUI 菜单、外部脚本、系统重启)绕开也不受它管。
135
- 它要解决的是"agent 顺手 `taskkill` 自己",不是防止恶意绕过。
136
-
137
- 所以**不需要改 AGENTS.md**——这是 Host 插件钩子(工具注册 + system prompt section + tools.guard),对 profile 内所有 agent 生效;
138
- 你当然也可以额外在 AGENTS.md 里写一条团队规范,但它只是重复。
139
-
140
- ## 需要 goal 吗?
141
-
142
- 不需要。
143
-
144
- | 情况 | 重启后行为 |
145
- | --- | --- |
146
- | 有 active goal | `goals.resume()` 重新武装 → goal round driver 自动开下一轮(可跨多轮持续,直到 goal 完成/预算用尽) |
147
- | 没有 goal | 注入一条 `<continuum_resume>` 续接消息 → 新的一轮把活干完(这一轮结束后不会自动再起一轮) |
148
-
149
- 也就是说:**goal 是"多轮自动驾驶"的增强项,不是使用前提**。长任务建议用 `create_goal` 获得更强的连续性;
150
- 一次性收尾任务不用 goal 也完全能接续。
151
-
152
- ## 健康检查
153
-
154
- ```powershell
155
- curl http://127.0.0.1:<当前 GUI 端口>/__dsh_autorestart/health
156
- # {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
157
- # "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":2,…}
55
+ From source:
158
56
 
159
- # 自检:把页面心跳注入逻辑作用到一段探针 HTML 上(不需要鉴权页面)
160
- curl "http://127.0.0.1:<端口>/__dsh_autorestart/health?probeIndex=1"
57
+ ```sh
58
+ git clone https://github.com/Eason-22/dsh-autorestart.git
59
+ cd dsh-autorestart
60
+ dsh plugin --profile desktop add /absolute/path/to/dsh-autorestart
161
61
  ```
162
62
 
163
- `bootId` / `pid` 每次**新进程**变化(重启后立刻能看出换了进程)。`autoReloadPage` 打开时,
164
- `dsh web` 页面会轮询该路由;发现 `bootId` 变了就自动 `location.reload()`,不用手动刷新。
165
-
166
- ## 验证与已知限制
167
-
168
- - **真实重启端到端已实测通过**:agent 自己调 `continuum_restart` → 等空闲 → WMI 脱离进程树 `taskkill` + 重启应用 →
169
- 新 host 进程(pid `6560 → 17884`、bootId 更新、端口 `60909 → 65403`)自动 `resolveAgent` + 注入续接消息,
170
- **无人工输入**地开始下一轮继续原任务。证据见 `verification/e2e-restart-result.md`。
171
- - **system prompt 钩子已实测**:启用后 agent 的 system prompt 中出现 `## Restart policy (dsh-autorestart)`;禁用插件后消失。
172
- - **手动重启硬拦截已实测**:`pwsh` 执行 `echo taskkill /IM "DeepSeek Harness.exe" /T /F`(故意用 echo,即使守卫失效也不会
173
- 真的杀进程)在 dispatch 前被拒绝;`Stop-Process -Name "DeepSeek*"`、`wmic … call terminate`、`npx kill-port <当前端口>`
174
- 三种形态同样被拒;随后 `echo autorestart-guard-final-ok` 正常执行;`health.manualRestartBlocks` 计数递增。
175
- `blockManualRestart: false` 可关闭。
176
- - **页面心跳已实测**:`health.pageHeartbeat=true`,`?probeIndex=1` 返回注入后的 HTML(含 `data-dsh-autorestart-heartbeat`)。
177
- - **Config schema 已实测**:`Config.listConfigs` 对该行返回 `status: "schema"`(17 个键 + 默认值齐全);`health.configSchema=true`。
178
- (注:profile 里的 linked bundle 用真实路径解析,找不到 `@deepseek-ai/schemastery`;插件会回退到
179
- `<dsh 安装目录>/app.asar/dsh/node_modules/@deepseek-ai/schemastery/lib/index.mjs` 加载。健康路由的
180
- `schemastery` 字段只报告粗粒度来源 `bare` / `asar-fallback` / `unresolved`;schema 缺失时降级为"配置照常可用、
181
- 只是没有可视化 schema",并在日志里给出警告,**不会**因此让插件激活失败。)
182
- - 测试:**88 个插件用例全绿**(`plugins/dsh-autorestart/test/*.test.mjs`)。连同对抗回归共 113 个用例,现已全绿;
183
- `verification/adversarial.test.mjs` 的 "bundle patch advertises only config keys the code implements" 原先仍断言旧的
184
- 4 秒默认值(`relaunchDelaySeconds: 4`),已随这次 2 秒默认值一起更新为 2。运行:
185
- `<bundled node> --test "plugins/dsh-autorestart/test/*.test.mjs" "verification/adversarial.test.mjs"`
186
- (Node 24 把位置参数当 glob,务必加引号)。
187
- - 独立验证员复验:`verification/verify-report.md`。
188
- - WMI 脱离进程树已实测(`Invoke-CimMethod Win32_Process Create` 的子进程 parent 是 `WmiPrvSE.exe`)。
189
- - 健康路由在"开机时激活"的情况下会短暂晚于 webserver 就绪,插件会重试注册(≤30 次 × 2s)。
190
- - **限制**:`restartMode: app` 只对 DSH Desktop 生效;`dsh web`/CLI 部署请设 `restartMode: command` 并给出你们自己的重启命令。
191
- - 手动重启守卫是**模式匹配**(命令文本里 kill/restart 动词 + DSH 字样或本进程 pid);如果你自己写了绕过它的脚本(例如
192
- 编码混淆后再执行),守卫生效前提是文本可被识别。它防的是"agent 顺手 taskkill",不是安全边界。
193
- - 重启会关闭并重新打开桌面窗口(这是整应用重启,不是热重载)。
194
- - 杀进程后没有二次确认:若助手进程创建成功但 `taskkill`/`start` 本身失败,会停在 `phase: restarting` 直到下次启动。
195
- - 若进程在 **turn 结束时恰好** 被杀,checkpoint 可能仍记着 running,最多导致一次多余的续接提示。
196
- - 恢复出的新一轮会消费 API token(和人工催"继续"一样)。
197
-
198
- ## 目录
63
+ Inside DSH the equivalent is the `plugin_manager` tool:
199
64
 
200
65
  ```
201
- index.js 插件入口:apply / 事件跟踪 / 工具 / 健康路由 / 启动恢复
202
- lib/checkpoint.js 原子 JSON 检查点,容错读取,boot/restart/session 变更函数
203
- lib/lifecycle.js 纯决策:bootPlan / restartDecision / continuationPrompt / describeState
204
- lib/restart.js Windows 脱离重启(WMI 主、schtasks 兜底、dryRun、自定义命令)
205
- locale/{en,zh}.json Plugin Manager 显示用标题/描述
206
- icon.svg 插件图标
207
- LICENSE MIT 许可(发布到插件市场需要)
208
- test/ node:test 用例(状态机 / 检查点 / 重启命令 / 集成恢复)
66
+ plugin_manager install_bundle target = <absolute path to the package>
209
67
  ```
210
68
 
69
+ Installing into a profile adds a row with the id `dsh-autorestart` to the plugin list. The host half
70
+ of the plugin takes effect immediately, but the client half (the pending restart pill and the
71
+ sidebar dot) is loaded by the browser, so refresh the page or restart DSH once after the install.
211
72
 
212
- ## Two-gate restart safety
213
-
214
- A restart is never scheduled while another DSH conversation is still working.
215
-
216
- 1. Conversation gate: busyAgents() inspects every live agent (subagents and
217
- teammates included). An agent blocks the restart when its status is 'running'
218
- or when it still has queued inbox work (inbox.nextTurn / inbox.nextStep, or
219
- inbox.hasPending). The verdict is { allow:false, reason:'conversation-active',
220
- blockedBy:[...] }.
221
- 2. Settle gate: even with no busy conversation, the host must have been quiet for
222
- settleMs (default 2000 ms) since the last observed activity (agent/status,
223
- session/event, agent/inbox/inserted). Otherwise the verdict is
224
- { allow:false, reason:'settling' }. This closes the gap between the decision
225
- and the detached helper's ping delay, where a new turn could otherwise start
226
- and be killed.
227
-
228
- The gate is evaluated three times: in the poll tick, again right after the
229
- pre-restart session flush (before anything is scheduled), and once more inside the
230
- detached helper (restart-<generation>.cmd in the state-file directory). The helper
231
- GETs http://127.0.0.1:<webPort><healthPath>?gate=1 and exits without killing when
232
- the gate denies the restart or the request fails.
233
-
234
- Because a denied helper leaves this process alive, a watchdog armed for
235
- relaunchDelaySeconds + 20 seconds treats that as a gate abort: it logs, increments
236
- restart.gateAborts, rolls the checkpoint back to phase 'deferred' (decrementing
237
- attempt so a later retry is allowed) and polls again. After more than 20 gate
238
- aborts the plugin logs an error and schedules one ungated restart instead of
239
- retrying forever. Even that ungated fallback re-checks the same gate before
240
- anything is scheduled, so a busy conversation is never killed.
241
-
242
- The hard deadline (deferHardMs) only warns: it forces a restart over a running
243
- conversation only when forceRestartWhenBusy is enabled; by default it waits for
244
- the other conversations, because a restart must never kill a conversation that is
245
- still working.
246
-
247
- Inspect the live verdict with continuum_status (gateAllow, quietForMs, blockedBy)
248
- or by requesting the health path with ?gate=1.
249
- ## 多会话合并重启(coalescing)
250
-
251
- - 多个会话同时调用 `continuum_restart` 时,请求会**合并成一次进程重启**:`generation` 仍然每次 +1,但 `restart` 会被替换为包含所有请求者的记录。
252
- - `restart.requesters` 保存每个请求会话 `{ sessionId, requestedAt, reason, resume }`(按新到旧排序,最多 16 个);`restart.bySessionId` 仅保留最后一个请求者以兼容旧逻辑。
253
- - 每个请求者都会被标记 `pendingResume`,重启后**全部**被唤醒;`continuum_cancel_restart`(以及放弃重试/回滚路径)会一次性释放**所有**请求者,而不是只释放最后一个。
254
- - 启动恢复按 `maxSessions` 分批执行,每批之间等待 `recoveryBatchDelayMs`(默认 1200ms),最多 5 批,因此一轮启动就能唤醒多于 `maxSessions` 的会话;只有超过批次上限时才会留到下一次启动。
255
- - `resolveAgent` 在同一轮启动内最多重试 2 次(`resumeRetryDelayMs`,默认 1000ms);最终失败会写入 `lastResumeError` 并保持 `pendingResume`,该字段也会出现在 `continuum_status` 的每个会话条目中。达到 `maxResumeAttempts` 后 `pendingResume` 被清除并记录最后的错误,避免坏会话无限重试。
256
-
257
- ## UI:等待重启小胶囊 + 侧边栏圆点
258
-
259
- - 胶囊渲染在**输入框工具行左侧**(`conversation.input.left` 槽位),是一个高度约 22px 的小胶囊,**不额外占一行**(不再是输入框上方的一整行)。
260
- - 只有当健康路由 `/__dsh_autorestart/health?state=1` 返回 `pendingRestart: true` **且**本会话 id 出现在 `waiters` 列表里时才会出现;别的会话等待重启时你看不到它。
261
- - 文案有两种状态:还有别的会话占着时显示「待重启 · 被 N 个会话占用」;都空闲时显示「待重启 · 空闲后自动重启」。
262
- - 悬停提示(原生 `title`):「等待其他会话空闲后自动重启;重启后会自动继续本会话进度」。
263
- - 颜色走**品牌色(蓝紫)** `var(--dsw-alias-brand-primary, #5b8def)`,**不再用告警黄(warning)色**:胶囊里的小圆点和侧边栏圆点都是品牌色,胶囊边框保持中性(`--dsw-alias-border-l1`),文字保持中性。
264
- - 左侧边栏会话行的**行首 16px 状态格**里也会出现一个同款 8px 圆点(`sidebar.session.row.leading` 槽位,id `autorestart-restart-dot`,order 20):纯图标,完整说明放在悬停 `title` 里,aria-label 为「待重启」/「Pending restart」。
265
- - 那个 16px 状态格会被**优先级更高的行状态**(ask / 未读 / 运行中)替换,所以圆点只在该行**空闲**时出现。
266
- - 胶囊与侧边栏圆点**共用同一个 2 秒轮询**(客户端只有一条 2s 定时器 + 一次 fetch,不是各轮各的);重启一旦真正触发、状态里的 `pendingRestart` 变成 false,两个界面同时自动消失。
267
- - 胶囊是**行内紧凑**的:`flex: none` + 固定高度,不撑高工具行;文字过长时按省略号截断(`white-space: nowrap` + `text-overflow: ellipsis`),不换行也不溢出。
268
- - 工具行左侧的胶囊与费用条等其它 dock 条目互不影响:不依赖 `position:absolute/fixed`、不使用负 margin,也不新增兄弟元素。
269
- - 客户端这一半(`client.js`)需要**刷新页面或 DSH 重启后**才会加载进前端,Host 端改动则随插件热更新生效。
270
-
271
- ### 升级 1:重启在即时的呼吸脉冲
272
-
273
- - 当 `?state=1` 返回 `pendingRestart: true` **且** `allowRestart: true`(Host 的空闲闸门已开,重启随时都会发生)时,胶囊里的小圆点和侧边栏圆点都会**轻微呼吸**:`scale(1) -> scale(1.5) -> scale(1)`,同时透明度略微下沉。
274
- - 动画只动 `transform` 和 `opacity`,**不改变布局**(不动 `width` / `height`),所以工具行不会被撑开。
275
- - `@keyframes dsh-autorestart-pulse` 只往 `<head>` 里注入**一条**规则(按 id `dsh-autorestart-pulse` 去重),两个界面共用;最后一个订阅者离开时这条规则会被移除。
276
- - 通过 `animation` 样式属性应用;系统开启「减少动态效果」(`prefers-reduced-motion: reduce`)时**不播放**任何动画。
277
-
278
- ### 升级 2:点胶囊看详情
279
-
280
- - 胶囊现在是**可点击**的:`cursor: pointer`、`role="button"`、`tabIndex=0`、`aria-expanded`,鼠标点击以及 **Enter / Space** 都能展开或收起。
281
- - 展开后是一个锚定在胶囊上方的小详情面板(胶囊外层 `position: relative`,面板 `position: absolute`、`bottom: 100%`、左对齐、`min-width: 260px`、圆角、小阴影、字号 12;背景 / 边框 / 文字颜色走宿主主题 token)。
282
- - 面板内容:一行标题「待重启」;一行当前状态(**等待其他会话空闲** / **等待空闲窗口**);**每个占用会话一行**(Host 给了标题就显示标题,没给就显示「有一个会话正在运行」);一行**已等待时长**(客户端从第一次看到 pending 状态开始计时);一行「重启后本会话会自动继续进度」。
283
- - 点击面板外部或按 **Esc** 关闭;所有监听器和计时器在卸载时都会清理。
284
- - 没有 pending 重启时整个组件**什么都不渲染**,绝不会影响输入框。
285
-
286
- ### `?state=1` 响应字段
287
-
288
- ```json
289
- {
290
- "ok": true,
291
- "pendingRestart": true,
292
- "phase": "restarting",
293
- "generation": 3,
294
- "waiters": ["sess-a", "sess-b"],
295
- "blockedBy": [{ "sessionId": "sess-c", "reason": "running", "title": "重构解析器" }],
296
- "allowRestart": false,
297
- "quietForMs": 1200,
298
- "settleMs": 2000,
299
- "updatedAt": 1730000000000
300
- }
301
- ```
302
-
303
- - `blockedBy[]` 的每一项在原有字段(`sessionId`、`reason`)之外,可能多一个**可选的 `title`**:Host 会尽力从会话控制器(`ctx.get('sessionController').list({})`)里读出该会话的标题。
304
- - 这是**尽力而为**:查不到标题就**不带 `title` 字段**,原有字段和行为完全不变;会话控制器缺失、抛错或没有标题都**不会**让这条路由失败。
305
- - 标题查询结果会缓存约 **5 秒**(客户端每 2 秒轮询一次,不会把会话列表打成风暴)。
73
+ ## Usage
306
74
 
307
- ## 平台与部署
75
+ The plugin registers six tools for the agent. All of them are also visible to you in the session,
76
+ so you can call them yourself.
308
77
 
309
- | 部署形态 | `restartMode` | 怎么重启 |
78
+ | Tool | What it does | Parameters |
310
79
  | --- | --- | --- |
311
- | DeepSeek Harness 桌面应用 | `app`(默认) | 用宿主内置的**应用重启**能力:脱离进程树地杀掉并拉起桌面端 |
312
- | 托管的 `dsh web` 部署 | `command` | 桌面端那套「重启应用」在 web 部署里不存在,必须自己给一条重启命令 |
80
+ | `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 |
81
+ | `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 |
82
+ | `continuum_cancel_restart` | Cancels a pending restart and releases the continuation request of every requester. Does nothing when no restart is pending. | none |
83
+ | `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 |
84
+ | `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 |
85
+ | `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 |
86
+
87
+ A goal is an enhancement, not a requirement: without one, the plugin injects a single continuation
88
+ message instead of re-arming a goal.
89
+
90
+ ## Configuration
313
91
 
314
- 托管 `dsh web` 的配置(注意是**整段替换 config**,不是合并):
92
+ Every key has a default, written in the `insert` row of `cordis.patch.yml`. To override a key, add
93
+ an `- id: dsh-autorestart` row with the same key to the `cordis.patch.yml` of the profile at
94
+ `~/.dsh/profiles/<profile>/cordis.patch.yml` (it replaces the whole config, it is not merged). The
95
+ plugin also exports a real schemastery `Config` schema, so the keys, their types and their defaults
96
+ show up in the Plugin Manager and in the config editor.
97
+
98
+ | Key | Default | Meaning |
99
+ | --- | --- | --- |
100
+ | `enabled` | `true` | Turns the whole plugin off. |
101
+ | `stateFile` | `''` | Path of the checkpoint file. Empty means `<DSH_HOME>/storages/dsh-autorestart/state.json`. |
102
+ | `resumeOnBoot` | `true` | Whether interrupted sessions are resumed automatically at boot. |
103
+ | `recoveryDelayMs` | `6000` | How long to wait after boot before resuming, so the session store and the model are ready. |
104
+ | `maxSessions` | `4` | Maximum number of sessions resumed per boot. |
105
+ | `maxResumeAttempts` | `2` | Maximum resume attempts per session, counted across boots, so a bad session cannot loop forever. |
106
+ | `deferPollMs` | `3000` | Poll interval while waiting for the conversations to go idle. |
107
+ | `deferSoftMs` | `300000` | Soft deadline: warn when the conversations are still busy after this long. |
108
+ | `deferHardMs` | `900000` | Hard deadline: warn after this long. The restart is only forced when `forceRestartWhenBusy` is enabled. |
109
+ | `deferPolicy` | `auto` | `auto` = after the hard deadline, follow `forceRestartWhenBusy` (by default keep waiting). `wait` = wait forever. |
110
+ | `forceRestartWhenBusy` | `false` | Whether a restart may be forced while a conversation is still running. The default `false` never interrupts a session: the plugin waits until the other conversations finish. |
111
+ | `restartMode` | `app` | `app` = restart the DeepSeek Harness desktop app. `command` = run `restartCommand`. |
112
+ | `restartCommand` | `''` | Custom restart command. Supports the `{pid}`, `{ppid}` and `{app}` placeholders. |
113
+ | `relaunchDelaySeconds` | `2` | How long the detached helper waits before it kills the app (`ping -n 3`, roughly 2 seconds). |
114
+ | `settleMs` | `2000` | Quiet window in milliseconds: the host must stay silent this long after the last conversation activity before a restart may start. |
115
+ | `autoReloadPage` | `true` | `dsh web`: reload the page automatically when the host boot token changes. |
116
+ | `blockManualRestart` | `true` | Hard-block an agent that tries to kill or restart DSH with a shell command, and point it at `continuum_restart` instead. |
117
+ | `healthPath` | `/__dsh_autorestart/health` | Health check route. Returns the boot id, the pid, the phase and the self-check flags. |
118
+ | `continuationText` | `''` | Overrides the default continuation prompt text. |
119
+
120
+ The two waits are independent of each other, and both default to two seconds:
121
+
122
+ - `settleMs` (`2000` ms) is the quiet window after the last conversation activity during which the
123
+ host has to stay silent before a restart is allowed to start.
124
+ - `relaunchDelaySeconds` (`2` seconds) is how long the detached helper waits before it kills the app.
125
+
126
+ Both can be overridden separately in the profile config. A larger `settleMs` is the more
127
+ conservative choice, because it is less likely to cut a session short that has just started.
128
+
129
+ ### Platform note
130
+
131
+ `restartMode: app` only applies to the DeepSeek Harness desktop app. A hosted `dsh web`
132
+ deployment has no desktop app to relaunch, so it must set `restartMode: command` and provide a
133
+ restart command of its own:
315
134
 
316
135
  ```yaml
317
136
  - id: dsh-autorestart
@@ -320,36 +139,104 @@ or by requesting the health path with ?gate=1.
320
139
  restartCommand: systemctl --user restart dsh-web
321
140
  ```
322
141
 
323
- - **浏览器侧的「待重启」小胶囊**依赖 `GET <healthPath>?state=1` 这条状态路由。这条路由现在接受
324
- **loopback 调用者**,*或* **宿主 connection 服务判定为「已认证浏览器」的请求**
325
- (`connection.requestRejection` 不给拒绝码就放行)。所以:**托管部署必须允许已认证浏览器访问这条状态路由**,
326
- 否则远程浏览器拿不到 pending 标记,小胶囊永远不出现。
327
- 两者都不成立时仍然返回 `403 {"ok":false,"error":"loopback-only"}`。
328
- - 宿主没有 connection 服务时(例如更老的宿主版本),行为不变:仍然只接受 loopback。
329
- - **两段等待现在都是 2 秒,且互相独立**:
330
- - `settleMs`(默认 `2000` ms):最后一次会话活动之后,宿主必须静默这么久,重启才允许开始;
331
- - `relaunchDelaySeconds`(默认 `2` 秒):脱离的 helper 在杀掉应用**之前**等这么久(`ping -n 3`,约 2 秒)。
332
-
333
- 两个值都可以在 profile 的 config 里单独覆盖;把 `settleMs` 调大会更保守(更不容易打断刚起步的会话)。
334
-
335
- ## 发布到插件市场
336
-
337
- ```powershell
338
- # 1. 打包
339
- npm pack # -> dsh-autorestart-0.2.1.tgz
340
- # 2. 发布到 registry
341
- npm publish
342
- # 3. 从市场安装
343
- # dsh CLI:dsh plugin install dsh-autorestart
344
- # 或直接对 Agent 说:把 dsh-autorestart 装到当前 profile
142
+ Remember that this replaces the whole config instead of merging into it.
143
+
144
+ ## Requirements and limits
145
+
146
+ - **Windows desktop.** `restartMode: app` restarts the DSH desktop app on Windows. Any other
147
+ platform, and every hosted `dsh web` deployment, has to use `restartMode: command` with its own
148
+ restart command.
149
+ - **The manual-restart guard is pattern matching, not a sandbox.** It looks at the command text for
150
+ kill and restart verbs together with DSH process names or the pid of this host. Encoded commands,
151
+ killing by image name, and routes around the guard — a GUI menu, an external script, a system
152
+ reboot — are not covered. It exists to stop an agent from casually killing itself, not to contain
153
+ a hostile command.
154
+ - **A session past the model context window cannot continue.** Once a conversation has grown beyond
155
+ the model context window, the provider rejects every request with `invalid_request_error`, and the
156
+ host's own compaction cannot rescue it because the compaction request needs the same oversized
157
+ context. Hand such a session over with `continuum_handoff`; the plugin deliberately refuses to
158
+ auto-resume it.
159
+ - **A hosted deployment must let the browser read the state route.** The pending restart pill reads
160
+ `GET <healthPath>?state=1`. The route accepts loopback callers and requests that the host
161
+ connection service considers authenticated browsers. A hosted deployment that blocks it will never
162
+ show the pill.
163
+ - **The restart closes and reopens the desktop window.** It is a full app restart, not a hot reload.
164
+ - **The resumed round consumes API tokens**, exactly like typing "continue" by hand.
165
+
166
+ ## Verify
167
+
168
+ The health route tells you whether the plugin is alive and what it has done:
169
+
170
+ ```sh
171
+ curl http://127.0.0.1:<gui port>/__dsh_autorestart/health
172
+ # {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
173
+ # "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":2,…}
345
174
  ```
346
175
 
347
- `package.json` 里已经带齐市场需要的元数据:MIT license、`keywords`、`repository`,以及
348
- `dsh.compatibility.dshReleases`(声明在 DSH `0.2.1-rc.2` 上实测兼容)。
349
- 改了代码记得重新 `npm pack`——市场里装的是 tarball,不是工作区目录。
176
+ `bootId` and `pid` change with every new process, so a restart is visible right away.
177
+
178
+ - `?state=1` returns the pending restart state: `pendingRestart`, `waiters`, `blockedBy`,
179
+ `allowRestart`, `quietForMs`, `settleMs`, `parkedGoals`, `recovery` and `handoffs`.
180
+ - `?probeIndex=1` applies the page heartbeat injection to a probe HTML page, so the client half can
181
+ be checked without an authenticated page.
182
+ - `?gate=1` returns the live idle gate verdict that the detached helper uses before it kills
183
+ anything.
184
+ - `POST /__dsh_autorestart/park` disarms a goal, `POST /__dsh_autorestart/recover` re-runs the
185
+ recovery immediately. Both use the same authentication rules as the health route.
186
+ - Inside a session, `continuum_status` reports the same lifecycle.
187
+
188
+ The plugin ships its own test suite: **129 test cases** in `plugins/dsh-autorestart/test/`, all
189
+ green. Run it with the bundled Node, from the workspace root:
190
+
191
+ ```sh
192
+ node --test "plugins/dsh-autorestart/test/*.test.mjs"
193
+ ```
194
+
195
+ Node 24 treats a positional argument as a glob, so keep the quotes. The same suite also runs with
196
+ a bare `node --test` from the package directory. Passing the directory itself (`node --test
197
+ test/`) does not work: the argument is read as a glob, and a glob does not match directories.
198
+
199
+ ## Screenshots
200
+
201
+ ![dsh-autorestart icon](assets/icon-180.png)
202
+
203
+ ![dsh-autorestart screenshot 1](assets/screenshot-1.png)
204
+
205
+ ![dsh-autorestart screenshot 2](assets/screenshot-2.png)
206
+
207
+ ![dsh-autorestart screenshot 3](assets/screenshot-3.png)
208
+
209
+ ## License
210
+
211
+ MIT — see [LICENSE](./LICENSE).
212
+
213
+ ## Submitting to the curated plugin list
214
+
215
+ The curated list lives in the `awesome-dsh-plugin/awesome-dsh-plugin` repository, and one pull
216
+ request that adds a single file is the whole submission: `data/plugins/<owner>__<repo>.yml`. For
217
+ this plugin that file is `data/plugins/Eason-22__dsh-autorestart.yml`:
218
+
219
+ ```yaml
220
+ url: https://github.com/Eason-22/dsh-autorestart
221
+ name: Eason-22/dsh-autorestart
222
+ category: session
223
+ description:
224
+ en: 'DeepSeek Harness plugin: restart without losing work, with an idle-aware graceful restart and automatic session resume after the host comes back.'
225
+ zh: 'DeepSeek Harness 插件:重启不丢工作——等所有对话空闲后优雅重启,重启后自动接续被打断的会话。'
226
+ icon: https://raw.githubusercontent.com/Eason-22/dsh-autorestart/main/assets/icon-180.png
227
+ ```
350
228
 
351
- ## AI 生成声明
229
+ The list's CI checks that the repository declares a `dsh.bundle` manifest, that the repository is
230
+ at least one day old (the check re-runs by itself, so no need to resubmit), that the repository
231
+ exists and is not archived, and that a pull request adds at most three entries. Descriptions are
232
+ read as claims about the plugin and are checked against the code, so keep them factual and free of
233
+ marketing. The marketplace icon must be a GitHub-hosted raster image (png, jpg, jpeg or webp; SVG
234
+ is refused) — this repository ships `assets/icon-180.png` — and the optional `screenshots.json` next
235
+ to `package.json` lets the storefront show the images listed in `assets/`. The two READMEs of the
236
+ list are generated from the entry files; do not edit them by hand.
352
237
 
353
- 本插件由 AI 生成,**未经人工代码审核**。它会重启你的 DSH 进程并读写本地状态文件。请先在测试 profile 里验证行为符合预期,再用于日常/生产 profile;使用风险由使用者自行评估。
238
+ ## AI-generated notice
354
239
 
355
- This plugin was generated by an AI and has **not been reviewed by a human**. It restarts your DSH process and writes local state files. Verify it in a test profile before using it in your working profile.
240
+ This plugin was generated by an AI and has **not** been reviewed by a human. It restarts your DSH
241
+ process and writes local state files, so it can interrupt a running session. Try it in a test
242
+ profile first, evaluate the risk yourself, and only then use it in your daily profile.