DeepSeek Harness Hub
← 返回列表

崩溃守护与自动恢复ddtcorex/dsh-maestro-supervisor

DeepSeek Harnessspec-screened在 GitHub 查看 ↗
未验证

崩溃自动回滚快照,重启后恢复会话并重载页面

尚未跑自动兼容性验证,可查看页面内的依赖与入口分析。 · 最近上游提交 2026/9/15 · 已提供中文文档

# DSH Web 弹性守护进程 — 自动检测崩溃、回滚到 LKG、报告

综合分
30.6
GitHub 分
30.6
用户评分
★ Stars
1
周下载量
安装插件(需先安装 dsh CLI 引擎:npm install -g @deepseek-ai/dsh)
dsh plugin --profile web add ddtcorex/dsh-maestro-supervisor
该插件未发布到 npm,走 GitHub 源安装(pnpm 若拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑)
数据截至 2026/9/16(元数据每日更新 · 实装验证按队列轮转,单条结论的验证时间见上方)
用户评分
还没有人投票,来当第一个
订阅周报,不错过优质插件更新
每周一封 · 高评分插件 + 新用户活动

README

dsh-maestro-supervisor

用于 DSH Web 韧性的 Supervisor —— 阶段 1 守护与报告 + 阶段 3 自动恢复与自动重载。

运行在 pnpm → sh → node 树之外(systemd 守护进程)以在树崩溃时存活,同时作为宿主+客户端 Cordis 插件运行在 dsh web 之内,以自动恢复被中断的会话并在重启后自动重载浏览器。

- 守护进程: 每 3 秒轮询 :3080,保留最后已知良好(LKG)快照(~/.dsh/.supervisor/lkg/、rotate 3、sha256 校验、df >500MB 保护),在崩溃时自动回滚(debounce 60s、flock 锁),写入 report-.md(健康状态 + git diff + 日志尾部),并通过 Telegram 通知(宽松模式,绝不阻塞)。
- 宿主插件: 启动后 8 秒执行 runAutoResume() —— 在 autoResumeWithin(默认 5 分钟)内执行 findInterrupted(尾部 100 行)+ findDanglingOpenTurns(对近期会话进行全量扫描)→ 从 request/context 恢复 agents.resume({resumeSessionId, agentOptions: {provider,model}}) → followup('continue')。为守护进程提供回环 RPC POST /dsh-maestro-supervisor-resume/{scan,resume}(权限 loopback)(resumeViaRpc)。
- 客户端插件: 混合自动重载 —— 在 offline/WebSocket close/visibilitychange 时以 1 秒间隔轮询 fetch HEAD / → 200 → location.reload()。通过 dsh.client 以 window.__ModuleLoader__.load bundle 形式在 /plugins/@ddtcorex/dsh-maestro-supervisor/client.js 提供。

安装

1. 构建

pnpm --dir packages/dsh-maestro-supervisor install
pnpm --dir packages/dsh-maestro-supervisor build   # tsc host + tsc client + node scripts/build-client.mjs → lib/ + lib/client.js
pnpm --dir packages/dsh-maestro-supervisor verify  # tsc --noEmit host + client
pnpm --dir packages/dsh-maestro-supervisor test    # vitest run
test -f packages/dsh-maestro-supervisor/lib/index.js
test -f packages/dsh-maestro-supervisor/lib/client.js

任何 src/ 变更后都需要执行 pnpm build;lib/ 是被 gitignore 的构建产物。客户端需要 tsc 两个步骤以及 build-client.mjs 包装脚本 —— 仅用普通 tsc 会使 lib/client.js 成为裸 ES 模块,dsh web 将失败并报 exports no "./client" bundle。

2. 添加到 DSH Web profile(宿主 + 客户端)

该包声明了 dsh.client(platform: web、inject: ["@deepseek-ai/dsh-client-runtime"]),因此浏览器部分会自动加载 —— 无需额外的 dsh.client 标志。

dsh plugin --profile web add @ddtcorex/dsh-maestro-supervisor
or manually:
edit ~/.dsh/profiles/web/package.json:
"@ddtcorex/dsh-maestro-supervisor": "link:/packages/dsh-maestro-supervisor"
pnpm --dir ~/.dsh/profiles/web install
ls -l ~/.dsh/profiles/web/node_modules/@ddtcorex/dsh-maestro-supervisor  # → .../packages/dsh-maestro-supervisor

预检(必需): 在添加到活动 profile 的 bundles 之前,试启动必须通过:

DSH_HOME=$(mktemp -d) pnpm --dir deepseek-harness dsh web --port 0 &
wait for "dsh web: http://127.0.0.1:" and curl 200, then kill
这会捕获加载时故障(缺少 lib/index.js、构建过期、错误的 cordis.patch.yml)
这些是任何代码内 try/catch 都无法捕获的。参见 dsh-safe-web-update 技能。

