Files
dsh_ai1net_server/归档/技能-退役-20261001/multi-session-collab/references/pitfalls.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 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/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

32 KiB
Raw Blame History

踩坑清单(每条都真实发生过)

🔴 当前结论(先读这里 · 最后更新 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 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。 探针只要"行里同时含这两个子串"就命中 ⇒ 命中了自己的回声。
  • 修法:判据必须只认真正的记录行,并显式排除回显行:
    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 是否跟着"会话收尾"推进 —— 比读代码可靠得多(本节即靠这一条定位的)。