Files
workbuddy_skills/session-mechanism/references/supervise-persistence.md
T
admin 19101acd65 init: workbuddy_skills 重建,仅收录 session-mechanism
- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件)
- 附 .gitignore(产物 + 本机凭据)
- 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库)
- 本提交为孤儿提交(父提交为空),历史自此重新开始
2026-10-05 14:13:24 +08:00

27 KiB
Raw Blame History

常驻程序长期在线(Windows)

📌 本档=「想让目标检查程序长期在线」时的可选做法(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/心跳判据/退避/停止标志四项要点)。

$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 落盘再读 (⛔ 别指望命令回显)。

$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)。

四、验收(三样都齐才算成,⛔ 打印"已启动"不算)

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 秒。

# 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))。

根因:守卫只问「心跳新鲜吗」,⛔ 不问「目标还活着吗」 ⇒ "完成即收工"只做到"停掉常驻",没做到"别再拉它" —— 那是同一件事的两半。

✅ 正解=循环开头(+退避等待中)都先查停止标志,在则退出循环:

$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/三条封死路的原始错误)。⛔ 任何"应该行得通但没实测"的写法都别往里加。