dsh-autorestart 0.2.0 → 0.3.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/LICENSE +0 -0
- package/README.md +204 -295
- package/README.zh.md +504 -0
- package/assets/icon-180.png +0 -0
- package/assets/screenshot-1.png +0 -0
- package/assets/screenshot-2.png +0 -0
- package/assets/screenshot-3.png +0 -0
- package/client.js +267 -8
- package/cordis.patch.yml +0 -0
- package/icon.svg +0 -0
- package/index.js +1284 -43
- package/lib/checkpoint.js +516 -2
- package/lib/lifecycle.js +387 -0
- package/lib/restart.js +0 -0
- package/locale/en.json +0 -0
- package/locale/zh.json +0 -0
- package/package.json +6 -3
- package/screenshots.json +5 -0
package/LICENSE
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
@@ -1,301 +1,136 @@
|
|
|
1
1
|
# dsh-autorestart
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Restart DeepSeek Harness without losing the work that is currently running.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-autorestart)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://github.com/topics/dsh-plugin)
|
|
8
|
+
[](https://github.com/Eason-22/dsh-autorestart)
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
This Host plugin makes a DSH restart a *resumable* event instead of a dead end: a durable checkpoint, automatic
|
|
9
|
-
session resume on boot, durable-goal re-arm, and an agent-callable idle-aware graceful restart.
|
|
10
|
+
Chinese version: [README.zh.md](README.zh.md)
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
> 不依赖 systemd,使用 WMI/schtasks 脱离将要被销毁的 Electron 进程树来重启应用。
|
|
12
|
+
## What it does
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
`dsh-autorestart` turns a DeepSeek Harness restart into a resumable event instead of a dead end.
|
|
15
15
|
|
|
16
|
-
|
|
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.
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
| Agent 正在跑长任务,DSH 被重启/崩溃 | 这一轮丢失,必须人工催"继续" | 新进程启动后自动 resume 该会话并注入续接提示,继续干活 |
|
|
21
|
-
| Agent 有一个持久 goal(`create_goal`)跨重启 | goal 的 activation 是进程内的,重启后变回 `disarmed`,自动续跑停止 | 启动时重新 `goals.resume()`,goal round driver 立刻开始下一轮 |
|
|
22
|
-
| Agent 想装/升级插件但"重启必打断" | 只能等或赌 | 调 `continuum_restart`:等所有 agent idle(当前这一轮跑完)再重启,回来自己接着做 |
|
|
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.
|
|
23
24
|
|
|
24
|
-
|
|
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.
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
├─ continuum_restart 工具:checkpoint 标记 pendingResume + generation+1,轮询等 idle
|
|
30
|
-
│ └─ 全空闲 ──► WMI 创建脱离进程树的 cmd:ping 延迟 → taskkill 整个应用 → 重新启动 exe
|
|
31
|
-
│
|
|
32
|
-
└─ 进程意外死亡 ──► 下一次启动时,旧 bootPid 记录里 running=true 的会话被判定为“中断”
|
|
33
|
-
│
|
|
34
|
-
新进程 boot ──► apply():beginBoot()(新 pid ⇒ 新 bootId)──► recoveryDelayMs 后恢复
|
|
35
|
-
│
|
|
36
|
-
┌──────────────┴──────────────┐
|
|
37
|
-
session 有 active+disarmed 的 goal 无 goal(或 goal 不可续)
|
|
38
|
-
│ │
|
|
39
|
-
ctx.goals.resume(agent, ref) agent.followup(<续接消息>)
|
|
40
|
-
│ │
|
|
41
|
-
goal round driver 自动开下一轮 一条带 <continuum_resume> 标记的续接消息
|
|
42
|
-
```
|
|
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.
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
续接消息通过 `agent.followup(...)` 进入该会话的 next-turn 队列 —— 不用 `sessionController.prompt`,
|
|
48
|
-
因为它的进程内签名是 `prompt(request, signal)` 且会硬编码 human `user` source。
|
|
49
|
-
- **续接消息是普通 user message**,`source.kind='continuum'`(非 human authority)、内容明确标记
|
|
50
|
-
`<continuum_resume>`,模型能看到完整历史后自行判断中断点。
|
|
51
|
-
|
|
52
|
-
## 安装
|
|
53
|
-
|
|
54
|
-
```powershell
|
|
55
|
-
# 在 dsh 里(或对 Agent 说):把工作区里的这个包安装为 profile bundle
|
|
56
|
-
# plugin_manager install_bundle target = <本目录绝对路径>
|
|
57
|
-
# 等价命令行(需要 dsh CLI):
|
|
58
|
-
dsh plugin --profile desktop add C:\path\to\dsh-autorestart
|
|
59
|
-
```
|
|
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.
|
|
60
38
|
|
|
61
|
-
|
|
39
|
+
## Why
|
|
62
40
|
|
|
63
|
-
|
|
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 |
|
|
64
46
|
|
|
65
|
-
|
|
66
|
-
`~/.dsh/profiles/<profile>/cordis.patch.yml` 里加同名 `- id: dsh-autorestart` 行(**整段替换 config**)。
|
|
67
|
-
插件同时导出真实的 `Config` schema(schemastery),所以 Plugin Manager / 配置编辑器里能看到键名、类型与默认值
|
|
68
|
-
(见 `GET /__dsh_autorestart/health` 的 `configSchema: true`)。
|
|
47
|
+
## Install
|
|
69
48
|
|
|
70
|
-
|
|
71
|
-
| --- | --- | --- |
|
|
72
|
-
| `enabled` | `true` | 关掉整个插件 |
|
|
73
|
-
| `stateFile` | `''` | 检查点路径;空 = `<DSH_HOME>/storages/dsh-autorestart/state.json` |
|
|
74
|
-
| `resumeOnBoot` | `true` | 启动时是否自动恢复 |
|
|
75
|
-
| `recoveryDelayMs` | `6000` | 启动后等多久再恢复(给 session store / model 就绪留时间) |
|
|
76
|
-
| `maxSessions` | `4` | 一次启动最多恢复几个会话 |
|
|
77
|
-
| `maxResumeAttempts` | `2` | 每个会话最多尝试恢复几次(跨启动累计,防重启循环) |
|
|
78
|
-
| `deferPollMs` | `3000` | 等 idle 的轮询间隔 |
|
|
79
|
-
| `deferSoftMs` | `300000` | 软期限:还在忙就告警 |
|
|
80
|
-
| `deferHardMs` | `900000` | 硬期限:超过后告警;只有 `forceRestartWhenBusy` 打开时才强制重启 |
|
|
81
|
-
| `deferPolicy` | `auto` | `auto` = 硬期限后按 `forceRestartWhenBusy` 决定(默认继续等);`wait` = 一直等 |
|
|
82
|
-
| `forceRestartWhenBusy` | `false` | 硬期限到了但还有会话在跑时是否强制重启;默认 `false` = **不打断任何会话**,一直等到其他会话结束 |
|
|
83
|
-
| `restartMode` | `app` | `app` = 重启 DeepSeek Harness 桌面应用;`command` = 跑 `restartCommand` |
|
|
84
|
-
| `restartCommand` | `''` | 自定义重启命令,支持 `{pid}` `{ppid}` `{app}` 占位符 |
|
|
85
|
-
| `relaunchDelaySeconds` | `2` | 脱离的 helper 在杀掉应用**之前**等几秒(`ping -n 3`,约 2 秒) |
|
|
86
|
-
| `settleMs` | `2000` | 静默窗口(ms):最后一次会话活动之后,宿主要安静这么久,重启才允许开始 |
|
|
87
|
-
| `autoReloadPage` | `true` | `dsh web`:host 重启(bootId 变化)后让页面自动 reload |
|
|
88
|
-
| `blockManualRestart` | `true` | 硬拦截 agent 用 shell 命令手动杀掉/重启 DSH,并引导到 `continuum_restart` |
|
|
89
|
-
| `healthPath` | `/__dsh_autorestart/health` | 健康检查路由(返回 bootId/pid/phase/自检标志) |
|
|
90
|
-
| `continuationText` | `''` | 覆盖默认续接提示文本 |
|
|
91
|
-
|
|
92
|
-
## Agent 工具与"它怎么知道该用插件"
|
|
93
|
-
|
|
94
|
-
| 工具 | 作用 |
|
|
95
|
-
| --- | --- |
|
|
96
|
-
| `continuum_restart` | 请求**优雅重启**:等所有 agent idle 后重启应用,回来自动续接本会话。参数 `reason` / `resume` / `defer_seconds` |
|
|
97
|
-
| `continuum_status` | 只读:phase、generation、是否有待重启、检查点路径、识别的重启环境、被记录的会话 |
|
|
98
|
-
| `continuum_cancel_restart` | 取消待重启,并释放该会话的续接请求 |
|
|
99
|
-
|
|
100
|
-
除了工具本身(模型在工具表里能看到名字/描述),插件还有两层保证:
|
|
101
|
-
|
|
102
|
-
1. **system prompt 段落**(`systemPrompt.section('dsh-autorestart:restart-policy')`,全局、随插件上下线):
|
|
103
|
-
要重启 DSH 就调用 `continuum_restart`,不要自己 `taskkill`/`Stop-Process`;重启前把进度写进工作区/todo/goal;
|
|
104
|
-
用 `continuum_status` / `continuum_cancel_restart` 查看与取消;**不要求 goal**。
|
|
105
|
-
2. **硬拦截工具守卫**(`ctx.tools.guard(...)`,`blockManualRestart` 默认开启):当 agent 试图通过命令类工具
|
|
106
|
-
(`pwsh` / `bash` / 终端 / `run_code` 等)执行"杀掉或重启 DeepSeek Harness"的命令时,**在 dispatch 之前直接拒绝**,
|
|
107
|
-
返回一段说明并引导改用 `continuum_restart`。命中规则包括:
|
|
108
|
-
- 杀进程类动词:`taskkill` / `Stop-Process` / `pkill` / `kill` / `terminate` / `wmic … delete|terminate` / `kill-port`,
|
|
109
|
-
且命令文本里出现 `DeepSeek Harness`、`DeepSeek*`、`Deep*Harness*` 等进程选择形式;
|
|
110
|
-
- 针对**本 host 进程 pid / 父进程(Electron 主进程)pid** 的 kill;
|
|
111
|
-
- 针对**当前 web 端口**的 `kill-port`(端口从 `webServer.getOrigin()` 读取);
|
|
112
|
-
- `schtasks /run|/create` 且指向 dsh/deepseek/continuum;
|
|
113
|
-
- **系统级关机/重启**:`shutdown /r|/s|/p|-r|-s`、`Restart-Computer`、`Stop-Computer`(一定会连带杀掉 DSH)。
|
|
114
|
-
普通命令(如 `echo`、`git status`、`git commit -m "…taskkill…"` 不含 DSH 字样、`kill 777`、`npx kill-port 3000`、
|
|
115
|
-
写文件/读文件/网页抓取里提到这些词)不受影响。拒绝次数可在健康路由的 `manualRestartBlocks` 看到。
|
|
116
|
-
|
|
117
|
-
**残差(诚实说明)**:这是**字符串模式匹配**,不是安全沙箱。编码混淆(`-EncodedCommand`)、按 `node.exe` 镜像名杀进程、
|
|
118
|
-
以及命令文本超过 200KB / 嵌套超过 4 层的参数扫描不到;用别的路径(GUI 菜单、外部脚本、系统重启)绕开也不受它管。
|
|
119
|
-
它要解决的是"agent 顺手 `taskkill` 自己",不是防止恶意绕过。
|
|
120
|
-
|
|
121
|
-
所以**不需要改 AGENTS.md**——这是 Host 插件钩子(工具注册 + system prompt section + tools.guard),对 profile 内所有 agent 生效;
|
|
122
|
-
你当然也可以额外在 AGENTS.md 里写一条团队规范,但它只是重复。
|
|
123
|
-
|
|
124
|
-
## 需要 goal 吗?
|
|
125
|
-
|
|
126
|
-
不需要。
|
|
127
|
-
|
|
128
|
-
| 情况 | 重启后行为 |
|
|
129
|
-
| --- | --- |
|
|
130
|
-
| 有 active goal | `goals.resume()` 重新武装 → goal round driver 自动开下一轮(可跨多轮持续,直到 goal 完成/预算用尽) |
|
|
131
|
-
| 没有 goal | 注入一条 `<continuum_resume>` 续接消息 → 新的一轮把活干完(这一轮结束后不会自动再起一轮) |
|
|
132
|
-
|
|
133
|
-
也就是说:**goal 是"多轮自动驾驶"的增强项,不是使用前提**。长任务建议用 `create_goal` 获得更强的连续性;
|
|
134
|
-
一次性收尾任务不用 goal 也完全能接续。
|
|
135
|
-
|
|
136
|
-
## 健康检查
|
|
137
|
-
|
|
138
|
-
```powershell
|
|
139
|
-
curl http://127.0.0.1:<当前 GUI 端口>/__dsh_autorestart/health
|
|
140
|
-
# {"ok":true,"plugin":"dsh-autorestart","bootId":"…","pid":17884,"phase":"running",
|
|
141
|
-
# "pageHeartbeat":true,"systemPrompt":true,"configSchema":true,"indexTaps":2,…}
|
|
49
|
+
From npm — the package is published under the name `dsh-autorestart`:
|
|
142
50
|
|
|
143
|
-
|
|
144
|
-
|
|
51
|
+
```sh
|
|
52
|
+
dsh plugin install dsh-autorestart
|
|
145
53
|
```
|
|
146
54
|
|
|
147
|
-
|
|
148
|
-
`dsh web` 页面会轮询该路由;发现 `bootId` 变了就自动 `location.reload()`,不用手动刷新。
|
|
149
|
-
|
|
150
|
-
## 验证与已知限制
|
|
151
|
-
|
|
152
|
-
- **真实重启端到端已实测通过**:agent 自己调 `continuum_restart` → 等空闲 → WMI 脱离进程树 `taskkill` + 重启应用 →
|
|
153
|
-
新 host 进程(pid `6560 → 17884`、bootId 更新、端口 `60909 → 65403`)自动 `resolveAgent` + 注入续接消息,
|
|
154
|
-
**无人工输入**地开始下一轮继续原任务。证据见 `verification/e2e-restart-result.md`。
|
|
155
|
-
- **system prompt 钩子已实测**:启用后 agent 的 system prompt 中出现 `## Restart policy (dsh-autorestart)`;禁用插件后消失。
|
|
156
|
-
- **手动重启硬拦截已实测**:`pwsh` 执行 `echo taskkill /IM "DeepSeek Harness.exe" /T /F`(故意用 echo,即使守卫失效也不会
|
|
157
|
-
真的杀进程)在 dispatch 前被拒绝;`Stop-Process -Name "DeepSeek*"`、`wmic … call terminate`、`npx kill-port <当前端口>`
|
|
158
|
-
三种形态同样被拒;随后 `echo autorestart-guard-final-ok` 正常执行;`health.manualRestartBlocks` 计数递增。
|
|
159
|
-
`blockManualRestart: false` 可关闭。
|
|
160
|
-
- **页面心跳已实测**:`health.pageHeartbeat=true`,`?probeIndex=1` 返回注入后的 HTML(含 `data-dsh-autorestart-heartbeat`)。
|
|
161
|
-
- **Config schema 已实测**:`Config.listConfigs` 对该行返回 `status: "schema"`(17 个键 + 默认值齐全);`health.configSchema=true`。
|
|
162
|
-
(注:profile 里的 linked bundle 用真实路径解析,找不到 `@deepseek-ai/schemastery`;插件会回退到
|
|
163
|
-
`<dsh 安装目录>/app.asar/dsh/node_modules/@deepseek-ai/schemastery/lib/index.mjs` 加载。健康路由的
|
|
164
|
-
`schemastery` 字段只报告粗粒度来源 `bare` / `asar-fallback` / `unresolved`;schema 缺失时降级为"配置照常可用、
|
|
165
|
-
只是没有可视化 schema",并在日志里给出警告,**不会**因此让插件激活失败。)
|
|
166
|
-
- 测试:**88 个插件用例全绿**(`plugins/dsh-autorestart/test/*.test.mjs`)。连同对抗回归共 113 个用例,现已全绿;
|
|
167
|
-
`verification/adversarial.test.mjs` 的 "bundle patch advertises only config keys the code implements" 原先仍断言旧的
|
|
168
|
-
4 秒默认值(`relaunchDelaySeconds: 4`),已随这次 2 秒默认值一起更新为 2。运行:
|
|
169
|
-
`<bundled node> --test "plugins/dsh-autorestart/test/*.test.mjs" "verification/adversarial.test.mjs"`
|
|
170
|
-
(Node 24 把位置参数当 glob,务必加引号)。
|
|
171
|
-
- 独立验证员复验:`verification/verify-report.md`。
|
|
172
|
-
- WMI 脱离进程树已实测(`Invoke-CimMethod Win32_Process Create` 的子进程 parent 是 `WmiPrvSE.exe`)。
|
|
173
|
-
- 健康路由在"开机时激活"的情况下会短暂晚于 webserver 就绪,插件会重试注册(≤30 次 × 2s)。
|
|
174
|
-
- **限制**:`restartMode: app` 只对 DSH Desktop 生效;`dsh web`/CLI 部署请设 `restartMode: command` 并给出你们自己的重启命令。
|
|
175
|
-
- 手动重启守卫是**模式匹配**(命令文本里 kill/restart 动词 + DSH 字样或本进程 pid);如果你自己写了绕过它的脚本(例如
|
|
176
|
-
编码混淆后再执行),守卫生效前提是文本可被识别。它防的是"agent 顺手 taskkill",不是安全边界。
|
|
177
|
-
- 重启会关闭并重新打开桌面窗口(这是整应用重启,不是热重载)。
|
|
178
|
-
- 杀进程后没有二次确认:若助手进程创建成功但 `taskkill`/`start` 本身失败,会停在 `phase: restarting` 直到下次启动。
|
|
179
|
-
- 若进程在 **turn 结束时恰好** 被杀,checkpoint 可能仍记着 running,最多导致一次多余的续接提示。
|
|
180
|
-
- 恢复出的新一轮会消费 API token(和人工催"继续"一样)。
|
|
181
|
-
|
|
182
|
-
## 目录
|
|
55
|
+
From source:
|
|
183
56
|
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
lib/restart.js Windows 脱离重启(WMI 主、schtasks 兜底、dryRun、自定义命令)
|
|
189
|
-
locale/{en,zh}.json Plugin Manager 显示用标题/描述
|
|
190
|
-
icon.svg 插件图标
|
|
191
|
-
LICENSE MIT 许可(发布到插件市场需要)
|
|
192
|
-
test/ node:test 用例(状态机 / 检查点 / 重启命令 / 集成恢复)
|
|
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
|
|
193
61
|
```
|
|
194
62
|
|
|
63
|
+
Inside DSH the equivalent is the `plugin_manager` tool:
|
|
195
64
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
A restart is never scheduled while another DSH conversation is still working.
|
|
199
|
-
|
|
200
|
-
1. Conversation gate: busyAgents() inspects every live agent (subagents and
|
|
201
|
-
teammates included). An agent blocks the restart when its status is 'running'
|
|
202
|
-
or when it still has queued inbox work (inbox.nextTurn / inbox.nextStep, or
|
|
203
|
-
inbox.hasPending). The verdict is { allow:false, reason:'conversation-active',
|
|
204
|
-
blockedBy:[...] }.
|
|
205
|
-
2. Settle gate: even with no busy conversation, the host must have been quiet for
|
|
206
|
-
settleMs (default 2000 ms) since the last observed activity (agent/status,
|
|
207
|
-
session/event, agent/inbox/inserted). Otherwise the verdict is
|
|
208
|
-
{ allow:false, reason:'settling' }. This closes the gap between the decision
|
|
209
|
-
and the detached helper's ping delay, where a new turn could otherwise start
|
|
210
|
-
and be killed.
|
|
211
|
-
|
|
212
|
-
The gate is evaluated three times: in the poll tick, again right after the
|
|
213
|
-
pre-restart session flush (before anything is scheduled), and once more inside the
|
|
214
|
-
detached helper (restart-<generation>.cmd in the state-file directory). The helper
|
|
215
|
-
GETs http://127.0.0.1:<webPort><healthPath>?gate=1 and exits without killing when
|
|
216
|
-
the gate denies the restart or the request fails.
|
|
217
|
-
|
|
218
|
-
Because a denied helper leaves this process alive, a watchdog armed for
|
|
219
|
-
relaunchDelaySeconds + 20 seconds treats that as a gate abort: it logs, increments
|
|
220
|
-
restart.gateAborts, rolls the checkpoint back to phase 'deferred' (decrementing
|
|
221
|
-
attempt so a later retry is allowed) and polls again. After more than 20 gate
|
|
222
|
-
aborts the plugin logs an error and schedules one ungated restart instead of
|
|
223
|
-
retrying forever. Even that ungated fallback re-checks the same gate before
|
|
224
|
-
anything is scheduled, so a busy conversation is never killed.
|
|
225
|
-
|
|
226
|
-
The hard deadline (deferHardMs) only warns: it forces a restart over a running
|
|
227
|
-
conversation only when forceRestartWhenBusy is enabled; by default it waits for
|
|
228
|
-
the other conversations, because a restart must never kill a conversation that is
|
|
229
|
-
still working.
|
|
230
|
-
|
|
231
|
-
Inspect the live verdict with continuum_status (gateAllow, quietForMs, blockedBy)
|
|
232
|
-
or by requesting the health path with ?gate=1.
|
|
233
|
-
## 多会话合并重启(coalescing)
|
|
234
|
-
|
|
235
|
-
- 多个会话同时调用 `continuum_restart` 时,请求会**合并成一次进程重启**:`generation` 仍然每次 +1,但 `restart` 会被替换为包含所有请求者的记录。
|
|
236
|
-
- `restart.requesters` 保存每个请求会话 `{ sessionId, requestedAt, reason, resume }`(按新到旧排序,最多 16 个);`restart.bySessionId` 仅保留最后一个请求者以兼容旧逻辑。
|
|
237
|
-
- 每个请求者都会被标记 `pendingResume`,重启后**全部**被唤醒;`continuum_cancel_restart`(以及放弃重试/回滚路径)会一次性释放**所有**请求者,而不是只释放最后一个。
|
|
238
|
-
- 启动恢复按 `maxSessions` 分批执行,每批之间等待 `recoveryBatchDelayMs`(默认 1200ms),最多 5 批,因此一轮启动就能唤醒多于 `maxSessions` 的会话;只有超过批次上限时才会留到下一次启动。
|
|
239
|
-
- `resolveAgent` 在同一轮启动内最多重试 2 次(`resumeRetryDelayMs`,默认 1000ms);最终失败会写入 `lastResumeError` 并保持 `pendingResume`,该字段也会出现在 `continuum_status` 的每个会话条目中。达到 `maxResumeAttempts` 后 `pendingResume` 被清除并记录最后的错误,避免坏会话无限重试。
|
|
240
|
-
|
|
241
|
-
## UI:等待重启小胶囊 + 侧边栏圆点
|
|
242
|
-
|
|
243
|
-
- 胶囊渲染在**输入框工具行左侧**(`conversation.input.left` 槽位),是一个高度约 22px 的小胶囊,**不额外占一行**(不再是输入框上方的一整行)。
|
|
244
|
-
- 只有当健康路由 `/__dsh_autorestart/health?state=1` 返回 `pendingRestart: true` **且**本会话 id 出现在 `waiters` 列表里时才会出现;别的会话等待重启时你看不到它。
|
|
245
|
-
- 文案有两种状态:还有别的会话占着时显示「待重启 · 被 N 个会话占用」;都空闲时显示「待重启 · 空闲后自动重启」。
|
|
246
|
-
- 悬停提示(原生 `title`):「等待其他会话空闲后自动重启;重启后会自动继续本会话进度」。
|
|
247
|
-
- 颜色走**品牌色(蓝紫)** `var(--dsw-alias-brand-primary, #5b8def)`,**不再用告警黄(warning)色**:胶囊里的小圆点和侧边栏圆点都是品牌色,胶囊边框保持中性(`--dsw-alias-border-l1`),文字保持中性。
|
|
248
|
-
- 左侧边栏会话行的**行首 16px 状态格**里也会出现一个同款 8px 圆点(`sidebar.session.row.leading` 槽位,id `autorestart-restart-dot`,order 20):纯图标,完整说明放在悬停 `title` 里,aria-label 为「待重启」/「Pending restart」。
|
|
249
|
-
- 那个 16px 状态格会被**优先级更高的行状态**(ask / 未读 / 运行中)替换,所以圆点只在该行**空闲**时出现。
|
|
250
|
-
- 胶囊与侧边栏圆点**共用同一个 2 秒轮询**(客户端只有一条 2s 定时器 + 一次 fetch,不是各轮各的);重启一旦真正触发、状态里的 `pendingRestart` 变成 false,两个界面同时自动消失。
|
|
251
|
-
- 胶囊是**行内紧凑**的:`flex: none` + 固定高度,不撑高工具行;文字过长时按省略号截断(`white-space: nowrap` + `text-overflow: ellipsis`),不换行也不溢出。
|
|
252
|
-
- 工具行左侧的胶囊与费用条等其它 dock 条目互不影响:不依赖 `position:absolute/fixed`、不使用负 margin,也不新增兄弟元素。
|
|
253
|
-
- 客户端这一半(`client.js`)需要**刷新页面或 DSH 重启后**才会加载进前端,Host 端改动则随插件热更新生效。
|
|
254
|
-
|
|
255
|
-
### 升级 1:重启在即时的呼吸脉冲
|
|
256
|
-
|
|
257
|
-
- 当 `?state=1` 返回 `pendingRestart: true` **且** `allowRestart: true`(Host 的空闲闸门已开,重启随时都会发生)时,胶囊里的小圆点和侧边栏圆点都会**轻微呼吸**:`scale(1) -> scale(1.5) -> scale(1)`,同时透明度略微下沉。
|
|
258
|
-
- 动画只动 `transform` 和 `opacity`,**不改变布局**(不动 `width` / `height`),所以工具行不会被撑开。
|
|
259
|
-
- `@keyframes dsh-autorestart-pulse` 只往 `<head>` 里注入**一条**规则(按 id `dsh-autorestart-pulse` 去重),两个界面共用;最后一个订阅者离开时这条规则会被移除。
|
|
260
|
-
- 通过 `animation` 样式属性应用;系统开启「减少动态效果」(`prefers-reduced-motion: reduce`)时**不播放**任何动画。
|
|
261
|
-
|
|
262
|
-
### 升级 2:点胶囊看详情
|
|
263
|
-
|
|
264
|
-
- 胶囊现在是**可点击**的:`cursor: pointer`、`role="button"`、`tabIndex=0`、`aria-expanded`,鼠标点击以及 **Enter / Space** 都能展开或收起。
|
|
265
|
-
- 展开后是一个锚定在胶囊上方的小详情面板(胶囊外层 `position: relative`,面板 `position: absolute`、`bottom: 100%`、左对齐、`min-width: 260px`、圆角、小阴影、字号 12;背景 / 边框 / 文字颜色走宿主主题 token)。
|
|
266
|
-
- 面板内容:一行标题「待重启」;一行当前状态(**等待其他会话空闲** / **等待空闲窗口**);**每个占用会话一行**(Host 给了标题就显示标题,没给就显示「有一个会话正在运行」);一行**已等待时长**(客户端从第一次看到 pending 状态开始计时);一行「重启后本会话会自动继续进度」。
|
|
267
|
-
- 点击面板外部或按 **Esc** 关闭;所有监听器和计时器在卸载时都会清理。
|
|
268
|
-
- 没有 pending 重启时整个组件**什么都不渲染**,绝不会影响输入框。
|
|
269
|
-
|
|
270
|
-
### `?state=1` 响应字段
|
|
271
|
-
|
|
272
|
-
```json
|
|
273
|
-
{
|
|
274
|
-
"ok": true,
|
|
275
|
-
"pendingRestart": true,
|
|
276
|
-
"phase": "restarting",
|
|
277
|
-
"generation": 3,
|
|
278
|
-
"waiters": ["sess-a", "sess-b"],
|
|
279
|
-
"blockedBy": [{ "sessionId": "sess-c", "reason": "running", "title": "重构解析器" }],
|
|
280
|
-
"allowRestart": false,
|
|
281
|
-
"quietForMs": 1200,
|
|
282
|
-
"settleMs": 2000,
|
|
283
|
-
"updatedAt": 1730000000000
|
|
284
|
-
}
|
|
65
|
+
```
|
|
66
|
+
plugin_manager install_bundle target = <absolute path to the package>
|
|
285
67
|
```
|
|
286
68
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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.
|
|
290
72
|
|
|
291
|
-
##
|
|
73
|
+
## Usage
|
|
292
74
|
|
|
293
|
-
|
|
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.
|
|
77
|
+
|
|
78
|
+
| Tool | What it does | Parameters |
|
|
294
79
|
| --- | --- | --- |
|
|
295
|
-
|
|
|
296
|
-
|
|
|
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
|
|
297
91
|
|
|
298
|
-
|
|
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:
|
|
299
134
|
|
|
300
135
|
```yaml
|
|
301
136
|
- id: dsh-autorestart
|
|
@@ -304,30 +139,104 @@ or by requesting the health path with ?gate=1.
|
|
|
304
139
|
restartCommand: systemctl --user restart dsh-web
|
|
305
140
|
```
|
|
306
141
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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,…}
|
|
329
174
|
```
|
|
330
175
|
|
|
331
|
-
`
|
|
332
|
-
|
|
333
|
-
|
|
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
|
+

|
|
202
|
+
|
|
203
|
+

|
|
204
|
+
|
|
205
|
+

|
|
206
|
+
|
|
207
|
+

|
|
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
|
+
```
|
|
228
|
+
|
|
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.
|
|
237
|
+
|
|
238
|
+
## AI-generated notice
|
|
239
|
+
|
|
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.
|