Vision & Multimodal

dsh-restart-ajia1206

ajia1206/dsh-restart · ★ 0

ajia1206

One-call restart for the running DSH process, with launchd-aware supervision detection, a verifiable restart log, and a bottom-right GUI control.

README body is shown in Chinese.

License
MIT
Added
2026-10-30
Maintenance
active
Install kind
github
Manifest details
Manifest status
Declared
Manifest path
package.json#dsh
Manifest checked
2026-10-30T09:38:25.280Z
Lifecycle scripts
None found

Static manifest checks report declarations only; they are not compatibility or security review.

安装

dsh plugin --profile web add 'github:ajia1206/dsh-restart#b23fb47c3df9f25fc8858eb279dd50e83c69a465'

GitHub 源码安装:首次需按提示配置 allowBuilds 构建授权后重试

For install and environment setup, plus plugin development tutorials, see DSH community docs ↗

README

Source README · body: zh · Read Chinese README

Table of contents
  1. 为什么需要它
  2. 四种策略
  3. 三个工具
  4. 浏览器控件
  5. 重启日志
  6. 配置
  7. launchd 与父进程
  8. 安装(第三方用户)
  9. 安装(本机开发)
  10. 开发
  11. 已知边界
  12. 验证记录
  13. 2026-09-25 桌面版修复
  14. 2026-09-29 复审修复回合
  15. 自查补上的两处(同一回合)

一键重启正在运行的 DSH 进程,并且把这次重启留下可核验的证据——不是发个信号就完事。

为什么需要它

DSH 对 profile 配置保持热监听:新增或移除一个 bundle 会当场挂载,工具立刻可用,不需要重启。但 hmr 默认以 root: [] 启用——只监听配置,不监听源码模块——所以改动一个已经挂载的插件源码不会生效,必须重启。而重启 DSH 意味着当前浏览器连接断开,如果重启失败,服务就再也起不来了。这个插件把这件事做成一次工具调用,并保证两件事:

  1. 优雅退出:走 DSH 自己的 ctx.appExit,先 dispose 整棵插件树(会话落盘、HTTP 服务关闭),再让进程自然结束——不是 kill。(两条例外:桌面版走 macOS 应用退出入口;万一 launcher 没提供 appExit,才退化成给自己发 SIGTERM。)
  2. 退出前先确认"有人会把它拉起来":确认 launchd 有 KeepAlive 才直接退出;没有可确认的托管就派一个 detached supervisor 并等它完成握手,握手失败就取消退出,服务保持运行。

四种策略

插件先探测自己是被谁托管的,再决定怎么做:

策略 触发条件 行为
desktop 识别为 macOS 桌面版(有主进程与 IPC 连接) 通过 macOS 应用退出/重新打开走一遍,等新的 shell 与 Host 重新连上
launchd 本进程自己是该 launchctl job 的主进程,且 KeepAlive 是无条件开启(条件式策略一律不认) 调 ctx.appExit(0) 优雅退出,由 launchd 按该 job 的配置拉起
supervisor 没有可确认的 KeepAlive 托管(手动启动、或任务主进程只是本进程的父进程) 派 detached supervisor:等旧 pid 退出 → 确认端口释放 → 用同一 execPath/execArgv/argv/cwd/env 拉起 → 健康检查 + 身份确认 → 结果写日志
none 没有托管且 superviseFallback=false 拒绝重启,服务保持运行

supervisor 的握手是关键:它在被杀之前先写一条 supervisor 记录,再在恢复资源预检通过后写一条 supervisor-ready;插件必须等到这两条齐全才允许自己退出,8 秒内没等到就写 aborted 并取消退出。握手只能证明"supervisor 已经起来了",不能证明"新进程一定能起来"——后者发生在旧进程退出之后,只能由 supervisor 记录下来,日志里会如实区分。

desktop 这一行不是凑数:lib/plan.js 在探测到桌面事实时最先选它,Web 的 launchd/supervisor 判定根本不会走到。

三个工具

dsh_restart — 安排一次重启。

参数:delaySeconds(默认 10)、reason(写进日志)、dryRun(只看计划不安排)。

默认延迟 10 秒,是为了让工具结果和最终回复先送达浏览器;退出本身是优雅的,会话会被 dispose 流程落盘。调用后应当只回一句短话,不要再调其他工具。

dsh_restart_status — 看托管方式、当前是否已安排重启、以及重启历史。

