- 变更规模:新增 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/ 知识文件,按口径入库)
32 KiB
踩坑清单(每条都真实发生过)
🔴 当前结论(先读这里 · 最后更新 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 是毫秒级写小文件,且棒通常不会精确同时上报 ⇒ 实际概率低,但不是零。
✅ 立刻可用的规避(⛔ 不改代码):上报后回读核对 ——
<python> collabd.py --report N9 --state done --by "[协作]-…" --line <线> --artifact <产物>
<python> collabd.py --reqs # ← 回读:确认自己那条在、状态对(防被别的上报覆盖)
⚠️ 若回读发现自己那条被覆盖/丢失 ⇒ 立刻重报(把它写回去),并在上报里提一句。
🔴 未解决(如实登记):并发写锁还没做。
⚠️ 我 2026-09-30 06:06 试过一次(换成 OS 级文件锁 msvcrt.locking + 给 6 处 state 写加锁)⇒ 导致 --once 与 --report 全部卡死(rc=124 超时) ⇒ 已从备份回退(tmp/bak-concurrency-20260930-060649/)。
⇒ 结论:这个改造必须在"隔离环境先验证 msvcrt.locking 行为"之后再做,⛔ 别在"用户在等"的状态下赶工(本次教训)。
⇒ 正确的下一步:① 先写一个两进程并发压测脚本(隔离目录)② 验证锁真能互斥且不卡 ③ 再改进生产。
P0-7 🔴🔴 宿主推给前端的「会话元数据」是陈旧缓存** ⇒ 前端与实际不匹配 ⇒ 用户消息静默蒸发(★ 2026-09-30 实测定型 · 用户凭直觉指出,被证实)
- 症状(用户原话):「我发消息发不出去卡住,看上去是发出去了 实际没有(这种情况消息应该出现在待发送框中)」
- 用户的关键判断(✅ 被证实):「我的感觉是会话状态不对,导致前端界面和会话实际动作不匹配」
取证链(三条,全部实测)
| # | 读数 | 说明 |
|---|---|---|
| ① 消息确实丢了 | 转录里搜用户原话关键词 ⇒ 只有他重发的那条(06:01),05:57–06:00 那条完全不存在;同期 PromptIterator 也没有 |
⛔ 不是队列(不是 park)、⛔ 不是服务端丢 ⇒ 丢在「客户端 → 宿主」这一跳 |
| ② 前端拿到的是旧元数据 | 宿主 [AcpView] Sent session_info_update with title: **用powershell 运行 试试呢**而 DB 里 sessions.title = 接续 · 机制线(钩子锚点真实投递取证) |
🔴 不一致 |
| ③ 且长期停在旧值 | 05:51:37 / 05:53:04 / 05:54:16 / 05:57:51 / 06:02:15 ⇒ 五次推送全是同一个旧标题 | 不是瞬时抖动,是缓存陈旧 |
⇒ 结论(⛔ 严格划清"可证"与"未证" —— 别学 P0-2 的老毛病)
| 内容 | 状态 | |
|---|---|---|
| ✅ 可证 | ① 用户那条消息确实丢了(转录、PromptIterator 双无)⇒ 丢在「客户端 → 宿主」这一跳(服务端无任何记录)② 宿主推给前端的元数据是旧的:推送 title=旧值,DB sessions.title=真值,五次推送同值 |
已实测 |
| ⚠️ 未证 | ② 是否就是①的原因("陈旧元数据 ⇒ 发送走偏 ⇒ 蒸发")—— 我没有任何直接证据 | 🔴 ⛔ 不得当结论说(这正是 P0-2 的教训:相关性 ≠ 因果) |
源码级补证(app.asar 实读):session_info_update 的 schema 定义写着是 "update session information like title"、由 agent 侧推送(⛔ 不是前端自己算的)
⇒ 所以"推旧值"确实不对(它本应反映当前会话名)⇒ 但"它导致了消息丢失"仍未证。
- 🔴 归属(如实):这是产品侧缺陷(宿主 ↔ 前端的状态同步)。 ⛔ 机制侧看不到、也修不了(消息根本没到服务端 ⇒ 服务端无任何记录 ⇒ 对账也发现不了)。 ⇒ 机制侧唯一能做的是降低触发概率(例如本次已把心跳从"定期噪音"改成"真停滞才发",减少主会话 busy 占比)。
- ✅ 可做的缓解:重启 WorkBuddy(清掉陈旧元数据缓存)。
⚠️ 代价:会杀掉所有会话后台任务(覆盖网络节点中继客户端/设备接入本地反代/投递守护)⇒ 重启后必须按
deploy.md §5b重起那两条腿。 - 📌 上报要点(给产品):附 ①转录缺失 ②
session_info_update与sessions.title的对照 ③五次同值的推送时间线。 - 🔑 推广:"消息发出去了但没到",先查三处——① 转录有没有(有没有进会话)②
PromptIterator有没有(有没有进队列)③ 宿主推给前端的元数据是不是旧的(前端与实际是否一致)。⚠️ ⛔ 别只数"成功的条数"就下结论"没丢"(本次我犯过:只统计到 14 条全成功,却没核对"应该有多少条")。
P0-6 🔴 宿主 SafeDelete 护栏会"杀掉"高频删文件的常驻进程 —— 心跳时钟断掉的真正原因(★ 2026-09-30 01:50 实测)
- 现象:常驻投递进程(已停用)跑约 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 connectionId303 次,其他小时 0 次 ⇒ 客户端连接状态在该小时反复丢失。 - 与相邻几型的区别:②f 有中断证据 · ②g 工具在循环 · ②h 干完了推不出去 · 本型=消息在队列里、没有消费者(
workbuddy-session-forensics §2i)。 - ⚠️ 窗口里可能同时叠着另一层:本次 22:58:40–22:59:50 还有
diagnostic log write failed(droppedLines496→2580)—— 那是日志层, ⛔ 它不解释"消息卡住",别把两层混成一个因(同类错误见 P0-2 的教训)。 - 🔴 对"程序化投递"的直接后果(本机制必须知道):经
/api/v1/acp/网关reply投进去的消息,本来就没有 UI 等待者 ⇒ 天然带hasWaiter=false风险。⇒ "往正在执行的会话投要延后"(target-busy)+"同一内容成对重复投要拦"(wake.lock+哈希+wake_min_gap)不是可选项。 ⚠️ 但本次这 3 条是adopt upstream promptRequestId,⛔ 不是本机制投的(wakeups.jsonl显示本机制当日最后两次投递是 22:51:01)—— ⛔ 别把这次卡住算到投递头上(教训同 P0-2:先要实物,再定因果)。 - 处置:① 先按指纹确认是不是这一型;② 是 ⇒ 让客户端重新挂上该会话(切走再切回/重开该会话窗口)通常即可排空; ③ ⛔ 别反复发消息试探(那只会往队尾再堆一条,延长卡住时间)。
P0-5a 🔴 探针⛔ 不许"在日志里搜字符串" —— 它会命中你自己的取证回声(★ 2026-09-30 当场踩到)
- 现象:刚做好的 park 探针立刻报了一次假命中(
park=1 次 最近 00:36:15),而那一刻并没有任何会话在卡。 - 根因:取证命令的输出会被写进同一份工作区日志 —— 我为了查这个坑跑了一次
grep … parkInQueue …,宿主把它记成[SandboxShell] ProcessOutput … content=… route=parkInQueue … hasWaiter=false。 探针只要"行里同时含这两个子串"就命中 ⇒ 命中了自己的回声。 - 修法:判据必须只认真正的记录行,并显式排除回显行:
def _is_park_line(ln): return ("parkInQueue" in ln and "hasWaiter=false" in ln and "[AcpView][PromptIterator] received prompt" in ln and "ProcessOutput" not in ln and "content=" not in ln) - 🔑 推广(比这条本身重要):任何"扫日志找关键字"的探针都有这个自污染风险 —— 只要有人(或你自己)为了排查而把那个关键字
打进日志一次,探针就会永久看见它。⇒ 一、认结构不认词(要求"记录行"的固定字段组合);二、排除回显容器
(
ProcessOutput/content=/Sandbox);三、必须先拿一条真记录 + 一条回声行做对照用例(已固化进selftest.py)。
P0-5b 🔴 本轮新增的两道自动护栏(collabd.py --tick)
| 护栏 | 做什么 | 判据(⛔ 不看 rc,看这个) |
|---|---|---|
宿主卡住探针 probe_host_park() |
每轮只读日志尾部 400 KB,找 park 指纹(最近 30 分钟内才算)⇒ 落 NEED-USER.md(含"切走再切回"),节流 10 分钟 |
打印 tick: … park=<n> 次 最近 <HH:MM:SS> |
投递消费回查 check_delivery_consumed() |
投递成功后不当作成功:4 分钟(CONSUME_GRACE)内目标会话 updated_at 没晚于投递时刻 ⇒ 判「没被消费」⇒ 落 NEED-USER.md |
打印 tick: … consume=waiting/consumed/unconsumed |
🔴 口径:「投出去」≠「它跑起来了」 —— 网关回 200 只证明对方收下,不证明有人执行。 ⚠️ 这两条护栏只能"发现 + 告诉用户点哪一下";根因(宿主客户端连接状态丢失)AI 侧修不了,⛔ 不许因此承诺"自动恢复"。
P0-2 🔴 「卡消息输出」的真因:会话日志撞上限被 dropped —— ⛔ 与"后台任务归属"无关(2026-09-30 用户反证后重写)
- 现象:会话里发消息后窗口不显示内容 / 像是没反应;用户看到的是"卡消息输出"。⚠️ 我自己(主会话
fe146dd9)就是当事人。 - 🔴 实证真因(实物在档,⛔ 不是推断):宿主的会话对话日志撞 ~10 MiB 上限后开始丢事件。
· 该会话日志尾部原文:
diagnostic-log:dropped {"droppedLines":14261,"droppedBytes":1731465}(UTC 12:06:44 = 本地 20:06:44); · 其前最后一批正常记录是同一requestId的tool_call_update在毫秒级反复落盘(本地 16:29:53,completeAssistantStream:false); · 该文件 10,485,606 字节,本地 23:00 被 logcap sweep 挪为*.log.stuck-20260929-230043;同日全机共 6 个这类满额文件。 ⇒ 高频工具事件 / 大量输出 ⇒ 日志膨胀过上限 ⇒ 宿主丢弃 ⇒ 对话不再显示。 - ⛔ 已作废的旧结论(我 2026-09-29 写的,留着会误人):曾断言"任务记在会话名下 ⇒ 宿主认为该会话一直有长跑任务 ⇒ 拖住它",
而当时唯一的"证据"只是"起守护的任务 id 随停工变
completed"—— 那只说明任务结束,⛔ 不构成因果。 🔴 用户反证(2026-09-30):mcn-short-video的:8900就是会话后台任务,一直挂着;用户在那个会话里照样随便发消息。 - ✅ 正确口径:卡不卡看"输出/事件量",与"是不是后台任务"无关。
⇒ 会话后台任务只要完全静默(
stdout重定向到文件、⛔ 不往标准输出打长跑日志)就可以安全长期运行。 ⇒ 会话内后台任务 vs 会话外起,在"会不会拖累会话"这一项上没有差别(旧表作废)。 - 处置(日志已撞上限时):把
<sid>.log改名挪开(⛔ 不删),宿主数十秒内重建并恢复写入 ⇒ 详见技能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/ PowerShellStart-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 ⛔ 反复重启常驻程序 ⇒ 把主会话自己弄卡(最容易被忽视的一条)
- 现象:每次进入"改常驻程序"的阶段,主会话就变卡、被反复打断(用户原话:"为什么每次一改到这里就把自己的会话弄卡")。
- 根因(两条叠加 + 一条次因):
- 🔴 用会话后台任务起常驻 ⇒ 该进程成为本会话的附带物;每次 kill / launch 都产生一条
failed任务通知 ⇒ 每次都打断会话。实测:一个阶段里重启 7~8 次 ⇒ 7~8 次打断。 - 🔴 同一会话里做了 500+ 次工具调用 + 反复读写长文件 ⇒ 上下文极大 ⇒ 每轮推理显著变慢。
- ⚠️ 注入钩子每轮往上下文加 ~1KB(应压在几百字符)。
- 🔴 用会话后台任务起常驻 ⇒ 该进程成为本会话的附带物;每次 kill / launch 都产生一条
- 修法:
- 攒批重启:代码改动攒到一次再重启(≈3 处以上/或等功能自测全过)。⛔ 不要"改一行重启一次"。
- 优先热加载:把规则/配置/任务图/队列做成程序每轮读文件 ⇒ 改这些根本不用重启。
- 长任务换会话:一个会话不要既"建系统"又"跑长验收" ⇒ 到阈值就交棒接续(这正是本机制存在的意义)。
- 注入瘦身:
additionalContext压到几百字符。 - 🔴 改成"拉取模型"(最根治):程序只写队列/文件,⛔ 不调用会话、不通知、不起后台任务;会话在"用户发话 / 棒收尾 / 需要时"三个时机主动拉队首 ⇒ 没有"推"就没有打断。详见
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跳过。 - 修法:
- 🔴 "能自动跑起来"的路只有一条 = 钩子(每个会话收尾跑一轮、零 token、不占会话)⇒ 把机制的关键环节挂在这条路上,⛔ 别押在"常驻一直活着"上。
- 钩子必须指向最新那份(含队列/唤醒);同目录⛔ 不要留旧副本,或让旧副本显式转调新版(fail-open + 静默 + 超时,⛔ 不改自己原有行为)。
- 认口=「第一个带活会话的口」,⛔ 不是
uptime最大者(实测两者常不同)—— 同 cwd 多实例时,这条正是投得出去 / 投不出去的分水岭。 - 判"接线成立"看产物:每轮覆写的视图/队列/状态文件 mtime 是否跟着"会话收尾"推进 —— 比读代码可靠得多(本节即靠这一条定位的)。