起因是「全量测试偶发挂一条」。查下来既不是回归、也不是那 45 个孤儿进程(它们 CPU 早归零、
不再写任何东西),而是**还活着的心跳在测试扫描 system/agents/ 的同时往里建目录**:
locator.taskDir() 的落点是 system/agents/<agent>/<companyId>/<taskId>,
运行态目录落在本体目录里属既定设计;但心跳每 5 分钟建一个新的
(cto/_global/ 已 263 个子项且在涨),于是扫描 system/agents/ 的守卫测试
(no-dead-path-references 那一类)会撞上瞬时竞态:列到了目录名,进去时那一层还没写完。
症状是偶发假失败、重跑就好 —— 这类最费人,因为第一反应总是去怀疑自己刚改的代码。
改法:server/index.js 启动的那个 setTimeout 块里,恰好装着全部「会自己起 agent 的东西」
(两处中断恢复 + 任务队列扫描 + 两路心跳),所以一条早退就够,不必在五个地方各加一个判断。
DISABLE_BACKGROUND_AGENTS=1 bash start.sh
实测:127 文件 / 1226 用例全绿,跑前跑后 cto/_global 子项数都是 263。
**验证它生效不能只看日志里的 ⏸** —— 那只证明代码走到了早退。硬判据是目录数前后相等,
也就是真的没有东西在写。文档 §七 把这条写成了可粘的三行。
排掉的两个坑,都记在文档和 .env.example 里:
1. `.env` 里不能给这个键赋空值。start.sh 是 `set -a; source .env` 加载的,.env 里只要
存在同名键(哪怕空的)就会盖掉命令行传进来的 =1,而且不报错。所以 .env.example 里
它只以注释形式出现 —— 一个「示例文件顺手写全」的习惯会直接让开关失效。
2. 开关生效后 `[TaskQueue] 🚀 已启动` 仍会打印,那是另一个模块:services/task-queue.js
(ArenaSK 时代 /api/claude-chat 的队列,routes/claude-chat.js 模块加载时起),
和批 3 的 services/task-scheduler.js 同名不同物。它的心跳只扫内存里的 this.tasks,
不碰 system/agents/,不是干扰源。名字撞车,容易误判成开关没生效。
文档同步:
- docs/done/claude-cli-session-leak.md §七 重写(原文写的是「得让测试隔离,或跑前停心跳」,
现在后者已实现,改成开关的用法 + 硬判据 + 上面两个坑);原第 2 条降为 §七之二
- CLAUDE.md 测试命令块加一行(这是找「怎么跑测试」的人真正会看的地方)
- docs/README.md 索引行补上开关
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>