Files
dsh_ai1net_server/归档/技能修-删ps1模板-20261006-2355/supervise-persistence.md.旧正文-20261006-2248
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

411 lines
28 KiB
Plaintext
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.
# 常驻程序长期在线(Windows)
> 🔴🔴 **2026-10-06 过期警告 · 读之前必看**
> 本档 §一(包装脚本)、§三(铺脚本步骤)等节描述的 **`start-supervise.ps1` 形态已整套废弃**。
> 现行形态见 `SKILL.md`「两个启动器」表 + `pitfalls.md` **P0-74**:
> **计划任务 → `pythonw.exe` → `supervise-launch.py`**(GUI 子系统 ⇒ 零控制台 ⇒ **不闪窗**)。
> ⛔ 凡本档出现 `start-supervise.ps1`、`assets/start-supervise.ps1.tpl`(**该模板已删除**)、
> 或 `New-ScheduledTaskAction ... -File` 的段落,**一律按历史留痕读,⛔ 不许照着做**。
> ⚠️ 另:本档所说"载体是可选需求、常态用不着计划任务"的**2026-10-04 口径已被推翻** ——
> 2026-10-05 用户拍板「**常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?**」
> ⇒ 计划任务现在是**既定形态**(每 5 分钟兜底),开关是 `supervise.switch`(`collabctl.py on|off`)。
> 📌 **整份重写待办**(本轮只加了本警告块,未重写正文)。
> 📌 **本档=「想让目标检查程序长期在线」时的可选做法**(2026-10-03 夜里整理)。
> 🔴 **注意:常态用不着它** —— 常态是「调用技能完成目标时起后台任务 + 检查程序」,
> 那套一直好好在跑(见下面那段口径)。
> 证据源=测试3 的复盘 `目标-会话协作测试3-7cd276/S3_常驻启动成功复盘_20261003.md`
> + `S4_常驻自愈闭环复盘_20261003.md` + `S5_A6自指修复与目标收口_20261004.md`。
> 🔴🔴 **口径(2026-10-04 用户定案,当场订正)**
>
> 用户原话:「**从来没说过什么开机自启,只有调用技能完成目标时启动 后台任务和检查程序**」。
>
> ⇒ **常态就是:调用技能完成目标时,起后台任务 + 检查程序。** 这套**一直好好在跑**
> (实测本区 pid 19424 从 10-03 17:05 活到现在 **24.7 h**)。
> ⇒ ⛔ **「开机自启 / 计划任务 / 载体脚本」不是需求,是本文件后半段自己推演出来的可选项**。
> ⛔ **不许**把它当"欠项 / 待办 / 风险"往用户面前摊(本轮就是这么摊错的)。
>
> 📌 **本文档怎么读**:
> · 只想**把技能用起来** ⇒ **读到「〇」就够了**,后面的装法与本场景无关。
> · 只在**明确要"目标做完了程序还得继续跑"**时 ⇒ 才看后面那几节。
## 〇、🔴🔴 先分场景:两种"长期"不是一回事
**两种都要长期在线,但它们要的东西不同** —— ⛔ 别混用,混用就会出现
"看起来在跑、其实随时会死"或"该停时还在跑"。
| | **场景 A · 用户手动创建主会话** | **场景 B · 定时任务创建主会话** |
|---|---|---|
| 典型形态 | 用户在 WorkBuddy 里自己开一条 `[主]-…` 会话,盯着它推进 | 排期按点拉起 `[主]-…`(`recurring`,如 `FREQ=HOURLY;INTERVAL=1`) |
| 会话寿命 | **随用户何时收工**(可能几分钟,也可能几小时) | 跑完即 `completed`,**与下一跳之间有静默窗** |
| 🔴 **对"常驻"的硬要求** | ⛔ **不能靠会话活着** —— 一收工就没人拉 ⇒ **必须脱离会话** | ✅ 会话自己周期性回来 ⇒ 但**每跳之间仍有空窗** ⇒ **同样要脱离会话** |
| 唯一合格载体 | **计划任务 + 守护循环**(本文档第三~五节) | 同左(⛔ **排期代替不了载体** —— 排期只是"谁按点喊一次",喊完会话就结束) |
| 怎么验它真活着 | 心跳 `pid` 活 ∧ `ts` 距今 < 90 s + `round` 递增 | 同左,⚠️ **且要静置 ≥ 12 分钟**(⛔ 短观察证明不了事) |
| 谁负责"目标完成后关它" | 守护循环见 `guard.stop` 即 `exit 0`(第五节之二) | 同左 —— ⛔ **排期不会帮你关** |
### 🔴 两个都踩过的坑(**与场景无关**,因为根子在载体)
1. **"一次性工具调用起的子进程"在两种场景下都不合格** ——
实测测试3 开工时 pid 69080 已死;本区两个后台任务(`collabd --supervise` 活 842 分钟、
`board.py --serve` 活 634 分钟)父链都是
`bash → bash → bash → sandbox-cli → WorkBuddy.exe`(**会话树内**),
⛔ `.workbuddy/collab/` 里**既无 `.ps1` 也无 keeper** ⇒ **没走本文档这套**。
⚠️ **它们现在活着,只因为发起它们的那条会话进程树至今没被回收**
⇒ 「**存活时长 = 发起会话的存活时长**」(测试3 `S4` 第六节原话),
**⛔ 不能用它论证"这种起法也能长期"**(该文明确写「这条『反例』**不成立**」)。
2. **"排期是 `once` 且已过期"=哑排期** —— 实测 `[主]-会话协作测试3-主会话` 是
`schedule_type=once`/`scheduled_at=2026-10-03T20:33`/`next_run_at=None`/
`last_run_at=None` ⇒ **它一次都没被触发过**,而界面上 `status=ACTIVE` **看起来像在跑**。
⚠️ **`status=ACTIVE` ≠ "在跑"**;判"真会触发"要看 `schedule_type='recurring'` ∧ `next_run_at` 非空。
## 一、为什么必须换载体(⛔ 别再自己发明)
> 🔴 **「载体」= 那个"负责把常驻程序拉起来、并盯着它别死"的启动脚本。**
> 说白了就一件事:**谁来开机就把它叫起来。**
> 本文档里它指两样:① 批处理 `start-supervise.ps1` ② 把它挂上开机自启的**计划任务**。
> (⚠️ 这个词是内部简写,不在别处用;本节表格用列名「**启动方式**」更直白。)
| 启动方式 | 能不能长期 | 实证 |
|---|---|---|
| 工具调用起的子进程 | ❌ | 父进程一退出就被回收(活不过当轮/当会话) |
| 宿主排期拉的会话起 | ❌ | 测试3 开工时 pid 69080 已死(心跳停在 22:55:31) |
| `CREATE_BREAKAWAY_FROM_JOB` | ❌ | **`PermissionError(13,'拒绝访问。')`** —— 本机被作业对象拒绝,**这条路封死** |
| `cmd /c start /B` | ❌ | 本机沙箱拦截 |
| 排期到点起一次会话 | ❌ | **跑完即退** ⇒ 静默窗内无人(`once` 更糟:可能**永不触发**) |
| **计划任务 + 守护循环** | ✅ | **实测 23:16:37 起活到 23:29 仍在跑**,`round` 1→74;死亡后 **5–6 秒**自动恢复 |
⚠️ **本表判据是"父链根在谁",⛔ 不是 `in_job` 标志**(见下方 2026-10-04 深夜实测更正:
本机**所有**被起的进程 `in_job` 都是 `Y`,含计划任务起的那个 ⇒ 标志位判不出死活)。
**关键差别=父链根在谁**:计划任务创建的进程**根在 `svchost -s Schedule`(任务计划服务)**
⇒ 与 WorkBuddy 会话树**无关** ⇒ 不会被会话回收。
> 🔴🔴 **2026-10-04 深夜实测更正:⛔ 别再用 `IsProcessInJob` 判死活 —— 本机它对所有进程都返回 `Y`。**
>
> 起因:vibe 会话用 `proc_chain.py` 看到常驻 `in_job=True` ⇒ 判"它在会话的作业对象里、所以会被回收"。
> **结论(「会话树内的会死」)是对的,但引用的判据是错的** —— 我做了三组对照:
>
> | 进程 | `in_job` | 实际命运 |
> |---|---|---|
> | 工具调用直接起(`DETACHED_PROCESS`) | **Y** | 会话边界即死 |
> | **计划任务起**(`svchost→cmd→pythonw`) | **Y** | ✅ **长期存活**(看板实测 634+ 分钟) |
> | 零创建 flags(仅 `STARTUPINFO`) | **Y** | 死 |
> | `svchost -s Schedule`(服务本体) | N | — |
> | 系统进程(System / explorer) | N | — |
>
> ⇒ 🔴 **`in_job=Y` 在本机是"全局容器",⛔ 不区分"会话专属"与"服务创建"** ⇒ **标志位判不出死活**。
> ⭐ 这与 `workbuddy-resident-service/scripts/job_lifetime_probe.py` 开头那句自述完全一致:
> 「**让行为说话,而不是只看 `IsProcessInJob` 的标志位**」。
>
> ✅ **正解判据=看父链里有没有 `svchost -s Schedule`**:
> 有 ⇒ 服务创建 ⇒ 长活;根在会话树(`WorkBuddy.exe`/`bash`/已退出的会话进程)⇒ 会话收回时一起死。
> 实测:看板 62992 父链 `pythonw→cmd.exe→svchost(3924)→services.exe→wininit.exe` ⇒ 活;
> 常驻 63628 父链 `pythonw→71800(已退出)→父已退出` ⇒ 会话回收即死。
> 🔴🔴 **2026-10-04 补一条实测更正(⛔ 我自己违反过,判据已落 `t_supervise_lifespan_not_invented`)**
> ⛔ **别把「现在活着」当成"这种起法也能长期"**。实测:pid 19424 从 10-03 17:05 活到 10-04 17:15
> = **24.2 h / 8695 轮**,而它的**父进程早已退出** ⇒ 它是**孤儿进程**,孤儿化后**不受会话结束影响**。
> ⇒ ⚠️ **「会话/工具调用起的活一定活不过当轮」是错的说法**:能不能活取决于**父链还在不在**,
> ⛔ **不许凭这个推断编造后果**。要判"还能活多久"=**先验父链 + 读心跳 `started_ts`**。
> ✅ 但**开机自启这一层仍然缺**:本机 5 个区都**没有** `start-supervise.ps1`,
> 全机只有 1 条计划任务(`collabd-supervise-ws3`)而它指向**不存在的文件** ⇒ 每次触发都失败。
## 二、装法(照抄,⛔ 别自创参数)
### ① 包装脚本 `<工作区>\.workbuddy\collab\start-supervise.ps1`
⚠️ **必须是「永不返回的守护循环」版**。⛔ **别写成"跑一次就退"** —— 那样常驻死掉时脚本
**正常返回 0** ⇒ 任务被判「成功」⇒ `Restart*` 永不触发(测试3 实测:活 12 分钟后再不复活)。
完整可用版见第五节(含 `-Wait`/心跳判据/退避/**停止标志**四项要点)。
```powershell
$ErrorActionPreference = "Continue"
# 🔴 坑①:必须设,否则 _wb_db() 退回读 0 字节空库 ⇒ 闸二永久失效
$env:CODEBUDDY_CONFIG_DIR = "E:\ProgramData\.workbuddy"
$env:COLLABD_CONFIG = "<工作区>\.workbuddy\collab\collabd.config.json" # 🔴 坑②
$env:PYTHONIOENCODING = "utf-8" # 中文日志不乱码
$env:PYTHONUNBUFFERED = "1" # ⛔ 否则重定向到文件时 Python 块缓冲、诊断不可见
$root = "<工作区>"; Set-Location $root
$hbPath = "<工作区>\.workbuddy\collab\logs\supervise-heartbeat.json"
$stopFlag = "<工作区>\tmp\supervise-inbox\guard.stop" # 🔴 目标完成信号(第五节之二)
$logPath = "<工作区>\tmp\keeper.log"
$staleSec = 90 # 与 collabd.supervise_alive() 同口径
function Say([string]$m) { Add-Content -LiteralPath $logPath `
-Value ("[" + (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + "] " + $m) -Encoding UTF8 }
function Get-HbAge { try { $o = (Get-Content -LiteralPath $hbPath -Raw -Encoding UTF8) | ConvertFrom-Json
$ts = [double]$o.ts; if ($ts -le 0) { return -1 }; return [int]((Get-Date).ToString("U") - $ts)
} catch { return -1 } }
$backoff = 5
while ($true) {
if (Test-Path -LiteralPath $stopFlag) { Say "guard.stop present => stands down"; exit 0 } # 🔴
$age = Get-HbAge
if ($age -ge 0 -and $age -lt $staleSec) { Start-Sleep -Seconds 5; continue } # 别人在跑 ⇒ 让位
Say ("spawn collabd --supervise (backoff=" + $backoff + "s)")
# 🔴 -Wait 必须:& 对 pythonw.exe(GUI 子系统)**不阻塞** ⇒ 会叠出多个常驻
Start-Process -FilePath "<pythonw.exe>" `
-ArgumentList @("-u", "<工作区>\.workbuddy\collab\collabd.py", "--supervise") `
-WorkingDirectory $root -WindowStyle Hidden `
-RedirectStandardOutput "<工作区>\.workbuddy\collab\logs\supervise.out.log" `
-RedirectStandardError "<工作区>\.workbuddy\collab\logs\supervise.err.log" `
-PassThru -Wait | Out-Null
Say ("collabd exited (heartbeat age=" + (Get-HbAge) + "s)")
$w = $backoff
while ($w -gt 0) { if (Test-Path -LiteralPath $stopFlag) { Say "stop flag during backoff"; exit 0 }
Start-Sleep -Seconds 5; $w -= 5 }
if ($backoff -lt 60) { $backoff = [Math]::Min(60, $backoff * 2) }
}
```
⚠️ **落盘必须带 BOM**(坑③):`[System.IO.File]::WriteAllText($p,$text,[System.Text.UTF8Encoding]::new($true))`。
### ② 建任务(⚠️ 一律 PowerShell 的 `ScheduledTasks` 模块)
⛔ **`schtasks.exe` 在本机沙箱被黑名单拦截**(Security Center → Command Security)
⇒ **建/查/改/停计划任务全部走 `Get-/New-/Set-/Start-ScheduledTask` + `Out-File` 落盘再读**
(⛔ 别指望命令回显)。
```powershell
$a = New-ScheduledTaskAction -Execute "C:\windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
-Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "<工作区>\.workbuddy\collab\start-supervise.ps1"'
$t = New-ScheduledTaskTrigger -AtLogOn -User "Administrator"
# 🔴 三个设置缺一不可(少一个就前功尽弃):
$s = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) `
-MultipleInstances IgnoreNew -RestartCount 999 -RestartInterval (New-TimeSpan -Minutes 1)
New-ScheduledTask -TaskName "collabd-supervise-<区名>" -Action $a -Trigger $t -Settings $s -Principal $p
# 改动作不影响已配的触发器 ⇒ 更新时只传 -Action
Set-ScheduledTask -TaskName "..." -Action $a
```
| 设置 | 值 | 漏了会怎样 |
|---|---|---|
| `ExecutionTimeLimit` | `0`(无时限) | 默认 72 h ⇒ 到点被杀 |
| `MultipleInstances` | `IgnoreNew` | 重复起 ⇒ 双写台账 |
| `RestartCount`/`RestartInterval` | `999`/`1 min` | 死了不重拉 |
| Trigger | `AtLogOn`(Interactive・最高权限) | 开机不启动 |
⚠️ **动作一律用 `powershell.exe`(不是 `pythonw.exe`)** —— 动作里直接跑解释器没有"设环境变量"这一步。
(用户长期记忆里那条"`python.exe` 会闪黑窗"针对的是**交互式起进程**;计划任务走 `Hidden`+无控制台,不闪。)
## 三、🔴 三个坑(都可复现,逐个都是"秒退"或"看着在其实没跑")
### 坑① 🔴 不设 `CODEBUDDY_CONFIG_DIR` ⇒ 读到 **0 字节空库**
- 计划任务**不继承**会话的环境变量 ⇒ `_wb_db()` 退回 `Path.home()/.workbuddy/workbuddy.db`
(本机那个是 **0 字节、无 `sessions` 表**)。
- 症状:每 20 秒刷一次 `all_sessions_idle 读库失败 no such table: sessions`
⇒ 闸二(所有会话都结束)**永久失效** ⇒ fail-safe 判"有会话在跑" ⇒ **检查会话永远建不出来**。
- ✅ **正解=用机制自带的配置项 `host_db`**(`collabd.py` 里它本就**优先于**环境变量),
指向真库 `E:/ProgramData/.workbuddy/workbuddy.db`。
⇒ **这比设环境变量更可靠**(配置跟着工作区走,不依赖谁起的它)。
### 坑② 🔴 配置查找靠 `cwd` ⇒ 传错 cwd 当场拒跑
`_cfg_candidates()` = `COLLABD_CONFIG` → `<cwd>/.workbuddy/collab/collabd.config.json`。
⛔ **不要把 `cwd` 设成 `collab/` 目录** —— 那会拼出多一级 `.workbuddy` ⇒ 当场拒跑。
✅ 设成**工作区根** + 同时显式给 `COLLABD_CONFIG`(双保险)。
(`ensure_supervise()` 内部原先传 `cwd=脚本所在目录` —— 已由测试3 主会话修成 `cwd=str(WS)`。)
### 坑③ 🔴 PowerShell 5.1 读**无 BOM** 的 UTF-8 `.ps1` ⇒ 中文路径全毁 ⇒ **秒退**
- 症状:计划任务 `LastTaskResult=1` **秒退**,心跳毫无动静。
- 定位:`Get-Content -Encoding Byte -TotalCount 3` ⇒ `36 69 114`(ASCII 的 `$Er`)⇒ **无 BOM**。
PowerShell 5.1 对无 BOM 文件按 **ANSI(GBK)** 解码 ⇒ 脚本里 `会话协作测试3` 变乱码 ⇒ 路径不存在。
- ✅ 落盘**必须带 BOM**:
`[System.IO.File]::WriteAllText($p,$text,[System.Text.UTF8Encoding]::new($true))`(`$true` = 带 BOM)。
- 📌 **本机所有含中文路径的 `.ps1` 一律带 BOM**,否则一律秒退。
- ⚠️ 判据:**`LastTaskResult`** —— `0` = 成功;`1` = 秒退(八成是 BOM)。
## 四、验收(三样都齐才算成,⛔ 打印"已启动"不算)
```powershell
Get-ScheduledTask -TaskName "collabd-supervise-<区名>" | Select-Object TaskName,State
Get-ScheduledTaskInfo -TaskName "collabd-supervise-<区名>" | Select-Object LastRunTime,LastTaskResult
```
🔴 `LastTaskResult` **必须 0**。
**存活唯一机读判据**(=机制里那条,⛔ 别用"日志在更新"代替):
`.workbuddy/collab/logs/supervise-heartbeat.json` 里 **`pid` 活着 ∧ `ts` 距今 < 90 秒**。
```powershell
# pid 是否真在进程表(⛔ tasklist 走 bash 会被当路径;用 PowerShell)
$p = Get-Process -Id <pid> -ErrorAction SilentlyContinue # $null = 已死
```
**静置观察**(≥ 4 分钟):`round` 持续递增、`argv0` 逐字等于本区 `collabd.py`、
`已有活的常驻 ⇒ 本实例退出(幂等)` 出现一次(证明没双写)。
## 五、🔴 载体是**一个**永不返回的守护循环(⛔ 不是"两层")
⚠️ **旧版这里写的是"必须两层=计划任务 + 周期性复活排期",🔴 已被实测推翻** ——
真正常态兜底**就在这个计划的守护循环里**,⛔ 不需要另一条排期。
**实测 2026-10-03**:单纯"计划任务跑一次脚本"**救不了"被外部收走"** ——
测试3 常驻 `23:16:37` 起、`23:28:47` 停(活 12 分钟),而计划任务
`LastRunTime` 仍 `23:17:03`、`LastTaskResult=0`、`State=Ready` ⇒ **它没重拉**。
**根因**:`RestartCount/RestartInterval` **只在本任务自己非零退出时生效**。
而脚本里若是**前台阻塞**调用常驻,常驻死掉 ⇒ 脚本**正常返回 0** ⇒
任务调度器判定「成功」⇒ **`Restart*` 永不触发**(测试3 复盘原话:「自愈机制被**成功退出**这三个字废掉了」)。
⇒ ✅ **正解=守护循环版脚本**(⛔ 别用"跑一次就退"):
```
while ($true) {
if (Test-Path $stopFlag) { Say "…stands down"; exit 0 } # 🔴 见第五节之二
if (心跳新鲜(<90s)) { 让位等待; continue } # 活性判据=心跳,⛔ 不是退出码
Start-Process -Wait -PassThru pythonw collabd.py --supervise # 🔴 -Wait 必须
# 退避 5→10→20→40→60s 封顶;每轮开头都再查一次 $stopFlag
}
```
| 要点 | 为什么 |
|---|---|
| `while ($true)` 包住 | 任务永远 `Running` ⇒ 调度器**不介入**、自己管复活 |
| 🔴 **`Start-Process -Wait`** | ⛔ **`&` 对 `pythonw.exe`(GUI 子系统)不阻塞**、立即返回 ⇒ 循环误判"刚起的已退出"⇒ **叠出多个常驻**(测试3 实测 5/10/20/40s 连续重拉) |
| 活性判据=**心跳新鲜度**(`<90s`),⛔ 不是退出码 | `collabd` 发现已有活常驻会**幂等 exit 0** ⇒ 按退出码重拉会造出风暴 |
| 退避封顶 60s | 真崩时别刷屏 |
| `.NET Process` + `add_OutputDataReceived` | ⚠️ PowerShell 5.1 **无消息泵 ⇒ 回调不触发**、日志被静默吞掉 ⇒ 用 `Start-Process -RedirectStandardOutput` |
**实测效果**:23:40 第一次死亡 **5 秒恢复**;23:47 单变量 kill 演练 **6 秒恢复**、新 pid、`pythonw` 实例数 `count=1`(无叠加)。
### 之二 🔴🔴 守护循环**必须看停止标志**(2026-10-04 补,本文档初版漏了)
**症状**:目标已完成(`--set-life 已完成` ⇒ `supervise_stop()` 写下 `guard.stop`、常驻已优雅退出),
**但守护循环仍在每 60 秒重拉一次、每次秒退** ⇒ 实测**空转 370 次/6 小时**
(`keeper.log` 连续 `exited (exit=0, heartbeat unreadable)`)。
**根因**:守卫只问「心跳新鲜吗」,**⛔ 不问「目标还活着吗」** ⇒
**"完成即收工"只做到"停掉常驻",没做到"别再拉它"** —— 那是同一件事的两半。
✅ **正解=循环开头(+退避等待中)都先查停止标志**,在则**退出循环**:
```powershell
$stopFlag = "<工作区>\tmp\supervise-inbox\guard.stop"
while ($true) {
if (Test-Path -LiteralPath $stopFlag) { Say "guard.stop present => stands down"; exit 0 }
... # 起常驻
$waitLeft = $backoff
while ($waitLeft -gt 0) { # 🔴 退避期间也可能出现标志
if (Test-Path -LiteralPath $stopFlag) { Say "…stands down early"; exit 0 }
Start-Sleep -Seconds 5; $waitLeft -= 5
}
}
```
📌 退出前**必须 `Say` 一行**(⛔ 否则又变成"静默消失",同族红线:读到了就要说)。
**实测(2026-10-04 07:03,单变量:只换脚本、没动任务)**:
新 keeper 起来 **0 秒**识别标志 ⇒ `keeper stands down` ⇒ 任务 `State=Ready`(⛔ 不是 `Running`)
⇒ 重拉次数 **372 → 372**(一没涨)⇒ **空转彻底止住**。
⚠️ 验「是否被回收」必须**只做单一变量**(⛔ 不许同时停/起任务)—— 否则因果会搞错。
⚠️ **验收要静置 ≥ 12 分钟**(⛔ 短于 10 分钟证明不了任何事 —— 常驻就死在第 12 分钟)。
## 六、⚠️ 还没有它就不算长期(两条,别自欺)
1. **静默基准**:检查会话要 `静默 ≥ 20 分钟` 才建,而基准取台账 `tasks.json` 的 mtime
⇒ **新建/改动任何排期都会把计时归零** ⇒ 刚派完棒必然等满 20 分钟(实测 0.2/4.3/…/12.6 分钟,**从未越过 20**)。
2. **闸二会自锁(正确行为,不是 bug)**:`_all_sessions_idle()` 要求"所有会话都结束",
而**发起这一切的那条会话自己也在里面** ⇒ 只要它活着,检查会话就建不出来。
⇒ **要让检查会话跑起来,必须先结束发起会话**(fail-safe 方向=宁可不建,不误建)。
## 七、还有一条(已由机制收口,⛔ 别再自己写)
- **目标完成 ⇒ 关后台+常驻程序**:`collabd.py --set-life 已完成` 现在**真的会停常驻**
(`supervise_stop()`:先写 `guard.stop` 走优雅退出 → 等 45 s → 不死才 `taskkill /T /F`)。
⛔ **受阻 / 已暂停不关**(口径如此)。
- **"完成"可逆**:`ensure_supervise()` 续命前**清掉 `guard.stop`** ⇒ 新目标/目标调整后能再起来。
⛔ 不清就永远起不来。
- **跨区自愈**:本区常驻每 6 轮顺带扫一遍 `peer_workspaces`,**只补"目标进行中"的区**
(已完成/受阻/有停止标志的 ⛔ 一律不拉)⇒ 但**计划任务是更靠前的第一道**。
## 八、按场景对照(选哪条 / 怎么验 / 常见坑)
| 场景 | 主会话怎么来 | 常驻载体 | 目标完成后谁关 | 常见坑 |
|---|---|---|---|---|
| **A · 用户手动创建主会话** | 用户自己开 `[主]-…`,盯着推进 | 计划任务 + 守护循环 | keeper 见 `guard.stop` → `exit 0` | 🔴 用户一收工就没人拉 ⇒ 若没装载体,常驻会在会话结束时**静默死掉**(看板还显示"在线") |
| **B · 定时任务创建主会话** | 排期 `recurring` 到点拉 `[主]-…` | 同上(⛔ **排期代替不了载体**) | 同上 | 🔴 **`once` 排期过期即哑**(`next_run_at=None`、界面仍显示 ACTIVE)⇒ **静默窗内无人**;🔴 排期跑完即 `completed`,**下一跳之前是空窗** |
**共同判据(两种场景都一样,⛔ 每次改完都验)**:
1. `LastTaskResult` = `0`(`1` ⇒ 秒退,八成是 BOM);
2. 任务 `State` = **`Running`**(⛔ `Ready` 只说明"此刻没在跑",**不代表机制坏**,
但要配合 ③ 才算稳);
3. 心跳 `pid` 活 ∧ `ts` 距今 **< 90 s** ∧ `round` 递增;
4. **静置 ≥ 12 分钟**再看 ③(⛔ 短观察证明不了事 —— 常驻常死在第 12 分钟);
5. 目标完成后:keeper 日志出现 `stands down` + **重拉计数不再增长**(验"真的停了",
⛔ 不许只看"心跳停了"—— 那可能是死了而不是收工)。
## 九、四个区各自的任务名(⛔ 名字别撞)
`collabd-supervise-<工作区名>`,例:`collabd-supervise-ws3`。
⚠️ 一个区一个任务;⛔ 多个区**共用一个**任务名会被 `IgnoreNew` 挡掉第二个。
## 十、🔴🔴 「各区都能常驻」怎么一次做齐(2026-10-04 用户点问)
> 用户原话:「看看是否有**各工作区都能启动常驻程序**的最好办法,
> 而不是现在这种**只能某个工作区才能常驻**的办法」
✅ **答案:能,而且就是第八节那套 —— 每区各建一条任务,同一条命令换个区名即可。**
⛔ **不存在"一个载体管所有区"的写法**,⛔ 也**不需要**:任务天然可以并存,
`IgnoreNew` 只在**同一个任务名**上生效(⛔ 撞名才挡)。
### 一区一条,四步(把 `<区>` 换成工作区绝对路径、`<名>` 换成区名)
1. **铺包装脚本** `<区>\.workbuddy\collab\start-supervise.ps1`
—— 用第二节①的守护循环版,**落盘必须带 BOM**(坑③,否则秒退)。
`collabd.config.json` 里补两项:`host_db`(指真库,⛔ 别依赖环境变量,见坑①)
+ 本区 `workspace`。
2. **建任务**:`collabd-supervise-<名>`,动作=`powershell.exe -File <该区 ps1>`,
触发器 `AtLogOn`,四个设置按第二节②的表配齐(`ExecutionTimeLimit=0`/
`MultipleInstances=IgnoreNew`/`RestartCount=999`+`RestartInterval=1min`)。
3. **立即 `Start-ScheduledTask`**(⛔ 别等下次登录)。
4. **验收**:`LastTaskResult=0` ∧ 心跳 `pid` 活 ∧ `ts<90s` ∧ `round` 递增 ⇒ **静置 ≥ 12 分钟**再看一次。
### 各区之间的隔离(为什么"能各建各的")
- **任务名不同** ⇒ `IgnoreNew` 互不影响;
- **`COLLABD_CONFIG` 指向各区自己的配置** ⇒ 各读各的 `goal.json`/台账;
- **跨区自愈是"顺带",⛔ 不是主靠**:本区常驻每 6 轮扫一遍 `peer_workspaces`,
只补"目标进行中"的区(已完成/受阻/有 `guard.stop` 的 ⛔ 一律不拉)。
⚠️ 所以**别把跨区自愈当载体的替代品** —— 它要求本区常驻先活着,是第二道。
### ⛔ 别犯的三个错
1. **⛔ 用一条任务带多个区**(一个动作只能跑一个区的脚本);
2. **⛔ 一区多条任务**(会双写台账);
3. **⛔ 以为"某区现在活着"就不用建任务** —— 那是孤儿化幸存,
父链一断就没了(第一节末尾那条实测更正)。
### 🔴🔴 两个"看着做好了、其实没生效"的坑(2026-10-04 实测踩到)
**坑 A:从副本发起时,找载体模板不能写死包根。**
`_escalate_to_keeper()` 原来写死 `Path(__file__).resolve().parent.parent / "assets"`:
- 技能目录里对(`<pkg>/scripts/collabd.py` ⇒ `.parent.parent` = `<pkg>`);
- **工作区副本里错**(`.workbuddy/collab/collabd.py` ⇒ `.parent.parent` = `<WS>/.workbuddy`
⇒ 去找 `<WS>/.workbuddy/assets/`,**不存在**)。
⇒ 症状**极具误导性**:返回值写着「⛔ 缺模板 assets/start-supervise.ps1.tpl」,
看着像仓库少了个文件,其实只是**算错了包根**;而模板好好躺在
`<WS>/.workbuddy/skills/session-mechanism/assets/`。
✅ 修法=`_find_keeper_tpl()` 认三处(按优先级):
① `<pkg>/assets/` ② `<WS>/.workbuddy/skills/session-mechanism/assets/`(**副本正解**)
③ `<CFGDIR>/skills/session-mechanism/assets/`(兜底)。
⚠️ **凡"从 `__file__` 往上推包根"的代码,副本换布局后都会错** —— 副本是
`<WS>/.workbuddy/collab/`(**不是** 包树里的 `scripts/`),别套用同一套相对层数。
**坑 B:重铺 `.ps1` 后,`Stop/Start-ScheduledTask` 不足以让它生效。**
keeper 是 `powershell.exe -File <ps1>`:PowerShell **启动时把脚本读进内存**,
之后磁盘上改了、任务重启了,**旧 keeper 进程可能仍在跑老脚本**。
实测:同时存在两个 keeper(57680 旧的 + 20180 新的),**旧的按老路径拉常驻**
⇒ 现象是"我明明改好了,起来还是老样子"。
✅ 做法:重铺后**显式杀 keeper 进程**(`taskkill /F /T /PID <keeper_pid>`)再 `Start-ScheduledTask`;
复核判据=**新常驻的父链根**(`svchost -s Schedule`)+ 心跳 `argv0` 指向**本区副本**。
**坑 C:改完副本必须重启常驻,判据是 `argv0` 而不是文件 md5。**
`deploy_code.py` 覆盖的只是**磁盘上的副本** —— 正在跑的进程仍持有旧代码
(P0-17 同族)。✅ 判据=心跳里的 `argv0` 是否指向本区副本路径,
⛔ 不是"`md5` 一致"(那只证明文件换了,⛔ 不证明进程换了)。
---
📌 **本文档的事实全部来自实测**(计划任务状态/`LastTaskResult=0`/心跳 13 分钟/BOM 字节
`239 187 191`/三条封死路的原始错误)。⛔ 任何"应该行得通但没实测"的写法都别往里加。