Commit 401840b68818715d751930b7c7354435c9630ff7

Authored by yibo
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>
.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。
... ...