正是这一类故障导致了 2026-08-27 的 dsh web 中断(缺少 lib/index.js)。参见 AGENTS.md 约定。

3. Systemd 守护进程(可选,用于检测目录树之外的崩溃)

bash packages/dsh-maestro-supervisor/scripts/install-systemd.sh
systemctl --user daemon-reload
systemctl --user enable --now dsh-web-supervisor
systemctl --user status dsh-web-supervisor
journalctl --user -u dsh-web-supervisor -f

该模板将 Environment=TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID 保留为注释状态——如果你想要 Telegram,请通过 systemctl --user edit dsh-web-supervisor 取消注释,否则它只记录日志。

不使用 systemd 运行(前台,用于调试):

node packages/dsh-maestro-supervisor/lib/index.js daemon   # 每 3 秒轮询一次
node packages/dsh-maestro-supervisor/lib/index.js status
node packages/dsh-maestro-supervisor/lib/index.js logs --tail 50

配置

当以 number 形式给出时,所有 autoResumeWithin 值均以分钟为单位(例如 5 → 5 分钟)。字符串支持 30s/5m/1h。优先级(从高到低):

1. Cordis 配置(cordis.patch.yml 的 config: 或 apply(ctx, config))——每次安装显式指定。
2. 环境变量 DSH_SUPERVISOR_AUTO_RESUME / DSH_SUPERVISOR_RESUME_WITHIN(环境变量中裸写 5 → 出于易用性视为 5m)。
3. Supervisor 配置 ~/.dsh/.supervisor/config.json(autoResumeEnabled、autoResumeWithin)。
4. Maestro 设置 ~/.dsh/maestro/settings.json(domains.supervisor.)。
5. 默认值: true / 5。

| 键 | 类型 | 默认值 | 环境变量 | 文件 | 备注 |
|-----|------|---------|-----|------|-------|
| autoResumeEnabled | boolean | true | DSH_SUPERVISOR_AUTO_RESUME(1/true/yes/on/enabled 对比 0/false/no/…) | config.json: autoResumeEnabled、settings.json: domains.supervisor.autoResumeEnabled | false → 仅通知 |
| autoResumeWithin | number(分钟)或 string(5m) | 5 | DSH_SUPERVISOR_RESUME_WITHIN | config.json: autoResumeWithin、settings.json: domains.supervisor.autoResumeWithin | findInterrupted/findDangling 的时间窗口(mtime + event.time) |

示例 ~/.dsh/.supervisor/config.json:

{
"autoResumeWithin": 5,
"autoResumeEnabled": true
}

CLI

node packages/dsh-maestro-supervisor/lib/index.js --help
node packages/dsh-maestro-supervisor/lib/index.js status
node packages/dsh-maestro-supervisor/lib/index.js daemon   # 轮询 3 秒,防抖 60 秒
node packages/dsh-maestro-supervisor/lib/index.js logs --tail 50
node packages/dsh-maestro-supervisor/lib/index.js rollback --latest

RPC(仅限回环,authority: loopback)

扫描(仅 findInterrupted,tail 100)
curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/scan -X POST \
-H 'content-type: application/json' \
-d '{"type":"client-request","rpcId":"r1","method":"scan","payload":{"withinMs":300000}}'
→ {"type":"server-response","rpcId":"r1","result":{"ok":true,"value":{"scanned":425,"interrupted":[]}}}

Resume (re-attaches agent + followup continue, recovers provider/model)
curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/resume -X POST \
-H 'content-type: application/json' \
-d '{"type":"client-request","rpcId":"r2","method":"resume","payload":{"ids":["--example-project--/session-abc"]}}'
→ {"type":"server-response","rpcId":"r2","result":{"ok":true,"value":{"resumed":["--example-project--/session-abc"]}}}
or {"ok":false,"error":{"code":"bad-request","message":"resume requires at least one session id"}}

守护进程使用 resumeViaRpc()(supervisor.ts:24),它通过 fetch 将相同的信封 POST 到 http://127.0.0.1:3080/dsh-maestro-supervisor-resume/resume,并校验 server-response + rpcId + result.ok。

自动恢复详情

