Files
cc-web/.planning/ccweb-unrouted-notification-fix/findings.md

5.0 KiB
Raw Blame History

调查结论

2026-07-12 根因证据

  • ccweb PID 1606383 持续占满单个 CPU 核心,RSS 约 130 MiB,系统可用内存约 4.4 GiB。
  • curl http://127.0.0.1:8002/ 能立即建立 TCP,但 8–10 秒无响应,符合事件循环阻塞而非端口未监听。
  • logs/process.log 每约 0.6 秒出现 codex_app_notification_unrouted,来源主要是三个 Codex App threadId。
  • 三个 threadId 均存在于当前 sessions/*-run/codexapp-state.json,却未恢复进内存路由映射。
  • findCodexAppRouteByRuntime 在内存命中失败后调用 adoptCodexAppUnroutedTurn。
  • findCodexAppSessionByThreadId 对每条未路由通知执行 readdirSync(SESSIONS_DIR),再逐个 loadSession;当前 113 个会话 JSON 合计约 61.3 MiB。
  • 高频 delta × 同步全量扫描导致 Node 主线程单核 100%,所有 HTTP 请求饥饿。
  • PM2 error log 中的 V8 OOM 修改时间为 2026-06-15,是历史故障,不是当前直接原因。
  • 当未路由子线程通知停止后,未部署补丁的旧进程 CPU 从单核 100% 降至 0–2%,本地首页恢复为 HTTP 200/约 2.3 ms;这证明故障与通知风暴强相关,而非持续内存不足或端口问题。

工作区约束

  • 用户已有修改:public/app.js、public/style.css、scripts/regression.js。
  • 现有未跟踪计划/任务目录属于其他工作,不得覆盖或删除。
  • .trellis/.current-task 当前指向 00-bootstrap-guidelines,另有 07-11-subagent-card-metadata 进行中;不得切换共享 current-task,以免干扰并发会话。
  • 项目要求重启前检查运行会话;只有除当前会话外没有其他 running 会话时才能重启。
  • 当前磁盘运行态检查只发现本对话 4b9700f7-... 的一个 codexapp-state.json,状态为 running;没有发现其他会话 run 目录或 classic PID 文件。最终重启前仍需再次复核。
  • 重启前通过运行中 ccweb 的内部 MCP ccweb_list_conversations 检查:仅当前对话为 running,其余全部为 idle,满足项目重启条件。

部署验证

  • 执行 pm2 restart ccweb --update-env 成功,新 PID 为 1802039,PM2 restart 计数从 60 增至 61。
  • 启动日志出现 ccweb_mcp_child_threads_recovered,restored=3,证明真实恢复文件中的三个子线程路由被重建。
  • 连续 5 次本地 HTTP 探针均返回 200,后续最终探针约 3.6 ms。
  • 新进程 RSS 约 64 MiB,事件循环 P95 约 1.5 ms,短时 CPU 回落至 0–12%,没有持续占满单核。
  • 重启后的日志窗口仅包含 recovery/server_start 事件,没有新的 codex_app_notification_unrouted 风暴。

项目上下文

  • Trellis 将项目识别为单仓库,规范层为 backend/frontend;本次只涉及 backend。
  • codebase-memory 项目 home-cc-web 索引状态为 ready,当前 3091 个节点、7446 条边。
  • 独立计划审查已通过;建议在实施记录中明确测试入口与重启检查。
  • 项目测试入口为 npm run regression(实际执行 node scripts/regression.js),服务入口为 node server.js。
  • backend 规范目录当前仍是模板状态,没有额外项目特定限制;本次重点遵守现有结构化 plog、同步 I/O 热路径规避和回归脚本惯例。
  • 预计修改位置:recoverCodexAppTurnState、findCodexAppSessionByThreadId、adoptCodexAppUnroutedTurn、handleCodexAppNotification,以及独立/现有回归脚本。
  • 当前 codexapp-state.json 只持久化父线程 entry;子线程 ID 仅间接存在于 toolCalls[*].input.agentThreadId 等协作工具数据中,recoverCodexAppTurnState 没有重建 ccwebMcpChildThreads。
  • findCodexAppRouteByRuntime 当前先调用父线程磁盘收养逻辑,再检查 ccwebMcpChildThreads;对子线程通知会先触发昂贵的父会话全盘扫描,路由顺序本身也需要调整。
  • 现有 scripts/regression.js 含用户未提交的“子代理卡片元数据”测试,修复必须追加且保留这些改动。
  • 真实恢复文件中,协作子线程以 toolCalls[].name/kind = subAgentActivity 持久化;input.agentThreadId 是路由键,input.agentPath 可作为恢复标签,input.kind 表示 started/interacted 等活动。

HAPI 对比

  • HAPI 的“归档”主要是会话生命周期操作:将 active session 断开并把 metadata.lifecycleState 设为 archived,支持后续 reopen;它不是专门为解决 ccweb 本次每条通知全盘扫描而设计。
  • HAPI 的归档确实能减少活动会话集合和实时订阅压力,但历史记录仍保留在存储/缓存层,因此只有配合索引化 SessionCache/SyncEngine 才能避免热路径扫描。
  • 对 ccweb 而言,未来增加归档有产品和容量管理价值,但不能替代当前 threadId 路由索引、恢复映射和负缓存修复。
  • 源码确认 HAPI archiveSession 调用 rpcGateway.killSession 后进入 handleSessionEnd;Hub 存储基于 bun:sqlite,SessionCache 维护内存态并以 active 作为权威运行标志。