Files
workbuddy_skills/session-mechanism/references/supervise-persistence.md
T
admin e5dfb6e4ef 发布:常驻文档按现行形态重写 + 作业规矩/01 先并后删
一、常驻机制文档 references/supervise-persistence.md(整份重写 410→469 行)
- 照抄段由废弃的 start-supervise.ps1 形态,改为现行形态:
  计划任务 collabd-keepalive-<区>(每 5 分钟)→ pythonw.exe → supervise-launch.py → collabd.py --supervise
- 旧 ps1 形态整体降级为「§四 旧形态留痕」(逐条标 🟠,内容一字未删,供读老日志/老进程用)
- 判据订正:LastTaskResult 在长驻运行中是 267009(旧口径「必须 0」属"跑一次就退"时代,已作废);
  BOM 那条随 ps1 形态一并降为历史;另两条仍有效(CODEBUDDY_CONFIG_DIR 空库 / WorkingDirectory 必须是工作区根)
- 删除 assets/start-supervise.ps1.tpl —— SKILL.md、manifest.md、collabd.py、selftest.py 四处早已按「该模板已删除」口径,本次把磁盘与文档对齐

二、references/作业规矩/01 先并后删(用户选定 A 方案:不丢内容 + 只剩一处)
- 判据类并入 references/02-功能优先协作协议.md:
  §3.2 补「规则冲突裁决顺序」「冲突 ≠ 门禁」;§3.3 补回完整语言转换表(原处"已收敛到通用技能"在并包后成了悬空指针);
  §5.3 铁律由五条补到六条 + 接上配套硬红线「只做正向迭代」;新增 §5.5「提报前必答三问 + 回话前自检」
- 排版类并入 references/03-回复排版-核心块.md 的「完整版」段(十四条反模式 / 骨架路由表 / 文案点名主体 / 文档直入主题)
  该段放在 REPLY-CORE 标记之外 ⇒ 每轮注入块逐字未变(实测仍 1172 字符)
- 删除 references/作业规矩/01-协作与提报用户判据.md,并跟改 6 处引用(SKILL.md 第一屏表、作业规矩/00 索引表与两处指针、
  作业规矩/03-多棒接力编排、00 的 frontmatter 变更记录)——含一处二级指针「见 §6 第 0 条」,已改指 03 完整版段

三、判据与卫生
- pitfalls.md:P0-74 去冗余(7107 → 5982 B,重复叙述合并),「沉淀纪律」由 FAIL 1 转绿
- install.py:manifest_files() 补排 logs/ —— 与它自己「运行日志不入表」的口径一致,否则跑一次钩子表就过期
- .gitignore:补 logs/;撤跟踪 logs/_dr_probe.json(钩子输出转储,全包零引用)
- references/manifest.md 用生成器重算:66 份文件,语法失败 0

验收读数:selftest.py rc=0 PASS 99 / FAIL 0(另 1 条报告型不计入);
session-rules-check.py fail 0 / warn 1("此刻无活会话",正常态)/ ok 13;改动文件行尾全 LF。
2026-10-07 00:32:47 +08:00

34 KiB
Raw Blame History

常驻程序长期在线(Windows)

✅ 2026-10-06 已按收口后的形态重写完毕(本档头部原先挂的「📌 整份重写待办」已完成,过期警告块已撤)。

🔴🔴 现行形态(照这个做,⛔ 别自己发明): 计划任务 collabd-keepalive-<区>(每 5 分钟触发)→ pythonw.exe → supervise-launch.py → collabd.py --supervise

⛔ 现行形态里没有 .ps1,也没有"守护循环脚本" —— 复活由「计划任务每 5 分钟重触发」 +「常驻本体幂等(发现已有活常驻就 exit 0)」两件一起承担。 📌 权威同源:SKILL.md「常驻机制的真实形态」+ pitfalls.md P0-73(启动器为什么必须有) /P0-74("两条起法"收口)。 📌 旧 .ps1 形态(2026-10-03 ~ 10-05)的完整留痕在第四、五节,⛔ 只当历史读,别照着做。

