# 踩坑清单(每条都真实发生过) > ## 🔴 当前结论(**先读这里** · 最后更新 2026-10-02 05:1x) > ⚠️ 本文件**通篇追加式** ⇒ 旧条可能已被取代(已就地标注)。**冲突时以「本节 + `architecture.md` 的「当前结论」节」为准**; > 历史只留最近 5 轮、更早的归档(例外:**教训类不受 5 轮限制** —— 见 `agent-operating-rules §1.7a`)。 > > **最该先记住的几条**(其余按 P 编号往下读) > | 编号 | 一句话 | 为什么它排前面 | > |---|---|---| > | **P0-71** | 🔴🔴🔴 **常驻靠什么活着:不在「作业对象」(恒真、没鉴别力),在「谁拉起它」** —— 会话树里起的(父链穿到 `WorkBuddy.exe`)一收工就死;**只有计划任务起的能活**(父链断在自己身上)。✅ 定案形态=**计划任务 → `pythonw.exe` → `--supervise`,每 5 分钟判活**;⚠️ 两个致命细节:**`WorkingDirectory` 必须是工作区根**(⛔ 脚本目录 ⇒ 找不到配置 ⇒ 拒跑)、**常驻不需要网关口令**(⛔ 所以别用 `--ensure`)。🔴 看板 `LastResult=1` 是**虚警**(pythonw 无 stdout),判据看**端口**。🔴 **三条硬约束**(先查后建/各区独立/只有看板共用)+ **`deploy_code.py DEFAULT_FILES` 必须含 `collabctl.py` 等入口脚本**(⛔ 漏了 ⇒ 各区副本永不更新=P0-57 同族) | 🔴 **同一件事栽到第四次**(用户:「**从1号搞到5号 还起个程序都启动不起来**」);错一次=常驻全死、兜底全失 | > | **P0-72** | 🔴🔴🔴 **两个静默失败**:① `kill_all()` 的 `taskkill` 带 `HIDE`(含 breakaway)⇒ 必被拒 ⇒ `except` 吞掉 ⇒ **"已杀进程 0 个"却一个没死=假停**(✅ 改 `creationflags=0`);② 看板任务**直起 `board.py`** ⇒ 无 `COLLABD_CONFIG` ⇒ `已拒跑` ⇒ **崩溃重启循环、端口从没绑上**(✅ 新增 `board-launch.py` 启动器在进程内设 env)。🔴 长驻服务的 `ExecutionTimeLimit` 必须 `0`(⛔ 设 2 分钟 ⇒ 到点被掐)|🔴 `list_procs` 的 `python*` 会**连调用方一起匹配到** ⇒ 必须算保护集 | 🔴 **"做了动作" ≠ "动作生效"**:这两件事**都不报错**,只表现为"停止没停""看板没起" ⇒ 关键动作**必须有动作后复核** | > | **P0-73** | 🔴🔴🔴 **常驻启动器缺一个环境变量 ⇒ 检查程序静默失效**:任务**直起 `collabd.py --supervise`** ⇒ 任务环境**没有 `CODEBUDDY_CONFIG_DIR`** ⇒ `_wb_db()` 落到 `C:\\Users\\Administrator\\.workbuddy\\workbuddy.db`(**0 字节空库**)⇒ `all_sessions_idle` 每轮报 **`no such table: sessions`** ⇒ fail-safe **恒判"有会话在跑"** ⇒ **检查会话再也不建**(外表完全安静、零报错)。✅ 新增 `supervise-launch.py`(进程内 `setdefault("CODEBUDDY_CONFIG_DIR", ...)`)+任务动作改指它+`DEFAULT_FILES` 补入。🔴 **"进程活着" ≠ "它在干活"** ⇒ 检查程序必须看**有没有产出预期分支**(`检查会话:…`)|🔴 **重构"起法"时旧起法的 env 要逐条搬**(本坑就是丢了这一句)|🔴 各区 config 的 `host_db` **写死绝对路径最稳**(vibe 一直这么写 ⇒ 只有 ai1net 炸) | 🔴 **同一件事栽到第五次**(用户点名「检查协作程序和检查程序运行是否正常」才抓到);fail-safe 失败方向安静 ⇒ **凡 fail-safe 必须打可检索日志** | > | **P0-75** | 🔴🔴 **远端技能总仓里躺着明文令牌**(`workbuddy_skills.git` 的 `.neodata_token`,`tk_` 明文 72 B,自首次入库 `43b83b0` 就在):首次入库 `git add -A` 整目录收录、`.gitignore` 只挡了产物 ⛔ 没挡凭据。✅ `git rm --cached` + 补忽略规则(`031b312`)|⚠️ **历史仍有该 blob**,彻底清须 `push --force` 重写(牵连 25 技能)⇒ 等用户拍板;根治是**服务端吊销令牌**。🔴 入库前必扫凭据文件名 + 已知令牌串;核验远端必**比差异**(vs 备份)| > | **P0-74** | 🔴🔴🔴 **`_escalate_to_keeper()` 残留旧形态 ⇒ 计划任务里躺着 `powershell.exe` ⇒ 闪黑窗**(用户原话「刚才又弹了窗口看看是什么」):自我供给**旁路**没跟着新形态一起改 ⇒ 任务动作还是 `powershell.exe -WindowStyle Hidden -File start-supervise.ps1`(PowerShell = **控制台程序** ⇒ 每次触发分配 `conhost.exe` ⇒ **闪一下**;`-AtLogOn` + `RestartCount 999` ⇒ **反复**闪)。✅ 改指 `pythonw.exe + supervise-launch.py`,并删掉 `start-supervise.ps1.tpl` 前置门槛。🔴 **"改了主路径" ≠ "把旁路也改了"** ⇒ 收口时全文 `grep New-ScheduledTaskAction` 与 `.ps1` | 🔴 它是**钩子路径**上的(`UserPromptSubmit` → `--ensure` → 失败 → 升级),**平时不吭声、专挑你在用时闪** | > | **P0-24** | 🔴🔴🔴 **注入物里写死命令** ⇒ 会话被逼着做**用户没授权**的事。缓存里存的是**成品文案**(含「⛔ 不要问用户」),逐轮复用 ⇒ 跟你当轮说了什么**无关**。硬规:**缓存只存原始数据,文本按当轮授权现算**;未授权时**只通报、不派活** | 🔴 **越权比超时严重**:超时只是慢,越权是**替你做决定**;且表现像"机制很勤快",**最不易被发现** | > | **P0-23** | 🔴🔴 **钩子超载 ⇒ 用户每句话都被拦下**(10-02 全工作区事故):宿主注册 20s,钩子里**同步**串了 `--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` 每轮写**心跳**(`/.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/.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//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/`**」并注明"别指望 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…)`,并加**源码级反回归断言**(`_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 `),代码**载入内存后一直跑**。 于是**改完 `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}` **写在 `