这是核验手段:插件就绪时会写一条 boot 记录(带 pid、profile 与 cwd),和之前的 armed/exiting 记录配对后就能算出这次重启的停机时长。注意 boot 是「插件就绪」的证据而非「进程启动」的证据(热挂载也会写一条),且宿主没有就绪信号时会立即写、写入失败会被静默吞掉——所以「没有 boot」不等于「没启动」。

dsh_restart_cancel — 在触发前取消。误触发时用。

浏览器控件

web 界面右下角有一个悬浮的「↻ 重启」按钮,点开是确认弹窗:显示当前托管方式、进程与端口、上一次重启的停机时长;可以选延迟(立即 / 10s / 30s / 60s)、填一句原因(写进重启日志),再点「确认重启」。确认后弹窗变成倒计时,并提示本页连接即将断开。

它落在 ui-layout 声明的 shell.overlay 座位上——那是整个框架的浮层,本身点击穿透,只有控件自己开启 pointer events,所以不会挡住下面的界面。

控件走三条宿主路由(同源 fetch):

路由 方法 作用
/dsh-restart/status GET 托管方式、端口、是否已安排、重启历史
/dsh-restart/arm POST 安排重启,body 为 {delaySeconds?, reason?}
/dsh-restart/cancel POST 取消已安排的重启

鉴权说明(这条别想当然):注册路由本身不带鉴权,所以每条请求都会先交给宿主的 connection 服务判定。宿主没有这个服务时,插件不会直接放行,而是强制只接受回环来源(127.0.0.0/8、::1、::ffff:127.x),非回环一律 403;取不到来源地址也按非回环处理。也就是说这三条路由不会因为「反正是本机」而对外网开放。两个必须说清的边界:一是在只有回环的机器上,它们等同于「本机进程都能调」;二是如果本机跑着反向代理,经代理转发过来的外部请求来源地址也是回环,这种部署下缺了 connection 服务就等于没有鉴权——别把这条回环兜底当成对外防护。

这三条路由和三个工具共用同一套实现(index.js 里的 armRestart / cancelRestart / readStatus),不存在两套逻辑。路由只在有 webServer 服务的载体上注册(用 ctx.inject(['webServer'], ...) 可选注入),headless 等载体只有工具,不会卡在等 web 服务。

客户端半边是手写的 lib/client.js,直接写成客户端模块加载器要求的 lazy-CJS 形状(window.ModuleLoader.load({id, factory})),因此这个包不需要任何构建步骤,也没有 tsdown 依赖。它只 require react 这一个基座模块,配色全部走 --dsw-* 主题 token。

重启日志

默认 <home>/.dsh/logs/dsh-restart.log,JSONL,一条一行(重启日志以 0600 创建,因为 boot/armed 会记录进程命令行;注意 supervisor 的子进程 stdout/stderr 日志 childLogPath 不是 0600,按系统默认权限创建):

kind 含义
boot 插件就绪后写入,带 pid/ppid/模式/端口/profile/启动命令。宿主没提供就绪信号时立即写;写入失败被静默吞掉,所以"没有 boot"不等于"没启动"
armed dsh_restart 安排了重启,带 armId、fireAt 与原因
supervisor detached supervisor 已启动并完成握手
supervisor-ready supervisor 的恢复资源预检通过了。宿主等到这条才允许自己退出:只有握手不够,预检失败时退出会被取消
exiting 进程即将优雅退出(写完后才调 appExit),带同一个 armId 与 pid
supervised supervisor 拉起新进程后的结果(ok/新 pid/健康检查详情/端口是否确认释放/新进程身份是否确认)
spec-cleanup-failed spec 文件删不掉(不影响重启,只记一笔)
cancelled / aborted 取消或中止,带原因

什么才算一次完成的重启:armed 与 exiting 的 armId 相同、随后出现的 boot 来自另一个 pid、且该 armId 没有失败记录。只靠"armed 后面跟着 boot"是不够的——默认日志被所有 profile 共用,别人的 boot、或者热挂载同一个进程写下的 boot,都不该被算成你这次重启成功。崩溃不会被误报成重启成功。

supervisor 路径的拉起动作不被证据阻断:只要宿主提交了退出、旧进程也真的退出了,它就会用同一份命令行把新进程拉起来——端点探测的结论只决定它怎么报告,绝不决定它做不做。

ok 的判定则很严:portReleased(旧端点在 socket 层不再监听)与 bootMatched(新进程自己写下了 pid 相同的 boot 记录)都必须为真;没有可探测端点时 portReleased 记为 null,此时只认 boot 记录。少任何一个都报 ok:false 并写明原因——宁可误报失败,也不假报成功。代价是:宿主不写 boot 记录的场合,supervisor 兜底会持续报失败(而服务其实活着)。

