-
泄漏被修过一次(310a2b2 / 3e718aa),修完照样漏。第二次排查发现原因不是修错了,而是: 系统里有两个互相独立的会话池,上一次只修了其中一个;而被修的那个,还漏了一条分支。 池 A routes/agent-chat.js 的 sessions ← /api/agent/:id/chat,含两路心跳 池 B src/core/session-pool.js 的 SessionPool ← AgentManager.callAgent,图执行 / DevLoop 两者没有任何共享代码,回收逻辑各写各的。这个结构性事实此前没有任何一处记着, 所以第二个人查到同一个症状时会重走一遍全程 —— 这份文档就是为了让第三个人不必再走。 正文八节,除了两个洞各自的成因与修法(b0b2b62 / 2092a2e),有三处是排查方法而非结论: §三 按 cwd 第三层判断孤儿归属的对照表。两个池子都把 cwd 下沉到运行态目录, `_global` / `_no-task` 是 agent-locator 的保留缺省值、两边都会出现,不能靠它区分。 §四 为什么上一版守卫是瞎的。dropSession 和 closeSession 都会让会话从池子里消失, 黑盒探针两种情况都返回 false —— 差别只在子进程死没死,而那是探针看不见的维度。 选探针要问的不是「能不能测出正确行为」,而是「能不能区分正确行为和这个具体的错误行为」。 §八 三步定位脚本,外加一条:先确认跑着的服务器进程是什么时候启动的,再去看代码。 本次排查中,正在跑的实例比修复 commit 早 18 秒启动,代码里有的修复它并没有。 线上验证(PID 63099,13 分钟窗口两轮心跳):触发 5 / 完成 5 / 回收 5,峰值子进程 3、稳态 0。 17:13:32 那个 3 正是泄漏时代的签名(cto+coo+qa-judge 同秒触发),清理前那批孤儿一个都不回收, 现在 30 秒内全部释放。窗口内没有失败心跳,所以 2092a2e 那条 catch 路径是靠单测的变异检验 保证的、不是靠线上观察 —— 文档 §六 里写明了,没算成已验证。 docs/README.md 同步加索引条目(no-dead-path-references 守卫 4 条全绿)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>