📌 本文档怎么读: · 只想把技能用起来 ⇒ 读到「〇」就够了(常态用不着计划任务)。 · 明确要"目标做完了程序还得继续跑 / 崩了自己爬起来" ⇒ 从「一」往下照着做。 · 只想查历史("以前那套 ps1 是怎么回事")⇒ 直接跳第四节。

📌 本档来源(2026-10-03 夜里整理,2026-10-06 重写)。 证据源=测试3 的复盘 目标-会话协作测试3-7cd276/S3_常驻启动成功复盘_20261003.md + S4_常驻自愈闭环复盘_20261003.md + S5_A6自指修复与目标收口_20261004.md + 2026-10-05/06 的 P0-72/P0-73/P0-74 实测。

〇、🔴🔴 先分场景:两种"长期"不是一回事

两种都要长期在线,但它们要的东西不同 —— ⛔ 别混用,混用就会出现 "看起来在跑、其实随时会死"或"该停时还在跑"。

场景 A · 用户手动创建主会话 场景 B · 定时任务创建主会话
典型形态 用户在 WorkBuddy 里自己开一条 [主]-… 会话,盯着它推进 排期按点拉起 [主]-…(recurring,如 FREQ=HOURLY;INTERVAL=1)
会话寿命 随用户何时收工(可能几分钟,也可能几小时) 跑完即 completed,与下一跳之间有静默窗
🔴 对"常驻"的硬要求 ⛔ 不能靠会话活着 —— 一收工就没人拉 ⇒ 必须脱离会话 ✅ 会话自己周期性回来 ⇒ 但每跳之间仍有空窗 ⇒ 同样要脱离会话
唯一合格载体 计划任务(第一~三节) 同左(⛔ 排期代替不了载体 —— 排期只是"谁按点喊一次",喊完会话就结束)
怎么验它真活着 心跳 pid 活 ∧ ts 距今 < 90 s + round 递增 同左,⚠️ 且要静置 ≥ 12 分钟(⛔ 短观察证明不了事)
谁负责"目标完成后关它" 常驻主循环见 guard.stop 即收工(第七节) 同左 —— ⛔ 排期不会帮你关

🔴 常态口径(2026-10-04 用户定案,当场订正)

用户原话:「从来没说过什么开机自启,只有调用技能完成目标时启动 后台任务和检查程序」。

⇒ 常态是:调用技能完成目标时,起后台任务 + 检查程序。 这套一直好好在跑。 ⇒ ⛔ 不许把"没装计划任务"当"欠项 / 待办 / 风险"往用户面前摊(2026-10-03 夜里就是这么摊错的)。

🟠 本档 2026-10-04 曾据此写过一句「计划任务 / 载体脚本不是需求,是本文档自己推演出来的可选项」 —— 这句已被 2026-10-05 用户拍板推翻。 用户原话:「常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?」 ⇒ 计划任务现在是既定形态(每 5 分钟兜底),开关是 supervise.switch(collabctl.py on|off)。 ⚠️ 两句话不矛盾:日常不必操心(机制自己装),但它是既定形态,⛔ 不是"可有可无的选项"。

🔴 两个都踩过的坑(与场景无关,因为根子在载体)

  1. "一次性工具调用起的子进程"在两种场景下都不合格 —— 实测测试3 开工时 pid 69080 已死;本区两个后台任务(collabd --supervise 活 842 分钟、 board.py --serve 活 634 分钟)父链都是 bash → bash → bash → sandbox-cli → WorkBuddy.exe(会话树内), ⛔ .workbuddy/collab/ 里既无载体脚本也无 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 非空。

一、🔴 现行载体:pythonw.exe + 启动器(⛔ 别再自己发明)