「端口已释放」按 TCP 连接判定:连接被拒才算已释放,能连上或超时都算仍在监听——一个一直回 503 的监听者不算「端口已释放」。

「仍然拉起」的已知代价:重复实例。 supervisor 的两个闸门只能证明"本宿主提交了退出"和"本宿主的旧 pid 已经消失",不能证明没有别人已经把它拉起来(launchd、另一个 supervisor、手工操作都算)。所以这种场合我们仍会再 spawn 一次,结果是可能出现两个实例:端口被争用时通常第二个会因绑不上而自行退出并被记成 ok:false(newPid 非空),但若监听不独占端口,两个都可能继续跑下去。这是为了「绝不因为端点证据而放弃恢复」而付出的代价,取舍方向是明确的:宁可多拉一次,不可少拉一次。

downtimeMs 量的是从写 exiting 到新进程写 boot 的间隔,包含优雅退出(会话落盘等)耗时,不等于 HTTP 不可用时长。

崩溃可能留下一行没写完的 JSON:读取时跳过它;下一次写入前会先补一个换行,避免把新记录粘到那行残骸上一起被丢掉。

配置

- id: dsh-restart
  name: dsh-restart
  config:
    logPath: /绝对路径/.dsh/logs/dsh-restart.log
    childLogPath: /绝对路径/.dsh/logs/dsh-restart-child.log
    defaultDelaySeconds: 10
    maxDelaySeconds: 600
    superviseFallback: true
    trustParentJob: false
    healthUrl: ''
    healthTimeoutMs: 60000
    healthIntervalMs: 500
    exitTimeoutMs: 45000
    bootHistoryLimit: 5

路径必须是绝对路径:这两个值会被原样使用,不展开 ~,也不展开环境变量。写 ~/.dsh/... 会在当前工作目录下真的建一个名叫 ~ 的目录。不想手写路径就不要配这两项,用默认值——默认值由 homedir() 拼出来,是正确的。

字段 默认值 含义
logPath <home>/.dsh/logs/dsh-restart.log 重启记录(JSONL)
childLogPath <home>/.dsh/logs/dsh-restart-child.log supervisor 拉起的新进程的 stdout/stderr
defaultDelaySeconds 10 默认延迟秒数
maxDelaySeconds 600 delaySeconds 上限
superviseFallback true 无托管时是否派 supervisor 兜底
trustParentJob false 见下方"launchd 与父进程"
healthUrl 空 留空则用 --port 推导 http://127.0.0.1:<端口>/
healthTimeoutMs 60000 supervisor 等端口恢复的上限
healthIntervalMs 500 健康检查轮询间隔
exitTimeoutMs 45000 supervisor 等旧进程退出的上限;超时不强制终止任何进程,直接如实报失败
bootHistoryLimit 5 状态里保留的最近重启次数

端口是从 DSH 自己的 cmdlineArgs 里读 --port 得来的,不靠猜;读不到才退回 lsof。两者都拿不到且 healthUrl 为空时,拉起照常执行(这条路径上能阻止拉起的只有两件事:旧进程没退出、宿主没提交退出),只是「服务是否恢复」只能由新进程写的 boot 记录来确认,没有端口层面的证据。

launchd 与父进程

判断"launchd 会不会把我拉回来"时,只认两种情况之一:本进程就是某个 KeepAlive 任务的主进程(job.pid === pid),或者该任务的主进程是本进程的直接父进程(job.pid === ppid)。

只有第一种能证明 launchd 会重启我。第二种依赖一个额外假设——父进程会随本进程一起退出——而这个假设无法在本地证明:如果父进程是个不随子进程退出的包装进程,本进程退出后就没有任何人拉起它。

所以第二种默认不走 launchd,改用 supervisor 兜底(慢一点,但不会把自己关在门外)。只有你明确知道自己的父进程会随子进程退出时,才把 trustParentJob 打开;打开后 warning 仍会保留,因为那条链路上 KeepAlive 重启的毕竟是 job 主进程,比自持多两个不确定环节。

KeepAlive 的读法按这个原则收紧:只在 launchctl print 输出的 properties 里读,且支持它的两种真实渲染——本机(macOS 26)是单行旗标表 properties = keepalive | runatload | ...,列表里有 keepalive 才算无条件开启;旧式多行 properties = { keepalive = 1 } 块也认。不对整段输出做关键字匹配——否则 job label、环境变量值或 endpoint 名里出现 "keepalive" 就会被误判成有人托管。条件式策略(semaphores = { successful exit => 0 },或块里的字典值)一律不认;没有 properties 行时按"无法确认"处理,同样倒向 supervisor。

