dsh-safety-restart
English TL;DR — A safe, seamless restart plugin for DeepSeek Harness. The agent can restart the host itself (host-half code changes only take effect after a full process restart) and gets woken up afterwards to continue. Before restarting it must pass four gates: restart-loop breaker, other sessions mid-turn, unresolvable profile bundles, and (on desktop) an unavailable app.relaunch(). Any gate that fails means refuse with a reason, never force it through. Desktop uses Electron app.relaunch(); web/CLI uses a detached helper.
DSH 的安全无感重启插件。
它解决什么
改完宿主半(index.js / lib/*.js)必须整进程重启才生效。这不是洁癖,是实测:
回收渲染器进程只会重载外壳。插件确实会重新
apply、日志也照打,但服务请求的那张路由表仍然是更早那次 apply 的 —— 新加的路由一律 404,而返回的是插件自己那句not found,看起来像"路由写错了",其实是"根本没走到新 handler"。
而在此之前 agent 没有重启的手段:只能让用户去点。这个插件把它变成一次工具调用。
四道闸("安全"具体是什么)
每一条都对应一次真实踩坑,不是设想出来的风险。
| 闸 | 拒绝条件 | 为什么 |
|---|---|---|
| 重启循环熔断 | 窗口内已重启 ≥ loopMax(默认 3 次 / 10 分钟) |
一个起不来的配置会让每次启动都失败,而 agent 看到失败只会想再重启 —— 那是重启风暴,代价是机器一直在 boot |
| 有别的会话在跑一轮 | 除调用方外还有会话处于 turn/start 之后未 turn/end(除非 force: true) |
盲着重启会把别人正在做的事从中间切断,而且切断后那一轮的工具调用结果未知 |
| profile 的 bundle 表解析不了 | 某个 bundle 在 node_modules 里不存在、或入口文件不存在;或者根本找不到 profile 根目录 / 读不到它的 bundle 表(checked:false 一律拒绝) |
重启进一个坏掉的 profile,得到的是起不来的 app —— 连报错界面都没有;而"没查到"不等于"检查通过"—— 未知状态应当拒绝,不是绿灯 |
| 拿不到可靠拉起手段 | 桌面端 electron.app.relaunch 不可达 |
绝不退化成"杀进程":那会把 app 关掉而不拉起来,把可恢复的状态变成不可恢复的 |
任一不过就是拒绝重启并说明原因,返回里带 gate 与 why。拒绝不是失败 —— 那道闸就是为了不让你把环境弄成不好收拾的样子。
无感在哪
- 重启前写续作标记(会话 id),重启后把"你已经重启过、请继续"投回原来那个会话。 顺序是刻意的:先写标记,再拉起。反过来会在进程先退出时丢掉标记。
- 续作提示里明确要求:重启前结果未知的工具调用不要盲目重试,先核查实际影响。
- 标记消费掉就删;标记文件损坏时返回空数组而不是抛 —— 重启后的启动不能因为读标记而挂。
两条重启路径
| 环境 | 机制 | 为什么不能只用一种 |
|---|---|---|
| 桌面端(Electron 内嵌宿主) | app.relaunch() + 延迟 app.quit() |
桌面端宿主的 webServer 是 IPC 载体(app://),不开端口 —— 任何"起 helper 等健康检查通过"的做法在这里都不成立 |
| web / CLI | 写一个 detached helper(detached:true + stdio:'ignore' + unref()),老进程随后 process.exit(0) |
helper 必须在父进程死掉之后继续活着;它等旧 pid 退出 → 用同一份 execPath / execArgv / argv 拉起新实例 → 等它活过 10 秒 → 若命令行里推得出 web 端口(--port 或默认 3080)再等端口恢复(健康检查) → 把结果写进审计。健康检查来自 dsh-guardian 编排器(90s 健康检查)与 start-harness.ps1(端口探测 + HTTP HEAD)的同类实践 |
装
// profiles/<name>/package.json
{
"dependencies": { "dsh-safety-restart": "link:/path/to/dsh-safety-restart" },
"dsh": { "profile": { "bundles": [ "...", "dsh-safety-restart" ] } }
}
装完必须重启一次 app —— 插件自己也要先被加载才谈得上重启。
用
Agent 侧(工具):
safety_restart({ reason: "改了宿主半的 index.js,让新路由生效" })
safety_restart({ reason: "...", force: true }) // 即使有别的会话在跑一轮也重启
人侧:设置 → 插件 → 「dsh-safety-restart」卡片(运行环境、熔断计数、最近一次审计、立即重启)。
HTTP(web 端;只接受本机请求,反代/远程访问会被 403):
GET /safety-restart/status
POST /safety-restart/restart { "reason": "ui" }
审计与状态文件
都在 $DSH_HOME(默认 ~/.dsh):
| 文件 | 内容 |
|---|---|
safety-restart-audit.jsonl |
只追加。一次重启算一次的判定是 kind:"restart" && phase:"scheduled";其余(helper / exit / quit / refused / resume)都是诊断信息 |
safety-restart-resume.json |
续作标记(待叫醒的会话 id),消费后删除 |
safety-restart-helper.mjs |
web 端生成的 helper(每次重写) |
safety-restart.json |
可选配置:loopWindowMs / loopMax / delayMs / busyStaleMs |
配置
{
"loopWindowMs": 600000,
"loopMax": 3,
"delayMs": 1500,
"busyStaleMs": 1800000
}
delayMs 是"发起重启"到"真的退出"之间的等待,留出时间把这次工具结果回传给模型。设成 0 会让结果大概率丢掉。
已知限制(如实写,不粉饰)
busy只覆盖本进程启动之后的轮次。 它是按session/event维护的,更早就在跑的会话不在里面。所以第二道闸是"尽力而为",不是权威快照。超过busyStaleMs的记录会被当作过期清掉(防止一条漏掉的turn/end把闸门永久焊死)。- bundle 体检是轻量的:它只证明"包在、入口文件在",不证明它能被 import 成功,更不证明客户端半在 slot 契约上是对的。要那一层用 dsh-guardian 的 bundle scan。
- web 健康检查是"端口恢复",不是"HTTP 200"。 helper 只做 TCP 连接探测(127.0.0.1 上的端口能连上就算恢复)。它证明的是 web 在监听,不是 web 健康 —— 后者需要服务的业务路由(dsh-guardian 探
/guardian/report就是这样)。端口怎么推:--port <n>/--port=<n>优先,否则 argv 里有web子命令默认 3080,推不出来就跳过健康检查退回 10 秒存活检查。宁可不探测也不探测错误的端口:新实例其实活着,却因为探错端口误报失败,比不探测更糟。 - web 端退出是
process.exit(0)(非优雅)。 会话日志是只追加的 jsonl,写到哪算哪;工具结果在delayMs之前已回传;尾巴真被截了也有续作提示兜。不退就永远重启不成,那才是真的坏。 - 客户端半按 rc 契约写:
settings.plugin.item在 rc.12 是list(要id),更晚的契约是keyed(要key)。所以两个都传 —— 多传一个用不到的字段不会报错,少传必需的那个才会。整个apply包在 try/catch 里:契约再漂,结果也只是"没有这张卡片",不会牵连界面。
这里的坑都是踩过的
开发过程中端到端测出来、并已修掉的两个:
- web 路径最初根本没完成:helper 起来了、等了 30 秒、然后报
old pid still alive — giving up。因为没有任何人让老进程退出 —— helper 等的是"老进程死掉",而它就那样继续服务着。修法是 helper 起来后老进程自己process.exit(0)。 - 熔断计数翻倍:一次重启会在审计里留下多条(
begin/scheduled/helper/quit),全数进来会让"一次重启动"算成两次 —— 实测第三次就直接熔断了,而配置写的是 3。修法是只数一条。
自检
node scripts/run-all.mjs
47 条断言,都是"闸门的判定边界":熔断窗口与计数口径、checked:false 的诚实降级、标记损坏不抛、生成的 helper 是合法 ESM(含带健康检查端口的变体)、--port 推端口的解析规则、会话 id 的三种取值回退、调用方自身的排除。
License
MIT
No comments yet. Be the first to write one.