🔴 「载体」= 那个"负责把常驻程序拉起来、并盯着它别死"的东西。 说白了就一件事:谁来把它叫起来。 现行答案=计划任务;它每 5 分钟触发一次,动作是 pythonw.exe "<区>/.workbuddy/collab/supervise-launch.py"。

1.1 为什么"计划任务"是唯一合格答案(判据=父链根在谁)

启动方式 能不能长期 实证
工具调用起的子进程 ❌ 父进程一退出就被回收(活不过当轮/当会话)
宿主排期拉的会话起 ❌ 测试3 开工时 pid 69080 已死(心跳停在 22:55:31)
CREATE_BREAKAWAY_FROM_JOB ❌ PermissionError(13,'拒绝访问。') —— 本机被作业对象拒绝,这条路封死
cmd /c start /B ❌ 本机沙箱拦截
排期到点起一次会话 ❌ 跑完即退 ⇒ 静默窗内无人(once 更糟:可能永不触发)
计划任务 + 启动器 ✅ 进程根在 svchost -s Schedule(任务计划服务)⇒ 与会话树无关;实测 23:16:37 起活到 23:29 仍在跑,round 1→74;死亡后 5–6 秒自动恢复

⚠️ 本表判据是"父链根在谁",⛔ 不是 in_job 标志:本机所有被起的进程 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。

1.2 🔴🔴 为什么必须夹一个"启动器"(2026-10-05 血的教训 · P0-73)

计划任务的"动作"里没有 env 字段 ⇒ 一切环境变量只能由启动器在进程内设。 ⇒ 动作**⛔ 不许直起 collabd.py,必须经 <区>/.workbuddy/collab/supervise-launch.py。 直起的后果(实测踩到):任务环境没有 CODEBUDDY_CONFIG_DIR** ⇒ _wb_db() 落到 ~/.workbuddy/workbuddy.db(0 字节空库)⇒ _all_sessions_idle() 每轮报 no such table: sessions ⇒ 闸② 恒判"有会话在跑" ⇒ 检查会话再也不建(实测日志 36 次读库失败)。 ⚠️ 为什么有的区没炸:那区 collabd.config.json 里 host_db 写死了绝对路径(兜住了)。

两个启动器对称存在,缺一个就静默半瘫(scripts/ 下,由 deploy_code.py 分发到各区):

启动器 服务的程序 进程内必须设 ⛔ 缺了会怎样
supervise-launch.py 协作程序(collabd.py --supervise) CODEBUDDY_CONFIG_DIR + COLLABD_CONFIG + PYTHONIOENCODING + PYTHONUNBUFFERED + 兜 stdout/stderr 检查程序静默失效(读 0 字节空库 ⇒ no such table: sessions ⇒ fail-safe 恒判"有会话"⇒ 再也不建检查会话)
board-launch.py 看板(board.py --serve 20099) COLLABD_CONFIG + PYTHONIOENCODING + 兜 stdout/stderr 看板崩溃重启循环(已拒跑)、20099 从没绑上

supervise-launch.py 的四条实现要点(见该文件头注,⛔ 别自己另写一份):

  1. 它所在目录即本区 .workbuddy/collab —— collabd.py/collabd.config.json 都在同目录,用相对自身定位(⛔ 别写死 E:\);
  2. CODEBUDDY_CONFIG_DIR 用 setdefault(宿主 env 优先,兜底才写死);COLLABD_CONFIG 必须指向本区那份配置;
  3. pythonw.exe 下 sys.stdout/stderr 可能是 None ⇒ 任何 print 都会 AttributeError 秒退 ⇒ 启动器自己兜到 logs/_supervise-launch.log;
  4. 用 runpy.run_path(CB, run_name="__main__")(⛔ 不用 exec)—— runpy 会把 __name__/__file__/sys.path[0] 按"真在跑那个脚本"设好; 且 sys.argv 由启动器自己补 --supervise ⇒ 任务参数里 ⛔ 不要再写。

