Files
workbuddy_skills/session-mechanism/references/deploy.md
T
admin d81f75f173 技能包:配置与代码分家 —— 每包自建 env/ 放本机专属配置,scripts/ 不再写死本机路径
用户 2026-10-11 原话:
· 19:22「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」
· 19:28「不用挪出 用env文件夹区分就行,重点是 脚本是脚本 配置是配置」
· 19:29「每个技能 需要环境配置的 都有自己的 env文件夹」
· 19:30「修改完成后要仔细检查 避免调整后 各技能脚本运行失败」

改动:
1、session-mechanism 新增 env/(roots.env=本机真身不入库 + roots.env.example=样例入库),
   install.py 的 ROOTS_ENV 改指 env/roots.env;全包脚本/hook 的配置定位统一为
   「向上 4 层 × env/roots.env 优先 → 包根 roots.env 兼容 → 技能库那份兜底」。
2、去硬编码:collabctl / keepalive / supervise-launch / board-launch 里的本机路径与本机工作区
   清单全清,改为 DSH_AIROOT(兜底 dirname(DSH_WS_ROOT))+ 扫 <AIROOT>/*/.workbuddy/collab/
   自动发现;保活任务名按 collabd-keepalive-<工作区名> 现推;启动器默认值改 ~/.workbuddy 形态。
3、browser-harness:bh_chrome_keepalive.py 默认值全部改 env 驱动,新增 env/bh.env.example。
4、.gitignore:env/ 下只放行 *.example(三行三种层级)+ 兜住旧位置的包根 roots.env。
5、改包纪律.md 新增「四、包结构:脚本是脚本,配置是配置」,立为全仓通用规矩。
6、文档同步:SKILL.md / deploy.md(新增 §3b 配置层级、§6 换机器必查 6 件事)/ rules.md /
   pitfalls.md / supervise-persistence.md。

验收(本机):py_compile 34 个 py 全过;selftest PASS 109 / FAIL 0;
逐文件实证 8 个 loader 命中 env/roots.env;preflight-lock.sh 同目录实测解出两个根;
install.py --manifest 77 份、语法失败 0。

异机验证(包拷成「新克隆」(只有 *.example)+ 另造假工作区 ws-alpha/ws-beta):
install.py --apply 自生成该机 env/roots.env;collabctl/keepalive 自动发现的确实是
ws-alpha/ws-beta、任务名现推(写死清单确已消除);部署副本本地无 roots.env 时靠
CODEBUDDY_CONFIG_DIR 兜底读到技能库那份;整套 selftest 有宿主库时 PASS 108 / FAIL 1
(唯一 FAIL=该区没有常驻心跳,环境所致;本机常驻在跑时同一套为 PASS 109 / FAIL 0)。
2026-10-11 21:49:28 +08:00

255 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 换一台机器怎么用(部署手册)
> ## 🔴 当前结论(**先读这里** · 最后更新 2026-09-30 05:55)
> | 项 | **当前结论** |
> |---|---|
> | **本机起法** | 唯一可行 = **宿主后台任务机制 + `stdout` 全重定向到文件**(完全静默)。⛔ `detached` 活不过工具调用边界;⛔ `schtasks` 被**内置程序黑名单**硬拦。 |
> | **投递** | 🔴 **定案:协作与投递「一直运行」(常驻)** —— 09-29 定案,**2026-10-01 用户再确认**(原话「**协作与投递一直运行(常驻)**」;理由「**可能不是所有队列都是钩子产生的**」)。⚠️ **现状:守护处于停机**(2026-09-29 23:59 止损;其归因已于 09-30 审计修正 —— 真因是**日志撞 ~10 MiB 被丢写**,⛔ 不是"挂在会话名下")⇒ **拉起常驻见 §5**。⛔ 「用自动任务当闹钟」**2026-10-01 已废弃**。 |
> | **前置(覆盖网络节点中继客户端 + 设备接入本地反代)** | **会周期性掉**(实测:设备接入本地反代约 1h 被 `reg.exe` 拦死)⇒ **探测与自起见 §5b**;⚠️ **AI 侧无法自动重起**,只能报警 + 人工/会话拉起。 ⚠️ **两段都在线 ≠ 链路通过**(判据=中继真转发过流 `streams>0`)。 |
> | **三条死路(都实测过,⛔ 别再试)** | `detached` spawn · `schtasks` · 从 bash 调 `powershell.exe` |
> ⚠️ 本文件为**追加式** ⇒ 与本节冲突时**以本节 + `architecture.md` 的「当前结论」节为准**。
> 目标:**整套「多会话协作机制」只在这一个 skill 里**(代码 + 架构 + 规范 + 手册),换机器=拷这个目录 + 改一份配置 + 接一次钩子。
> ⛔ 换机器**不需要**拷贝任何工作区里的脚本 —— 若发现机制代码出现在工作区,那就是旧副本,按 §4 退役。
---
## 0 这个 skill 里有什么(机制本体)
| 文件 | 是什么 |
|---|---|
| `SKILL.md` | 操作入口:怎么派活、怎么收尾、怎么排坑 |
| `references/architecture.md` | 🔴 **唯一架构文档**(五主体/需求台账四态/单条握手/三条件唤醒/红线/判据/术语) |
| `references/deploy.md` | 本文件(换机器) |
| `references/pitfalls.md` | 踩坑清单(每条都真实发生过) |
| `references/taskgraph.md` | 任务图规范 |
| `scripts/collabd.py` | **常驻程序**(`--once` 投影轮)+ **投递**(`--tick` 投递轮)+ 🔴 **常驻形态**(`--supervise`,**定案=一直运行**,见 §5)+ 上报 / 台账 / 看板 |
| `scripts/guard.py` | 🟡 **可选**:守护(只做**发现+落盘**,⛔ 不承担投递)。⛔ 不是投递的前置 |
| `scripts/selftest.py` | **回归自测**(20 条用例 · 不碰生产):**改完必跑,全绿才算改完** |
| `scripts/collabd.config.example.json` | 配置模板(拷成 `collabd.config.json` 改路径) |
**⛔ 不进 skill 的**:运行态产物(台账/看板/通知/日志)—— 它们属于**每个工作区自己**,路径由配置的 `workspace` + `inbox` 决定。
---
## 1 前置(一次性)
| 项 | 要求 |
|---|---|
| Python | 3.11+(实测 3.13);`ast.parse` 可用的标准库即可,**⛔ 无第三方依赖** |
| 宿主 | WorkBuddy 桌面版(需要它的三张只读表 + 自动化排期 + 钩子) |
| 目录 | 把本 skill 放到 `<配置目录>/skills/session-mechanism/`(如 `E:/ProgramData/.workbuddy/skills/`) |
---
## 2 三分钟部署
```
⓪ 声明**任务目标**(🔴 没有目标 ⇒ guard **拒绝启动**,rc=3):
<python> <skill>/scripts/guard.py --goal "<一句话任务目标>"
也支持手写 <workspace>/<inbox>/goal.json(title 必需;可附 acceptance_doc / taskgraph / lines)
① 拷 skill 目录到新机器的 skills/ 下
② 在 scripts/ 下:cp collabd.config.example.json collabd.config.json
改 4 处必填:workspace / inbox / live / taskgraph (其余按需,见 §3)
③ 🔴 **拉起常驻(投递本体)**:`<python> <skill>/scripts/collabd.py --supervise` —— **它同时就是唤醒时钟**。
⚠️ 起法约束见 §5;⛔ 「用自动任务当闹钟」已于 2026-10-01 废弃。
④ 🔴 **接钩子(只作"即时性"补充,⛔ 不是主路径)**
让宿主在**两个**事件上调用一个**本地脚本**(脚本内部再去调 collabd):
事件 A:`UserPromptSubmit` → <python> .../wb-result-hook.py
事件 B:`PreToolUse`(matcher `^Bash$`) → <python> .../wb-result-hook.py
脚本内部做两件事(都**节流**、都**隐窗**):
· `collabd.py --once` ⇒ 常驻程序跑一轮**投影**(⛔ 不投递)
· `collabd.py --tick` ⇒ 投递跑一轮 ⇒ **投递 + 推进队列**
🔴 **它的定位 = 补充**:钩子是**宿主起的子进程** ⇒ 自带网关口令 + ⛔ 不占会话 + 零 token ⇒ 有事件时**立刻**投一版;
但 ⛔ **不能靠它兜底** —— 「需要投递的时刻必然伴随会话在动」**是错假设**(队列可能由**非钩子来源**产生,那时没有事件 ⇒ 漏)。
⚠️ **新加的钩子事件要宿主重启后才生效**(`UserPromptSubmit` 那条**立即生效**);
⚠️ 钩子**只指向 skill/工作区里那一份**脚本,⛔ 不许有第二份(见 §4)。
⑤ 🟡 **守护:可选**(⛔ 不是投递前置;**投递本体= ③ 的常驻**)。
它的职责只剩**发现**:长时间没人动 ⇒ 落 `STALL.md`/`NEED-USER.md`。
· ⛔ 别在会话里起(会被回收);要起 ⇒ 独立窗口或「启动」文件夹 ⇒ 但它**拿不到口令 ⇒ 投递不了**(设计使然)。
```
**验收(逐条可测)**
1. `<python> collabd.py --where` ⇒ 打印出正确的 workspace / inbox / 任务图路径;
2. `<python> collabd.py --once` ⇒ `rc=0`,`inbox/` 下出现看板与机械摘要,**无异常栈**;
3. 造一条上报:`collabd.py --report T1 --state running --line <线> --by <会话名>` ⇒
`collabd.py --reqs` 能看到 `T1 执行中`;
4. `<python> collabd.py --tick` ⇒ `rc=0`,打印一行 `tick: … deliver=…`;
🔴 **真投递的判据 ⛔ 不是 rc=0**:看 `wakeups.jsonl` **有没有新增一行 `ok:true`**;
打印 `no-token` ⇒ 说明这个进程**不在宿主进程树内**(手工跑属正常;由钩子唤起时出现 ⇒ **异常**)。
5. 让一个会话声明角色:`collabd.py --declare --role main` ⇒ 输出 `已声明角色:<sid> = main`;
6. `<python> selftest.py` ⇒ **PASS 39 / FAIL 0**。
7. 🔴 **常驻投递在跑**(定案项,见 §5):进程活着 + `collabd.py --where` 解析到正确 workspace;
判「真投递」⛔ **不看 rc**,看 `wakeups.jsonl` **有没有新增 `ok:true`**。
---
## 3 配置项(`collabd.config.json`)
| 键 | 必填 | 说明 |
|---|---|---|
| `workspace` | ✅ | 你的工作区绝对路径(正斜杠);台账/看板/通知都相对于它 |
| `inbox` | ✅ | 运行态目录(相对 `workspace`),建议 `tmp/supervise-inbox` |
| `live` / `taskgraph` | ✅ | 看板文件 / 任务图 JSON(相对 `workspace`) |
| `lines` | 建议 | 线名 → 中文名 |
| `host_db` | 否 | 宿主库路径;空 ⇒ 用 `CODEBUDDY_CONFIG_DIR` 推导 |
| `shim_port` / `client_entry` / `client_runner` | 否 | 服务自愈用;**不做自愈就留空** |
| `wake_enable` / `wake_min_gap` / `wake_text` | 否 | 唤醒投递(需要网关口令在环境里) |
⚠️ **本机配置(`collabd.config.json`)不要提交到公共仓库** —— 它含本机绝对路径。
---
## 3b 🔴 配置层级:技能包级 `env/` vs 工作区级 `collabd.config.json`(2026-10-11 立)
> 用户令(2026-10-11):19:28「不用挪出 用env文件夹区分就行,重点是 **脚本是脚本 配置是配置**」/
> 19:29「**每个技能 需要环境配置的 都有自己的 env文件夹**」。
**两层,一层一份,各自单一真源**:
| 层 | 文件 | 装什么 | 谁读 |
|---|---|---|---|
| **L0 技能包级**(每台机器一份) | `<技能包>/env/roots.env`(真文件不入库;样例 `*.example` 入库) | 这台机器的根:`DSH_WS_ROOT`、`DSH_AIROOT`、`WB_RESULT_HOOK_WS`、`COLLABD_PROD_CONFIG`、`CODEBUDDY_CONFIG_DIR`、`DSH_DOCS_ROOT` | **包内脚本**:钩子(`scripts/hooks/*`)、`board.py`、`goalctl.py`、`collabctl.py`、`keepalive.py` |
| **L1 工作区级**(每工作区一份) | `<工作区>/.workbuddy/collab/collabd.config.json` | 这个工作区自己的东西:`workspace`/`inbox`/`live`/`taskgraph`/`host_db`(可写本机绝对路径)/`peer_workspaces` | **部署副本**:`<ws>/.workbuddy/collab/` 下的 `collabd.py`、`goalctl.py` |
**桥接(谁把两层接起来)**:`scripts/supervise-launch.py`(常驻)与 `scripts/board-launch.py`(看板)。
计划任务的动作里**没有 env 字段**,所以这两个启动器在**进程内**设
`COLLABD_CONFIG=<本区>/collabd.config.json` + `CODEBUDDY_CONFIG_DIR`,再 `runpy` 起本体。
- **常驻**:启动器装在 `<ws>/.workbuddy/collab/`(由 `deploy_code.py` 分发)⇒ `HERE` 就是本区 ⇒ 配置恒指本区。
- **看板**:启动器在技能包内,主工作区根从 L0 的 `DSH_WS_ROOT` 读;读到就用,读不到就**写日志 + 退出码 2**(⛔ 不猜路径)。
**部署副本怎么拿 L0 的配置**:从 `<ws>/.workbuddy/collab/` 跑**管理入口**(`collabctl.py`/`keepalive.py`)时,
`_sm_load_roots()` 向上找不到 `env/roots.env` ⇒ 回落到**本机技能库**那份
(`<CODEBUDDY_CONFIG_DIR>/skills/session-mechanism/env/roots.env`;`CODEBUDDY_CONFIG_DIR` 由宿主给)
⇒ 全机**只有一份** L0 配置,副本目录里**不放第二份**(避免各区分叉)。
🔴 而 `collabd.py` 的副本**不读 L0 的 `DSH_WS_ROOT`**(那会把别的区认成自己 —— P0-85 静默错归属族):
它靠**启动器设的 `COLLABD_CONFIG`** 定位自己。
**日常该改哪一层**:
- 换机器 / 换工作区根 / 换文档库 ⇒ 改 **L0**(重跑 `install.py --apply`,或手改 `env/roots.env`)
- 换本区配置(inbox 名、端口、`host_db`、peer 列表)⇒ 改 **L1**
- 两层都**不把**「别台机器才成立的值」写进入库代码(依据:用户 19:22「这个环境都是针对一台机器的,同步到别的机器把环境都搞坏了」)
---
## 4 旧副本退役(⛔ 否则"代码在 A、钩子在 B")
症状:改了 skill 里的程序却没生效;或 `advance.md` 等产物由**另一个副本**写出(内容与你预期不同)。
```
① 工作区里若有 collabd.py / advance-watch.py / keepalive.py / thin-consumer.py
⇒ 全是旧副本(职责已被本 skill 吸收)⇒ 改名加 `.retired-<日期>`,⛔ 不删(留回滚)
② 确认钩子指向的是 **skill 里的** collabd.py(`--where` 自证路径)
③ 重启后看 `inbox/` 产物的 mtime 是否随会话事件刷新 ⇒ 是则接线正确
```
🔴 **同名的两份实现是"最难查的故障"**(实测踩过:两份同时跑,同一秒给出互相矛盾的读数)。
---
## 5 投递的边界(🔴 一句话:**投递 = 一个常驻进程**(它同时就是唤醒时钟);钩子只作补充,⛔ 不排期、⛔ 不用自动任务)
| | 有口令(能投递) | 需要容器会话 | 必须活着的进程数 | 跨重启自动生效 | 投递延迟 | 零 token |
|---|---|---|---|---|---|---|
| 🔴 **宿主后台任务(现定案的载体)** | ✅ | ✅ 需 1 个 | ≥1 | ⛔ 需再拉起 | ✅ **可调(轮询间隔)** | ✅ |
| 宿主钩子唤起的一次性进程(**降为补充**) | ✅ | ⛔ 不需要 | **0** | ✅ | ⛔ **依赖事件 ⇒ 会漏** | ✅ |
| 会话外常驻(启动文件夹/独立窗口) | ⛔ **否** | ⛔ 不需要 | ≥1 | ✅ | ✅ 可调 | ✅ |
⇒ 🔴 **载体 = 宿主后台任务**(本机实测:`detached` **活不过工具调用边界**、`schtasks` **被内置程序黑名单硬拦**)
⇒ ✅ **唯一可行 = 宿主后台任务机制 + `stdout` 全重定向到文件(完全静默)**。
⚠️ 「必须活着的进程数 = 0」**⛔ 不得再拿来否掉常驻**(2026-09-30 踩过:丢掉时钟 ⇒ 用户点破"成摆设")。
**三条硬约束**:
1. 🔴 **必须完全静默** —— `stdout` 重定向到文件。⚠️ 真风险是**输出/事件量把会话日志推过 ~10 MiB 上限 ⇒ 宿主 `diagnostic-log:dropped` ⇒ 界面不再显示**(⇒ `pitfalls.md P0-2`);
⛔ **不是"任务挂在会话名下"**(该归因已作废)。
2. ⚠️ **别从会话/工具调用里"直接"起**(`detached spawn`)—— 调用一结束就被回收(实测:唤醒停在调用结束那一秒);
要用**宿主后台任务机制**起,`stdout` 重定向到文件。
3. ⛔ **计划任务(`schtasks`)在部分机器被安全策略硬拦** ⇒ 兜底走「启动」文件夹(⚠️ 但那**拿不到口令 ⇒ 投递不了**,只能做发现)。
⚠️ **口令边界的正确表述**:由会话之外起的常驻**拿不到网关口令** ⇒ 它只**发现 + 落盘**(`STALL.md`/`NEED-USER.md`/看板)——
**这不是故障,是设计**:`collabd.py` 用 `FROM_HOOK` 分辨两种"没口令"(钩子唤起却没口令 ⇒ 异常 ⇒ 落 `NEED-USER.md`;常驻没口令 ⇒ 只记日志)。
---
## 5b 🔴 「覆盖网络节点 · 中继客户端 + 设备接入 · 本地反代」的**探测与自起**(2026-09-30 实测定型 · 任何棒都要会)
> 背景:手机接入链路的**最前一格**是"设备接入本地反代 + 覆盖网络节点中继客户端 都在线"。它不在 ⇒ relay 无通道 ⇒ 入口④闸 `503 device-unreachable` ⇒ 整条链断。
> ⚠️ 这两个进程**不跨 WorkBuddy 重启**,且**本机无法做成计划任务**(`schtasks` 被内置程序黑名单硬拦)。
**① 先探测(⛔ 在跑就别重复起 —— 两份会抢同一 host 注册/同一端口)**
```bash
netstat -ano | grep ":20090" # 设备接入 · 本地反代在听否
tail -1 E:/dsh-worker-dev/logs/overlay-bg-*.out.log # 最近一行 state=up(for …) ⇒ 覆盖网络节点中继客户端在跑
curl -s --noproxy '*' -o /dev/null -w "%{http_code}\n" http://127.0.0.1:20090/ # 期望 200
```
**② 不在才起 —— 🔴 必须"宿主后台任务机制 + stdout 全重定向到文件"(完全静默)**
| 件 | 命令 |
|---|---|
| **覆盖网络节点 · 中继客户端** | `node D:/github/dsh_shenxian/lib/net/relay/main.js --client --url wss://ai1net.com/dshs-relay --host <hostId> --network <network> --keys-file E:/dsh-worker-dev/overlay/relay-keys.local.json --ports 20090`(env:`DSHS_OVERLAY_NODE_KEY_FILE` / `DSHS_OVERLAY_NODE_GRANT_FILE`;`<hostId>/<network>` 从 `E:/dsh-worker-dev/overlay/overlay-node.local.json` 读) |
| **设备接入 · 本地反代** | `node E:/github/dsh-desktop-0.1.7rc2/node_modules/tsx/dist/cli.mjs E:/ProgramData/AIProject/ai1net-dsh-desktop/.workbuddy/_devkit/launch-desktop-dev-017.mts`(cwd=`E:/github/dsh-desktop-0.1.7rc2`;⚠️ **必须先 `unset ELECTRON_RUN_AS_NODE`**,否则 Electron 退化成纯 Node) |
**成功判据(两条都要)**:覆盖网络节点中继客户端 ⇒ 日志出现**本轮新增**的 `registered host=… accepted=[20090]` 且 `state=up`;设备接入本地反代 ⇒ `20090 LISTENING` + `curl` **200**。
**⛔ 三条死路,别再试**(都实测过)
1. **`detached` spawn** —— 活不过工具调用边界(日志 0 字节即死);`wb-overlay-node-launch.mjs --detached` 注释里"本机实测不可用"是真的。
2. **`schtasks`** —— 内置程序黑名单硬拦(见 `pitfalls` 里那六项),⛔ 命令内不可放行。
3. **从 bash 调 `powershell.exe`** —— 被策略拦("绕过 PowerShell 安全检查");而 `overlay-node-daemon.ps1 -Action run` 是 **powershell 前台长跑**,也活不下来。
**⚠️ 遗留单点(如实登记)**:这两条后台任务**挂在"起它的那个会话"名下** ⇒ **起它的会话被回收 / WorkBuddy 退出 ⇒ 链路断**。
⇒ 起它们的会话**在那段时间内不要关**;断了就按上面"② 不在才起"重起一次。
## 6 换机器后必查的 6 件事
1. `--where` 的三个路径是否都对;
2. 钩子接线是否指向 skill(§4);
3. 宿主库能否只读打开(`sessions` / `automation_runs` / `automations` 三张表读得到);
4. 有没有**别的实现**在抢同一个 inbox(§4);
5. `guard.py` 是否真的起来了(`--status`)+ 它是否会随开机自启;
6. 🔴 **计划任务里有没有指向不存在路径的项**(2026-10-11 加)。判据:逐条取出动作里的脚本路径
`Test-Path` 一遍;指向已删目录的(例:老工作区改名、`tmp/selftest/…` 这类一次性沙箱)=**静默死任务**。
处置:`Disable-ScheduledTask`(`collabctl.py` 的旧任务清单已含本机这几条)——
**禁用不删**,可逆。
```
Get-ScheduledTask | ForEach-Object { $t=$_; $t.Actions | ForEach-Object { "$($t.TaskName)|$($t.State)|$($_.Execute)|$($_.Arguments)" } }
```
⚠️ 判存在性时先**展开 `%windir%`/`%SystemRoot%`**,否则系统自带任务会被误报成缺失。
---
## 5d 🔴 「会话整体停止」后怎么恢复(2026-09-30 立 · 用户追问逼出来的)
> 用户原话:「所以现在的问题就是 **会话整体停止了怎么办** 的问题」。
**先分清三档 —— 只有第三档才需要动手:**
| 档 | 场景 | 会怎样 | 怎么办 |
|---|---|---|---|
| ① | **只有某个会话停了**(WorkBuddy 还开着) | 机制照跑 —— 钩子是**全局**注册的,**任何**会话跑 Bash/发消息都会唤起它 | **不用办** |
| ② | **所有会话都 idle**(WorkBuddy 还开着) | 钩子不被唤起 ⇒ 不投递 | **不用办** —— 那时没人在看;一有动作,积压立刻补投 |
| ③ | **WorkBuddy 退出 / 机器休眠** | **整条协作链停摆** —— 唯一能"开新会话"的通道就是宿主自动化 | **按下面清单恢复** |
### 恢复清单(重开 WorkBuddy 后按序跑)
1. **看前置两段**:`netstat -ano | grep ":20090"`(判据**只认 LISTENING 行**,⛔ 别用 curl/connect —— 本机 Proxifier 会代理回环,返回是假的)。
缺了 ⇒ 按 **§5b** 拉。⚠️ **中继客户端那半边已计划任务化**,通常自己会回来。
2. **查过期未跑的一次性排期**(`status='ACTIVE' and schedule_type='once'` 且 `next_run_at < 现在`):
有 ⇒ **新建一条**(⛔ **改时间不会触发**)。
3. **跑一轮投影** `collabd.py --once`,让队列/看板/`NEXT.md` 刷新。
4. **看 `NEED-USER.md`**:有没有等你拍板的事。
5. **看 `blocked.json`**:受阻件是否已随状态更新解除(⛔ 别让它一直当队首、每次唤醒白跑)。
### 🔴 为什么"不需要防"(实测结论,⛔ 别再想做常驻去扛)
- **实测(2026-09-30 08:2x)**:11 条一次性排期里 **9 条准点跑**;唯一 0 次那条的排期在**创建时就已过期** ——
⇒ **宿主在跑,排期就准点**;**宿主不在,排期不会自己跑**(**调度器就是宿主本身**)。
- **能扛过 WorkBuddy 退出的**:只有**不需要口令**的长跑 —— 实测就是**覆盖网络节点中继客户端**(已由计划任务持有,
父链 `powershell ← svchost ← services ← wininit`,⛔ 无 bash/无 WorkBuddy)。
- **扛不过的**:任何**要投递**的东西 —— 投递要**网关口令**,而**口令的唯一合法来源是 WorkBuddy 进程 env**
(08:06:31 实测:计划任务上下文 `envPresent=false / envLen=0`)⇒ **「不占会话」与「有口令」二选一**。
- ⇒ **结论**:**「会话整体停止」不需要"防",只需要"恢复流程"**。
⛔ 不要再设计"常驻监督进程"去扛 —— 那条路已被实测堵死(要么没口令、要么拖住会话)。