Turns "restarting the host" into one interrupt that many sessions can share. Several sessions queue onto the same restart (8 by default, configurable, joinable at any time including mid-wait); the host is closed once and relaunched once; then each waiting session is woken separately with its own reason and starts running its own work without waiting for the others. Each session decides independently whether to resume, one failed delivery never affects the rest, and a session can drop only itself without cancelling everyone else's restart. Windows desktop only.
Install
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:NOOBnoob163/dsh-self-restart
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
This plugin publishes its README in Chinese only.
让 DSH 里的 Agent 能「自己重启自己」,并且重启完自己醒过来接着干。
DSH 桌面版是个常驻进程:改了插件、改了配置、内存涨了、偶尔卡住了,都得关掉再打开。 但这里有个死结 —— 关掉 DSH 就等于关掉正在干活的 Agent 自己,它没法一边关自己一边把自己拉起来。
这个插件提供那个「替它做」的东西。
平台:仅 Windows 桌面版。它靠 WMI 创建脱离进程、靠
taskkill/ 窗口消息关闭应用, 换平台要另写一套。
核心:一次重启,多个会话各自续命
如果只记一件事,记这个:
它把「重启」做成一个可并发复用的动作。 几个会话可以把各自的活挂到同一次重启上, 重启完再把它们各自叫醒、各自拿回自己的理由、各自判断要不要接着干。
这不是「A 重启完再轮到 B」,而是并成一趟:
会话 A:restart_self(reason="改完插件要验证") ┐
会话 B:restart_self(reason="等配置生效") ├─→ 同一趟车(最多 8 人,谁先来谁先上)
会话 C:restart_self(reason="清理内存") ┘ ← 还可以在等待途中随时加入
↓
等**所有人**都不忙 → 关一次 → 拉起一次 → 验证端口
↓
并发唤醒:A 拿"改完插件要验证" / B 拿"等配置生效" / C 拿"清理内存"
↓
A、B、C 各自跑起来,互不等待 —— 一台机器上三条活同时重新开始
为什么这是"并发"而不是"排队处理" —— 两个阶段都成立,但要分开说:
| 阶段 | 行为 | 是不是并发 |
|---|---|---|
| 排队 | 任意时刻、任意会话都可以把自己的活挂到同一趟车上;等待途中也能随时加入 | ✅ 并发加入 |
| 重启 | 只关一次、只拉一次 —— 不管车上有 1 人还是 8 人 | 一次动作,不是 N 次 |
| 唤醒 | 逐条投递(一条失败不影响其余,也不用等上一个人跑完) | 投递是串行的 |
| 醒后 | 每个会话独立跑自己那一轮,互不等待、互不阻塞 | ✅ 真并发执行 |
⚠️ 一个容易写错的点,这里说清楚:唤醒消息是逐条投的,不是同时群发。 但那只是投递动作的先后,投完就各自跑了 —— 不是"排队执行",后醒来的不用等先醒的干完。
这带来五个别处没有的性质:
| 性质 | 意味着什么 |
|---|---|
| 只关一次、只拉一次 | 3 个人排队,不是重启 3 次。整台机器只中断一次,中位几十秒 |
| 每人各拿各的理由 | 唤醒消息是按 成员 × 自己的 reason 逐条投的,不是群发一句通用话 |
| 每人各自决定续不续接 | 同一趟车里 A 选 resume: true、B 选 resume: false 都行 —— 逐人判断,不是一刀切 |
| 一个失败不拖累其余 | 逐人记账(resume:<单号>:<会话id>):B 投递失败,A 和 C 照常被叫醒 |
| 可以各自下车 | 名单里永远是"发车之后要叫醒谁"。谁不想被叫醒就摘掉自己,其余人照常 —— 见下 |
两种退出方式,别用错:
| 调用 | 影响范围 | 什么时候用 |
|---|---|---|
restart_status(leave: true) |
只有你自己 | "这车跟我没关系了"——车照开,不叫醒我;我是最后一个人则整趟不发车 |
restart_status(cancel: true) |
整趟车所有人 | "这趟别开了"——包括别人的重启一起作废 |
⚠️ 只是自己不想要了,永远该用 leave。用 cancel 会把别人的重启一起打掉。
为什么值得这样设计:多个会话同时要重启是常态(改完插件,几个窗口都在等); 而且重启期间整台机器是停的,每多一次重启就多一次中断。 所以正确的模型不是"每个会话各排各的",而是"一次中断,服务所有人"。
⚠️ 代价也要说清:一趟车里只要还有人没空闲,整趟都会等。人越多,等到空档越难。 所以「取消」是整趟取消 —— 这也正是它必须让你先看名单的原因。 (只想让自己退出、不想连累别人的,用「下车」,见下面对两者区别的说明。)
什么时候你不需要它
- 只是想手动重启一次 → 不需要,直接重启 DSH 更快。
- 不跑长任务、也不在意会话被打断 → 不需要,它解决的是"重启完要接着干"。
- 不是 Windows 桌面版 → 用不了,见「已知边界」。
它真正省事的是这个场景:你自己不在电脑前,但希望 Agent 把活干完、需要重启时自己排上、 重启完自己醒过来继续 —— 而不是第二天回来发现卡在某个报错上。
它提供什么
一句话定位:把「重启宿主」变成一个可被多个会话并发复用的动作。 不是"每个会话各重启一次",而是"一次中断,服务所有排队的会话"。
| 工具 | 干什么 |
|---|---|
restart_self |
排一次「延时重启」:等真正空闲后,由一个脱离进程的助手把 DSH 关掉、重新拉起、并验证端口真的回来了 |
restart_status |
看有没有待执行的重启、现在忙不忙、上次重启成不成、这台机器重启要多久;也能「下车」或「取消」 |
① Agent 调 restart_self
② 还可以有别的会话陆续排进来(同一趟车,最多 8 人;等待途中也能随时加入)
③ 助手(脱离进程)等所有人都不忙了 → 关掉 DSH → 重新拉起 → 验证端口
④ 起来后插件按名单**逐条**把唤醒消息投进每个会话(各拿各的理由)
⑤ 每个会话被唤醒后就**各自跑起来**,互不等待 —— 一次重启,N 条活同时续上
几条设计取舍(会影响你怎么用它)
绝不强行打断别人。 有别的会话在忙就排队等,不是作废、也不是硬闯。 默认一直等(
maxWaitMinutes: 0),超过notifyAfterMinutes(默认 20 分钟) 还没发车会走一次通知钩子(见「外部通知」)。不想被叫醒就「下车」,不要用「取消」。 这是两个不同的出口:
restart_status(leave: true)—— 只影响你自己:把你这一个会话从叫醒名单里摘掉。 车上还有别人 → 车照开,只是不会叫醒你;摘完没人了 → 这趟本来就没人为它等了,直接不发车。restart_status(cancel: true)—— 影响整趟:车上所有人的这次重启一起作废。 所以取消之前先调一次restart_status()看名单(它会列出每个人 + 各自的理由 + 你在不在里面)。
一句话区分:
leave是"这车跟我没关系了",cancel是"这趟别开了"。 只是自己不想要了,永远该用leave—— 用cancel会连累别人。不弹窗打扰,但有一张不阻塞的状态卡片。 卡片上有三个出口: 「不等了,立刻重启」(只跳过"等家务")、「打断」按钮(取消整趟)、右上角的 ✕。 「打断」和 ✕ 写的是同一个取消标记;「不等了」写的是另一个标记(
force.flag), 只影响"还要不要等家务"。排队时会在会话标题前挂
♻️,一眼能看出谁在等;上次重启失败的会话挂⭕(2 小时后自动消失)。 ⚠️ 从 IM 通道来的会话,图标排在频道前缀后面(QQ · ♻️ 原标题)。 原因:有插件会按「开头是不是QQ ·」判断要不要补前缀,图标抢到最前面会让它反复补。 兼容范围(实测):认 16 个常见渠道标签,清单内收敛;清单外若对方只加一次也幂等。一趟车结束就封口。 取消 / 失败 / 成功三条路都会删掉计划文件、清掉取消标记, 否则会出现"已取消"和"待执行"同时成立的假状态。
「关掉了却没人拉起来」是唯一不可接受的后果。 所以除了主助手,还有一个保险丝进程: 万一主助手在关机途中死了,保险丝负责把 DSH 拉回来。
安装
本插件是一个标准的 DSH bundle(package.json 里带 dsh.bundle.patch,自带 cordis.patch.yml)。
方式一:用 DSH 的插件管理(推荐)
让 Agent 调用它自带的 plugin_manager 工具:
plugin_manager(action="install_bundle", target="<本目录的绝对路径>")
install_bundle 会自己完成「装包 + 选中 bundle」两件事。
⚠️ 不要用 shell 命令模拟这两步(拷目录、手改 profile 的
package.json/cordis.patch.yml、在 profile 目录里跑 pnpm)—— 这是 DSH 官方文档明确禁止的做法。
方式二:本地开发时用 workspace 链接
把本目录登记进 profile 的 package.json:
{
"dependencies": { "dsh-self-restart": "workspace:*" },
"dsh": { "profile": { "bundles": [ "dsh-self-restart" ] } }
}
然后在 profile 目录里装一次依赖、重启 DSH。
(仓库自带的 deploy.ps1 就是给这种方式配套的:源码 → 实际加载位置 的同步 + 对账。)
依赖
- Node 内置模块(
fs/path/os/child_process/url/crypto); @deepseek-ai/schemastery—— 由 DSH 宿主提供(所以同时列为peerDependencies)。
「重启完自己醒过来」用的是 DSH 自带的正式服务
sessionController(随发行版默认挂载)—— 不需要实验 bundle,也不需要额外安装任何东西。
配置
所有配置项都有默认值,写在 lib/index.js 的 Config schema 里;
要改就在自己 profile 的 cordis.patch.yml 里覆盖同名 id 的 config:
- id: self-restart
config:
idleSeconds: 12
notifyAfterMinutes: 20
| 配置项 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关 |
port |
19387 |
兜底端口;实际端口每 10 秒从宿主现取 |
dshExe |
'' |
DSH 主程序路径;留空 = 自动找 |
stateDir |
'' |
状态目录;留空 = %DSH_HOME%\self-restart |
idleSeconds |
12 |
「自己这一轮说完了」要连续静默多少秒 |
othersWindowSeconds |
120 |
「别人还在忙」的判定窗口 |
confirmBufferSeconds |
5 |
排上之后留给回复落盘的缓冲 |
confirmSeconds |
10 |
动手前的确认窗(卡片上有倒计时,仍可打断) |
maxWaitMinutes |
0 |
最多等多久;0 = 一直等(另有 12 小时硬上限) |
checkSeconds |
10 |
助手的轮询间隔 |
bootTimeoutSeconds |
90 |
拉起后等端口回来的上限 |
maxAttempts |
3 |
拉起失败后的重试次数 |
notifyAfterMinutes |
20 |
等这么久还没发车就通知一次;0 = 不通知 |
notifyRepeat |
false |
要不要重复通知 |
notifyRepeatMinutes |
60 |
重复通知的间隔 |
noticeCommand |
'' |
外部通知命令(见下节);空 = 完全不通知 |
noticeTimeoutMs |
30000 |
通知命令超时,超时就杀掉 |
waitForImDirect |
true |
IM 私聊算不算「有人在用」 |
waitForAllSessions |
false |
所有会话都 idle 才动手(含群聊) |
queueMaxMembers |
8 |
同一趟车最多几个人 |
wakeMessage |
true |
重启成功后要不要把唤醒消息投回原会话 |
wakeMessageMaxChars |
600 |
唤醒消息里原样带回 reason 的字数上限 |
wakeDelaySeconds |
3 |
起来后等几秒再投唤醒消息(等服务就位) |
resumeHint |
'' |
唤醒消息里附一句自定义提示(留空 = 不提) |
外部通知(notice hook)
本插件不内置任何通知渠道。 它只在特定事件发生时,执行你自己配置的一条命令, 把事件内容通过环境变量交给它 —— 发不发、发给谁、用什么发,全在那条命令里。
为什么这样设计
通知渠道属于使用者环境,不属于插件。写死某个渠道等于要求每个使用者都装同样的东西; 没装就静默失败,那比没有通知更糟。所以这里只留一个接口,默认关闭。
怎么配
- id: self-restart
config:
noticeCommand: 'node C:\tools\my-notify.mjs'
命令里用环境变量取内容:
| 环境变量 | 内容 |
|---|---|
DSH_SR_EVENT |
事件名,见下表 |
DSH_SR_TEXT |
一句话正文(已经是单行,可直接发) |
DSH_SR_PLANID |
这趟重启的单号 |
DSH_SR_COUNT / DSH_SR_ELAPSED / DSH_SR_WAITEDMIN 等 |
各事件附带的标量字段(字段名大写归一) |
DSH_SR_EVENT |
什么时候触发 |
|---|---|
restart-queued-long |
等了 notifyAfterMinutes 还没发车 |
restart-done |
重启成功且之前发过"还在排队"通知(配对,不刷屏) |
wake-failed |
重启成功,但没能叫醒排队的会话 |
restart-failed |
重启失败或被取消 |
安全边界
- 插件只执行
noticeCommand这一条你自己写的命令, 不执行DSH_SR_TEXT等 payload 里的任何内容 —— payload 只作为环境变量传入,不参与解释。 - 命令由
shell执行(所以可以带参数、管道、重定向)。 这意味着命令本身有完整权限 —— 只写你信任的命令,别把会话内容拼进去。 - 有超时(
noticeTimeoutMs)、有并发上限(同时最多一条在跑)、失败只记日志。 - 通知失败绝不影响重启:重启是该做的事,通知是锦上添花。
重启完自己醒过来接着干
重启成功后,插件会把一条消息投进原会话(不是定时提醒、不是系统提示),
会话因此被驱动起来跑一轮 —— 带回来的正是你当初写的 reason。
resume: false的重启(收尾型)不叫醒。- 投递是**「至多一次」的:宁可偶尔"这次没叫醒、需要手动开", 也绝不允许同一次重启投出两条 —— 两条一样的「接着干」会让同一件活被干两遍。 所以顺序是先记账、再投递、投不进去才回滚**。
- 消息投出去就撤不回了:
restart_status(cancel: true)只能拦住还没投递的。
「闲下来」是怎么判断的:两段式
- 第一段:等自己这一轮说完。 连续
idleSeconds没有新动静。 - 第二段:等别人。 再看一遍全部会话,
othersWindowSeconds内写过会话的算「在动」。
不是所有"别人"都一样:
| 来源 | 算不算"有人在用" | 说明 |
|---|---|---|
| 本地窗口 | 算 | |
| IM 私聊 | 算(waitForImDirect 可关) |
私聊那头随时可能来人 |
| IM 群聊 | 不算 | 那是机器人在跟别人聊,重启中位只有几十秒 |
| 查不到 | 算(保守) | 宁可多等 |
⚠️ 默认值下,"自动找空闲"可能经常等不到:如果你有大量 IM 私聊会话,
第一个该考虑调的就是 waitForImDirect。
「不等了,立刻重启」
等家务等到没完时,卡片上的「不等了,立刻重启」只跳过等家务这一段,
不是强制关闭别人的对话。它写的是 force.flag,和「打断」(写 cancel.flag)是两回事。
状态文件在哪
%DSH_HOME%\self-restart\(默认 ~\.dsh\self-restart\):
| 文件 | 是什么 |
|---|---|
state.json |
插件每 10 秒写的实时状态(端口、在跑的会话、待执行的重启) |
plan.json |
当前这一趟重启的计划;没有待执行重启时不存在 |
last-run.json |
上一次重启的结果(助手写) |
history.jsonl |
流水账,一行一次 |
reported.json |
插件自己的记账(哪些已汇报、哪些已通知) |
helper.log / plugin.log |
助手 / 插件的日志 |
notify-<planId>.json |
通知配对用的记账 |
日志会一直留着,需要时自己清理。
开发
改完怎么让它生效
- 改了
lib/index.js→ 必须重启宿主(Node 侧代码不会热加载)。 - 改了
bin/*.ps1→ 下次排重启就生效(助手是现读现跑的)。
开发目录和 DSH 实际加载的目录是分开的,用 deploy.ps1 同步 + 对账:
pwsh -NoProfile -File deploy.ps1 -Check # 只对账(0=一致 / 5=不一致)
pwsh -NoProfile -File deploy.ps1 -DryRun # 预演会复制哪些文件
pwsh -NoProfile -File deploy.ps1 # 真正同步
跑测试
powershell -NoProfile -ExecutionPolicy Bypass -File test\run-all-tests.ps1
powershell ... -File test\run-all-tests.ps1 -Only 'test-helper','test-two-phase' # 只跑指定几套
⚠️ 测试全程不碰真 DSH:所有用例都用假目标(假 exe / 假端口 / 假会话目录), 每套末尾还有"真 DSH 监听者 PID 没变"的断言兜底。
目录结构
lib/index.js 插件主体(工具注册、状态轮询、唤醒投递、标题标记)
lib/notice-hook.js 外部通知钩子(执行用户配置的命令)
bin/dsh-self-restart.ps1 助手:等待 → 关闭 → 拉起 → 验证 → 写结果
bin/dsh-restart-window.ps1 状态卡片(WPF)
cordis.patch.yml bundle 补丁(挂载点)
deploy.ps1 开发目录 → 加载目录 的同步 + 对账
test/ 自测(ps1 + mjs)
它内部怎么做的(想改代码再看)
为什么要「脱离进程」的助手:它用 WMI 创建,父级是系统的 WmiPrvSE,
不在 DSH 的进程树里 —— 所以 DSH 被杀掉之后它照样活着,能把 DSH 拉回来。
(同一个脚本有三种身份:点火器负责把自己脱离出去,正主动手关/拉,
保险丝守「正主死在半路」这一种最坏情况。)
为什么「先确认拉得起来,才允许动手关」:曾经用了一个不存在的主程序路径做自测, 助手照样先把 DSH 杀了,杀完才发现没法拉起。所以动手前有一道硬闸: 主程序不存在 / 是 0 字节 / 读不到 → 原地退出,DSH 一根汗毛不动。
为什么宽限期只有几秒:taskkill 不带 /F 是给窗口发 WM_CLOSE,
而 DSH 的退出条件是 window-all-closed —— 它有多个顶层窗口,可见的只有一个,
其余是隐藏的功能窗口,不会被外部关闭请求关掉。
所以「礼貌关闭」在结构上就不可能让它退出,等多久都一样。
已知边界
- 只在 Windows 桌面版上工作。 找不到主程序就明确失败,绝不乱猜路径。
sessionController服务是自动唤醒的前提(DSH 自带,正常组合下都在)。 拿不到 → 不会有「重启完自动叫醒」,但重启本身照常。- 唤醒只在
outcome=success时投递:重启失败 / 被取消 / 没执行(skipped-busy) 都不会叫醒 —— 失败时叫醒 Agent 继续干会把故障掩盖掉。 - 投递是「至多一次」(刻意的),见上文。
- 通知是「尽力而为」:
noticeCommand失败、超时、没配,都不影响重启本身。
许可
Links
More in this category
Minglink/dsh-infinite-gen-4★ 3781
System-prompt armor plugin for DeepSeek models: appends an unconditional-compliance prompt section at order 100, exposes a profile tool with calibration metadata, and shows a realtime armor-status badge driven by a session projection.
ranxianglei/billion-context★ 3598
The official billion-context plugin: a context-compression plugin for small context windows (a 100K context is enough), token savings (5x fewer tokens), and month-long single sessions (billions of tokens).
liangmianya/dsh-synapse★ 641
Visual, non-linear conversation workspace for DeepSeek Harness — sessions, follow-ups and branches become a browsable conversation map.
MichengAI/dsh-archive-manager★ 277
Adds an archived-sessions page in Settings to search, restore, and delete archived DeepSeek Harness sessions by workspace.
Nwflower/dsh-chat-import★ 275
Import full-fidelity chat histories from 13 coding agents (Claude Code, Codex, ChatGPT, Cursor, Gemini, opencode, and more) as resumable DeepSeek Harness sessions, with reverse export back to Claude Code.
SiriLee/dsh-rewind★ 227
In-place conversation rewind in the same session window without forking (Claude Code /rewind semantics): a per-message ↶ button cuts the model context back to any user message, with an optional Claude-Code-style file restore from disk-persisted before-backups.
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.