• 起因是「全量测试偶发挂一条」。查下来既不是回归、也不是那 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>
    yibo authored
     
    Browse File »

  • 承接 310a2b2 的两个遗留:
    
    ① 四类被错标成心跳
       company-heartbeat-service._sendAgentChat 给五类调用(心跳巡检 / Pipeline 步骤 /
       日程 / 告警 / 日报)无差别打 isHeartbeat:true,于是对话列表里全都显示成
       「⏱️ 自主心跳巡检」、meta.type 一律 'heartbeat'。改成显式传 kind,五类各自标注。
       isHeartbeat 仍然发,但只对真心跳为真,老消费方行为不变。
    
    ② 310a2b2 让 Pipeline 重试变成了冷启动
       Pipeline 步骤的 convId 是 `pipeline-{runId}-step-{i}`,被驳回重做时**复用同一个**。
       310a2b2 按 isHeartbeat 一刀切回收,等于每次重试都要走 buildPrompt 场景 C 重注入历史。
       现在回收策略挂在 CALL_KINDS 表上,判据只有一条:**convId 会不会被下一轮复用** ——
       会复用的(pipeline / 人类对话)留着走 TTL,不复用的(心跳 / 日程 / 告警 / 日报)跑完即回收。
       不按「是不是后台调用」分:五类后台调用里 pipeline 就是要留的那一类。
    
    守卫补到 12 条(原 3 + kind 标注 5 + 回收策略 4)。对 310a2b2 跑过:8 红 4 绿。
    convId 一律用固定字面量而非拼时间戳 —— 撞上 test-id-stability 那道防线后按它的判据改的,
    固定之后第二次跑还顺带走了「复用已有对话」那条分支。
    
    文档:
      - CLAUDE.md 注意事项加一条(新增会话创建路径时必须同时定回收路径,附症状与守卫位置)
      - 28-followups 守卫清单补第五条,并把「前四条守单一持有者」与第五条「守资源生命周期」
        的形状差异写明;顺手修掉表格已有 4 行、结尾却仍写「这三条」的失配
      - .env.example 补 AGENT_SESSION_IDLE_MS
    
    全量单测 667/667 绿。重启后 /api/health 正常,心跳照常挂载。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse File »

  • ## 问题
    
    server/middleware/jwt-auth.js 的兜底密钥 'lemon-dev-secret-change-in-production'
    提交在仓库里,而 .env.example **没有 JWT_SECRET 这一项** —— npm run setup 就是
    从 .env.example 生成 .env(CLAUDE.md「快速启动」),所以照文档装出来的部署
    必然用这个公开密钥签会话,且启动时零告警。
    
    危害不止「密钥弱」。extractUser 拿 decoded.userId 回库重查用户
    (所以伪造 role:'admin' 这条路本身是堵住的,这个设计是对的),但
    user-db 的 id 是自增小整数、不是 uuid:拿已知密钥签 {userId: N}、从 1 试起
    就能命中真实账号,拿到的是该账号在库里的真实 role。命中 admin 就是
    requireAdmin 守着的整个 /api/users/* 全部敞开。不需要密码、不需要 OAuth,
    只要读过这个仓库。
    
    本机 .env 有 64 字符密钥,未受影响;受影响的是默认部署路径。
    
    ## 三处改动
    
    1. .env.example 补 JWT_SECRET(含 openssl rand -hex 32 生成命令与危害说明)
    2. jwt-auth.js:NODE_ENV=production 且未设 JWT_SECRET 时**拒绝启动**
       (签名密钥缺失不是可以边跑边补的事);开发环境保留兜底但每次起进程吼一次。
       三种情形都实测过:生产无密钥→抛错拒启;生产有密钥→正常且不告警;开发无密钥→放行+告警。
    3. 新增 guard server/tests/unit/env-example-secrets.test.js:
       「安全类 env + **非空**硬编码兜底」必须在 .env.example 里有一行。
       只管非空兜底是刻意的 —— 空串兜底(FEISHU_ENCRYPT_KEY、OPEN_API_KEYS)是
       「未配置」的正当写法,缺了 fail closed;把它们也算进来只会制造噪音,
       然后被加白名单,然后白名单腐烂。危险的是「缺了也能跑、还跑得像配好了」那一类。
    
    ## 顺带修掉一个能把全新克隆直接搞坏的
    
    .env.example 写 PORT=3000,而 config.js 默认 3010、web/vite.config.js 的代理
    目标硬编码 3010 → npm run setup 装出来的环境里前端每个 API 调用都 502,
    而症状离原因很远。改成 3010,并加一条 guard 钉住「example / config 默认 /
    vite 代理」三者一致。
    
    ## 验证
    
    857 全绿(85 文件),既有断言未改。3 处变异精确变红后逐字还原。
    其中 S3 那条变异(把兜底常量改成内联字面量)是必要的:guard 第一版只认内联
    字面量,而我重构时把兜底提成了命名常量 DEV_FALLBACK_SECRET,于是 guard 对它
    诞生的那个 case 恰好失明 —— 被它自己的自检当场抓到,遂加上同文件常量绑定解析。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse File »

  • - Agent as Micro-Container: each agent lives in data/agents/{id}/ with full encapsulation
    - Persona vs Capability split: agent.md (identity) + .claude/skills/main_skill/SKILL.md (workflow)
    - Conversations co-located: data/agents/{id}/conversations/ replaces global dir
    - Meta Skills renamed: meta_hunt, meta_digest, meta_creator, meta_evolving, meta_benchmark, meta_research
    - Dynamic Hot-Reload: AgentManager.getAgentConfig() falls back to filesystem scan
    - Parallel Sub-Agent orchestration via Promise.all with multi-terminal UI
    - Removed SQLite dependency, global skills/ directory, and legacy conversations/
    - Full architecture docs: 18-local-filesystem-first-agent-encapsulation.md
    yibo authored
     
    Browse File »