Commit 401840b68818715d751930b7c7354435c9630ff7
1 parent
d8b4346d
feat(startup): DISABLE_BACKGROUND_AGENTS —— 跑全量测试前一条早退关掉所有会起 agent 的后台驱动
起因是「全量测试偶发挂一条」。查下来既不是回归、也不是那 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>
Showing
5 changed files
with
66 additions
and
7 deletions
.env.example
| ... | ... | @@ -41,3 +41,11 @@ export FEISHU_VERIFICATION_TOKEN=your_verification_token |
| 41 | 41 | export FEISHU_ENCRYPT_KEY=your_encrypt_key |
| 42 | 42 | export FEISHU_MODE=ws |
| 43 | 43 | # export FEISHU_ALLOW_FROM=ou_xxx,ou_yyy |
| 44 | + | |
| 45 | +# 跑全量测试前关掉后台驱动(两路心跳 / 任务队列扫描 / 中断恢复)。 | |
| 46 | +# 它们会往 system/agents/<agent>/<companyId>/<taskId>/ 建运行态目录, | |
| 47 | +# 扫描 system/agents/ 的守卫测试会被这个瞬时竞态打成偶发假失败。 | |
| 48 | +# 用法:DISABLE_BACKGROUND_AGENTS=1 bash start.sh | |
| 49 | +# | |
| 50 | +# 故意**不在这里赋空值**:start.sh 是 `set -a; source .env` 加载的, | |
| 51 | +# .env 里只要存在同名键(哪怕是空的),就会盖掉命令行传进来的 =1,而且不报错。 | ... | ... |
CLAUDE.md
| ... | ... | @@ -40,6 +40,10 @@ npm run test:unit # vitest run (CI 用) |
| 40 | 40 | npx vitest run path/to/foo.test.js # 跑单个测试文件 |
| 41 | 41 | npx vitest run -t "test name" # 按名称过滤单个测试用例 |
| 42 | 42 | |
| 43 | +# 跑全量前先起「不带后台驱动」的 server:心跳每 5 分钟往 system/agents/ 建运行态目录, | |
| 44 | +# 扫描该目录的守卫测试会被瞬时竞态打成偶发假失败(详见 docs/done/claude-cli-session-leak.md §七) | |
| 45 | +DISABLE_BACKGROUND_AGENTS=1 bash start.sh | |
| 46 | + | |
| 43 | 47 | # DevLoop 规范 |
| 44 | 48 | npm run spec:extract # 从 docs/prd/newfunction/*.html 重新生成 _spec/*.md(改完 HTML 后必须跑) |
| 45 | 49 | npm run spec:check # CI/pre-commit 用,校验 _spec/ 与 HTML 是否同步 | ... | ... |
docs/README.md
| ... | ... | @@ -17,7 +17,7 @@ |
| 17 | 17 | | [LEMONBOSS-SYSTEM-DESIGN.md](./done/LEMONBOSS-SYSTEM-DESIGN.md) | 系统设计主索引 v2.0,含 27 章 `system-design/` 的总目录 | |
| 18 | 18 | | [architecture-v5.md](./done/architecture-v5.md) | v5.3 架构方案,Phase 1-6 已完成(Phase 7 认知架构条目单独见 todo/) | |
| 19 | 19 | | [claude-code-cron.md](./done/claude-code-cron.md) | Claude Code 原生 Cron 调度功能参考手册 | |
| 20 | -| [claude-cli-session-leak.md](./done/claude-cli-session-leak.md) | **常驻 CLI 子进程泄漏排查手册** — ✅ 已修复(`b0b2b62` + `2092a2e`):系统里有**两个互不相干的会话池**(`routes/agent-chat.js` 的 `sessions` / `src/core/session-pool.js`),各漏各的。含按 cwd 判断孤儿归属的方法、变异检验为什么必要、三步定位脚本 | | |
| 20 | +| [claude-cli-session-leak.md](./done/claude-cli-session-leak.md) | **常驻 CLI 子进程泄漏排查手册** — ✅ 已修复(`b0b2b62` + `2092a2e`):系统里有**两个互不相干的会话池**(`routes/agent-chat.js` 的 `sessions` / `src/core/session-pool.js`),各漏各的。含按 cwd 判断孤儿归属的方法、变异检验为什么必要、三步定位脚本,以及 `DISABLE_BACKGROUND_AGENTS=1` —— 跑全量测试前关掉后台驱动的开关 | | |
| 21 | 21 | | [open-api-manual.md](./done/open-api-manual.md) | ArenaSK Open API 外部调用手册 | |
| 22 | 22 | | [qa_judge_req.md](./done/qa_judge_req.md) | QA Judge Agent 需求文档 — 已在 `system/agents/qa-judge/` 落地 | |
| 23 | 23 | | [task_timeline_design.md](./done/task_timeline_design.md) | Task Timeline View 设计文档 — 已实现(`server/routes/tasks.js`、`server/services/task-service.js`、`web/src/views/TaskTimeline.vue`) | | ... | ... |
docs/done/claude-cli-session-leak.md
| ... | ... | @@ -195,13 +195,46 @@ DevLoop 的 retry-with-reflection 复用同一个 taskId,一刀切关掉会砍 |
| 195 | 195 | |
| 196 | 196 | --- |
| 197 | 197 | |
| 198 | -## 七、仍未解决的(不在本次范围) | |
| 198 | +## 七、跑全量测试前先关后台驱动 | |
| 199 | 199 | |
| 200 | -1. **心跳持续往 `system/agents/` 写运行态目录**。`locator.taskDir()` 的落点设计就是 | |
| 201 | - `system/agents/<agent>/<companyId>/<taskId>`,属既定设计,不是 bug;但心跳每 5 分钟建一个新目录 | |
| 202 | - (`cto/_global/` 已 260+ 子项且在涨),扫描 `system/agents/` 的守卫测试会被瞬时竞态打红。 | |
| 203 | - 要跑干净的全量测试,得让测试隔离掉 `system/agents/`,或跑之前先停心跳。 | |
| 204 | -2. **机器级闸门不存在**。`SessionPool` 没有 `maxSessions`,全靠 TTL 兜底。登记在 `28-followups`。 | |
| 200 | +**这不是泄漏,是既定设计带来的测试干扰** —— 但它是本次排查的起点(「全量测试挂的那一条」), | |
| 201 | +所以记在这里。 | |
| 202 | + | |
| 203 | +`locator.taskDir()` 的落点就是 `system/agents/<agent>/<companyId>/<taskId>`,运行态目录落在 | |
| 204 | +本体目录里属既定设计。问题在于心跳每 5 分钟建一个新的(`cto/_global/` 已 263 个子项且在涨), | |
| 205 | +而扫描 `system/agents/` 的守卫测试(`no-dead-path-references` 那一类)会被瞬时竞态打红: | |
| 206 | +**列到了目录名,进去时那一层还没写完**。症状是偶发假失败、重跑就好,最难查。 | |
| 207 | + | |
| 208 | +```bash | |
| 209 | +DISABLE_BACKGROUND_AGENTS=1 bash start.sh | |
| 210 | +``` | |
| 211 | + | |
| 212 | +一条早退关掉全部「会自己起 agent 的东西」(`server/index.js` 的启动 setTimeout 块): | |
| 213 | +两处中断恢复 + 任务队列扫描 + 两路心跳。实测 127 文件 / 1226 用例全绿。 | |
| 214 | + | |
| 215 | +**验证它真的生效,别只看日志里的 `⏸`** —— 那只证明代码走到了早退。硬判据是目录数: | |
| 216 | + | |
| 217 | +```bash | |
| 218 | +ls system/agents/cto/_global | wc -l # 跑前 | |
| 219 | +npm run test:unit | |
| 220 | +ls system/agents/cto/_global | wc -l # 跑后:必须完全相等 | |
| 221 | +``` | |
| 222 | + | |
| 223 | +两个坑: | |
| 224 | + | |
| 225 | +- **`.env` 里不要给这个键赋空值**。`start.sh` 是 `set -a; source .env` 加载的, | |
| 226 | + `.env` 里只要存在同名键(哪怕空的)就会盖掉命令行传进来的 `=1`,而且不报错。 | |
| 227 | + 所以 `.env.example` 里它只以注释形式出现。 | |
| 228 | +- **`[TaskQueue] 🚀 已启动` 仍会打印,那是另一个模块**。`services/task-queue.js` | |
| 229 | + (ArenaSK 时代 `/api/claude-chat` 的队列,`routes/claude-chat.js` 模块加载时起) | |
| 230 | + 和批 3 的 `services/task-scheduler.js` 同名不同物。它的心跳只扫内存里的 `this.tasks`, | |
| 231 | + 不碰 `system/agents/`,不是干扰源。名字撞车,容易误判成开关没生效。 | |
| 232 | + | |
| 233 | +--- | |
| 234 | + | |
| 235 | +## 七之二、仍未解决的(不在本次范围) | |
| 236 | + | |
| 237 | +**机器级闸门不存在**。`SessionPool` 没有 `maxSessions`,全靠 TTL 兜底。登记在 `28-followups`。 | |
| 205 | 238 | |
| 206 | 239 | --- |
| 207 | 240 | ... | ... |
server/index.js
| ... | ... | @@ -139,6 +139,20 @@ async function start() { |
| 139 | 139 | |
| 140 | 140 | // 恢复中断任务并起搏 Agent (延迟 5s,等 HTTP 服务完全就绪) |
| 141 | 141 | setTimeout(() => { |
| 142 | + // 跑全量测试前把后台驱动全关掉:`DISABLE_BACKGROUND_AGENTS=1 bash start.sh`。 | |
| 143 | + // | |
| 144 | + // 为什么需要这个开关:这些驱动会起 agent,而 agent 的 cwd 是运行态目录 | |
| 145 | + // `system/agents/<agent>/<companyId>/<taskId>/`(locator.taskDir,落在本体目录里属既定设计)。 | |
| 146 | + // 心跳每 5 分钟建一个新的,实测 cto/_global/ 攒到 260+ 子项还在涨 —— 于是扫描 | |
| 147 | + // system/agents/ 的守卫测试(no-dead-path-references 那一类)会被瞬时竞态打红: | |
| 148 | + // 列到了目录名,进去时那一层还没写完。症状是偶发假失败,重跑就好,最难查。 | |
| 149 | + // | |
| 150 | + // 关的是「会自己起 agent 的东西」,恰好全在这个 setTimeout 里,所以一条早退就够: | |
| 151 | + // 两处中断恢复 + 任务队列兜底扫描 + 两路心跳。 | |
| 152 | + if (process.env.DISABLE_BACKGROUND_AGENTS === '1') { | |
| 153 | + console.log('[Startup] ⏸ DISABLE_BACKGROUND_AGENTS=1 —— 中断恢复 / 任务队列 / 两路心跳全部跳过'); | |
| 154 | + return; | |
| 155 | + } | |
| 142 | 156 | resumeIncompleteTasks().catch(err => console.error('[Startup] 恢复任务失败:', err.message)); |
| 143 | 157 | // 27-P3C:goal 级验收窗口里被打断的那些(status 已终态、判词还是占位值)。 |
| 144 | 158 | // 和上面同属「中断任务恢复」,也同样要等 HTTP 就绪——它收尾时会打自家 /api/agent/task/report。 | ... | ... |