安装(第三方用户)

装进任意 profile,两个来源二选一:

dsh plugin --profile web add github:ajia1206/dsh-restart     # 仓库公开后可用
# 或从 GitHub Release 的 tgz 安装(市场一键安装用的就是这条地址)
dsh plugin --profile web add https://github.com/ajia1206/dsh-restart/releases/latest/download/dsh-restart.tgz

本包没有发布到 npm:dsh-restart 这个名字在 npm 上已被他人占用,要发只能用带作用域的 @ajia1206/dsh-restart,而那会同时改掉 package.json 与 cordis.patch.yml 里的包名、并需要重装一次 profile 依赖。目前选择的是 Release tgz 这条更轻的路。

装完服务端半边当场生效(新增 bundle 立即挂载,三个工具马上可用);右下角的按钮要重启一次才出现——客户端 bundle 是宿主在启动时快照进启动图的,运行中新增的客户端半边不会被补扫。用 dsh_restart 重启一次即可。

安装(本机开发)

必须用 file: 前缀装成 profile 内的真实目录(裸目录会被 pnpm 装成软链,裸包名解析不到共享层,插件会静默不激活):

cd /path/to/dsh-restart
dsh plugin --profile web add file:"$PWD"
dsh --profile web --dump-config | grep -A2 dsh-restart

之后改这个插件自己的源码也需要重启(源码模块不在热监听范围内)。想先只读确认,可以调 dsh_restart_status,它会报告托管方式、端口和重启历史。

如果插件本身装坏了、连工具都起不来,就得手动兜底:

# launchd 托管时,换成你自己 job 的 label
launchctl kickstart -k gui/$(id -u)/<你的 launchd label>

卸载:

dsh plugin --profile web remove dsh-restart

dsh 不在 PATH 上时,用共享层入口(路径随安装位置变化):

node <共享层 node_modules>/@deepseek-ai/dsh/lib/bin.js

开发

