1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
3.7 KiB
3.7 KiB
30 · 编排器孤儿实例清理(单实例保证从内存升到 OS 层)
- 日期:2026-09-11
- 触发:admin 对话窗口报「模型连接异常」
- 状态:✅ 已实施并 live 验证(2026-09-11)
一、现象与根因
现象:admin 的 dsh 对话窗口「模型连接异常」。排查发现 admin 有 2 个实例同时运行:
dsh-114801-bf8afe93(port 35685)= 编排器认的 main;dsh-114801-f7ea9f97(port 36449)= 孤儿残留(编排器不认识)。
两实例共享同一 profile(DSH_HOME)→ session/settings 写冲突 → 模型配置读取异常。(KEY 注入、网络均正常。)
根因(已读源码定位):编排器的「单实例保证」只在内存层:
| 位置 | 事实 |
|---|---|
launch() L123 |
if (this.mains.has(userId)) throw AlreadyRunningError —— 只查内存 map |
constructor() L60-72 |
只建 portGuard + reap timer,不扫描 OS 层已有 scope |
teardown() L230 |
只 stop map 内实例,不清理未纳管的 scope |
| 全文 | 无任何 systemctl list-units 扫描(对 OS 层不感知) |
根因链:
- 实例真实生命周期在 OS 层(systemd scope),但编排器只靠内存 map 追踪;
- portal 重启 → map 清空,而 systemd scope 不随 portal 死(继续运行)→ 旧实例变孤儿;
- 重启后再
enter/launch时不检查 OS 层 → 直接 spawn 新实例 → 新旧并存; - 两实例共享 profile → 写冲突 → 模型连接异常。
触发条件:portal 每次重启都可能留孤儿(本次由白名单部署重启 + 此前熔断测试/A1 重启叠加命中)。
二、方案(治本:单实例保证升到 OS 层)
改动文件:src/supervisor/orchestrator.ts
- spawn 前清孤儿(核心):
spawnInstance前,用resolveUid(userId)拼 scope 前缀dsh-<uid>-,systemctl list-units --type=scope扫描 → 对该 uid 名下、且不等于当前 map 实例 unit 的 scopesystemctl stop。 - portal 启动时清一次(防重启留孤儿):
LocalSpawner启动(构造后)扫描所有dsh-<数字>-*.scope并清理(编排器重启后无法接管它们,安全清)。 - kill 后校验(可选):
killInstance后确认 scope 已消失,否则再 stop 一次。
安全边界:
- 只清理
dsh-<uid>-*(严格前缀),不误伤 portal 自身(dshs.service名字不同)与其它服务; - 清理是"停止后交给 systemd 回收",不改任何数据。
三、验证结果(live,2026-09-11)
- 场景 A(启动时清):造假孤儿
dsh-114801-deadbeef.scope→ restart portal → scope 数 0(cleanAllStaleScopes 生效)→ enter 后只剩 1 个 ✅; - 场景 B(spawn 前清):造孤儿
deadbeef→ 停真实 main 触发退避重启 → spawn 前清理生效,deadbeef 残留 0、只剩 1 个新实例 ✅; npm run build通过。
三之二、原验证计划(留档)
- 单测:模拟
systemctl list-units输出含孤儿 → 断言stop被调用且前缀匹配; - live:手工造孤儿(
systemd-run --scope起一个假的)→ 触发launch→ 断言孤儿被清、只剩 1 个实例; - 回归:portal 重启后
enter,断言实例数 = 1(此前会 2)。
四、红线遵守
- 不碰官方 dsh 主程序(R2 ✅);2. 不升级 dsh(R1 ✅);3. 只改本地编排器(
dshs)。
五、风险
systemctl list-units在每次 spawn 前调用 → 轻微开销(~几十 ms),可接受;- 若 uid 解析失败(用户无 uid)→ 跳过清理(不误伤),仅记日志。