diff --git a/.env.example b/.env.example index b3f2cac..47c25e5 100644 --- a/.env.example +++ b/.env.example @@ -41,3 +41,11 @@ export FEISHU_VERIFICATION_TOKEN=your_verification_token export FEISHU_ENCRYPT_KEY=your_encrypt_key export FEISHU_MODE=ws # export FEISHU_ALLOW_FROM=ou_xxx,ou_yyy + +# 跑全量测试前关掉后台驱动(两路心跳 / 任务队列扫描 / 中断恢复)。 +# 它们会往 system/agents//// 建运行态目录, +# 扫描 system/agents/ 的守卫测试会被这个瞬时竞态打成偶发假失败。 +# 用法:DISABLE_BACKGROUND_AGENTS=1 bash start.sh +# +# 故意**不在这里赋空值**:start.sh 是 `set -a; source .env` 加载的, +# .env 里只要存在同名键(哪怕是空的),就会盖掉命令行传进来的 =1,而且不报错。 diff --git a/CLAUDE.md b/CLAUDE.md index 95f5426..5802b86 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,10 @@ npm run test:unit # vitest run (CI 用) npx vitest run path/to/foo.test.js # 跑单个测试文件 npx vitest run -t "test name" # 按名称过滤单个测试用例 +# 跑全量前先起「不带后台驱动」的 server:心跳每 5 分钟往 system/agents/ 建运行态目录, +# 扫描该目录的守卫测试会被瞬时竞态打成偶发假失败(详见 docs/done/claude-cli-session-leak.md §七) +DISABLE_BACKGROUND_AGENTS=1 bash start.sh + # DevLoop 规范 npm run spec:extract # 从 docs/prd/newfunction/*.html 重新生成 _spec/*.md(改完 HTML 后必须跑) npm run spec:check # CI/pre-commit 用,校验 _spec/ 与 HTML 是否同步 diff --git a/docs/README.md b/docs/README.md index 82b6fdc..790f602 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ | [LEMONBOSS-SYSTEM-DESIGN.md](./done/LEMONBOSS-SYSTEM-DESIGN.md) | 系统设计主索引 v2.0,含 27 章 `system-design/` 的总目录 | | [architecture-v5.md](./done/architecture-v5.md) | v5.3 架构方案,Phase 1-6 已完成(Phase 7 认知架构条目单独见 todo/) | | [claude-code-cron.md](./done/claude-code-cron.md) | Claude Code 原生 Cron 调度功能参考手册 | -| [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 判断孤儿归属的方法、变异检验为什么必要、三步定位脚本 | +| [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` —— 跑全量测试前关掉后台驱动的开关 | | [open-api-manual.md](./done/open-api-manual.md) | ArenaSK Open API 外部调用手册 | | [qa_judge_req.md](./done/qa_judge_req.md) | QA Judge Agent 需求文档 — 已在 `system/agents/qa-judge/` 落地 | | [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`) | diff --git a/docs/done/claude-cli-session-leak.md b/docs/done/claude-cli-session-leak.md index 79caa1e..37d36ab 100644 --- a/docs/done/claude-cli-session-leak.md +++ b/docs/done/claude-cli-session-leak.md @@ -195,13 +195,46 @@ DevLoop 的 retry-with-reflection 复用同一个 taskId,一刀切关掉会砍 --- -## 七、仍未解决的(不在本次范围) +## 七、跑全量测试前先关后台驱动 -1. **心跳持续往 `system/agents/` 写运行态目录**。`locator.taskDir()` 的落点设计就是 - `system/agents///`,属既定设计,不是 bug;但心跳每 5 分钟建一个新目录 - (`cto/_global/` 已 260+ 子项且在涨),扫描 `system/agents/` 的守卫测试会被瞬时竞态打红。 - 要跑干净的全量测试,得让测试隔离掉 `system/agents/`,或跑之前先停心跳。 -2. **机器级闸门不存在**。`SessionPool` 没有 `maxSessions`,全靠 TTL 兜底。登记在 `28-followups`。 +**这不是泄漏,是既定设计带来的测试干扰** —— 但它是本次排查的起点(「全量测试挂的那一条」), +所以记在这里。 + +`locator.taskDir()` 的落点就是 `system/agents///`,运行态目录落在 +本体目录里属既定设计。问题在于心跳每 5 分钟建一个新的(`cto/_global/` 已 263 个子项且在涨), +而扫描 `system/agents/` 的守卫测试(`no-dead-path-references` 那一类)会被瞬时竞态打红: +**列到了目录名,进去时那一层还没写完**。症状是偶发假失败、重跑就好,最难查。 + +```bash +DISABLE_BACKGROUND_AGENTS=1 bash start.sh +``` + +一条早退关掉全部「会自己起 agent 的东西」(`server/index.js` 的启动 setTimeout 块): +两处中断恢复 + 任务队列扫描 + 两路心跳。实测 127 文件 / 1226 用例全绿。 + +**验证它真的生效,别只看日志里的 `⏸`** —— 那只证明代码走到了早退。硬判据是目录数: + +```bash +ls system/agents/cto/_global | wc -l # 跑前 +npm run test:unit +ls system/agents/cto/_global | wc -l # 跑后:必须完全相等 +``` + +两个坑: + +- **`.env` 里不要给这个键赋空值**。`start.sh` 是 `set -a; source .env` 加载的, + `.env` 里只要存在同名键(哪怕空的)就会盖掉命令行传进来的 `=1`,而且不报错。 + 所以 `.env.example` 里它只以注释形式出现。 +- **`[TaskQueue] 🚀 已启动` 仍会打印,那是另一个模块**。`services/task-queue.js` + (ArenaSK 时代 `/api/claude-chat` 的队列,`routes/claude-chat.js` 模块加载时起) + 和批 3 的 `services/task-scheduler.js` 同名不同物。它的心跳只扫内存里的 `this.tasks`, + 不碰 `system/agents/`,不是干扰源。名字撞车,容易误判成开关没生效。 + +--- + +## 七之二、仍未解决的(不在本次范围) + +**机器级闸门不存在**。`SessionPool` 没有 `maxSessions`,全靠 TTL 兜底。登记在 `28-followups`。 --- diff --git a/server/index.js b/server/index.js index 2abe9cf..b70db3d 100644 --- a/server/index.js +++ b/server/index.js @@ -139,6 +139,20 @@ async function start() { // 恢复中断任务并起搏 Agent (延迟 5s,等 HTTP 服务完全就绪) setTimeout(() => { + // 跑全量测试前把后台驱动全关掉:`DISABLE_BACKGROUND_AGENTS=1 bash start.sh`。 + // + // 为什么需要这个开关:这些驱动会起 agent,而 agent 的 cwd 是运行态目录 + // `system/agents////`(locator.taskDir,落在本体目录里属既定设计)。 + // 心跳每 5 分钟建一个新的,实测 cto/_global/ 攒到 260+ 子项还在涨 —— 于是扫描 + // system/agents/ 的守卫测试(no-dead-path-references 那一类)会被瞬时竞态打红: + // 列到了目录名,进去时那一层还没写完。症状是偶发假失败,重跑就好,最难查。 + // + // 关的是「会自己起 agent 的东西」,恰好全在这个 setTimeout 里,所以一条早退就够: + // 两处中断恢复 + 任务队列兜底扫描 + 两路心跳。 + if (process.env.DISABLE_BACKGROUND_AGENTS === '1') { + console.log('[Startup] ⏸ DISABLE_BACKGROUND_AGENTS=1 —— 中断恢复 / 任务队列 / 两路心跳全部跳过'); + return; + } resumeIncompleteTasks().catch(err => console.error('[Startup] 恢复任务失败:', err.message)); // 27-P3C:goal 级验收窗口里被打断的那些(status 已终态、判词还是占位值)。 // 和上面同属「中断任务恢复」,也同样要等 HTTP 就绪——它收尾时会打自家 /api/agent/task/report。