node --test test/*.test.mjs    # 107 项:进程探测、launchd/KeepAlive 解析、计划决策、日志历史与配对、HTTP 路由与鉴权、客户端 bundle、恢复预检与 supervisor 启动、supervisor 真实拉起、桌面版退出路径
node scripts/smoke.mjs <pid>   # 对任意 pid 只读打印探测结果与 Web 重启计划(不提供桌面事实)

lib/proc.js、lib/plan.js、lib/log.js、lib/http.js 不 import 任何 @deepseek-ai/*,也不碰 DSH 运行时,所以单测不需要启动 DSH;只有 index.js 依赖 dsh-tools 与 schemastery。supervisor.mjs 是独立进程,可以单独跑。

test/client.test.mjs 把 lib/client.js 放进一个假的 window.ModuleLoader 里求值,用 React 桩渲染组件,因此客户端半边的不少错误(请求了非基座模块、布防/取消的状态判断写错、渲染出错误文案)会直接测试失败,不必开浏览器才发现。但它不是浏览器:effect 被禁用、事件处理器只被直接调用,所以 effect 内部的错误仍可能逃过这层测试。

改完代码后同步到 profile(pnpm 的 file: 目录依赖是硬链接,编辑器常见的"写临时文件再改名"会断开链接):

dsh plugin --profile web remove dsh-restart
dsh plugin --profile web add file:"$PWD"

已知边界

  • Web 只管当前进程;macOS 桌面版重启整个应用与 Host。桌面版必须确认主进程身份与 IPC 连接,退出经由系统应用退出入口,保留任务确认;取消或超时不强杀、不启动独立 Host。其他桌面平台或脱离主进程的 Host 拒绝重启。
  • 超时不做强制终止。旧进程超过 exitTimeoutMs 还没退出时,supervisor 只如实记录失败(ok:false);它不会发 SIGTERM/SIGKILL,也不会替你"解决"卡住的进程。
  • supervisor 兜底路径的停机时间更长:要等旧进程退出 + 端口释放 + 新进程起来,本机实测 2-5 秒;launchd 路径由 launchd 直接拉起,本机实测 1-2 秒。这两个数字是本机测量,不是跨机器的承诺——实际取决于你的 launchd 配置与机器负载。
  • 不等待"当前回合结束"。DSH 只暴露了 cmdlineArgs / appExit / appReady 三个启动期服务,没有回合级钩子,所以重启是在固定延迟后触发。延迟内如果模型还在输出,那条消息可能来不及落盘——默认 10 秒就是为这个留的余量,必要时用 delaySeconds 调大。
  • 健康检查把 4xx 当作"已恢复":DSH web 在没有 token 时返回 401,这已经说明服务起来了。另外,健康检查只能证明"那个端口上有人应答",所以判定成功还要加上新进程自己的 boot 记录作为身份确认。
  • env 不落盘,但 argv 会。supervisor 拉起用的是当前进程的 env,env 不写进 spec 文件;但 spec 文件里有进程命令行(boot/armed 记录里也有),所以命令行里带的凭据会落到磁盘上。插件为此把日志与 spec 都写成 0600,并在 supervisor 读完后删除 spec;如果你把 token 直接写在命令行里,仍请注意这一点。
  • 不校验新进程真的是"新版本"。它只是用同样的命令再拉一次;如果新代码本身启动失败,行为取决于你的 launchd 配置(本机未设 ThrottleInterval,默认 10 秒,会反复重试;别的机器可能只试一次或根本不试)。supervisor 路径不会自动重试。
  • 本 README 里关于 DSH 宿主行为的话,都是实测口径。热监听只覆盖配置、运行中新增的客户端半边不会被补扫、启动期只有那三个服务——这些是在本机 0.2.0-rc.1 运行时上观察到的行为,不是对任意版本的保证;换版本请重新验证。
  • 桌面版判定依赖 profile 名与工作目录。桌面就绪判定要求 profile 名是 desktop 且 cwd 不变;scripts/smoke.mjs 不提供桌面事实,所以它只覆盖 Web 的重启计划。

验证记录

本机实测(2026-09-18,北京时间):

检查 结果
单测 node --test 46/46 通过
supervisor 真实拉起(集成测试) 杀掉旧进程 → 等端口释放 → 拉起新进程 → 健康检查 ok,新 pid 存活并重新服务端口
supervisor 失败路径 新进程起不来时记录 ok:false 与原因,不谎报成功
真实端到端重启(scratch web profile,临时端口) 三个工具全部注册;日志依次出现 boot → armed → supervisor 握手 → exiting → supervised ok:true → 新进程 boot;停机 3317ms;原 pid 消失,新 pid 重新服务端口(401)
重启保留启动上下文 重启后的命令行仍是 --profile --patch ... --host 127.0.0.1 --port --no-open
对活 web 进程的只读探测 pid → 父进程 → job ,KeepAlive=true,端口 ,判定 launchd,无告警
误判回归(重要) 从 DSH 会话里启动的进程(launchd job 只是远祖)曾被误判成 launchd——那会导致退出后再也起不来;已改为只认本进程或直接父进程,回归测试覆盖
又一次误判回归(2026-09-29) KeepAlive 原先是拿 /keepalive/i 扫 launchctl print 全文,job label / 环境变量 / endpoint 名里出现这个词就会被误判为有人托管;已改为只在 properties = {} 块内读键,且"任务主进程只是父进程"不再默认走 launchd(改用 supervisor 兜底),回归测试覆盖
在活的 web 进程里调用工具(2026-09-18 当时口径) dsh_restart dryRun 返回 strategy=launchd、label=、port=、restartable=true;dsh_restart_status 返回 uptimeSeconds=23092、portResponds=true。注意:该形态下 job 主进程是 DSH 的父进程(pnpm),按 2026-09-29 收紧后的规则,同一场景现在会返回 strategy=supervisor
热挂载行为 安装进 web profile 后,正在运行的宿主进程当场挂载了本插件并写下 boot 记录,工具立刻可用,无需重启
浏览器控件(真实 Chromium 实测) 右下角渲染出「↻ 重启」;点开后弹窗正确显示托管方式、pid 与运行时长、端口与响应、上次停机时长,延迟选项与原因输入均可用
从 GUI 触发的真实重启 点「确认重启」→ 弹窗变倒计时 → 日志出现 armed → supervisor 握手 → exiting → supervised ok:true → 新进程 boot(停机 2.0s)→ 端口重新响应(401)
主题适配回归 深色主题下主按钮填充是近白色,文字必须用 --dsw-alias-label-primary-foreground;曾因硬编码 #fff 变成白底白字,已修并有渲染测试覆盖

2026-09-25 桌面版修复

旧 supervisor 直接重建 Host 会丢失 Electron IPC,即使 HTTP 端口恢复也不能证明桌面恢复。现在桌面策略通过 macOS 退出/打开应用,并等待新 shell 与 Host 的连接启动记录;Web 的 launchd/supervisor 策略保留。源码通过 desktop profile 的 link 依赖生效,首次加载修改需要正常退出并重新打开桌面应用。

本机验证:52 项测试通过;通过桌面右下角插件按钮实际重启两次,均恢复原会话界面。最终一次 shell/Host 从 55467/55504 更换为 58814/58846,日志记录 desktopConnected: true、supervised.ok: true,耗时 13.7 秒。尚未实测有运行中任务时的原生确认交互;单元测试覆盖取消/超时不重新启动。

2026-09-29 复审修复回合

第三方复审(Codex,只读模式)指出:KeepAlive 判定是拿正则扫 launchctl print 全文(label/环境变量里出现该词即误判)、鉴权 fail-open、两处 spawn 无异步错误处理、健康检查把"端口没释放"也报成功、重启历史不按 armId 配对(跨 profile 会拼出假重启)、arm 先起定时器后写记录、恢复资源在退出后才发现不可写、日志/spec 无权限保护且 spec 不删、崩溃残行会吞掉下一条记录、路由内请求流读取出错会逃逸、客户端把 armed:false/cancelled:false 显示成成功、forceStop 是死代码且写了假的 forced:true、README 有几处承诺强于实现。

以上条目均已处理,并补了对应回归测试(含"label/环境变量里含 keepalive 必须是 null"这类误判回归,以及对每处修复做变异回退、确认新测试真会失败)。

其中三处是有意的取舍,不是遗漏:「超时不强制终止」;「宿主不写 boot 记录时 supervisor 会持续报失败(服务其实活着)」;以及「已存在的日志文件权限不会被改动」(0600 只在新创建时生效)。

另有三处已知边界不打算在这一版解决,如实记在这里:日志的并发写入(插件与 supervisor 同时 append)仍有一个极小的交错窗口,appendRecord 的补换行与追加不是一次原子操作;readRecords 只读尾部窗口,日志被高频写入时理论上可能读不到很久以前的记录;已存在的日志文件若权限过宽,插件不会去改它。

检查 结果
全套单测 107/107 通过(node --test test/*.test.mjs,约 15s,含真实拉起 supervisor 的集成测试)
实机只读探测 node scripts/smoke.mjs <pid> 对活着的宿主返回 keepAlive=null(RunningBoard 提交的桌面 app job 没有 properties 块)并给出"无法确认 KeepAlive"warning,策略判为 supervisor,不误判为 launchd
实机 HTTP 鉴权 真 socket:无 connection 服务时 127.0.0.1 的 status/arm 正常 200(本机 web profile 不受影响);伪造非回环来源的 arm 得 403 且 handler 未执行
无 unhandled rejection 客户端中途断流时进程挂 unhandledRejection 监听、退出码 0;handler resolve 出结构化 500 JSON

这一轮改动的跨文件协议:supervisor 在预检通过后才写 supervisor-ready(带 armId),宿主必须等到"握手 + ready"两条才允许退出——否则预检刚失败、宿主仍会照常退出。supervisor 侧有测试覆盖(含 ready 标记与失败路径);宿主的等待逻辑在 index.js 的 waitForSupervisor(),它没有单测——index.js 依赖 @deepseek-ai/* 基座,在当前测试基建下无法直接导入,这一侧只有语法检查与人肉走查,属于已知覆盖缺口。

自查补上的两处(同一回合)

修完之后自己又过了一遍新逻辑,发现两个由本轮改动引入的问题,一并修掉:

  1. bootMatched 会被上一次运行留下的 boot 记录满足。判定原本只要求"boot 记录的 cwd 匹配、pid 不在排除集内",而 cwd 每次重启都一样,于是日志里更早那次重启的 boot 会立刻让它为真——新进程其实还没起来。已改为把新进程自己的 pid 作为匹配条件(buildReplacementBootMatcher(cwd, newPid)),并补了一条"旧运行的 boot 不得当成就绪"的回归测试。桌面路径本来就有时间下界与进程身份复核,不受影响。
  2. lib/supervisor.mjs 不能被 import:main() 在文件底部无条件执行,导入它会把导入方的进程按 supervisor 的退出码结束——这也是它此前只能靠起子进程来测的原因。已加入口守卫(只有该文件本身作为程序运行时才执行 main()),真实启动路径不受影响(集成测试即为证据),现在它的内部函数可以被直接单测了。