- 时机: apply() 在启动后设置 setTimeout 8000,然后执行 runAutoResume()——仅在全新启动后、dsh web 是唯一所有者时才安全(此时发现的未关闭回合不可能属于仍在运行的生成)。resumeInterrupted 还会检查 agents.get(sessionId),如果已处于活动状态则跳过。
- 内容: findInterrupted(尾部 100 行,查找 time 在窗口内且 reason.kind === 'interrupted' 的 turn/end)+ findDanglingOpenTurns(对近期会话进行全量扫描,查找 time 在窗口内、没有匹配 turn/end 的 turn/start)→ merged = Set([...interrupted, ...dangling]) → 对每个 id 执行 resumeInterrupted。
- 方式: 如果 agents.get(sessionId) 处于活动状态 → followup('continue');否则 sessionPersistence.load(sessionId) → 查找带有 provider/model 的 request/context → agents.resume({resumeSessionId, agentOptions}) → followup('continue')。如果 load 失败,仍会在没有 agentOptions 的情况下恢复(降级)。返回 string[] resumed 并记录 sent continue trigger。
- 子代理: findDangling 对近期会话进行全量日志扫描(mtime 在窗口内,1-2 个文件)——而非尾部——因为子代理 b6487e33 唯一的 turn/start 位于一个 1906 行日志最开始的第 6 个序列,被 tail -100 遗漏。findInterrupted 保持尾部 100(中断关闭器始终位于尾部)。mtime 预过滤使全量扫描保持低成本(此前在没有它的情况下,425 个会话耗时 5.5 秒)。

自动重载详情(混合)

- 客户端(src/client/auto-reload.ts,通过 window.__ModuleLoader__.load 加载 lib/client.js):ctx.effect 挂钩 WebSocket(修补 window.WebSocket 以捕获同源 DSH ws 的 close)、offline/online、visibilitychange → 当断开时 setInterval(fetch HEAD / 1s) → 200 → location.reload()(一次,reloading 守卫)。还会在加载时检查 HEAD /,以防页面在断开期间被打开。
- 宿主(supervisor.ts 的 pollHealth 3 秒 + notify,plugin.ts 的 runAutoResume):健康检查 + 重启 + 通知是宿主这一半;与客户端轮询一起,它们覆盖了手动、supervisor 和 systemd 重启,无需 F5。不需要额外的宿主推送通道——客户端轮询是主要方式,宿主健康检查是次要方式;window.__ModuleLoader__ bundle 通过 ClientModuleRegistry 在 /plugins/@ddtcorex/dsh-maestro-supervisor/client.js 提供(dsh.client + exports["./client"])。

验证

构建与单元测试
pnpm --dir packages/dsh-maestro-supervisor verify   # 宿主 + 客户端
pnpm --dir packages/dsh-maestro-supervisor test     # vitest run
test -f packages/dsh-maestro-supervisor/lib/index.js
test -f packages/dsh-maestro-supervisor/lib/client.js
curl -s http://127.0.0.1:3080/plugins/@ddtcorex/dsh-maestro-supervisor/client.js | grep -c "window.location.reload"  # 2

实时
curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3080/  # 200
curl -s http://127.0.0.1:3080/dsh-maestro-supervisor-resume/scan -X POST -H 'content-type: application/json' -d '{"type":"client-request","rpcId":"t","method":"scan","payload":{"withinMs":300000}}' | head -c 200
node --input-type=module -e "import {findDanglingOpenTurns} from './packages/dsh-maestro-supervisor/lib/resume.js'; console.log(await findDanglingOpenTurns(undefined,{withinMs:5601000}))"
创建一个真实的悬空:pnpm --dir deepseek-harness dsh --profile headless "Run bash synchronously sleep 60" & sleep 4; kill $!; node -e "...findDangling..."  # 应为 1
重启后,它应已 turn/end interrupted → continue → turn2

故障排查

