2026-10-05 14:13:24 +08:00
|
|
|
|
# 踩坑清单(每条都真实发生过)
|
|
|
|
|
|
|
|
|
|
|
|
> ## 🔴 当前结论(**先读这里** · 最后更新 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 必须打可检索日志** |
|
2026-10-06 22:27:03 +08:00
|
|
|
|
> | **P0-76** | 🔴🔴🔴 **技能加载闸门的词表漏了「主触发句」** ⇒ 用户说了「使用任务会话完成目标」,钩子**全程 0 命中**(旧词表 22 词全是「决策方法/回复排版」族)⇒ 技能从未强制加载 ⇒ **自决策白名单没进上下文** ⇒ AI 拿"注入通报"当准、把已授权的事**反复要签字**(用户连问三次「你去执行不行啊」「会话技能里没告诉你自决策的规则和机制嘛」)。🔴 **⛔ 不是作用域问题**(实测 `_in_scope=True`);✅ 修法=**扩已有钩子的触发面**(新增会话/派活族 17 词 + 第三条路由 `_LOAD_SESSION`)+ `SKILL.md` 首屏加「第 0 步加载门槛」。🔴 **闸门类机制必须做「触发面覆盖审计」**:光验"闸门能响"不够,要验"**该响的场景是否都在词的覆盖面上**"|🔴 **日志里全是 `entry` 没有 `HIT` = "听见了但没认出"**(比"没被调用"更难发现)| 🔴 它是**"机制看着在跑、其实没在管事"**的典型 —— 闸门本身完好,只是听不见你说话 |
|
2026-10-05 14:13:24 +08:00
|
|
|
|
> | **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,钩子里**同步**串了 `--gap` 13.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-session` |
|
|
|
|
|
|
> | **P0-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`**照旧每几十秒一行** ⇒ 前四棒都据此认为"常驻在跑",只有**全量进程表**才发现「早就没了」。
|
|
|
|
|
|
|
|
|
|
|
|
**根因(本棒实测,⛔ 非推断)**:
|
|
|
|
|
|
1. 🔴 **日志的写者不止常驻** —— 宿主钩子(`PreToolUse ^Bash$` + `UserPromptSubmit`)每轮都会跑 `--tick`,
|
|
|
|
|
|
它写的是**同一个日志**。⇒ **「日志在走」根本不能推出「常驻在跑」**(本条的最大教训)。
|
|
|
|
|
|
2. 🔴 **本机不存在"能一直活着"的进程**:① `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` 的返回里**):
|
|
|
|
|
|
> ```sql
|
|
|
|
|
|
> 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 日志定时清理`(id `07976988-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`、不写日志**
|
|
|
|
|
|
⇒ **它越正常,日志里越查不到** ⇒ 单看日志必然误判。
|
|
|
|
|
|
- **怎么定死真因(本轮用的三步,可复用)**:
|
|
|
|
|
|
1. **去找那条回路该产出的文件**:`gate-done.stamp` ⇒ **不存在** ⇒ 回路没跑过(**比读日志硬**);
|
|
|
|
|
|
2. **看消费方**:`collabd.py` **真读它**(判 `gate=busy`)⇒ 不是死代码,**是有害的沉默**;
|
|
|
|
|
|
3. **分辨"日志行"的来源**:`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:55 `http:200 ok:true`)
|
|
|
|
|
|
目标是 `f8a792ab` —— 一条**日志已冻结的"死"会话** ⇒ **只有"死"会话才投得进去**。
|
|
|
|
|
|
- 🔴 **真正区分得开的东西在宿主自己的状态机日志里**(`[SessionRunStateMachine]`,**只写工作区日志、
|
|
|
|
|
|
⛔ 不进数据库** —— 所以查库永远查不到):
|
|
|
|
|
|
| 事件 | 结果 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `event=AGENT_STARTED` / `RUN_ACCEPTED` | `lifecycle=running` **`busy=true`** |
|
|
|
|
|
|
| `event=AGENT_ENDED` | `to=idle` `lifecycle=idle` **`busy=false`** `queueBusy=false` |
|
|
|
|
|
|
- **修法**(`collabd.py::_session_busy()` + `_target_busy()`,**语义不变、只换判据形态** ——
|
|
|
|
|
|
用户口径原话「**跟进会话 执行完了 在上报没问题,执行中就等待上报**」,⛔ **不是**"超时即投"):
|
|
|
|
|
|
1. 库里 `status` ∈ `SID_DEAD` ⇒ **直接判"没在跑"**(⛔ 不看日志)。
|
|
|
|
|
|
🔴 **必须有这一道**:实测**自动化拉起的会话跑完不写 `busy=false`**
|
|
|
|
|
|
(整份日志里 `busy=false` 只出现过 12 次、**全是主会话的**),它直接在库里变 `completed`,
|
|
|
|
|
|
而**日志末条仍是 `busy=true`**(`e2ccdea3`:末条 `22:42:10 … busy=true`,库里 `22:45:53` 已 `completed`)
|
|
|
|
|
|
⇒ 只看日志会把**早就结束的会话**再"忙"上十几分钟。
|
|
|
|
|
|
2. `status == working` ⇒ **才**读日志细分:**末条** `busy=` 取**最后一条**记录,
|
|
|
|
|
|
`busy=true` **且新鲜**(`SRSM_FRESH`)⇒ 忙;否则 ⇒ 空闲。
|
|
|
|
|
|
🔴 「新鲜」这半**必须有**:会话真在跑时状态机**每几百毫秒写一行** ⇒
|
|
|
|
|
|
「末条 `busy=true` 却已是 N 分钟前」**只可能**是它早停了 ⇒ **不必依赖"正好抓到 `AGENT_ENDED`"**
|
|
|
|
|
|
(那一条会被挤出尾部窗口)。⚠️ `SRSM_FRESH` 取 **900 s**:一轮里跑**很长的单个工具调用**时
|
|
|
|
|
|
状态机**全程静默**(实测一次 8 MB 日志扫描静默 ~6 分钟)⇒ 窗口太短会把**正在跑**的误判成**空闲**
|
|
|
|
|
|
(方向相反的错:往正在跑的会话里插话)。多等 ≤15 分钟**不算损失**(队列件本来就压着)。
|
|
|
|
|
|
3. 读不到 ⇒ **按"没在跑"处理**并**留一行日志**:⛔ 不能按"忙"处理 —— 那等于把死结换个形状留着。
|
|
|
|
|
|
- 🔴 **同一轮我自己写出的两个"判据看着在、其实恒假/恒真"**(都已修,并写成断言):
|
|
|
|
|
|
- **(a) 正则位置组错位**:写成 `(?:\.(\d+))?` 时**内层 `(\d+)` 仍是捕获组** ⇒ 后面 `m.group(8)`
|
|
|
|
|
|
**整个错位一位** ⇒ `busy` **恒读成 `False`** ⇒ **恒判"空闲"**(方向相反:会往正在跑的会话里插话)。
|
|
|
|
|
|
✅ 改用**命名组** `(?P<busy>…)`,并加**源码级反回归断言**(`_session_busy` 段内零位置组引用)。
|
|
|
|
|
|
⚠️ 这条断言**不是"看代码像不像"** —— 它是**唯一**能在这类错位再发生时立刻报红的东西。
|
|
|
|
|
|
- **(b) 同一秒内"后出现的没胜出"**:只比 `t > last[0]` ⇒ 同一秒(甚至同毫秒)的两条,
|
|
|
|
|
|
**前一条胜出** ⇒ 取到**旧状态**。✅ 改成比较 `(时间, 行序)` 二元组。
|
|
|
|
|
|
- **(c) 我自己的红绿对照脚本是空的**:变异体跑的是 `-k 忙判据`,而那条 `target-busy` 用例的**名字里没有"忙判据"**
|
|
|
|
|
|
⇒ **变异体根本没被试**,报"全绿" ⇒ 差点当成"断言恒真"。✅ 过滤词改成能覆盖两条用例的词。
|
|
|
|
|
|
⇒ 🔴 **判据**:**变异法跑完必须回读"这个变异体到底被哪几条用例跑了"**,⛔ 不看"合计 PASS"就当证毕。
|
|
|
|
|
|
- **残留(⛔ 不是本条的修法能解决的)**:判据换对之后,拦截原因从 `target-busy` 变成
|
|
|
|
|
|
**`no-follow-session`** —— 那是**真状态**:此刻**一条活的 `[跟进]` 会话都没有**
|
|
|
|
|
|
(`e2ccdea3` 跑完变 `completed` 就退出了网关的活会话集)。⇒ **"推"只在目标活着时能用**;
|
|
|
|
|
|
目标不在线时靠**排期到点拉起一条**(`[跟进]-…` 每小时,`nextRunAt` 23: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 条执行会话可见)。
|
|
|
|
|
|
- **判据(⛔ 不看"页面像不像")**:
|
|
|
|
|
|
1. 直接读 **`/board.json`**:看 `ts`(快照时间)**是不是刚才**、**会话条数**、**每条会话的类别**,
|
|
|
|
|
|
再与**磁盘上的真实快照**(宿主库/`sessions` 表)**逐条对照**。
|
|
|
|
|
|
2. 🔴 **`--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 手写的当场被覆盖)。
|
|
|
|
|
|
- **判据(两条纪律,都落在文件上)**:
|
|
|
|
|
|
1. **同一句话要节流**:窗口(`DSH_NEED_GAP`,默认 600 s)内**同原因** ⇒ **不重写**(⛔ 不刷屏)。
|
|
|
|
|
|
⚠️ 判据必须落在**文件**上(⛔ 不是内存)—— 钩子每次都是**新进程**,内存节流**跨不了进程**。
|
|
|
|
|
|
2. **人写的内容必须原样带走**:重写时把 `## 解除条件` 及其后整段**保留**。
|
|
|
|
|
|
- 🔴 **为什么这条难自己发现**:程序侧"我明明喊了"是**对的** —— 问题出在**喊的姿势**(频率 + 覆盖),
|
|
|
|
|
|
⛔ 任何"写成功了吗"的断言都**查不出来**。⇒ 得换问法:**"人看到这条会怎么想?"**
|
|
|
|
|
|
- **判据落地**:`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 红**(符合预期)。
|
|
|
|
|
|
- **判据**:
|
|
|
|
|
|
1. **先想清楚"这个样式从哪来"再写判据** —— 三种来源要分开查:**内联属性** / **`<style>` 里的类规则** /
|
|
|
|
|
|
**SVG 的 presentation attribute 默认值**。⛔ 别默认"属性应该挂在标签上"。
|
|
|
|
|
|
2. 🔴 **判据要能在改前报红** ⇒ 参与对照的**备份文件路径必须跟着一起换**(本轮靠 `BOARD_HTML` 环境变量指过去)。
|
|
|
|
|
|
⚠️ 备份里根本没有 `.grp` 规则 ⇒ **报红正是预期**,⛔ 别看到红就以为判据坏了。
|
|
|
|
|
|
- **自查**:新增一条「样式类」判据时,问一句「**这条规则写在哪个文件里?我读的是那个文件吗?**」
|
|
|
|
|
|
|
|
|
|
|
|
## P0-14 🔴 **替换注释块时漏掉结尾 `*/` ⇒ 整段注释吞掉后面代码,而「数括号」查不出来**(★ 2026-10-01 实测)
|
|
|
|
|
|
|
|
|
|
|
|
- **形状**:用编辑工具换掉一段 `/* … */` 注释时,替换的两端**都没带上结尾 `*/`** ⇒
|
|
|
|
|
|
新注释与它下面那一行代码**粘连成一条注释**,从那行往下的代码**全被吃掉**。
|
|
|
|
|
|
- **实测**:`assets/board.html` 改完,`node --check` 只报一句 `SyntaxError: Unexpected token '}'`,
|
|
|
|
|
|
并说「多余的 `}` 在 772 行」—— 而**真正的病灶在 ~1000 行**(那里少了一个 `*/`)。
|
|
|
|
|
|
当场表现为**渲染断言 35 条全红**,看着像"改坏了半个文件"。
|
|
|
|
|
|
- 🔴 **为什么这个坑难查**:第一反应是数 `{` 和 `}` 的个数。**没用** ——
|
|
|
|
|
|
被吞掉的那段里本来就有 `{`,它从「代码」变成了「注释里的字符」⇒ **两边计数照样相等**;
|
|
|
|
|
|
同理,字符串字面量里的 `}`(含中文引号包住的文案)也会干扰朴素计数。
|
|
|
|
|
|
⛔ **别用字数统计判括号平衡**。
|
|
|
|
|
|
- **判据(两条,任选)**:
|
|
|
|
|
|
1. ✅ **最省事**:改完**立刻**跑一次语法检查 —— JS `node --check <f>` / Python `python -m py_compile <f>`。
|
|
|
|
|
|
⚠️ 本坑当时**确实跑了** `node --check` 也报了错,**只是它报的行号把人带偏了** ⇒ 报了错也别只信行号。
|
|
|
|
|
|
2. ✅ **要精确定位**:用**能识别字符串/注释/正则字面量**的扫描器做嵌套深度统计 ——
|
|
|
|
|
|
本轮落在 `tmp/_brace4.js`。它同时跑「改前备份」与「当前文件」:备份 `depth=0`、当前 `depth=-1`
|
|
|
|
|
|
⇒ 一眼看出「多的不是括号,是**注释没闭合**」。
|
|
|
|
|
|
- **自查**:凡改动跨 `/* … */` 边界 ⇒ **必跑语法检查** +(大文件)**深度扫描器对照备份**。
|
|
|
|
|
|
⛔ 别只信「我只改了一处、不可能影响别处」。
|
|
|
|
|
|
|
|
|
|
|
|
## P0-13 🔴🔴 **断言在「空样本」上恒真 ⇒ 看着绿其实从没执行过**(★ 2026-10-01 实测 · 空样本一出现就翻红)
|
|
|
|
|
|
|
|
|
|
|
|
- **形状**:`arr.every(...) === true` 这种写法,**`arr` 为空时恒真** ⇒ 用例**一条都没验**却是绿的。
|
|
|
|
|
|
🔴 与 P9「校验"可用"时只看"能跑起来"」同族,但更阴:**它不是不严,是根本没跑**。
|
|
|
|
|
|
- **实测**:`tmp/render-check.mjs` 里那条「唤醒会话/跟进会话⛔ 不许混进第三层」写成
|
|
|
|
|
|
`_notWorker.every(s => !arch.includes(s.id8))` —— 而当时**线上一条唤醒/跟进会话都还没有**
|
|
|
|
|
|
⇒ `_notWorker` 恒空 ⇒ **恒真**。⚠️ 当天真建出唤醒会话后它**立刻翻红**,
|
|
|
|
|
|
而且红得**没道理**:它查的是「**整张图**里有没有这个 id」,而唤醒会话**本来就该画在主会话左侧**
|
|
|
|
|
|
(那是它的正式位置)⇒ **同一处两个毛病**:空样本假绿 + 判据写错了地方。
|
|
|
|
|
|
- **判据(两条)**:
|
|
|
|
|
|
1. **空样本不许算通过** —— 要么 **SKIP**(像本文件里"前提不成立就 SKIP"那几条),
|
|
|
|
|
|
要么**用合成样本**把分支逼出来(`render-check.mjs` 的 `unrecBad` / `retBad` / `wlBad` 三个注射块就是这个手法);
|
|
|
|
|
|
2. 🔴 **断言必须能"改前报红"** —— 拿**改前备份**跑同一套桩做**红绿对照**;
|
|
|
|
|
|
备份已被后续改动覆盖时,用**变异法**(把判据回退成旧写法,看它是否报红)。
|
|
|
|
|
|
- ⚠️ **连带教训(同轮同时踩到的第二大坑)**:**别把期望值写死在断言里**。
|
|
|
|
|
|
同一批断言里有 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` ✅ —— 说明**只要标题带 `主控` 就没问题**,
|
|
|
|
|
|
坏的是"**规范没被执行**"(实际建出来的主会话续棒写成了 `接续 · <线> · <具体>`,没带 `主控`)。
|
|
|
|
|
|
- **两条正解(⛔ 都属方案变更,先拍板再动)**:
|
|
|
|
|
|
1. **补登记**(零代码改动):`collabd.py --declare --role main` 登记该会话 ⇒ 走 `resolve_main()` 第①层。
|
|
|
|
|
|
✅ 立刻可用;⚠️ 登记是**静态**的,换主会话后不会自己变。
|
|
|
|
|
|
2. **改判据 + 收紧命名**(治本):`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/`**。
|
|
|
|
|
|
- **两个真因(同一条链,都要修)**:
|
|
|
|
|
|
1. 🔴 **钩子把技能包当工作区**:`wb-result-hook.py` 的 `_WS_ROOT` 原按 `3×dirname(__file__)` 推 ——
|
|
|
|
|
|
包内这份(`<包根>/scripts/hooks/`)推出来**正好=技能包自己** ⇒ 交给 `collabd` 的
|
|
|
|
|
|
`COLLABD_WORKSPACE` 是**包目录** ⇒ 运行产物落进包里。**同文件里本来就有正确的 `resolve_ws()`**
|
|
|
|
|
|
(env 优先 + 内容标志上溯)⇒ 这不只是"推错",是**同一事实有两套实现**。
|
|
|
|
|
|
✅ 改为复用 `resolve_ws()`,并加「**未知工作区 ⇒ 停手**」(⛔ 不拿 cwd/包目录顶替)。
|
|
|
|
|
|
2. 🔴 **重定向 ⇒ 编码崩**:`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` 是**毫秒级写小文件**,且棒通常**不会精确同时**上报 ⇒ **实际概率低,但不是零**。
|
|
|
|
|
|
**✅ 立刻可用的规避(⛔ 不改代码)**:**上报后回读核对** ——
|
|
|
|
|
|
```bash
|
|
|
|
|
|
<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 运行 试试呢**`<br>而 DB 里 `sessions.title` = **`接续 · 机制线(钩子锚点真实投递取证)`** | 🔴 **不一致** |
|
|
|
|
|
|
| ③ **且长期停在旧值** | 05:51:37 / 05:53:04 / 05:54:16 / 05:57:51 / 06:02:15 ⇒ **五次推送全是同一个旧标题** | 不是瞬时抖动,是**缓存陈旧** |
|
|
|
|
|
|
|
|
|
|
|
|
⇒ **结论(⛔ 严格划清"可证"与"未证" —— 别学 P0-2 的老毛病)**
|
|
|
|
|
|
|
|
|
|
|
|
| | 内容 | 状态 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| ✅ **可证** | ① **用户那条消息确实丢了**(转录、`PromptIterator` 双无)⇒ **丢在「客户端 → 宿主」这一跳**(服务端无任何记录)<br>② **宿主推给前端的元数据是旧的**:推送 `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 connectionId` **303 次,其他小时 0 次** ⇒ **客户端连接状态在该小时反复丢失**。
|
|
|
|
|
|
- **与相邻几型的区别**:②f 有中断证据 · ②g 工具在循环 · ②h 干完了推不出去 · **本型=消息在队列里、没有消费者**(`session-mechanism`(`references/forensics.md` §3))。
|
|
|
|
|
|
- ⚠️ **窗口里可能同时叠着另一层**:本次 22:58:40–22:59:50 还有 `diagnostic log write failed`(`droppedLines` 496→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`。
|
|
|
|
|
|
探针只要"行里同时含这两个子串"就命中 ⇒ **命中了自己的回声**。
|
|
|
|
|
|
- **修法**:判据必须**只认真正的记录行**,并**显式排除回显行**:
|
|
|
|
|
|
```python
|
|
|
|
|
|
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 的正确姿势。
|
|
|
|
|
|
- **修法(三条硬规则 · 此后新增钩子一律照此办)**:
|
|
|
|
|
|
1. 🔴 **拿到结果才有用 ⇒ 才允许同步等**;否则**一律后台**(`_bg`:`Popen` + stdout 落**文件** + `DETACHED_PROCESS`,⛔ 不用 PIPE、⛔ 不 wait)。
|
|
|
|
|
|
判断句:「这个子过程的返回值,钩子会用吗?」答不上来 ⇒ 后台。
|
|
|
|
|
|
2. 🔴 **算术题先算再写**:`宿主注册值 - 解释器冷启动(实测 0.3~1.0s,波动不可控) - Σ子过程实测耗时 > 50%` ⇒ ⛔ 不许同步。
|
|
|
|
|
|
本机为证:20 − 1 − 13.5 = 5.5s 余量,而同事件另 5 个钩子在抢 CPU ⇒ **必越线**。
|
|
|
|
|
|
3. 🔴 **⛔ 别信"实测均值",要信"硬超时"**:`--once` 均值 0.5s 看着安全,但它的 `timeout` 是 **12s** ⇒ 抖动时照样吃掉大半预算。
|
|
|
|
|
|
4. 🔴🔴 **脚本自己必须带"自身硬闸"**(本次**没做这一步就是没根治**):前三式只修掉了**这一次的那三个具体活**;
|
|
|
|
|
|
**任何人(含之后的另一个会话)只要再往这条路上加一个同步子进程,故障就原样复发**,
|
|
|
|
|
|
而复发的第一现场是「用户说不了话」——**不可能等到事后排查**。
|
|
|
|
|
|
⇒ 落地:`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` 字段**逐轮复用
|
|
|
|
|
|
⇒ **无论用户当轮说了什么,每轮都在下同一条命令**。三个后果:
|
|
|
|
|
|
① 越权(用户没要求就建排期)② 重复(同名无去重,越排越多)③ 无上限/无过期(用户原话「越排越多」)。
|
|
|
|
|
|
- **修法(三条,⛔ 改文案治不了,必须动结构)**:
|
|
|
|
|
|
1. 🔴 **授权闸**:`_authorized_now()` —— **只认用户当轮原话里的触发词**(`GAP_TRIGGERS`)。
|
|
|
|
|
|
⚠️ 刻意**不用**「上一轮说过」「队列有缺口」「机制建议」当授权 —— 那些都是**系统自己的诉求,不是用户的**。
|
|
|
|
|
|
⚠️ 必须把当轮 `payload` 存进模块级 `_CUR_PAYLOAD`,否则判据读到空串、闸门形同虚设。
|
|
|
|
|
|
2. 🔴 **缓存存原始数据、不存成品文案**:`gap-cache.json` 改存 `_raw`(`--gap` 原始 JSON),
|
|
|
|
|
|
文本在**读取时现算**(`_gap_text(raw, authorized)`)⇒ 呈现永远跟当轮授权一致。
|
|
|
|
|
|
旧版缓存(只有 `_text` 无 `_raw`)**判为无效自动重刷**,⛔ 不做兼容迁移(迁移=把旧文案再投一次)。
|
|
|
|
|
|
3. 🔴 **未授权时只通报、不派活**:标题写「ℹ️ 现状通报(⛔ 不构成行动指令)」并**明写「⛔ 不要建任何排期」**;
|
|
|
|
|
|
授权时才输出「⚠️ 缺会话 ⇒ 自动拉起」,且要求**在回复里说清是依据本轮授权做的**。
|
|
|
|
|
|
另加 `_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`。
|
|
|
|
|
|
- ✅ **两条硬纪律**:
|
|
|
|
|
|
1. ⛔ **绝不把 `delete` 的 `success:true` 当已删** —— 每次删完必须回读 `deleted_at`(`coalesce(deleted_at,'')=''` 才算未删)核对。
|
|
|
|
|
|
2. ⛔ **`list` 的返回行数 ⛔ 不能当在册总数**(本例 `list` 2 条 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/webserver` | `ai1net-dsh-server/domains` | ❌ 全部撞键 |
|
|
|
|
|
|
| `…/ai1net-dsh-server/domains/desktop` | `ai1net-dsh-server/domains` | ❌ 同上 |
|
|
|
|
|
|
| `…/ai1net-dsh-server/webserver` | `ai1net-dsh-server/webserver` | ✅ 独立 |
|
|
|
|
|
|
| `…/ai1net-dsh-server/交付物/webserver` | `ai1net-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_id` 300 秒内只放行一次**。
|
|
|
|
|
|
三个场景用同一个 `sid` ⇒ 后两个被**静默挡在早退分支** ⇒ `ctx` 恒空。
|
|
|
|
|
|
- 🔴 **危害不在这一个钩子**:「没输出」与「没检查」**长得一模一样** ⇒ 一旦顺着"钩子坏了"往下查,
|
|
|
|
|
|
会把时间全花在读代码上,而真因是**自己造的自检环境**。本轮就是这样绕了一圈。
|
|
|
|
|
|
- ✅ **两条硬纪律**:
|
|
|
|
|
|
1. 验任何带节流/去重的钩子,**每次调用必须换一个唯一标识**(本例=`session_id`)。
|
|
|
|
|
|
2. 验收脚本里加一道**硬门**:`注入为空 ⇒ 立即判失败`(本轮做成 `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 实测根治)
|
|
|
|
|
|
|
|
|
|
|
|
- **两类药,缺一不可**:
|
|
|
|
|
|
1. **常驻/会重生的 daemon ⇒ 用 `pythonw.exe`**(GUI 子系统,Windows **永不**为其分配控制台)。
|
|
|
|
|
|
`sys.executable` ⇒ `python.exe`(控制台子系统)⇒ 被 detach 后**必新开控制台** ⇒ 每次重生闪一次。
|
|
|
|
|
|
⚠️ **`DETACHED_PROCESS` 单独用不够**:它只是"不继承父控制台",控制台 exe 仍会被新分配窗口。
|
|
|
|
|
|
2. **一次性短 spawn(`netstat`/`taskkill`/`--tick`)⇒ 补 `creationflags=CREATE_NO_WINDOW(0x08000000)`**。
|
|
|
|
|
|
⚠️ 常驻那批可加 `| DETACHED_PROCESS(0x8) | NEW_PROCESS_GROUP(0x200)`;
|
|
|
|
|
|
⛔ **不要** `CREATE_BREAKAWAY_FROM_JOB`(会被拒)。
|
|
|
|
|
|
- 🔴 **自检必须用 AST 全量扫**(⛔ 别 grep):本轮靠它捞出 `board.py:1212` 的 `taskkill`——
|
|
|
|
|
|
那是七文件修完后**最后一处漏网**(`--takeover` 路径上的)。
|
|
|
|
|
|
- ⚠️ **不是所有缺 flags 都该死**:`NeoData金融搜索服务/scripts/query.py:51` 是 **pip 装包**
|
|
|
|
|
|
(只在缺 requests 时跑一次、非周期)⇒ 与闪窗无关;`selftest.py` 7 处是**自检脚本**。
|
|
|
|
|
|
⇒ **判据="这个调用会不会被周期/重生路径反复执行"**,⛔ 不是"缺了就是错"。
|
|
|
|
|
|
⚠️ 第三方技能包缺 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`)长期存活 |
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 教训(判据级)**:
|
|
|
|
|
|
|
|
|
|
|
|
1. **⛔ 别用「这一种起法失败」推「本机不可能」** —— 至少验**三种载体**(工具调用树/宿主后台任务/用户登录会话)再下全称否定。
|
|
|
|
|
|
2. **反证优先于推断**:用户一句「那个工作台是如何一直运行的」就是现成反例 ⇒ **该先去找它怎么起的**(`start.bat` 一眼看到 `start "" node.exe`),⛔ 别先写一段"本机不可能"的理论。
|
|
|
|
|
|
3. **查证方向错了要整条撤回,不只是改措辞**:基于假前提给的建议(「改用排期当时钟」)⇒ **整条撤**,⛔ 不许"保留但加个注释"—— 那会让下一个人照着错的前提做决策。
|
|
|
|
|
|
4. **改了代码 ≠ 改了知识**:这类结论**至少落在 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 实测)
|
|
|
|
|
|
|
|
|
|
|
|
**现象**:两处「改了配置但看板纹丝不动」,成因**完全不同**,⛔ 若不取证会把两次都归成同一个错。
|
|
|
|
|
|
|
|
|
|
|
|
1. **配置根本不重读** —— `board.py` 的 `C`(配置字典)是**模块级加载一次**,⛔ **不每次快照重读**。
|
|
|
|
|
|
⇒ 改 `collabd.config.json` 后**必须重启看板**。
|
|
|
|
|
|
⚠️ **这是 P0-17「改了看不见」漏记的一面** —— 之前只记了「改 `board.py` 代码要重起」,⛔ 漏了「改配置同样要重起」。
|
|
|
|
|
|
⇒ 判据:看进程启动时间 vs 源文件 mtime(`board.py` 的 `C` 无 re-read 路径)。
|
|
|
|
|
|
|
|
|
|
|
|
2. 🔴🔴 **读的是另一份配置**(更隐蔽)—— 启动服务时**没设 `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)。
|
|
|
|
|
|
|
|
|
|
|
|
3. **连带发现**:`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` 认领方式对机制层不成立)。
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 判据**:
|
|
|
|
|
|
1. **锁的生命周期 = 任务的生命周期**(2026-09-14 用户明令)—— 做完**必须** `--release-exec`。
|
|
|
|
|
|
⚠️ 干等自己那把锁是最贵的浪费:别人进不来、我也忘了它在谁手上。
|
|
|
|
|
|
2. **机制层文件 ⇒ 认领必须 ⛔ 不带 `--domains`**(带域=域锁,对机制层不成立);
|
|
|
|
|
|
正确姿势:`--claim-exec "<名>"`(不带域)= 全局独占。
|
|
|
|
|
|
3. **派棒前先 `--status` 看锁**;⛔ 别让自己成为别人的路障。
|
|
|
|
|
|
4. 🔴 **执行棒"零改动"先查锁,别急着判它失败** —— 判据=
|
|
|
|
|
|
`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 次删除动作**。三个原因叠加:
|
|
|
|
|
|
1. `exists()` → `unlink()` 之间有**窗口**(TOCTOU);
|
|
|
|
|
|
2. **投影轮(`mutate=False`)与常驻轮(`mutate=True`)是两个进程各跑一遍** ⇒ 判据被重复执行;
|
|
|
|
|
|
3. 护栏计的是**动作数**,⛔ 不是"真的删掉几个" ⇒ **失败/异常也算一次**。
|
|
|
|
|
|
|
|
|
|
|
|
**修法(判据级)**:**"一辈子只删一次"落成状态位**,⛔ 不是靠"检查文件在不在"。
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 判据(写下来防复发)**:
|
|
|
|
|
|
1. **常驻里任何"周期性写/删"的文件,稳态下每轮的写删次数必须 = 0** ——
|
|
|
|
|
|
「降低频率」**不是修法**(P0-6 已经吃过一次:那套"快速否决"省不掉,因为内容一直变)。
|
|
|
|
|
|
2. ⛔ **别用"文件在不在"当幂等判据**(TOCTOU + 跨进程重复 + 异常也计数)⇒ 用**状态位**。
|
|
|
|
|
|
3. 🔴 **本条与 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>"}
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 判据**:
|
|
|
|
|
|
1. **判"有没有调用"** ⇒ **AST**(只认 `Call` 节点的 `func` 位);⛔ 判"有没有出现"才是字面 grep。
|
|
|
|
|
|
2. ⚠️ 滤 `#` 开头行**不够** —— docstring、`'''` 块注释、字符串字面量里的字样都会骗过你。
|
|
|
|
|
|
3. ⛔ **不许因为字面判据假红就把断言改弱**("改成 `deliver=False`"=换个说法说同一件事,P0-13 同族)
|
|
|
|
|
|
⇒ 换**可证伪的判据**(本轮用的是「**真调用** `supervise(deliver=True, mutate=True)` ⇒ `deliver.skipped`
|
|
|
|
|
|
必须为 `retired-20261003`」——⛔ 改实现就会红,不是恒真)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-37 ⚠️ **「一个角色退役了」与「承载它的那个进程也退役了」是两件事**(★ 2026-10-03 实测)
|
|
|
|
|
|
|
|
|
|
|
|
**现场**:常驻 `--supervise` 原本**只有两个职责**(① 队列投递 + 唤醒 ② 建检查会话排期)。
|
|
|
|
|
|
投递退役后我一度想**连常驻一起删**(既然它不投递了)。查完发现 ② 那条腿**只有它承载** ——
|
|
|
|
|
|
`maybe_spawn_check_agent()` 是**唯一**建 `[检查]-…` 排期的地方,而检查会话**只能靠排期开**(⛔ 开不了别的通道)。
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 判据**:
|
|
|
|
|
|
1. 删一个组件前先答一句:**「它现在还承担什么?那些职责另有谁承载?」**
|
|
|
|
|
|
——「它以前主要做 X」⛔ 不等于「它只做 X」(本轮就是靠这句话漏看了 ②)。
|
|
|
|
|
|
2. **职责退役 ⇒ 改名 + 改副标题,⛔ 不等于删节点** —— 架构图上那格画着「常驻投递 · 一直运行」
|
|
|
|
|
|
而实际那两段已删,就是 **P0-25「服务自报健康 ≠ 服务有内容」**的活标本。
|
|
|
|
|
|
本轮改成「建检查会话排期 · 一直运行」+「⛔ 投递已于 2026-10-03 退役」两行。
|
|
|
|
|
|
3. ⚠️ **反例同样要认**:`--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 里**还留着**测试值。
|
|
|
|
|
|
|
|
|
|
|
|
**修法(三条,缺一不可)**
|
|
|
|
|
|
1. **加载前换成目标配置、用完还原**(`_old = {k: os.environ.get(k) …}` + `try/finally` 逐个还原)
|
|
|
|
|
|
—— ⛔ 不还原 ⇒ 污染后续用例,且这种污染**不报错、只让别的用例莫名飘**。
|
|
|
|
|
|
2. **在真磁盘上造目标**(`tempfile.mkdtemp()` 里各放一份 `goal.json`)
|
|
|
|
|
|
⇒ 读数来自**真文件**,⛔ 不从夹具推导。
|
|
|
|
|
|
3. **加变异对照**:把 `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」的名字 ⇒ **把本工作区的执行情况说成了对方的**。
|
|
|
|
|
|
⛔ **假数据比空白危险**:空白会被追问,假数据会被当成结论直接用。
|
|
|
|
|
|
|
|
|
|
|
|
**修法(三条,缺一不可)**
|
|
|
|
|
|
1. **每格的数据源必须跟着格走** ⇒ peer 格只喂 `_peer_tasks()`/`_peer_srows()`/`_peer_state()`,
|
|
|
|
|
|
三者**只读对方目录下的文件**;读不到 ⇒ **空 + 界面如实说明**(`peer_scope` + 前端 `renderPeerNote()`),
|
|
|
|
|
|
⛔ **绝不拿本工作区的数据补位**。
|
|
|
|
|
|
2. **必须补捞** ⇒ `_session_rows()` 取的是**全库最近 50 条**(按 `last_activity_at` 倒序)
|
|
|
|
|
|
⇒ 对方工作区的会话**一条都不在里面** ⇒ peer 格显示「0 条」=**假象**(对方主会话明明 `working`)。
|
|
|
|
|
|
⇒ 单独 `_peer_session_rows()` 按 `cwd` 精确补捞(SQL 里 `replace(cwd,'\','/')=?`,⛔ 别拉到 Python 侧全表扫)。
|
|
|
|
|
|
⛔ **不许**为了捞它把全局 `limit` 放大 —— 那会让**本工作区**的 `others_running` 计数暴涨(为一个新功能改坏老行为)。
|
|
|
|
|
|
3. **作用域要跟着换** ⇒ `_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` 非空**。
|
|
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
**三个秒退坑**(详 ⇒ `supervise-persistence.md`;⚠️ ③ 随 `.ps1` 形态 2026-10-06 整套废弃,⛔ 仅存历史):
|
2026-10-05 14:13:24 +08:00
|
|
|
|
① 不设 `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 个 ✗)。
|
|
|
|
|
|
|
|
|
|
|
|
✅ 变异脚本**必须**带两道护栏(缺任一道,一次误判就够):
|
|
|
|
|
|
```python
|
|
|
|
|
|
# 护栏 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`」,**那串明明就在文件里** —— 在**注释**里。
|
|
|
|
|
|
**根因**:那段说明注释**正当**引用了改之前的旧写法("为什么改"的证据,⛔ 不许删)⇒ 全文件查旧写法**永远命中**。
|
|
|
|
|
|
|
|
|
|
|
|
✅ 正解(缺一步仍假绿):
|
|
|
|
|
|
```python
|
|
|
|
|
|
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:sendPrompt` **4** 次(✅ 这个对得上)。
|
|
|
|
|
|
>⚠️ 大概率是当初**数的是另一个口径**(如只数某个时间窗/某个 `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 个不补,副本就带着旧缺陷跑。**
|
|
|
|
|
|
|
|
|
|
|
|
✅ **同步四步**(⛔ 别整目录覆盖):
|
|
|
|
|
|
1. **先逐文件 md5 双向 diff** —— ⛔ 用**文件数**当判据会错(副本多一份《副本使用说明.md》)。
|
|
|
|
|
|
2. **只覆盖真差异** —— ⛔ **`roots.env` 绝不覆盖**:它是**工作区专属**指针(`setdefault` 兜底
|
|
|
|
|
|
⇒ 覆盖会让副本去读写**另一个仓库**)。
|
|
|
|
|
|
3. **备份挪出包体** —— 同步完把备份移到 `<工作区>/交付物/`;留在包内会被 `t_pkg_hygiene`
|
|
|
|
|
|
的「⛔ 不许长回备份」判红(实测副本自测 FAIL 1)。
|
|
|
|
|
|
4. 🔴 **副本必须真跑一次** —— ⛔「编译过」≠「能跑」。本轮副本首跑 **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()`(会话级日志源):
|
|
|
|
|
|
1. **跨日覆盖**:`_conv_log()` 返回 `[今天, 昨天]`,而取状态用的是**遍历到的最后一条**
|
|
|
|
|
|
⇒ **昨天**卡住的 `working` 把**今天**已经 `idle` 的盖掉。
|
|
|
|
|
|
2. **无新鲜度闸**:`_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` 指的既不是本区发布物,也不是「全局技能」,
|
|
|
|
|
|
而是「本区技能副本」** —— 看起来像"跑对了",实际**判据②不过**。
|
|
|
|
|
|
|
|
|
|
|
|
**根因(两层,缺一不可)**:
|
|
|
|
|
|
1. **引用没跟着口径改**:`tmp/start_supervise.py` 第 11 行写死
|
|
|
|
|
|
`WS + "/.workbuddy/skills/session-mechanism/scripts/collabd.py"`(**技能副本**),
|
|
|
|
|
|
而口径(`deploy_code.py` 开头,用户 10-03 定案)要求各跑
|
|
|
|
|
|
`<工作区>/.workbuddy/collab/collabd.py`(**本区发布物**)。
|
|
|
|
|
|
2. **`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()` 一个写法)**:
|
|
|
|
|
|
1. `ensure_supervise()` 起常驻时填 `Path(__file__).resolve()` =「**当前正在跑的那一份**」;
|
|
|
|
|
|
若它本身是**技能目录那份**(P0-57 形态)⇒ 续命又起技能目录那份 ⇒ **互为镜像、生生不息**。
|
|
|
|
|
|
2. `_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.02~0.09 s CPU / 20 s ≈ 0.1~0.5% 单核**)⇒
|
|
|
|
|
|
「常驻吃 CPU」的**真因是条数**,⛔ 不是单条本身 —— 排查时先数条数,⛔ 别去优化循环体。
|
|
|
|
|
|
|
|
|
|
|
|
✅ **修法=抽唯一基准 `_own_path()`(发布物优先),三处共用**:
|
|
|
|
|
|
```python
|
|
|
|
|
|
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()` 的闸① 写成了
|
|
|
|
|
|
```python
|
|
|
|
|
|
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`
|
|
|
|
|
|
⇒ 检查会话照它读 **读空** ⇒ **又回去到处找** —— 正是要避免的那件事。
|
|
|
|
|
|
|
|
|
|
|
|
🔴 **活证据(本区台账,实测)**:
|
|
|
|
|
|
```json
|
|
|
|
|
|
{"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()`**。
|
|
|
|
|
|
⛔ 不许用「起完立刻验一次活着」当结论。
|
|
|
|
|
|
|
|
|
|
|
|
**同型的另外两条(一并记住)**:
|
|
|
|
|
|
1. **别把「现在活着」当「这种起法能长期」** —— pid 19424 曾活 24.2 h,只因为**它的父链已退出**
|
|
|
|
|
|
(孤儿化后不受会话结束影响)。⇒ 要判「还能活多久」=**先验父链 + 读心跳 `started_ts`**。
|
|
|
|
|
|
2. **判「是不是被正常停的」要看 `guard.stop`** —— 它在 ⇒ `supervise_stop()` 走的收工流程(正当);
|
|
|
|
|
|
**它不在 + 进程没了 ⇒ 被外力杀**(本轮的判定路径就是这条)。
|
|
|
|
|
|
|
|
|
|
|
|
**口径边界(⛔ 别搞反)**:本现象**不是故障**。`supervise-persistence.md` 口径明写:
|
|
|
|
|
|
常态=「调用技能完成目标时起后台任务 + 检查程序」,**会话一收工就断,断了自己会补**
|
2026-10-07 00:32:47 +08:00
|
|
|
|
(实测补起延迟 1~2 分钟)。⭐ **只有「目标做完还要继续跑」才需要计划任务那一层**。
|
|
|
|
|
|
🟠 本行原写「那层**是可选,⛔ 不许当欠项摊**」(2026-10-04 订正)——**已被 2026-10-05 用户拍板推翻**:
|
|
|
|
|
|
计划任务现在是**既定形态**(`collabd-keepalive-<区>` 每 5 分钟兜底,开关 `collabctl.py on|off`)。
|
|
|
|
|
|
⚠️ 两句话不矛盾:**日常不必操心**(机制自己装),但它**是既定形态**,⛔ 不是"可有可无的选项"。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 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` **全部按它定死**:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
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` 起=绑在会话上。
|
|
|
|
|
|
|
|
|
|
|
|
**取证方法(三步,缺一不可)**:
|
|
|
|
|
|
1. **同刻对比两区心跳**:`round` 与 `started_h` —— 同一时刻起、round 差一倍 ⇒ 一区被回收过。
|
|
|
|
|
|
2. **看续命 `why`**:`grep "常驻续命" <区>/logs/_collabd.log | tail` —— `why=tick` 健康,`why=cli` 病态。
|
|
|
|
|
|
3. **看该区有没有 `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`。
|
|
|
|
|
|
|
|
|
|
|
|
**排除项(逐个做掉,别跳)**:
|
|
|
|
|
|
|
|
|
|
|
|
1. **代码没问题** —— 加一层 `runpy` wrapper(同一 `pythonw.exe`、同一参数、同一工作目录)
|
|
|
|
|
|
跑 `board.py` ⇒ `LastTaskResult=**267009**`(=`0x41301`「任务正在运行」),
|
|
|
|
|
|
日志里明写 `看板已起:http://127.0.0.1:20099/`。
|
|
|
|
|
|
2. **命令行没问题** —— `(Get-ScheduledTask).Actions[0]` 逐字打出来(含 `ConvertTo-Json`)
|
|
|
|
|
|
与手工串**完全一致**,无多余引号、无 BOM 残留、无转义问题。
|
|
|
|
|
|
3. **同一串命令由 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 形状**(⛔ 别自创):
|
|
|
|
|
|
|
|
|
|
|
|
1. 任务动作改成 **`powershell.exe`**:
|
|
|
|
|
|
`-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "<keeper>.ps1"`
|
|
|
|
|
|
(⛔ 不加 `-NonInteractive`;⛔ 动作里**不要**出现 `pythonw`)。
|
|
|
|
|
|
2. `.ps1` 里用 **`Start-Process -FilePath <pythonw> -ArgumentList … -PassThru -Wait`**
|
|
|
|
|
|
—— `-Wait` 给的是**真阻塞**(这是 keeper 能"守着"子进程的前提)。
|
|
|
|
|
|
3. `.ps1` **必须存成 UTF-8 带 BOM**(`encoding="utf-8-sig"`,行尾 `\r\n`)。
|
|
|
|
|
|
落盘后立刻自检前 3 字节 = `\xef\xbb\xbf`,并用
|
|
|
|
|
|
`[System.Management.Automation.Language.Parser]::ParseFile()` 过一遍语法。
|
|
|
|
|
|
4. keeper 内部:**端口上已有本看板 ⇒ 只睡不重拉**(判据=`/healthz` 同时含 `"ok"` 与 `"snapshots"`,
|
|
|
|
|
|
⛔ 不是「端口开着」)⇒ 避免叠实例。
|
|
|
|
|
|
|
|
|
|
|
|
**验收判据(全绿才算完,⛔ 缺一不可)**:
|
|
|
|
|
|
|
|
|
|
|
|
- `(Get-ScheduledTask).State` = `Running`
|
|
|
|
|
|
- `LastTaskResult` = **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']`**。
|
|
|
|
|
|
|
|
|
|
|
|
**真因**(两层,都要治):
|
|
|
|
|
|
|
|
|
|
|
|
1. **同一份代码存在「技能包」与「工作区部署副本」两份**,而**常驻进程加载的是副本**:
|
|
|
|
|
|
- `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`。
|
|
|
|
|
|
2. **副本可能停在很久以前**:本区副本的 `board.py` md5 与技能包**差 46 行**、
|
|
|
|
|
|
`collabd.py` **少 718 字节**,副本里 `ACC_ADV_PREFIX` **一次都没出现**(`grep -c` = 0)。
|
|
|
|
|
|
⚠️ 副本**不是**被人手改过(`diff` 显示差异**只有**我们那几处新代码)⇒ 是**纯陈旧快照**,
|
|
|
|
|
|
属于 P0-51「修完全局忘了同步」的**同一族第 N 次**,只是这次栽的是**运行中的常驻**不是副本自检。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解(四步,⛔ 缺一不可)**:
|
|
|
|
|
|
|
|
|
|
|
|
1. **先取证「谁在跑哪一份」**:`Get-CimInstance Win32_Process` 列 `python%` 的
|
|
|
|
|
|
`ProcessId + CommandLine`(⛔ 别靠"我以为它读技能目录")—— 一个工作区里
|
|
|
|
|
|
`board` 与 `collabd` **可能来自两个不同位置**。
|
|
|
|
|
|
2. **同步副本**:技能包 → `<ws>/.workbuddy/collab/{board.py,collabd.py}`,
|
|
|
|
|
|
先 `shutil.copy2` 备份成 `*.bak-sync-<时间戳>`,再**写临时文件 + `os.replace` 原子替换**,
|
|
|
|
|
|
最后**逐字 md5 复核**(⛔ 不靠"我复制过了")。
|
|
|
|
|
|
⚠️ `workspace_mirror.py` **管不了这个目录** —— 它要求副本里有 `SKILL.md`,
|
|
|
|
|
|
而 `.workbuddy/collab/` 是**部署目录不是技能副本** ⇒ 会直接 `⛔ …不像技能副本 ⇒ 不动`
|
|
|
|
|
|
(本目录曾因此静默跳过,`--check` 报 97 项漂移却没人看)。
|
|
|
|
|
|
3. **重启常驻**:`Stop-ScheduledTask` → 杀旧 pid → `Start-ScheduledTask`;
|
|
|
|
|
|
验收=**心跳里出现新 pid** + `argv0` 指向本区副本 + 任务 `Running`/`267009`。
|
|
|
|
|
|
⚠️ 只同步**不重启** ⇒ 进程内存里仍是旧 bytecode(实测就是这个状态。
|
|
|
|
|
|
铁证:本机 `board._acc_summary()` 返回「全部 pass(9 条)」而服务端返回旧的 `非 pass` 列表
|
|
|
|
|
|
—— **同一份磁盘代码,两个结果 ⇒ 差的一定是"谁在跑"**)。
|
|
|
|
|
|
4. **判据落到进程级**:确认新 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 条**。
|
|
|
|
|
|
- **谁排的(两个创建者,⛔ 都不是"会话自己")**:
|
|
|
|
|
|
1. **常驻程序** `collabd.py::create_check_schedule()`(第 ~3894 行)—— 唯一的**检查会话**建排期入口
|
|
|
|
|
|
(用户第④条:「创建者是协作程序,不是会话」)。固定 `schedule_type='once'`、延迟默认 90s。
|
|
|
|
|
|
实测产物=`[检查]-[结果检查]-selftest-第N棒`(11 条)。
|
|
|
|
|
|
2. **`_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 时**必须**搬走的三件事(漏一件就断链)
|
|
|
|
|
|
|
|
|
|
|
|
1. **建检查会话排期的 `sessions-ended` 腿**
|
|
|
|
|
|
⛔ 它原先**只**挂在 `--tick` ⇒ 删了 tick 就**永远建不出"结果检查"会话**(队列非空=有活没人干)。
|
|
|
|
|
|
✅ 现在在 `--supervise` 主循环里:`if queue_pending() > 0: sessions-ended else: queue-empty`
|
|
|
|
|
|
2. **park 指纹探针**(`parkInQueue` + `hasWaiter=false`)⇒ 抽成 `_tick_park_probe()`,常驻每 20 轮调
|
|
|
|
|
|
⚠️ 它**不依赖投递** ⇒ 投递退役后仍有效,⛔ 不许跟着 tick 一起删
|
|
|
|
|
|
3. **顺手续命常驻** ⇒ 钩子改调 `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**。
|
|
|
|
|
|
|
|
|
|
|
|
### 自测判据的两条死穴(本轮实测踩到)
|
|
|
|
|
|
1. ⛔ **字面判 `"--tick" not in <源码>`** ⇒ **永久假红**:删除说明的注释里**必然**提到它
|
|
|
|
|
|
(P0-13 同族)⇒ ✅ 只认**带引号的 `"--tick"`**,⛔ 不认裸串。
|
|
|
|
|
|
2. ⛔ **判据由环境状态决定** ⇒ 合成测试区残留真常驻时,`ensure_supervise` 走**闸①幂等让位**、压根
|
|
|
|
|
|
不到口令闸 ⇒ `startswith("no-token")` 恒红 ⇒ ✅ 两条闸的返回**都要接受**(目标同为"不起新的")。
|
|
|
|
|
|
|
|
|
|
|
|
## P0-67 · 排期不是「定时任务」,是**开会话的申请书**(2026-10-05 用户点破,⛔ 别再说错)
|
|
|
|
|
|
|
|
|
|
|
|
用户原话:「你就算是 once 不也是走的定时任务吗,难道能直接创建会话?」
|
|
|
|
|
|
|
|
|
|
|
|
**完全正确**。⛔ 我此前说「`once` 是唯一开新会话的通道」**容易被误读**成"once 有开会话的能力"——
|
|
|
|
|
|
它没有。准确表述:
|
|
|
|
|
|
|
|
|
|
|
|
- **`once` 自己也是排期**,也是被宿主到点拾取的;它**同样不能**直接创建会话。
|
|
|
|
|
|
- **能开会话的永远只有宿主**。AI 侧(本程序是宿主之外的 python 进程)**没有任何开会话的能力**。
|
|
|
|
|
|
- ⇒ `once` 的真实身份 = **AI 侧唯一能递到宿主手里的"纸条"**:
|
|
|
|
|
|
「请你在 X 时刻开一条会话,prompt 是 …」。宿主扫描到它 ⇒ 才开会话。
|
|
|
|
|
|
|
|
|
|
|
|
### 由此纠正两个长期绕圈的说法
|
|
|
|
|
|
1. ⛔ 「为什么会产生排期/能不能不排期」—— **不能**。要开会话就必须写这张纸条,
|
|
|
|
|
|
⛔ 没有第二种投递方式。排期**不是"定时需求"**,是**"开会话的申请书"**。
|
|
|
|
|
|
2. ⛔ 「把 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 交的也是同一式样的申请书。
|
|
|
|
|
|
|
|
|
|
|
|
### 由此定死两条结论
|
|
|
|
|
|
1. 🔴🔴 **`once` ⛔ 不能删**。删它不是"去掉定时任务",是**掐断 AI 侧开会话的唯一通道**
|
|
|
|
|
|
⇒ 我的检查会话 + MCN 的按钮任务**同时断掉**(两处都是 `INSERT … 'once'`)。
|
|
|
|
|
|
2. 🔴 **"程序自己判断状态、创建会话"已经做到了**——判断在常驻五道闸(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 条的构成(实证)
|
|
|
|
|
|
1. **ai1net 25 条(10-01 09:05 → 10-02 10:49)= 正常,不是堆积**。
|
|
|
|
|
|
名字**各不相同**(`主控 · …`/`接续 · …`/`[协作]-…`/`[跟进]-…`/`[唤醒机制] …`),
|
|
|
|
|
|
间隔 2.6 ~ 420 分钟 ⇒ **一次一个需求、有需要才建**,正是用户说的那种。
|
|
|
|
|
|
⛔ 其中**没有一条**是 `[检查]`(`[检查]` 体系是后来才进常驻的,时间线问题,⛔ 不是卡闸)。
|
|
|
|
|
|
2. **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 分钟)。
|
|
|
|
|
|
3. 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`、
|
|
|
|
|
|
**20099 `Listen` 计数恒 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 端到端实测,全绿)
|
|
|
|
|
|
1. `off` ⇒ 打印「**已杀进程:3 个**」;5 秒后 `list_procs` 只剩"诊断脚本自己+列举进程的 ps" ⇒ **真停**。
|
|
|
|
|
|
2. `on` ⇒ 三任务重建触发;25 秒后两区常驻**活、心跳新鲜**;看板 `State=Running`/`267009`。
|
|
|
|
|
|
3. 看板 **`curl --noproxy '*' http://127.0.0.1:20099/` ⇒ HTTP 200 · 148274 B**;端口 `Listen` 计数 = 1。
|
|
|
|
|
|
4. 用户可见入口 `会话机制-一键开关.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 用户点名「检查之前创建的协作程序和检查程序运行是否正常」时抓到)
|
|
|
|
|
|
|
|
|
|
|
|
**用户口径**(逐字):「**处理完毕后,检查之前创建的协作程序和检查程序运行是否正常。**」
|
|
|
|
|
|
|
|
|
|
|
|
### 一、先分清"两套程序"(⛔ 别混为一谈)
|
|
|
|
|
|
|
|
|
|
|
|
| | 协作程序 | 检查程序 |
|
|
|
|
|
|
|---|---|---|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
| 是什么 | `collabd.py --supervise`(常驻本体,自带循环) | **循环内建的检查会话排期**(`maybe_spawn_check_agent()`) |
|
|
|
|
|
|
| 判活 | 心跳新不新 + `argv0` 指不指本区 | 日志里**有没有 `检查会话:…` 分支** |
|
|
|
|
|
|
| 载体 | `collabd-keepalive-<ws>` 计划任务 | **同一个进程**(⛔ 无独立任务) |
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
⇒ 🔴 **检查程序跟着协作程序走** ⇒ 协作程序"活着"**不代表**检查程序在工作。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
### 二、症状:协作程序活得好好的,检查程序**悄悄地不干活了**
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
- **协作侧全正常**:两区心跳新鲜、`argv0` 各指本区、进程恒 3 个。
|
|
|
|
|
|
- **检查侧(ai1net)**:`_collabd.log` **每轮刷一行** `all_sessions_idle 读库失败 no such table: sessions`(累计 **36 次**);最后一次成功建排期**停在 11:54**,此后一次都没再建。
|
|
|
|
|
|
- **对照组(vibe)正常**:读库失败 **0 次**,一路在跑 `检查会话:目标状态=已完成 ⇒ 不建`。
|
|
|
|
|
|
- 🔴 **为什么"静默"**:`_all_sessions_idle()` 的 fail-safe 是**读库失败 ⇒ 判「有会话在跑」⇒ 不建**(`return False`)。失败方向**恰好是"什么都不做"** ⇒ 表现成"机制很安静",**⛔ 不报错、不告警**。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
### 三、根因:常驻任务**直起 `collabd.py --supervise`**,任务环境**没有 `CODEBUDDY_CONFIG_DIR`**
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
- `_wb_db()` 逐字:`cfg = os.environ.get("CODEBUDDY_CONFIG_DIR") or ""` ⇒ 没设 ⇒ 落到 `C://Users//Administrator//.workbuddy//workbuddy.db`。
|
|
|
|
|
|
- 🔴 **那个文件是 0 字节空库**(无任何表)⇒ 查询 `sessions` ⇒ **`no such table: sessions`**。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
| 库 | 大小 | `sessions` 表 |
|
|
|
|
|
|
|---|---|---|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
| `E://ProgramData//.workbuddy//workbuddy.db`(**活动库**) | 34.9 MB | ✅ 247 行 |
|
|
|
|
|
|
| `C://Users//Administrator//.workbuddy//workbuddy.db`(旧库) | **0 字节** | ❌ 无 |
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
- 🔴 **为什么只有 ai1net 炸**:vibe 的 config 里 `host_db` **写死绝对路径**;ai1net 的 `host_db` **一直是空字符串**。⇒ **同一份代码,两区行为不同,差别只在 config 这一格**。
|
|
|
|
|
|
- 🔴 **"计划任务的『动作』里没有 env 字段"**(P0-72 §二 同一条硬约束)⇒ `CODEBUDDY_CONFIG_DIR` **只能由启动器在进程内设**。
|
|
|
|
|
|
- 🔴 **怎么弄丢的(诚实记录)**:旧看守 `start-supervise.ps1` 本有 `$env:CODEBUDDY_CONFIG_DIR = ...`;P0-72 改成"两层形态"时**只保了 `COLLABD_CONFIG`,丢了这一句**。⇒ **重构"起法"时,旧起法里的 env 必须逐条搬**(⛔ 不许凭记忆只搬"我记得的那个")。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
### 四、✅ 修法:新增常驻启动器(与 `board-launch.py` 对称)
|
|
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
- 新增 **`scripts/supervise-launch.py`**:进程内 `os.environ.setdefault("CODEBUDDY_CONFIG_DIR", ...)` + 设 `COLLABD_CONFIG=<本区>/collabd.config.json` + 兜住 `sys.stdout/stderr`(pythonw 下可能是 `None`)+ `runpy.run_path(collabd.py, run_name="__main__")`。
|
|
|
|
|
|
- 常驻任务动作改指 `pythonw.exe "<区>/.workbuddy/collab/supervise-launch.py"`(⛔ **不再自带 `--supervise`**),`-WorkingDirectory` 仍是**工作区根**,`-ExecutionTimeLimit` 恒 `0`。
|
|
|
|
|
|
- `deploy_code.py` 的 `DEFAULT_FILES` **补入该启动器**(否则各区吃不到,P0-57 同族)。
|
|
|
|
|
|
- ⚠️ **`roots.env` 位置缺口(连带发现)**:`_sm_load_roots()` 从 `__file__` **向上**找 `roots.env`;工作区副本在 `<ws>/.workbuddy/collab/collabd.py` ⇒ 工作区里没有该文件 ⇒ **找不到** ⇒ **启动器里写死最稳**,⛔ 别指望 `roots.env` 兜住各区。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
### 五、✅ 验收(2026-10-05 12:15 实测)
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
**① 分界线干净得可当判据**:
|
2026-10-05 14:13:24 +08:00
|
|
|
|
```
|
2026-10-06 22:27:03 +08:00
|
|
|
|
[12:14:49] all_sessions_idle 读库失败 no such table: sessions ← 旧进程最后一次
|
|
|
|
|
|
[12:15:02] supervise loop start pid=58860 ← 新形态进程起来
|
|
|
|
|
|
[12:15:14] all_sessions_idle:还有 1 条 working(a80f300d)⇒ 不算全结束
|
2026-10-05 14:13:24 +08:00
|
|
|
|
```
|
2026-10-06 22:27:03 +08:00
|
|
|
|
⇒ **`pid=58860` 之后 `no such table` 计数 = 0**(`awk` 分界**实测**),从 fail-safe 的"恒判有会话"变成**真实读库判定**。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
**② 那条 `working(a80f300d)` 是真的**(直查活动库):就是**当前这条会话本身** ⇒ 闸②「本区有会话在跑 ⇒ 不建检查会话」是**正确的合法拦截**(⛔ 不是 bug)。
|
|
|
|
|
|
|
|
|
|
|
|
**③ 变异对照证明判据非恒绿**(两档):
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
| 档 | 环境 | 落的库 | 结果 |
|
|
|
|
|
|
|---|---|---|---|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
| A | **无** `CODEBUDDY_CONFIG_DIR` | `C://…`(0 B) | **FAIL no such table: sessions** |
|
|
|
|
|
|
| B | **有**(注入后) | 活动库(34.9 MB) | **OK sessions=247 working=1** |
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
⇒ **A 必 FAIL、B 必 OK** ⇒ 路径来源**真实决定成败**(⛔ 不是"应该绿")。
|
|
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
**④ 运行时**(恒 3 进程、父链断 ⇒ 真常驻):两台**启动器**(各指本区)+ 一个**看板**。
|
|
|
|
|
|
**⑤ 三处 md5 一致**;看板 `curl --noproxy '*' :20099/` ⇒ **HTTP 200**、`netstat` 实测 **LISTENING**。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
### 六、⛔ 教训(一句话)
|
|
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
**"进程活着" ≠ "它在干活"** —— 常驻本体活着,但内部某一环读错了库、被 fail-safe 兜成"什么都不做",**外表完全安静、零报错**。⇒
|
2026-10-05 14:13:24 +08:00
|
|
|
|
1. **检查"程序是否正常",必须看它有没有产出预期的分支**(`检查会话:…`),⛔ 不能只看进程在不在;
|
2026-10-06 22:27:03 +08:00
|
|
|
|
2. **fail-safe 的方向要选对**;凡 fail-safe,**必须同时打一条可检索的日志**(否则"安全"就等于"不可见");
|
|
|
|
|
|
3. **重构"起法"时,旧起法里的 env/inner 参数要逐条搬**(本坑就是丢了一句 `CODEBUDDY_CONFIG_DIR`);
|
2026-10-05 14:13:24 +08:00
|
|
|
|
4. 各区 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 0x4
|
|
|
|
|
|
```
|
|
|
|
|
|
⇒ **`powershell.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`
|
|
|
|
|
|
⇒ **起来就不会安静**。
|
2026-10-07 00:32:47 +08:00
|
|
|
|
- 🔴 **触发链**:钩子 `supervise-ensure-hook.py`(每次 `UserPromptSubmit`)→ `collabd.py --ensure`
|
|
|
|
|
|
→ **就地起失败** → `_escalate_to_keeper()` → 建/启旧任务 → 闪窗。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
实测日志逐字:`常驻自我供给(计划任务):✅ 已建本区计划任务 collabd-supervise-ai1net-dsh-server`。
|
|
|
|
|
|
|
|
|
|
|
|
### 三、✅ 修法(收敛成**同一个形态**,⛔ 不留第二套)
|
|
|
|
|
|
|
|
|
|
|
|
在 `_escalate_to_keeper()` 里:
|
|
|
|
|
|
1. **⛔ 不再铺 `start-supervise.ps1`、⛔ 不再做 ps1 语法校验、⛔ 不再依赖 `start-supervise.ps1.tpl`**
|
|
|
|
|
|
(删掉 `_find_keeper_tpl()` 前置门槛 —— 否则缺模板就直接"没建",与启动器形态无关了)。
|
|
|
|
|
|
2. 任务动作改指 **`pythonw.exe` + `<区>/.workbuddy/collab/supervise-launch.py`**
|
|
|
|
|
|
(`-WorkingDirectory` =**工作区根**;pythonw 是 **GUI 子系统 ⇒ 零控制台 ⇒ 不闪窗**)。
|
2026-10-06 22:39:51 +08:00
|
|
|
|
3. ~~**任务名保持** `collabd-supervise-<区>`(⛔ 不改名 ⇒ `collabctl` 的名单不用动)~~。
|
|
|
|
|
|
🔴 **2026-10-06 订正 —— 这条结论是错的,已改**:
|
|
|
|
|
|
当时以为"不改名就不用动 `collabctl` 的名单",但**正好反过来** ——
|
|
|
|
|
|
`collabctl.py` 的 `SCHED_TASKS` 里列的 `collabd-supervise-<区>` 是**"旧形态 ⇒ 一律禁用"**的名单,
|
|
|
|
|
|
⇒ 保留旧名的后果是:**`_escalate_to_keeper()` 自愈建起来的常驻,会被下一次 `collabctl off` 当旧形态干掉**。
|
|
|
|
|
|
✅ 现行口径:**统一用 `collabd-keepalive-<区>`**(与 `KEEPALIVE_TASKS` 同名)——
|
|
|
|
|
|
这样 `off` 认得它、`on` 会重建它,**开关才真正管得住这条自愈路径**。
|
|
|
|
|
|
⚠️ **教训**:判"改不改名"时,⛔ 不能只看"改名的成本",要**查这个名字在别处被怎么用**
|
|
|
|
|
|
—— 这里它同时是"检查名单"和"禁用名单"的键。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
4. `_escalate_to_keeper` 里**被删段**用 `~~删除线~~` 在 docstring 里标注(⛔ 别留下与实现不符的说明)。
|
|
|
|
|
|
|
2026-10-06 22:39:51 +08:00
|
|
|
|
### 三·补、🔴 2026-10-06 二次收口(第一次只改了动作,没改名字、也没扫旁路)
|
|
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
**三个漏网**(本轮实测抓到;探针当时只扫 `collabd.py` ⇒ 全没报警):
|
2026-10-06 22:39:51 +08:00
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
1、**任务名两套并存**(见上条第 3 项订正)⇒ 已统一 `collabd-keepalive-<区>`,
|
|
|
|
|
|
触发形态同步对齐(`-AtLogOn` → **每 5 分钟重复触发**;去掉 `RestartCount 999/1min`)。
|
|
|
|
|
|
2、**`init_workspace.py` 是第二个建任务者** —— 自带 `_register_keeper_task()`/`_keeper_task_state()`/
|
|
|
|
|
|
`_ps_check()`(验 ps1 语法),还铺 `assets/start-supervise.ps1.tpl`。**整份删掉**
|
|
|
|
|
|
(含 `--with-keeper`/`--no-task` 开关),改为只检查 `supervise-launch.py` 在位。
|
|
|
|
|
|
⛔ **建任务只许有一处实现** = `collabctl.py`(受总电闸 `supervise.switch` 管辖);
|
|
|
|
|
|
自己建就绕过开关,就是"第二条起法"。
|
|
|
|
|
|
3、**守形态的探针打偏** —— `t_keeper_legacy_ps1_form_gone` 原来只读 `collabd.py`。
|
|
|
|
|
|
✅ 已扩成扫**全族**(`collabd.py`/`init_workspace.py`/`collabctl.py`),并新增两条判据:
|
|
|
|
|
|
「**任务名口径唯一**」「旧形态死代码已清」。
|
2026-10-06 22:39:51 +08:00
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
⚠️ **探针自己的坑**:新判据"任务名唯一"**扫文本** ⇒ 第一版**误红**,命中了 `collabctl.py` 里
|
|
|
|
|
|
`SCHED_TASKS = [...]`(那是**故意**列旧名去禁用的)⇒ 必修 `re.sub` 剥掉该块再扫。
|
|
|
|
|
|
**本项目第三次踩"判据扫到自己的否定句"**(前两次:查 PRD 视觉词命中免责声明、查六份旧产出命中禁令文本)。
|
|
|
|
|
|
⚠️ **另一个操作坑**:脚本读写这两个 `.py` 后行尾由**纯 LF 变纯 CRLF**(同族其余 7 个都是 LF)⇒ 已还原。
|
|
|
|
|
|
**凡用脚本改技能源码,改完必核行尾**(否则整个文件显示为"全改",审不出真改动)。
|
2026-10-06 22:39:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-10-05 14:13:24 +08:00
|
|
|
|
### 四、✅ 验收(2026-10-05 12:42 实测)
|
|
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
- 回读任务动作:`EXE=…\pythonw.exe`/`ARG=…/.workbuddy/collab/supervise-launch.py`/`WD=<工作区根>`
|
|
|
|
|
|
⇒ **不再是 `powershell.exe`**(`grep "New-ScheduledTaskAction -Execute 'powershell.exe'"` ⇒ **0 命中**)。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
- **零 PowerShell 看守残留**:`Get-CimInstance` 筛 `powershell.exe` 且命令行含 `start-superv` ⇒ **空**。
|
2026-10-07 00:32:47 +08:00
|
|
|
|
- **三常驻全部 `ppid=3924`(调度器)且都是启动器形态**:`48372` ai1net/`61956` vibe/`63684` 看板
|
2026-10-05 14:13:24 +08:00
|
|
|
|
⇒ 父链断在调度器 ⟹ **真常驻**;形态统一 ⟹ **不会再长出第二套起法**。
|
|
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
### 五、⛔ 教训
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
2026-10-07 00:32:47 +08:00
|
|
|
|
**"改了主路径" ≠ "把旁路也改了"** —— 漏掉的那条**自我供给旁路**平时不吭声,专挑你在用时闪一下。
|
|
|
|
|
|
⇒ **凡"同一种东西有第二条起法",收口时必须全文搜一遍**(判据=`grep New-ScheduledTaskAction` 与 `\.ps1`)。
|
2026-10-05 14:13:24 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-75 🔴🔴 **远端技能总仓里躺着明文令牌(`.neodata_token`)**(★ 2026-10-05 收尾核验时发现)
|
|
|
|
|
|
|
|
|
|
|
|
### 一、症状
|
|
|
|
|
|
|
|
|
|
|
|
核验 `git@work.alotbuy.com: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` 是**同一类东西**,但一个是"部署契约"、一个是"密钥")。
|
|
|
|
|
|
|
|
|
|
|
|
### 三、修(最小改动 · ⛔ 未改写历史)
|
|
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
|
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-mechanism` **57 文件**(⛔ 没被波及)。
|
|
|
|
|
|
- `.gitignore` 尾部新规则就位。
|
|
|
|
|
|
|
|
|
|
|
|
### 五、⚠️ 残留风险(必须如实告知用户)
|
|
|
|
|
|
|
|
|
|
|
|
**历史里仍有那个 blob**(`55941e52`)—— `git log --all -- .neodata_token` 还能查到,
|
|
|
|
|
|
`git show 43b83b0:.neodata_token` 仍能打印明文。
|
|
|
|
|
|
⇒ 彻底清除 = **`push --force` 重写全部历史**(会改写 25 个技能的全部提交哈希,牵连所有 clone)
|
|
|
|
|
|
⇒ **⛔ 不当场做,必须用户拍板**。
|
|
|
|
|
|
(更根本的处置是**在服务端吊销/轮换该令牌** —— 重写历史管不住已经 clone 走的副本。)
|
|
|
|
|
|
|
|
|
|
|
|
### 六、⛔ 教训(可复用判据)
|
|
|
|
|
|
|
|
|
|
|
|
1. 🔴 **技能仓入库前必扫两样**:① 名字含 `token|secret|credential|password|\.env` 的文件;
|
|
|
|
|
|
② **已知令牌串**出现在哪些文件(`grep -rl "<串>" <目录>`)。
|
|
|
|
|
|
2. 🔴 `.gitignore` 必须显式挡**本机令牌落点**(`.neodata_token` 这类),
|
|
|
|
|
|
⛔ 别只挡"日志/缓存/备份" —— 产物与凭据是**两回事**。
|
|
|
|
|
|
3. 🔴 **核验远端别只比"技能在不在"**,要比**差异**(现远端 vs 备份,`comm -23`/`-13`)。
|
|
|
|
|
|
本次就是靠差异比对才发现;只看"25 个技能都在"会**直接漏掉**。
|
|
|
|
|
|
4. ⚠️ 首次入库用 `git add -A` 是**高危动作**(整目录收录)⇒ 收录前先跑一遍第 1 条的两扫。
|
|
|
|
|
|
|
2026-10-06 22:27:03 +08:00
|
|
|
|
## P0-76 🔴🔴🔴 **技能加载闸门的词表漏了「主触发句」⇒ 用户说了「使用任务会话完成目标」,钩子全程 0 命中 ⇒ AI 靠记忆干活 ⇒ 把已授权的事反复要签字**(★ 2026-10-05 实测取证 · 用户连问三次「你去执行不行啊」)
|
|
|
|
|
|
|
|
|
|
|
|
**症状**(用户视角):用户明明说了「**2、使用任务会话完成目标:XXX**」,AI 却回头问「要我现在派任务会话,还是先自己执行?」。
|
|
|
|
|
|
用户连问三次:「任务会话的目的不就是建立任务会话执行嘛,不然我调用任务会话技能干什么」「我想知道你哪来的这个问题啊,你去执行不行啊」
|
|
|
|
|
|
「我想知道你哪来的这么多问题啊,你去执行不行啊,**会话技能里面没有告诉你自决策的规则和机制嘛**」。
|
|
|
|
|
|
|
|
|
|
|
|
**取证过程**(会话 `b232218f` · vibe-product 主会话 · 20.7 MB / 5023 行 jsonl):
|
|
|
|
|
|
| 步 | 读数 | 结论 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| ① 钩子日志 | 10-05 该会话 **155 条 entry、6 次 HIT**,且 6 次全在 10-02~10-04 | 10-05 当天 **零命中** |
|
|
|
|
|
|
| ② 会话内 `Skill` 调用 | 全文件 8 次,最近一次在 **L3530**(更早的一轮);**下发目标那一刻(L4959)前后完全没加载** | 靠记忆干活 |
|
|
|
|
|
|
| ③ 独立复现 | 喂 `vibe-product` cwd + 用户原话 ⇒ rc=0、**stdout 空** | 静默放行 |
|
|
|
|
|
|
| ④ 内部判定 | `_in_scope()=True`、`_workdirs()=('*',)` | **⛔ 不是作用域问题** |
|
|
|
|
|
|
| ⑤ 词表逐条比对 | 旧 `TRIGGERS` 22 词**全是「决策方法」族 + 「回复排版」族** | **「使用任务会话完成目标」一个词都匹配不上** |
|
|
|
|
|
|
|
|
|
|
|
|
**根因(一句话)**:**词表只覆盖「点名方法论」,漏了「点名派活」—— 而后者才是 `session-mechanism` 的主入口。**
|
|
|
|
|
|
「使用任务会话」「任务会话完成」「执行会话完成」「继续完成目标」「会话技能」这些**主触发句里一个词都没有**;
|
|
|
|
|
|
用户那句「会话技能里面没有告诉你自决策的规则和机制嘛」是**反问**(含"规则"二字但那是质问,⛔ 不构成点名)。
|
|
|
|
|
|
|
|
|
|
|
|
**为什么后果这么重**:技能没被强制加载 ⇒ **自决策白名单没进上下文** ⇒ AI 拿"机制层旁路信号"(注入通报里那句「本轮没有要求使用协作会话」)当了准,而**用户原话优先级高于它** ⇒ 于是把"怎么执行"当成待拍板项。
|
|
|
|
|
|
|
|
|
|
|
|
**修法(三处,都在技能内)**:
|
|
|
|
|
|
1. `scripts/hooks/skill-load-guard.py`:新增**分组 3 触发词**(会话机制/派活族 17 词)+ 第三条路由 `_LOAD_SESSION`(→ 加载 `session-mechanism`,⛔ 不是 `dsh-decision`)+ 注入文本里写明「用户说『使用任务会话完成目标』本身就是授权,⛔ 不许再问『要不要建任务会话』」。
|
|
|
|
|
|
2. `SKILL.md` 首屏:新增 **「🔴🔴 第 0 步:加载门槛」**(在十条禁令之前)—— 三张「⛔ 禁止的跳过理由」表 +「**用户原话 > 注入通报**」冲突裁决 + 白名单九类指向。
|
|
|
|
|
|
3. 副本同步:`vibe` 副本从落后 28 文件 → 全对齐(仅 `roots.env` / `install.log` 两处刻意差异)。
|
|
|
|
|
|
|
|
|
|
|
|
**验收(全部实测)**:
|
|
|
|
|
|
| 项 | 结果 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 端到端 7 用例(逐字复刻 settings.json 那条命令) | **7/7 通过**(5 应命中+2 反例正确放行) |
|
|
|
|
|
|
| 跨工作区(vibe / mcn-short-video / ai1net-decision-laya) | 三个区**全部 HIT**(钩子挂全局 ⇒ 全机生效) |
|
|
|
|
|
|
| **变异对照**(把分组 3 词表删回旧状态) | **5 个应命中全部报红(2/7)** ⇒ 判定**不是恒绿** |
|
|
|
|
|
|
| 副本一致性 | 源 58 文件 / 副本 59(多一份 `副本使用说明.md`),**仅 `roots.env`+`install.log` 差异** |
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 教训(可迁移)**:
|
|
|
|
|
|
1. **闸门类机制必须做「触发面覆盖审计」** —— 光验证"闸门能响"不够,要验证"**该响的场景是否真的都在词的覆盖面上**"。本坑就是"闸门本身好的,但它听不见你说话"。
|
|
|
|
|
|
2. **钩子日志里全是 `entry` 而没有 `HIT`,就是"听见了但没认出"** —— 这比"完全没被调用"更难发现(前者看起来机制在跑)。
|
|
|
|
|
|
3. **反问句不算点名** —— 用户质问「没告诉你规则吗」时含关键词,但语义是追责不是请求;靠关键词匹配的闸门**必须把这类误判考虑进去**(本次靠"命中才注入"的保守设计避开了,代价是漏掉了真正的点名)。
|
|
|
|
|
|
4. ⛔ **别在钩子旁边再造第二套** —— 本次是**扩已有钩子的覆盖面**,不是新建钩子(用户明示前提:「从机制上兜住"忘了加载"这一主因」)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-77 🔴🔴🔴 **前缀发源地「一对多」:实测 4 类落点,改一处只改一半 ⇒ 半新半旧**(★ 2026-10-05 · 用户连说两遍「**协作 全部改为 执行 不要我在说第二遍**」)
|
|
|
|
|
|
|
|
|
|
|
|
### 一、症状
|
|
|
|
|
|
|
|
|
|
|
|
用户 10-03 已下过令「协作 → 执行」,10-05 发现**新建会话前缀仍叫 `[协作]`** ⇒ 追问「**协作 全部改为 执行 不要我在说第二遍**」。
|
|
|
|
|
|
|
|
|
|
|
|
### 二、根因 A — 发源地是「一对多」,共 4 类落点
|
|
|
|
|
|
|
|
|
|
|
|
| # | 发源地 | 位置 | 漏改的后果 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| ① | `_zh` 字典(**新建前缀唯一出处**) | `collabd.py::_gap_plan()` | 不改 ⇒ 新建的**永远是旧前缀** |
|
|
|
|
|
|
| ② | 提示词模板 **9 处** | `collabd.py` 1417/1419/1421/1869/3262/4145/4160/4189/4206 | 不改 ⇒ **派活的指令文本**还在教 AI 写旧前缀 |
|
|
|
|
|
|
| ③ | 扫描面(`SESS_PREFIXES` + SQL) | `session-rules-check.py` | 不改 ⇒ **新建的 `[执行]` 会话被体检整体漏掉**(**静默失管**) |
|
|
|
|
|
|
| ④ | 识别映射表(`role`/`_PFX`) | `collabd.py` + `board.py` | **⛔ 这个不许改** —— 一删,存量全部解析成 `role=""` ⇒ 看板画不出、派活漏管,**不可逆** |
|
|
|
|
|
|
|
|
|
|
|
|
🔴 **最阴的是 ③** —— 它是**静默**的:体检照跑、照出 pass,只是**新前缀的会话从来没被扫进来**。与 P0-76 同型:**闸门本身是好的,但它看不见新东西**。
|
|
|
|
|
|
|
|
|
|
|
|
### 三、修法(全量,含存量)
|
|
|
|
|
|
|
|
|
|
|
|
① `_zh` → `"执行"`|② 9 处提示词模板 → `[执行]`|③ `SESS_PREFIXES` + SQL 补齐|④ 映射表**只加注释加固、一个键不动**。
|
|
|
|
|
|
|
|
|
|
|
|
- 存量**一并改写**(用户要「全部」):宿主库 `sessions` **55 条** + `automations` **5 条活排期**,事务内 `replace()`,`busy_timeout=30s`,**改后 `integrity_check = ok`**。
|
|
|
|
|
|
- 备份走官方 `backup()` API(⛔ 禁文件级 `cp`)+ `snapshot_titles.json` 逐条留痕。
|
|
|
|
|
|
|
|
|
|
|
|
### 四、顺带修的真缺陷(第三个)
|
|
|
|
|
|
|
|
|
|
|
|
**类别名自带方括号 ⇒ 拼出双层**:`goal.json` 的 `topics` 写成 `["[暗色主题补抓]", …]`(**值里自带方括号**),套进模板 `"[%s]-[%s]-%s"` ⇒ 拼出 `[执行]-[[暗色主题补抓]]-承接队列` ⇒ **形不合规**。
|
|
|
|
|
|
- 修法:`_gap_plan()` 里**只剥一层**首尾方括号(⛔ 不全 strip,免得把 `[a][b]` 也吞了);**归一化只作用于拼装名,不回写 `goal.json`**(那是用户的输入)。
|
|
|
|
|
|
- 实测三种输入 `[暗色主题补抓]`/`暗色主题补抓`/`[a][b]` → 输出全合规。
|
|
|
|
|
|
|
|
|
|
|
|
### 五、验收(全部实测 · 2026-10-05 15:2x)
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 结果 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `_zh` 发源地读数 | `{"follow":"跟进","worker":"执行","waker":"唤醒"}` ✅ |
|
|
|
|
|
|
| `_gap_plan()` 实测产物 | `[执行]-[机制排查与修复]-承接队列` ✅ |
|
|
|
|
|
|
| 识别映射表(**5 样本**,含死前缀) | `[协作]`/`[协作目标]`/`[任务会话]`/`[执行]` → 全 `worker`;`[检查]` → `check` ✅ |
|
|
|
|
|
|
| **变异对照**(`_zh` 改回 `"协作"`) | **FAIL 2 按预期报红** ⇒ 判据**不是恒绿**;还原复验全绿 ✅ |
|
|
|
|
|
|
| 宿主库改写 | `[协作]%` 会话 **55→0**、活排期 **5→0**;`[执行]%` 55→56;**`integrity_check = ok`** ✅ |
|
|
|
|
|
|
| 两处映射表**逐条同款** | `collabd.py::role` ⇄ `board.py::_PFX` **6 键全等** ✅ |
|
|
|
|
|
|
| 扫描面覆盖 | SQL 前缀集合 ⊇ 活类别前缀集合 ✅ |
|
|
|
|
|
|
| 副本同步(4 份) | 源包/vibe 技能副本/ai1net 运行副本/vibe 运行副本 **md5 全同** ✅ |
|
|
|
|
|
|
| 常驻重启(部署≠生效) | ai1net 48372→**41468**、vibe 51308→**39440**;`started_h` **晚于**部署时间 ✅ |
|
|
|
|
|
|
| **自测基线对比** | **改前 PASS 94 / FAIL 3 = 改后 94 / 3**(那 3 条**改前既有**,⛔ 非本次回归) ✅ |
|
|
|
|
|
|
| 踩到的**自造回归** | 把 `协作目标`/`任务会话` 加进活类别表 ⇒ 体检报「类别缺失」(那是**纯兼容死前缀**)⇒ 已拆成 `SESS_PREFIXES`(扫描面)⇄ `ROLES_LIVE`(活类别)**两层** ✅ |
|
|
|
|
|
|
|
|
|
|
|
|
### 六、⛔ 教训(可迁移)
|
|
|
|
|
|
|
|
|
|
|
|
1. **「发源地唯一」是句口号,落地要清单** —— 光"改前缀"就牵出 **4 类落点**(新建/提示词/扫描面/识别表),其中**识别表必须反向保留**。凡"XX 只有一处"的断言,**必须 grep 数一遍**再信。
|
|
|
|
|
|
2. **改名类改动,读的一侧比写的一侧更危险** —— 写入面改错**看得见**(新建名不对),**扫描面改错是静默的**(旧的照扫、新的没进,体检还全绿)。⛔ 改名前先把"谁会读这个前缀"**列全**。
|
|
|
|
|
|
3. **「兼容层」与「活类别」必须分成两张表** —— 混成一张 ⇒ 死前缀被当成"必须有活的" ⇒ **每轮必报假缺口**。判据:**只有"要求它现在活着"的才进活类别表**。
|
|
|
|
|
|
4. **改了代码 ≠ 生效** —— 保活任务判定"已活"就**不会重启**;必须**终止进程 + 触发保活**,以 `started_h 晚于部署时间` 作**唯一判据**(⛔ 副本 md5 对 ≠ 进程换了代码)。
|
|
|
|
|
|
5. **活库改写的安全顺序**:官方 `backup()` 备份 → 导出待改行快照 → `BEGIN IMMEDIATE` + `busy_timeout` → `replace()` → `COMMIT` → `integrity_check`。⛔ **全程不用文件级 `cp`**(WAL 三件套 salt 世代错位 ⇒ `file is not a database`)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-78 🔴🔴🔴 **「一个工作区同时只能一个目标 + 换目标要用户确认」在代码里几乎零实现**(★ 2026-10-05 用户口径 · `goalctl.py` 全文搜 `goals/` 零命中)
|
|
|
|
|
|
|
|
|
|
|
|
### 一、用户口径(逐字,两条)
|
|
|
|
|
|
|
|
|
|
|
|
> **「一个工作区 历史的旧的目标会有很多个,当前要求完成什么目标 就因该关注和检查那个目标」**
|
|
|
|
|
|
> **「一个工作区 同时只能执行一个目标,如果要切换目标,需要用户确认,然后切换和关注检查切换后的目标」**
|
|
|
|
|
|
|
|
|
|
|
|
### 二、改前取证(三条,全部实测)
|
|
|
|
|
|
|
|
|
|
|
|
| # | 项 | 实测读数 | 判定 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| ① | 当前目标载体 | `tmp/supervise-inbox/goal.json`(**单文件**) | ✅ 天然满足"同时只有一个" |
|
|
|
|
|
|
| ② | 历史目标归档 | `goalctl.py`/`collabd.py` **全文搜 `goals/` = 零命中** | 🔴 **归档机制度:零实现** |
|
|
|
|
|
|
| ③ | 换目标确认闸 | `declare` **直接覆盖 `title`**,无任何准入判断 | 🔴 **确认闸:零实现** |
|
|
|
|
|
|
|
|
|
|
|
|
- `vibe-product/tmp/supervise-inbox/goals/机制自检-20261004.json` 那份归档件是**手工造的**(代码从不写这个目录)。
|
|
|
|
|
|
- `ai1net-dsh-server` 区**连 `goals/` 目录都没有**(从未归档过)—— 它换过目标,旧目标只剩 `goal.json` 里一句手写的 `_旧验收作废说明`。
|
|
|
|
|
|
- ⇒ **换目标 = 旧目标当场消失**,靠着人自觉补一行字留痕。
|
|
|
|
|
|
- ⇒ **换目标无需任何人同意** —— 只要有人跑 `declare --title`,目标就换了。
|
|
|
|
|
|
|
|
|
|
|
|
### 三、修法(两道闸,都落在 `goalctl.py::declare()`)
|
|
|
|
|
|
|
|
|
|
|
|
**① 确认闸**:标题与现有不同 ⇒ **默认拒绝**(`rc=3`,**零写入**),必须带 `--switch-goal` 才放行。
|
|
|
|
|
|
- 🔴 **`--switch-goal` ⛔ 不等于 `--yes`**:前者=「**用户已同意换目标**」,后者只是「干跑转真写」。**两者绝不合并** —— 合并就把"确认"降级成"顺手加个 yes",闸门失效。
|
|
|
|
|
|
- ⚠️ **只改措辞/补 `--why`/补 `--kpi`(标题没变)⇒ ⛔ 不触发本闸**(那是同一目标在细化)。
|
|
|
|
|
|
|
|
|
|
|
|
**② 旧目标归档**:确认后把旧 `goal.json` **整份**写到 `goals/<旧标题≤40字>__<YYYYMMDD-HHMM>.json`(文件名净化 Windows 非法字符、重名自动加序号),并在新目标里留 `_前身目标` 指向归档件。
|
|
|
|
|
|
- ⚠️ 归档失败 ⇒ **不阻断换目标**(用户已确认),但**必须喊出来**(⛔ 不许静默)。
|
|
|
|
|
|
|
|
|
|
|
|
### 四、过程中踩到并修掉的第二个真缺陷
|
|
|
|
|
|
|
|
|
|
|
|
**`declare` 没给 `--title` 时回落成工作区名 ⇒ 把"纯细化"误判成"换目标"。**
|
|
|
|
|
|
- 旧写法 `title = _opt("title") or NAME`:只想补 `--why`(标题根本不动)时,`title` 变成 **`NAME`=工作区名** ⇒ 与现有标题不同 ⇒ **触发换目标闸** ⇒ 一个纯细化动作被拦成"未确认换目标"。
|
|
|
|
|
|
- ✅ 正解:**没给 `--title` ⇒ 保持现有标题**(与 `--topics`/`--kpi` 同一规矩:不给=不动);只有**一个目标都没有**(首次声明)时才回落 `NAME`。
|
|
|
|
|
|
- ⛔ **这是"给旧行为加闸"的典型连带伤** —— 加闸前该行为是"静默改标题",加了闸才暴露成"硬拒绝"。**加闸必须连带检查旧行为里被掩盖的缺陷**。
|
|
|
|
|
|
|
|
|
|
|
|
### 五、验收(全部实测 · 2026-10-05 15:3x)
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 结果 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| ① 换目标未确认 | **rc=3、零写入**,并打印用户口径原文与正确命令 ✅ |
|
|
|
|
|
|
| ② 同目标细化(补 `--why`,标题不变) | **rc=0 正常放行**(⛔ 不被拦)✅ |
|
|
|
|
|
|
| ③ 确认后真换(隔离测试区) | 旧目标归档成功(**32 字段 / 6186 B**),新目标 `_前身目标` 指向归档件 ✅ |
|
|
|
|
|
|
| ④ 归档件内容完整性 | 与旧目标**同源**(title/why/acceptance/topics 全在),字段数 32 vs 新 33 ✅ |
|
|
|
|
|
|
| ⑤ **变异对照**(闸短路 `if False and ...`) | **放行 rc=0** ⇒ 判据会失败、**不是恒绿**;还原后 **rc=3** 复验 ✅ |
|
|
|
|
|
|
| ⑥ 真区未被污染 | `title` 未变、`lifecycle` 未变、无 `goals/` 残留 ✅ |
|
|
|
|
|
|
| ⑦ 副本同步 | 源包 + 两工作区运行副本 + vibe 技能副本 **md5 全同** ✅ |
|
|
|
|
|
|
|
|
|
|
|
|
### 六、⛔ 教训(可迁移)
|
|
|
|
|
|
|
|
|
|
|
|
1. **"用户口径"与"代码事实"必须分开核** —— 用户说"一个工作区同时只能一个目标"时,**载体确实满足**(单文件),但**配套的归档与确认两条腿是空的**。⛔ 别因为①对了就以为整条口径都落地了,**要逐条拆开验**。
|
|
|
|
|
|
2. **「文档里写了」≠「代码里做了」** —— `goals/` 目录**真实存在且有文件**,极易让人以为机制在跑;真相是**那份文件是手工造的、代码从不写它**。⇒ **凡"某目录/某文件存在"就当证据 ⇒ 必错**,要**搜写入方**(本次:搜 `goals/` 零命中)。
|
|
|
|
|
|
3. **默认拒绝(fail-closed)是"用户确认"类闸的唯一正确形态** —— 若做成"默认放行 + 提示",等于没闸(本次若默认放行,AI 顺手一跑就换了)。
|
|
|
|
|
|
4. **闸的开关必须专用、⛔ 不与通用开关复用** —— `--switch-goal`(人的授权)与 `--yes`(干跑转真写)**语义正交**,复用即失效。
|
|
|
|
|
|
5. **给旧行为加闸时,务必回归旧行为的所有分支** —— 本次就是加闸后才发现"没给 `--title` 会静默改标题"这个**既存**缺陷被放大成硬拒绝。
|
|
|
|
|
|
|
|
|
|
|
|
## P0-79 🔴🔴🔴 **`declare` 换目标不重置 `lifecycle` ⇒ 检查机制静默空转 ~17 小时**(★ 2026-10-05 · 从 P0-77 拆出,两个主题原本塞在一条里)
|
|
|
|
|
|
|
|
|
|
|
|
### 一、症状
|
|
|
|
|
|
|
|
|
|
|
|
`vibe-product` 目标明摆着没完成(`acceptance_state` N1/N2/N3 全「未过」),却**十几小时不建检查会话**。用户质问「**vibe 目标没完成,检查机制也没运行 到底什么问题怎么解决**」。
|
|
|
|
|
|
|
|
|
|
|
|
### 二、根因 B — `lifecycle` 的唯一写入点是 `--set-life`,`declare` 一个字都不碰
|
|
|
|
|
|
|
|
|
|
|
|
- `vibe-product` 实测时间线:10-04 19:2x 前 `等待` → **22:02 起 `已完成`** → **10-05 14:04 `declare` 了新目标**,但 `lifecycle` **仍是「已完成」**,`lifecycle_at` 停在 10-04(**残留**)。
|
|
|
|
|
|
- 拦它的只有**一行**:`collabd.py:4557` `if reason == "queue-empty" and life != GOAL_LIFE_RUN: return None`。
|
|
|
|
|
|
- 常驻于是**每 60 秒打一条** `检查会话:目标状态=已完成(非进行中)⇒ 不建`,**从 10-04 22:02 空转约 17 小时**(日志 14760+ 行**全是这一句**)。
|
|
|
|
|
|
- 为什么设计成"绝不自动改":`GOAL_LIFE_WAIT="等待"` 的语义是「**用户没点头就不动**」,代码原话「宁可等,不可动」⇒ **⛔ 不许把这条闸拆掉**。
|
|
|
|
|
|
|
|
|
|
|
|
### 三、止血(已做)/机制(未定)
|
|
|
|
|
|
|
|
|
|
|
|
- **已做**:手工把 `lifecycle` 由「已完成」改回「进行中」(`--set-life`,`by=主会话`)⇒ **90 秒内**日志即出现 `已建检查会话排期 … fire=+90s`。
|
|
|
|
|
|
- ⛔ **`declare` 是否该自动重置 `lifecycle` 仍未拍板**(三候选:A 仅终态重置/B 只警告/C 只写文档)—— 触及「机制会不会自作主张」红线,**须用户定**。
|
|
|
|
|
|
|
|
|
|
|
|
### 四、⛔ 教训(可迁移)
|
|
|
|
|
|
|
|
|
|
|
|
1. **目标状态是"人的决定",⛔ 不许机制替它改** —— `lifecycle` 残留会让检查机制**静默空转**(日志每 60s 一条、看着"在跑");但修法**不能是"自动重置"**,那会把「用户没点头就不动」这条红线一起拆掉。**方向:让残留可见(告警/体检项),而不是让它自动消失。**
|
|
|
|
|
|
2. **两个主题别塞进一条** —— 本条原与 P0-77(前缀改造)合写,7.4 KB;拆开后各自 <5 KB。判据:**单条讲一件事,跨主题就该拆**(P0-2 同型先例)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## P0-80 🔴🔴🔴 **机制新建的会话/排期「模型写死」⇒ 与主会话不一致;且已有判据只扫 `recurring` 恰好看不见它**(★ 2026-10-05 用户问「为什么 vibe-product 检查进程创建的 执行和检查会话没有使用主会话相同的模型」)
|
|
|
|
|
|
|
|
|
|
|
|
### 一、症状(用户视角)
|
|
|
|
|
|
|
|
|
|
|
|
vibe-product 常驻新建的 `[执行]`/`[检查]` 会话,模型全是 `space-bunny`;而**主会话是 `deepseek-v4.1-flash`(thought=high)**
|
|
|
|
|
|
⇒ **同一件活在两种模型上跑**,能力档位、思考开关全不一致。
|
|
|
|
|
|
|
|
|
|
|
|
### 二、实测读数(2026-10-05 16:0x · 只读取证)
|
|
|
|
|
|
|
|
|
|
|
|
| 对象 | 模型 | 条数 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| **本区主会话**(`a80f300d`=本条会话) | `deepseek-v4.1-flash`(thought=**high**) | 1 |
|
|
|
|
|
|
| 机制新建的会话(`bg=1` 且 `[执行]`/`[检查]`) | `space-bunny` | **49** |
|
|
|
|
|
|
| 同上(历史,10-02 那批) | `deepseek-v4.1-flash` | 28 |
|
|
|
|
|
|
| 排期表 `automations` | `space-bunny` | **9** |
|
|
|
|
|
|
| 排期表对照:**手工建**的那条 | `deepseek-v4.1-flash` | 1 |
|
|
|
|
|
|
|
|
|
|
|
|
⇒ **10-04 起从 `deepseek…` 整体变成 `space-bunny`** —— 分界线清楚。
|
|
|
|
|
|
|
|
|
|
|
|
### 三、根因(两条,各自独立)
|
|
|
|
|
|
|
|
|
|
|
|
**根因 A — 模型是硬编码字面量,写在唯一一处**
|
|
|
|
|
|
|
|
|
|
|
|
- `collabd.py:3941`:`create_check_schedule()` 的 INSERT 里**字面写死** `"space-bunny"`,同时 `model_is_thinking=0`。
|
|
|
|
|
|
- 🔴 **关键是"两条建会话的路,模型来源不同"**:
|
|
|
|
|
|
· **常驻程序直连 SQLite 建**(`create_check_schedule()`)⇒ 模型 = **代码里写死的那个**
|
|
|
|
|
|
· **会话侧走宿主工具建**(`automation_update`)⇒ 模型 = **宿主默认**(跟主会话一致,实测那条 `deepseek-v4.1-flash` 就是这么来的)
|
|
|
|
|
|
- ⇒ 用户看到的"检查进程创建的会话模型不对",正因它走的是**第一条路**;而 `_gap_plan()` **自己不写库**(只产出参数给会话照抄)⇒ 执行会话若由常驻直建,同样中招。
|
|
|
|
|
|
|
|
|
|
|
|
**根因 B — 已有「模型可用性」判据只扫 `recurring`,而机制新建的全是 `once`**
|
|
|
|
|
|
|
|
|
|
|
|
- `session-rules-check.py` ⑧ 逐字:`_recur = [a for a in mine if a["schedule_type"] == "recurring"]`。
|
|
|
|
|
|
- 机制新建的排期 **`schedule_type` 恒为 `once`**(P0-65)⇒ **一条都不进扫描面** ⇒ 该判据恒判 ok。
|
|
|
|
|
|
- 🔴 与 **P0-76 / P0-77 的 ③ 完全同型**:**闸门本身是好的,但它看不见这类东西**(扫描面漏 ⇒ 静默失效)。
|
|
|
|
|
|
|
|
|
|
|
|
### 四、修法(待落地,方向已定)
|
|
|
|
|
|
|
|
|
|
|
|
- **A**:`create_check_schedule()` 的模型**不许写死** ⇒ 从**本区最近一条人开会话**取 `model` + `thought_level`
|
|
|
|
|
|
(实测取数可行:`sessions WHERE is_background_automation<>1 AND cwd LIKE <本区> ORDER BY created_at DESC`),
|
|
|
|
|
|
取不到时**回落**到一个显式常量并**打日志**(⛔ 不许静默)。
|
|
|
|
|
|
- **B**:判据 ⑧ 的扫描面**从 `recurring` 扩到「本区全部排期」**(含 `once`)。
|
|
|
|
|
|
- ⚠️ **改完必须带变异对照**:把取数短路成常量 ⇒ 判据须报红;还原 ⇒ 复绿。
|
|
|
|
|
|
|
|
|
|
|
|
### 五、⛔ 教训(可迁移)
|
|
|
|
|
|
|
|
|
|
|
|
1. **凡"机制替你决定"的参数,都要先问「该跟谁一致」** —— 模型、思考档、上下文窗口、权限模式这几格,
|
|
|
|
|
|
用户在界面上选的是**一次选择**;机制另建会话时**不继承**就相当于**偷偷替用户换了**。
|
|
|
|
|
|
2. **硬编码的参数,本质是「把一次实测当成了永恒」** —— `space-bunny` 当时能用,但用户后来换成了
|
|
|
|
|
|
`deepseek-v4.1-flash` ⇒ 机制还在按几个月前的选择跑。**判据:凡写死的业务参数,一律问"它会不会变"。**
|
|
|
|
|
|
3. **两条独立代码路径做同一件事,就会有"两条不一致的默认值"** —— 修的时候 **⛔ 别只修一条**
|
|
|
|
|
|
(本次:常驻直连 vs 会话侧工具,两者的模型默认值必须**同源**)。
|
|
|
|
|
|
4. **判据的扫描面要按「这类东西实际长什么样」定,⛔ 不能按"我记得的那一种"定** ——
|
|
|
|
|
|
⑧ 只写 `recurring` 是因为"当初只想到周期钟";机制后来改成 `once` 了,扫描面**没跟着改** ⇒
|
|
|
|
|
|
**同一类事故第 N 次复发**(P0-76 ①、P0-77 ③、本条 ⑧ 是同族)。
|
|
|
|
|
|
|
|
|
|
|
|
## P0-81 🔴🔴🔴 **判据拿「被测的同一个常量」当下界 ⇒ 拆掉被测对象竟然全绿**(★ 2026-10-05 实测,变异对照当场抓到)
|
|
|
|
|
|
|
|
|
|
|
|
**场景**:用户定案「目标文件夹收进 `<工作区根>/执行会话/目标-xxx-xxxxxx/`」⇒ 新增常量 `_GOAL_DIR_PARENT = "执行会话"`
|
|
|
|
|
|
+ 新增自检判据「目录名在 `执行会话/` 一层下」。
|
|
|
|
|
|
|
|
|
|
|
|
**第一版判据(❌ 恒绿)**:
|
|
|
|
|
|
```python
|
|
|
|
|
|
("🔴 目标文件夹在 `执行会话/` 一层下", d1.startswith(m._GOAL_DIR_PARENT + "/"))
|
|
|
|
|
|
```
|
|
|
|
|
|
**变异对照**(把 `_GOAL_DIR_PARENT` 改成 `""` 模拟"父层被拆掉")⇒ 结果 **PASS 95 / FAIL 2**,
|
|
|
|
|
|
与**正确实现逐字相同** —— **拆掉被测对象竟然全绿**。
|
|
|
|
|
|
|
|
|
|
|
|
**根因**:判据的**下界**取自**被测的同一个常量** ⇒ `_GOAL_DIR_PARENT=""` 时判据变成
|
|
|
|
|
|
`startswith("/")` ⇒ 而返回值是 `"/目标-甲-8b984a"`(父层空 ⇒ 多一个前导斜杠)⇒ **照样为真**。
|
|
|
|
|
|
⇒ **判据与实现同源 ⇒ 一起错 ⇒ 恒绿**(这就是"判据恒绿"最隐蔽的一种:它**不是写死了期望值**,
|
|
|
|
|
|
而是**把期望值挂在了被测对象身上**)。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解**:判据里**写死期望字面**(`"执行会话/"`),⛔ 不引用被测常量:
|
|
|
|
|
|
```python
|
|
|
|
|
|
("🔴 目标文件夹在 `执行会话/` 一层下", d1.startswith("执行会话/") and d1.split("/")[-1].startswith("目标-"))
|
|
|
|
|
|
```
|
|
|
|
|
|
修完再跑同一变异体 ⇒ **FAIL 3**(三条新判据全红)⇒ 判据真抓得住。
|
|
|
|
|
|
|
|
|
|
|
|
**另两条同批教训**:
|
|
|
|
|
|
1. **判据的假设会过期**:老判据 `not re.search(r"[\\/:*?\"<>|]", clean)` 假设"目录名里没有 `/`",
|
|
|
|
|
|
加了父层后 `clean` **必然含一个 `/`** ⇒ **误报**。✅ 正解=只清洗/只判**末级目录名**:
|
|
|
|
|
|
`clean.split("/")[-1]`。⇒ 同族第 2 次(**判据自身的假设过期 ⇒ 伪装成产品缺陷**)。
|
|
|
|
|
|
2. **落点判据要同时认新旧形态**:`parts[0].startswith("目标-")` → 补
|
|
|
|
|
|
`parts[0]=="执行会话" and parts[1].startswith("目标-")`。⛔ **不许硬切新形态** ——
|
|
|
|
|
|
那会把存量旧目录里的产物全判成"散在别处"(**误报**,不是事实)。实测两态并存时
|
|
|
|
|
|
`130 在 / 22 散`,与改前**逐字相同** ⇒ 证零新增散落。
|
|
|
|
|
|
|
|
|
|
|
|
📌 **纪律**:凡判据里出现 `m.<常量>` 当作**期望值**,先问一句「**把它改坏,判据会红吗**」;
|
|
|
|
|
|
答不上来 ⇒ 立刻做变异对照。**"跑一次是绿的"不构成验收**(恒绿的那次也绿)。
|
|
|
|
|
|
|
|
|
|
|
|
## P0-82 🔴🔴🔴 **自检夹具「静默写到真工作区」+「判据剔注释却漏了 docstring」+「过期判据被当缺陷」**(★ 2026-10-05 · 同一天连栽三次,`goal.json` 被真实改写两次)
|
|
|
|
|
|
|
|
|
|
|
|
**场景**:给 `goalctl.py` 加「换目标自动复位 `lifecycle`」,配一条**行为级**自检用例
|
|
|
|
|
|
(真跑 `declare --switch-goal --yes`,读回落盘的 `lifecycle`)。
|
|
|
|
|
|
|
|
|
|
|
|
### 坑①:夹具隔离失效 ⇒ **静默改写真工作区**
|
|
|
|
|
|
|
|
|
|
|
|
**现象**:用例 `rc=0`,但读回 `title` **还是旧值**、`lifecycle` **也没变**
|
|
|
|
|
|
—— 看着像"功能没生效"。**真相是它写到别处去了**:真工作区
|
|
|
|
|
|
`<ws>/tmp/supervise-inbox/goal.json` 的 `title` 被改成了测试值「新目标乙」,
|
|
|
|
|
|
并生成了一条归档件。**跑一次自检 = 改一次真目标**。
|
|
|
|
|
|
|
|
|
|
|
|
**根因(两层,缺一不可)**:
|
|
|
|
|
|
1. `goalctl.py:_resolve_ws()` 的候选顺序是
|
|
|
|
|
|
**`DSH_COLLAB_WS` → `DSH_WS_ROOT` → cwd(含 `.workbuddy/collab/`)**,
|
|
|
|
|
|
⛔ **完全不看 `COLLABD_CONFIG`,⛔ 也不认 `COLLABD_WORKSPACE`**。
|
|
|
|
|
|
而夹具第一版只设了后两个 ⇒ `_resolve_ws()` 两个都不认、cwd 又没有 `.workbuddy/collab/`
|
|
|
|
|
|
⇒ **回落到技能包上级目录**。⚠️ 那里若叠着真工作区 ⇒ **静默写真**。
|
|
|
|
|
|
2. `goalctl.py` 的 `INBOX` 是 **硬编码** `WS/"tmp"/"supervise-inbox"`
|
|
|
|
|
|
—— ⛔ **不读配置里的 `inbox` 字段**(实测 `{"inbox":"inbox"}` 被无视)。
|
|
|
|
|
|
夹具第一版把 `goal.json` 写在 `<tmp>/inbox/` ⇒ 脚本读到「(无)」、写到 `<tmp>/tmp/supervise-inbox/`
|
|
|
|
|
|
⇒ **读回当然还是旧值**(伪装成"功能没生效")。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解(三层,缺一不可)**:
|
|
|
|
|
|
```python
|
|
|
|
|
|
# ① 落点跟着脚本走(⛔ 别照配置猜)
|
|
|
|
|
|
inbox = _pl.Path(tmp) / "tmp" / "supervise-inbox"
|
|
|
|
|
|
# ② 变量必须是 _resolve_ws() 的第一顺位
|
|
|
|
|
|
env = {**os.environ, "COLLABD_CONFIG": str(cfg), "DSH_COLLAB_WS": tmp.replace("\\", "/")}
|
|
|
|
|
|
# ③ **兜底断言**:从子进程 stdout 抠出它自报的落点,核对前缀;不在 tmp 内 ⇒ 直接记红
|
|
|
|
|
|
```
|
|
|
|
|
|
**③ 是关键**:⛔ 别靠"我记得传对变量"(**人的记性不是判据**)。
|
|
|
|
|
|
子进程自报 + 前缀核对 ⇒ 把「隔离失效」从**静默事故**变成**当场报红**。
|
|
|
|
|
|
另加一道**外部护栏**:跑写动作前后量**真文件 md5**,必须逐字不动(这次实测
|
|
|
|
|
|
`30ec31d7…` 前后一致 ⇒ 通过)。
|
|
|
|
|
|
|
|
|
|
|
|
### 坑②:判据「已剔除注释」却**漏了 docstring**
|
|
|
|
|
|
|
|
|
|
|
|
**现象**:判「旧形态已清除」用 `"字面" not in src` ⇒ **误红**。第一版修法是「剔掉 `#` 注释行」,
|
|
|
|
|
|
换了另一条判据又误红 —— 因为**该字面在 docstring(三引号)里**。
|
|
|
|
|
|
|
|
|
|
|
|
📌 **纪律**:**「剔注释」≠「剔 docstring」,这是两件事**。
|
|
|
|
|
|
本仓库的考古纪律要求废弃形态的说明**必须写在注释/docstring 里**
|
|
|
|
|
|
⇒ 那些字面**注定存在** ⇒ 拿全文判「已清除」**必然误红**。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解**:用 `tokenize` + `ast` **精确剔两样**:
|
|
|
|
|
|
用 `ast` 抓 docstring 的起止行 → `tokenize` 逐 token,只剔 `COMMENT`
|
|
|
|
|
|
和「**位于 docstring 的** `STRING`」。
|
|
|
|
|
|
|
|
|
|
|
|
⛔ **别把 `STRING` 全剔** —— 第一版这么干过,结果把「写在命令**字符串里**的字面」
|
|
|
|
|
|
(任务动作名、启动器文件名)也剔了 ⇒ **由误红变另一种误红**。
|
|
|
|
|
|
⇒ "代码干了什么"经常就写在字符串里,剔掉等于**看不见真实现**。
|
|
|
|
|
|
|
|
|
|
|
|
### 坑③:**过期判据被当成产品缺陷**
|
|
|
|
|
|
|
|
|
|
|
|
**现象**:全量自检 `FAIL 2`,都指向 `collabd.py` 里 `_escalate_to_keeper()` / `_own_path()`。
|
|
|
|
|
|
看着像真 bug,实际是**判据判的形态已经整套废弃**(10-05 已把「旧看守形态」换成
|
|
|
|
|
|
`pythonw.exe` + 启动器)⇒ 连**被测对象都不存在了** ⇒ 恒红、且**无法修复**。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解(两条动作,都要做)**:
|
|
|
|
|
|
1. **删掉死代码**(`_find_keeper_tpl()` 零调用点)—— ⛔ 留着只会**再招一批过期判据**;
|
|
|
|
|
|
2. **把过期用例换成形态守卫**:判「旧形态已彻底清除 + 不许回潮」
|
|
|
|
|
|
(壳脚本动作不许再现、动作必须是 `pythonw.exe` + 启动器、`CREATE_NO_WINDOW` 在位)。
|
|
|
|
|
|
变异对照:把动作改回壳脚本 ⇒ **红**;还原 ⇒ **绿**。
|
|
|
|
|
|
|
|
|
|
|
|
📌 **纪律**:判据红了,**先问「被测对象还在不在」**。
|
|
|
|
|
|
不在了 ⇒ 是判据过期,⛔ 不是"改断言把它弄绿"(那是把判据废掉),而是
|
|
|
|
|
|
**删死代码 + 换一条守新形态的判据**。
|
|
|
|
|
|
|
|
|
|
|
|
### 坑④(并列):**夹具隔离失效时的备份不可信**
|
|
|
|
|
|
|
|
|
|
|
|
`bak-goalctl-*` 是**覆盖前一刻**的内容 —— 若前一步**已经写坏**,备份**也坏**
|
|
|
|
|
|
(实测本轮备份与坏件**逐字相同**,不可作恢复源)。
|
|
|
|
|
|
✅ **可用的恢复源 = 被污染的归档件本身**(`goals/<旧标题>__<时间>.json` 里存着**改前的完整 34 键**)
|
|
|
|
|
|
⇒ 逐字还原、复核 `title`/`lifecycle` 四件套/`acc keys`/顶层键数全部复原。
|
|
|
|
|
|
|
|
|
|
|
|
📌 **纪律**:**验收前必须 md5 目标真文件**("跑完自检没报错"≠"真文件没被动")。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-83 🔴🔴🔴 **「兼容」二字是大杂烩的孵化器 —— 凡旧物必答"删了会坏在哪"**(★ 2026-10-05 · 用户当场点破)
|
|
|
|
|
|
|
|
|
|
|
|
**用户原话(逐字)**:「**兼容个毛线,今天兼容一个明天兼容一个 过不了一周就成大杂烩了**」。
|
|
|
|
|
|
|
|
|
|
|
|
**触发场景**:报障「vibe 区怎么还有一条 `[协作]` 前缀的会话,是不是技能没更新完全」。
|
|
|
|
|
|
AI 第一版给的方案是「把 `[协作]` 从**活角色**降为**兼容扫描**」—— **被用户当场否掉**。
|
|
|
|
|
|
⚠️ 要害不是"降级"这个动作错了,而是**用了「兼容」这个词**:它把一件有硬理由的事
|
|
|
|
|
|
(**删了就坏**)说成了一件可以拖的事(**先兼容着**)⇒ 后人读到就继续拖。
|
|
|
|
|
|
|
|
|
|
|
|
### 判据:一句话分清两种"留"
|
|
|
|
|
|
|
|
|
|
|
|
面对任何旧前缀/旧字段/旧值,**必须能一句话答出它属于哪一种**:
|
|
|
|
|
|
|
|
|
|
|
|
| 类型 | 判据(一句话能答出) | 处置 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| **① 删了会坏** | 「删它 ⇒ **谁变成什么**」(点名受害对象 + 后果 + 可逆性) | **必须留**,但注释里⛔ **不许写「兼容」**,要写**受害事实** |
|
|
|
|
|
|
| **② 留着没用** | 答不出受害对象,或只能说"万一以后要用" | **当场删**(不给"兼容期") |
|
|
|
|
|
|
|
|
|
|
|
|
🔴 **"答不出 ⇒ 就是该删的"** —— 这是本条的可操作形式。
|
|
|
|
|
|
⚠️ **别把 ① 也写成"兼容"**:那是**把有据的保留伪装成没据的拖延**,
|
|
|
|
|
|
下一个读代码的人会照拖,于是**真的变成大杂烩**。
|
|
|
|
|
|
|
|
|
|
|
|
### 本仓实测(把「删了会坏在哪」答出来)
|
|
|
|
|
|
|
|
|
|
|
|
`board.py::_PFX` 里 `协作`/`协作目标`/`任务会话` 三条 —— 2026-10-05 **当场跑函数验的**(⛔ 非推理):
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
删任一条 ⇒
|
|
|
|
|
|
[协作]-[手机接入]-N9 复测 ⇒ role = ''
|
|
|
|
|
|
[协作]N9 派活 · V2 手机发出的消息 ⇒ role = ''
|
|
|
|
|
|
[协作目标]-xxx ⇒ role = ''
|
|
|
|
|
|
[任务会话]-会话机制取证… ⇒ role = ''
|
|
|
|
|
|
⇒ 看板画不出、派活漏管,且不可逆
|
|
|
|
|
|
```
|
|
|
|
|
|
⇒ 这就是"删了会坏在哪"的答案 ⇒ **留,但注释必须写这个,不写"兼容"**。
|
|
|
|
|
|
|
|
|
|
|
|
### ✅ 正解(三件一起做,缺一仍是拖)
|
|
|
|
|
|
|
|
|
|
|
|
1. **活类别表只留在用前缀**:`session-rules-check.py::ROLES_LIVE` **只剩 `[执行]`**。
|
|
|
|
|
|
旧前缀从"活"里**彻底踢出**,只保留在**扫描面** `SESS_PREFIXES`(捞历史行用)。
|
|
|
|
|
|
⚠️ 踢出的收益是**实打实的**:踢之前 ⑪ 判据每轮把 `唤醒、协作、跟进` 三个报成"类别缺失"
|
|
|
|
|
|
(**假红**),踢之后只剩 `执行` 一项,指标准确了。
|
|
|
|
|
|
2. **加机制闸门,防"明天又长一条"**:`session-rules-check.py` **第 ⑬ 项自检**
|
|
|
|
|
|
—— 扫**活排期名**+**活会话标题**,出现旧前缀 ⇒ **当场 fail**。
|
|
|
|
|
|
🔴 判据**只扫活件**(⛔ 不扫注释/docstring/考古段/软删的历史行)——
|
|
|
|
|
|
后者是"记录过去发生了什么",扫它们就是**误红**。
|
|
|
|
|
|
**变异对照(已跑)**:全 `[执行]`⇒ok|混 1 条 `[协作]`⇒fail|混 1 条 `[任务会话]`⇒fail|
|
|
|
|
|
|
历史行但**不以旧前缀开头**(`接续:…`)⇒ok ⇒ **非恒绿非恒红**。
|
|
|
|
|
|
3. **存量活件改名,⛔ 不删行**:改 `automations.name` / `sessions.title` 的**前缀**
|
|
|
|
|
|
(`[协作]`→`[执行]`),**先备份原样(3 行)**,改后 `PRAGMA integrity_check` = `ok`。
|
|
|
|
|
|
⚠️ 改名前**先查 `deleted_at`** —— 别把**软删的死单**也算进"存量"(实测 59 条 `[协作]` 里
|
|
|
|
|
|
**58 条早已软删**,真正的活件只有 1 条;把软删的算进来 ⇒ 报出 59 条的假规模)。
|
|
|
|
|
|
|
|
|
|
|
|
### 坑(两个,都踩到了)
|
|
|
|
|
|
|
|
|
|
|
|
- **坑①:`sqlite3` 的 `LIKE` 转义写法失效** ⇒ **假"0 条"**。
|
|
|
|
|
|
`WHERE title LIKE '[[]协作]%'` 实测**恒返回 0**(同一时刻 `GLOB '[[]协作]*'` 与
|
|
|
|
|
|
`instr(title,'[协作]')>0` 都返回 1)。
|
|
|
|
|
|
✅ **不要用 `LIKE` 转义方括号** —— 改 `instr(title, '[协作]') > 0`(最稳)或 `GLOB`。
|
|
|
|
|
|
📌 **纪律**:**"查到 0 条"≠"没有"** —— 换个写法复验再下结论(本次差点据此报"库里已清干净")。
|
|
|
|
|
|
- **坑②:把"软删的历史行"当成"当前存量"报给用户** ⇒ 规模虚高 59 倍。
|
|
|
|
|
|
✅ 数存量**必须带 `deleted_at IS NULL`**;报"活排期/活会话"与"历史痕迹"要**分开说**。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-84 · 回收站「每半天十几个 G」—— 两个叠加根因(宿主 safe-delete shim × Windows `truncate` 不稀疏)
|
|
|
|
|
|
|
|
|
|
|
|
**用户原话(2026-10-05)**:
|
|
|
|
|
|
> 你再看一看,现在每半天就得创建十几个 G 的文件在回收站里。有必要创建这么多吗?看看是不是都是这个绘画创建的。
|
|
|
|
|
|
|
|
|
|
|
|
### 现象
|
|
|
|
|
|
|
|
|
|
|
|
E 盘回收站 **15.36 GB / 32,557 文件**,其中:
|
|
|
|
|
|
|
|
|
|
|
|
| 实占 | 占比 | 路径 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| **12.02 GB** | **77.7%** | `ai1net-dsh-server\tmp\selftest`(6,966 件) |
|
|
|
|
|
|
| **1.79 GB** | **11.6%** | `vibe-product\tmp\selftest`(1,185 件) |
|
|
|
|
|
|
| 0.35 GB | 2.2% | `ai1net\.workbuddy\collab` |
|
|
|
|
|
|
|
|
|
|
|
|
⇒ **两工作区 `tmp\selftest` 合计 13.81 GB = 89.3%**。删除时刻 **10-04 11:08 → 10-05 10:06 仅 23 小时**。
|
|
|
|
|
|
|
|
|
|
|
|
### 根因一:宿主 safe-delete shim 把工作区 `tmp/` 的删除**全送回收站**
|
|
|
|
|
|
|
|
|
|
|
|
- 载体:`<WorkBuddy>\resources\app.asar.unpacked\cli\vendor\shim\sitecustomize.py`
|
|
|
|
|
|
- 机制:WorkBuddy 把该目录**前置到 `PYTHONPATH` 上** —— `sitecustomize` 是 Python **保留模块名**,
|
|
|
|
|
|
解释器启动时**自动 import** ⇒ 它 monkey-patch:
|
|
|
|
|
|
`os.remove` / `os.unlink` / `os.rmdir` / `shutil.rmtree` / `pathlib.Path.unlink|rmdir`
|
|
|
|
|
|
→ 全部改道 **`shell32.SHFileOperationW(wFunc=FO_DELETE, fFlags=FOF_ALLOWUNDO|NOCONFIRMATION|NOERRORUI|SILENT)`**。
|
|
|
|
|
|
- **实测坐实(可复跑)**:`shutil.rmtree(<某探针目录>)` ⇒ 回收站 `$I` 条数 **9705 → 9706**。
|
|
|
|
|
|
⚠️ 直觉"`rmtree` 是真删、不过回收站" **在本机是错的**。
|
|
|
|
|
|
- 触发:`CODEBUDDY_SESSION_ID` 非空(=在 WorkBuddy 会话里跑 Python)+ `CODEBUDDY_SAFE_DELETE_ENABLED != "0"`。
|
|
|
|
|
|
- 🔴 **豁免名单只有三类**:`_is_under_os_tmp_dir`(**系统** temp)/ pip site-packages 临时目录 / WorkBuddy 托管 venv。
|
|
|
|
|
|
⇒ **`E:/ProgramData/AIProject/<线>/tmp/` 不在内** ⇒ 工作区里所有 `tmp/` 删除**照单进回收站**。
|
|
|
|
|
|
- ⇒ **这是宿主的"防误删"安全设计,不是 bug**;但**它不认识"本工作区的 tmp/ 是一次性自检残渣"**。
|
|
|
|
|
|
|
|
|
|
|
|
### 根因二:`selftest.py::_fake_cfg()` 的"稀疏文件"假设在 Windows 上**不成立**
|
|
|
|
|
|
|
|
|
|
|
|
原注释(`selftest.py:1123`):
|
|
|
|
|
|
> 用 `truncate` 建**稀疏文件**:秒级拿到 10 MiB 的**体积读数**(`getsize`),**⛔ 不真写 10 MiB**。
|
|
|
|
|
|
|
|
|
|
|
|
🔴 **实测反证**:
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
with open(p, "wb") as f: f.truncate(9_600_000)
|
|
|
|
|
|
# 10 个这样的档:逻辑 91.6 MB ⇒ 实占 91.6 MB (1:1,比值 100%)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
⇒ **NTFS 上 Python 的 `f.truncate()` 不产生稀疏文件**(真稀疏要 `CreateFileW` 带
|
|
|
|
|
|
`FILE_ATTRIBUTE_SPARSE_FILE` + `SetFilePointerEx` + `SetEndOfFile`;实测该法才得 `logical 9.2 MB / real 9.2 MB`… 也不省,须配合 `FSCTL_SET_ZERO_DATA`)。
|
|
|
|
|
|
**结论:注释里的假设是错的**,每个"假 10 MiB 档"都真占 10 MiB。
|
|
|
|
|
|
|
|
|
|
|
|
- 单次 selftest 造:`9.6MB×3 + 10.0MB×3 + 4.2MB×1 ≈ 60 MB` **真占盘**。
|
|
|
|
|
|
- 13.81 GB ÷ 60 MB ≈ **235 次 selftest**(23 小时内!)。
|
|
|
|
|
|
- 触发源:**`install.py --verify` 每次都跑 selftest**(`install.py:376`)。⛔ **无任何自动排期** ——
|
|
|
|
|
|
全是**人工/会话每改一次就跑一遍 `--verify`**。
|
|
|
|
|
|
|
|
|
|
|
|
### 判据(怎么量才不骗自己)
|
|
|
|
|
|
|
|
|
|
|
|
- 🔴 **`du -sh` 在回收站上会虚高**:顶层 `$R` 是**目录**,按"顶层条目"逐条量会漏掉子树 ⇒ 得到**假 2.17 GB**。
|
|
|
|
|
|
✅ **正解**:`$I` 解析原路径 → 定位 `$R` 子树 → **`os.walk` 逐文件** `GetCompressedFileSizeW`。
|
|
|
|
|
|
实测实占 **15.19 GB ≈ 逻辑 15.46 GB**(98%)⇒ **没有稀疏红利**。
|
|
|
|
|
|
- 🔴 **`$I` 解析**(Win10/11):`ver(8B) + size(8B) + FILETIME(8B) + plen(4B)` ⇒ 路径在 **offset 28**、长 `plen*2` 字节、`utf-16-le`。
|
|
|
|
|
|
- 🔴 **⛔ 别在生产工作区裸跑 `selftest.py` 做"量体积"实验**:本轮实测跑 **16m56s / rc=15**,
|
|
|
|
|
|
且部分用例会 `m.WS = str(tb)`(`selftest.py:3683`)**改写工作区指向** ⇒ 真改状态。
|
|
|
|
|
|
|
|
|
|
|
|
### 正解(按优先级)
|
|
|
|
|
|
|
|
|
|
|
|
1. **治本·改造档方式**:`_fake_cfg()` 不必造真 9.5 MiB 文件 —— `_deaf_sids()` 只读 `getsize`,
|
|
|
|
|
|
可 **mock `os.path.getsize`** 或在用例里**注入体积读数** ⇒ 单次从 **60 MB → ~0**。
|
|
|
|
|
|
⚠️ 须保持"判据读的是体积数字"这层语义,⛔ 不许把判据改成恒真。
|
|
|
|
|
|
2. **治本·子进程关掉 safe-delete**:`selftest` 起的子进程 env 加
|
|
|
|
|
|
`CODEBUDDY_SAFE_DELETE_ENABLED=0` ⇒ 真删不进回收站(只对**自检残渣**用,⛔ 别全局关)。
|
|
|
|
|
|
3. **治标**:定期清空回收站(这些全是**可再生的**自检残渣)。⚠️ 由用户在资源管理器里做。
|
|
|
|
|
|
4. 🔴 **别忘了**:给**工作区 `tmp/`** 加豁免 → 除非宿主支持(当前**不支持**路径级豁免),
|
|
|
|
|
|
否则**唯一可控的就是"少造"**(第 1 条)。
|
|
|
|
|
|
### 🔴 已修(2026-10-05 · 当轮落地,附读数)
|
|
|
|
|
|
|
|
|
|
|
|
**正解第 1 条落地了**:`selftest.py::_fake_cfg()` 现在**先标 `FSCTL_SET_SPARSE`、再 `truncate`**
|
|
|
|
|
|
(新增 `_make_sparse()`;⛔ 顺序不能反 —— 先 truncate 再加标志,已分配的簇不会自动归还)。
|
|
|
|
|
|
|
|
|
|
|
|
| | 逻辑 | 实占 | 比 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| 老写法(只 `truncate`) | 9,600,000 | 9,600,000 | **100.00%** |
|
|
|
|
|
|
| 新写法(先标稀疏) | 9,600,000 | 65,536 | **0.68%** |
|
|
|
|
|
|
|
|
|
|
|
|
5 档合计:48,000,000 → 327,680 字节(**省 99.3%**)。
|
|
|
|
|
|
⇒ 单次 `selftest` 的假配置从 ~60 MB 降到 ~0.4 MB;`install.py --verify` 不再往回收站搬几十 MB。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ **⛔ 不追求零占用**:文件系统不支持稀疏时 `_make_sparse()` 返回 False,行为**回落成真写**
|
|
|
|
|
|
(与老实现同),但**会打 stderr 提示** —— ⛔ 不许静默退化(否则又是"看着省了、其实没省")。
|
|
|
|
|
|
⚠️ 正解第 2 条(子进程 `CODEBUDDY_SAFE_DELETE_ENABLED=0`)**仍未接线**,第 1 条已足够按住主量。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## P0-85 🔴🔴🔴 **装机制时「工作区解析」静默错归属 —— `roots.env` 残留顶替了脚下的目录**(★ 2026-10-05 用户报「新工作区加载会话和执行会话技能遇到的问题」)
|
|
|
|
|
|
|
|
|
|
|
|
### 现象
|
|
|
|
|
|
|
|
|
|
|
|
在 `E:/ProgramData/AIProject/agent-product` 里跑 `install.py --dry-run`,
|
|
|
|
|
|
解析出的工作区 **= `ai1net-dsh-server`**(⛔ 不是当前目录),踩技能 S 红线(归属工作区不能错)。
|
|
|
|
|
|
⚠️ **`--dry-run` 全程零告警** —— 如果把 `--dry-run` 换成 `--apply`,就是**静默装错地方**。
|
|
|
|
|
|
|
|
|
|
|
|
### 根因(两层,各自都是"判据写错")
|
|
|
|
|
|
|
|
|
|
|
|
**① 优先级颠倒**(`install.py::detect_workspace` 老顺序):
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
显式 --workspace → 宿主 env → ⚠ roots.env 里上次装的值 → 才轮到 cwd 向上找
|
|
|
|
|
|
↑ 排在这里是错的
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
🔴 **`roots.env` 是「本包全局单例」**(一个包只此一份,`install.py:22` 设计如此),
|
|
|
|
|
|
它记的是**上次装到哪**,⛔ 不是"当前工作区"。把它排在 cwd 之前 ⇒
|
|
|
|
|
|
**只要装过一次,此后在任何目录跑都会继承同一个值**。
|
|
|
|
|
|
|
|
|
|
|
|
**② cwd 判据硬要 `state.py` ⇒ 非 ai1net 线全判死**(`install.py` 老写法):
|
|
|
|
|
|
`(cand/".workbuddy").is_dir() and (cand/"state.py").is_file()`。
|
|
|
|
|
|
`state.py` 只是 **ai1net 那条线**的现状快照脚本 —— `vibe-product` / `agent-product` **本来就没有**
|
|
|
|
|
|
⇒ 老判据在那些工作区里**永远返回 None** ⇒ 于是**恰好**由根因①的残留顶替。两条一起才成灾。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 另有一处**易误认**:`E:/ProgramData/AIProject/.workbuddy` **也存在**
|
|
|
|
|
|
(早期遗留:`automations/` + `memory/2026-08-30.md`)⇒
|
|
|
|
|
|
"只看 `.workbuddy` 存不存在"会把**父目录**认成工作区。
|
|
|
|
|
|
|
|
|
|
|
|
### 判据(怎么验才算数)
|
|
|
|
|
|
|
|
|
|
|
|
- 基线必须**四个真实目录全绿**(缺一个就是假绿):
|
|
|
|
|
|
`ai1net-dsh-server` ✅ / `vibe-product` ✅ / `agent-product` ✅ / `AIProject`(父目录)❌
|
|
|
|
|
|
- 三个变异**必须都能翻**(否则判据恒绿):
|
|
|
|
|
|
① 退化成"有 `.workbuddy` 即算" ⇒ `AIProject` 翻
|
|
|
|
|
|
② 退化成老判据(+`state.py`)⇒ `vibe-product`、`agent-product` 双双翻
|
|
|
|
|
|
③ 优先级退化成"残留 > cwd" ⇒ `vibe-product` 里解析成 `ai1net-dsh-server`(**原病复现**)
|
|
|
|
|
|
- 🔴 **⛔ 别只测"能解析"** —— 本轮第一版基线就写错了期望值(把 `agent-product` 期望成 False),
|
|
|
|
|
|
结果**基线自己先红**;若当时偷懒"看到有红就当判据生效",就是把**期望错**读成**判据对**。
|
|
|
|
|
|
|
|
|
|
|
|
### 正解(已落地 · `install.py`)
|
|
|
|
|
|
|
|
|
|
|
|
1. **重排优先级**:`显式 > 宿主 env > cwd 就近 > roots.env 残留`。
|
|
|
|
|
|
2. **放宽 cwd 判据**:有 `.workbuddy/collab/`,或 `state.py`,或(`.workbuddy/memory/` + `state.py`)
|
|
|
|
|
|
⇒ 三选一即算工作区。⛔ 不再把 `state.py` 当必要条件。
|
|
|
|
|
|
3. **加归属自检(关键)**:`cmd_apply` 里把「cwd 判据」与「最终选用」并排打出来,⛔ 不许静默:
|
|
|
|
|
|
- `cwd 判据 is None` + 未显式传 `--workspace` ⇒ 🔴 **最危险的一档**:
|
|
|
|
|
|
"现在的工作区是从 `roots.env` **残留**推出来的,⛔ 不是从你脚下的目录认出来的"。
|
|
|
|
|
|
⚠️ **老写法只判 `here != ws`,恰好漏掉这一档**(`None != ws` 为真但语义不对,
|
|
|
|
|
|
第一版就是这么写的,实测告警**没触发**)⇒ 这正是"判据绿灯与被检对象状态无关"的老病。
|
|
|
|
|
|
- `roots.env` 原指向 ≠ 本次将写 ⇒ 🔴 警示「改指之后,原工作区侧也会跟着指过来」。
|
|
|
|
|
|
|
|
|
|
|
|
### 🔴 一条**必须说给用户**的结构事实
|
|
|
|
|
|
|
|
|
|
|
|
`roots.env` 是**包级单例**,而钩子条目**没有 `cwd` / `env` 字段**
|
|
|
|
|
|
(实测 `settings.json` 里本包条目只有 `command`/`timeout`/`type`)⇒
|
|
|
|
|
|
所有脚本都靠 `__file__` 反推包根 → 读**同一份** `roots.env`。
|
|
|
|
|
|
⇒ **一个技能包同时只服务一个工作区**;「两个工作区都要跑机制」是**已知不支持**的形态。
|
|
|
|
|
|
(此结论有读数支撑:`grep hooks` 字段 + 各脚本 `_sm_load_roots()` 同源。)
|
|
|
|
|
|
|
|
|
|
|
|
### ⚠️ 本轮另发现(未处理,留给后续)
|
|
|
|
|
|
|
|
|
|
|
|
- `settings.json` 里有**两条不属于本包声明表**的钩子:`reply-style-guard.py`、`supervise-ensure-hook.py`
|
|
|
|
|
|
⇒ `--dry-run` 的"被排除条数"只数了 `decision_bridge`(=4),**没覆盖这两条**。
|
|
|
|
|
|
- `agent-product/.workbuddy/collab/collabd.config.json` 里带着 `_说明` / `_形态` 富文本字段
|
|
|
|
|
|
(内容:「由 install 流程本地部署生成(未改全局 roots.env,看板仍由 ai1net-dsh-server 主视图承担)」),
|
|
|
|
|
|
⛔ **不是** `collabd.config.example.json` 的产物(该模板**无** `_形态` 字段)
|
|
|
|
|
|
⇒ 是**另一次会话手搓的规避性 workaround**。⚠️ 这种"本地部署"是不是该收进 `install.py`,未定。
|
|
|
|
|
|
### 🔴 已修(2026-10-05 · 当轮落地,附读数)
|
|
|
|
|
|
|
|
|
|
|
|
**正解第 1 条落地了**:`selftest.py::_fake_cfg()` 现在**先标 `FSCTL_SET_SPARSE`、再 `truncate`**
|
|
|
|
|
|
(新增 `_make_sparse()`;⛔ 顺序不能反 —— 先 truncate 再加标志,已分配的簇不会自动归还)。
|
|
|
|
|
|
|
|
|
|
|
|
| | 逻辑 | 实占 | 比 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| 老写法(只 `truncate`) | 9,600,000 | 9,600,000 | **100.00%** |
|
|
|
|
|
|
| 新写法(先标稀疏) | 9,600,000 | 65,536 | **0.68%** |
|
|
|
|
|
|
|
|
|
|
|
|
5 档合计:48,000,000 → 327,680 字节(**省 99.3%**)。
|
|
|
|
|
|
⇒ 单次 `selftest` 的假配置从 ~60 MB 降到 ~0.4 MB;`install.py --verify` 不再往回收站搬几十 MB。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ **⛔ 不追求零占用**:文件系统不支持稀疏时 `_make_sparse()` 返回 False,行为**回落成真写**
|
|
|
|
|
|
(与老实现同),但**会打 stderr 提示** —— ⛔ 不许静默退化(否则又是"看着省了、其实没省")。
|
|
|
|
|
|
⚠️ 正解第 2 条(子进程 `CODEBUDDY_SAFE_DELETE_ENABLED=0`)**仍未接线**,第 1 条已足够按住主量。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## P0-86 🔴🔴🔴 **`goal.json` 的 `execution_doc` 是「只读不写」的字段 —— 无人维护 ⇒ 必然漂移,且能漂成跨区污染**(★ 2026-10-05 修 P0-85 时顺手查出)
|
|
|
|
|
|
|
|
|
|
|
|
### 现象
|
|
|
|
|
|
|
|
|
|
|
|
`install.py --verify` 报「生产工作区该文件**真存在**」FAIL,路径 `…/ai1net-dsh-server/目标-本机协作-c8154d/目标执行状态.md`。
|
|
|
|
|
|
⚠️ 文件**其实存在**,真身多一层 `执行会话/`。
|
|
|
|
|
|
|
|
|
|
|
|
### 根因:整条链路只有一个读者、零个作者
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 对该键做了什么 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `collabd.py` | 只在 `CHECK_PROMPT_GOAL` 说明文字里提一句,**无写入代码** |
|
|
|
|
|
|
| `goalctl.py` | 一次都没出现 |
|
|
|
|
|
|
| `init_workspace.py` | 建 goal 模板时只有 `_唯一来源` **注释**,**不含该字段** |
|
|
|
|
|
|
| `selftest.py` | **唯一读者**(判据按它定位文件) |
|
|
|
|
|
|
|
|
|
|
|
|
⇒ 代码口径会改(10-05 加了 `执行会话/` 一层),登记值永远不跟着改(无作者 ⇒ 只能手工改 JSON ⇒ 迟早忘)。
|
|
|
|
|
|
|
|
|
|
|
|
### 比"假红"更坏:漂成了跨区污染
|
|
|
|
|
|
|
|
|
|
|
|
| 区 | 登记的 `execution_doc` | 按本区 `title` 算出的真路径 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `ai1net-dsh-server` | `目标-本机协作-c8154d/…` | `执行会话/目标-本机协作-c8154d/…` |
|
|
|
|
|
|
| `vibe-product` | `目标-vibe-product-3e3182/…` | `执行会话/目标-vibe-product-5d27fe/…` |
|
|
|
|
|
|
| `agent-product` | **(未登记)** | `执行会话/目标-agent-product-3e3182/…` |
|
|
|
|
|
|
|
|
|
|
|
|
🔴 `vibe-product` 那行最毒:`3e3182` = **`agent-product` 目标标题的 `sha1` 前 6 位**,正确值是 `5d27fe`
|
|
|
|
|
|
⇒ 该值是**从别区串写来的**,只看这一行**推不出该找哪个文件**(不是"前缀写错",是**指向了别人的目标**)。
|
|
|
|
|
|
⚠️ 噪音:`3e3182` 在 `ai1net-dsh-server/执行会话/` 下也有同名空壳目录(建于 10-03 12:12,早于 10-05 口径变更)⇒ **旧算法遗留,与本条无关**。
|
|
|
|
|
|
|
|
|
|
|
|
### 原判据设计是对的,缺的是倒数第二环
|
|
|
|
|
|
|
|
|
|
|
|
`t_execution_doc` **故意读生产 `goal.json` 登记值**(⛔ 不拿测试夹具算法重算 —— 上一版那样做过,问生产文件系统**必然不存在 ⇒ 假红**)。
|
|
|
|
|
|
⇒ 设计意图没错;缺的是**没人保证登记值跟得上代码**。判据没错、数据错了,而数据的错是代码不改它造成的。
|
|
|
|
|
|
|
|
|
|
|
|
### 正解(当轮已落地)
|
|
|
|
|
|
|
|
|
|
|
|
**① 接线:`collabd.py --ensure-goal-dir` 里顺手对齐**(按现行口径算真路径的唯一一步):
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
_want = exec_doc_rel() # 🔴 唯一真源(与 prompt/goal_dir_rel() 同源)
|
|
|
|
|
|
_gj = INBOX / "goal.json"
|
|
|
|
|
|
if _gj.is_file():
|
|
|
|
|
|
_gd = json.loads(_gj.read_text(encoding="utf-8")) or {}
|
|
|
|
|
|
if isinstance(_gd, dict) and _gd.get("title"):
|
|
|
|
|
|
_have = str(_gd.get("execution_doc") or "")
|
|
|
|
|
|
if _have != _want:
|
|
|
|
|
|
_gd["execution_doc"] = _want
|
|
|
|
|
|
_gd["execution_doc_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
_gj.write_text(json.dumps(_gd, ensure_ascii=False, indent=1),
|
|
|
|
|
|
encoding="utf-8", newline="")
|
|
|
|
|
|
out["doc_sync"] = ("✅ `execution_doc` 已对齐:%r → %r" % (_have or "(未登记)", _want))
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 放这里的理由:`--ensure-goal-dir` **幂等**、`init_workspace.py` 每次初始化必调 ⇒ 每区自动追平,⛔ 不靠人记。
|
|
|
|
|
|
⚠️ **只改这一个键**(整份读回原样写回,其余字段一字不动 —— 那是状态真源,覆盖=毁进度)。
|
|
|
|
|
|
⚠️ **失败不影响建目录**:整段包 `try`,出错只记 `out["doc_sync"]`。
|
|
|
|
|
|
|
|
|
|
|
|
**② 出声:CLI 把对齐结果印出来**
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
if _r.get("doc_sync"):
|
|
|
|
|
|
print("状态文档登记:%s" % _r["doc_sync"])
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
⛔ 不许静默对齐(静默 ⇒ 用户以为本来就对,下次再漂仍查不出)。
|
|
|
|
|
|
|
|
|
|
|
|
**③ 判据加两条**(一条判数据、一条判接线,缺一条都会"改完数据又漂回去"):
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
sync_ok = (not _exp_rel) or (_nrm(prod_g_rel) == _nrm(_exp_rel)) # 数据:登记值 == 按本区算出的
|
|
|
|
|
|
_ensure_sync = "_want = exec_doc_rel()" in _cd_code # 接线:对齐代码还在不在
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ 比对**必须归一化**(`/`↔`\`、大小写),否则 Windows 上加一条假红。
|
|
|
|
|
|
⚠️ 期望值**用生产那份 `title`/`short` 现算**(`goal_dir_name()` 纯函数、不依赖 `WS`)。
|
|
|
|
|
|
|
|
|
|
|
|
### 实测读数
|
|
|
|
|
|
|
|
|
|
|
|
| 区 | 对齐前 | 对齐后 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `ai1net-dsh-server` | `目标-本机协作-c8154d/…` | `执行会话/目标-本机协作-c8154d/…` |
|
|
|
|
|
|
| `vibe-product` | `目标-vibe-product-3e3182/…` | `执行会话/目标-vibe-product-5d27fe/…` |
|
|
|
|
|
|
| `agent-product` | `(未登记)` | `执行会话/目标-agent-product-3e3182/…` |
|
|
|
|
|
|
|
|
|
|
|
|
`selftest`:`PASS 95 / FAIL 2` → **`PASS 97 / FAIL 0`**;`install.py --verify` ⇒ **✅ 全绿**。
|
|
|
|
|
|
|
|
|
|
|
|
### 变异对照(⛔ 必修,否则可能是恒绿)
|
|
|
|
|
|
|
|
|
|
|
|
| 变异 | 预期 | 实测 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| A 基线 | 绿 | ✅ `PASS 1 / FAIL 0` |
|
|
|
|
|
|
| B 把 `execution_doc` 改回旧值 | 红 | ✅ `PASS 0 / FAIL 1` |
|
|
|
|
|
|
| C 还原 | 绿 | ✅ `PASS 1 / FAIL 0` |
|
|
|
|
|
|
| D 把 `_want = exec_doc_rel()` 改成 `_want = ""` | 红 | ✅ `PASS 0 / FAIL 1` |
|
|
|
|
|
|
| E 还原 | 绿 | ✅ `PASS 1 / FAIL 0` |
|
|
|
|
|
|
|
|
|
|
|
|
### 同轮另一处 P0:判据「全文 `in` 一把梭」=恒绿假修
|
|
|
|
|
|
|
|
|
|
|
|
修 `t_init_points_to_own_copy` 末条时,第一版写成 `("…", "不起看板" in src and "peer_workspaces" in src)`。
|
|
|
|
|
|
🔴 **实测:把那段 `print` 整块删掉(137 字节),判据照样绿。**
|
|
|
|
|
|
原因:`peer_workspaces` 全文出现 3 次、`不起看板` 2 次,**多数在注释里**(`src` 是**未剥注释**的全文)⇒ 全文 `in` = **在注释里找证据**。
|
|
|
|
|
|
|
|
|
|
|
|
✅ 正解:**锚定到那段代码**(`code` 已剥注释 ⇒ 注释字样不命中):
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
def _tail_has_board_note(code: str) -> bool:
|
|
|
|
|
|
i = code.find("✅ 完成。后续协作")
|
|
|
|
|
|
if i < 0:
|
|
|
|
|
|
return False
|
|
|
|
|
|
seg = code[i:i + 1500]
|
|
|
|
|
|
return ("不起看板" in seg) and ("peer_workspaces" in seg)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
⇒ 变异:删整段 ⇒ 红;只删「要并看」那一句(84 字节)⇒ 红;还原 ⇒ 绿。
|
|
|
|
|
|
**教训:判据的"作用域"必须跟着被判对象走,⛔ 不能拿一个大得多的范围去 `in`。**
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-87 🔴🔴🔴 **同一个技能里,同一份数据读三处、口径各写各的 —— `goalctl` 两处漏网**(★ 2026-10-06 · 用户「vibe-product 最新会话反应新问题 看看如何解决」)
|
|
|
|
|
|
|
|
|
|
|
|
### 症状(两条都不报错、不崩溃,只是"少说一句真话")
|
|
|
|
|
|
|
|
|
|
|
|
| # | 现象 | 真因 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| ① | 目标**四条验收全过**,`goalctl.py status` 仍报「未完成(验收 V1=过;V2=过;…)」——**字面自相矛盾** | `goalctl.py:262` 写成 `if str(acc[k]) != "pass":` —— **死板比字面值**,而存值**真源就是中文** |
|
|
|
|
|
|
| ② | 配置里**已改指**新目标的任务图,`goals_open()` 仍报 `bad=['任务图读不到(taskgraph.json)']` ⇒ 目标判不出来 | `goalctl.py:136` 写成 `CFG = HERE / "collabd.config.json"` —— `HERE` = **技能包目录**,而按定则「技能目录里⛔ 不放生产配置」⇒ 该文件**恒不存在** |
|
|
|
|
|
|
|
|
|
|
|
|
### 🔴🔴 病根是同一个:**「唯一实现」的约定只写在注释里,没有强制**
|
|
|
|
|
|
|
|
|
|
|
|
`board.py::acc_is_pass()` 的 docstring 明写:
|
|
|
|
|
|
|
|
|
|
|
|
> 🔴 三处判据(`board.py` 本函数 / `collabd.py::_acc_is_pass` / `board.html::accIsPass`)**必须同款**
|
|
|
|
|
|
|
|
|
|
|
|
而 `board.py:1618` 与 `collabd.py:3022` **都做了中文归一化**(`ACC_PASS_WORDS = ("pass","过","通过","达","达标","合格","完成")` + 剥完成副词)。**实际是四处,`goalctl.py` 被漏掉了。**
|
|
|
|
|
|
|
|
|
|
|
|
⇒ 同一个字段(`goal.json.acceptance_state`)在**同一技能内有三套读法**:两处归一化、一处死板。
|
|
|
|
|
|
⇒ 后果:**看板说通过、常驻程序说不通过、`goalctl` 说未完成** —— 三张嘴各说各的。
|
|
|
|
|
|
|
|
|
|
|
|
**同轮第二处同型**:`collabd.py` 有完整的配置三级解析(`_cfg_candidates()`:env → 工作区标准落点),
|
|
|
|
|
|
而 `goalctl.py` **写死技能目录** ⇒ 配置改了不生效。**又是"一个技能两套读法"。**
|
|
|
|
|
|
|
|
|
|
|
|
### ✅ 正解
|
|
|
|
|
|
|
|
|
|
|
|
1. **判定层复用真身,⛔ 不复制词表**(复制就是下一次漂的来源):
|
|
|
|
|
|
`goalctl` 新增 `_acc_is_pass()` ⇒ 首选 `collabd._acc_is_pass()`(它自己又首选 `board.acc_is_pass()`);
|
|
|
|
|
|
拿不到 ⇒ **fail-closed 声明式兜底** + stderr 留一行(⛔ 绝不静默返回 True/False)。
|
|
|
|
|
|
2. **配置解析与 `collabd._cfg_candidates()` 同款三级**,⛔ 不新造顺序。
|
|
|
|
|
|
3. **提示文案必须与判定层一致**:`:605` 原写「要 `名字=pass` 或 `名字=未过`」⇒ **误导**(`未过` 与任意中文值在旧判定层同样恒判未过)⇒ 改为说明"值可写 `pass`,也可写中文 `过`/`已过`/`达`——判定层会归一化"。
|
|
|
|
|
|
|
|
|
|
|
|
### 实测读数
|
|
|
|
|
|
|
|
|
|
|
|
| 项 | 修前 | 修后 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| vibe-product `goals_open()` | `{'open': True, 'why': [], 'bad': ['任务图读不到']}` | **`{'open': False, 'why': [], 'bad': []}`** |
|
|
|
|
|
|
| `selftest` | PASS 97 / FAIL 0 | **PASS 99 / FAIL 0** |
|
|
|
|
|
|
| `--verify` | — | **rc=0 ✅** |
|
|
|
|
|
|
|
|
|
|
|
|
### 变异对照(⛔ 必修,否则可能是恒绿)
|
|
|
|
|
|
|
|
|
|
|
|
| 变异 | 预期 | 实测 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| A 基线 | 绿 | ✅ `PASS 99 / FAIL 0` |
|
|
|
|
|
|
| B `goalctl` 判定改回 `!= "pass"` | 红 | ✅ `PASS 97 / FAIL 1`(**命中本用例**) |
|
|
|
|
|
|
| C 还原 | 绿 | ✅ `PASS 99 / FAIL 0` |
|
|
|
|
|
|
|
|
|
|
|
|
### 🔴 写用例时连踩三个坑(都是"我写错了,不是代码错")
|
|
|
|
|
|
|
|
|
|
|
|
1. **`imp()` 加载的是 `collabd`,不是 `goalctl`** —— 两者 `goals_open()` **同名但返回类型不同**
|
|
|
|
|
|
(collabd 返 `bool`、goalctl 返 `dict`)⇒ 直接用 ⇒ `TypeError: 'bool' object is not subscriptable`。
|
|
|
|
|
|
2. **两个模块的 `INBOX` 不是同一个目录** —— `collabd.INBOX` 来自**配置**;
|
|
|
|
|
|
`goalctl.INBOX = WS/"tmp"/"supervise-inbox"`(**硬编码**)。写错落点 ⇒ `bad=['任务图读不到']` ⇒ **用例恒红**。
|
|
|
|
|
|
3. **`CFG` 修好之后,用例的图落点又变了** —— 修前 `CFG` 读不到 ⇒ `taskgraph` 回落默认 `INBOX/taskgraph.json`,
|
|
|
|
|
|
修后 ⇒ 跟着配置走 `TEST_WS/tg.json`。⇒ **用例不许硬编码任一侧,要从配置里那个值算**。
|
|
|
|
|
|
|
|
|
|
|
|
> ⭐ **教训:用例红了先分"是被测代码错,还是我喂错了"。** 三次都是后者,若当时直接去"修代码",会把好的代码改坏。
|
|
|
|
|
|
> ⭐ **教训:`selftest` 的夹具要跟着"被测模块自己算出来的路径"走**(`_gc.INBOX` / 配置里的 `taskgraph`),
|
|
|
|
|
|
> ⛔ 不要按"我以为的目录"写死 —— 同一技能里两个模块的落点可以完全不同。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-88 🔴🔴🔴 本机**没有**任何回收站工具 —— `gio` 不存在,`Add-Type` 被拦,只剩 `SHFileOperationW`
|
|
|
|
|
|
|
|
|
|
|
|
> 症状:要"把目录送回收站"(可逆删除),三条常规路**全断**;且唯一能走的那条**判据写错会假报错**。
|
|
|
|
|
|
|
|
|
|
|
|
### 症状(本机实测)
|
|
|
|
|
|
|
|
|
|
|
|
| 途径 | 结果 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| `gio trash <dir>` | ❌ `gio: command not found`(PortableGit 里 `gio`/`trash-put`/`trash` **一个都没有**) |
|
|
|
|
|
|
| PowerShell `[Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(...,'SendToRecycleBin')` | ❌ **被安全策略拦**("Add-Type compiles and loads .NET code at runtime") |
|
|
|
|
|
|
| PowerShell `New-Object -ComObject Shell.Application`(`Namespace(10)`) | ❌ **被安全策略拦**("COM object instantiation can run arbitrary code") |
|
|
|
|
|
|
| **纯 `ctypes` 调 `SHFileOperationW`** | ✅ **可用**(不编译代码 / 不碰 .NET / 不弹 UAC) |
|
|
|
|
|
|
|
|
|
|
|
|
### ✅ 正解(可直接抄)
|
|
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
|
import ctypes, os
|
|
|
|
|
|
from ctypes import wintypes
|
|
|
|
|
|
|
|
|
|
|
|
class SHFILEOPSTRUCTW(ctypes.Structure):
|
|
|
|
|
|
_fields_ = [
|
|
|
|
|
|
("hwnd", wintypes.HWND),
|
|
|
|
|
|
("wFunc", wintypes.UINT),
|
|
|
|
|
|
("pFrom", ctypes.c_void_p), # 🔴 必须 c_void_p,见坑 ①
|
|
|
|
|
|
("pTo", ctypes.c_void_p),
|
|
|
|
|
|
("fFlags", ctypes.c_uint16),
|
|
|
|
|
|
("fAnyOperationsAborted", wintypes.BOOL),
|
|
|
|
|
|
("hNameMappings", ctypes.c_void_p),
|
|
|
|
|
|
("lpszProgressTitle", ctypes.c_void_p),
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
FO_DELETE, FOF_ALLOWUNDO = 0x0003, 0x0040 # 🔴 有 ALLOWUNDO 才进回收站
|
|
|
|
|
|
FOF_SILENT, FOF_NOCONFIRMATION = 0x0004, 0x0010
|
|
|
|
|
|
FOF_NOERRORUI, FOF_NOCONFIRMMKDIR = 0x0400, 0x0200
|
|
|
|
|
|
|
|
|
|
|
|
abspath = str(p.resolve()).replace("/", "\\")
|
|
|
|
|
|
buf = ctypes.create_unicode_buffer(abspath, len(abspath) + 2) # 🔴 双 \0 结尾
|
|
|
|
|
|
op = SHFILEOPSTRUCTW()
|
|
|
|
|
|
op.wFunc = FO_DELETE
|
|
|
|
|
|
op.pFrom = ctypes.cast(buf, ctypes.c_void_p)
|
|
|
|
|
|
op.fFlags = (FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_SILENT
|
|
|
|
|
|
| FOF_NOERRORUI | FOF_NOCONFIRMMKDIR)
|
|
|
|
|
|
rc = ctypes.windll.shell32.SHFileOperationW(ctypes.byref(op))
|
|
|
|
|
|
|
|
|
|
|
|
# 🔴🔴 判据:不是 rc == 0!
|
|
|
|
|
|
ok = (not p.exists()) and (not op.fAnyOperationsAborted)
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 🔴 三个坑
|
|
|
|
|
|
|
|
|
|
|
|
1. **`pFrom` 必须手搓双 `\0` 结尾缓冲。** ctypes 的 `LPCWSTR` **会在第一个 `\0` 处截断**
|
|
|
|
|
|
⇒ 结构体字段必须声明成 `c_void_p`,用 `create_unicode_buffer(s, len(s)+2)` 再 `cast` 进去。
|
|
|
|
|
|
⛔ 写成 `wintypes.LPCWSTR` 并直接赋 `str` ⇒ 静默只删半截路径(或直接 `rc=2`)。
|
|
|
|
|
|
2. **判据⛔ 不是 `rc == 0`。** 实测**送回收站成功**(目录已消失、回收站 `$I` 元数据能解出原路径),
|
|
|
|
|
|
返回值却**恒为 `rc=2`**(`ERROR_FILE_NOT_FOUND`)—— 这是 Shell 内部探测残留的 `GetLastError`,
|
|
|
|
|
|
**不是真失败**。⇒ 拿 `rc` 当判据 ⇒ **假报错**(用户看到"失败",但东西已经进回收站了,以为没删掉会再点一次)。
|
|
|
|
|
|
✅ 真判据只有两条:**① 原路径不存在了 ② `fAnyOperationsAborted` 为假**。
|
|
|
|
|
|
3. **验证"是否真进了回收站"要解 `$I` 元数据。** 格式:`v2`、路径 **UTF-16LE**、起点 **offset 24**
|
|
|
|
|
|
(⛔ 不是 26;开头 4 字节是长度前缀,解出来会多一个乱字符,`.replace("\x00","").strip()` 后再比对)。
|
|
|
|
|
|
⛔ 别拿 `ls $RECYCLE.BIN` 的条数当证据 —— `$R*` 才是数据、`$I*` 是元数据,两两配对,容易数错。
|
|
|
|
|
|
|
|
|
|
|
|
### 本机取证(2026-10-06)
|
|
|
|
|
|
|
|
|
|
|
|
`_pmtest-{verify,trash,wsdel,wsdel2,http}` 五个测试目录全部送进 `E:/$RECYCLE.BIN/S-1-5-21-…-500/`,
|
|
|
|
|
|
共 **29+ 条 `$I` 元数据**,逐条解出的原路径与送入路径**一致** ⇒ **确认可恢复**。
|
|
|
|
|
|
|
|
|
|
|
|
> ⭐ **教训:判"操作成功没有",要挑那个"跟操作语义直接对应的观测量",⛔ 不是挑接口返回码。**
|
|
|
|
|
|
> 返回码是**实现细节**(这里还带 Shell 的脏状态),`exists` / `aborted` 才是**语义**。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-89 🔴🔴 **CSS 逐字正确 ≠ 渲染正确** —— 「容器自己就是那个布局类」时内层再套一次 = 宽度被吃两层
|
|
|
|
|
|
|
|
|
|
|
|
**症状**(2026-10-06 项目管理界面,用户肉眼先发现):
|
|
|
|
|
|
四列看板整块挤在页面左边一条,每列只有 ~89 px、卡片 67 px,右侧一大片空白,页高 3192 px。
|
|
|
|
|
|
|
|
|
|
|
|
**第一诊断陷阱**:`grep` 样式表 ⇒ **CSS 全对**
|
|
|
|
|
|
(`.cols{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}` 逐字正确)
|
|
|
|
|
|
⇒ 读者会以为"样式没问题,是不是内容/数据的问题"。**实际是结构问题,读样式表永远查不出来。**
|
|
|
|
|
|
|
|
|
|
|
|
**真因 —— 嵌套 grid**(自己上一轮改造时引入):
|
|
|
|
|
|
|
|
|
|
|
|
```html
|
|
|
|
|
|
<!-- 容器**本身**就带 class="cols" ⇒ 它已经是那个 4 列 grid -->
|
|
|
|
|
|
<div class="cols" id="cols"></div>
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
// ✗ 错:又包了一层 .cols
|
|
|
|
|
|
cols.innerHTML = '<div class="cols">' + cols.map(renderCol).join("") + '</div>';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
内层 div 成了**外层 grid 的一个 grid item** ⇒ 先被 `min-width:auto` 收缩到**内容宽**
|
|
|
|
|
|
(实测 399 px,=4×89+3×14),再在 399 px 里**自己又切 4 列** ⇒ 89 px/列。
|
|
|
|
|
|
**两层 grid 叠着吃宽度**,而两层各自看都是"对的"。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解**:容器是布局类时,**内层只铺子元素,⛔ 不再套同名类**。
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
cols.innerHTML = colsData.map(function(c){ return _colHtml(c.key, c.label, …); }).join("");
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 判据(同族通用)**:当 `#X` 的 `class` 里**已经含**你正要写进 `innerHTML` 的那个类名
|
|
|
|
|
|
⇒ 立刻停下,**这就是嵌套信号**。别写第二层。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 取数姿势 —— 量,不读**:
|
|
|
|
|
|
|
|
|
|
|
|
```javascript
|
|
|
|
|
|
var el = document.querySelector('#cols > .col');
|
|
|
|
|
|
var r = el.getBoundingClientRect();
|
|
|
|
|
|
// 再 getComputedStyle(el).gridTemplateColumns ⇐ 看实际生效的列宽
|
|
|
|
|
|
```
|
|
|
|
|
|
实测读数(改前→改后):`col 89px → ~400px`、`card 67px → 381px`、`页高 3192 → 1415px`。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ **探针选择器要跟着结构改**:去掉内层 `.cols` 之后 `#cols > .cols > .col` **恒为 0 条**
|
|
|
|
|
|
—— 那是**选择器过期**,⛔ 不是"布局又坏了"(差一步就误判成回归)。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-90 🔴 布局「虚胖」的默认值:grid 的 `align-items:stretch` 会把空列拉到和高列一样高
|
|
|
|
|
|
|
|
|
|
|
|
**症状**(同日,用户:「卡片上下之间的距离太远」):
|
|
|
|
|
|
12 张卡片全在「已完成」列,另三列**空着**,但四列被拉成**等高** ⇒ 整页虚胖、空列白占 1100 px。
|
|
|
|
|
|
|
|
|
|
|
|
**真因**:grid 默认 `align-items:stretch` ⇒ 每个 grid item(=列)**拉满行高**,
|
|
|
|
|
|
而行高由最高那列决定。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解**:`.cols{align-items:start}` ⇒ 每列**各自撑自己的高度**,空列就是一个小盒子。
|
|
|
|
|
|
**连带压紧**:卡片 `gap` 9→6 px、`.cards` padding 10→8、列的 `min-height:160px`→0。
|
|
|
|
|
|
|
|
|
|
|
|
⚠️ **别用「把空列删掉」来治** —— 四列是**固定四态**(待执行/执行中/受阻/已完成),
|
|
|
|
|
|
空态本身是信息。✅ 治的是**高度**,⛔ 不是**列本身**。
|
|
|
|
|
|
|
|
|
|
|
|
> ⭐ **教训:`display:grid` 有一堆"看起来对、实际在拉橡皮"的默认值**(`stretch`/
|
|
|
|
|
|
> `min-width:auto`/`min-height:auto`)。凡是"某个方向被撑大或压扁"的怪相,
|
|
|
|
|
|
> 先怀疑这三条默认值,⛔ 别先怀疑自己的 `gap`/`width` 数值写错。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-91 🔴 **"工作区" 与 "目标" 是两个维度** —— 计数、选择器、文案,混用必错
|
|
|
|
|
|
|
|
|
|
|
|
**症状**(同日,用户连问三句才问出来):
|
|
|
|
|
|
选择器写「全部(**3 个目标**)」,用户:「不对吧,本机不是只有一个目标吗」。
|
|
|
|
|
|
**追问两层**:① 计数用的是 `projects.length` = **工作区条数**(3);
|
|
|
|
|
|
② 用户真实口径是「**下拉框是选工作区的,跟目标没关系**」—— 连"选目标"这个定位都是我加的。
|
|
|
|
|
|
|
|
|
|
|
|
**事实**(同一份 `/api/projects`):
|
|
|
|
|
|
|
|
|
|
|
|
| 维度 | 数 | 说明 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| **工作区**(装了 `.workbuddy/collab` 的目录) | **3** | `ai1net-dsh-server` / `vibe-product` / `agent-product` |
|
|
|
|
|
|
| **目标**(工作区里登记的那件事) | **2** | 前两个才有 `goal.json.title` |
|
|
|
|
|
|
|
|
|
|
|
|
⇒ 「3」既不是"3 个目标"也不是错的 —— 它只是**工作区数**,被贴了"目标"的标签。
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 两条口径**:
|
|
|
|
|
|
|
|
|
|
|
|
1. **计数与文案必须标清维度**:说"工作区"时数目录,说"目标"时数 `has_goal` 为真的。
|
|
|
|
|
|
⛔ 别拿 `projects.length` 去填「N 个目标」(**这是"张冠李戴"型错误,看起来还挺自洽**)。
|
|
|
|
|
|
2. **没登记目标的工作区 ⛔ 不许藏**(用户原话:「加载机制就可以显示」)。
|
|
|
|
|
|
⚠️ 我一度把它判成"空壳"、还提议从选择器里去掉 —— **被否**。
|
|
|
|
|
|
实况:它**装了机制只是没登记目标**(有 `collabd-state.json` + `_ensure.stamp`,
|
|
|
|
|
|
**只是没有 `goal.json`**)⇒ 它是**合法状态**,不是垃圾。
|
|
|
|
|
|
⇒ 同族「作用域⛔ 不许静默排除」:**列不出来**比**列出一个"未登记"**危险得多。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 呈现**:选项文字=「目录名(目标名)」;没有目标就只印目录名
|
|
|
|
|
|
(⛔ 别印「(未命名 · xxx)」——用户原话「未命名 看不懂个是什么」;
|
|
|
|
|
|
改印 **「未登记目标 · <dir>」**,直接点出缺什么 + 怎么补)。
|
|
|
|
|
|
|
|
|
|
|
|
**⚠️ 命名优先级**:`name = short or title` ⇒ 登记过 `short` 的区在界面上**一律显示短名**
|
|
|
|
|
|
(`vibe-product` 这种目录名)。用户:「显示的是目录名,应该是真正名称」。
|
|
|
|
|
|
⇒ **`short` 是给文件名/路径用的短标识,⛔ 不是给人读的名字** ⇒ `title or short`。
|
|
|
|
|
|
|
|
|
|
|
|
> ⭐ **教训:凡是"张冠李戴"型错误(拿 A 维度的数去填 B 维度的标签),**
|
|
|
|
|
|
> **它不会报错、不会崩、看起来还挺自洽** —— 只有**用户对照现实**才看得出来。
|
|
|
|
|
|
> ⇒ 报数字时**连维度一起报**("工作区 N 个,其中已登记目标 M 个"),把两个数都摆出来。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-92 🔴 报「疑似是我刚才改坏的」之前,**先 diff 备份** —— 别让怀疑变成向用户上报的事实
|
|
|
|
|
|
|
|
|
|
|
|
**经过**(同日):用户说「显示的是目录名,应该是真正名称」。我一看,`vibe-product` 的分支名
|
|
|
|
|
|
变成 `vibe-product` 了(此前显示的是目标短名 `本机协作` 那一套)⇒ **我立刻怀疑是自己刚才
|
|
|
|
|
|
执行「收工」操作时改坏的**,并准备这么向用户报告。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 一 diff 就清楚了**:`goal.json` 的 `title` / `short` 在**收工前后逐字一致**
|
|
|
|
|
|
(对比 `goal.json.bak-setlife-20261006-0926`)⇒ 是**老口径**(`short or title`),
|
|
|
|
|
|
⛔ 与收工操作**毫无关系**。
|
|
|
|
|
|
|
|
|
|
|
|
**为什么值得单列一条**:差一点我就把"**我猜的**"当"**事实**"报上去了。
|
|
|
|
|
|
在用户那边的效果是:「你自己改坏的吧」—— **一次未经核实的自责,比不说更糟**:
|
|
|
|
|
|
它会把排查方向**从真因上带偏**,而且**看起来还很坦诚**(所以不会被人拦下)。
|
|
|
|
|
|
|
|
|
|
|
|
**🔴 规矩**:
|
|
|
|
|
|
- 动过某个文件/某份状态 **之后**,任何"怎么变成这样了"的疑问 ⇒ **第一步 diff 备份**
|
|
|
|
|
|
(本轮正好有:动手前备了 `goal.json.bak-setlife-<时间戳>`)。
|
|
|
|
|
|
- ⛔ **⛔ 不许把"疑似副作用"直接写成结论**。措辞必须落到
|
|
|
|
|
|
「**我核过了/我没核**」二选一:核过 ⇒ 报事实;没核 ⇒ **先核再说**。
|
|
|
|
|
|
- ✅ 通则:**做变更前先留一份可比对的快照** —— 它的价值不在回滚,而在**事后能自证清白**。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## P0-93 「未发现」≠「不存在」—— 报缺失前先证明你的检测器能看见它
|
|
|
|
|
|
|
|
|
|
|
|
**2026-10-06 一轮里连撞三次,同一个病。**
|
|
|
|
|
|
|
|
|
|
|
|
### 现场
|
|
|
|
|
|
|
|
|
|
|
|
一天之内,我三次向用户/自己报"东西缺了",三次都是**假红**:
|
|
|
|
|
|
|
|
|
|
|
|
| # | 我报的 | 真相 | 我的判据错在哪 |
|
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
| 1 | 「`dsh-diagnose` 三个退役名**无独立详情档**,只在正文里被提到」 | 三档**都有**,且每档档头都写了 `本档覆盖:原技能 XXX 全文` + `原行段` + `逐行未改` | 检测判据=**文件名/目录名含退役名** ⇒ ⛔ 认不出「档头声明 provenance」这种落点 |
|
|
|
|
|
|
| 2 | 「`dsh-decision`/`dsh-workflow` 的清单表**没登记子目录档**」 | **都登记了**(`references/dsh-decision-method/` 那一行) | 正则只写 `references/xxx.md` ⇒ ⛔ 认不出目录行 `references/xxx/` |
|
|
|
|
|
|
| 3 | 「界面**四列全空**」(上一轮) | 界面好的,已完成列 **12 张卡** | 探针打了 `/api/board`(**404**)⇒ 拿到空 JSON 就当"数据是空的"。真端点=`/api/projects`,且卡片在 `projects[].cards`,⛔ 不在 `cols[].items` |
|
|
|
|
|
|
|
|
|
|
|
|
### 为什么这个病特别毒
|
|
|
|
|
|
|
|
|
|
|
|
1. **它长得像"发现了问题"** —— 报告读起来是"我查得很细,发现 N 处缺失",**比说"没发现问题"更像在工作**;
|
|
|
|
|
|
2. **下一步动作是破坏性的** —— 你会去"修"那个不存在的缺失:给已经有好档的地方再补一份(**制造重复**)、
|
|
|
|
|
|
或者按错判去删/改(**制造事故**);
|
|
|
|
|
|
3. **它自我强化** —— 一旦你按假红动手,改完"验证"还是红的(因为判据本身就错),
|
|
|
|
|
|
于是你会**继续加码修**,越修越乱。
|
|
|
|
|
|
|
|
|
|
|
|
### 根因(一句话)
|
|
|
|
|
|
|
|
|
|
|
|
**判据写窄了。** 我写检查脚本时,脑子里只有**一种**落点形态(文件名、`.md` 文件、某个端点),
|
|
|
|
|
|
而实际系统的落点形态**不止一种**(档头声明、目录、`projects[].cards`)。
|
|
|
|
|
|
⇒ **检测器的表达能力 < 系统的真实形态** ⇒ 看不见的就被报成"不存在"。
|
|
|
|
|
|
|
|
|
|
|
|
同族已知条:`dsh-local-env` 里记的「**判据写窄=假红**」、`dsh-diagnose` 的「**静默失败当默认假设**」。
|
|
|
|
|
|
|
|
|
|
|
|
### 规矩(动手前过一遍)
|
|
|
|
|
|
|
|
|
|
|
|
1. 🔴🔴 **报"缺失"之前,先给检测器做一次「正例测试」** ——
|
|
|
|
|
|
拿一个**你已知存在**的东西喂进去,看它**认不认**。
|
|
|
|
|
|
· 测试 1:拿 `dsh-diagnose/references/00-服务器实例故障.md`(**确定有 provenance 头**)
|
|
|
|
|
|
喂给"找独立档"的脚本 ⇒ 它报 ❌ ⇒ **判据就是坏的**,⛔ 不许把结果上抛。
|
|
|
|
|
|
· 测试 2:在界面上**肉眼**看到 12 张卡之后,才允许说"某处为空"。
|
|
|
|
|
|
**认不出的正例,比认得出的反例更有诊断价值。**
|
|
|
|
|
|
2. 🔴 **"我在 A 处没看到" ≠ "A 处没有"** —— 先问一句:**"它还可能在哪?"**
|
|
|
|
|
|
同一件事的落点常有多种形态:文件名 / 目录名 / **档头声明** / 表格某列 / 另一个 API 字段。
|
|
|
|
|
|
3. 🔴 **拿到"空结果"时,先证伪"我的取数姿势"** ——
|
|
|
|
|
|
空 JSON、404、0 条,**优先怀疑端点/路径/键名写错**,而不是"对方是空的"。
|
|
|
|
|
|
· 本轮实例:`curl /api/board` 返回空 ⇒ 正解是 **`grep 路由定义`** 找真端点,**⛔ 不是**下结论"没数据"。
|
|
|
|
|
|
4. ⚠️ **报"我没发现"时,把判据一并写出来** —— 让用户能一眼看出你的判据窄不窄。
|
|
|
|
|
|
⛔ 禁止只写「未发现缺失」这种**无法证伪**的表述。
|
|
|
|
|
|
5. ✅ **措辞三态**(不许含混):
|
|
|
|
|
|
· 「**我核了 X 处,判据是 Y,全部命中**」=有结论
|
|
|
|
|
|
· 「**我核了 X 处,判据是 Y,有 N 处没命中**」=有发现
|
|
|
|
|
|
· 「**我只核了 X,没核 Y**」=**边界声明**(⛔ 不许写成"没问题")
|
|
|
|
|
|
|
|
|
|
|
|
### 一句话
|
|
|
|
|
|
|
|
|
|
|
|
> **报"缺了"的举证责任,比报"有"更重** —— 因为你接下来要动的手,是按这个结论去改。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-94 钩子超时:先算「几条 × 冷启动」,别去改脚本
|
|
|
|
|
|
|
|
|
|
|
|
**事故形状(2026-10-06 实测)**
|
|
|
|
|
|
|
|
|
|
|
|
`UserPromptSubmit` 一次报 4 条 hook `timed out after 10000ms`,看着像"这几个脚本卡住了"。
|
|
|
|
|
|
|
|
|
|
|
|
**先做的一件事:把每条单独计时。** 实测:
|
|
|
|
|
|
|
|
|
|
|
|
| 跑法 | 耗时 |
|
|
|
|
|
|
|---|---|
|
|
|
|
|
|
| 任一条单跑 | 0.23–0.36 s |
|
|
|
|
|
|
| 4 条各单跑(累计) | 1.247 s |
|
|
|
|
|
|
| 7 条**并发** | 0.84 s |
|
|
|
|
|
|
|
|
|
|
|
|
⇒ **单条都不慢,也没有并发争抢** ⇒ **不是脚本的问题**。
|
|
|
|
|
|
|
|
|
|
|
|
**真因**:同一事件上**串行挂了 7 条 hook**,每条都是**一个独立进程**
|
|
|
|
|
|
(`python.exe <hook>.py`)⇒ 宿主每轮**冷启 7 次解释器**。前 3 条预算 15+30+20 s,
|
|
|
|
|
|
后 4 条 `timeout=10` **被排队挤破**。
|
|
|
|
|
|
|
|
|
|
|
|
🔴 **判据**:`单条耗时 × 条数` 远小于超时值、但集群一起超时 ⇒ **问题在"条数 × 冷启动",
|
|
|
|
|
|
不在任何单个脚本**。⛔ 此时去优化某个脚本是白费(它本来只要 0.3 s)。
|
|
|
|
|
|
|
|
|
|
|
|
**✅ 正解:同一事件、同一 stdin 契约、fail-open、无副作用的守卫 ⇒ 合并成一个进程。**
|
|
|
|
|
|
|
|
|
|
|
|
- 本包实现:`scripts/hooks/prompt-guards.py`(一次读 stdin,`runpy` 本进程内依次跑,
|
|
|
|
|
|
临时接管 stdout 收 JSON 再合并)。实测 1.247 s → **0.357 s(3.5×)**,注入内容**逐字一致**。
|
|
|
|
|
|
- ⛔ **不许合并**有**副作用**的(常驻确保 / 结果投递 / 决策桥)—— 合并会改语义。
|
|
|
|
|
|
- 合并时**只能加信息、不能减判定**:`additionalContext` 拼接;
|
|
|
|
|
|
`decision`/`permissionDecision` 等冲突时**保留先到者并留痕**(⛔ 不静默丢阻止)。
|
|
|
|
|
|
|
|
|
|
|
|
**⚠️ 顺带两个坑**
|
|
|
|
|
|
|
|
|
|
|
|
1. 🔴 **别被报错里的路径带偏**:本次有一条写 `E:/ProgramData.workbuddy/...`(**少一个点**),
|
|
|
|
|
|
实际该目录**不存在** —— 它只是历史 trace 里的残影,⛔ 不是当前接线。
|
|
|
|
|
|
判据:**报错里的每个路径都先 `ls` 一下**,别直接顺着它去"修"。
|
|
|
|
|
|
2. 🔴 **合并入口收 stdout 的坑**:`_Tee` 继承 `TextIOBase` 时若写 `self.buffer = self`
|
|
|
|
|
|
⇒ guard 的 `sys.stdout.buffer.write(bytes)` **撞上 `TextIOBase.write`**
|
|
|
|
|
|
⇒ bytes 被 `str()` 成 `b'{"..."}'` ⇒ **JSON 解析必失败**(现象:报"输出了非 JSON(6228 字节)")。
|
|
|
|
|
|
✅ `buffer` 必须是**独立的 `io.BytesIO`**,文本/字节两路**各收各的**。
|
|
|
|
|
|
|
|
|
|
|
|
**⚠️ 另一个附带教训**:手工接的线(⛔ 不在 `install.py::OWN_BASENAMES` 里的)
|
|
|
|
|
|
`--uninstall` **认不出** ⇒ 换包/重装后**同一件事挂两条钩子**。
|
|
|
|
|
|
本次实测 `reply-style-guard.py` 就是漏网的 ⇒ 已补登记。
|
|
|
|
|
|
|
|
|
|
|
|
### 一句话
|
|
|
|
|
|
|
|
|
|
|
|
> **钩子超时,先算「几条 × 冷启动」,再决定动谁** ——
|
|
|
|
|
|
> 单条 0.3 s 却被判超时的,要治的是**条数**,不是那个脚本。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## P0-95 🔴🔴🔴 **「换了个看起来更对的调用」≠「判据变强了」—— 改判据后不重跑变异对照,会把假红换成恒绿**(★ 2026-10-06 当场被自己的变异对照抓住)
|
|
|
|
|
|
|
|
|
|
|
|
### 现象(本轮三连)
|
|
|
|
|
|
|
|
|
|
|
|
一天之内,`session-rules-check.py` 里查出**三个判据缺陷**,性质完全一样:**判错了**。
|
|
|
|
|
|
|
|
|
|
|
|
| 判据 | 错法 | 表象 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| `hook_reg` | 按**旧文件名**找钩子 | 合并成 `prompt-guards.py` 后,**每轮报「关键钩子不在册」**(假红,惩罚的正是做过的合并) |
|
|
|
|
|
|
| `snap_sync` | 拿 **mtime** 当内容判据 | 权威 `CODEBUDDY.md` 被 touch ⇒ 报「快照比权威旧」;**实测 md5 逐字节相同、diff 0 行** ⇒ 连续 **4 天**假红 |
|
|
|
|
|
|
| `mem_ptr` | 只查**全局**技能根 | 工作区自带的技能(`product-planning`)被判「悬空」——判据问错了问题 |
|
|
|
|
|
|
|
|
|
|
|
|
### 🔴🔴 本轮最该记住的一幕:**我修 `snap_sync` 时差点交付一个恒绿判据**
|
|
|
|
|
|
|
|
|
|
|
|
- 第一版修法:把 mtime 比对**换成** `resident-rules.py --check` —— **看起来更对**(内容口径嘛)。
|
|
|
|
|
|
- 按老规矩跑**变异对照**:把快照文件**截断到前 400 字节** ⇒ 期望报 fail。
|
|
|
|
|
|
- 实测:**它报 `✅ 关键规则齐备`(rc=0)** ⇒ **判据恒绿**。
|
|
|
|
|
|
⚠️ 恒绿**比假红更坏**:假红至少还在喊,恒绿是**彻底哑掉**,且看起来更"先进"。
|
|
|
|
|
|
- 真因:`--check` 校验的是「**目标 `CODEBUDDY.md` 里关键规则齐不齐**」,
|
|
|
|
|
|
**根本不含「与快照比对」这一步** —— 我没读它的实现就假设了语义。
|
|
|
|
|
|
- ✅ 正解:**导入抽取器本体**(`importlib` 加载 `resident-rules.py`,把它的 `SNAP` 常量临时改指临时文件,
|
|
|
|
|
|
调**它自己的** `snapshot()` 拿"应有内容"),再与磁盘真快照比对。
|
|
|
|
|
|
⛔ 不抄第二份抽取逻辑(两处 ⇒ 漂移);⛔ 不让它写真快照(体检不改资产)。
|
|
|
|
|
|
- 修完四段复验:正常→ok|**变异→fail**|还原→ok|前提不成立(无 `CODEBUDDY.md`)→warn「本项不适用」。
|
|
|
|
|
|
|
|
|
|
|
|
### 🔴 同族第二个坑:**fail-open + 只看 `rc=0` 的体检 = 假绿温床**
|
|
|
|
|
|
|
|
|
|
|
|
`stop-dialog-guard.py` 因 `session_budget()` **返回元组长度不一致**
|
|
|
|
|
|
(两条早退 `return None, None`=2 值,末尾 `return a, b, c`=3 值,调用方按 3 值解包)
|
|
|
|
|
|
⇒ transcript **> 64 MiB**(本会话实测 **192.8 MB**)时**每轮 `ValueError`**。
|
|
|
|
|
|
|
|
|
|
|
|
- 而它 **fail-open**(异常仍 `sys.exit(0)`)⇒ **宿主零报错**;
|
|
|
|
|
|
- `install.py --verify` 只判 `rc=0` ⇒ **判它 "ok"**;
|
|
|
|
|
|
- 实际死的是**整条钩子**:水位与收口 / 接续机制起点 / 预算告警 / 门禁自检 / 路径自检。
|
|
|
|
|
|
|
|
|
|
|
|
⇒ **判「钩子活着」,⛔ 不看 `rc`**,要看**它自己的日志尾部**:
|
|
|
|
|
|
有没有 `EXCEPTION`、有没有写出**完整的 `invoked(...)` 行**(崩在半路的只会写 `entry`)。
|
|
|
|
|
|
|
|
|
|
|
|
### 判据(下次改判据前照做)
|
|
|
|
|
|
|
|
|
|
|
|
1. **改判据 ⇒ 必须重跑变异对照**:**真去破坏一次**(截断 / 改一字 / 删一节 / 造成前提不成立),
|
|
|
|
|
|
看它**会不会报**。⛔ **"换了个看起来更对的调用" 不算验证。**
|
|
|
|
|
|
2. **先读实现再信语义**:调别人的脚本当判据前,**读它的实现**(或至少读它到底比什么);
|
|
|
|
|
|
⛔ 别按函数名/文档一句话就假设它做了你想的那件事(本轮 `--check` 就是这么坑的)。
|
|
|
|
|
|
3. **同一函数的所有 `return` 必须同长**(长**短不一致**是"防护性早退"最常见的漏)。
|
|
|
|
|
|
4. **判据要问对问题**:`mem_ptr` 该问「**本机取不取得到这个名字**」(全局 ∪ 工作区),
|
|
|
|
|
|
⛔ 不是「全局库目录里有没有」。
|
|
|
|
|
|
5. **判据别拿"元数据"当"内容"**:mtime 只说明**谁最后被写过**,不说明**内容差没差**。
|
|
|
|
|
|
6. **前提不成立时给 `warn` 并写明「本项不适用」**,⛔ 不给 `fail`(那会把合法状态判成故障),
|
|
|
|
|
|
⛔ 也不给 `ok`(那是假绿)。
|
|
|
|
|
|
|
|
|
|
|
|
### 一句话
|
|
|
|
|
|
|
|
|
|
|
|
> **改判据的核心动作不是"换个更对的比法",是"再破坏一次看它会不会喊"。**
|
|
|
|
|
|
> 变异对照不做 ⇒ 你的"修复"很可能只是把**假红**换成了**恒绿**。
|