• 310a2b2 / 3e718aa 把「正常跑完的对话」补进了回收路径,但 catch 分支没跟上:
    
        } catch (err) {
            dropSession(key);      // 只摘出池子,不杀子进程
        } finally {
            if (kind?.[2]) closeSession(key, ...);   // 池子里已经没了,返回 false,空转
            else armSessionIdle(key);
        }
    
    dropSession 的语义是「子进程已经自己死了」,可这里子进程活得好好的 —— 错的是它背后的
    API 调用(529 / DNS / 证书),CLI 一个都不会自己退。于是会话先被摘出池子,再也没人
    杀得掉它:一个既没有引用、也没有回收路径的孤儿进程。
    
    实测:一个跑了 23 小时的实例攒下 45 个孤儿 / 9.6 GB RSS,其中 38 个是心跳。抽查泄漏的
    convId,status **无一例外**是 failed;对照 cto 最近 60 次心跳是 55 completed / 5 failed
    —— 8% 的失败率,一次漏一个,数字对得上。成功的心跳一个都没漏,说明 310a2b2 本身是对的。
    
    改法:catch 里 dropSession → closeSession。可观测行为完全不变(两者都把会话摘出池子、
    都让下一轮冷启动),只是多杀一个本该死的进程。
    
    守卫:agent-chat-session-reclaim.test.js 新增一条。这里必须说清楚为什么破例去 spy
    disconnect —— **旧探针对这个 bug 是瞎的**:dropSession 和 closeSession 都会让会话从
    池子里消失,`/chat/:convId/stop` 两种情况都返回 success:false,差别只在子进程死没死。
    上一版修复正是因此漏过了这条路径。用 vi.spyOn 让 sendPrompt reject 来复现「CLI 活着、
    API 挂了」,再断言 disconnect 被调过。做过变异检验:退回 dropSession 后该用例转红。
    
    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 »
  • sessions 里存的是**常驻**的 claude CLI 子进程(stream-json 模式,不是一次性的
    `claude -p`),此前只有四条回收路径:协议 error / 子进程 disconnected / 手动删对话 /
    手动 stop —— 「正常跑完的对话」不在其中。
    
    而两路心跳都用一次性 convId 打进 /api/agent/:id/chat:
      - heartbeat-service:`heartbeat-${Date.now()}`,cto / coo 各 5 分钟一次
      - company-heartbeat-service:_sendAgentChat 的五个调用点(CEO 巡检 / Pipeline 步骤 /
        日程 / 告警 / 日报)convId 也全是一次性的,且无差别带 isHeartbeat:true
    
    于是每次心跳新起一个进程且永不释放。实测单次运行 2h43m 攒到 82 个进程 / 17.5 GB RSS。
    
    改法:
      - 心跳会话跑完立刻 closeSession(一次性 convId,会话复用对它零价值)
      - 普通对话上空闲 TTL(默认 30 分钟,AGENT_SESSION_IDLE_MS 可覆盖),复用 engine/idle-timer
      - 计时器只在「没有在途请求」的间隙走:getOrCreateProtocol 命中复用时 cancel,请求结束时
        重新上弦。所以再长的单次生成也不会被 TTL 打断,比在流式回调里 touch() 少一个调用点
      - 删对话 / stop 两处手抄的 disconnect+delete 收敛到 closeSession,否则它们不停表
    
    守卫:agent-chat-session-reclaim.test.js 用现成的 /chat/:convId/stop 当黑盒探针
    (查得到会话才返回 success:true),不为测试加导出口。对 HEAD 版本跑过:2 红 1 绿 ——
    两条回收断言修前失败,「普通对话保留会话」修前修后都绿,证明复用能力没被顺手砍掉。
    
    线上验证:重启后第一轮心跳 2 触发 / 2 回收,server 名下 claude 子进程数 0。
    
    Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
    yibo authored
     
    Browse File »