起因(2026-10-06 用户问):能自决的问题为什么还要问。
复盘发现注入的判据里**缺动作**:
- 缺「十步」的两个停止点 → ① 判类型(方案请求 vs 直接执行)② 判方向(扩大/收窄/中性);
- 缺「自主裁决顺序」的排序轴 → 更小改动 → 更少新增形态 → **不扩大可见面**。
⇒ 前者导致「会扩大可见面」时不停手;后者导致排完仍把已定项混进待拍板清单(=变相征询)。
本次只补这两条动作骨架(1459 → 1906 字符),⛔ 不塞十步全文(避免稀释)。
411 lines
28 KiB
Markdown
411 lines
28 KiB
Markdown
# 常驻程序长期在线(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`/三条封死路的原始错误)。⛔ 任何"应该行得通但没实测"的写法都别往里加。
|