| 症状 | 原因 | 修复 |
|---------|-------|-----|
| Cannot find package '.../dsh-maestro-supervisor/index.js' | 未运行 pnpm build 或 lib/ 已过期 | pnpm --dir packages/dsh-maestro-supervisor build && pnpm --dir ~/.dsh/profiles/web install |
| exports no "./client" bundle / client bundle not found | 缺少 lib/client.js 或 exports["./client"] | pnpm build(运行 tsc + tsc -p tsconfig.client.json + node scripts/build-client.mjs),检查 package.json 的 exports 和 dsh.client,test -f lib/client.js,curl .../client.js |
| dsh web --port 0 上出现 EADDRINUSE ::3000 | 旧的 MainThread 仍占用 :3000+:3080 | ss -tlnp | grep 3080 → pid,kill (同一 pid 占用两者),等待 ss 释放。切勿 pkill -f "dsh web"——它会杀掉测试 shell。 |
| uses .jsonl but backend is zstd | 后端为 zstd 时手写了 session.jsonl | 使用 zstd -c plain.jsonl > session.jsonl.zstd 或 JsonlSessionPersistence API。切勿手写相反的编码——listArtifacts 在启动时检查每个项目目录,一个杂散文件会阻塞整个 dsh web。 |
| first frame is not exactly one header line | zstd 缺少 type: session 头 | 使用 toHeaderLine + compressZstdFrame(header) + compressZstdFrame(body),如 encodeMaterialization 中所示。 |
| findDangling 为 0 但子代理仍处于打开状态 | 尾部窗口太小(在 63b7719 之前) | 已修复:findDangling 现在会全量扫描最近的会话(mtime 在窗口内)。findInterrupted 仍保持尾部 100 行。 |
| resumed: [] 或 RESUME FAILED | agents.resume 失败(无持久化、无 provider/model) | 检查 session.jsonl.zstd 是否存在,且 zstd -d -c ... \| head -n 1 是有效的头部。带有 provider/model 的 request/context 会被恢复——如果缺失,仍会在没有 agentOptions 的情况下恢复。 |
| RESUME SKIPPED | 在 autoResumeWithin 窗口内没有会话 | 将 autoResumeWithin 增大到 10/"10m",检查 config.json 和 DSH_SUPERVISOR_RESUME_WITHIN,用 withinMs: 60601000 验证。 |
| 重启后页面不重新加载 | lib/client.js 未被提供或浏览器缓存 | curl .../client.js | grep -c "window.location.reload" → 2,强制刷新 Ctrl+Shift+R,在控制台检查 window.__ModuleLoader__。客户端在 offline/WS close 时每秒轮询 HEAD /。 |
| 发现 dangling 但 agents.get 显示为存活 | 会话在当前进程中仍存活(不是崩溃) | findDangling 仅在刚启动后、dsh web 是唯一所有者时才安全。如果 agents.get 显示存活,resumeInterrupted 会跳过——这是正确的,等待下次启动。 |

已知问题

- 手动 session.jsonl 与 zstd: ~/.dsh/sessions/ 下有一个编码相反的游离文件,会在每次启动时阻塞整个 workspace 的 listArtifacts(encodingMismatch)。参见故障排除。
- 尾部扫描与全量扫描: 在 63b7719 之前,b6487e33 子代理被漏掉,因为它唯一的 turn/start 位于一个 1906 行日志最开头的第 6 个序列。已修复,但如果你添加新的扫描变体,请复用带 mtime 预过滤的 readSessionAllLines。
- 端口对: 一个 MainThread 占用 :3080+:3000。使用 ss -tlnp + kill  处理那一个 pid,而不是 pkill -f。
- 客户端打包: lib/client.js 必须是通过 scripts/build-client.mjs 生成的 window.__ModuleLoader__.load 包装器,而不是裸 export。将新的客户端文件放在 src/client/ 下,并确保 tsconfig.client.json 包含它们,然后执行 pnpm build。
- 配置优先级: cordis.patch.yml 的 config: > 环境变量(DSH_SUPERVISOR_RESUME_WITHIN 裸 5 → 5m)> config.json > settings.json > 默认值。参见 plugin.ts:71 和 supervisor.ts:82。

开发

pnpm --dir packages/dsh-maestro-supervisor verify
pnpm --dir packages/dsh-maestro-supervisor test
pnpm --dir packages/dsh-maestro-supervisor build

对于守护进程更改:DSH_HOME=$(mktemp -d) pnpm --dir deepseek-harness dsh web --port 0 + 损坏 settings.json → 断言 report + rollback。

Telegram

默认宽松:notifier.ts 先尝试 import('@ddtcorex/dsh-maestro-notifier'),然后尝试 TELEGRAM_BOT_TOKEN/TELEGRAM_CHAT_ID 环境变量,最后 console.log。通过 systemctl --user edit dsh-web-supervisor 启用 → 取消注释 Environment=TELEGRAM_ → daemon-reload + restart。
困难模式(可选):package.json 添加 "@ddtcorex/dsh-maestro-notifier": "workspace:^0.1.0" + pnpm-workspace.yaml packages: ["../dsh-maestro-notifier"] → pnpm install 将其链接。

另请参阅

- 规范:/docs/specs/2026-08-27-dsh-web-resilience-design.md
- 技能:maestro-skills/skills/dsh-safe-web-update/(restart-dsh-web.sh,带有 dry_boot_and_verify() 和 --auto)
- 客户端打包:dsh-maestro-mobile(scripts/build-client.mjs 模式)

上游仓库有新提交时邮件通知你(每天最多一封,无更新不打扰),随时一键退订。

💬 加入 DPharness 群聊

插件用法、部署报错、新插件第一时间同步——群里问,比一个人翻文档快。

点击加入 QQ 群
DPharness 群聊二维码,手机 QQ 扫码进群
扫码进群