🔴 重构"起法"时,旧起法里的 env 必须逐条搬(P0-73 就是改成两层形态时丢了 CODEBUDDY_CONFIG_DIR 这一句)。 🔴 启动器里写死最稳(⛔ 别指望 roots.env 兜住各区 —— 它只在技能目录,而各区副本在 <ws>/.workbuddy/collab/,_sm_load_roots() 向上 4 层找不到)。 ⚠️ 变量名坑:roots.env 给的是 COLLABD_PROD_CONFIG,而 board.py/collabd.py 读的是 COLLABD_CONFIG ⇒ 名字对不上 ⇒ roots.env 兜不住。

二、装法(⛔ 别自己写 Register-ScheduledTask)

2.1 🔴 建任务只许有一处实现

处 谁 说明
✅ 唯一正门 scripts/collabctl.py::ensure_keepalive_tasks() 受 supervise.switch 总电闸管辖;on / ensure 都走它
✅ 自愈旁路(同形态、同任务名) scripts/collabd.py::_escalate_to_keeper() 常驻发现本区没任务时自己补一条
⛔ 已删 scripts/init_workspace.py::_register_keeper_task() 2026-10-06 整份去掉(含 --with-keeper/--no-task 开关与 _ps_check())—— 建任务只许有一处实现,自己建就绕过了总电闸

⚠️ 两处必须同形态同名 —— 否则自愈起的常驻会被下一次 off 当"旧形态"顺手干掉(2026-10-06 实测踩到)。

2.2 🔴 推荐做法:一句话让机制自己装

# 开关/盘点一律走这个入口(pythonw 没有 stdout ⇒ 必须 --out 落盘再读)
& "<pythonw.exe>" "<技能目录>\scripts\collabctl.py" on --out "E:\tmp\collabctl-on.txt"
  • on ⇒ 建齐保活任务 + 立刻触发 + 开关 on;
  • off ⇒ 一键全关(开关 off + 禁用任务 + 杀进程,幂等);
  • ensure ⇒ 幂等收敛:on 就补齐缺失的、off 就全停;
  • 用户可见入口(双击):会话机制-一键开关.bat(工作区根 + 桌面各一份)。

⚠️ 本机一律 pythonw.exe 跑(python.exe 是控制台程序 ⇒ 闪黑窗,用户 2026-10-01 投诉过); 而 pythonw 没有 stdout ⇒ 不落盘就完全看不到结果(判据不可信=没章法)。

2.3 动作/触发器/设置(照抄 collabctl.py::ensure_keepalive_tasks())

