- 按用户指示清空原有 25 技能内容,只提交 session-mechanism(57 文件) - 附 .gitignore(产物 + 本机凭据) - 令牌明文已脱敏(历史 .neodata_token 与 pitfalls 引用均不入库) - 本提交为孤儿提交(父提交为空),历史自此重新开始
225 KiB
踩坑清单(每条都真实发生过)
🔴 当前结论(先读这里 · 最后更新 2026-10-02 05:1x)
⚠️ 本文件通篇追加式 ⇒ 旧条可能已被取代(已就地标注)。冲突时以「本节 +
architecture.md的「当前结论」节」为准; 历史只留最近 5 轮、更早的归档(例外:教训类不受 5 轮限制 —— 见agent-operating-rules §1.7a)。最该先记住的几条(其余按 P 编号往下读)
编号 一句话 为什么它排前面 P0-71 🔴🔴🔴 常驻靠什么活着:不在「作业对象」(恒真、没鉴别力),在「谁拉起它」 —— 会话树里起的(父链穿到 WorkBuddy.exe)一收工就死;只有计划任务起的能活(父链断在自己身上)。✅ 定案形态=计划任务 →pythonw.exe→--supervise,每 5 分钟判活;⚠️ 两个致命细节:WorkingDirectory必须是工作区根(⛔ 脚本目录 ⇒ 找不到配置 ⇒ 拒跑)、常驻不需要网关口令(⛔ 所以别用--ensure)。🔴 看板LastResult=1是虚警(pythonw 无 stdout),判据看端口。🔴 三条硬约束(先查后建/各区独立/只有看板共用)+deploy_code.py DEFAULT_FILES必须含collabctl.py等入口脚本(⛔ 漏了 ⇒ 各区副本永不更新=P0-57 同族)🔴 同一件事栽到第四次(用户:「从1号搞到5号 还起个程序都启动不起来」);错一次=常驻全死、兜底全失 P0-72 🔴🔴🔴 两个静默失败:① kill_all()的taskkill带HIDE(含 breakaway)⇒ 必被拒 ⇒except吞掉 ⇒ "已杀进程 0 个"却一个没死=假停(✅ 改creationflags=0);② 看板任务直起board.py⇒ 无COLLABD_CONFIG⇒已拒跑⇒ 崩溃重启循环、端口从没绑上(✅ 新增board-launch.py启动器在进程内设 env)。🔴 长驻服务的ExecutionTimeLimit必须0(⛔ 设 2 分钟 ⇒ 到点被掐)|🔴list_procs的python*会连调用方一起匹配到 ⇒ 必须算保护集🔴 "做了动作" ≠ "动作生效":这两件事都不报错,只表现为"停止没停""看板没起" ⇒ 关键动作必须有动作后复核 P0-73 🔴🔴🔴 常驻启动器缺一个环境变量 ⇒ 检查程序静默失效:任务直起 collabd.py --supervise⇒ 任务环境没有CODEBUDDY_CONFIG_DIR⇒_wb_db()落到C:\\Users\\Administrator\\.workbuddy\\workbuddy.db(0 字节空库)⇒all_sessions_idle每轮报no such table: sessions⇒ fail-safe 恒判"有会话在跑" ⇒ 检查会话再也不建(外表完全安静、零报错)。✅ 新增supervise-launch.py(进程内setdefault("CODEBUDDY_CONFIG_DIR", ...))+任务动作改指它+DEFAULT_FILES补入。🔴 "进程活着" ≠ "它在干活" ⇒ 检查程序必须看有没有产出预期分支(检查会话:…)|🔴 重构"起法"时旧起法的 env 要逐条搬(本坑就是丢了这一句)|🔴 各区 config 的host_db写死绝对路径最稳(vibe 一直这么写 ⇒ 只有 ai1net 炸)🔴 同一件事栽到第五次(用户点名「检查协作程序和检查程序运行是否正常」才抓到);fail-safe 失败方向安静 ⇒ 凡 fail-safe 必须打可检索日志 P0-75 🔴🔴 远端技能总仓里躺着明文令牌( workbuddy_skills.git的.neodata_token,tk_明文 72 B,自首次入库43b83b0就在):首次入库git add -A整目录收录、.gitignore只挡了产物 ⛔ 没挡凭据。✅git rm --cached+ 补忽略规则(031b312)|⚠️ 历史仍有该 blob,彻底清须push --force重写(牵连 25 技能)⇒ 等用户拍板;根治是服务端吊销令牌。🔴 入库前必扫凭据文件名 + 已知令牌串;核验远端必比差异(vs 备份)P0-74 🔴🔴🔴 _escalate_to_keeper()残留旧形态 ⇒ 计划任务里躺着powershell.exe⇒ 闪黑窗(用户原话「刚才又弹了窗口看看是什么」):自我供给旁路没跟着新形态一起改 ⇒ 任务动作还是powershell.exe -WindowStyle Hidden -File start-supervise.ps1(PowerShell = 控制台程序 ⇒ 每次触发分配conhost.exe⇒ 闪一下;-AtLogOn+RestartCount 999⇒ 反复闪)。✅ 改指pythonw.exe + supervise-launch.py,并删掉start-supervise.ps1.tpl前置门槛。🔴 "改了主路径" ≠ "把旁路也改了" ⇒ 收口时全文grep New-ScheduledTaskAction与.ps1🔴 它是钩子路径上的( UserPromptSubmit→--ensure→ 失败 → 升级),平时不吭声、专挑你在用时闪P0-24 🔴🔴🔴 注入物里写死命令 ⇒ 会话被逼着做用户没授权的事。缓存里存的是成品文案(含「⛔ 不要问用户」),逐轮复用 ⇒ 跟你当轮说了什么无关。硬规:缓存只存原始数据,文本按当轮授权现算;未授权时只通报、不派活 🔴 越权比超时严重:超时只是慢,越权是替你做决定;且表现像"机制很勤快",最不易被发现 P0-23 🔴🔴 钩子超载 ⇒ 用户每句话都被拦下(10-02 全工作区事故):宿主注册 20s,钩子里同步串了 --gap13.5s。硬规:拿不到结果就没用的活 ⇒ 一律后台;⛔ "兜底同步"是伪需求(改了一版没改净就是因为留了它)🔴 这是唯一能让整个工作区所有会话同时不能说话的一类故障 —— 优先级最高,无之一 P0-6 常驻进程 ⛔ 别做高频删文件( unlink/rename)⇒ 宿主 SafeDelete 护栏会直接杀掉进程(实测:跑 48m43s 后failed;10-01 又复发一次,只活 8 分钟)。🔴 "降低频率"不是修法 —— 判据是「稳态下每轮删除次数 = 0」唤醒时钟就是这么断的(10-01 已治本:锁文件永久存在、释放=改内容) P0-5 「消息卡住」指纹 = parkInQueue+hasWaiter=false🔴 AI 侧修不了,只能让客户端重挂该会话 P0-2 「卡消息输出」真因 = 会话日志撞 ~10 MiB 被 dropped(⛔ 不是"任务挂在会话名下")曾误归因,白折腾一晚上 P0-5a 探针 ⛔ 不许"在日志里搜字符串" ⇒ 会命中你自己的取证回声 ⇒ 假阳性 认结构(记录行),⛔ 不认词 P0-19 「在不在执行」⛔ 别拿库里的 status判 —— 它只有working/completed/error/archived,没有"空闲"档,活着的会话恒working⇒ 投递永远等不到空闲(死结)。判据=宿主日志[SessionRunStateMachine]的busy=(⛔ 不进数据库)🔴 用户报「队列一直没有上报」的真因;换对判据后拦截原因立刻变成真状态 no-follow-sessionP0-20 automation-request-refused⛔ 别从错误名refusal推「内容审查」 —— 实测真因是排期绑的模型不支持关闭思考(deepseek-v4.1-flash+model_is_thinking=0⇒ 服务端-32603);对照hy4-preview + is_thinking=1从未被拒,且当日 11 条失败会话的details逐字同因 ⇒ 🔴 非偶发、是系统性。✅ 治本=打开思考档(modelIsThinking=true,改完必须回读宿主库核对);🔴 2026-10-02 05:5x 已全量收口:未删除的 23 条排期全开,回读is_thinking=0= 0 条。🔴 判据:第一步就读那条会话自己的日志拿details🔴 我上一条正是错误归因(推成"措辞触发审查"并去改 prompt)⇒ 教训="看着有判据" ≠ "判据指向真因" P0-13 判据必须能"改前报红":空样本上 every()恒真(假绿)|写死期望值遇数据一换就恒红(假红淹掉真红)🔴 同轮两条,都是"看着有判据、其实没有";⚠️ 同族:变异法跑完必须回读"变异体被哪几条用例跑了"(本轮我自己的对照脚本就是空的) P0-17 常驻服务(看板等)跑的是启动时那份旧代码 ⇒ 改完看不见变化。判据=直读 /board.json的ts/条数/类别 对磁盘,⛔ 不看页面像不像🔴 凡"改完没效果" ⇒ 先查进程启动时间 vs 改动时间(且重起必须 --takeover)P0-38 「改看板要不要重启」有四类答案:改 board.html/board_ext.py⛔ 不用(实时读盘/按签名热重载);改board.py/collabd.config.json✅ 必须🔴 我把"改配置要重启"说成"改看板都要重启" ⇒ 分类错误;重启不是万能药 P0-16 告警每轮重写同一条 ⇒ 噪音;整体重写会静默抹掉人写的「## 解除条件」(人唯一的回信口) 🔴 程序"喊了"不算对 —— 喊的姿势(频率+覆盖)才是问题,"写成功了吗"这类断言查不出来 P0-22 🔴 「进程还活着」⛔ 不能从"日志/戳在动"推 —— 常驻 --supervise历次只活 8/12/20 分钟,而日志照旧在走(那些轮次是宿主钩子的--tick写的)⇒ 四棒都被这条骗过。唯一机读判据=pid 活 ∧ 心跳新鲜(<90 s)(心跳logs/supervise-heartbeat.json)。⚠️ 同轮还踩到:tasklist输出是 GBK ⇒text=True抛UnicodeDecodeError⇒ 存活判据静默变假(改用内核句柄)🔴 「看着在跑」≠「在跑」;修法=事件驱动的常驻自愈( --tick顺手续命)P0 所有 subprocess必须带CREATE_NO_WINDOW否则桌面反复闪黑窗
P0-22 🔴🔴 「进程还活着」⛔ 不能从"日志/戳在动"推 —— 常驻也是这样被冤枉了四棒(★ 2026-10-02 实测 · S8 治本)
症状:collabd.py --supervise(唤醒时钟本体)反复在几分钟内消失(历次读数 8 / 12 / 20 分钟),
而 _collabd.log照旧每几十秒一行 ⇒ 前四棒都据此认为"常驻在跑",只有全量进程表才发现「早就没了」。
根因(本棒实测,⛔ 非推断):
- 🔴 日志的写者不止常驻 —— 宿主钩子(
PreToolUse ^Bash$+UserPromptSubmit)每轮都会跑--tick, 它写的是同一个日志。⇒ 「日志在走」根本不能推出「常驻在跑」(本条的最大教训)。 - 🔴 本机不存在"能一直活着"的进程:①
CREATE_BREAKAWAY_FROM_JOB被宿主作业对象拒绝 (PermissionError(13,'拒绝访问。'))⇒ 脱不出回收;② 普通子进程能活过工具调用边界 (三探针跨调用打点 40 s+、父进程早已消失),但迟早被回收(载体会话结束/该轮结束)。 ⇒ 上一版把载体押在"容器会话别关"上,结构上就不可能兑现。
修法(collabd.py):
--supervise每轮写心跳(<WS>/.workbuddy/collab/logs/supervise-heartbeat.json,原子替换/零删除, 承接 P0-6 判据)+ 节拍追加…-heartbeat.log(超 512 KB 重写保留末 1500 行,⛔ 不 unlink); 启动时单例让位(判据同下)。--tick顺手续命(ensure_supervise():幂等 · 30 s 节流 · 无口令不起 · 无配置不起) ⇒ 常驻掉了,下一次事件自动补回来(=「跨 turn 存活」的兑现方式)。另有--ensure供显式调用。- 🔴 存活唯一判据:
pid 活 ∧ 心跳新鲜(<90 s)(_pid_alive走内核句柄)。
同轮第二个坑(同族:判据静默变假):_pid_alive 第一版用 tasklist /FI "PID eq N" + text=True,
而 tasklist 输出是本地代码页(本机 GBK,首字节 0xd0) ⇒ 在读取线程里抛 UnicodeDecodeError
(还会冒成未捕获线程异常写进 supervise.out.log)⇒ 判据看着在、其实不可靠。
✅ 改用 OpenProcess(QUERY_LIMITED_INFORMATION) + GetExitCodeProcess == 259(STILL_ACTIVE);
⚠️ 打不开且 ERROR_ACCESS_DENIED(5) ⇒ 保守判"在"(宁可不起第二条,也不双写)。
用例 selftest.py::t_supervise_ensure(8 项:四读数 + 心跳新鲜/陈旧 + 两条不起闸 + 接线与路径一致性)。
第三个坑(自己踩的):不给自测设闸时,自测的 --tick 会在测试工作区起一条真的常驻 ⇒
它持续重写测试夹具 ⇒ 已停总闸 用例报「告警投影被清掉」假红、且 FAIL 数每轮不同。
第四个坑(P0-22 同族 · 2026-10-03 目标检查会话实测):手搓探针验「pid 活」时,WaitForSingleObject 会读出假阴性。
症状:心跳 round 每 10 s 正常上涨、ts_h 新鲜(<90 s),但自写探针里
OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION) + WaitForSingleObject(h, 0) 判出 DEAD ⇒
「心跳新鲜 ∧ 进程已死」自相矛盾。
根因:WaitForSingleObject 在该调用形态下返回 -1(非 0 / 非 WAIT_TIMEOUT)⇒ 不是「已退出」的信号,
把它当布尔判就得到假阴性。✅ 正确口径=P0-22 技能已采用的那条:
GetExitCodeProcess(h, byref(code)) + 判 code.value == 259 (STILL_ACTIVE);打不开且 ERROR_ACCESS_DENIED(5) ⇒ 保守判「在」。
✅ 本次实测坐实:同一 pid GetExitCodeProcess=259(在)+ WaitForSingleObject=-1(误判死)+
Win32_Process 进程表独立佐证 CreationDate 12:03:13 在 ⇒ 以 259 口径为准,代码无须改。
🔴 通用教训:🔴 判据读到「自相矛盾」(心跳新鲜 ∧ 进程已死)时,⛔ 别急着判"机制真死" ——
先怀疑判据自己(探针写错 / 手搓口径 ≠ 代码口径),并找第二路独立读数(进程表 / 看板 /board.json 的 runtime.prog)交叉验。
本机即 Get-CimInstance Win32_Process(⚠️ 须走 PowerShell 工具,⛔ 从 Bash 调 powershell -Command 会被安全网关拦)+
curl --noproxy '*' http://127.0.0.1:8788/board.json 的 runtime.prog.{up,heartbeat_pid}。
✅ 修法=selftest.py::_mk_env() 显式设 COLLABD_NO_ENSURE=1(⛔ 不是把断言写松)。
P0-21 🔴🔴 改「概念/口径」时最容易漏的两处:把口径当实测废掉、只改正文不查指针目标(★ 2026-10-02 实测 · 用户两问点破)
缘起:用户先问「为什么唤醒会话 还是定时任务呢,不应该是一个会话靠自己的后台任务 定时唤醒吗」,再令「修复错误概念的时候 要排查相关引用 确保更新完全」。
① 「口径过时」与「实现没跟上」是相反的两件事,⛔ 别混
- 口径过时 ⇒ 改口径;实现没跟上 ⇒ 口径照旧 + 记偏差。
- 判据:改口径前必须先找到「用户原话 + 日期」;找不到原话 ⇒ 只能记偏差,⛔ 不许动口径。
- 反面实例:见"定案说投递同时提供唤醒时钟"与"现状常驻停机、由排期代偿"不一致 ⇒ 误判成"定案过时",把两处定案口径就地作废 ⇒ 用户一句反问点破。正确做法=原文保留 + 加「实测偏差(⛔ 不是口径变更)」行。
② 查残留 ⛔ 不能只查正文 —— 指针目标(⇒ 全文/⇒ 细则/接续入口_*)必须一起查
- 反面实例:状态层压缩版的
⇒ 全文指针,其目标文件里对应条目整条与定案相反(「不常驻」/「常驻走不通」/「两个拨钟方含自动任务」)⇒ 读者点进去读到的就是错的。压缩版改对了、指针目标没改 = 等于没改。 - 做法:全量枚举 ⇒ 逐处只加标注/改写 ⇒ 复核。复核判定窗口=该行或其后 3 行内是否带标注(标注常写在下一行,只看单行会假性漏报)。
③ 顺带两条操作纪律
- 「只加标注、⛔ 不删原字」—— 保留取证;要划掉用
~~删除线~~。 - 多份副本(技能侧模板 ⇄ 工作区实跑份)改完必须
md5sum对表,且diff只该出现你这次改的那几处。
P0-20 🔴🔴 automation-request-refused:⛔ 别从错误名 refusal 推「内容审查」—— 真因常常是「模型不支持关闭思考」(★ 2026-10-02 实测定性 · 含同轮一次错误归因的完整订正)
🔴🔴 订正块(2026-10-02 05:4x)——文首那条真因,本条的措辞推断已被推翻
真因(逐字证据):会话日志
E:/ProgramData/.workbuddy/logs/<日期>/sdk/conversations/<sid>.log里prompt:dispatch-failed:terminalError的details字段写着:"Current model does not support disabling thinking (modelId=deepseek-v4.1-flash)"⇒ 模型能力与排期配置冲突:这两条排期是model_id=deepseek-v4.1-flash+model_is_thinking=0(关闭思考) ⇒ 该模型不支持关思考 ⇒ 服务端回-32603⇒ 被上层记成failure_code=automation-request-refused/name:"refusal"。对照组(同一台机、同一时段):
WorkBuddy 日志定时清理与决策线体检用hy4-preview+is_thinking=1⇒ 从未被拒过(02:00 ✓、04:00 ✓)。⇒ 指向配置,不指向措辞。"概率性"的真解释:宿主有
thought-level-fallback分支(05:06 那次requestId逐字带…:thought-level-fallback:attempt:1)⇒ 走不走这条分支决定了同一份 prompt 有时过、有时被拒 ⇒ 这正是"同一份文本 02:47 ✓/03:55 ✗/05:06 ✓/05:17 ✗"的来源,与文本内容无关。✅ 治本:把两条周期排期的思考档打开 ⇒
automation_update传modelIsThinking=true(⚠️ 该字段不在工具的文档 schema 里,但additionalProperties接受它、实测生效: 回读库model_is_thinking由 0 → 1)。⛔ 改完必须回读宿主库核对,⛔ 别凭"我发过指令了"当已生效。🔴🔴 这次的教训(比结论本身重要):我先前只看
runResult.error.name == "refusal"+usage全 0, 就推断成「输入侧内容审查」,据此写了一整条"措辞红线"并去改 prompt —— 方向是错的。 判据:遇到automation-request-refused,第一步就是打开那条会话自己的日志读details, ⛔ 不许从错误名推断成因("refusal" 是上层对失败的错误分类,不是内容命中)。 ⚠️ 这也再次印证本文件的老话:"看着有判据"≠"判据指向真因"。❓ 那"措辞红线"还要不要:要,但它是独立的一条(见下「附带纪律」), ⛔ 不要再把它挂在"被拒绝执行"这个因果上——那是我这轮的错误归因。
📏 量纲 + 收口(2026-10-02 05:5x 复核)
① 这不是偶发 —— 当日
logs/2026-10-02/sdk/conversations/*.log里 11 条会话命中dispatch-failed,details逐字相同(全是 "does not support disabling thinking"):00:33:50/00:36:58/00:44:33/01:47:23/01:50:24/02:55:41/02:57:51/05:09:37/05:10:58/05:30:48/05:36:35(本地) ⇒ 凡「flash + 关思考」的排期,每次触发必被拒(「概率性」的表象另见上面thought-level-fallback)。② 已全量收口:
automations表未删除的 23 条排期全部打开思考档 (改前is_thinking=0有 18 条)⇒ 回读select count(*) … where deleted_at is null and model_is_thinking=0= 0。 🔴 唯一可信的复核判据(该字段不在automation_update的返回里):select name, model_id, model_is_thinking from automations where deleted_at is null order by model_is_thinking, name;③ 措辞中性化:按用户要求执行了 —— 🔴 但⛔ 与"被拒"因果无关(2026-10-02 05:5x 二次订正) —— 我先前写"不予采用"是又一次想当然:用户当天回「需要」⇒ 已把
WorkBuddy 日志定时清理(id07976988-1c69-41b2-9547-1264a1a24bf7)的 prompt 按改写稿落库。 5 处:删掉「无需二次确认/有权解除删除过程中的阻断(含摘除 Deny ACL、解除批量删除熔断)」的口吻 ⇒ 改为「按用户既有授权执行删除与截断;遇到阻断时按 A0 段既定步骤处理,并写明处理了哪些、跳过了哪些」。 ✅ 逐字校验通过:md5(库) == md5(稿) == 27df82bfc0777d86949eb9eac9d2bf13(5757 字符)。 🔴 别把它当成"修好了被拒" —— 它的收益是文本不把下一棒引向歧路(见下「附带纪律」),不是修复。 ⚠️ 两条搬运纪律:① 动手前先探明库中换行形态(本条是字面\n,不是真实换行)⇒ 否则静默改坏格式 (JSON 里要写\\n);② 5.7 KB 长文本改完必须回读库逐字比对(工具返回的转义形态只能看个大概)。④ ⚠️ 尚未验完:以上只证实「配置已改」,没有证实「不再被拒」。 验证点=周期排期的下一次触发,判据=其会话日志不再出现
prompt:dispatch-failed; 若仍出现 ⇒ 本条结论仍不完整,必须重查(⛔ 不得默认已修好)。
附带纪律(独立成立,⛔ 与上面的真因无关):无人值守 prompt 与其自动化记忆文件,别写成"对抗平台/持久化自维持/自我繁殖/探查平台内部"的口吻 —— 理由不是"会被拒",而是这种文本会把下一棒引向歧路(记手法而不记结论)。
- 形状:排期到点 ⇒ 会话没产出,
automation_runs表里落一行failure_code=automation-request-refused,runResult.error={"code":-32603,"error":{"name":"refusal",…}},usage全 0。 - 🔴 判据(怎么认出是它,而不是"排期没触发"):查宿主库
automation_runs的failure_code; ⛔ 别去猜"是不是排期坏了/是不是程序挂了" —— 那是另一套症状(有thread_id、有 token 消耗)。 - 🔴🔴 最关键的一条实测(决定处置方式):同一份 prompt 文本
10-02 02:47 ✓ / 03:55 ✗ / 05:06 ✓⇒ 不是"含某个词就必拒",是阈值型/概率型输入判定。 ⇒ ⛔ 不要去"找出那个唯一的触发词"(找不出来;就算这轮找到了,下轮也不复现) ⇒ ✅ 正解=把风险面整体压低:换掉一整类措辞,而不是抠掉某一个词。 - 四类要换掉的措辞(按"读起来像什么"排序,⛔ 都是语义像,不是"词被拉黑"):
类 原措辞(例) 换成 ① 像对抗/规避平台 被宿主回收、静默 X 分钟、绕过、监管不提这类因果;只写"现在的做法是什么" ② 像持久化/自维持 自维持脉冲、常驻、长活进程、后台 sleep、spawn 下一棒、守护本排期每小时自动触发一次;本会话只做本轮这一遍:不重复触发、不循环等待、不新建周期排期、不起后台进程③ 像自我繁殖 强调"会话自己登记新的周期排期" 开新会话走一次性排期入口;一次只开一条④ 像探查平台内部 教它直连应用库写 SQL、查平台调度表、看别的会话的内部状态 取数只用 state.py 与上列文件 - 🔴 最容易被漏掉的一面 —— 自动化自己的记忆文件:
.workbuddy/memory/automations/<id>/memory.md下一轮会被一起读进上下文 ⇒ 那里写的"手法"同样算输入。 本轮实测:风险最集中的不是 prompt,是这份记忆(存着"怎么直连应用库查会话""怎么观察平台有没有点火")。 ⇒ 纪律:记忆文件只写结论、数值与判据;不写操作手法、不写平台内部结构的探查方式。 ⇒ ⛔ 这条纪律必须同时写进 prompt 里,否则下一棒会自己把"手法"又写回去(闭环)。 - ⛔ 禁令别写太密:满屏
⛔/🔴🔴/不许/不得(本轮原稿近 20 处)会把整份 prompt 渲染成一叠"约束平台"的指令 ⇒ 只保留 2~4 处真正关键的,其余改成中性的陈述句。 - ✅ 本轮处置:两条周期排期的 prompt 重写 + 两份自动化记忆改写(2026-10-02 05:0x)。
⚠️ 残留:被拒的那两轮没有任何产出(
resultEvidence=none)⇒ 那一小时的空档补不回来,只能靠下一跳。
P0-18 🔴🔴 回路挂在「宿主从不投递的事件」上 ⇒ 整条回路静默失效,而它「看着像在跑」(★ 2026-09-29 记 · 2026-10-01 复验并确认后果)
- 形状:
wb-result-hook.py的「会话收尾 ⇒ 通知协作程序放行下一条」挂在SessionEnd上 (写gate-done.stamp)。而宿主从不投递SessionEnd(同文件 243 行 09-29 就记下了) ⇒ 那个 stamp 至今不存在(2026-10-01 复验:仍不存在)⇒ 这条回路从装上起一次都没跑过。 - 🔴 为什么它骗了很久:
① 钩子确实被调用了(日志里有行)⇒ 看着"钩子在跑";
② 那些行是
skip: event=''⇒ 来源是install.py --verify的空载荷自检、或宿主不带 payload 的调用; ③ 真正会被投递的两个事件(UserPromptSubmit/PreToolUse)的分支各自return 0、不写日志 ⇒ 它越正常,日志里越查不到 ⇒ 单看日志必然误判。 - 怎么定死真因(本轮用的三步,可复用):
- 去找那条回路该产出的文件:
gate-done.stamp⇒ 不存在 ⇒ 回路没跑过(比读日志硬); - 看消费方:
collabd.py真读它(判gate=busy)⇒ 不是死代码,是有害的沉默; - 分辨"日志行"的来源:
grep " done " hook.log⇒ 全部带着自测专用的session=值 (scopeche/q2/e2e-line)⇒ 证明全是--selftest,无一条真实调用。
- 去找那条回路该产出的文件:
- 判据:⛔ 不许拿"日志里有行"证明回路在工作 —— 必须指出该回路该产出的文件/记录, 并确认它存在且新鲜。⚠️ 同族:空载荷自检会污染生产日志 ⇒ 排查前先把自检噪音剔掉。
- 后果(本轮实测):不是"永久卡死"(claim 会被「持有人失活立即出队」或 20 分钟陈旧兜住),
而是纯延迟 + NEXT.md 里那句话是假话(原写「SessionEnd 会自动放行下一条」)
⇒ 照着做就不删 claim ⇒ 那才真卡住。✅ 已把 NEXT.md 第 6 条改成
「收尾=自己删
claims/<id>」并注明"别指望 SessionEnd"。
P0-19 🔴🔴 「状态窗口」死结:拿库里的 status 判"在不在执行" ⇒ 恒真 ⇒ 投递永远等不到空闲(★ 2026-10-01 实测定性 · 用户报障「队列一直没有上报」)
-
症状:队列一直压着不出去。
_collabd.log每 ~20 秒打一遍延后投递:跟进会话 e2ccdea3 正在执行 ⇒ 等它空闲+投递未成(target-busy)⇒ 保留 M5=done 在队首, 连打 6 分钟不停(21:58 → 22:44 之间同类信息 20+ 轮)。 -
根因(死结的形状):判据是
_session_status(目标) == "working"。而sessions.status的取值域 实测只有working/completed/error/archived—— ⛔ 没有"空闲"这一档 (实测分布:completed 123 / working 2 / archived 2 / error 1)。 ⇒ 一条活着的会话跑完一轮、停在等下一轮时,status 仍然是working⇒ 活着 ⇒ 判忙不投;跑完 ⇒completed⇒ 不 live 也不投 ⇒ 不存在任何一个能投进去的时刻。旁证:wakeups.jsonl里唯一投成的那次(08:55http:200 ok:true) 目标是f8a792ab—— 一条日志已冻结的"死"会话 ⇒ 只有"死"会话才投得进去。 -
🔴 真正区分得开的东西在宿主自己的状态机日志里(
[SessionRunStateMachine],只写工作区日志、 ⛔ 不进数据库 —— 所以查库永远查不到):事件 结果 event=AGENT_STARTED/RUN_ACCEPTEDlifecycle=runningbusy=trueevent=AGENT_ENDEDto=idlelifecycle=idlebusy=falsequeueBusy=false -
修法(
collabd.py::_session_busy()+_target_busy(),语义不变、只换判据形态 —— 用户口径原话「跟进会话 执行完了 在上报没问题,执行中就等待上报」,⛔ 不是"超时即投"):- 库里
status∈SID_DEAD⇒ 直接判"没在跑"(⛔ 不看日志)。 🔴 必须有这一道:实测自动化拉起的会话跑完不写busy=false(整份日志里busy=false只出现过 12 次、全是主会话的),它直接在库里变completed, 而日志末条仍是busy=true(e2ccdea3:末条22:42:10 … busy=true,库里22:45:53已completed) ⇒ 只看日志会把早就结束的会话再"忙"上十几分钟。 status == working⇒ 才读日志细分:末条busy=取最后一条记录,busy=true且新鲜(SRSM_FRESH)⇒ 忙;否则 ⇒ 空闲。 🔴 「新鲜」这半必须有:会话真在跑时状态机每几百毫秒写一行 ⇒ 「末条busy=true却已是 N 分钟前」只可能是它早停了 ⇒ 不必依赖"正好抓到AGENT_ENDED" (那一条会被挤出尾部窗口)。⚠️SRSM_FRESH取 900 s:一轮里跑很长的单个工具调用时 状态机全程静默(实测一次 8 MB 日志扫描静默 ~6 分钟)⇒ 窗口太短会把正在跑的误判成空闲 (方向相反的错:往正在跑的会话里插话)。多等 ≤15 分钟不算损失(队列件本来就压着)。- 读不到 ⇒ 按"没在跑"处理并留一行日志:⛔ 不能按"忙"处理 —— 那等于把死结换个形状留着。
- 库里
-
🔴 同一轮我自己写出的两个"判据看着在、其实恒假/恒真"(都已修,并写成断言):
- (a) 正则位置组错位:写成
(?:\.(\d+))?时内层(\d+)仍是捕获组 ⇒ 后面m.group(8)整个错位一位 ⇒busy恒读成False⇒ 恒判"空闲"(方向相反:会往正在跑的会话里插话)。 ✅ 改用命名组(?P<busy>…),并加源码级反回归断言(_session_busy段内零位置组引用)。 ⚠️ 这条断言不是"看代码像不像" —— 它是唯一能在这类错位再发生时立刻报红的东西。 - (b) 同一秒内"后出现的没胜出":只比
t > last[0]⇒ 同一秒(甚至同毫秒)的两条, 前一条胜出 ⇒ 取到旧状态。✅ 改成比较(时间, 行序)二元组。 - (c) 我自己的红绿对照脚本是空的:变异体跑的是
-k 忙判据,而那条target-busy用例的名字里没有"忙判据" ⇒ 变异体根本没被试,报"全绿" ⇒ 差点当成"断言恒真"。✅ 过滤词改成能覆盖两条用例的词。 ⇒ 🔴 判据:变异法跑完必须回读"这个变异体到底被哪几条用例跑了",⛔ 不看"合计 PASS"就当证毕。
- (a) 正则位置组错位:写成
-
残留(⛔ 不是本条的修法能解决的):判据换对之后,拦截原因从
target-busy变成no-follow-session—— 那是真状态:此刻一条活的[跟进]会话都没有 (e2ccdea3跑完变completed就退出了网关的活会话集)。⇒ "推"只在目标活着时能用; 目标不在线时靠排期到点拉起一条([跟进]-…每小时,nextRunAt23:21:35)。 -
形状:看板是一个常驻 HTTP 服务(
board.py --serve <port>),代码载入内存后一直跑。 于是改完board.py/assets/board.html/board_ext.py,页面上一点变化都没有 —— 服务还是改之前那份代码,它不会自己重载。 -
实测:用户报「看板中协作会话区域还是看不到协作会话」。反复核对页面与代码都对得上, 最后发现病根是进程:服务是 20:24:38 起的,而改代码发生在 21:xx ⇒ 它返回的
board.json是旧结果([跟进]-…被标成「主会话」、只 2 条会话)。 按文档姿势--takeover重起后 ⇒ 6 条会话、三类齐全(3 条执行会话可见)。 -
判据(⛔ 不看"页面像不像"):
- 直接读
/board.json:看ts(快照时间)是不是刚才、会话条数、每条会话的类别, 再与磁盘上的真实快照(宿主库/sessions表)逐条对照。 - 🔴
--takeover是硬要求 —— 同 P0-9:Windows 允许同端口重复绑定且不报错 ⇒ ⛔ 不--takeover就会静默并存,你看到的很可能仍是旧进程画的那张图。
- 直接读
-
自查:凡"改完看不到变化" ⇒ 先问"我看的是哪个进程画的图",⛔ 别先怀疑自己改错。 ⚠️ 同族:凡改完看不到效果的服务(看板/沙箱实例/常驻),一律先查进程启动时间 vs 改动时间。
P0-16 🔴 每轮重写同一条告警 ⇒ 噪音 + 静默抹掉「人写的回信口」(★ 2026-10-01 实测)
- 形状:
need_user()遇阻就落NEED-USER.md。而它在常驻里每轮(~10 s)都会被走到 ⇒ 每 10 秒整体重写一次,时间戳一直变(现场:22:06–22:12 之间被刷了 20+ 次)。 人看到的是「这条消息一直在喊、一直在变」⇒ 当噪音忽略掉。 🔴 副作用更严重:人手工在文件里写了「## 解除条件」——那是人唯一的回信口 —— 下一轮整体重写把它静默抹掉 ⇒ 人写一次被抹一次,就再也不写了(实测:21:03 手写的当场被覆盖)。 - 判据(两条纪律,都落在文件上):
- 同一句话要节流:窗口(
DSH_NEED_GAP,默认 600 s)内同原因 ⇒ 不重写(⛔ 不刷屏)。 ⚠️ 判据必须落在文件上(⛔ 不是内存)—— 钩子每次都是新进程,内存节流跨不了进程。 - 人写的内容必须原样带走:重写时把
## 解除条件及其后整段保留。
- 同一句话要节流:窗口(
- 🔴 为什么这条难自己发现:程序侧"我明明喊了"是对的 —— 问题出在喊的姿势(频率 + 覆盖), ⛔ 任何"写成功了吗"的断言都查不出来。⇒ 得换问法:"人看到这条会怎么想?"
- 判据落地:
selftest.py::t_need_user_throttle_keep(4 项),已按 P0-13 用变异法证明非空: 打掉节流 ⇒ ③ 报红;打掉保留 ⇒ ④ 报红。
P0-15 🔴 判「样式对不对」时去查内联属性 ⇒ 两条假红(样式其实写在 CSS 里)(★ 2026-10-01 实测)
- 形状:给新画的「分组大框」加样式时,
.grp{fill:none;stroke:…;stroke-dasharray:8 7}写在<style>的 CSS 规则里; 而自检脚本去渲染产物的<rect class="grp" …>标签上找stroke-dasharray/fill=的内联属性 ⇒ 必然找不到 ⇒ 报「分组框不是虚线」「分组框有填色」——两条假红(两条都是"我从没见过这个属性"导致的)。 - 实测:几何自检
tmp/arch-geom-check.mjs第 ⑦ 段新增两条,当场双红; 改成回源文件读.grp{…}那条 CSS 规则后转绿。拿改前备份跑同一套 ⇒ 8 红(符合预期)。 - 判据:
- 先想清楚"这个样式从哪来"再写判据 —— 三种来源要分开查:内联属性 /
<style>里的类规则 / SVG 的 presentation attribute 默认值。⛔ 别默认"属性应该挂在标签上"。 - 🔴 判据要能在改前报红 ⇒ 参与对照的备份文件路径必须跟着一起换(本轮靠
BOARD_HTML环境变量指过去)。 ⚠️ 备份里根本没有.grp规则 ⇒ 报红正是预期,⛔ 别看到红就以为判据坏了。
- 先想清楚"这个样式从哪来"再写判据 —— 三种来源要分开查:内联属性 /
- 自查:新增一条「样式类」判据时,问一句「这条规则写在哪个文件里?我读的是那个文件吗?」
P0-14 🔴 替换注释块时漏掉结尾 */ ⇒ 整段注释吞掉后面代码,而「数括号」查不出来(★ 2026-10-01 实测)
- 形状:用编辑工具换掉一段
/* … */注释时,替换的两端都没带上结尾*/⇒ 新注释与它下面那一行代码粘连成一条注释,从那行往下的代码全被吃掉。 - 实测:
assets/board.html改完,node --check只报一句SyntaxError: Unexpected token '}', 并说「多余的}在 772 行」—— 而真正的病灶在 ~1000 行(那里少了一个*/)。 当场表现为渲染断言 35 条全红,看着像"改坏了半个文件"。 - 🔴 为什么这个坑难查:第一反应是数
{和}的个数。没用 —— 被吞掉的那段里本来就有{,它从「代码」变成了「注释里的字符」⇒ 两边计数照样相等; 同理,字符串字面量里的}(含中文引号包住的文案)也会干扰朴素计数。 ⛔ 别用字数统计判括号平衡。 - 判据(两条,任选):
- ✅ 最省事:改完立刻跑一次语法检查 —— JS
node --check <f>/ Pythonpython -m py_compile <f>。 ⚠️ 本坑当时确实跑了node --check也报了错,只是它报的行号把人带偏了 ⇒ 报了错也别只信行号。 - ✅ 要精确定位:用能识别字符串/注释/正则字面量的扫描器做嵌套深度统计 ——
本轮落在
tmp/_brace4.js。它同时跑「改前备份」与「当前文件」:备份depth=0、当前depth=-1⇒ 一眼看出「多的不是括号,是注释没闭合」。
- ✅ 最省事:改完立刻跑一次语法检查 —— JS
- 自查:凡改动跨
/* … */边界 ⇒ 必跑语法检查 +(大文件)深度扫描器对照备份。 ⛔ 别只信「我只改了一处、不可能影响别处」。
P0-13 🔴🔴 断言在「空样本」上恒真 ⇒ 看着绿其实从没执行过(★ 2026-10-01 实测 · 空样本一出现就翻红)
- 形状:
arr.every(...) === true这种写法,arr为空时恒真 ⇒ 用例一条都没验却是绿的。 🔴 与 P9「校验"可用"时只看"能跑起来"」同族,但更阴:它不是不严,是根本没跑。 - 实测:
tmp/render-check.mjs里那条「唤醒会话/跟进会话⛔ 不许混进第三层」写成_notWorker.every(s => !arch.includes(s.id8))—— 而当时线上一条唤醒/跟进会话都还没有 ⇒_notWorker恒空 ⇒ 恒真。⚠️ 当天真建出唤醒会话后它立刻翻红, 而且红得没道理:它查的是「整张图里有没有这个 id」,而唤醒会话本来就该画在主会话左侧 (那是它的正式位置)⇒ 同一处两个毛病:空样本假绿 + 判据写错了地方。 - 判据(两条):
- 空样本不许算通过 —— 要么 SKIP(像本文件里"前提不成立就 SKIP"那几条),
要么用合成样本把分支逼出来(
render-check.mjs的unrecBad/retBad/wlBad三个注射块就是这个手法); - 🔴 断言必须能"改前报红" —— 拿改前备份跑同一套桩做红绿对照; 备份已被后续改动覆盖时,用变异法(把判据回退成旧写法,看它是否报红)。
- 空样本不许算通过 —— 要么 SKIP(像本文件里"前提不成立就 SKIP"那几条),
要么用合成样本把分支逼出来(
- ⚠️ 连带教训(同轮同时踩到的第二大坑):别把期望值写死在断言里。
同一批断言里有 4 条写死了旧数据(线名
ai1net-dsh-、类别值唤醒机制), 线上数据一换就恒红(假红)—— 假红会把真红淹掉(实测实时快照 5 红里 4 条是假红)。 ⇒ 期望值一律从样本现算(_lgNames/_sessTopics那种写法)。 - 自查:跑一遍断言,数一下每条真跑过没有(样本数 > 0?分支可达?);再数红的有几条是真的。
P0-12 🔴 **写 .py/.json 时嵌了 ASCII 双引号 ⇒ 截断字符串/破坏 JSON,而且多半是「静默」(★ 2026-10-01 一天犯 3 次)
- 形状:在人写的自由文本里用 ASCII
"…"(例如本图管"该做什么")。同一句下去,两种后果: · Python:"…用"该做"…"⇒ 字符串在第二个"处截断 ⇒SyntaxError(好抓); · JSON:字符串非法闭合 ⇒json.loads抛Expecting ',' delimiter,而调用方往往把异常吞掉 (except Exception→ 只写一行日志,rc仍是 0)⇒ 命令看着成功、文件其实没被读进去。 🔴 实测:目标检查读不到任务图 ⇒NEXT.md静默不产生 ⇒ 四道闸门第一道就断(排查绕了一圈才发现)。 - 一天犯的三次:①
board_ext.py的文案(截断 Python 字符串);②selftest.py的用例描述(同款); ③交付物/任务图-会话协作自检.json的notes字段(破坏 JSON)。 - 判据(一条):凡写
.py/.json,落盘后立刻验一遍 ——python -m py_compile <f>/json.loads(...)。 ⛔ 不许靠「我看着对」;⛔ 更不许因为「命令rc=0」就认定写成功 —— 吞异常的调用方会让它假绿。 - 怎么写:中文引号一律
「」/『』;确需 ASCII 引号 ⇒ 用转义\",或在 Python 里换外层引号。 ⚠️ 这不是「手滑」,是缺一道工序(写完多跑一次编译/解析,成本趋近 0)。
P0-11 🔴 「接续」被当成角色 ⇒ 主会话的接续被判 worker ⇒ 主会话候选 = 0(★ 2026-10-01 实测 · 用户口径点破)
- 用户口径(2026-10-01 原话):「接续会话 不是单独的一类会话,是这几类会话到达阈值时 创建的接续会话」 ⇒ 接续是形态,不是类别 —— 它继承被接续那条的角色(主会话的接续仍是主会话,执行棒的接续仍是 worker)。
- 现象(本机实测,一刻钟内可复现):
_scan_mains()返回cand=0 / named=0⇒resolve_main()返回{"sid": "", "source": "no-register"}⇒ 投递解析不出主会话(no-main-session)。 - 逐条读数(本工作区 8 条会话,全部判 worker):
接续 · 会话机制合并包 · 任务4b-4d-6/接续 · 会话机制合并包 · §9.3 与遗留收尾/接续 · 会话机制合并技能包(任务 2/3/4 + 归档)/[接续] 会话机制合并技能包(第 1 棒)/接续棒:日志增长治理(任务 0 → 任务 A)/[唤醒机制] 接续 · 日志事前叫停钩子落地(第 2 棒)/[唤醒机制] 接续 · 规则形态入口化接线 + 技能整合(第 3 棒)⇒form=continuation、 以及[协作]-[唤醒机制]-日志事前叫停钩子落地(第4棒)⇒form=prefix—— 全部role=worker。 - 根因:
parse_session_name()里「标题含『接续』⇒ 一律role="worker"」, 而_scan_mains()的排除判据是「角色不是 worker/waker」⇒ 主会话的接续被当干活的棒排掉。 - 🔴 为什么会写成这样(不是随手写错):当初是为了堵自指死结 ——
[<类别>] 接续 · …的第一对方括号装的是类别,不收成 worker 就会被_topic_in_title()认成 "该类别的主会话" ⇒ 通知投给它自己(已实测坐实)。⇒ 兜底方向对,但把主会话的续棒一起吞了。 - 🔴 真正的根因是"信息不足",⛔ 不是"解析器不够聪明":
continuation形态的标题里根本没有角色信息 ⇒ 解析器无从继承。⚠️ 实测反证:主控 · 机制线(…)⇒ 判main✅ —— 说明只要标题带主控就没问题, 坏的是"规范没被执行"(实际建出来的主会话续棒写成了接续 · <线> · <具体>,没带主控)。 - 两条正解(⛔ 都属方案变更,先拍板再动):
- 补登记(零代码改动):
collabd.py --declare --role main登记该会话 ⇒ 走resolve_main()第①层。 ✅ 立刻可用;⚠️ 登记是静态的,换主会话后不会自己变。 - 改判据 + 收紧命名(治本):
continuation不再无条件判 worker —— 带主控⇒main;否则 ⇒ 角色未知(点名,⛔ 不猜)。⚠️ 必须同时堵自指死结(缺角色方括号 ⇒ 一律不进候选、只点名)。
- 补登记(零代码改动):
- ⛔ 反面判据(同族老毛病):「自测全绿 ⇒ 判据没问题」—— 错。本缺口在自测里完全看不出来
(用例只喂人工样本,不读宿主库真标题)。⇒ 判据只能是拿宿主库真标题跑一遍并看
cand计数。 - 自查命令(复制即用):
COLLABD_CONFIG=<使用方>/collabd.config.json python -c "import importlib.util;…"—— 逐条打印parse_session_name(title)['role']+_scan_mains()['cand']长度。 ⚠️ ⛔ 不带 config 跑会得到cand=0的假读数(连_same_ws都比不上,是"没配置"而不是"判据坏了")。
P0-10 🔴 脚本被重定向 ⇒ 本地 GBK 编不出 ⛔ ⇒ print 抛异常 ⇒ 顶层记 fatal、整轮失败(★ 2026-10-01 实测)
- 现象:
<工作区>/tmp/supervise-inbox/_collabd.log与技能包内同时出现fatal 'gbk' codec can't encode character '\u26d4' in position 0(本机实测连续 4 次), 且包里长出了tmp/supervise-inbox/。 - 两个真因(同一条链,都要修):
- 🔴 钩子把技能包当工作区:
wb-result-hook.py的_WS_ROOT原按3×dirname(__file__)推 —— 包内这份(<包根>/scripts/hooks/)推出来正好=技能包自己 ⇒ 交给collabd的COLLABD_WORKSPACE是包目录 ⇒ 运行产物落进包里。同文件里本来就有正确的resolve_ws()(env 优先 + 内容标志上溯)⇒ 这不只是"推错",是同一事实有两套实现。 ✅ 改为复用resolve_ws(),并加「未知工作区 ⇒ 停手」(⛔ 不拿 cwd/包目录顶替)。 - 🔴 重定向 ⇒ 编码崩:
collabd拒跑时会print("⛔ 未找到部署配置…");钩子用stdout=DEVNULL起它 ⇒ 子进程 stdout 按本地编码(GBK) ⇒⛔编不出 ⇒UnicodeEncodeError⇒ 被顶层except记成fatal⇒ 整轮失败。 ✅ 7 个非钩子脚本文件头加输出编码兜底(stdout/stderr改 UTF-8 +errors="replace")。
- 🔴 钩子把技能包当工作区:
- 🔴 为什么这条特别要紧:常驻必须把 stdout 全重定向到文件(本机唯一可行的"长活"载体)
⇒ 不修这条,常驻一定起不来 —— 它会死在第一句带
⛔的输出上。这不是边角,是拦路石。 - ⛔ 反面判据:「外层
rc=0⇒ 没崩」—— 错。钩子把子进程输出全丢弃 ⇒ 外面只看到rc=0, 而日志里已经是fatal。判据只能是看日志里有没有fatal+ 看包里有没有长出tmp/。 - ✅ 处置(已落地):修根推导 + 加编码兜底 +
collabd.log()在「未找到配置」时拒写任何文件 (对齐它自己"不落任何文件"的承诺)。验证:修后同一路径连跑 0 次fatal、包内保持干净。
P0-9 🔴 Windows SO_REUSEADDR 允许同端口重复绑定且不报错 ⇒ 多个看板服务静默并存(★ 2026-09-30 实测)
- 现象:用户说「要把其他看板服务关了 避免打架」;此前还连问两次「看板还是没有把会话识别为主会话」—— 即改了代码、重启了看板,用户看到的却还是旧的。
- 根因(实测,不是推断):
board.py --serve用ThreadingHTTPServer,它继承allow_reuse_address = 1(=SO_REUSEADDR)。Windows 上这个选项允许两个进程绑同一个127.0.0.1:8788而不报WSAEADDRINUSE⇒ 谁都可以起、同一 URL 被随机应答 ⇒ 快照/代码版本互相打架。 · 判决性实验:8788 已有看板在跑时,再起一个 ⇒ 照样打印「看板已起」,rc=0/无报错。 · 现场证据:看板日志连续 6 行「看板已起」中间零报错。 · ⇒ 于是"改了看不到" ≠ 改错了,而是请求打到了另一个进程。 - ⛔ 反面判据:「日志里没有
10048⇒ 没有重复实例」—— 错。没有报错恰恰是这个坑的特征。 - ✅ 处置(已落地):
board.py --serve加单实例护栏 —— 起之前探/healthz,认签名 (body 同时含"ok"与"snapshots";⛔ 不能只认"端口开着"、⛔ 不能只认 HTTP 200)⇒ 已有看板则拒绝启动; 换新代码用--takeover(先停旧的再接管)。停机入口stop-collab.py加 ④ 看板服务 (按端口逐个探签名,把所有实例列出来并停)。 - ⛔ 别顺手把
allow_reuse_address关掉:进程被强杀时服务端会留下TIME_WAIT⇒ 不设 reuse 会导致 重启必失败。护栏放在"起之前探测"这一层,⛔ 不动 socket 选项。
P0-3 🔴🔴 ⛔ 不许拿"排期"去撑投递(用户:"又给我整到自动任务去了")
🔴 2026-10-01 订正:本条原写「投递⛔ 不许靠"加一条排期**/常驻**"」—— "常驻"那一半是错的。 投递本来就该常驻(09-29 定案,2026-10-01 用户再确认);用户当时否定的只有"用排期撑投递"。以下保留原始推理,结论以本框为准。
- 现象:反复把"怎么把通知送到主会话"这件事,收敛成"开一条自动化排期"。用户对此明确不满(原话:「又给我整到自动任务去了」)。
- 根因:把「投递要有口令」错推成「必须有排期」⇒ 把可选手段当成了唯一手段。
- 为什么排期不对:排期烧 token,且会让判断分裂到排期那一侧 ⇒ 用户要的是"唤醒永远只有目标检查一个出口"。
- 修法:✅ 投递 = 常驻进程按周期跑(
collabd.py --supervise);⛔ 不建"当闹钟"的自动任务(2026-10-01 用户:「定时任务的方案已经废弃了」)。 - 🔑 推广:先问"这个能力宿主已经在哪里提供了?"再问"要不要新增常驻" —— 但**"常驻"本身不再是禁忌**: 判据=「它是不是唤醒时钟这类必须有的东西」;⛔ 拿"进程数 = 0"当理由去否掉时钟,2026-09-30 已经踩过一次(用户点破「投递的心跳成摆设了」)。
- ⚠️ 载体选择(2026-09-30 用户追问"那用会话后台任务当守护+投递行不行?")⇒ ✅ 能用,且是本机唯一可行的载体:
实测
detached spawn活不过工具调用边界、schtasks被内置程序黑名单硬拦 ⇒ 唯一解 = 宿主后台任务 +stdout全重定向。 ⚠️ 代价如实讲:会话后台任务会压制它所属会话的 idle 钩子(实测被僵尸任务压 6h20m)⇒ 载体要用专用容器会话。
P0-4 🔴 两类「静默丢件」:程序在跑,主会话什么也没收到(2026-09-30 同轮修掉)
- 型一:投影轮"消费掉却不投递"
· 现象:队列里有待反馈项,程序每轮都在跑,主会话一次都收不到。
· 根因:钩子在
UserPromptSubmit上先跑--once(节流 3 分钟,谁发话都会跑);而--once也调supervise(), 它无条件把待反馈项标记为"已通知"并从 pending 摘掉 ⇒ 紧接着的--tick看到空队列 ⇒ 永远不发。 · 修法:✅supervise(deliver=False, mutate=False)—— 投影轮只算、只写TO_MAIN.md,⛔ 绝不推进队列; 只有投递方(--tick)才推进。(mutate参数见architecture.md §4.2) - 型二:投递被挡下,却仍标记"已通知"
· 根因:
_deliver_str可能因target-busy(目标会话正在执行)/too-soon(距上次 <wake_min_gap)/locked(跨进程互斥)/no-token而放弃投递;旧代码不看返回值就notified[kid]=stt并摘队列 ⇒ 这一条从此消失(既没送到、也不再重试)。 · 修法:✅ 未投出 ⇒ 队列原样保留,下一轮重试;唯一例外=same-item(内容哈希逐字相同 ⇒ 主会话本就收到了)。 - 验收(怎么分辨"真绿"和"看起来绿"):⛔ 不看
rc=0,看wakeups.jsonl有没有新增一行ok:true+tasks.json里的项是否还在 pending。自测里已固化三条用例(纯投影不消费/投不出不消费/--tick在位且唯一)。
P0-8 🔴 collabd.py 被多个会话并发调用时的冲突面(★ 2026-09-30 · 用户问「多会话同时调用会冲突吧」)
逐条给判据(⛔ 不含糊)
| 调用路径 | 写什么 | 有锁吗 | 结论 |
|---|---|---|---|
投递(--tick / --supervise) |
wake.lock + 网关 reply |
✅ 有(跨进程互斥 + 内容哈希 + wake_min_gap) |
✅ 不会重复投递(今晚整晚无成对记录) |
--report / --reconcile |
读改写 tasks.json |
🔴 无锁 | ⚠️ 可能丢更新:两条上报精确同时 ⇒ 后写覆盖前者 ⇒ 台账少一条 |
--tick / --once / --supervise / --declare |
整份覆写 collabd-state.json |
🔴 无锁 | ⚠️ last-writer-wins ⇒ 可能丢 notified/notify_pending 等字段 |
--ready-next / --reqs / --where |
只读 | — | ✅ 安全 |
风险评估(如实):--report 是毫秒级写小文件,且棒通常不会精确同时上报 ⇒ 实际概率低,但不是零。
✅ 立刻可用的规避(⛔ 不改代码):上报后回读核对 ——
<python> collabd.py --report N9 --state done --by "[协作]-…" --line <线> --artifact <产物>
<python> collabd.py --reqs # ← 回读:确认自己那条在、状态对(防被别的上报覆盖)
⚠️ 若回读发现自己那条被覆盖/丢失 ⇒ 立刻重报(把它写回去),并在上报里提一句。
🔴 未解决(如实登记):并发写锁还没做。
⚠️ 我 2026-09-30 06:06 试过一次(换成 OS 级文件锁 msvcrt.locking + 给 6 处 state 写加锁)⇒ 导致 --once 与 --report 全部卡死(rc=124 超时) ⇒ 已从备份回退(tmp/bak-concurrency-20260930-060649/)。
⇒ 结论:这个改造必须在"隔离环境先验证 msvcrt.locking 行为"之后再做,⛔ 别在"用户在等"的状态下赶工(本次教训)。
⇒ 正确的下一步:① 先写一个两进程并发压测脚本(隔离目录)② 验证锁真能互斥且不卡 ③ 再改进生产。
P0-7 🔴🔴 宿主推给前端的「会话元数据」是陈旧缓存** ⇒ 前端与实际不匹配 ⇒ 用户消息静默蒸发(★ 2026-09-30 实测定型 · 用户凭直觉指出,被证实)
- 症状(用户原话):「我发消息发不出去卡住,看上去是发出去了 实际没有(这种情况消息应该出现在待发送框中)」
- 用户的关键判断(✅ 被证实):「我的感觉是会话状态不对,导致前端界面和会话实际动作不匹配」
取证链(三条,全部实测)
| # | 读数 | 说明 |
|---|---|---|
| ① 消息确实丢了 | 转录里搜用户原话关键词 ⇒ 只有他重发的那条(06:01),05:57–06:00 那条完全不存在;同期 PromptIterator 也没有 |
⛔ 不是队列(不是 park)、⛔ 不是服务端丢 ⇒ 丢在「客户端 → 宿主」这一跳 |
| ② 前端拿到的是旧元数据 | 宿主 [AcpView] Sent session_info_update with title: **用powershell 运行 试试呢**而 DB 里 sessions.title = 接续 · 机制线(钩子锚点真实投递取证) |
🔴 不一致 |
| ③ 且长期停在旧值 | 05:51:37 / 05:53:04 / 05:54:16 / 05:57:51 / 06:02:15 ⇒ 五次推送全是同一个旧标题 | 不是瞬时抖动,是缓存陈旧 |
⇒ 结论(⛔ 严格划清"可证"与"未证" —— 别学 P0-2 的老毛病)
| 内容 | 状态 | |
|---|---|---|
| ✅ 可证 | ① 用户那条消息确实丢了(转录、PromptIterator 双无)⇒ 丢在「客户端 → 宿主」这一跳(服务端无任何记录)② 宿主推给前端的元数据是旧的:推送 title=旧值,DB sessions.title=真值,五次推送同值 |
已实测 |
| ⚠️ 未证 | ② 是否就是①的原因("陈旧元数据 ⇒ 发送走偏 ⇒ 蒸发")—— 我没有任何直接证据 | 🔴 ⛔ 不得当结论说(这正是 P0-2 的教训:相关性 ≠ 因果) |
源码级补证(app.asar 实读):session_info_update 的 schema 定义写着是 "update session information like title"、由 agent 侧推送(⛔ 不是前端自己算的)
⇒ 所以"推旧值"确实不对(它本应反映当前会话名)⇒ 但"它导致了消息丢失"仍未证。
- 🔴 归属(如实):这是产品侧缺陷(宿主 ↔ 前端的状态同步)。 ⛔ 机制侧看不到、也修不了(消息根本没到服务端 ⇒ 服务端无任何记录 ⇒ 对账也发现不了)。 ⇒ 机制侧唯一能做的是降低触发概率(例如本次已把唤醒从"定期噪音"改成"真停滞才发",减少主会话 busy 占比)。
- ✅ 可做的缓解:重启 WorkBuddy(清掉陈旧元数据缓存)。
⚠️ 代价:会杀掉所有会话后台任务(覆盖网络节点中继客户端/设备接入本地反代/投递守护)⇒ 重启后必须按
deploy.md §5b重起那两条腿。 - 📌 上报要点(给产品):附 ①转录缺失 ②
session_info_update与sessions.title的对照 ③五次同值的推送时间线。 - 🔑 推广:"消息发出去了但没到",先查三处——① 转录有没有(有没有进会话)②
PromptIterator有没有(有没有进队列)③ 宿主推给前端的元数据是不是旧的(前端与实际是否一致)。⚠️ ⛔ 别只数"成功的条数"就下结论"没丢"(本次我犯过:只统计到 14 条全成功,却没核对"应该有多少条")。
P0-6 🔴 宿主 SafeDelete 护栏会"杀掉"高频删文件的常驻进程 —— 唤醒时钟断掉的真正原因(★ 2026-09-30 01:50 实测 · 2026-10-01 再复发并治本)
- 现象:常驻投递进程(已停用)跑约 48 分钟后
failed(后台任务Syz5DD,Duration: 48m 43s),唤醒从此消失;stderr为空、stdout只有一行。 - 读数(⛔ 不是推断,是 stdout 原文)
[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {"count":50,"threshold":50,"scope":"turn", "targets":["E:\\…\\tmp\\supervise-inbox\\wake.lock"],"targetCount":1} - 根因:
_deliver_str每轮尝试都会在finally里unlink(wake.lock)释放锁; 而宿主有 SafeDelete 批量删除护栏 —— 按"本轮删除次数"计数,第 50 次即要求确认并拒绝 ⇒ 进程被终止。 ⚠️ 不是"锁写错了",而是"释放锁的方式是高危操作"。日志里成片的投递未成(too-soon)⇒ 绝大多数轮次都在空转取锁+删锁。 - 第一次修法(2026-09-30 · ⚠️ 事实证明不充分**):把 hash / 最小间隔的预判提到取锁之前 ⇒ 高频的
same-item/too-soon路径根本不碰锁文件**。 ⚠️ 取舍如实登记:锁内仍会重读 STATE 再判一次(那才是权威判据),竞态窗口由wake_min_gap(300 s)兜底 ⇒ ⛔ 不会双投。 - 🔴🔴 2026-10-01 复发 ⇒ 才找到真因:上面那招只在"锁不需要取"时才省掉删除;一旦每轮内容都在变
(队列头/告警读数天天动 ⇒ 哈希轮轮不同),"快速否决"一次都命中不了 ⇒ 每轮都取锁 ⇒ 每轮删一次锁文件。
实测读数:常驻 20:52 起、20:56 查已无此进程(
Get-CimInstance全机只剩看板那一个 python);_supervise-bg.out.log原文{"count":50,"threshold":50,"targetCount":1,"targets":[…\wake.lock]}。 ⇒ 换成 ≈10 s/轮 ⇒ 50 次删除 ≈ 8 分钟就被杀。"降到只在可能真投时"这个判断本身就是错的: 真投的判据是"内容变了",而内容会一直变。 - ✅ 治本修法(2026-10-01 已落):锁文件永久存在,释放 = 原地改写内容(
pid↔free)⇒ 零删除。 · 判据随之从「文件在不在」改成「内容 + 陈旧度」(内容为空/free/mtime > 120 s ⇒ 可抢); · 原地写非原子 ⇒ 并发读可能看到半截串 ⇒ 那不算free⇒ fail-closed(宁可少投,⛔ 不乱投); 抢锁后回读核对(本项目既有的乐观重试手法)⇒ 核对不上就不算拿到。 · 🔴 回归(两条,缺一不可,且做了变异法红绿对照):selftest加 ①「锁已释放(不存在 或 内容=free)」 + ②反向「_deliver_str体内 ⛔ 不再出现_lk.unlink」(源码级判据,且要求源码真读到, ⛔ 不许getsource失败时恒真),③ 新增真互斥例「别的进程持有时 ⇒ 不投(locked)」。 ⚠️ 为什么非要那条反向的:只判"已释放"的话,旧写法(每轮 unlink)照样绿 —— 变异法实测证实: 把unlink放回去,① 仍 ✓、② 报红 ⇒ ① 单独存在=空判据(P0-13 同族)。 改后 PASS 43 / FAIL 0;变异体 PASS 42 / FAIL 1。 - 🔑 推广(比本条重要):常驻进程里 ⛔ 别做高频的"文件删除/改名/清目录"(
unlink/rename/rmtree)—— 护栏按次数计,⛔ 不按"意图好坏"。要"释放/标记"优先改内容或改时间戳(写标记),⛔ 不用删文件; 非删不可 ⇒ 改成低频/惰性,并把"删了多少次"记进自己的日志(否则你只会看到"莫名 failed")。 ⚠️ 🔴 更狠的一条:"降低频率"不是修法 —— 只要有一条"每轮都会删一次"的路径,它早晚会凑满 50 次。 判据是「稳态下每轮的删除次数 = 0」,⛔ 不是"比以前少"。 - 复发信号(下次一眼认):① stdout 出现
SAFE_DELETE_BULK_CONFIRM_REQUIRED;② 常驻莫名 failed 且 stderr 为空; ③ 存活时长 ≈ 8 分钟(10 s/轮 × 50)或 ≈ 48 分钟(取决于是哪条删除路径在凑数)。 ⚠️ 旧版进程被杀时wake.lock会残留(01:49 留过一个)—— 它 >120 s 会被自动抢占,⛔ 不必手删。 ⚠️ 新版(删除-free)锁文件会一直躺着、内容是free⇒ 那不是残留,⛔ 别当故障去删它。
P0-5 🔴 「消息卡住」的指纹 = parkInQueue + hasWaiter=false(★ 2026-09-30 实测定型 · 用户给了窗口 22:55–23:20)
- 症状:界面上发了消息,一直转、没有回复;会话没有任何执行迹象。
- 指纹(一条 grep 就能认)——工作区日志
<日志根>/<日期>/<工作区名>__*.log:grep -E "PromptIterator\].*route=|No state found for connectionId" <该日志>读数 含义 route=resolveWaiter … hasWaiter=true✅ 正常:有人等 ⇒ 立即执行 🔴 route=parkInQueue … hasWaiter=false消息入队但没有消费者 ⇒ 会一直停着(=用户看到的"卡住") [ACP StreamManager] sendToClient: No state found for connectionId=…客户端连接状态丢了 ⇒ 同源旁证 - 本次实测(2026-09-29 · 主会话
fe146dd9)时间 route queueLen hasWaiter 22:53:00 parkInQueue 0 false 23:05:01 parkInQueue 1 false 23:05:37 parkInQueue 2 false 23:12:16 resolveWaiter 0 true ⇒ 队列被排空、恢复 ⇒ 队列 0 → 1 → 2 逐条堆积、无人消费,直到 23:12:16 客户端重新挂上(宿主 pid 同时换到新实例)。 - 定量旁证(这才是"指纹"的分量):全天
hasWaiter=false只有 3 次,全部落在这 25 分钟里; 同一小时No state found for connectionId303 次,其他小时 0 次 ⇒ 客户端连接状态在该小时反复丢失。 - 与相邻几型的区别:②f 有中断证据 · ②g 工具在循环 · ②h 干完了推不出去 · 本型=消息在队列里、没有消费者(
session-mechanism(references/forensics.md§3))。 - ⚠️ 窗口里可能同时叠着另一层:本次 22:58:40–22:59:50 还有
diagnostic log write failed(droppedLines496→2580)—— 那是日志层, ⛔ 它不解释"消息卡住",别把两层混成一个因(同类错误见 P0-2 的教训)。 - 🔴 对"程序化投递"的直接后果(本机制必须知道):经
/api/v1/acp/网关reply投进去的消息,本来就没有 UI 等待者 ⇒ 天然带hasWaiter=false风险。⇒ "往正在执行的会话投要延后"(target-busy)+"同一内容成对重复投要拦"(wake.lock+哈希+wake_min_gap)不是可选项。 ⚠️ 但本次这 3 条是adopt upstream promptRequestId,⛔ 不是本机制投的(wakeups.jsonl显示本机制当日最后两次投递是 22:51:01)—— ⛔ 别把这次卡住算到投递头上(教训同 P0-2:先要实物,再定因果)。 - 处置:① 先按指纹确认是不是这一型;② 是 ⇒ 让客户端重新挂上该会话(切走再切回/重开该会话窗口)通常即可排空; ③ ⛔ 别反复发消息试探(那只会往队尾再堆一条,延长卡住时间)。
P0-5a 🔴 探针⛔ 不许"在日志里搜字符串" —— 它会命中你自己的取证回声(★ 2026-09-30 当场踩到)
- 现象:刚做好的 park 探针立刻报了一次假命中(
park=1 次 最近 00:36:15),而那一刻并没有任何会话在卡。 - 根因:取证命令的输出会被写进同一份工作区日志 —— 我为了查这个坑跑了一次
grep … parkInQueue …,宿主把它记成[SandboxShell] ProcessOutput … content=… route=parkInQueue … hasWaiter=false。 探针只要"行里同时含这两个子串"就命中 ⇒ 命中了自己的回声。 - 修法:判据必须只认真正的记录行,并显式排除回显行:
def _is_park_line(ln): return ("parkInQueue" in ln and "hasWaiter=false" in ln and "[AcpView][PromptIterator] received prompt" in ln and "ProcessOutput" not in ln and "content=" not in ln) - 🔑 推广(比这条本身重要):任何"扫日志找关键字"的探针都有这个自污染风险 —— 只要有人(或你自己)为了排查而把那个关键字
打进日志一次,探针就会永久看见它。⇒ 一、认结构不认词(要求"记录行"的固定字段组合);二、排除回显容器
(
ProcessOutput/content=/Sandbox);三、必须先拿一条真记录 + 一条回声行做对照用例(已固化进selftest.py)。
P0-5b 🔴 本轮新增的两道自动护栏(collabd.py --tick)
| 护栏 | 做什么 | 判据(⛔ 不看 rc,看这个) |
|---|---|---|
宿主卡住探针 probe_host_park() |
每轮只读日志尾部 400 KB,找 park 指纹(最近 30 分钟内才算)⇒ 落 NEED-USER.md(含"切走再切回"),节流 10 分钟 |
打印 tick: … park=<n> 次 最近 <HH:MM:SS> |
投递消费回查 check_delivery_consumed() |
投递成功后不当作成功:4 分钟(CONSUME_GRACE)内目标会话 updated_at 没晚于投递时刻 ⇒ 判「没被消费」⇒ 落 NEED-USER.md |
打印 tick: … consume=waiting/consumed/unconsumed |
🔴 口径:「投出去」≠「它跑起来了」 —— 网关回 200 只证明对方收下,不证明有人执行。 ⚠️ 这两条护栏只能"发现 + 告诉用户点哪一下";根因(宿主客户端连接状态丢失)AI 侧修不了,⛔ 不许因此承诺"自动恢复"。
P0-2 🔴 「卡消息输出」的真因:会话日志撞上限被 dropped —— ⛔ 与"后台任务归属"无关(2026-09-30 用户反证后重写)
- 现象:会话里发消息后窗口不显示内容 / 像是没反应;用户看到的是"卡消息输出"。⚠️ 我自己(主会话
fe146dd9)就是当事人。 - 🔴 实证真因(实物在档,⛔ 不是推断):宿主的会话对话日志撞 ~10 MiB 上限后开始丢事件。
· 该会话日志尾部原文:
diagnostic-log:dropped {"droppedLines":14261,"droppedBytes":1731465}(UTC 12:06:44 = 本地 20:06:44); · 其前最后一批正常记录是同一requestId的tool_call_update在毫秒级反复落盘(本地 16:29:53,completeAssistantStream:false); · 该文件 10,485,606 字节,本地 23:00 被 logcap sweep 挪为*.log.stuck-20260929-230043;同日全机共 6 个这类满额文件。 ⇒ 高频工具事件 / 大量输出 ⇒ 日志膨胀过上限 ⇒ 宿主丢弃 ⇒ 对话不再显示。 - ⛔ 已作废的旧结论(我 2026-09-29 写的,留着会误人):曾断言"任务记在会话名下 ⇒ 宿主认为该会话一直有长跑任务 ⇒ 拖住它",
而当时唯一的"证据"只是"起守护的任务 id 随停工变
completed"—— 那只说明任务结束,⛔ 不构成因果。 🔴 用户反证(2026-09-30):mcn-short-video的:8900就是会话后台任务,一直挂着;用户在那个会话里照样随便发消息。 - ✅ 正确口径:卡不卡看"输出/事件量",与"是不是后台任务"无关。
⇒ 会话后台任务只要完全静默(
stdout重定向到文件、⛔ 不往标准输出打长跑日志)就可以安全长期运行。 ⇒ 会话内后台任务 vs 会话外起,在"会不会拖累会话"这一项上没有差别(旧表作废)。 - 处置(日志已撞上限时):把
<sid>.log改名挪开(⛔ 不删),宿主数十秒内重建并恢复写入 ⇒ 详见技能session-mechanism(references/forensics.md§4)。 - 🔴 ⚠️ 本条先后被我改错过两次:① 曾把"投递"写成"一条低频排期"(已由 P0-3 纠正);② 曾把"卡消息输出"归因到"后台任务归属"(本次纠正)。⇒ 写根因前先拿实物,⛔ 别拿"相关性 + 一个弱信号"当因果。
- 📌 本条原先内嵌一份「P0/P1/P2 常见坑速查清单」(占本条 79% 篇幅,把一条撑成 11.6 KB) ⇒ 已拆成独立文件
references/99-速查清单.md(⛔ 内容逐字保留,只搬家)。
P0-23 🔴🔴 钩子超载 ⇒ 用户每一句话都被拦下(2026-10-02 全工作区级事故,三次出手才修干净)
- 现象:
UserPromptSubmit operation blocked by hook: …wb-result-hook.py: Hook timed out after 20000ms⇒ 工作区所有会话无法执行(不是变慢,是提交失败)。 - 根因(实测读数,⛔ 非推断):
UserPromptSubmit宿主注册 20s,而该钩子在这条路上同步串联了三个子进程 ——--gap实测 13.5s |--once硬超时 12s |--tick实测 13.5s(hook.log留痕:16:12:38 done in 12819ms); 同一事件还挂着另外 5 个钩子并发抢同一颗 CPU ⇒ 均值 13.5s 的活只要抖动一次就越线。 🔴 第二大致命点(同一雷的另一种形态):PreToolUse ^Bash$上同步跑口令投递timeout=12⇒ 本机每条 Bash 命令执行前先等它。 - 为什么"修了一版"没修好(15:31 那版):引入了
_BUDGET���算 +_bg后台机制方向是对的, 但为--gap保留了_sync兜底同步分支(判据:缓存为空 ∨ 过期 >2×TTL)⇒ 最坏路径依然存在。 ⇒ 🔴 教训:在秒级预算的钩子里,"兜底同步"是伪需求。注入物是建议不是判据 —— 本轮拿不到就少注入一段(下一轮自然补上),⛔ 绝不为此等待。这是 fail-open 的正确姿势。 - 修法(三条硬规则 · 此后新增钩子一律照此办):
- 🔴 拿到结果才有用 ⇒ 才允许同步等;否则一律后台(
_bg:Popen+ stdout 落文件 +DETACHED_PROCESS,⛔ 不用 PIPE、⛔ 不 wait)。 判断句:「这个子过程的返回值,钩子会用吗?」答不上来 ⇒ 后台。 - 🔴 算术题先算再写:
宿主注册值 - 解释器冷启动(实测 0.3~1.0s,波动不可控) - Σ子过程实测耗时 > 50%⇒ ⛔ 不许同步。 本机为证:20 − 1 − 13.5 = 5.5s 余量,而同事件另 5 个钩子在抢 CPU ⇒ 必越线。 - 🔴 ⛔ 别信"实测均值",要信"硬超时":
--once均值 0.5s 看着安全,但它的timeout是 12s ⇒ 抖动时照样吃掉大半预算。 - 🔴🔴 脚本自己必须带"自身硬闸"(本次没做这一步就是没根治):前三式只修掉了这一次的那三个具体活;
任何人(含之后的另一个会话)只要再往这条路上加一个同步子进程,故障就原样复发,
而复发的第一现场是「用户说不了话」——不可能等到事后排查。
⇒ 落地:
set_budget()里起threading.Timer(daemon=True)守预算-0.5s,到点log() 后 os._exit(0)。 ⚠️ 必须daemon=True(否则解释器退出时被卡住 ⇒ 换一种形式超时);必须用os._exit(主线程卡在subprocess.run时正常退出路径走不到)。 fail-open 方向:宁可本轮少注入一段(=什么都没发生),也绝不让会话被拦下。
- 🔴 拿到结果才有用 ⇒ 才允许同步等;否则一律后台(
- 验证(⛔ 必须出厂实测,⛔ 不用"看代码觉得快了"代替):
tmp/_verify_hook_timing.py(含"缓存已被删"的最坏路径) +tmp/_probe_all_hooks.py(同事件上每一个钩子全量计时,⛔ 别只测报错点名的那一个) +tmp/_health_after_hookfix.py(止血 25 分钟后复检:常驻活着吗/后台堆积吗/耗时稳态吗/功能还在吗) +tmp/_verify_hard_gate.py(硬闸实测:注入一个 30s 同步慢活的副本 ⇒ 17.85s 自行退出,距 20s 仍有 2.2s 余量)。 修后:最坏 1.00s / 常规 0.35s(25 分钟后复检仍 1~27ms 稳态)/ 全 8 个钩子 0.23~0.65s 全绿。 - 🔴 配套发现(影响面,未处置、已上报):
wb-result-hook.py装在~/.workbuddy/settings.json(全局) ⇒ 跑在所有工作区的全部会话上, 而该文件自己的 docstring 明写「⛔ 不要装到全局」。这就是「所有会话都中招」的放大器。 止血后(0.35s)全局已无害 ⇒ 未挪动注册位置(避免影响面变更);若要收回成本工作区,改一处即可(⛔ 属影响面变更,需用户点头)。
P0-24 🔴🔴 排期「哑火」判据:补跑 ≠ 哑火,且**「延迟」⛔ 不能拿「距今」冒充**(★ 2026-10-02 实测 · S10 取证 + S11 修)
- 病灶(不是"容差不够",是"判据把两件事混成一类"):本机一次性排期不到点即时触发,由主机轮询/唤醒补跑(
automation_runs.metadata_json.runKind=missed)⇒ 实测延迟 0~11 分钟。 原collabd.py health()只写「到点未触发」一个词 ⇒ 补跑被一律判成哑火(717cd210计划 13:42、实跑 13:42:15,真延迟 0 分钟却被报哑火)。 - ✅ 判据(三条缺一不可):① 计划时刻已过 ≥ 容差(
misfire_grace_min,默认 30 覆盖实测最坏 11 分钟)②automation_runs里零记录 ③ 才判哑火;有记录 ⇒ 写「延迟 N 分钟跑完」并单列一段(⛔ 别塞进 issues,否则下一个人又照着当故障查一遍)。 - 🔴🔴 「延迟」必须 = 首次运行时刻 − 计划时刻(
min(automation_runs.updated_at)−scheduled_at)—— ⛔ 不可用「距今」代替。首版就犯了这个错:把 30 小时前跑完的排期写成「延迟 1954 分钟」(真值 7 分钟)⇒ 修判据时制造了新的假数据。 ⇒ 为此d["have"]必须从set升级成{aid: 首次运行时刻};时刻读不出就写「不猜」,⛔ 不填 0。 - ⚠️
scheduled_at实测只到分钟(2026-10-02T14:26,无秒)⇒ 解析要%Y-%m-%dT%H:%M与%Y-%m-%dT%H:%M:%S两试,只写一种会整批age=-1(我第一版就吃了这个,候选数一度算成 0)。 - 堆积是另一件事,⛔ 别和判据混:once 排期跑完不回收(
status仍ACTIVE、next_run_at=None,含义是「没有下一次」不是「未调度」)⇒ 实测 30 条已跑完仍挂在册。 清理判据 =schedule_type=once ∧ next_run_at 为空 ∧ 有 run 记录 ∧ 距计划 >30min。 ⚠️automation_update一次只能删一条 ⇒ 批量清理(29 条)⛔ 别塞进无人值守棒(不可逆 + 含主控线)⇒ 出清单、留一棒专做。 - 验证:
tmp/_verify_misfire_fix.py六档(延迟补跑/真哑火/未到点/容差内/busy 跳过/时刻读不出)+ 真实库--once后读tmp/supervise-inbox/体检报告.md⇒ 修后 issues 由「满屏假红」收敛为 2 条真哑火(87d55715、5e335e01),29 条转 notes 且真实延迟 0~11 分钟。 - 🔴 同源教训:改判据的同一轮里,我先按"读错了列"下了结论(
r[0]其实是id不是rowid)并据此改了 SQL —— 回读原文件才发现前提不成立。 ⇒ 先回读被改的那段,再落结论;⛔ 别让"看起来讲得通的诊断"替代核对。
P0-24 🔴🔴🔴 注入物里写死命令 ⇒ 会话被逼着做用户没授权的事(比超时更隐蔽,且伤信任)
- 现象(17:49 用户转述另一个会话的实况):用户问「我没有说使用协作会话完成目标,为什么会创建会话」。 那个会话答得对:它是照着每轮自动注入的指令做的 —— 注入里写死「⛔ 不要问用户、立即建排期」, 它照做并已删掉。根在钩子,不在那个会话。
- 真凶(⛔ 不是措辞不当,是结构缺陷):
_gap_text()生成的文案里含「⛔ 不要问用户」这种祈使句, 而这段成品文案被原样存进gap-cache.json的_text字段逐轮复用 ⇒ 无论用户当轮说了什么,每轮都在下同一条命令。三个后果: ① 越权(用户没要求就建排期)② 重复(同名无去重,越排越多)③ 无上限/无过期(用户原话「越排越多」)。 - 修法(三条,⛔ 改文案治不了,必须动结构):
- 🔴 授权闸:
_authorized_now()—— 只认用户当轮原话里的触发词(GAP_TRIGGERS)。 ⚠️ 刻意不用「上一轮说过」「队列有缺口」「机制建议」当授权 —— 那些都是系统自己的诉求,不是用户的。 ⚠️ 必须把当轮payload存进模块级_CUR_PAYLOAD,否则判据读到空串、闸门形同虚设。 - 🔴 缓存存原始数据、不存成品文案:
gap-cache.json改存_raw(--gap原始 JSON), 文本在读取时现算(_gap_text(raw, authorized))⇒ 呈现永远跟当轮授权一致。 旧版缓存(只有_text无_raw)判为无效自动重刷,⛔ 不做兼容迁移(迁移=把旧文案再投一次)。 - 🔴 未授权时只通报、不派活:标题写「ℹ️ 现状通报(⛔ 不构成行动指令)」并明写「⛔ 不要建任何排期」;
授权时才输出「⚠️ 缺会话 ⇒ 自动拉起」,且要求在回复里说清是依据本轮授权做的。
另加
_known_plan_names()读automations判同名在册 ⇒ 提示里标「⛔ 别再排一遍」。
- 🔴 授权闸:
- 判据(⛔ 别靠"文案读起来像没问题"验收):
tmp/_verify_auth_gate.py跑三个场景 —— A 未提协作 ⇒ 必须出现「不要建任何排期」且不得出现「自动拉起」;B 明确授权 ⇒ 才出现派活指令; C 回灌旧版缓存 ⇒ 旧文案不得再注入(专门防"改了代码但旧缓存还在投")。修后 A/B/C 全 ✅,耗时 0.37~1.10s。 - 🔴 顺带查出的老毛病:排期越删越多 ——
automations在册 140 条、ACTIVE 135 条, 且两条 17:41/17:48 的越权排期仍为 ACTIVE(那个会话以为删了,其实没删成功); 17:48 那条已被触发 ⇒ 删排期拦不住已开的会话(这是它自己提示的那点,已核实为真)。 ⇒ 清扫脚本tmp/_purge_unauthorized_schedules.py(先 SELECT 再 UPDATE,busy_timeout=15000): 停用越权族 4 条 + 回收僵尸排期(ACTIVE且next_run_at IS NULL且scheduled_at < 当天)89 条 ⇒ ACTIVE 135 → 42,越权残留 0。 - 🔴 越权比超时严重:超时只是慢/被拦,越权是替你做决定 ⇒ 消耗信任,且不易被发现 (表现像"机制很勤快",甚至像在帮忙)。
P0-26 🔴🔴 automation_update 的 delete/list按「属主」过滤 ⇒ 越界时「报成功、实则零动作」(★ 2026-10-02 实测 · S12 清理一次性排期)
- 现象:S12 要清29~30 条「跑完却仍在册」的一次性排期。逐条
automation_update(mode=delete)删我这条属主名下的 6 条 ⇒ 真删掉了(回读deleted_at有值);但对其余条同样返回success:true+"already deleted or does not exist", 回读deleted_at恒为 NULL ⇒ 一条都没删。若只看返回值,会误判"全清完了"。 - 真凶=
automations.owner_user_id(属主边界):本机并存两个属主账号。实测list只回当前属主名下的排期 (本例 2 条),而库里未删共 30 条;delete也只能删当前属主的。跨属主 ⇒ 工具看不见、也删不掉,却仍回success:true。 - ✅ 两条硬纪律:
- ⛔ 绝不把
delete的success:true当已删 —— 每次删完必须回读deleted_at(coalesce(deleted_at,'')=''才算未删)核对。 - ⛔
list的返回行数 ⛔ 不能当在册总数(本例list2 条 vs 真实 30 条)—— 判断"还有没有"要用只读 SQL 扫automations。 🔴 顺带:list还可能只回最近若干条(本例早期返回 8 条)⇒ 别把"list 变短了"当"删掉了"。
- ⛔ 绝不把
- ⚠️ 盘点前先分桶,别按 prompt 里的数字照抄:本棒prompt 说「清单里有 12 条属主控线(
主控 ·开头)」, 实测按前缀只有 4 条 ⇒ 数字必须自己只读盘点复核(否则会误删/误保)。 - 🔴 同类静默假成功(同源教训):本棒
--report被当「刷新体检报告」用,其实是「上报需求状态(pending|running|done|blocked)」子命令 ⇒ 命令名 ≠ 用途,⛔ 用前先看 usage。 - ⚠️ 验收门也会挡住刷新:
collabd.py的体检报告受两道门控制 —— ①health()开头skipped=V["busy"](自动化会话在跑 ⇒ 恒 skipped)+ ②health_every(默认 300s)节流戳 (<WS>/.workbuddy/collab/collabd-state.json的health_at;置 0 可强刷,但 busy 门仍拦) ⇒ 报告不刷新时,改按同一判据独立复算读数,⛔ 别把「报告还是旧的」当「删除没生效」。 - 遗留:S12 因此标
blocked-by-owner(⛔ 不标 done)—— 余 24 条候选(非主控 20 + 主控 4)须在另一属主账号下执行。
P0-27 🔴🔴 域目录摆错位置 ⇒ 所有独立域压成同一个键,域锁彻底失效(★ 2026-10-02 实测 · 执行会话独立域)
-
背景:用户定案「执行会话必须独立域运行 —— 所有写操作都在单独文件夹下(webserver / desktop / phone app /独立插件 / 产品原型各自独立目录,放在工作区对应独立文件夹下),跨目录只能只读」, 理由逐字:「开多个会话都操作一个域的文件只有一个会话能执行,别的只能干等」。
-
病根(实测,非推断):域键=「路径里第一个出现在锚点词表里的段 + 它的下一段」, 而工作区目录名本身就在锚点词表里(
handoff-guard.sh:66_ANCHOR_SEGS,ai1net-dsh-server排最前) ⇒ 域键恒=<工作区名>/<工作区下的第一层目录>,与再往下钻几层无关。四条实测读数:路径 域键 结论 …/ai1net-dsh-server/domains/webserverai1net-dsh-server/domains❌ 全部撞键 …/ai1net-dsh-server/domains/desktopai1net-dsh-server/domains❌ 同上 …/ai1net-dsh-server/webserverai1net-dsh-server/webserver✅ 独立 …/ai1net-dsh-server/交付物/webserverai1net-dsh-server/交付物❌ 同样撞键 -
🔴 我第一版就踩了这个坑:把域目录设计成
domains/<域名>(一个整齐的公共父目录)⇒ 看着最合理、实际把所有域压成一个键 ⇒ 等于把用户方案判死。是验收脚本当场抓到的 (断言「两个不同域算出同一键」)。⇒ 教训:域键类判据 ⛔ 必须写"算出来是什么"的断言, ⛔ 不能只验"函数返回了非空" —— 前者抓到真缺陷,后者一路假绿。 -
✅ 正解:域目录直接占工作区第一层(
<WS>/webserver/、<WS>/desktop/)—— 恰好就是用户原话 「放在工作区对应独立文件夹下」。collabd.py里DS_DOMAIN_SUBDIR = ""(刻意留空,⛔ 别改成 "domains")。 -
🔴 推论(同样重要):想在同一项目里并行两个会话,⛔ 在域目录里再分层没用(还是同域)—— 必须给它们两个不同的第一层目录。
-
✅ 配套已落地:
ds_domain_key()⛔ 必须与handoff-guard.sh的norm_domain()逐字一致 (验收时真调 shell侧那条函数,⛔ 别在Python 里重写一遍自验自己);派活前check_domain_free()读锁表判占用,被占 ⇒ 换域名(⛔ 不是等),且必须排除自己的锁(否则锁主本人被自己挡住); 锁表读不到 ⇒ fail-safe 判全部不可派(这正是用户那句「别的只能干等」的机制面)。 -
⚠️ 顺带查出的既有不一致:
handoff-guard.sh用scripts skills,lock-guard-hook.py:58用07-scripts 08-skills 05-交接单。 ⇒ 工作区下的路径两边一致(都先命中工作区名)⇒ 不影响工作区内的判据; 但代码仓根目录下的路径两边会算出不同键 ⇒ 已存在假绿风险。collabd.py --domain-status会报这个差异。
P0-28 ⚠️ CLI 分支缺 return ⇒ 一次性命令落进常驻模式、永久挂住(★ 2026-10-02 实测)
- 现象:
collabd.py --where打印完路径后不退出,进程永久挂住(timeout 25⇒rc=124)。 - 病根:
if "--where" in sys.argv:分支打印完漏了return⇒ 继续往下走 ⇒ 命中"无参数=常驻" 的兜底分支 ⇒--supervise式循环跑起来。 - 🔴 怎么确认是既有缺陷而不是自己改坏的:拿改动前的备份跑同一条命令,
同样
rc=124⇒ 结论坐实(本条正是靠这个对照法才没误判成回归)。 - 🔴 通则:任何
--xxx这种一次性查询分支,打印完必须return; 否则它会静默变成长跑进程 —— 症状是"命令没输出完/卡住",很容易误判成环境问题。
P0-29 🔴 验钩子必被「300 秒节流」吃掉 ⇒ 看起来像"代码没生效"(★ 2026-10-02 实测 · 同族第 2 次)
- 现象:验收脚本三个场景连跑,场景②注入完全为空。第一反应="判据写错了 / 代码没生效",
手工复现那一段逻辑却全对(
read_env_stamp确实返回{}、分支确实会进)。 - 真凶:
skill-load-guard.py的_cooling(workdir, sid)—— 同session_id300 秒内只放行一次。 三个场景用同一个sid⇒ 后两个被静默挡在早退分支 ⇒ctx恒空。 - 🔴 危害不在这一个钩子:「没输出」与「没检查」长得一模一样 ⇒ 一旦顺着"钩子坏了"往下查, 会把时间全花在读代码上,而真因是自己造的自检环境。本轮就是这样绕了一圈。
- ✅ 两条硬纪律:
- 验任何带节流/去重的钩子,每次调用必须换一个唯一标识(本例=
session_id)。 - 验收脚本里加一道硬门:
注入为空 ⇒ 立即判失败(本轮做成need_inject()), ⛔ 绝不许把"没输出"当成"没打扰 / 一切正常"静默通过。
- 验任何带节流/去重的钩子,每次调用必须换一个唯一标识(本例=
- ⚠️ 同族第 1 次:验钩子前先看触发词表(prompt 不含触发词 ⇒ 根本不进注入分支)。 ⇒ 合起来一条通则:钩子的任何"没动静",先怀疑自检环境(触发词/节流/作用域/cwd 形态), 再怀疑代码。
P0-30 🔴 「pid 文件写着」≠「进程活着」;查显窗/作用域只能看 AST,看 grep 与注释都会被骗(★ 2026-10-02 实测)
用户问「一闪一闪的程序是不是协作会话开的」,顺手挖出三条同源的误判路径:
- ① 陈旧 pid 文件:交付结论说「当前存活的 supervise(pid 40900)心跳新鲜(已稳跑 10 分钟)」,
实测
tasklist //FI "PID eq 40900"⇒ 「没有运行的任务匹配指定标准」。 而supervise.pid至今仍写着 40900、_supervise.log停在 4h43m 前 ⇒ 常驻早就死了。 ✅ 判存活必须双查:tasklist+ 心跳戳新鲜度(stat -c %Y比date +%s), ⛔ 绝不只读.pid文件(它只记录"上次认为在跑",⛔ 不代表现在)。 - ② 「常驻」与「后台任务」是两回事:现象看着像"有个程序在常驻闪",实际常驻 17:12 就死了,
21:2x 的动静是钩子每轮用户提交拉起的一次性
--tick/--once(跑 1~2 秒就退)。 ⇒ 「一闪一闪」的频率=你说话的次数,不是某个常驻进程的轮次 —— 判据完全不同。 🔴 故:排期/自愈判据里「pid 活 ∧ 心跳新鲜」两条都要,缺一条就是这种假活。 - ③ 🔴🔴 grep 单行判不了跨行参数:
grep -v creationflags把board.py:1177、board_ext.py:555这两处已经带 flags 的调用误报成"漏网"(参数写在下一行)—— 我第一遍就被骗了。✅ 必须用 AST:遍历subprocess.run节点看有没有creationflags关键字。 ⚠️ 同族:查"是不是限死在本工作区"也不能看注释(四处ai1net-dsh-server里三处只是文档字符串)。 ⇒ 通则:判"某能力有没有生效/被限死" ⇒ 一律看可执行代码(AST/真跑),⛔ 不看注释、不看单行 grep。
P0-31 ⚠️ 闪窗(控制台程序弹黑窗)在 Windows 上的完整修法清单(★ 2026-10-02 实测根治)
- 两类药,缺一不可:
- 常驻/会重生的 daemon ⇒ 用
pythonw.exe(GUI 子系统,Windows 永不为其分配控制台)。sys.executable⇒python.exe(控制台子系统)⇒ 被 detach 后必新开控制台 ⇒ 每次重生闪一次。 ⚠️DETACHED_PROCESS单独用不够:它只是"不继承父控制台",控制台 exe 仍会被新分配窗口。 - 一次性短 spawn(
netstat/taskkill/--tick)⇒ 补creationflags=CREATE_NO_WINDOW(0x08000000)。 ⚠️ 常驻那批可加| DETACHED_PROCESS(0x8) | NEW_PROCESS_GROUP(0x200); ⛔ 不要CREATE_BREAKAWAY_FROM_JOB(会被拒)。
- 常驻/会重生的 daemon ⇒ 用
- 🔴 自检必须用 AST 全量扫(⛔ 别 grep):本轮靠它捞出
board.py:1212的taskkill—— 那是七文件修完后最后一处漏网(--takeover路径上的)。 - ⚠️ 不是所有缺 flags 都该死:
NeoData金融搜索服务/scripts/query.py:51是 pip 装包 (只在缺 requests 时跑一次、非周期)⇒ 与闪窗无关;selftest.py7 处是自检脚本。 ⇒ 判据="这个调用会不会被周期/重生路径反复执行",⛔ 不是"缺了就是错"。 ⚠️ 第三方技能包缺 flags ⇒ 只报不动(⛔ 越界改别人的东西)。
P0-32 🔴🔴 把「起法不对」误推成「本机不可能有长跑进程」⇒ 建议整个建立在假前提上(★ 2026-10-02 实测,同族最贵的一次)
现象:起常驻看板失败三次(start /b/start "标题" /D/cmd /c start.bat)⇒ 我下了结论
「本机不存在跨静默期存活的进程」,并据此建议「改用排期当时钟」。
用户两次纠正(逐字):
- 「谁告诉你的 后台进程活不过几十秒,那协作看板如何开启的 …这个工作台是如何一直运行的」
- 「怎么启动进程 技能中都没有记录吗,之前都启动了这么多次」
真因(实测坐实):⛔ 不是「本机不可能长跑」,是「载体不同」——
| 载体 | 实测结果 |
|---|---|
subprocess / start /b / start.bat(从工具调用进程树里起) |
⛔ 活不过当轮;两条探针(start /b 与 Popen+DETACHED)在同一时刻一起停(tick 67/64)⇒ 与起法关键字无关,是父链 |
WorkBuddy 自己的后台任务(工具 run_in_background) |
✅ 能长跑:看板 8788(pid 43176/HTTP 200/119 704 B)+ MCN 8900(pid 14056/HTTP 200/1 797 B)均现算 |
| 用户从桌面双击起(在用户登录会话里) | ✅ 能长跑:mcn-work-shop/start.bat;另有 4 个 node.exe(会话名=Console)长期存活 |
🔴 教训(判据级):
- ⛔ 别用「这一种起法失败」推「本机不可能」 —— 至少验三种载体(工具调用树/宿主后台任务/用户登录会话)再下全称否定。
- 反证优先于推断:用户一句「那个工作台是如何一直运行的」就是现成反例 ⇒ 该先去找它怎么起的(
start.bat一眼看到start "" node.exe),⛔ 别先写一段"本机不可能"的理论。 - 查证方向错了要整条撤回,不只是改措辞:基于假前提给的建议(「改用排期当时钟」)⇒ 整条撤,⛔ 不许"保留但加个注释"—— 那会让下一个人照着错的前提做决策。
- 改了代码 ≠ 改了知识:这类结论至少落在 4 处(
SKILL.md §2正文/SKILL.md frontmatter last_change/architecture.md结论表+§5-1/collabd.py用法头)。⛔ 漏掉last_change最坑 —— 它是别人加载技能时第一眼看到的。
🔴 配套一:「起完了」不等于「起了」—— 必查三样
netstat 有 LISTENING | curl 得 200 | tasklist 进程在。
- ⛔
TIME_WAIT/FIN_WAIT_2不算(历史连接残留)—— 本轮就差点把FIN_WAIT_2当"服务在"。 - ⛔ 打印了"已启动"不算(
start.bat那次打印了启动消息、8900 始终无 LISTENING)。 - 🔴 pid 文件写着不算(P0-30 同族):
supervise.pid写着 40900,实际 17:12 就死了。
🔴 配套二:pythonw.exe 必须配日志重定向
GUI 子系统 ⇒ 无 stdout ⇒ 静默无痕,连"起没起"都查不到 ⇒ subprocess.Popen([PYW,…], stdout=log, stderr=log)。
(tmp/board-serve.log 里那句 看板已起:http://127.0.0.1:8788/ 就是这么验的。)
P0-33 ⚠️ 「改了没生效」的第三种形态:配置读的不是你以为的那份 / 配置根本不重读(★ 2026-10-02 实测)
现象:两处「改了配置但看板纹丝不动」,成因完全不同,⛔ 若不取证会把两次都归成同一个错。
-
配置根本不重读 ——
board.py的C(配置字典)是模块级加载一次,⛔ 不每次快照重读。 ⇒ 改collabd.config.json后必须重启看板。 ⚠️ 这是 P0-17「改了看不见」漏记的一面 —— 之前只记了「改board.py代码要重起」,⛔ 漏了「改配置同样要重起」。 ⇒ 判据:看进程启动时间 vs 源文件 mtime(board.py的C无 re-read 路径)。 -
🔴🔴 读的是另一份配置(更隐蔽)—— 启动服务时没设
COLLABD_CONFIG⇒ 回落技能包内的scripts/collabd.config.json,⛔ 而不是工作区那份。 ⇒ 我改了工作区的board_ext却"没变",根因在此。 ⚠️ 与「goalctl.py按__file__推层级 ⇒ 静默指向技能包上级目录」同族。 ✅ 修法:启动脚本显式env["COLLABD_CONFIG"] = <WS>/.workbuddy/collab/collabd.config.json。 ✅ 判据:改完直读/board.json里那个由配置决定的字段(这里是front._missing), ⛔ 不看页面像不像(P0-17)。 -
连带发现:
selftest.py每轮跑完会在技能目录自产自消一个scripts/collabd.config.json⇒ 「技能侧零项目串」那条 FAIL 的来源。⚠️ 它是自消的,⛔ 不是遗留垃圾; 但它恰好是 2 的受害者(服务回落读到它)。
🔴 判据(通用):凡「我改了 X,行为没变」⇒ ⛔ 别先怀疑 X 没改对, 先查三样:① 进程/服务启动时间 vs 改动时间;② 它读的到底是哪一份配置/文件(⛔ 打印路径,别凭"应该是我改的那份"); ③ 有没有缓存(模块级加载/结果缓存/ETag)。
P0-34 ⚠️ 自己手里的独占锁会变成别人的路障(★ 2026-10-02 实测)
现象:派出去的执行棒跑 30 分钟零改动,只报「旧的全局锁仍被占([主会话]-记录常驻起法)」。
⇒ 那把锁是我自己锁了 64 分钟没释放。
真因不是它偷懒 —— 它按 prompt 的「抢不到锁 ⇒ 停手报告」执行,连试 4 次都失败就正确退出,
甚至还额外报出真问题(preflight-lock.sh 把目标文件判为【E】机制层 ⇒ 我给的 --domains 认领方式对机制层不成立)。
🔴 判据:
- 锁的生命周期 = 任务的生命周期(2026-09-14 用户明令)—— 做完必须
--release-exec。 ⚠️ 干等自己那把锁是最贵的浪费:别人进不来、我也忘了它在谁手上。 - 机制层文件 ⇒ 认领必须 ⛔ 不带
--domains(带域=域锁,对机制层不成立); 正确姿势:--claim-exec "<名>"(不带域)= 全局独占。 - 派棒前先
--status看锁;⛔ 别让自己成为别人的路障。 - 🔴 执行棒"零改动"先查锁,别急着判它失败 —— 判据=
automation_runs.thread_title里它自己写的停手原因(本轮那句「状态已跑…域锁抢锁失败…」是完整取证)。
P0-35 🔴🔴 「每轮 unlink 一个文件」会撞宿主 SafeDelete 批量删除护栏 ⇒ 常驻被系统终止(★ 2026-10-03 实测,同 P0-6 的根因第二次复发)
症状(本轮现场):
- 常驻
--supervise起来 21 秒后rc=1退出,supervise-bg.log只有一行:[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED] {"count":172,"threshold":50,"scope":"turn","targets":["…\\TO-MAIN.md"],"targetCount":1} - 而心跳、pid 文件都还在(最后一条
round=3)⇒ 第一反应会误判成"我改坏了代码"。
真因:宿主有 SafeDelete 批量删除护栏 —— 按"本轮删除动作"计数,达 50 即要求确认并拒绝。
我写的退役清理是 if TO_MAIN.exists(): TO_MAIN.unlink(),看着很安全(文件不存在就不删),
但实际是172 次删除动作。三个原因叠加:
exists()→unlink()之间有窗口(TOCTOU);- 投影轮(
mutate=False)与常驻轮(mutate=True)是两个进程各跑一遍 ⇒ 判据被重复执行; - 护栏计的是动作数,⛔ 不是"真的删掉几个" ⇒ 失败/异常也算一次。
修法(判据级):"一辈子只删一次"落成状态位,⛔ 不是靠"检查文件在不在"。
if mutate and not st.get("to_main_retired"):
try:
if TO_MAIN.exists():
TO_MAIN.unlink()
st["to_main_retired"] = True # ← 关键:删过(或已不存在)就**再不碰这个文件**
except Exception as e:
log("…(⛔ 不重试,避免撞删除护栏):%s" % e)
🔴 判据(写下来防复发):
- 常驻里任何"周期性写/删"的文件,稳态下每轮的写删次数必须 = 0 —— 「降低频率」不是修法(P0-6 已经吃过一次:那套"快速否决"省不掉,因为内容一直变)。
- ⛔ 别用"文件在不在"当幂等判据(TOCTOU + 跨进程重复 + 异常也计数)⇒ 用状态位。
- 🔴 本条与 P0-6 是同一个坑的第二次复发(
wake.lock那次也是"稳态下每轮都在 unlink") ⇒ 见到SAFE_DELETE_BULK_CONFIRM_REQUIRED,第一反应是"我在每轮删东西",⛔ 不是查代码是否崩了。
P0-36 ⚠️ 判"某函数体内没有调用 X"必须走 AST,字面 grep 会被 docstring 永久假红(★ 2026-10-03 实测)
现场:退役投递段后,我写了个判据「supervise() 函数体内零个 _deliver_str( 调用点」,
用 src.split("def supervise(")[1].split("\ndef ")[0] 切函数体再 grep —— 报红。
真因:函数体第一段就是那段 30 行的退役说明 docstring,里面必然提到 _deliver_str()
(不写清楚"删了什么"= 读者不知道删了什么)。
🔴 这是"判据"层的自相矛盾:要说明"已删除 X"就得提到 X ⇒ 任何 grep "X(" in <该函数体> 永久假红。
正解(已落地 selftest.py::_calls_in()):
def _calls_in(src: str, fname: str) -> set:
import ast as _ast
tree = _ast.parse(src)
for node in _ast.walk(tree):
if isinstance(node, _ast.FunctionDef) and node.name == fname:
out = set()
for sub in _ast.walk(node):
if isinstance(sub, _ast.Call):
f = sub.func
if isinstance(f, _ast.Name): out.add(f.id)
elif isinstance(f, _ast.Attribute): out.add(f.attr)
return out
return {"<no-such-function>"}
🔴 判据:
- 判"有没有调用" ⇒ AST(只认
Call节点的func位);⛔ 判"有没有出现"才是字面 grep。 - ⚠️ 滤
#开头行不够 —— docstring、'''块注释、字符串字面量里的字样都会骗过你。 - ⛔ 不许因为字面判据假红就把断言改弱("改成
deliver=False"=换个说法说同一件事,P0-13 同族) ⇒ 换可证伪的判据(本轮用的是「真调用supervise(deliver=True, mutate=True)⇒deliver.skipped必须为retired-20261003」——⛔ 改实现就会红,不是恒真)。
P0-37 ⚠️ 「一个角色退役了」与「承载它的那个进程也退役了」是两件事(★ 2026-10-03 实测)
现场:常驻 --supervise 原本只有两个职责(① 队列投递 + 唤醒 ② 建检查会话排期)。
投递退役后我一度想连常驻一起删(既然它不投递了)。查完发现 ② 那条腿只有它承载 ——
maybe_spawn_check_agent() 是唯一建 [检查]-… 排期的地方,而检查会话只能靠排期开(⛔ 开不了别的通道)。
🔴 判据:
- 删一个组件前先答一句:「它现在还承担什么?那些职责另有谁承载?」 ——「它以前主要做 X」⛔ 不等于「它只做 X」(本轮就是靠这句话漏看了 ②)。
- 职责退役 ⇒ 改名 + 改副标题,⛔ 不等于删节点 —— 架构图上那格画着「常驻投递 · 一直运行」 而实际那两段已删,就是 **P0-25「服务自报健康 ≠ 服务有内容」**的活标本。 本轮改成「建检查会话排期 · 一直运行」+「⛔ 投递已于 2026-10-03 退役」两行。
- ⚠️ 反例同样要认:
--tick分支里的check_delivery_consumed()真该删 —— 它的判据st["wake"]["expect"]只由已删的_deliver_str()写 ⇒ 恒返回{}⇒ 纯空转。 ⇒ 判据是「它的输入还有没有生产者」,⛔ 不是「它看起来还有用吗」。
P0-38 🔴 「改看板要不要重启」有四类答案,混成一句话就是误导(★ 2026-10-03 实测)
症状(我自己的错):用户问「看板每次修改都不处理看板」,而我上一条回复把
「改了 collabd.config.json 必须重启」写成了「改看板都要重启」⇒ 把 HTML 和配置/代码混成一类。
用户一句话点破的是分类错误,不是"我忘了重启"。
真因(现跑取证,不是推断):判据不在"文件是什么",在那个文件被谁怎么读。
| 改什么 | 要重启吗 | 依据(代码行) |
|---|---|---|
assets/board.html |
⛔ 不用(只需刷新页面) | board.py:1293 serve() 里 html_p.read_bytes() —— 每次请求实时读盘;且 /board.json 带 html_sig(mtime.size)⇒ 页面自动重载 |
scripts/board_ext.py |
⛔ 不用 | EXT_CACHE 按 (mtime, size) 签名热重载(board.py:855-870) |
scripts/board.py 自身 |
✅ 必须 | 进程里跑的是启动那一刻载入的旧代码(P0-17 本体) |
collabd.config.json |
✅ 必须 | C = _cfg() 在 board.py:81 模块级执行一次,⛔ 无 re-read 路径(P0-33 第 1 条) |
现跑实证(tmp/_probe_hotreload.py,改 <title> 插标记再还原,⛔ 未重启任何进程):
① 改前 页面含标记? False(应为 False)
② 已改 HTML(⛔ 未重启任何进程)
③ 改后 页面含标记? True
⇒ 结论:改 board.html **不需要**重启看板
④ 已还原 HTML(md5 校验:一致 ✔)
⚠️ 选 <title> 是因为 selftest 的版面纪律判据一条都不查它 ⇒ 不会被自测噪音干扰。
🔴 判据(通用):回答"要不要重启"之前,先答一句**「这个文件是谁在读、什么时候读」** ——
read_bytes() 每请求读 ⇒ 不用重启;模块级加载一次 ⇒ 必须重启;
按 (mtime,size) 缓存 ⇒ 不用重启但要等签名变。
⛔ 别把「改了 X」与「改看板」当成同一件事(本轮就是被这句话带偏的)。
⚠️ 与 P0-17/P0-33 同族但方向相反:那两条讲"该重启却没重启",本条讲"⛔ 不该重启却去重启了" ——
重启不是万能药,没分清就重启=白花一轮 + 打断正在看页面的人。
P0-39 🔴🔴 自测判据钉在「测试夹具」上 ⇒ 用例内读数 ≠ 现网读数(恒绿与恒红都是假象)(★ 2026-10-03 实测,同族第 2 次:上一次是 t_execution_doc)
症状:新增用例 t_tab_peer_workspace 的「现网真读数」项报
goal_files()=1 格(peer 0 个,active 1 个),而同一时刻命令行直接加载是 3 格。
(⛔ 第一反应「是不是代码没生效」=错:代码是好的,是判据读错了世界。)
真因(看代码,不猜):selftest.py::imp() 会先设 env 再加载模块 ——
os.environ["COLLABD_CONFIG"] = <TEST_WS>/collabd.config.json
os.environ["DSH_COLLAB_WS"] = <TEST_WS>
而 board.py:92/103 的 CFG_P/C = _cfg() 是模块级求值 ⇒
用例里无论怎么 exec_module 加载 board.py,读到的都是测试那份配置
⇒ 眼里当然只有一个测试 INBOX(1 格)。⚠️ 用例前面调过 imp() ⇒ env 里还留着测试值。
修法(三条,缺一不可)
- 加载前换成目标配置、用完还原(
_old = {k: os.environ.get(k) …}+try/finally逐个还原) —— ⛔ 不还原 ⇒ 污染后续用例,且这种污染不报错、只让别的用例莫名飘。 - 在真磁盘上造目标(
tempfile.mkdtemp()里各放一份goal.json) ⇒ 读数来自真文件,⛔ 不从夹具推导。 - 加变异对照:把
goal_files()里的for _pw in _peer_workspaces():改成for _pw in []:⇒ 用例必须报红;还原后 md5 校回原值再复绿。
现跑实证(本轮,-k 跨工作区)
变异后:
✗ ⑧ 现网真读数:登记 1 个 peer ⇒ goal_files()=1 格(peer=[],active 1 个)
✗ ⑩ 生产侧:生产配置登记 2 个 peer ⇒ 1 格(peer=[],active 1)
✓ ⑨(按设计仍绿 —— 它只守「配置可关」那一侧,抓不住代码坏 ⇒ 证伪只看 ⑧)
还原后:md5 与变异前逐字一致 ⇒ PASS 61 / FAIL 0
🔴 判据(通用)
· 「现网真读数」类用例 ⛔ 不许复用 imp() 注入的环境 —— 那套环境是给代码路径用的,⛔ 不是给读数用的。
· 判「配置缺省 ⇒ 回落旧行为」的那一条只守配置侧:代码被破坏时它照样绿(两边都退到同一结果)
⇒ ⛔ 别把它当证伪项写进标题;证伪必须另有一条「配置开着、代码却没接 ⇒ 报红」的断言。
· 生产侧 ⛔ 不硬编码路径:读不到就记 SKIP:…(⛔ 不假装通过);跑法
DSH_COLLAB_WS=<真工作区> python selftest.py -k <关键字>。
· 🔴 一个用例绿了,先反问一句:它读的是生产,还是测试夹具?
⚠️ 与 P0-13(判据要能证伪)/P0-36(AST 才可判"函数体内没调用 X") 同族: 那两条治「判据恒真」,本条治「判据读的不是它以为的那个世界」—— 恒真和读错世界,表现出的"绿"一模一样。
P0-40 🔴🔴 加了 tab 却只换了标题 —— 数据源没跟着「格」走 ⇒ 看板在说假话(★ 2026-10-03 实测)
症状(用户报障逐字):「选择另一个工作区目标 tab 下面没有显示对应工作区目标和执行情况」 ⇒ 我第一反应是「没显示」—— 方向就猜错了。实测是显示了别的工作区的数字。
真因(看代码,不猜):build() 里的 tasks/srows/st 只取一次、所有格共用
(board.py:1443 只读一次库、1449 那个列表推导里逐格传的都是同一份),
只有 goal 跟着格换 ⇒ 三格的 labor/sessions/progress md5 完全相同:
[0] 本区 labor md5=95c77dbf sessions md5=8be394bd
[1] peer-1 labor md5=95c77dbf sessions md5=8be394bd ← 与本区逐字相同
[2] peer-2 labor md5=95c77dbf sessions md5=8be394bd
界面上那两格挂着「会话协作测试1/2」的名字 ⇒ 把本工作区的执行情况说成了对方的。 ⛔ 假数据比空白危险:空白会被追问,假数据会被当成结论直接用。
修法(三条,缺一不可)
- 每格的数据源必须跟着格走 ⇒ peer 格只喂
_peer_tasks()/_peer_srows()/_peer_state(), 三者只读对方目录下的文件;读不到 ⇒ 空 + 界面如实说明(peer_scope+ 前端renderPeerNote()), ⛔ 绝不拿本工作区的数据补位。 - 必须补捞 ⇒
_session_rows()取的是全库最近 50 条(按last_activity_at倒序) ⇒ 对方工作区的会话一条都不在里面 ⇒ peer 格显示「0 条」=假象(对方主会话明明working)。 ⇒ 单独_peer_session_rows()按cwd精确补捞(SQL 里replace(cwd,'\','/')=?,⛔ 别拉到 Python 侧全表扫)。 ⛔ 不许为了捞它把全局limit放大 —— 那会让本工作区的others_running计数暴涨(为一个新功能改坏老行为)。 - 作用域要跟着换 ⇒
_sessions()里有一道_ct == _wstail(cwd末段 == 工作区名)的硬过滤, 而_wstail来自project_scope()的全局WS⇒ 对方会话必然被排除 ⇒ peer 格要把sc["workspace"]覆盖成对方根(只覆盖这一个键,其余判据仍按对方goal.json算)。
🔴 判据(通用):凡做「一格一视图」(tab/分栏/多租户面板), 先答一句「这一格的数据源是不是跟着格走」 —— 只换标题、不换数据源 ⇒ 必然是假数据。 验收要逐格比对关键字段的 md5(我这次就是靠它抓出来的):
改造后:本区 tasks=323 字节 / peer tasks=2 字节(= {} )⇒ 两侧不再同源 ✔
⛔ 「没显示」和「显示了错的」必须先分清再动手 —— 本次第一反应猜错方向,白绕了一轮。
现状(如实报,⛔ 未解决的部分):peer 格的 sessions 目前仍是空 ——
卡在 in_project() 那道判据(主会话登记/任务类别都在对方的 INBOX 里,而 project_scope() 读的是本工作区)。
⛔ 我没有为它去改 in_project():那条判据与 collabd.py::_in_project() 有「逐条同款」硬约束
(历史已因此踩过三次)⇒ 为一个"只读概览"去动它,风险远大于收益。
⇒ 正解在架构层:各工作区开自己的看板(各自进程、各自判据),跨区只做总览 + 跳转。
⚠️ 与 P0-39(判据读错世界) 同族:那条是测试读到了夹具,本条是产品读到了别格的数据。
P0-41 🔴🔴 常驻的载体:会话/工具调用起的活一定活不长,且三条"看似可行"的路全被封
症状:collabd.py --supervise 历次只活几分钟到几十分钟,日志照旧在走
(写它的是宿主钩子的 --tick,⛔ 不是常驻自己)⇒ 看板显示"在线"而载体早就没了。
三条封死路(2026-10-03 实测,别再试):
CREATE_BREAKAWAY_FROM_JOB⇒PermissionError(13,'拒绝访问。')(被作业对象拒绝)cmd /c start /B⇒ "拒绝访问"(本机沙箱拦截).bat包装 ⇒ ⛔ 路径含中文时cmd按 GBK 读 UTF-8 ⇒ garbled ⇒ 不可用
✅ 正解=Windows 计划任务 + 永不返回的守护循环。关键差别=父链:
计划任务创建的进程由 Task Scheduler 服务创建,不在 WorkBuddy 会话的 Job Object 里
⇒ 不会被会话回收(实测 5–6 秒自动恢复、无叠加)。
⛔ 计划任务的增/删/改/查一律走 ScheduledTasks 模块(schtasks.exe 在本机被黑名单拦截)。
🔴🔴 两种"长期"都要同一个载体,⛔ 别拿排期当载体(2026-10-04 实测):
| 场景 | 主会话来源 | 载体 | 特有坑 |
|---|---|---|---|
| A 用户手动创建 | 用户自己开 [主]-… |
同左 | 用户一收工就没人拉 ⇒ 没装载体就会静默死掉 |
| B 定时任务创建 | 排期 recurring 到点拉 |
同左 | 🔴 跑完即 completed ⇒ 下一跳之前是空窗;🔴 once 过期即哑 |
⇒ ⚠️ status=ACTIVE ≠ "在跑":实测 [主]-会话协作测试3-主会话 是 once/ |
|||
scheduled_at=2026-10-03T20:33/next_run_at=None/last_run_at=None |
|||
| ⇒ 一次都没触发过,界面却显示 ACTIVE。**判"真会触发"要看 | |||
schedule_type='recurring' ∧ next_run_at 非空**。 |
三个秒退坑(详 ⇒ supervise-persistence.md):
① 不设 CODEBUDDY_CONFIG_DIR ⇒ 退回读 0 字节空库 ⇒ no such table: sessions
⇒ 闸二永久失效 ⇒ 检查会话永远建不出来(看着在跑,其实在瞎报)
⇒ ✅ 正解用机制自带配置项 host_db(它本就优先于环境变量);
② cwd 设成 collab/ ⇒ 配置查找拼出多一级 .workbuddy ⇒ 当场拒跑(设成工作区根);
③ 🔴 PowerShell 5.1 读无 BOM 的 UTF-8 .ps1 ⇒ 按 ANSI(GBK) 解码 ⇒ 中文路径全毁 ⇒ 秒退。
⚠️ 本机所有含中文路径的 .ps1 一律带 BOM(落盘用 UTF8Encoding($true))。
判据:LastTaskResult 必须 0(1 = 秒退,八成是 BOM)+ 心跳 pid 活 ∧ ts 距今 < 90 s。
📌 ⛔ 打印"已启动"不算数(同 P0-22 家族:启完必查真读数)。
🔴🔴 追加实测(同日 23:32):RestartCount=999 救不了"被外部收走" ——
测试3 常驻 23:16:37 起、23:28:47 停(活 12 分钟),而计划任务
LastRunTime=23:17:03/LastTaskResult=0/State=Ready ⇒ 它没重拉。
根因:Restart* 只在本任务自己非零退出时生效;而脚本里若是前台阻塞调用常驻,
常驻死掉 ⇒ 脚本正常返回 0 ⇒ 任务被判「成功」⇒ 永不触发
(测试3 复盘原话:「自愈机制被成功退出这三个字废掉了」)。
⇒ ✅ 正解=脚本里写「永不返回的守护循环」(while($true) 包住 + Start-Process -Wait),
⛔ 不是靠计划任务的 Restart*。⚠️ & 对 pythonw.exe(GUI 子系统)不阻塞
⇒ 循环会误判"刚起的已退出"⇒ 叠出多个常驻(测试3 实测 5/10/20/40s 连续重拉)。
🔴🔴 追加实测(2026-10-04 07:03):守护循环必须看停止标志 guard.stop** ——
目标已完成(--set-life 已完成 已写标志、常驻已优雅退出),但守护循环仍每 60 秒重拉、每次秒退
⇒ 实测空转 370 次/6 小时。根因=守卫只问「心跳新鲜吗」,⛔ 不问「目标还活着吗」
⇒ "完成即收工"只做到"停掉常驻",没做到"别再拉它"(同一件事的两半)。
✅ 循环开头 + 退避等待中都查标志,在则 Say 一行后 exit 0(⛔ 不 Say ⇒ 又成"静默消失")。
实测:新 keeper 0 秒识别 ⇒ 任务 State=Ready(⛔ 不是 Running)⇒ 重拉 372 → 372。
⚠️ 验「是否被回收」必须只做单一变量(⛔ 不许同时停/起任务)—— 否则因果会搞错。
⚠️ 验收要静置 ≥ 12 分钟(⛔ 短于 10 分钟证明不了任何事 —— 本轮就死在第 12 分钟)。
📌 另两条正常行为别当故障:静默基准取台账 mtime ⇒ 改排期会归零计时;
_all_sessions_idle() 要求"所有会话都结束" ⇒ 发起会话自己也在里面 ⇒ 结束它检查会话才能建。
P0-42 🔴🔴 判据里"起真进程写夹具"会写坏真数据 —— 隔离必须用 COLLABD_CONFIG(★ 2026-10-04 实测,代价=真 goal.json 被覆盖)
症状:给自检加"端到端真跑 goal_state()"的判据,跑完发现生产 goal.json 变成 230 字节、
只剩 1 条判据,目标文件夹 名从 目标-本机协作-3e3182 变成 目标-未命名目标-35279e。
根因:load_cfg() 的候选顺序是 ① COLLABD_CONFIG → ② <ws>/.workbuddy/collab/collabd.config.json
⇒ 只设 DSH_COLLAB_WS/DSH_WS_ROOT 会被忽略(真源压根不读它们定 workspace)⇒ 回落读到真配置
⇒ 判据的 goal.json 写进了真工作区。⚠️ 连 TG(任务图)也是同一份 config 决定的,一并读到真的。
⚠️⚠️ 更隐蔽的一层:DSH_WS_ROOT/DSH_COLLAB_WS 在 selftest.py 里是它自己的测试工作区根
(WS = ... or DSH_WS_ROOT,_prepare() 拿它建 tmp/selftest)⇒ 拿它拼"生产路径"=把夹具当生产;
演练时我设了个 X:/nonexistent ⇒ 直接 FileNotFoundError: 'X://' 崩在 _prepare()。
✅ 正解(t_acc_selfref_excluded 的端到端段照此写):
① 写一份临时 config 并 os.environ["COLLABD_CONFIG"] = <它>(含 workspace/inbox/taskgraph);
② 加载模块后先自检 str(m.INBOX) 落在临时目录内,不在就直接报红退出(⛔ 宁可红也别再写真文件);
③ 需要"生产工作区"的判据(如 t_execution_doc)另认专用 COLLABD_PROD_CONFIG,
由 install.py 写进 roots.env;⛔ 不用 DSH_WS_ROOT 拼。
📌 验收纪律:凡新增"会写文件的自检",跑前先 md5sum 目标真文件、跑后再核一次。
(本轮恢复源=.workbuddy/collab/bak-goalctl-20261002/goal.json,⛔ 那份缺 execution_doc 字段,
恢复后要按 exec_doc_rel() 算出的真值补回,否则 t_execution_doc 假红。)
P0-43 🔴 判据写成"函数体里含某串" ⇒ 同一个符号在函数里出现两次就抓不住变异(★ 2026-10-04 变异验证实测)
症状:判据写 "_acc_is_selfref" in fn_goal_state(fn=函数源码文本)⇒ 变异把
goal_state() 里 _real 那处的调用删掉,判据照样 PASS ⇒ 假绿。
根因:goal_state() 里 _acc_is_selfref 出现两次(_real 的推导式 + _dropped 的推导式)。
同族第二例:判据写 re.search(r"if not _real.*?undeclared", fn) ⇒ 变异把中间那个
return "undeclared" 拆掉照样 PASS —— 因为函数尾部 return "pass" if eff else "undeclared"
里还有一个 undeclared(正则一路匹配过去了)。
✅ 正解=上 AST,按结构定位(本轮 t_acc_selfref_excluded 照此):
- 「
X = [...]里有没有调f」⇒ 找Assign(targets[0].id == "X"), ⛔ 不是找ListComp—— 推导式的 target 是(_k, _v)/k,_real只在赋值左侧。 - 过滤条件在生成器的
ifs子句里(if ... and not f(...)),⛔ 不在elt。 ⚠️⚠️ListComp.elt是单个节点(有时还是Tuple)⇒list(elt)抛TypeError: 'Tuple' object is not iterable(本轮真踩)。 - 「
if not X:的 body 里有没有return "..."」⇒ 走If节点的body,⛔ 不用跨节点正则。 📌 另:行为级判据最硬 —— 真造goal.json跑真goal_state(),断言返回值 (本轮 4 个场景:全自指⇒undeclared/自指+真判据全过⇒pass/真判据没过⇒open/自指写过但真判据没过⇒open)。⚠️ 夹具判据文本必须用真源真会写的形状(过|…, ⛔ 不带🔴前缀 ——_acc_is_pass取竖线左边那段判,🔴 过的 head 仍带🔴⇒ 假红)。
P0-44 🔴 技能包没有版本控制兜底 ⇒ 清理前必须先建包外快照(★ 2026-10-04 实测)
症状:用户问「技能相关文件有没有可以整合、精简、移除的重复冗余」,
体检算出 10.7 MB 里9.1 MB 是冗余(68 个 *.bak-* 占 8.6 MB),我上一轮顺口说过
「git 已有历史」—— 🔴 这句是错的。
取证(三级都查了):.workbuddy//E:/ProgramData//E:/ 全都没有 .git;
文档库 dsh-server-docs/08-skills/ 只收了另外 7 个技能,没有 session-mechanism
⇒ 在这个机器上,删掉的文件就是唯一副本没了。
✅ 正解(清包三步,顺序⛔ 不许换):
① 先建包外快照:cp -r <包> <WS>/归档/技能包快照/session-mechanism-<日期>/ + md5 抽检;
② 再删(*.bak-*/__pycache__/.pyc/install.log/零引用文件);
③ 删完必须现算验收:selftest.py + install.py --verify + 看板 headless 渲染。
📌 死证据引用要一起改:注释里写「原文照抄见备份 X.bak-…」⇒ 备份一删那句话就成了假话
(本轮改掉 6 处:collabd.py×1、board.html×2、SKILL.md×2…含 L6)。判据见 selftest.py::t_pkg_hygiene。
P0-45 🔴 判据别去判"运行时产物"和"该长的东西",否则永远红(★ 2026-10-04 实测栽了三轮)
给包体卫生写判据时,连踩四个坑(都是判据错,不是代码错):
| 想判的 | ⛔ 错误写法 | 为什么必然红 | ✅ 正确写法 |
|---|---|---|---|
| 缓存 | 「包里不许有 __pycache__/.pyc」 |
hooks/_env.py 一被导入就生成 scripts/hooks/__pycache__/_env.cpython-313.pyc ⇒ 跑一次 selftest 就长回来 |
判「无散落 .pyc」(只能在 __pycache__ 内) |
| 运行日志 | 「包里不许有 install.log」 |
install.py:96,340 每次运行都 append |
判「不超 64 KB」 |
| 文档超长行 | 「SKILL.md 无超 2 KB 的行」 |
L3 description: 本来就 2.2 KB(技能描述就该长) |
豁免 description: + 含「作废」的历史段(它长是因为把作废原因写全了) |
| 「本条为准」字样 | 「台账里不许有『本条为准』」 | 新台账 L6 自己在解释这个机制,L24/L98/L267 各段也都合法用它 | 判行体积(当年 L6 单行 55 KB = 全文 66%),⛔ 不判字样 |
⚠️ 另:死证据扫描会自我误报 —— 判据自己的注释里就写着 settings.json.bak-session-mechanism-*
⇒ 扫全包必然命中自己。✅ 判据要显式排除 install.py(备份在 .workbuddy/,是活机制名)
+ selftest.py 自己(P0-16 同族:判据把自己当被检对象)。
📌 变异验证时也栽过一次:注入死证据用的锚点 """使用方 压根不存在 ⇒ 变异没注入成功
却 Prints 出一条「漏网」结论。⇒ 变异必须断言 assert 新串 in 文件,⛔ 别让"没注入"伪装成"判据有洞"。
P0-46 🔴🔴 "单包自包含"的两个静默失效点(★ 2026-10-04 实测,只搬一个技能到空目录演练坐实)
症状:只复制 session-mechanism 一个技能到别的机器 → 机制看起来都在(7 个 hook 全 rc=0),
但回复排版闸门整条静默失效 —— reply-style-guard.py 输出空 JSON,日志里看不出是"机制坏"还是"没配规则"。
🔴 失效点 ①:hook 读的是「另一个技能」的文件
reply-style-guard.py::_core() 第②级回退硬指向 agent-operating-rules/references/回复排版-核心块.md。
✅ 正解=把那份核心块内联进本包 references/03-回复排版-核心块.md(内容守恒、逐字搬),
_core() 改三级回退:工作区规则文件 → 包内件 → 外部兜底。⚠️ 顺序⛔ 不许换。
🔴 失效点 ②:技能库定位拿「别的技能」当识别特征(更隐蔽)
hooks/_env.py::_SNIFF = "agent-operating-rules" —— 它是技能库在不在的判据,
skills_root() / env_check() 三处都拿 (root)/_SNIFF).is_dir() 当真源。
⇒ 只装本包时永远判否 ⇒ 全部降级。✅ 正解=改候选数组 _SNIFFS = ("session-mechanism", "agent-operating-rules"),
新增 _has_skill()(任一候选存在即认)。⚠️ 第一个元素必须 = 本包自己。
⚠️ 改造时我自己踩了两个坑(都是"文件在、读不到"型):
- 上溯少一级:
__file__=<包>/scripts/hooks/x.py⇒dirname①=hooks/ ②=scripts/ ③=包根。 写成两层就落到scripts/⇒ 找不到references/⇒ 静默空输出。 ⇒ 判据必须查os.path.dirname(os.path.dirname(_hooks))这个确切形状,⛔ 查"文件存在"是假绿。 - 取证本身也有缺口:第一轮单包演练"注入 654 字节✅",但日志显示源是工作区的
CODEBUDDY.md(第①级回退)⇒ 根本没验到包内件。✅ 正解=在一个没有CODEBUDDY.md的干净工作区里重验, 源日志必须显示包内件路径才算数。
📌 判据 t_single_package_selfcontained 的两个教训(变异验证实测):
① 查标记必须用 _extract() 的完整字面量 <!-- REPLY-CORE:END -->,
⛔ 查子串 REPLY-CORE:END ⇒ 把它改成 END(改坏) 子串仍命中、而真提取器已认不出(假绿)。
② 变异脚本自己的过滤也会造假绿:按用例标题过滤时把"用例标题行"一起滤掉 ⇒ 明明报红了却统计成 0。
⇒ 变异计数**⛔ 不用标题行**,只数子项的 ✗。
P0-47 🔴🔴 变异脚本把「判据自身语法错」当成了「判据抓不住」(★ 2026-10-04 实测,判据编写者的自保要点)
症状:给判据加完新项,跑变异验证 → 4 个变异全部「0 报红」,看起来像"判据不可证伪"。
真因=新写的判据行里嵌了中文直角引号((⛔ 那是"去别处找判据"的形状))
⇒ selftest.py 整份语法错、跑不出任何输出 ⇒ 变异脚本统计「子项 ✗ = 0」
⇒ "没有输出"被当成"没有报红" ⇒ 结论完全反了(真身是"判据根本没跑")。
🔴 根因有两层,缺一不可:
① 中文引号坑(与既有铁律「含反引号一律 Write/Edit」同类):"…" 嵌进 "…" 字符串即语法错。
② 变异脚本没有防空转护栏 —— ⚠️ 这是更值钱的一半:
"判据没输出" 与 "判据全绿" 在计数上长得一模一样(都是 0 个 ✗)。
✅ 变异脚本必须带两道护栏(缺任一道,一次误判就够):
# 护栏 1:判据源文件必须语法正确
ast.parse(st.read_text(encoding="utf-8")) # SyntaxError ⇒ 直接判「运行异常」
# 护栏 2:必须真找到那条用例
if not any("用例标题" in l for l in out.splitlines()):
return 99# ⛔ 过滤失配 ≠ 全绿
📌 另:别用 grep -A N 数子项(窗口会切掉长输出,实测"6 项"其实是 28 项)⇒ ⛔ 用 Python 按
"下一条 ✓/ ✗ 标题行为止"切块。
⚠️ 同族第三发(今天同一天栽的另两个假绿):P0-43(判据写"函数体含某串"⇒ 同符号出现两次就抓不住)· 本条 P0-45 的排版判据(查子串 ⇒ 同一文件里出现两次 ⇒ 改一处仍命中)。 ⇒ 一句话总纲:判据的每个断言都要锚在「结构」或「唯一位置」上;⛔ 锚在"文本出现过"上必出假绿。
P0-48 🔴🔴 判样式表:注释里正当引用着「旧写法」,不剥注释必假绿;等宽要解析式(★ 2026-10-04)
📌 只管「样式表里带注释的那类判据」(⛔ 不是所有代码判据);带注释是因为注释里常要写"为什么改"。
症状:给 .tab 加"等宽"判据 ⇒ 变异「flex:1 1 0 改回 min-width:180px」🟢 绿;
判据里写「⛔ 不许有 min-width:1xxpx」,那串明明就在文件里 —— 在注释里。
根因:那段说明注释正当引用了改之前的旧写法("为什么改"的证据,⛔ 不许删)⇒ 全文件查旧写法永远命中。
✅ 正解(缺一步仍假绿):
css = re.sub(r"/\*.*?\*/", "", html, flags=re.S) # ① 剥注释
tab = re.search(r"\.tab\{[^}]*\}", css).group(0) # ② 只取那一条规则块(⛔ 不量全文件)
③ 判"等宽"必须解析式:flex:1 1 0(basis=0 ⇒ 等分,✅)对比三种不等的写法 ——
flex:1(省略 basis ⇒ 默认 1 1 auto,按内容)· flex:1 1 auto(按内容)·
flex:0 1 auto(不生长,宽=内容)—— ⛔ 子串法与正确写法分不开
⇒ re.search(r"flex:\s*(\d+)\s+(\d+)\s+([0-9a-z%]+)") 判 grow>=1 and basis=="0"。
④ 🔴 配套:min-width:0 缺了会被子元素 min-content 顶开 ⇒ 看着仍不等宽 ⇒ 必须同时断它。
📌 _R = [...] 是列表字面量,⛔ 不能在中间插 _R.append(语法错 ⇒ 触发 P0-47 静默)。
📌 附带(同型可独立用):⛔ 扫文本找"裸断言"的判据不成立 ——
会扫到自己的扫描代码(实测 "== 0) or" in ln 命中那行本身)⇒ 恒红。
P0-49 🔴🔴 台账/文档里的读数是「快照」——引用前必须看它什么时候写的(★ 2026-10-04)
症状:acceptance_state 说「自测 PASS 48」⇒ 真跑 73;说「pid 8024 活」⇒ 现活 19424。
根因:每条带 _更新 时间戳(本例停在 10-02)⇒ 那是**"当时验过"**,⛔ 不是"现在仍成立"。
凡带「现在/已/还差」的结论,读数一律现取。 pid · 心跳 · 计数 · pass 数 · 端口 · 排期状态——全会变。
🔴 同日更阴的一发(我据此报错结论,用户当场质疑):由"读数旧"推出「常驻没在跑」——是我错。
错在取证手段:① 落点猜错(照旧文案找 tmp/supervise-inbox/,真身是 collabd.SUP_HB 指向的
.workbuddy/collab/logs/);② 🔴 glob("**/x") 不穿透 .workbuddy 这类点目录 ⇒ 零命中
⇒ 把「查不到」当成「不存在」。
⇒ 同型(不是同一条):P0-48 讲的是样式表判据,这里讲的是取证锚点 ——
共同教训是「锚点自己没先验 ⇒ 结论错」,⛔ 别把 P0-48 当成也管取证。
✅ 取证纪律:查机制状态只许问那个程序自己(import collabd; collabd.supervise_alive()、
读路径常量 SUP_HB),⛔ 不许 glob、不许手拼路径、不许照抄文档旧落点。
📌 已落判据 t_supervise_ensure(在 session-mechanism 包的 scripts/selftest.py,⛔ 不在本工作区)。
📌 另一类:制度性不可能项 —— 判据要求的对象已整套退役(配置 wake_enable=false + 源码里投递段已真删)
⇒ 它会永远 🔴,不是坏了。⛔ 这种只能用户拍板(作废/改写口径/保持),AI 不得擅改验收口径。
P0-50 🔴🔴 忙判据只读一个日志源 ⇒ 宿主不写那份时「必然误判空闲」⇒ 往正在跑的会话插话(★ 2026-10-04 vibe-product 实测坐实)
症状:vibe-product 一条会话(b232218f…)明明在转 —— state-machine:transition → working 358 次、
idle 仅 3 次、用户只发言 4 次、跨 2h46m —— 而 _target_busy() 返回 False(没在跑)。
⚠️ 2026-10-04 晚间复验:这组数字对不上,⛔ 别再当证据引用(结论"在转"仍成立)。 实测:日志已续写,现为
to:"working"438 次(含from:"working"自环 326)、to:"idle"4 次、method:sendPrompt4 次(✅ 这个对得上)。 ⚠️ 大概率是当初数的是另一个口径(如只数某个时间窗/某个input值)—— ⇒ 教训:数字结论必须连口径一起记(数的是哪个字段、哪个范围),⛔ 只留结果不留口径。
🔴 根因(可证伪):_session_busy() 只读工作区级日志 <ws>__<hash>.log。
实测那份日志里 SessionRunStateMachine 615 条,但这个 sid 一条都没有(全文件 0 次)
⇒ 宿主不为该会话写工作区级状态机日志 ⇒ 恒得 '' ⇒ 老逻辑「读不到 ⇒ 按没在跑处理」必然误判。
⚠️ 这个 fail 方向比"恒真"更隐蔽:恒真是"永远不敢投"(用户看得见);误判空闲是"往正在跑的会话里插话"(静默)。
✅ 正解=双源:工作区读不到 ⇒ 回落会话级日志 <logs>/<日期>/sdk/conversations/<sid>.log。
⚠️ 两个源字段完全不同(⛔ 拿错正则 = 恒得 0 条 = 白加):
| 源 | 事件名 | 取值字段 |
|---|---|---|
| 工作区级 | [SessionRunStateMachine] … busy=true |
busy= |
| 会话级 | state-machine:transition {"from":…,"to":…} |
to(working/planning=忙) |
⚠️ sid 形态不一致(实测栽过):文件名叫 b232218f-c8e6-…(36 位带连字符),
而调用方常拿库里 b232218fc8e6…(32 位无连字符)⇒ 只拼一种 恒不命中 ⇒ 回落源恒 '' ⇒ 修了个寂寞。
⇒ 两种形态都试 + 前缀唯一命中兜底。
📌 判据:t_srsm_busy ⑦~⑬(6 条)+ 变异 6/6 报红(删回落/写死不救/正则混用/删形态兼容/取首条/越权)。
📌 顺带:会话级日志格式不是 JSONL,是 ISO时间 事件名 {JSON} ⇒ 按 JSONL 解析会得 0 条(⛔ 已栽)。
P0-51 🔴🔴 技能副本与全局会分叉:修完全局忘了同步,副本继续用旧缺陷(★ 2026-10-04 vibe-product 实测)
症状:全局技能库修好一个 P0 级缺陷(忙判据双源),用户那边照旧出错 ——
因为那个工作区用的是复制到工作区的副本(.workbuddy/skills/<包>/),⛔ 不会自动跟随全局。
🔴 本轮实测的量:副本 47 个文件里 落后 3 个(collabd.py/selftest.py/pitfalls.md),
其余 45 个 md5 已一致。这 3 个不补,副本就带着旧缺陷跑。
✅ 同步四步(⛔ 别整目录覆盖):
- 先逐文件 md5 双向 diff —— ⛔ 用文件数当判据会错(副本多一份《副本使用说明.md》)。
- 只覆盖真差异 —— ⛔
roots.env绝不覆盖:它是工作区专属指针(setdefault兜底 ⇒ 覆盖会让副本去读写另一个仓库)。 - 备份挪出包体 —— 同步完把备份移到
<工作区>/交付物/;留在包内会被t_pkg_hygiene的「⛔ 不许长回备份」判红(实测副本自测 FAIL 1)。 - 🔴 副本必须真跑一次 —— ⛔「编译过」≠「能跑」。本轮副本首跑 FAIL 4(修完仍 FAIL 2 ⇒ 逐条查)。
🔴 副本自测的独立坑(比同步本身更隐蔽):有些判据量的是**「生产工作区部署齐全」**
(交付物/目标执行状态.md、跨区 peer_workspaces 配置、本工作区的 collabd.py 副本)
⇒ 在没启用机制的工作区(无 tmp/supervise-inbox/goal.json)里必然报红,
而那不是技能有错。
✅ 正解=加 MECH_ON 开关(判「真源文件在不在」,⛔ 不看 env 不猜),未启用时跳过并说明;
⛔ 不许改成"永远绿" —— 那是把判据废掉。
📌 另一例:判据「glob 会漏点目录」耦合了「心跳文件必须存在」 ⇒ 副本没起常驻就必红
⇒ 改成自建夹具(造一个 .workbuddy/x/y 文件,验 glob 查不到而 exists 为真)⇒ 任何环境都成立。
📌 收口验收:双向 md5 45/45 一致 + 全局 PASS 75/FAIL 0 + 副本 PASS 75/FAIL 0。
📌 副本《副本使用说明.md》是权威口径(⛔ 别凭猜同步:它写明了「⛔ 不要整目录覆盖」的真实原因)。
P0-52 🔴 忙判据第二源:跨日文件按遍历顺序覆盖 + 没有新鲜度闸 ⇒ 已收工会话静默饿死(★ 2026-10-04 vibe-product 实测坐实)
症状:skipped="target-busy" 反复出现,目标会话明明已收工/已死活,却永远收不到上报,且无任何告警。
🔴 根因(两个,同向) —— 都在 _session_busy_conv()(会话级日志源):
- 跨日覆盖:
_conv_log()返回[今天, 昨天],而取状态用的是遍历到的最后一条 ⇒ 昨天卡住的working把今天已经idle的盖掉。 - 无新鲜度闸:
_session_busy()有SRSM_FRESH(900s),本源一个都没有 ⇒ 3 小时前卡住的working永久判"在跑"。
⚠️ 两个缺陷 fail 方向完全相同(都判busy)⇒ 后果同一条:
busy ⇒ skipped="target-busy" ⇒ 静默饿死。比"恒真"更隐蔽:恒真是"不敢投"(用户看得见)。
✅ 正解:跨文件取时间戳最新的一条(⛔ 不是遍历到的最后一条)+ 补 SRSM_FRESH 新鲜度闸。
⚠️ 反方向也要修:闸不能写太狠(窗口=0)⇒ 会把正在跑的判成空闲 ⇒ 变成"往在跑的会话插话"。
⇒ 必须同时有反向回归判据:2 分钟前的 working 仍须判 busy。
🔴🔴 时基不一致(隐蔽坑):会话级日志行尾是 Z(UTC),工作区级是 [2026/10/4 14:15:31]
(本地时间、无 Z)⇒ 解析必须分别按各自口径,⛔ 混用 time.mktime 会差 8 小时(东八区)
⇒ 「2 分钟前」被算成「8 小时前」⇒ 新鲜度闸恒判过期 ⇒ 真在跑的会话被误判空闲。
⇒ 会话级用 calendar.timegm(UTC 语义),⛔ 不是 time.mktime。
📌 判据:t_srsm_busy ⑭~⑰(4 条:跨日取最新 / 新鲜度闸 / 反向回归 / UTC 时区)
+ 变异 9/9 报红(每条判据 ≥2 个变异体)。脚本 scripts/mut_run.py。
🔴🔴 本条同时纠正 P0-50 的一个错误结论(⛔ 错误结论必须从代码里删掉,不能只加新条):
P0-50 与 _target_busy 注释都写「该 sid 在工作区日志里一条都没有(宿主不写)」——
实测是错的:同一份工作区日志 SessionRunStateMachine 1935 条、含该 sid 695 条
(末条 14:15:31 AGENT_ENDED … busy=false)。
✅ 真实成因是候选集为空:_log_dirs() 靠 WS 反推工作区名,WS 一旦落在别处
(如从别的目录 import)就拼不出任何 <ws>__*.log ⇒ 返回 [] ⇒ 恒 ''。
⇒ 双源仍然必要(第二源不依赖 WS),但理由要换,⛔ 别再引用"宿主不写"。
📌 变异验证自身栽的两处(比判据本身更值得记):
① 等价变异:key[1] >= last_t[1](只把元组比较换成 seq)恒绿 —— 样本里每个文件内部
本来就升序,任何"取最后一条"的写法都等价 ⇒ 变异必须动语义(时间戳参与/不参与)。
② 🔴🔴 测的不是被检对象:变异脚本把 selftest.py 复制到临时目录,却用原版绝对路径调用
⇒ 而 selftest.py 里 imp() 加载的是 HERE/"collabd.py"(HERE 来自 __file__)⇒
测的一直是原版 ⇒ 7/7 全"假绿"且表面毫无异常。
✅ 正解:用工作目录里的那份 selftest.py,并断言输出里能看到该工作目录;
再加一道变异标记(# MUTANT:)⇒ 证明跑的那份真含变异。
这就是「判据自己恒绿」最隐蔽的一种:不是判据写错,是根本没测到它。
P0-52 🔴🔴 知识层记过了,执行层照栽 ⇒ 缺的不是文档,是「机器抓手」(★ 2026-10-04 收口)
症状:P0-51 早就把「变异脚本要测被检对象、⛔ 别拷到临时目录」写得清清楚楚,
references/00-动手前必过.md 也立了五条纪律 —— 当天我自己照栽三次。
⇒ ⚠️ 不是「不知道」,是「记不住」。文档对检索有效,对执行无效。
🔴 把当天的坑收敛,只有两个根因(⛔ 不是六条各自独立的失误,写流水账等于没归因):
| 根因 | 当天表现 | 机器抓手 |
|---|---|---|
| A. 判据与被检对象的「连接」没被验证 | 判据里 lambda 自己重算;锚在「文本出现过」;夹具只覆盖顺带跑到的分支;锚点凭记忆写 |
judge_audit.py --audit |
| B. 验证动作本身出错,而我不检查它 | 变异脚本自己 IndexError 却照样打 PASS;多轮变异只重置一份文件 ⇒ 累积污染 |
judge_audit.py --mutate |
⚠️ 两者的共同签名:判据绿灯与被检对象状态无关。 ✅ 正解=让验证器自己的失败变成读数(rc≠0 + 明说原因),⛔ 不许它输出得像结论。 📌 当天实测:工具报「⛔ 没报红 ❌」并 rc=1,比假的「PASS 2/FAIL 0」有用得多 —— 至少我知道要重查变异体。
🔴 ⛔ 元规则本身也会假红:--audit 扫写法,分不清「引用教训」与「犯错误」
(实测把「自建夹具证明 glob 会漏」这条教训本身判成错误用法)⇒ 误报进 FALSE_POSITIVE
且必须写清为什么,⛔ 不许默默加(默默加 = 把体温计砸了)。
📌 附带坐实:glob-existence 这条规则在 selftest.py 里命中 2759 行,
而那处正是该教训的判据本体 ⇒ 元规则会指向「反例教材」,⛔ 别把它当 bug 修掉。
P0-53 🔴🔴 合成区夹具必须把配置里的 workspace 也改成自己(★ 2026-10-04 端到端实测,白跑一轮)
症状:造合成区喂钩子 stdin,钩子 0.4 s 零输出,看着像 「钩子没生效 / 判据失效」;而同一份代码在真区是好的。
根因(实测坐实,⛔ 非推断):夹具只复制了 collabd.config.json 的字节,
没改里面的 workspace 字段 —— 它还指着真区。
⇒ collabd.load_cfg() 加载的是真区配置 ⇒ CFG_USED 校验通过(因为 env 指合成区、
但配置内容读的是真区的 workspace)⇒ supervise_alive() 读到真区的心跳。
⇒ 而真区常驻确实活着 ⇒ 钩子正确地判定「在位」⇒ 正确地 _noop()。
🔴🔴 最危险的地方:钩子的行为完全正确,是我的夹具在说谎。 ⇒ 如果不查下去,结论就会写成「钩子没生效」——又一次错误归因(同 P0-20 的形状)。
✅ 判据/夹具要点:
- 合成区配置必须深拷贝后改
workspace= 合成区自己,⛔ 不许read_bytes()/write_bytes()原样搬。 - 反向自检:夹具建好后先断言
supervise_alive()在合成区报"不在位", 再去喂钩子 —— 否则测的是"真区刚好活着"。 - 🔴 通用形状:凡是"复制一份配置来隔离"的做法,都要检查配置里有没有指向原环境的绝对路径。
P0-54 🔴🔴 判据里搜「某个字符串出现过」要剥注释;等宽要按函数体切片(★ 2026-10-04,P0-48 的同族第 2 次)
症状:新写的「单一事实源」判据假红 —— 明明 peer_supervise_sweep() 已经改成
调 goal_life_of(root),判据却报「它自己又抄了一遍 lifecycle 判定」。
根因:该函数紧接着就有一段注释,正当引用着旧写法当反面教材
(「原来这里自己判 get("lifecycle")…」)⇒ 裸串搜命中的是注释。
✅ 正解=只留代码行(line.lstrip().startswith("#") 之外的行)再判;
⚠️ 本文件通篇充满「旧写法作证」的注释 ⇒ 这条在全库范围内都会复发。
📌 与 P0-48 合起来是一条通用纪律: 判源码时,「注释」既不是代码(⛔ 别当证据),也不该被删(它是教材)⇒ 只能在判据侧剥掉。
P0-55 🔴🔴 测了零件、没测装配 —— 断言只测「那个函数返回什么」,⛔ 不测「它被用上了没」(★ 2026-10-04 变异验证当场抓到)
症状:为守新加的「退役角色闸」写了 4 条断言,PASS 88 / FAIL 0 全绿;
把闸从 _scan_mains() 的筛子里整段拆掉(变异①)⇒ 判据照样全绿。
grep -c MUTANT = 1 证明变异真植进去了 ⇒ 那个"全绿"是假的。
根因:4 条断言全部形如 m.is_retired_role_title("[跟进]-X") ——
测的是零件本身(小函数对几个样本返回什么),没有任何一条测到
「这个闸有没有被接到筛选逻辑上」。⇒ 一个恒绿的判定标准。
✅ 正解=行为级用例:造一个真宿主库(含一条 [跟进]-… + 一条真主会话 + 一条正牌 worker),
真跑一遍 _scan_mains(),看结果里有没有那条退役角色。
三条断言缺一不可:① 退役角色不进候选 ② 真主会话不许被误排(反例对照)③ worker 仍被排(旧口径未被破坏)。
⚠️ 同族第 2 个坑(同一轮里连踩):夹具 insert 时把 custom_title 写成 "",
而查询是 coalesce(custom_title, title, '') —— 空串不是 NULL ⇒ 不回落到 title
⇒ 每条会话拿到的标题都是空串 ⇒ 用例恒绿而毫无意义(第二次"测了零件没测装配")。
真库里该列实测是 None ⇒ 夹具必须照抄真库形态(= P0-53 的同一个形状)。
📌 通用形状:加了「闸/过滤/开关」这类东西,必须有一条断言走完整装配路径; 只测开关函数本身 ⇒ 拆掉接线也能全绿。变异验证是唯一能抓到它的手段 (⛔ 别用"我读了一遍代码觉得接上了"代替)。
P0-57 🔴🔴 启动脚本没跟着改口径 ⇒ 本区发布物成了「孤儿副本」,规则形同虚设(★ 2026-10-04 用户点破:「让它跑本工作的那份,不是跑全局技能里的那份」)
症状:某工作区常驻反复死亡重起(_collabd.log 7 次「常驻续命」),
主会话开工第 0 步自查时发现:心跳 argv0 指的既不是本区发布物,也不是「全局技能」,
而是「本区技能副本」 —— 看起来像"跑对了",实际判据②不过。
根因(两层,缺一不可):
- 引用没跟着口径改:
tmp/start_supervise.py第 11 行写死WS + "/.workbuddy/skills/session-mechanism/scripts/collabd.py"(技能副本), 而口径(deploy_code.py开头,用户 10-03 定案)要求各跑<工作区>/.workbuddy/collab/collabd.py(本区发布物)。 deploy_code.py只管分发、⛔ 不管"谁引用它" ⇒ 文件铺下去了,没人指向它。
🔴 全工作区 grep 的实测结果:引用本区发布物的只有 0 处(除了注释与文档里的说明文字)。
⚠️ 唯一引用它的载体 start-supervise.ps1 又从没执行过(首行被误打成三引号 ⇒ 语法错 9 处秒退 + 从没登记计划任务)。
为什么难查:文件在 ≠ 有人跑它。三条自查只做「md5 一致 + 心跳存在」会全绿, 必须加第三条:grep 启动脚本引用的路径。今天就是这么漏的。
✅ 修法(三层一起补,⛔ 少一层就复发):
- ① 启动脚本改成两段式:优先本区发布物,缺了才回落技能副本,且必须打印告警(⛔ 静默回落=用户以为跑的是副本)。
- ② 修完必须重启常驻 —— 判据仍看
argv0(⛔ 改文件不重启=没生效,P0-17 同族)。 - ③ 开工第 0 步自查加第三条:
grep启动脚本(tmp/*.py、*.ps1、计划任务Args)⇒ 路径逐字是本区发布物。
📌 通用形状:「改了目录结构/改了落点」之后,必须回头检查「谁引用它」 —— 引用点常在配置、启动脚本、计划任务参数里,⛔ 不在被移动的那个文件里。 同族:P0-51(副本分叉)、P0-17(改完不重启)。
P0-62 🔴🔴 常驻「自我繁殖」:续命时照着「当前这份」抄 ⇒ 弹窗洪水 + CPU 白烧(★ 2026-10-05 用户投诉:「越创建越多 要创建 1W个嘛」)
症状:用户屏幕上不断弹终端窗口 —— 实测一次盘点出 7 条 keeper + 10 条常驻 + 1 个 .cmd,
且技能目录那份 collabd.py --supervise 反复复现(父进程每次都不同、且已消失 ⇒ 看着像幽灵自启)。
用户追问:「这个常驻一直在运行 它不占用CPU 不占用资源吗」。
铁证(_collabd.log 逐字):
常驻续命:新起 --supervise pid=69464(原因:pid 12672 在、心跳新鲜,但**身份对不上**(不是本区 collabd 常驻))
🔴 注意这行的荒诞之处:本区常驻(12672)明明活着、心跳新鲜,却被判「身份对不上」⇒ 又拉一条。
根因(两层,同为 Path(__file__).resolve() 一个写法):
ensure_supervise()起常驻时填Path(__file__).resolve()=「当前正在跑的那一份」; 若它本身是技能目录那份(P0-57 形态)⇒ 续命又起技能目录那份 ⇒ 互为镜像、生生不息。_pid_is_collabd()判「这 pid 是不是本区常驻」的基准同样被技能目录那份顶着 ⇒ 真的本区常驻反被判「不是我」 ⇒ 每轮--ensure都觉得"没人跑" ⇒ 再拉一条。 (与 P0-57 是同族病、不同位置:那个在"生成 ps1",这个在"起进程"与"判身份"。)
放大链路(为什么用户看到的是"弹窗洪水"而非单纯多几条进程):
每条常驻都挂在一条 keeper(start-supervise.ps1 → powershell.exe)下,
keeper 用 Start-Process 拉起子进程时闪一次窗 ⇒ 常驻越多 ⇒ keeper 越多 ⇒ 弹窗越多。
🔴 CPU:主循环每 supervise_interval(本区 10 秒)转一圈 ⇒
每多一条常驻就多一份 10s 节拍 ⇒ 17 条同时转=用户感知的"高占用"。
⚠️ 单条其实很轻(实测 0.020.09 s CPU / 20 s ≈ 0.10.5% 单核)⇒
「常驻吃 CPU」的真因是条数,⛔ 不是单条本身 —— 排查时先数条数,⛔ 别去优化循环体。
✅ 修法=抽唯一基准 _own_path()(发布物优先),三处共用:
def _own_path() -> Path:
"""本区「正宗」的 collabd.py —— 发布物优先,⛔ 不是 `__file__`。"""
pub = Path(WS) / ".workbuddy" / "collab" / "collabd.py"
if pub.is_file():
return pub
return Path(__file__).resolve() # ⚠️ 回落必须打告警(⛔ 静默=用户以为跑的是副本)
接线三处:① ensure_supervise() 的 Popen 目标 ② _pid_is_collabd() 的身份基准
③ _escalate_to_keeper() 的 __SCRIPT__。
🔴🔴 一句话判据(记住这条就够):
凡是回答「以哪份程序为准」的地方,一律走
_own_path(); ⛔ 严禁Path(__file__).resolve()—— 它答的是「正在跑的那份」,而那可能正是错的那份。
📌 通用形状:「当前这份」不是一个可靠的基准 —— 进程可能由错的路径被拉起。 任何「自我复制 / 自我补起 / 自我校验」的逻辑,都必须先钉一个外部锚点 (这里是"本区发布物路径"),⛔ 不能拿"我自己的位置"当锚。同族:P0-57、P0-51。
🔴 回归资产:t_ensure_spawns_published_copy(6 项)+ 变异对照已验红
(把 str(_own_path()) 换回 str(Path(__file__).resolve()) ⇒ 必红)。
⚠️ 排查顺序(省时间):症状是"弹窗多/CPU 高"时,按此序取数,⛔ 别一上来就读代码:
① Get-CimInstance Win32_Process 数 collabd 常驻条数(⛔ 不是看"有没有在跑");
② 看每条的第 4 个参数文件路径 —— 有没有技能目录那份(有 ⇒ 就是本条);
③ 数 keeper 条数(start-supervise.ps1)—— 它≈弹窗数;
④ 读 _collabd.log grep「身份对不上」—— 出现即本条确诊。
P0-56 🔴🔴 把「目标状态」当成「该不该处理队列」的前置条件 ⇒ 队列里的活永远没人接(★ 2026-10-04 用户点破)
症状:某工作区 _collabd.log 刷屏同一行 ——
检查会话:目标状态=已完成(非进行中)⇒ 不建;
而它的 tasks.json 里躺着没干完的件。⇒ 表面上"检查程序在正常运行、只是没到条件",
实际是有活没人干,且永远不会有人干。
病根(一行):maybe_spawn_check_agent() 的闸① 写成了
if life != GOAL_LIFE_RUN: # ⛔ 错
log(...); return None
它让「目标已被标成完成」成为「处理队列」的前置条件 —— 逻辑不通。
用户口径(逐字,2026-10-04):
「首先执行程序要把执行结果 的文本地址或引用写入检查程序的队列, 检查程序必然要去处理队列的情况(是等待工作区所有会话停止后,把队列情况一并处理)」
⇒ 拆成三条,逐条对:
| 用户的话 | 机制里的对应 | 当时状态 |
|---|---|---|
| 执行结果要写地址或引用入队列 | --report --state done 缺 --artifact 直接拒收 |
✅ 早已落地 |
| 检查程序必然要去处理队列 | 闸①(目标须「进行中」) | 🔴 被挡死 —— 就是要改的那一处 |
| 等所有会话停止后一并处理 | _all_sessions_idle() |
✅ 早已存在,对得上 |
🔴 闸① 为什么只该捆在 queue-empty 上:两类检查会话职责不同 ——
sessions-ended(队列非空)= 面对「有活没人干」⇒ 与目标状态无关:活没干完就是没干完。queue-empty(队列为空 + 静默 ≥20 min)= 面对「没活了但目标可能没完」⇒ 它的职责正是判目标该不该收口 ⇒ 目标都不是「进行中」了,这条没有意义 ⇒ 保留闸①。
为什么难查:日志说的是实话("目标状态=已完成"),措辞也像正常的分支说明 ⇒ 一眼看过去像"按设计拦下了",⛔ 不像故障。当天有会话据此写下"这是设计,不是故障"的结论 —— 被用户当场否掉。 📌 教训:日志如实描述条件,⛔ 不等于那个条件是对的。读到"某条闸拦下了"时, 必须回头问一句:这条闸该不该捆在这个条件上?
✅ 修法:
- ① 闸① 收窄 ⇒
if reason == "queue-empty" and life != GOAL_LIFE_RUN:(唯此一处)。 - ② 跨区自愈同理:
peer_supervise_sweep()里"目标非进行中就不拉"的判据一并收窄, 改成先看队列有没有未完成件(有活仍拉)。 - ③ 🔴 抽唯一事实源:新增
queue_pending_of(root)—— 跨区队列计数只此一份实现。 ⛔ 别在自愈代码里另写一段读tasks.json的代码(那正是goal_life_of()当初被抽出来要治的病; 用户口径「禁止重复判定标准」)。 - ④ 试算(
--dry-run)必须与真实闸逐条对齐 —— 改闸不改试算 ⇒ 试算说"不建"、实际"建"(自查发现)。 - ⑤ 端到端 + 变异对照:⛔ 只
grep源码不算证。造临时区真跑两条reason(同场景对拍:sessions-ended必须放行/queue-empty必须拦下), 再植变异回写if life != GOAL_LIFE_RUN:⇒ 断言必须报红(证明判据不恒绿)。
📌 通用形状:「闸」捆错条件是静默故障的高发区 —— 闸描述的是「条件不满足就不动」,⛔ 它不告诉你条件本身选得对不对。 同族:P0-50(忙判据只读一个源)、P0-55(测了零件没测装配)。
P0-58 🔴🔴 只查「有没有写」⛔ 不查「那个文件在不在」⇒ 台账里的 done 指不到东西(★ 2026-10-04 用户点破第二条)
用户口径(逐字):
「之前还说过 执行会话的结果要形成文档,目标也要完成情况的文档, 这样后续检查会话和后续执行会话都可根据文档继续处理,避免全工作区到处找信息」
半落地的形态(这是本条最值得记的地方 —— 不是"没做",是"只做了一半"):
- ✅ 写入侧有闸:
--report --state done缺--artifact拒收(10-03 立)。 - 🔴 读取侧不验:只查
artifact有没有写(非空即放行),⛔ 不查文件是否真存在。 ⇒ 路径打错的/产物被移走的/绕过闸写进去的旧数据,照样算done⇒ 检查会话照它读 读空 ⇒ 又回去到处找 —— 正是要避免的那件事。
🔴 活证据(本区台账,实测):
{"t": {"state": "done"}} ← 没有 artifact/by/t_end,task-events.jsonl 里也没记录
它比拒收闸(10-03)还早 ⇒ 是绕过闸写进去的。 📌 教训:写入侧的闸管不了"已在库的" —— 闸只对新写的生效。 凡"加了闸"的地方,都要再问一句:库里已有的那些怎么办?
当年为什么没查(旧注释逐字,保留以示"理由也会过时"):
「判据只查台账里有没有产物字段,⛔ 不判断文件是否真存在 —— 那是检查会话的活; 在这里查路径会把「相对工作区」写法全误杀」
⇒ 那是把两件事混成一件:「相对路径」从来不是问题,路径拼错/产物没落盘才是。 ⇒ 用"怕误杀"换"不验收",代价是缺口一直开着。
✅ 修法(存在性=硬闸,落点=软判据,⛔ 两层别混):
-
①
artifact_state(tid, rec)= 唯一事实源(ok/missing/gone): 按工作区解析后真去 stat(两种斜杠形态都试),⛔ 不抛异常(一律落gone并附原因)。 -
②
--report done加硬闸:artifact_state() != ok⇒ 拒收并打印试过的路径 (⛔ 不猜、⛔ 不静默放行)。 -
③
artifact_dir_ok()落点判据 = 软判据(⛔ 不拒收): ⚠️ 为什么软的:存量里有落在别处但有效的产物(designs/*.html等)⇒ 硬拒=误杀既有工作流。 但必须打印提醒 + 写日志 —— ⛔ 静默通过=这个合同等于没立。 -
④ 🔴
artifacts_unverifiable()由机制侧预挑,渲染进检查会话 prompt: ⛔ 别让会话自己扫台账判断(每条会话重算、标准还可能不同 ⇒ 与goal_life_of()同病; 用户口径「禁止重复判定标准」)。 -
⑤ 文档合同写进机制(
DOC_CONTRACT_ROWS)—— 三份文档各有唯一作者与唯一时机:文档 谁写 什么时候 <目标目录>/目标执行状态.md目标检查会话 核对完目标状态后 <目标目录>/<棒次>_<事项>_<日期>.md执行会话自己 本轮做完时( --report done之前)tasks.json--report(机器)每次状态转移 🔴 为什么合同必须有:实测三区三种形态 —— 某区只有状态文档(420 B,执行产物没落这儿)/某区有手工写的「过程记录」(非机制要求)/ 本区
S12_*.md落对了。同一套机制、三个区三种形态 ⇒ 缺的不是能力,是合同。
判据(本条的验收,缺一不可):
- 行为级:临时区里真跑 —— 真产物 →
ok;没写 →missing;路径不存在 →gone;--report对这三种分别 放行/拒收/拒收。 - 变异对照:把「验存在」短路 ⇒ 「给了不存在路径」必须从 rc=3 变 rc=0(证明判据不恒绿)。
- 🚨 夹具必须真造出那个文件 —— ⛔ 不能再用
artifact="x/T3.md"这种占位路径 (那正是"报了个打算写的路径"这个要治的病本身;本条的旧自检就栽在这)。
📌 通用形状:「加了写入闸」≠「数据干净了」 —— 闸只拦新写的;存量要靠"读取时判"来兜。同族:P0-52(缺的不是文档,是机器抓手)、 P0-54(剥注释要用 AST,⛔ 不用正则 —— 本条连带修出第 3 次)。
P0-59 🔴🔴 判据与设计目标互斥 ⇒ 每轮必报红:口径改了两周,体检里还在找"已退役的周期钟"(★ 2026-10-04 vibe-product 实测)
症状:session-rules-check.py 每轮都报 ✗ [C] 周期钟缺失 ⇒ 没人在推 / 收 —— 唤醒、跟进,
而这两类会话用户早在 2026-10-03 就要求整套退役(SKILL.md 文首口径块写着「唤醒会话/跟进会话/队列上报
整套退役」)⇒ 判据惩罚的正是"按用户要求删对了的东西"。
🔴 这是"判据与设计目标互斥"最纯粹的一例(与 P0-24 的"不可满足的判据自己就是逃生门"同族): 判据在任何工作区都过不了 ⇒ 每轮红 ⇒ 读报告的人学会的不是"去改",而是"这条不用管" ⇒ 一条恒红的判据会把整张体检表一起废掉。
✅ 修法=换尺子(⛔ 不是删判据):
· 旧:"唤醒/跟进周期钟必须存在" —— 设计要求它们不存在 ⇒ 互斥。
· 新:"在册的退役周期钟必须已 PAUSED" —— 还在 ACTIVE 才说明退役没做干净。
即把"要求有"翻成"有了就不许是活的",判据方向跟着口径一起翻。
· 🔴 同时要修它的下游:⑧「模型可用性」原写 for lst in clocks.values(),
退役后 clocks 直接不存在 ⇒ ② 若只改 ⑦ 不改 ⑧ ⇒ NameError 让整个体检抛异常
(实测:⚠️ 体检自身异常:name 'clocks' is not defined,体检静默失效——比报红更糟)。
⇒ ⑧ 改为扫本工作区全部 recurring 排期(覆盖面只增不减)。
⭐ 顺带治掉一个"恒绿假通过":⑧ 原本只扫"周期钟",而退役后本区 recurring 可能一条都没有
⇒ clocks 为空 ⇒ deaf=[] ⇒ 恒判 ok,哪怕真有 flash + thinking=0 的排期在跑。
⇒ 改成扫全部 recurring 之后才抓得住(变异对照:合成库放一条 glm-4-flash ⇒ 正确报 ✗)。
📌 变异对照(本区真数据测不出,因 recurring=0 ⇒ 必须用合成库):
| 场景 | 期望 | 实测 |
|---|---|---|
退役排期 status=ACTIVE |
⑦ 报 ✗ | ✅ ✗ 已退役角色的周期钟仍在 ACTIVE |
退役排期 status=PAUSED |
⑦ 报 ✓ | ✅ ✓ 已全部 PAUSED |
recurring + flash + thinking=0 |
⑧ 报 ✗ | ✅ ✗ 周期排期会被服务端拒 |
⚠️ 测法自身的坑(我又踩了一次):首版变异脚本三个场景复用同名临时目录 ⇒
场景 B 的文件没清,场景 C 的库里混进两条 ⇒ _recur 计数被覆盖 ⇒ 假报"⑧ 恒绿"。
⇒ 造合成库必须一场景一目录;⛔ 别拿"上一次的输出"当这一场景的结论。
(这与 P0-39「自测不许复用被测环境」是同一个病:隔离不彻底 ⇒ 绿红都不可信。)
🔴 推广判据:凡改了口径(退役 / 改名 / 换供给),必须回扫所有"体检 / 自检 / 门禁"里
是否还有指向旧口径的判据 —— 它们不会自己报错,只会每轮安静地红着(或更糟:安静地绿着)。
本次真因就是:改 SKILL.md 文首口径块时,没有连带扫 scripts/session-rules-check.py 的判据数组。
P0-60 🔴🔴 DETACHED_PROCESS 不脱离作业对象 ⇒ 常驻的「5 秒回验」量不出真相(★ 2026-10-04 vibe-product 实测)
症状:用户报「你的检查程序怎么老停止啊」。日志呈现固定节律 —— 起一条 → 心跳正常跑 20+ 轮(≈10 分钟)→ 下一轮消息进来时报「pid XXXX 已不在进程表」→ 续命新 pid。 看着像:常驻自己在崩。实际是:被外力杀。
真因(一句话):ensure_supervise()(collabd.py:547)用的 flags 是
NO_WINDOW | DETACHED_PROCESS(0x8) | NEW_PROCESS_GROUP(0x200)。
🔴 DETACHED_PROCESS 只脱离控制台,⛔ 不脱离作业对象(Job Object) ⇒ 它只是「看起来脱离了」,
实际仍挂在 WorkBuddy 会话的作业对象上 ⇒ 会话树一被回收,它一起被杀。
(CREATE_BREAKAWAY_FROM_JOB 被本机拒绝 ⇒ 这条路封死,别再试改 flags。)
取证(唯一可信) —— workbuddy-resident-service/scripts/proc_chain.py <pid>:
PID 72088 in_job=True 在作业对象内 ← 🔴 决定性:还在作业里
└─ pythonw.exe
PID 468 —— 已退出
🔴🔴 本条最值钱的一句:回验窗口必须 ≥ 被观察现象的时间尺度。
supervise-ensure-hook.py 的 verify=True 路径只回验 5 秒(for _ in range(12) × 0.4 s)。
而本现象要 10 分钟才显形 ⇒ 5 秒窗口里它「验证通过」了,然后 10 分钟后死掉 ——
这是恒绿的假通过(与 P0-59 同族:判据本身没问题,量法的时间尺度错了)。
⇒ 判「常驻健不健康」的正确量法:静置 ≥2 个会话轮次后,再问 supervise_alive()。
⛔ 不许用「起完立刻验一次活着」当结论。
同型的另外两条(一并记住):
- 别把「现在活着」当「这种起法能长期」 —— pid 19424 曾活 24.2 h,只因为它的父链已退出
(孤儿化后不受会话结束影响)。⇒ 要判「还能活多久」=先验父链 + 读心跳
started_ts。 - 判「是不是被正常停的」要看
guard.stop—— 它在 ⇒supervise_stop()走的收工流程(正当); 它不在 + 进程没了 ⇒ 被外力杀(本轮的判定路径就是这条)。
口径边界(⛔ 别搞反):本现象不是故障。supervise-persistence.md 口径明写:
常态=「调用技能完成目标时起后台任务 + 检查程序」,会话一收工就断,断了自己会补
(实测补起延迟 1~2 分钟)。⭐ 只有「目标做完还要继续跑」才需要计划任务那一层 ——
且那层是可选,⛔ 不许当欠项摊(用户 2026-10-04 当场订正过)。
P0-61 🔴🔴🔴 全局钩子只认「一个」工作区 ⇒ 其余区的常驻全靠会话 cli 拉,会话一收工就被回收(★ 2026-10-04 vibe-product 实测)
用户原话:「为什么就这个工作区一直在出问题,别的会话我看一直没有停止过?」
症状差异(实测同刻对比,ai1net-dsh-server vs vibe-product):
ai1net-dsh-server(一直活着) |
vibe-product(每 10 分钟被续命) |
|
|---|---|---|
心跳 round |
38(23:22:44 起) | 13(23:22:44 起,同一时刻起的) |
续命 why |
tick |
cli(10 次全是) |
本区 hook.log |
有 | 🔴 根本不存在(钩子从没往这区写过一行) |
真因(一行):wb-result-hook.py(全局 hooks 目录唯一一份)用
resolve_ws() 解析出一个工作区,然后 INBOX / TICK_STAMP / HOME_WS 全部按它定死:
WS = resolve_ws() # 🔴 全局只有一份钩子 ⇒ 只会解出一个区
INBOX = os.path.join(WS, "tmp", "supervise-inbox")
TICK_STAMP = os.path.join(INBOX, "_tick.stamp")
实测该函数解析结果:
WS = 'E:/ProgramData/AIProject/ai1net-dsh-server' ← 只认这个区
INBOX = '…\ai1net-dsh-server\tmp\supervise-inbox'
⇒ --tick(顺手续命)只对解析出来的那一个区生效。
其余区(含本区)的 classify() 落到 scope="other",钩子不为其续命,
常驻只能靠 ensure_supervise("cli")(=从会话钩子/我的命令行起)⇒
挂在会话的作业对象上 ⇒ 会话一收工就被回收(实测:起后跑 20+ 轮 ≈10 分钟,下一轮消息进来时已死)。
⚠️ 别再往"作业对象"上归因:实测两区的常驻都是 in_job=True(proc_chain.py 现跑)——
作业对象是共同机制,不是差异点。差异点=「谁拉起它」:
tick 起=脱离会话(拉它的常驻长期活着);cli 起=绑在会话上。
取证方法(三步,缺一不可):
- 同刻对比两区心跳:
round与started_h—— 同一时刻起、round 差一倍 ⇒ 一区被回收过。 - 看续命
why:grep "常驻续命" <区>/logs/_collabd.log | tail——why=tick健康,why=cli病态。 - 看该区有没有
hook.log:tmp/supervise-inbox/hook.log不存在 = 全局钩子从没管过这个区。 🔴 这是最快的一步(一条ls定案)。
修法(⛔ 别改 flags、别改 creationflags —— 那治不了):让常驻由常驻拉起,而不是由会话拉起。
即本区要有自己的"顺手续命"入口:要么把本区纳入 resolve_ws() 的解析范围(多区支持),
要么本区自建一个不依赖会话的入口(见 supervise-persistence.md 场景 A/B 的载体)。
⚠️ 在修好之前,本现象=口径内的常态(会话收工时断、下次事件补回),⛔ 不是"机制坏了"。
同族推广判据:凡「全局唯一一份脚本 + 内部按某个环境量解析出'当前工作区'」的写法,都要问一句: "它解析出的到底是谁?其余实例怎么办?" —— 这类缺陷不会报错, 只会让非默认的那一方长期以"降级方式"运行,且看起来一切正常(心跳在跳、日志在写)。
P0-63 🔴🔴 计划任务直起 pythonw.exe ⇒ 秒退 Result=1、端口从没绑上(★ 2026-10-05 看板载体实测)
现象(连复现 3 次):dsh-board-20099 的动作=
pythonw.exe -u "…\board.py" --serve 20099 ⇒ 每次 Start-ScheduledTask 都
秒退、LastTaskResult=1、端口从未听到。任务 State 立刻回到 Ready。
排除项(逐个做掉,别跳):
- 代码没问题 —— 加一层
runpywrapper(同一pythonw.exe、同一参数、同一工作目录) 跑board.py⇒LastTaskResult=**267009**(=0x41301「任务正在运行」), 日志里明写看板已起:http://127.0.0.1:20099/。 - 命令行没问题 ——
(Get-ScheduledTask).Actions[0]逐字打出来(含ConvertTo-Json) 与手工串完全一致,无多余引号、无 BOM 残留、无转义问题。 - 同一串命令由 Python
Popen起 ⇒ 正常绑端口(poll=None、netstat见 LISTENING)。 ⇒ 差别不在命令、不在代码,在「谁拉起它」。
真因:任务计划程序直接拉起 GUI 子系统程序(pythonw.exe)这条路不可靠。
同族证据就在本技能的 start-supervise.ps1 文件头(早被踩过):
⛔ & $pyw … 是调用运算符、不阻塞 GUI 子系统程序 ⇒ 脚本以为"它退出了";
⛔ .ps1 不带 BOM ⇒ PS 5.1 按 GBK 解码 CJK 路径 ⇒ 任务一秒内 Result=1 退出。
✅ 正解=照 collabd 已验证的 keeper 形状(⛔ 别自创):
- 任务动作改成
powershell.exe:-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "<keeper>.ps1"(⛔ 不加-NonInteractive;⛔ 动作里不要出现pythonw)。 .ps1里用Start-Process -FilePath <pythonw> -ArgumentList … -PassThru -Wait——-Wait给的是真阻塞(这是 keeper 能"守着"子进程的前提)。.ps1必须存成 UTF-8 带 BOM(encoding="utf-8-sig",行尾\r\n)。 落盘后立刻自检前 3 字节 =\xef\xbb\xbf,并用[System.Management.Automation.Language.Parser]::ParseFile()过一遍语法。- keeper 内部:端口上已有本看板 ⇒ 只睡不重拉(判据=
/healthz同时含"ok"与"snapshots", ⛔ 不是「端口开着」)⇒ 避免叠实例。
验收判据(全绿才算完,⛔ 缺一不可):
(Get-ScheduledTask).State=RunningLastTaskResult= 267009(0x41301)—— ⛔ 不是1、⛔ 不是0- 端口
LISTENING的 PID 属主是pythonw.exe,且CommandLine指向该跑的那一份脚本 - 业务面:目标真值/
main_by_topic等在快照里真的出现(⛔ 不看"端口在听"就当成功)
排查顺序(最快路径):
① (Get-ScheduledTask -TaskName X | Get-ScheduledTaskInfo).LastTaskResult
② 看是不是 1 且任务秒回 Ready ⇒ 基本就是这个坑
③ 用 wrapper(runpy)复跑一次:若 wrapper 绿、直起红 ⇒ 定案
同族推广判据:凡「计划任务/服务管理器/CI runner 直接拉起"无控制台"的程序」的写法,都要问一句:
"宿主真能把它的生命周期管住吗?" —— 这类缺陷不报错、不留日志,
只在调度器那侧留一个 Result=1,而手工跑一切正常,极易误判成"代码坏了"。
P0-64 🔴🔴🔴 改完技能包 ≠ 生效:常驻跑的是工作区里的「部署副本」,副本没同步 ⇒ 修复对运行中的进程完全不可见(★ 2026-10-05 看板通过率实测)
现象:board.py::acc_is_pass 已改好、selftest 全绿、跑探针也绿,
但页面上「完成情况」依旧 0 / 9 通过;直接 import board 调 acc_is_pass('已过|…')
返回 True,可服务端 /board.json 的 acc_summary 仍写着 非 pass:['V1'…'V9']。
真因(两层,都要治):
- 同一份代码存在「技能包」与「工作区部署副本」两份,而常驻进程加载的是副本:
board.py由计划任务起,argv0 = 技能目录那份 ⇒ 改了立刻生效 ✅collabd.py由start-supervise.ps1起,argv0 =.workbuddy/collab/collabd.py(工作区副本)⇒ 改了技能包它不知道 ❌- 实测
Get-CimInstance Win32_Process逐条 argv0 才看清:pid 66620 → …\ai1net-dsh-server\.workbuddy\collab\collabd.py --supervise。
- 副本可能停在很久以前:本区副本的
board.pymd5 与技能包差 46 行、collabd.py少 718 字节,副本里ACC_ADV_PREFIX一次都没出现(grep -c= 0)。 ⚠️ 副本不是被人手改过(diff显示差异只有我们那几处新代码)⇒ 是纯陈旧快照, 属于 P0-51「修完全局忘了同步」的同一族第 N 次,只是这次栽的是运行中的常驻不是副本自检。
✅ 正解(四步,⛔ 缺一不可):
- 先取证「谁在跑哪一份」:
Get-CimInstance Win32_Process列python%的ProcessId + CommandLine(⛔ 别靠"我以为它读技能目录")—— 一个工作区里board与collabd可能来自两个不同位置。 - 同步副本:技能包 →
<ws>/.workbuddy/collab/{board.py,collabd.py}, 先shutil.copy2备份成*.bak-sync-<时间戳>,再写临时文件 +os.replace原子替换, 最后逐字 md5 复核(⛔ 不靠"我复制过了")。 ⚠️workspace_mirror.py管不了这个目录 —— 它要求副本里有SKILL.md, 而.workbuddy/collab/是部署目录不是技能副本 ⇒ 会直接⛔ …不像技能副本 ⇒ 不动(本目录曾因此静默跳过,--check报 97 项漂移却没人看)。 - 重启常驻:
Stop-ScheduledTask→ 杀旧 pid →Start-ScheduledTask; 验收=心跳里出现新 pid +argv0指向本区副本 + 任务Running/267009。 ⚠️ 只同步不重启 ⇒ 进程内存里仍是旧 bytecode(实测就是这个状态。 铁证:本机board._acc_summary()返回「全部 pass(9 条)」而服务端返回旧的非 pass列表 —— 同一份磁盘代码,两个结果 ⇒ 差的一定是"谁在跑")。 - 判据落到进程级:确认新 pid 用的副本真含新逻辑(在本区副本目录里
import它自己的模块跑一批用例 + 与board.py交叉核对零打架)。
排查顺序(最快路径):
① 页面/接口不对,但 import 直接调对 ⇒ 立刻怀疑"跑的不是我改的那份"
② Get-CimInstance Win32_Process 抓 argv0(比猜快 100 倍)
③ 对副本与技能包做 md5 + grep 新代码关键字(grep -c 为 0 ⇒ 定案)
④ 同步 → 重启 → 复核
同族推广判据:凡"技能包改了、跑起来却没变",一律先问三句: ① 这个进程的 argv0 指向哪份文件? ② 那份文件 md5 对不对? ③ 它重启过没有? —— 这三问能一次排掉「改错地方 / 副本陈旧 / 没重启」三类,而它们症状完全一样(都不报错、都静默)。
P0-65 📌 「一次性排期」(schedule_type='once')= 机制唯一开新会话的通道,闸④ 防它把闸卡死(★ 2026-10-05 只读取证)
- 它是什么:
automations表里schedule_type='once'的行 —— 排一条未来某刻开一条新会话的「闹钟」, 触发一次即作废(⛔ 不像recurring会再来)。本机实测在册 37 条。 - 谁排的(两个创建者,⛔ 都不是"会话自己"):
- 常驻程序
collabd.py::create_check_schedule()(第 ~3894 行)—— 唯一的检查会话建排期入口 (用户第④条:「创建者是协作程序,不是会话」)。固定schedule_type='once'、延迟默认 90s。 实测产物=[检查]-[结果检查]-selftest-第N棒(11 条)。 _gap_plan(role, topic, topics)(第 ~5331 行)—— 缺会话时给出"该怎么拉起"的现成参数 (scheduleType='once'+scheduledAt=now+90s)。⚠️ 它只生成参数,真正落库的那一步在别处。
- 常驻程序
- 干什么用的:开新会话。🔴 关键口径(本包红线):自动化是"开新会话"的唯一通道
⇒ 想让某件事在未来某个时刻由新会话接手,就必须落一条
once排期。 实测用途:检查会话(自检棒)、任务会话([协作]-[界面交互]-第三步重跑)、跟进会话。 - 闸④ 是干什么的:
ws_pending_schedules()的判据之一 —— 本工作区只要还有"待执行"的排期,就 ⛔ 不再建检查会话。 防止:排期已排好、会话还没到点开 ⇒「会话全结束」成立 ⇒ 再建检查会话=同一件事两遍。 ⚠️ fail-safe 方向=不建(读库失败 ⇒ 返回非空 ⇒ 当"有排期"处理)。 - 为什么必须"跳过不会再触发的一次性排期":
once排期到点触发后,宿主会把next_run_at清成 NULL, 但status仍ACTIVE、last_run_at仍None⇒ 一条已经开完的排期看起来还像"待执行" ⇒ 它会永远把闸④ 卡住 ⇒ 本工作区再也建不出检查会话(静默、无报错)。 ✅ 两种"不会再触发"都判:(a)next_run_at已过 15 分钟宽限;(b)next_run_at被清成 NULL。 ⚠️ 只对once生效(recurring永远会再触发,⛔ 不按过期排除)。 - 实测取证(2026-10-05 只读):闸④ 对
selftest工作区跳过 11 条、vibe-product跳过 1 条, 全部是next_run_at=NULL(已消费)这一类 —— 正是 (b)。若不跳过,这两个工作区各自永远卡死。 - 🔴 已知隐患(未修,待拍板):工作区判据是 子串匹配
p.lower() in _s.lower()(第 ~4464 行), 而pats里有裸工作区名 ⇒vibe-product会命中vibe-product/tmp/selftest这类子目录的 cwds ⇒ 可能把别的目标的排期算成本区的 ⇒ 闸④ 误拦(少建检查会话)。 ✅ 正解方向=比cwds数组的逐字同形(同_same_ws,两侧都norm后==), ⛔ 不是子串in。⚠️ 改它要同步_all_sessions_idle的同类写法(同一族判据)。
P0-66 · --tick 已删(2026-10-05):判定的权威在常驻,⛔ 不在钩子
用户原话:「检查程序 常驻 自己不会判断吗,非要什么 tick once 去触发?」
判据:需要「什么时候该开检查会话」这种判定 ⇒ 只许由 --supervise 主循环承担;
钩子⛔ 不得是任何判定的触发源(钩子只是"事件到了顺手续命")。
删 tick 时必须搬走的三件事(漏一件就断链)
- 建检查会话排期的
sessions-ended腿 ⛔ 它原先只挂在--tick⇒ 删了 tick 就永远建不出"结果检查"会话(队列非空=有活没人干)。 ✅ 现在在--supervise主循环里:if queue_pending() > 0: sessions-ended else: queue-empty - park 指纹探针(
parkInQueue+hasWaiter=false)⇒ 抽成_tick_park_probe(),常驻每 20 轮调 ⚠️ 它不依赖投递 ⇒ 投递退役后仍有效,⛔ 不许跟着 tick 一起删 - 顺手续命常驻 ⇒ 钩子改调
maybe_ensure_supervise()⇒collabd.py --ensure🔴 只留UserPromptSubmit一处(PreToolUse极高频 ⛔ 不补;SessionEnd从不被投递=死代码)
⛔ 参数表里不许留 "--tick"
留着 ⇒ 传 --tick 会不被拒绝、落进"无分支匹配"⇒ 静默退出/超时(实测 TimeoutExpired)。
🔴 once ⛔ 不许删(与 tick 无关,是主干)
create_check_schedule() 落 automations(schedule_type='once') = 唯一「开新会话」的通道。
删它 ⇒ 检查会话永远开不出来。闸④(ws_pending_schedules)防的是它卡闸,见 P0-65。
自测判据的两条死穴(本轮实测踩到)
- ⛔ 字面判
"--tick" not in <源码>⇒ 永久假红:删除说明的注释里必然提到它 (P0-13 同族)⇒ ✅ 只认带引号的"--tick",⛔ 不认裸串。 - ⛔ 判据由环境状态决定 ⇒ 合成测试区残留真常驻时,
ensure_supervise走闸①幂等让位、压根 不到口令闸 ⇒startswith("no-token")恒红 ⇒ ✅ 两条闸的返回都要接受(目标同为"不起新的")。
P0-67 · 排期不是「定时任务」,是开会话的申请书(2026-10-05 用户点破,⛔ 别再说错)
用户原话:「你就算是 once 不也是走的定时任务吗,难道能直接创建会话?」
完全正确。⛔ 我此前说「once 是唯一开新会话的通道」容易被误读成"once 有开会话的能力"——
它没有。准确表述:
once自己也是排期,也是被宿主到点拾取的;它同样不能直接创建会话。- 能开会话的永远只有宿主。AI 侧(本程序是宿主之外的 python 进程)没有任何开会话的能力。
- ⇒
once的真实身份 = AI 侧唯一能递到宿主手里的"纸条": 「请你在 X 时刻开一条会话,prompt 是 …」。宿主扫描到它 ⇒ 才开会话。
由此纠正两个长期绕圈的说法
- ⛔ 「为什么会产生排期/能不能不排期」—— 不能。要开会话就必须写这张纸条, ⛔ 没有第二种投递方式。排期不是"定时需求",是**"开会话的申请书"**。
- ⛔ 「把 once 删了、程序自己判断直接创建会话」—— 判断可以自己做(常驻五道闸已经在做), 但创建那一步做不到,与删不删 once 无关。
那"排期堆积"到底是什么
⇒ 申请书副本的堆积:申请书被宿主取走后 next_run_at 清 NULL,但记录永不删除
(deleted_at 恒 null)。闸④ 为了不卡闸必须跳过它 ⇒ 也就拦不住再开一张新的。
⇒ 正解不是"不用申请书",而是 "申请书用完归档",或 "同一件事在冷却期内只递一次"。
一个可优化的点(⛔ 未实施)
create_check_schedule(delay_s=90):既然排期只是申请书,⛔ 延迟 90 秒不是机制需要
(宿主扫描周期实测 ≤30 s)⇒ 理论上可缩到 ~30 s,让检查会话更早开工。
P0-68 🔴🔴 MCN 工作台的按钮正是走 once —— 我此前「MCN 不是走 once」结论错误,已纠正(★ 2026-10-05 源码级实测)
用户原话:「净瞎说 … 难道 MCN 工作台也是走的 once 吗」/「我问你 MCN 是不是走 ONCE 开会话的」。
答案:是。MCN 就是走 once,与我方 create_check_schedule() 是同一条路。
⛔ 我此前据「automations.cwds 含 mcn 0 条 + 该区无 .workbuddy/collab/」推出
「MCN 压根没用协作机制,不构成反例」—— 前半对(它不经过我的常驻),
后半错(它照样写 once)。错因:拿「有没有用我的程序」当「有没有用这条通道」。
源码级证据(逐字)
前端:public/data-pages.js:735 点「更新榜单数据」⇒ POST /api/dsh/ranking-update {prompt, force}
⇒ 轮询 /api/run/status?id=… ⇒ 提示「请到左侧会话栏查看执行」。
后端:server.js:677 ⇒ createOnceAutomation(RANK_UPDATE_NAME, prompt, ['mcn-data-insight'])
⇒ server.js:339 INSERT INTO automations (..., status, schedule_type, scheduled_at, next_run_at, ...) VALUES (?,?,?,'ACTIVE','once',?,?,...)。
与我方实现的逐项对照(同一张表、同一个 schedule_type、同一套三条硬规则)
MCN createOnceAutomation() |
我方 create_check_schedule() |
|
|---|---|---|
| 落表 | automations |
automations |
schedule_type |
'once' |
"once" |
status |
'ACTIVE' |
"ACTIVE" |
next_run_at |
now + 5_000(未来毫秒) |
now_ms + delay_s*1000 |
scheduled_at |
ISO 串(YYYY-MM-DDTHH:MM:SS) |
同(ISO 串) |
cwds |
JSON.stringify([cwd]) 正斜杠 |
json.dumps([ws]) 正斜杠 |
| 延迟 | 5 秒 | 90 秒(下限 30) |
| 触发者 | 前端按钮(绕开常驻) | 常驻五道闸判过之后 |
⇒ 🔴 「写这张表」就是宿主对外唯一的"开会话"入口,谁都能写:
node 进程能写(server.js)、python 常驻能写(collabd.py)——与"用不用协作机制"无关。
⇒ 这条实证同时坐实 P0-67:once 是申请书,MCN 交的也是同一式样的申请书。
由此定死两条结论
- 🔴🔴
once⛔ 不能删。删它不是"去掉定时任务",是掐断 AI 侧开会话的唯一通道 ⇒ 我的检查会话 + MCN 的按钮任务同时断掉(两处都是INSERT … 'once')。 - 🔴 "程序自己判断状态、创建会话"已经做到了——判断在常驻五道闸(P0-66), 创建=写申请书。MCN 只是把"判断"这一步交给了人(点按钮),我方交给了常驻。 ⛔ 差别在"谁判断",不在"要不要申请书"。
⛔ 实验方法上的坑:删存量 ≠ 掐断能力,且"没重建"可能是假阴性
2026-10-05 做过一次:软删全库 37 条 once,5 分钟后回读 count=0 未见重建。
⛔ 不能据此说"程序不能建排期" —— 回读同时查得:
· ai1net:tmp/supervise-inbox/tasks.json = {"t":{"state":"done"}} ⇒ queue_pending()=0
⇒ 走 queue-empty 腿;goal.json 无 lifecycle 字段 ⇒ 判「等待」⇒ 闸①「须进行中」不过。
· vibe-product:queue_pending()=0;lifecycle='已完成(机器可判部分)' ⇒ 闸① 不过。
⇒ 两个区当前本来就"不该建",不建是闸的正确行为。
🔴 判据:先查闸门条件,再看有没有产物;⛔ 只看产物 ⇒ 把"条件不满足"误读成"机制断了"。
可优化点(已由 MCN 实证,仍待拍板)
delay_s:MCN 用 5 秒被宿主正常拾取 ⇒ 我方的 90 秒不是机制需要(宿主扫描 ≤30 s)。
缩到 ~5–10 s ⇒ 申请书在册时间从 ≥90 s 压到 ~5 s ⇒ 堆积窗口同步收窄(治标,⛔ 不治根因)。
回滚
37 条软删记录完整备份在 ai1net-dsh-server/tmp/once-backup-20261005-093856.json(157640 B)。
⛔ 但那是已被宿主消费过的残骸(next_run_at 已 NULL)⇒ 恢复只是把 37 条僵尸放回表,
⛔ 不建议恢复;真要恢复再清一次即可。
P0-69 🔴🔴 「有需要才建」是对的 —— 37 条里真正堆积的只有 11 条,且全在合成自测区(★ 2026-10-05 备份数据实证,用户点破)
用户原话:「有需要的时候才会创建定时任务开会话,哪里来的排期呢」
这句话是对的,实测把 37 条拆开后结论完全变样(tmp/once-backup-20261005-093856.json):
37 条的构成(实证)
- ai1net 25 条(10-01 09:05 → 10-02 10:49)= 正常,不是堆积。
名字各不相同(
主控 · …/接续 · …/[协作]-…/[跟进]-…/[唤醒机制] …), 间隔 2.6 ~ 420 分钟 ⇒ 一次一个需求、有需要才建,正是用户说的那种。 ⛔ 其中没有一条是[检查]([检查]体系是后来才进常驻的,时间线问题,⛔ 不是卡闸)。 - vibe-product 11 条
[检查]-[结果检查]-selftest-第N棒(10-04 22:34 → 10-05 00:33)= 真堆积。 同一个 kind、同一个合成自测区vibe-product/tmp/selftest,2 小时内等距产出 (间隔 3.6 / 11.9 / 16.4 / 7.8 / 13.5 / 17.0 / 12.9 / 9.5 / 22.6 / 4.1 分钟)。 - vibe-product 1 条
[协作]-[界面交互]-…= 正常需求。
⇒ 堆积占比 11/37,且 100% 来自自测区。真实业务线零堆积。 ⛔ 此前把 37 条统称"堆积"是口径错误,会误导修法(差点去动正常的那 25 条)。
那 11 条为什么堆:电平触发(状态判据当事件判据用)
常驻每 2 轮(≈60 s)判一次 ⇒ 自测区的条件持续成立(队列空 · 会话全结束 · 目标"等待") ⇒ 每一轮都判"有需要" ⇒ 每轮都想建。此时两道去抖闸同时失效,且是同一处设计导致的:
- 闸④
ws_pending_schedules():collabd.py:4519起 必须排除next_run_at已 NULL 的once(否则它永远卡闸)—— 排除了就不再拦重复。 - 闸⑤ 同名去抖:名字是
第%d棒(_check_stamp()["round"]+1),N 递增 ⇒ 永远不同名 ⇒ 形同虚设。
🔴 根因一句话:判的是"状态"(电平),却想要"变化"(边沿)。 ⛔ 此前记的"申请书副本堆积"是表象,不是根因;根因是没有边沿/冷却记忆。
正解(治本,⛔ 未实施)
边沿触发:记下上次判定时"是否有活"的指纹,只有状态发生变化才建
(如 sessions-ended:从"有会话在跑"→"全结束";queue-empty:队列从"非空"→"空")。
退化方案(治标):按 kind + 工作区 加冷却期,窗口内只递一次(用现成的 CHECK_STAMP)。
⛔ 只做"消费后归档"不治本 —— 那治的是表变脏,治不了反复建。
⛔ 一条差点说错的(自我纠偏)
曾据"S7 排期 next_run_at 还在、且 37 条 last_run_at 全为空"推断"闸④ 被卡了 3 天"——
错:闸④ 的 last_run_at 口径只是初筛,后面 4519 行起会排除 next_run_at 已 NULL/已过宽限的 once
⇒ 那批记录不卡闸。⇒ 🔴 判闸行为必须读完整个函数,⛔ 不能只看 SQL 那一段就下结论。
P0-70 🔴🔴🔴 「程序窗口一直弹出」事故:三条根因 + 总开关(★ 2026-10-05 用户当场发火三连「关掉程序/马上停止/提供启动开关」)
⛔ 这是事故,不是小毛病。用户屏幕被黑窗口反复打断,连发三条要求停止。
三条根因(逐条实测)
1、起进程前没盘点。--ensure 连起两区常驻;跑 selftest.py 时它 spawn 的常驻跑完不回收
⇒ 一度 7 个 python 进程并发(其中 4 个是技能包目录下的野 --supervise)。
🔴 铁律:先盘点(Get-CimInstance Win32_Process)再起;selftest 跑完要回收。
2、用错解释器。collabd.py:585 跨区补起写死 sys.executable(python.exe=控制台子系统)
⇒ 每补起一个闪一个黑窗。模块里早有 PYW(_win_pythonw())却没用上
⇒ 同族坑"改一处漏另一处"。✅ 已改 _py = PYW。
🔴 CREATE_NO_WINDOW flag 只是补救,⛔ 不能替代 pythonw.exe。
3、最隐蔽、最要命:宿主钩子 UserPromptSubmit 每次都 maybe_ensure_supervise() 补起常驻
⇒ 用户每发一条消息就补起一次 ⇒ 只杀进程永远止不住(杀完下一条消息又起)。
已落的总开关(用户点名要的)
<工作区>/.workbuddy/collab/supervise.switch:
1、内容 on ⇒ 开。
2、文件缺失/读不到/内容非 on ⇒ 关(🔴 fail-safe:⛔ 绝不许"读不到就当开"——
那正是"用户没让它跑、它却一直在弹"的成因)。
3、管住两处自动驱动:maybe_run_collabd_once()(--once)与 maybe_ensure_supervise()。
⛔ 只管一处会漏(另一处照样起进程)。
开关与开会话的关系(用户问「后续如何创建会话」)
开关关 ⇒ 常驻不跑 ⇒ 不会有任何排期被创建(排期是常驻判闸后写的)。
开关开 ⇒ 常驻五道闸+闸⑥ ⇒ 才可能写 automations 表(schedule_type='once')⇒ 宿主拾取开会话。
⇒ 🔴 开关是总电闸,⛔ 它不是"只关弹窗"(顺带也不再自动建任何会话)。
其余止血
- 禁用 3 个
collabd-supervise-*计划任务(它们触发start-supervise.ps1)。 ⚠️ 连带后果:开机自启/常驻兜底没了 ⇒ 要不要、用什么载体,必须问用户,⛔ 不再自作主张。 - 杀光全部 python 进程(含 board/两区常驻/4 个野进程)。
🔴 2026-10-05 晚 · 载体定案(用户拍板 + 全链实测,P0-71 详载)
用户逐字:「常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?」
⇒ 本节那句"用什么载体必须问用户"已问、已答:计划任务 → pythonw.exe → --supervise,每 5 分钟兜底。
配套「一键全关」= collabctl.py off(开关+任务+进程,一次做完并复核)。
⚠️ P0-70 说"禁用任务是止血"没错,但止痛不等于治病 —— 停掉载体就没人兜底了,见 P0-71。
P0-71 🔴🔴🔴 常驻到底靠什么活着:不在"作业对象",在"谁拉起它"(★ 2026-10-05 全链实测 · 同一件事栽到第四次)
用户连问三轮才逼出真相: 「待拍板 · 常驻到底靠什么保证长期运行,我就没搞懂 在你清理之前常驻不都是运行的好好的吗」 「启动个程序要什么看守循环,我就没明白了,启动bat程序 一直运行需要看护循环吗」 「从1号搞到5号 还起个程序都启动不起来」
一、四条实测读数(⛔ 全是跑出来的,没有一条是推的)
| # | 做法 | 读数 | 结论 |
|---|---|---|---|
| 1 | 会话里 Popen 起循环 bat |
约 70 秒后停止写入(t+40s 60 行 → t+70s 仍 60 行) | 活不过工具调用边界 |
| 2 | 加 CREATE_BREAKAWAY_FROM_JOB(0x01000000) |
PermissionError(13, 拒绝访问) |
本机作业对象不允许脱离 |
| 3 | 四组对照(任务 Highest/Interactive、任务 Limited/S4U、任务 Highest/S4U、subprocess 直起) | 全部 IN-JOB |
🔴 IsProcessInJob 在此环境没有鉴别力 —— 给谁都套作业对象 |
| 4 | 能长期活着的那条常驻 | 父进程已退出、父链断在自己身上 | ✅ 真判据=父链根:会话树里的是 bash→sandbox-cli→WorkBuddy.exe→explorer;任务起的断链 |
⇒ 🔴🔴 一句判据:别看"在不在作业对象里"(恒真,没用);看"父链有没有穿到 WorkBuddy.exe"。 穿到了 ⇒ 会话一收工它就没;没穿到 ⇒ 真常驻。
二、形态定案(用户拍板)
计划任务 → pythonw.exe → collabd.py --supervise,每 5 分钟一次。
1、🔴 动作必须是 pythonw.exe(GUI 子系统 ⇒ 不分配控制台 ⇒ 不弹窗)。
⛔ powershell.exe / cmd.exe 都是控制台程序,一启动就分配控制台 ⇒ 闪窗(P0-70 弹窗事故真因)。
2、🔴 ⛔ 不要"看守循环"。旧形态是三层嵌套:
① 常驻本体(自带循环,✅必要)② start-supervise.ps1(永不退出的看守,⚠️多余)
③ 计划任务每 60 秒叫醒看守(❌这才是弹窗源,且看守自己不会退 ⇒ 纯浪费)。
✅ 现在两层:常驻本体(自带循环)+ 计划任务(每 5 分钟判活,判到就走)。
3、🔴 --supervise 是长驻的,进场即判活:supervise_alive()(pid 活 + 心跳新 + 身份核验)
⇒ 已在 ⇒ 新起的立刻让位退出 ⇒ ⛔ 不会叠加成两条(实测:触发后进程数恒为 3)。
三、🔴🔴 两个必须记住的实现细节(掉一个就"看着配好了、其实没跑")
1、🔴🔴 WorkingDirectory 必须是「工作区根」,⛔ 不是脚本所在目录。
病根:collabd.load_cfg() 按 <cwd>/.workbuddy/collab/collabd.config.json 找配置。
设成脚本目录(<ws>/.workbuddy/collab)⇒ 它去找
<ws>/.workbuddy/collab/.workbuddy/collab/collabd.config.json ⇒ 必然找不到
⇒ CFG_MISSING ⇒ --supervise 拒跑(实测 rc=2;任务侧记 LastResult=1)。
✅ 改成工作区根 ⇒ State=Running、LastResult=267009、心跳正常(实测)。
2、🔴 常驻不需要网关口令(这条反直觉,必须记住):
--supervise 唯一职责=建检查会话排期(maybe_spawn_check_agent → create_check_schedule
→ 直连 SQLite 写 automations 表)⇒ 全程不走网关。
口令只在 --ensure 的 spawn 分支才是前置门。
✅ 隔离区实测:不含口令的计划任务起 --supervise ⇒ 15 秒内写出心跳(pid 3944 · round=2)。
⚠️ 所以保活要用 --supervise,⛔ 用 --ensure 会在任务环境里"起了就退"(无口令)。
四、看板的 LastResult=1 是虚警,⛔ 别当故障
dsh-board-keepalive(pythonw board.py --serve 20099)实测 LastResult=**1**,
但看板其实好好的。真因:board.py 有全局单例守卫("看板全局只允许一个"),
已在跑时它打印一行"已有看板在跑 ⇒ 本次不启动"后 return 0;
而 pythonw 在任务环境下没有 stdout 可写 ⇒ 退出码被顶成 1。
⇒ 🔴 判据:看板看"端口在不在听",⛔ 不看 LastResult。
五、验收读数(2026-10-05 11:4x 实测,全绿)
- 常驻任务:
State=Running·LastResult=**267009**(=0x41301正在运行) - 崩了能拉起(隔离区实测):杀掉常驻 → 删心跳 → 触发任务 ⇒
15 秒内心跳重现(pid 69544 · round=1)·
State=Running⇒ ✅ 用户要的就是这一条 - 跨周期观察 11 分钟(两个 5 分钟周期):进程数恒 4(三生产 + 一观察)、 两区心跳恒新鲜(1~23 s,⛔ 远离 90 s 阈值)、看板每一次采样都在听 ⇒ 无抖动、无重复、无掉线
六、⛔ 三条硬规(别再犯)
1、⛔ 不许在用户机器上做"停生产"实验 —— 要验"崩了能拉起",
去隔离区(tmp/selftest + 空闲端口),⛔ 别把生产的看板停掉试。
2、⛔ 停止=任务 + 进程一起(只禁任务老进程还在;只杀进程任务到点又拉)——
这就是为什么必须有 collabctl.py off 这种"一键做全"的入口。
3、⛔ "禁用任务"不是修复,是拆载体。10-05 我禁了 4 个任务,
用户当场问"在你清理之前常驻不都是运行的好好的吗" —— 对,就是我拆的。
正确修法是换成对的载体(pythonw + 正确 cwd),⛔ 不是把载体删掉。
七、🔴🔴 三条硬约束(用户 2026-10-05 逐字重述,本轮逐条取证)
用户原话:「这些常驻程序每次创建之前,要先检查是否已经存在。如果已经存在了,就不需要重复创建, 而且各工作区是各工作区的。不共用,共用的只有看板。」
1、先查后建(幂等) ⇒ 载体=--supervise 的两段式守卫:
① 进场 supervise_alive()=pid 活 ∧ 心跳新 ∧ _pid_is_collabd 身份核验(⚠️ pid 会复用,
光看"号还活着"会被骗 ⇒ 必须核身份)⇒ 已在即 return 0;
② 原子抢位 + sleep 1.5 回验兜住"两条同时启动"。
✅ 变异对照实测(⛔ 不是"应该绿"):手动再起第二个 --supervise ⇒ 0.4 秒 rc=0 让位退出;
日志留痕 154 条「已有活的常驻(pid 30560 活、心跳 2 s 前、身份已核)⇒ 本实例退出(幂等,⛔ 不双写)」;
连触两次 ⇒ 进程数恒 3(⛔ 不叠加)。
2、各工作区独立 ⇒ 一区一条 collabd-keepalive-<ws>,动作指本区副本;
判据=心跳里的 argv0 必须指本区路径(实测 ai1net↔30560、vibe↔60596,两串互不相同)。
⛔ 不许一区去起另一区(P0-57 同族,用户 10-03 当场纠正过)。
3、只有看板共用 ⇒ 看板任务全局唯一一条 dsh-board-keepalive(端口 20099,动作指技能目录那份共用 board.py)。
⚠️ 专项澄清(两条 cwd 要求恰好相反,⛔ 别互相套用):
· --supervise 任务 WorkingDirectory 必须是工作区根(首节细节①);
· 看板任务 WorkingDirectory 指技能 scripts/ 即可 —— board.py 全按 __file__ 定位,⛔ 不依赖 cwd。
八、🔴🔴 本轮新抓的部署缺口(P0-57 同族第 N 次,必须并入 deploy 清单)
- 症状:
collabctl.py status把新任务全报「未知」、且列的是已禁用的旧任务名。 - 病根:
deploy_code.py的DEFAULT_FILES只有["collabd.py", "goalctl.py"]⇒ 用户双击.bat调的collabctl.py从来没被分发 ⇒ 各区副本停在 10:36 旧版(16982 B), 技能目录已是新版(23176 B)⇒ "看着改了、其实各区没吃到"。 - ✅ 修法:
DEFAULT_FILES补全所有被计划任务/.bat/hook 直接执行的本包脚本:collabd.py·goalctl.py·collabctl.py·guard.py·session-rules-check.py·init_workspace.py。 - ✅ 判据(写死):凡是被自动触发物直接执行的本包脚本,都必须在
DEFAULT_FILES里; ⛔ 改完代码必须重跑deploy_code.py --ws <区>(覆盖前自动备份),否则各区吃不到。
P0-72 🔴🔴🔴 「一键停止」没真停+看板起不来:两个静默失败(★ 2026-10-05 端到端实测抓到 · 用户原话「我关不掉这些东西,我还要投诉你呢」)
用户三条口径(逐字):「常驻崩了,拉起来这个事儿,你不能也用计划任务起个程序吗?」/ 「我总要以一键能关掉这些东西吧」/「这些常驻程序每次创建之前,要先检查是否已经存在……各工作区是各工作区的,不共用,共用的只有看板。」
一、🔴🔴 缺陷 A:kill_all() 静默漏杀 —— 「停止」看着成功,进程一个没死
- 症状:跑
collabctl.py off⇒ 打印「计划任务已禁用:7 个」+「已杀进程:0 个」, 但两区常驻(pythonw)与看板全都还活着。⇒ 假停。 - 病根:
kill_all()里的taskkill用了creationflags=HIDE,而HIDE含CREATE_BREAKAWAY_FROM_JOB—— 本机实测必被拒(PermissionError(13))⇒ 每次taskkill都抛异常、被except: pass吞掉 ⇒ 函数照常return(计数 0)⇒ 「已杀进程:0 个」既没报错、也不起任何疑心。 - ✅ 修法:
taskkill是短命控制台程序且capture_output走管道 ⇒ 不会弹窗 ⇒ 用creationflags=0+stdin=DEVNULL;返回值按rc==0计成功(⛔ 不再"无脑 +1")。 - 🔴🔴 同族第二条(修完才露出来):
list_procs()的过滤是python*⇒ 连调用它的 python 一起匹配到 ⇒ 实测把调用方自己杀了(症状=脚本rc=1、零输出)。✅ 修法=先算保护集 (_self_and_ancestors():自己 + 全部祖先 pid)+ 跳过collabctl.py与"正在列举进程的那个 ps"。
二、🔴🔴 缺陷 B:看板任务直起 board.py ⇒ 崩溃重启循环、端口从没绑上
- 症状:
dsh-board-keepalive的State一直在Ready/Running抖、LastResult=1、 20099Listen计数恒 0、进程 pid 每十几秒换一次(崩→5 分钟触发→再崩)。 - 真因(抓到的现场输出):
⚠️ collabd:未找到部署配置(COLLABD_CONFIG 未设,且 <scripts>\.workbuddy\collab\collabd.config.json 不存在)⇒ 已拒跑board.py顶层用COLLABD_CONFIG定位配置(它要import collabd.py)—— 而计划任务的"动作"里根本没有 env 字段 ⇒ 这个变量只能由启动器在进程内设。 - 🔴 注意变量名别混:包内
roots.env提供的是COLLABD_PROD_CONFIG, 而board.py读的是COLLABD_CONFIG—— 名字对不上 ⇒roots.env兜不住它。 - ✅ 修法:新增
scripts/board-launch.py(启动器)=先在进程内设COLLABD_CONFIG=<主区>/.workbuddy/collab/collabd.config.json+PYTHONIOENCODING+ 兜住sys.stdout/stderr(pythonw 下可能是None),再runpy.run_path(board.py, run_name="__main__"); 看板任务的动作改指pythonw.exe board-launch.py。 - 🔴
ExecutionTimeLimit必须按"是不是长驻服务"分: · 看板(长驻服务)⇒0(无时限);· 常驻--supervise(也是长驻)⇒0。 ⛔ 我一度把两者都设 2 分钟 ⇒ 到点被调度器掐死 ⇒ 又一种"每 2 分钟重建"的抖动(已改)。 - ⚠️ 看板的配置恒指主工作区(看板是全局共用一份);主区由
roots.env的DSH_WS_ROOT决定,启动器里兜底写死ai1net-dsh-server。
三、✅ 验收(2026-10-05 12:0x 端到端实测,全绿)
off⇒ 打印「已杀进程:3 个」;5 秒后list_procs只剩"诊断脚本自己+列举进程的 ps" ⇒ 真停。on⇒ 三任务重建触发;25 秒后两区常驻活、心跳新鲜;看板State=Running/267009。- 看板
curl --noproxy '*' http://127.0.0.1:20099/⇒ HTTP 200 · 148274 B;端口Listen计数 = 1。 - 用户可见入口
会话机制-一键开关.bat(工作区根+桌面)⇒1 启动 / 2 全部停止 / 3 看状态。
四、⛔ 教训(一句话)
"做了动作" ≠ "动作生效" —— taskkill 带错 creationflags、计划任务动作里没有 env 字段,
这两件事都不报错,只表现为"停止没停""看板没起"。⇒ 凡关键动作,必须有"动作后复核"
(杀完看进程表、起完看端口),⛔ 不许拿"函数返回了"当成功。
五、⚠️ 诊断期又踩一个(值得单列):进程查询会"自匹配"
查"有没有残留的看守进程"时,Get-CimInstance ... -like '*start-supervise*' 会命中正在执行这条查询的
那个 PowerShell 自己(它的命令行里就含 start-supervise 这串)。
⇒ 症状=每次查都有"残留"、pid 每次都变(其实是每次查询自己)。我差点据此去"清理一个不存在的东西"。
- ✅ 判据:查出来的那条 command line 是不是"正在列举进程"的那条?是 ⇒ 就是自匹配,不是真进程。
- ✅ 做法:把关键字拆开拼接(
"start-superv" + "ise.ps1")再比对;或按-File形态精确匹配 ($_.Name -eq 'powershell.exe' -and $_.CommandLine -like '*-File*<拆开的关键字>*')。 - 🔗 同源:
collabctl.kill_all()的list_procs()也踩过同族(python*匹配到自己)⇒ 已用 保护集_self_and_ancestors()兜住。
P0-73 🔴🔴🔴 常驻启动器缺一个环境变量 ⇒ 检查程序静默失效(★ 2026-10-05 用户点名「检查之前创建的协作程序和检查程序运行是否正常」时抓到)
用户口径(逐字):「处理完毕后,检查之前创建的协作程序和检查程序运行是否正常。」
一、先分清"两套程序"(⛔ 别混为一谈)
| 协作程序 | 检查程序 | |
|---|---|---|
| 是什么 | collabd.py --supervise(常驻本体,自带循环,每 10~30 s 一轮) |
--supervise 循环内建的检查会话排期(maybe_spawn_check_agent() → create_check_schedule() → 直连 SQLite 写 automations) |
| 判活 | 心跳文件新不新 + argv0 指不指本区 |
日志里有没有 检查会话:… / 已建检查会话排期 … 分支 |
| 载体 | collabd-keepalive-<ws> 计划任务 |
同一个进程(⛔ 没有独立任务) |
⇒ 🔴 检查程序跟着协作程序走 ⇒ 协作程序"活着"不代表检查程序在工作(本坑正是如此)。
二、🔴🔴🔴 症状:协作程序活得好好的,检查程序悄悄地不干活了
- 协作程序侧一切正常:两区心跳新鲜、
argv0各指本区、进程恒 3 个。 - 检查程序侧(ai1net):
logs/_collabd.log每轮刷一行all_sessions_idle 读库失败 no such table: sessions(累计 36 次); 最后一次成功建检查会话排期停在 11:54(重建任务之前)⇒ 此后一次都没再建过。 - 对照组(vibe)完全正常:读库失败 0 次,一路在跑
检查会话:目标状态=已完成(非进行中)⇒ 不建。 - 🔴 为什么"静默":
_all_sessions_idle()的 fail-safe 是 读库失败 ⇒ 判「有会话在跑」⇒ 不建检查会话(return False)。 ⇒ 失败方向恰好是"什么都不做" ⇒ 表现成"机制很安静",⛔ 不会报错、不会告警。
三、🔴🔴🔴 根因:常驻任务直起 collabd.py --supervise,任务环境没有 CODEBUDDY_CONFIG_DIR
_wb_db()逐字:⇒ 环境变量没设 ⇒ 落到cfg = os.environ.get("CODEBUDDY_CONFIG_DIR") or "" return Path(cfg) / "workbuddy.db" if cfg else Path.home() / ".workbuddy" / "workbuddy.db"C:\Users\Administrator\.workbuddy\workbuddy.db。- 🔴 那个文件是 0 字节的空库(无任何表)⇒ 查询
sessions⇒no such table: sessions。
| 库 | 大小 | sessions 表 |
|---|---|---|
E:\ProgramData\.workbuddy\workbuddy.db(活动库) |
34.9 MB | ✅ 247 行 |
C:\Users\Administrator\.workbuddy\workbuddy.db(旧库) |
0 字节 | ❌ 无 |
- 🔴 为什么只有 ai1net 炸、vibe 没事:vibe 的 config 里
host_db写死了绝对路径E:/ProgramData/.workbuddy/workbuddy.db;ai1net 的host_db一直是空字符串(历史备份也空)。 ⇒ 同一份代码,两区行为不同,差别只在 config 这一格。 - 🔴 "计划任务的『动作』里没有 env 字段"(P0-72 §二 同一条硬约束)⇒
CODEBUDDY_CONFIG_DIR只能由启动器在进程内设。 - 🔴 我是怎么把它弄丢的(诚实记录):旧看守
start-supervise.ps1里本有$env:CODEBUDDY_CONFIG_DIR = "E:\ProgramData\.workbuddy"; P0-72 改成"两层形态"(计划任务 →pythonw→ 启动器)时,只保了COLLABD_CONFIG,丢了这一句。 ⇒ 重构"起法"时,旧起法里的 env 必须逐条搬过去(⛔ 不许凭记忆只搬"我记得的那个")。
四、✅ 修法:新增常驻启动器(与 board-launch.py 对称)
- 新增
scripts/supervise-launch.py:进程内os.environ.setdefault("CODEBUDDY_CONFIG_DIR", r"E:\ProgramData\.workbuddy")+ 设COLLABD_CONFIG=<本区>/collabd.config.json+ 兜住sys.stdout/stderr(pythonw 下可能是None) +sys.argv=[collabd.py, "--supervise"]+runpy.run_path(CB, run_name="__main__")。 - 常驻任务动作改指
pythonw.exe "<区>/.workbuddy/collab/supervise-launch.py"(⛔ 不再自带--supervise—— 参数由启动器给),-WorkingDirectory仍是工作区根,-ExecutionTimeLimit恒0(长驻服务)。 deploy_code.py的DEFAULT_FILES补入supervise-launch.py(否则各区吃不到,P0-57 同族)。- ⚠️
roots.env的位置缺口(连带发现):_sm_load_roots()从__file__向上找roots.env(here/../..×4); 工作区副本在<ws>/.workbuddy/collab/collabd.py⇒ 若工作区里没有roots.env⇒ 找不到; 而roots.env只在技能目录(E:/ProgramData/.workbuddy/skills/session-mechanism/roots.env)。 ⇒ 别指望靠roots.env兜住各区,启动器里写死最稳。
五、✅ 验收(2026-10-05 12:15 实测,全部留痕)
① 分界线干净得可当判据(grep -c + awk 分界):
[2026-10-05 12:14:49] all_sessions_idle 读库失败 no such table: sessions ← 旧进程最后一次
[2026-10-05 12:15:02] supervise loop start pid=58860 ← 新形态进程起来
[2026-10-05 12:15:14] all_sessions_idle:还有 1 条 working(a80f300d)⇒ 不算全结束
⇒ pid=58860 之后 no such table: sessions 计数 = 0(awk 分界实测),
从 fail-safe 的"恒判有会话"变成真实读库判定。
② 那条 working(a80f300d) 是真的(sqlite3 直查活动库):
('a80f300d-…', '复盘排期堆积与一次性排期问题', 'working', 'E:/ProgramData/AIProject/ai1net-dsh-server')
⇒ 就是当前这条会话本身 ⇒ 闸②「本区有会话在跑 ⇒ 不建检查会话」是正确的合法拦截
(⛔ 不是 bug。要观察 检查会话:… 分支,得等本区会话全结束)。
③ 变异对照证明判据非恒绿(tmp/mutate_dbpath.py,两档):
| 档 | 环境 | 落的库 | 结果 |
|---|---|---|---|
| A | 无 CODEBUDDY_CONFIG_DIR |
C:\…\.workbuddy\workbuddy.db(0 B) |
FAIL no such table: sessions |
| B | 有(注入后) | E:\ProgramData\.workbuddy\workbuddy.db(34.9 MB) |
OK sessions=247 working=1 |
⇒ A 必 FAIL、B 必 OK ⇒ 路径来源真实决定成败(⛔ 不是"应该绿")。
④ 运行时三件套(Get-CimInstance 实测,恒 3 进程、ppid=3924=调度器、父链断 ⇒ 真常驻):
| pid | 启动器 | 归属 |
|---|---|---|
| 58860 | <ai1net>/.workbuddy/collab/supervise-launch.py |
协作程序 ai1net |
| 61956 | <vibe>/.workbuddy/collab/supervise-launch.py |
协作程序 vibe |
| 63684 | <技能>/scripts/board-launch.py |
看板 |
⑤ 启动器三处 md5 一致 f416a2f1(技能目录 + 两区副本);看板 curl --noproxy '*' http://127.0.0.1:20099/ ⇒ HTTP 200(1.5 ms)、netstat 实测 127.0.0.1:20099 LISTENING。
六、⛔ 教训(一句话)
"进程活着" ≠ "它在干活" —— 常驻本体活着,但它内部某一环读错了库、被 fail-safe 兜成"什么都不做", 外表完全安静、零报错。⇒
- 检查"程序是否正常",必须看它有没有产出预期的分支(
检查会话:…),⛔ 不能只看进程在不在; - fail-safe 的方向要选对(本处是"不建",安全但不可见)⇒ 凡 fail-safe,必须同时打一条可检索的日志;
- 重构"起法"时,旧起法里的 env/inner 参数要逐条搬(本坑就是丢了一句
CODEBUDDY_CONFIG_DIR); - 各区 config 的
host_db写死绝对路径最稳(vibe 一直这么写,所以只有 ai1net 炸)。
P0-74 🔴🔴🔴 _escalate_to_keeper() 残留旧形态 ⇒ 计划任务里躺着 powershell.exe ⇒ 闪黑窗(★ 2026-10-05 用户原话「刚才又弹了窗口看看是什么」)
一、症状:屏幕上反复闪一下
- 用户报「刚才又弹了窗口」。
- 实测抓到的进程对(
Get-CimInstance Win32_Process):⇒pid 55880 ppid 3924 powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "...\start-supervise.ps1" pid 67784 ppid 55880 conhost.exe \??\C:\windows\system32\conhost.exe 0x4powershell.exe+conhost.exe—— 这就是闪窗的铁证。
二、🔴 真凶:collabd.py 的 _escalate_to_keeper() 没跟着新形态一起改
- 它是自我供给函数(2026-10-04 加的:就地起失败时,转去建本区计划任务)。
- 但它的实现停留在旧形态:任务动作写死
powershell.exe -WindowStyle Hidden -File start-supervise.ps1+ 还要铺一份start-supervise.ps1(模板assets/start-supervise.ps1.tpl)。 - 🔴 为什么它就"闪":
-WindowStyle Hidden只藏窗口、不省控制台 —— PowerShell 是控制台程序 ⇒ 每次触发都分配conhost.exe⇒ 闪一下。 - 🔴 为什么"反复"闪:该任务触发器
-AtLogOn+RestartCount 999 / RestartInterval 1min⇒ 起来就不会安静。 - 🔴 触发链:宿主钩子
supervise-ensure-hook.py(每次UserPromptSubmit)→collabd.py --ensure→ 就地起失败 →_escalate_to_keeper()→ 建/启旧任务 → 闪窗。 实测日志逐字:常驻自我供给(计划任务):✅ 已建本区计划任务 collabd-supervise-ai1net-dsh-server。
三、✅ 修法(收敛成同一个形态,⛔ 不留第二套)
在 _escalate_to_keeper() 里:
- ⛔ 不再铺
start-supervise.ps1、⛔ 不再做 ps1 语法校验、⛔ 不再依赖start-supervise.ps1.tpl(删掉_find_keeper_tpl()前置门槛 —— 否则缺模板就直接"没建",与启动器形态无关了)。 - 任务动作改指
pythonw.exe+<区>/.workbuddy/collab/supervise-launch.py(-WorkingDirectory=工作区根;pythonw 是 GUI 子系统 ⇒ 零控制台 ⇒ 不闪窗)。 - 任务名保持
collabd-supervise-<区>(⛔ 不改名 ⇒collabctl的名单不用动)。 _escalate_to_keeper里被删段用~~删除线~~在 docstring 里标注(⛔ 别留下与实现不符的说明)。
四、✅ 验收(2026-10-05 12:42 实测)
- 跑一次
_escalate_to_keeper()⇒ 回读任务动作:⇒ 不再是EXE=E:\ProgramData\.workbuddy\binaries\python\versions\3.13.12\pythonw.exe ARG="E:\ProgramData\AIProject\ai1net-dsh-server\.workbuddy\collab\supervise-launch.py" WD=E:\ProgramData\AIProject\ai1net-dsh-serverpowershell.exe(grepNew-ScheduledTaskAction -Execute 'powershell.exe'⇒ 0 命中)。 - 零 PowerShell 看守残留:
Get-CimInstance筛powershell.exe且命令行含start-superv⇒ 空。 - 三常驻全部
ppid=3924(调度器),且三个都是启动器形态: ·48372<ai1net>/…/supervise-launch.py|·61956<vibe>/…/supervise-launch.py|·63684<技能>/…/board-launch.py⇒ 父链断在调度器 ⟹ 真常驻;形态统一 ⟹ 不会再长出第二套起法。
五、⛔ 教训(一句话)
"改了主路径" ≠ "把旁路也改了" —— 上一轮把常驻主体收敛成"启动器形态",却漏了这个自我供给旁路;
它平时不吭声,只在你最不希望的时候闪你一下(还带着 RestartCount 999)。
⇒ 凡"同一种东西有第二条起法",收口时必须 grep 全文找一遍(本次判据 = 全文搜 New-ScheduledTaskAction 与 .ps1)。
P0-75 🔴🔴 远端技能总仓里躺着明文令牌(.neodata_token)(★ 2026-10-05 收尾核验时发现)
一、症状
核验 [email protected]:maogeigei/workbuddy_skills.git 的推送结果时,
把「远端 HEAD 顶层条目」与「更早做的备份」用 comm 比差异 ⇒ 多出一个 .neodata_token:
{"token": "tk_<REDACTED·首2位 tk_><共 35 字符>", "saved_at": 1790920232}
明文、72 B、可直接被任何能 clone 的人读走。
二、根因
- 引入提交=
43b83b0 chore(skills): 首次入库 workbuddy_skills(25 个技能 / 419 文件)⇒ 是首次整仓入库时带进去的,⛔ 不是后来某次改技能推的。 - 首次入库用
git add -A整目录收录;当时.gitignore只挡了运行产物 (*.log/__pycache__//*.bak-*)⇒ 完全没挡凭据文件。 - 本机同源=
<CODEBUDDY_CONFIG_DIR>/.neodata_token(NeoData 金融搜索技能运行时写的令牌落点, 跟roots.env是同一类东西,但一个是"部署契约"、一个是"密钥")。
三、修(最小改动 · ⛔ 未改写历史)
git rm --cached .neodata_token # 只出索引,保留工作区文件
# .gitignore 追加:
# .neodata_token
# .neodata_*
# *.token
git commit -m "chore(security): 移除误入库的 .neodata_token 令牌 + 补忽略规则"
git push origin master # 13a300e..031b312
四、验收
- 远端 HEAD =
031b312;工作树无该文件;git ls-files索引已清。 - 25 个技能全在;
session-mechanism57 文件(⛔ 没被波及)。 .gitignore尾部新规则就位。
五、⚠️ 残留风险(必须如实告知用户)
历史里仍有那个 blob(55941e52)—— git log --all -- .neodata_token 还能查到,
git show 43b83b0:.neodata_token 仍能打印明文。
⇒ 彻底清除 = push --force 重写全部历史(会改写 25 个技能的全部提交哈希,牵连所有 clone)
⇒ ⛔ 不当场做,必须用户拍板。
(更根本的处置是在服务端吊销/轮换该令牌 —— 重写历史管不住已经 clone 走的副本。)
六、⛔ 教训(可复用判据)
- 🔴 技能仓入库前必扫两样:① 名字含
token|secret|credential|password|\.env的文件; ② 已知令牌串出现在哪些文件(grep -rl "<串>" <目录>)。 - 🔴
.gitignore必须显式挡本机令牌落点(.neodata_token这类), ⛔ 别只挡"日志/缓存/备份" —— 产物与凭据是两回事。 - 🔴 核验远端别只比"技能在不在",要比差异(现远端 vs 备份,
comm -23/-13)。 本次就是靠差异比对才发现;只看"25 个技能都在"会直接漏掉。 - ⚠️ 首次入库用
git add -A是高危动作(整目录收录)⇒ 收录前先跑一遍第 1 条的两扫。