- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次) - .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…), 目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪 - .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/) - .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*) - .gitignore 补:备份件(*.bak-*) - 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
317 lines
32 KiB
Markdown
317 lines
32 KiB
Markdown
# 踩坑清单(每条都真实发生过)
|
||
|
||
> ## 🔴 当前结论(**先读这里** · 最后更新 2026-09-30 05:55)
|
||
> ⚠️ 本文件**通篇追加式** ⇒ 旧条可能已被取代(已就地标注)。**冲突时以「本节 + `architecture.md` 的「当前结论」节」为准**;
|
||
> 历史只留最近 5 轮、更早的归档(例外:**教训类不受 5 轮限制** —— 见 `agent-operating-rules §1.7a`)。
|
||
>
|
||
> **最该先记住的 5 条**(其余按 P 编号往下读)
|
||
> | 编号 | 一句话 | 为什么它排前面 |
|
||
> |---|---|---|
|
||
> | **P0-6** | 常驻进程 **⛔ 别做高频删文件**(`unlink`/`rename`)⇒ 宿主 **SafeDelete 护栏会直接杀掉进程**(实测:跑 48m43s 后 `failed`,stdout 只有一行 `SAFE_DELETE_BULK_CONFIRM_REQUIRED`) | **心跳时钟就是这么断的** |
|
||
> | **P0-5** | 「**消息卡住**」指纹 = **`parkInQueue` + `hasWaiter=false`** | 🔴 **AI 侧修不了**,只能让客户端重挂该会话 |
|
||
> | **P0-2** | 「卡消息输出」真因 = **会话日志撞 ~10 MiB 被 `dropped`**(⛔ **不是**"任务挂在会话名下") | 曾误归因,白折腾一晚上 |
|
||
> | **P0-5a** | 探针 **⛔ 不许"在日志里搜字符串"** ⇒ 会命中**你自己的取证回声** ⇒ 假阳性 | 认结构(记录行),⛔ 不认词 |
|
||
> | **P0** | 所有 `subprocess` 必须带 **`CREATE_NO_WINDOW`** | 否则桌面反复闪黑窗 |
|
||
|
||
## 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 🔴🔴 **投递⛔ 不许靠"加一条排期/常驻"**(用户:"又给我整到自动任务去了")
|
||
- **现象**:反复把"怎么把通知送到主会话"这件事,收敛成"**开一条自动化排期**"或"**让它常驻**"。
|
||
用户对此明确不满(原话:**「又给我整到自动任务去了」**)—— 这正是架构里 §6「必须活着的进程数 = 0」要守的那条线。
|
||
- **根因(我当时的错推理)**:我把"**投递要有口令**"当成了"**必须有宿主起的进程常驻**"。
|
||
⇒ 真因是**我把可选手段当成了唯一手段**:**宿主钩子也是宿主起的子进程** —— 它**同样有口令**,而且**⛔ 不占会话、零 token、跑完即退**。
|
||
- **为什么"事件驱动"就够**(覆盖度,⛔ 不是拍脑袋):**需要投递的每一个时刻,都必然伴随某个会话在动** ——
|
||
| 需要投递的时刻 | 谁产生 | 钩子会响吗 |
|
||
|---|---|---|
|
||
| 协作会话上报(执行中/完毕/有阻碍) | 协作会话跑 `--report`(一次 **Bash** 调用) | ✅ `PreToolUse ^Bash$` |
|
||
| 主会话处理完上一条 ⇒ 发下一条 | 主会话的收尾 | ✅ `PreToolUse` + `UserPromptSubmit` |
|
||
| **停滞心跳**(谁都没动) | 需要**时钟** ⇒ **唯一真缺口** | ⚠️ ❌ ⇒ 会话外**只落标记**,下次任意钩子触发时**补投** |
|
||
- **修法**:✅ **投递 = 宿主钩子唤起投递跑一轮**(`collabd.py --tick`);⛔ 不排期、⛔ 不常驻。
|
||
⚠️ **代价如实讲**:真正"全员静止"期间通知不会自己飞出去(要等下一次任意宿主事件)—— 这是**为摆脱排期而明确接受**的代价。
|
||
- 🔑 **推广**:**先问"这个能力宿主已经在哪里提供了?"再问"要不要为此新增一个常驻/排期"**。⛔ 新增"必须活着的东西"永远是最后选项。
|
||
- ⚠️ **"那用会话后台任务当守护+投递行不行?"(2026-09-30 用户追问)⇒ ✅ 能用,是最佳次选。**
|
||
🔴 **我起初答"不行",三条理由被用户当场逐条反证**("占会话会卡"被他用 `:8900` 反证;"跨不了重启"被他指为边界而非缺陷;"输出唤醒"实为输出量问题)
|
||
⇒ 详见 **`architecture.md §4.1.1`(含一次自我纠错)**。
|
||
**仍是选钩子**的真实理由只剩三条**次要优势**:⛔ 不需要容器会话 · **必须活着的进程数 = 0** · 重启后自动生效;
|
||
⚠️ 而**后台任务在"投递延迟可控"上更强** ⇒ 若钩子延迟不可接受,上"专用容器会话 + 完全静默的后台任务"是很小的一步。
|
||
|
||
## 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 实测)
|
||
|
||
- **现象**:常驻投递进程(已停用)**跑约 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)` ⇒ 绝大多数轮次都在空转取锁+删锁。
|
||
- **修法(已落)**:把 **hash / 最小间隔的预判提到取锁之前** ⇒ 高频的 `same-item` / `too-soon` 路径**根本不碰锁文件** ⇒ 删除次数降到"只在可能真投时"。
|
||
⚠️ 取舍如实登记:锁内**仍会重读 STATE 再判一次**(那才是权威判据),竞态窗口由 `wake_min_gap`(300 s)兜底 ⇒ ⛔ 不会双投。
|
||
- 🔑 **推广(比本条重要)**:**常驻进程里 ⛔ 别做高频的"文件删除/改名/清目录"**(`unlink` / `rename` / `rmtree`)——
|
||
护栏按**次数**计,⛔ 不按"意图好坏"。要"释放/标记"优先**改内容或改时间戳**(写标记),⛔ 不用删文件;
|
||
非删不可 ⇒ 改成**低频/惰性**,并把"删了多少次"记进自己的日志(否则你只会看到"莫名 failed")。
|
||
- **复发信号(下次一眼认)**:① stdout 出现 `SAFE_DELETE_BULK_CONFIRM_REQUIRED`;② 常驻**莫名 failed 且 stderr 为空**。
|
||
⚠️ 进程被杀时 `wake.lock` 会**残留**(本次 01:49 留了一个)—— 它 >120 s 会被自动抢占,⛔ 不必手删。
|
||
|
||
## 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 干完了推不出去 · **本型=消息在队列里、没有消费者**(`workbuddy-session-forensics §2i`)。
|
||
- ⚠️ **窗口里可能同时叠着另一层**:本次 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` 改名挪开(⛔ 不删),宿主数十秒内重建并恢复写入 ⇒ 详见技能 `workbuddy-session-forensics §2h-1`。
|
||
- 🔴 **⚠️ 本条先后被我改错过两次**:① 曾把"投递"写成"一条低频排期"(已由 **P0-3** 纠正);② 曾把"卡消息输出"归因到"后台任务归属"(本次纠正)。⇒ **写根因前先拿实物,⛔ 别拿"相关性 + 一个弱信号"当因果。**
|
||
|
||
## P0 ⛔ 子进程不加 `CREATE_NO_WINDOW` ⇒ 桌面**反复闪黑窗**(用户:"一会弹出来一会弹出来的,影响我操作电脑")
|
||
- **现象**:守护/协作程序跑起来后,桌面每隔 10~30 秒**闪一个控制台窗口**。
|
||
- **根因**:程序里有 `subprocess.run(["netstat", "-ano"], stdout=PIPE, …)`(**认网关端口**用)——
|
||
**`netstat` 是控制台程序**,不带 `creationflags=0x08000000` 就会**新建一个控制台窗口**;而该函数**每轮都被调**(协作程序 ~10 s/投递 ~30 s)⇒ 桌面反复闪。
|
||
- **修法**:✅ **所有** `subprocess` 调用一律带 `creationflags=0x08000000`(CREATE_NO_WINDOW)。
|
||
⛔ **只重定向 `stdout`/`stderr` 是没用的** —— 窗口照样会建。
|
||
- **验收**:桌面从"反复闪"变为"零窗口";**功能侧**用"能否照常认出网关端口"核对(日志/`wakeups.jsonl` 仍有记录)。
|
||
- 🔑 **推广**:任何**长期循环**里的外部命令调用,先过一遍"**它会不会开窗**"。
|
||
|
||
> `multi-session-collab` 技能 · 详情档。**主干判据在 `SKILL.md §6`**,本档给**现象 / 根因 / 修法**。
|
||
|
||
## P1 ⛔ 用「会话后台任务」跑常驻 ⇒ 宿主"卡死"
|
||
|
||
- **现象**:用户说"卡住了/发消息不恢复";会话永不回 idle。
|
||
- **根因**:会话后台任务**每轮输出都会唤醒宿主会话**。
|
||
- **修法**:🔴 **首选=根本不要常驻** —— 由**宿主钩子按需唤起**一次性进程(见 **P0-3**)。
|
||
⛔ 旧建议"用 `run_in_background` 起常驻"是**错的**:那正是 **P0-2**(任务记在该会话名下 ⇒ 该会话卡输出)。
|
||
真需要常驻时,只能**会话之外**起(启动文件夹/独立窗口),并接受它**拿不到口令 ⇒ 只能落盘、不能投递**。
|
||
- ⚠️ **`cmd start` / `wmic process call create` / PowerShell `Start-Process` 常被安全策略拦** ⇒ ⛔ 别在"独立起进程"上耗时间。
|
||
|
||
## P2 ⛔ 自愈没有去抖 ⇒ 反复杀掉正在服务的好实例("自愈比故障更伤")
|
||
|
||
- **现象**:监控到的端口**反复通/断**;日志里"launching → 又被杀"循环。
|
||
- **根因**:启动器常有**幂等闸**("进程活着但端口不通 ⇒ 杀掉旧的再拉一份");若守护进程**每次探测失败就触发**,就变成**抖动**,把可能正在服务的好实例打死。
|
||
- **修法**:**连续 N 次(≥3)失败才动手** + **冷却(数百秒)** + 探测用**socket 连一下**(⛔ 别发真请求)。
|
||
|
||
## P3 ⛔ 粗粒度域锁 ⇒ 自造串行瓶颈,整轮白开
|
||
|
||
- **现象**:别的会话**抢锁失败 ⇒ 什么都不做就退出**(纯浪费的会话)。
|
||
- **根因**:把**整个工作区**声明成一个域 ⇒ 所有写操作串行。
|
||
- **修法**:**按节点涉及的文件/子目录声明域**;不重叠即可并行。**机制层才独占**。
|
||
|
||
## P4 ⛔ `fail-open` 掩盖字段名错误 ⇒ 视图静默为空
|
||
|
||
- **现象**:`rc=0`、日志无异常,但**某个区块一直是空的**。
|
||
- **根因**:异常被 `except: pass` 吞掉;查询写错列名(真实例:给 `sessions` 查了不存在的 `name` 列)。
|
||
- **修法**:① **必须核对输出非空**(⛔ 不能只看退出码)② 关键查询先 `pragma table_info(<表>)` 核对列名 ③ 定期 `diff` 视图一眼。
|
||
|
||
## P5 ⛔ 把"接续任务"当成果 ⇒ 被"空转链条"骗
|
||
|
||
- **现象**:看起来一直在推进("下一棒已派"),实际**总工期不动**。
|
||
- **根因**:**排期 ≠ 成果**。
|
||
- **修法**:**证据分级**(真成果/接续任务/刚开跑/哑火);"在跑的棒 N 个"是排期数,⛔ 不是成果数。
|
||
|
||
## P6 ⛔ 用"加自动化"回应一切需求 ⇒ 会话洪泛
|
||
|
||
- **现象**:一个需求挂上 7 个自动化,其中 5 个同用途。
|
||
- **修法**:**白名单+确认制**(只有"接续会话/派活"可免确认)+ **配额 ≤2**。建前自问:**非得开新会话吗?已有的能不能覆盖?**
|
||
|
||
## P7 ⚠️ 一次性自动化跑完 `status` 仍为 ACTIVE ⇒ 统计虚高
|
||
|
||
- **现象**:数"在挂的棒"= 6,实际只有 2 个真在等。
|
||
- **修法**:**一律用 `next_run_at > now` 过滤**;⛔ 不看 `status` 单独判断。清理僵尸旧件(已跑完的 once)。
|
||
|
||
## P8 ⛔ 诊断数据不落盘/不落库 ⇒ 只能靠"自述"
|
||
|
||
- **根因**:把"谁干完了、结论是什么"寄托在文件扫描/人报告上。
|
||
- **修法**:**读宿主已落的运行记录**(`automation_runs.thread_title` 等)⇒ **0 token、不轮询**。
|
||
- ⚠️ 但要**核对该列是否真有内容**(`IN_PROGRESS` 时标题可能为空 ⇒ 判"在跑"看 `status`)。
|
||
|
||
## P9 ⛔ 校验"可用"时只看"能跑起来"
|
||
|
||
- **现象**:结论写"八项判据起停各一遍全绿",但**端口此刻并不通**。
|
||
- **判据**:**"能跑起来" ≠ "可用"**。**可用 = 现在这一刻服务可达,且能被维持住**。
|
||
|
||
## P10 ⚠️ 长前台命令被沙箱杀,连带杀掉先前后台起的进程
|
||
|
||
- **现象**:命令无输出、`Exit -1`;随后发现后台进程也没了。
|
||
- **修法**:**单条前台命令控制在 ~90 秒内**;长活**拆短步**或**走异步**。
|
||
|
||
## P12 ⛔ 反复重启常驻程序 ⇒ **把主会话自己弄卡**(最容易被忽视的一条)
|
||
|
||
- **现象**:**每次进入"改常驻程序"的阶段,主会话就变卡、被反复打断**(用户原话:"为什么每次一改到这里就把自己的会话弄卡")。
|
||
- **根因(两条叠加 + 一条次因)**:
|
||
1. 🔴 用**会话后台任务**起常驻 ⇒ 该进程成为**本会话的附带物**;**每次 kill / launch 都产生一条 `failed` 任务通知** ⇒ **每次都打断会话**。实测:一个阶段里重启 **7~8 次** ⇒ **7~8 次打断**。
|
||
2. 🔴 **同一会话里做了 500+ 次工具调用** + 反复读写长文件 ⇒ 上下文极大 ⇒ 每轮推理显著变慢。
|
||
3. ⚠️ 注入钩子每轮往上下文加 ~1KB(应压在几百字符)。
|
||
- **修法**:
|
||
1. **攒批重启**:代码改动**攒到一次**再重启(≈3 处以上/或等功能自测全过)。⛔ **不要"改一行重启一次"**。
|
||
2. **优先热加载**:把**规则/配置/任务图/队列**做成**程序每轮读文件** ⇒ 改这些**根本不用重启**。
|
||
3. **长任务换会话**:一个会话**不要既"建系统"又"跑长验收"** ⇒ 到阈值就**交棒接续**(这正是本机制存在的意义)。
|
||
4. **注入瘦身**:`additionalContext` 压到几百字符。
|
||
5. 🔴 **改成"拉取模型"**(最根治):**程序只写队列/文件,⛔ 不调用会话、不通知、不起后台任务**;会话在"用户发话 / 棒收尾 / 需要时"三个时机**主动拉**队首 ⇒ **没有"推"就没有打断**。详见 `SKILL.md §1.3`。
|
||
|
||
> 🔑 **一句话**:**"改代码 → 重启常驻 → 产生失败通知 → 打断自己"这个循环,是主会话变卡的机制性原因**,而不是外部故障。
|
||
> ⇒ **"拉"取代"推",是这个问题的根治解。**
|
||
|
||
|
||
- **现象**:`SyntaxError: unterminated string literal`。
|
||
- **根因**:在双引号字符串里写了 ASCII 双引号。
|
||
- **修法**:文案里的引号一律用 **`「」`**;改完 **`ast.parse` 校验**(比 `py_compile` 报错更清楚)。
|
||
|
||
## P13 ⛔ 常驻"活不长" ∧ 钩子指向无唤醒码的旧副本 ⇒ 机制**静默停摆**(最阴的一条:没人会发现)
|
||
|
||
- **现象**:唤醒回路代码写完、也实测到 1 次真投递,但此后**再也不投**。查下去:常驻进程**已消失**、单例端口 `Connection refused`、日志停在某一刻;而"唯一会自动跑"的钩子那条路,跑的是**另一份旧的精简副本**(没有队列闸门、没有唤醒码)⇒ 整个机械层**静默停摆**,且**没有任何告警**——因为告警也是那个程序发的。
|
||
- **根因**:① **从会话里起的常驻会随其宿主会话结束被回收**(实测:`13:09` 起的 pid,`13:12` 之后再无一轮);② **同目录另存过一份旧副本且钩子指向它** ⇒ "代码在 A、钩子在 B" ⇒ **接了线等于没接**;③ 认口取"`uptime` 最大"的口,而**那个口可能没有活会话** ⇒ 有活会话也照样落 `no-live-session` 跳过。
|
||
- **修法**:
|
||
1. 🔴 **"能自动跑起来"的路只有一条 = 钩子**(每个会话收尾跑一轮、零 token、不占会话)⇒ **把机制的关键环节挂在这条路上**,⛔ 别押在"常驻一直活着"上。
|
||
2. **钩子必须指向最新那份**(含队列/唤醒);**同目录⛔ 不要留旧副本**,或让旧副本显式**转调**新版(fail-open + 静默 + 超时,⛔ 不改自己原有行为)。
|
||
3. **认口=「第一个带活会话的口」**,⛔ 不是 `uptime` 最大者(实测两者常不同)—— 同 cwd 多实例时,这条正是**投得出去 / 投不出去**的分水岭。
|
||
4. **判"接线成立"看产物**:每轮覆写的视图/队列/状态文件 **mtime 是否跟着"会话收尾"推进** —— 比读代码可靠得多(本节即靠这一条定位的)。
|