$a=New-ScheduledTaskAction -Execute '<pythonw.exe>' -Argument '"<区>/.workbuddy/collab/supervise-launch.py"' -WorkingDirectory '<区>'
$t=New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Minutes 5)
$s=New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
     -StartWhenAvailable -MultipleInstances IgnoreNew `
     -ExecutionTimeLimit (New-TimeSpan -Seconds 0) -Hidden
$pr=New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Highest
Register-ScheduledTask -TaskName 'collabd-keepalive-<区名>' -Action $a -Trigger $t -Settings $s -Principal $pr -Force
设置 值 漏了/写错会怎样
-Execute pythonw.exe ⛔ powershell.exe/cmd.exe 是控制台程序 ⇒ 每次触发分配 conhost.exe ⇒ 闪黑窗(10-05 事故真因);⛔ python.exe 同病
-Argument 启动器路径 ⛔ 不许直起 collabd.py(见 1.2);⚠️ 参数里 ⛔ 别再写 --supervise(启动器自己补)
-WorkingDirectory 🔴 工作区根(反斜杠形态) ⛔ 写成脚本所在目录 ⇒ load_cfg() 去找 <ws>/.workbuddy/collab/.workbuddy/collab/… ⇒ 找不到 ⇒ CFG_MISSING ⇒ --supervise 拒跑(实测 rc=2、任务侧 LastTaskResult=1)
-RepetitionInterval 5 分钟 这就是"复活"的全部来源;⛔ 别改成每分钟
-ExecutionTimeLimit 0(无时限) --supervise 是长驻进程,默认 72 h ⇒ 到点被掐死;设 2 分钟 ⇒ 每 2 分钟重建一次=另一种抖动
-MultipleInstances IgnoreNew 重复起 ⇒ 双写台账
-Hidden + Interactive + Highest — 缺 ⇒ 开机/登录后不启动;权限不足
🟠 -RestartCount 999/-RestartInterval 1min 已去掉 旧写法。每 5 分钟重复触发本身就提供重拉;每分钟重启是多余的抖动(P0-74 记过"反复"的成因)

2.4 ⚠️ 本机两条操作纪律

  1. ⛔ schtasks.exe 在本机沙箱被黑名单拦截(Security Center → Command Security) ⇒ 建/查/改/停计划任务全部走 PowerShell 的 ScheduledTasks 模块 (Get-/New-/Set-/Start-/Unregister-ScheduledTask)+ Out-File 落盘再读, ⛔ 别指望命令回显。
  2. 改"起法"必须两边都改 —— 判据 = grep -n "New-ScheduledTaskAction" + grep -n "\.ps1"。 ⚠️ "改了主路径" ≠ "把旁路也改了"(2026-10-05 只换动作、没换名字与旁路 ⇒ 2026-10-06 二次收口)。

三、🔴 两个仍然有效的坑(现行形态下照样会踩)

坑① 🔴 不设 CODEBUDDY_CONFIG_DIR ⇒ 读到 0 字节空库

  • 计划任务不继承会话的环境变量 ⇒ _wb_db() 退回 Path.home()/.workbuddy/workbuddy.db (本机那个是 0 字节、无 sessions 表)。
  • 症状:每 20 秒刷一次 all_sessions_idle 读库失败 no such table: sessions ⇒ 闸二(所有会话都结束)永久失效 ⇒ fail-safe 判"有会话在跑" ⇒ 检查会话永远建不出来。
  • ✅ 两道兜底都要有: · 启动器进程内设(通用,见 1.2); · 本区 collabd.config.json 写死 host_db(指真库 E:/ProgramData/.workbuddy/workbuddy.db,单区保险)。 ⚠️ 实测只有 ai1net 炸、vibe 没事 —— 差别就在 vibe 那份配置里 host_db 写了绝对路径。

坑② 🔴 配置查找靠 cwd ⇒ 传错 cwd 当场拒跑

_cfg_candidates() = COLLABD_CONFIG → <cwd>/.workbuddy/collab/collabd.config.json。 ⛔ 不要把 cwd 设成 collab/ 目录 —— 那会拼出多一级 .workbuddy ⇒ 当场拒跑。 ✅ 设成工作区根 + 同时显式给 COLLABD_CONFIG(双保险)。 (对应到任务侧就是 -WorkingDirectory 必须是工作区根,见 2.3。)

🟠 原坑③「PowerShell 5.1 读无 BOM 的 UTF-8 .ps1 ⇒ 中文路径全毁 ⇒ 秒退」 —— 随 .ps1 形态整套废弃而失效,⛔ 现行形态已无 .ps1 可读。 留痕见第四节 5.1 条(本机所有含中文路径的 .ps1 仍应带 BOM,是通用经验)。 ⚠️ 判据口径也跟着变了:旧形态 LastTaskResult 必须 0; 现行形态是长驻进程 ⇒ 正在跑时 LastTaskResult=267009(0x41301="任务正在运行"), ⛔ 别再拿"必须 0"当判据(见第五节的正确判据)。

四、🟠 旧形态留痕(2026-10-03 ~ 10-05 · ⛔ 只当历史读)

⛔ 本节所有写法都已废弃,⛔ 不许照着做。 保留只为两件事: ① 出问题时能读懂老日志/老进程;② 里面有若干通用教训(下面标了 📌)。

4.1 旧载体=<区>\.workbuddy\collab\start-supervise.ps1(永不返回的守护循环)

⚠️ 旧版要求"必须是永不返回的守护循环版",⛔ 别写成"跑一次就退" —— 那样常驻死掉时脚本 正常返回 0 ⇒ 任务被判「成功」⇒ Restart* 永不触发(测试3 实测:活 12 分钟后再不复活)。

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

旧形态的建任务(⛔ 与 2.3 的区别就是 -Execute 与 -Argument):

$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

⚠️ 旧文档曾写「动作一律用 powershell.exe(不是 pythonw.exe)—— 动作里直接跑解释器没有'设环境变量'这一步」 —— 🔴 这句是 P0-72/P0-73 的病根:env 应该由启动器在进程内设,⛔ 不该靠 powershell.exe 去设; 而 powershell.exe 是控制台程序 ⇒ 闪黑窗。

4.2 旧形态的三个坑(5.1 是通用经验,另两条随 .ps1 一并废弃)

5.1 🔴 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 变乱码 ⇒ 路径不存在。
  • 📌 通用经验(仍然有效):本机所有含中文路径的 .ps1 一律带 BOM,否则一律秒退。 ⚠️ 现行形态不用 .ps1 了,所以这条只在"你自己还要写 .ps1"时才相关。

5.2 🟠 keeper 是 powershell.exe -File <ps1> ⇒ Stop/Start-ScheduledTask 不足以让它生效 PowerShell 启动时把脚本读进内存 ⇒ 磁盘上改了、任务重启了,旧 keeper 进程可能仍在跑老脚本。 实测:同时存在两个 keeper(57680 旧的 + 20180 新的),旧的按老路径拉常驻 ⇒ "我明明改好了,起来还是老样子"。 ✅ 旧做法:重铺后显式杀 keeper 进程(taskkill /F /T /PID <keeper_pid>)再 Start-ScheduledTask。 📌 通用教训(仍有效):改完副本必须重启常驻,判据是心跳里的 argv0,⛔ 不是文件 md5 (md5 一致只证明文件换了,⛔ 不证明进程换了。P0-17/P0-74 同族)。

5.3 🟠 从工作区副本发起时,找载体模板不能写死包根 旧 _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/。 📌 通用教训(仍有效):凡"从 __file__ 往上推包根"的代码,副本换布局后都会错 —— 副本是 <WS>/.workbuddy/collab/(不是包树里的 scripts/),别套用同一套相对层数。 🔴 2026-10-05 起 _find_keeper_tpl() 与 assets/start-supervise.ps1.tpl 依赖已整套删除。

4.3 🔴 为什么被换掉(两个事故,都在用户眼前犯的)

  1. 闪黑窗:任务动作写死 powershell.exe -WindowStyle Hidden -File start-supervise.ps1, 而 PowerShell 是控制台程序 ⇒ 每 5 分钟触发一次就闪一个黑窗。用户 2026-10-05 报 「又弹了窗口」/2026-10-01 「程序在界面一会弹出一会弹出的,影响我操作」, 实测抓到 powershell.exe + conhost.exe 这对进程。 ✅ 修法:动作改 pythonw.exe(GUI 子系统 ⇒ Windows 永不为它分配控制台窗口)+ 启动器。
  2. 两条起法并存:collabd.py::_escalate_to_keeper() 这条自我供给旁路还在建旧形态任务 ⇒ "改了主路径没改旁路"。2026-10-06 二次收口:任务名统一 collabd-keepalive-<区>、 旁路清除(init_workspace.py)、探针扩成扫全族。 ⚠️ 教训:凡"这一族文件都不许有 X",必须枚举整族,⛔ 不许只挑一个代表 —— 挑一个就等于没扫。

五、验收(⛔ "打印已启动"不算,三样都要机读)

# ⚠️ schtasks 被拦;一律 PowerShell 模块 + 落盘看结果
Get-ScheduledTask     -TaskName "collabd-keepalive-<区名>" > E:\tmp\t1.txt
Get-ScheduledTaskInfo -TaskName "collabd-keepalive-<区名>" > E:\tmp\t2.txt
# 判据 现值应该是 ⚠️ 别搞错
1 State Running Ready 只说明"此刻没在跑",⛔ 不代表机制坏(5 分钟触发窗内常是 Ready);要配合 ③ 才算稳
2 LastRunTime 近 5 分钟内 —
3 LastTaskResult 267009(0x41301="任务正在运行") ⛔ 别再拿"必须 0"当判据(那是"跑一次就退"时代的);1 = 秒退(旧形态八成是 BOM)

存活唯一机读判据(=机制里那条,⛔ 别用"日志在更新"代替): .workbuddy/collab/logs/supervise-heartbeat.json 里 pid 活着 ∧ ts 距今 < 90 秒 ∧ round 递增。

$p = Get-Process -Id <pid> -ErrorAction SilentlyContinue   # $null = 已死
# ⛔ tasklist 走 bash 会被当路径;一律用 PowerShell

静置观察(≥ 12 分钟,⛔ 短于 10 分钟证明不了任何事 —— 常驻就死在第 12 分钟): round 持续递增、心跳里 argv0 逐字等于本区 .workbuddy/collab/collabd.py、 并且 已有活的常驻 ⇒ 本实例退出(幂等) 出现一次(证明没双写)。

判"改的代码真生效了":心跳里 started_h(或 started_ts)晚于你改完/分发完的时刻 (⛔ 只看文件 md5 不算 —— 见 4.2 的 5.2 条)。

六、⚠️ 还没有它就不算长期(两条,别自欺)

  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 走优雅退出 → 等 STOP_GRACE(45 s)→ 不死才 taskkill /T /F)。 ⛔ 受阻 / 已暂停不关(口径如此)。
  • 🔴 停止标志由常驻本体自己看(collabd.py::_guard_says_stop(),对所有实例生效)—— ⛔ 现行形态不再有"守护循环/ps1 去看标志"这一层。 ⚠️ 于是"任务每 5 分钟还在触发"≠"还在徒劳重拉":常驻起来见 guard.stop 即优雅退出, 不会空转(旧 ps1 时代那 6 小时空转 370 次的毛病,结构上已不存在)。
  • "完成"可逆:ensure_supervise() 续命前清掉 guard.stop ⇒ 新目标/目标调整后能再起来。 ⛔ 不清就永远起不来。
  • 跨区自愈:本区常驻每 6 轮顺带扫一遍 peer_workspaces,只补"目标进行中"的区 (已完成/受阻/有停止标志的 ⛔ 一律不拉)⇒ 但计划任务是更靠前的第一道 (⛔ 别把跨区自愈当载体的替代品:它要求本区常驻先活着)。

八、按场景对照(选哪条 / 怎么验 / 常见坑)

场景 主会话怎么来 常驻载体 目标完成后谁关 常见坑
A · 用户手动创建主会话 用户自己开 [主]-…,盯着推进 计划任务 → pythonw → supervise-launch.py 常驻见 guard.stop → 优雅退出 🔴 用户一收工就没人拉 ⇒ 若没装载体,常驻会在会话结束时静默死掉(看板还显示"在线")
B · 定时任务创建主会话 排期 recurring 到点拉 [主]-… 同上(⛔ 排期代替不了载体) 同上 🔴 once 排期过期即哑(next_run_at=None、界面仍显示 ACTIVE)⇒ 静默窗内无人;🔴 排期跑完即 completed,下一跳之前是空窗

共同判据(两种场景都一样,⛔ 每次改完都验):

  1. State = Running(或 5 分钟窗内 Ready);
  2. LastTaskResult = 267009(长驻中);⛔ 1 也是信号(秒退);
  3. 心跳 pid 活 ∧ ts 距今 < 90 s ∧ round 递增;
  4. 静置 ≥ 12 分钟再看 ③(⛔ 短观察证明不了事 —— 常驻常死在第 12 分钟);
  5. 目标完成后:常驻日志出现收工行 + 心跳不再更新但进程已退(验"真的停了", ⛔ 不许只看"心跳停了" —— 那也可能是死了而不是收工;旧形态的判据是 keeper 日志出现 stands down + 重拉计数不再增长)。

九、任务名(⛔ 名字别撞,也别再改名)

对象 任务名 备注
常驻(协作程序) collabd-keepalive-<工作区名> 例:collabd-keepalive-vibe-product
看板 dsh-board-keepalive 动作 = pythonw.exe + board-launch.py
🟠 旧名 collabd-supervise-<区> 已废弃;collabctl.py::SCHED_TASKS 把它列进禁用名单,见一条禁一条

⚠️ 一个区一个任务;⛔ 多个区共用一个任务名会被 IgnoreNew 挡掉第二个。 🔴 ⛔ 别再改任务名 —— 它同时是 collabctl.py 的"启用名单"和"禁用名单"的键; 改名必须同时改两边,否则自愈起的常驻会被 off 顺手干掉(2026-10-06 实测踩到)。

十、🔴🔴 「各区都能常驻」怎么一次做齐(2026-10-04 用户点问)

用户原话:「看看是否有各工作区都能启动常驻程序的最好办法, 而不是现在这种只能某个工作区才能常驻的办法」

✅ 答案:能,而且就是本节那套 —— 每区各建一条任务,同一条命令换个区名/换条路径即可。 ⛔ 不存在"一个载体管所有区"的写法,⛔ 也不需要:任务天然可以并存, IgnoreNew 只在同一个任务名上生效(⛔ 撞名才挡)。

一区四步(把 <区> 换成工作区绝对路径)

  1. 确认启动器在位:<区>\.workbuddy\collab\supervise-launch.py (⛔ 别自己铺 —— 缺就跑 deploy_code.py --ws <区> 分发)。 collabd.config.json 里补两项:host_db(指真库,⛔ 别依赖环境变量,见坑①)+ 本区 workspace。
  2. 建任务:走 collabctl.py on(正门)或等 _escalate_to_keeper() 自愈; 任务名 collabd-keepalive-<名>,动作/触发器/设置按 2.3 那四条。
  3. 立即 Start-ScheduledTask(⛔ 别等下次登录/别等 5 分钟)。
  4. 验收:按第五节的 5 条 ⇒ 静置 ≥ 12 分钟再看一次。

各区之间的隔离(为什么"能各建各的")

  • 任务名不同 ⇒ IgnoreNew 互不影响;
  • COLLABD_CONFIG 指向各区自己的配置 ⇒ 各读各的 goal.json/台账;
  • 跨区自愈是"顺带",⛔ 不是主靠:本区常驻每 6 轮扫一遍 peer_workspaces, 只补"目标进行中"的区(已完成/受阻/有 guard.stop 的 ⛔ 一律不拉)。 ⚠️ 所以别把跨区自愈当载体的替代品 —— 它要求本区常驻先活着,是第二道。

⛔ 别犯的三个错

  1. ⛔ 用一条任务带多个区(一个动作只能跑一个区的启动器);
  2. ⛔ 一区多条任务(会双写台账);
  3. ⛔ 以为"某区现在活着"就不用建任务 —— 那是孤儿化幸存,父链一断就没了(第一节末尾那条实测更正)。

📌 本文档的事实在哪: · 2026-10-03/04 部分(判据表、in_job 更正、静置 12 分钟、自锁)= 三份复盘 + 当晚实测; · 2026-10-05/06 部分(启动器、任务名统一、旁路清除、探针扩族)= P0-72/P0-73/P0-74 实测, 权威源在 SKILL.md「常驻机制的真实形态」与本技能 scripts/。 ⛔ 任何"应该行得通但没实测"的写法都别往里加。