claude-mem 搜索与 Chroma 稳定性修复实录:MCP 500 错误、Collection 初始化失败与静默断连的三层防线
本文以 claude-mem 仓库中 Phase 07 分诊手册 TRIAGE-07-Search-Chroma-Stability.md 为主体,完整还原“MCP 搜索返回 500、Chroma Collection 初始化失败、Worker 看似健康但 MCP 实际断连”三类线上问题的诊断路径与修复方案,并结合仓库源码(SearchRoutes.ts、SearchManager.ts、ChromaMcpManager.ts 等)解析当前的错误处理、降级回退与自动重连实现。读完后你将能够独立定位 claude-mem 搜索链路的故障层(路由层 / Chroma MCP 子进程层 / 连接管理层),并按仓库既有的验证流程完成回归。
问题定位:为什么搜索稳定性是“中等优先级”的头号议题
分诊手册开篇给出了本阶段的定位:搜索是 claude-mem 面向用户的核心能力,而这一组 bug 会让 MCP 搜索要么返回 HTTP 500,要么根本无法连接 Chroma,直接侵蚀产品的核心价值主张。修复点集中在两个层面:搜索编排层(search orchestration)和 Chroma 连接层(Chroma connection layer)。
该手册明确列出了本次处理的四个 issue:
| Issue | 症状 | 故障层 |
|---|---|---|
| #1263 | MCP search 工具经 Worker API 返回 HTTP 500,用户端只看到不透明的错误 | 搜索路由 / 搜索编排 |
| #1261、#1232 | Chroma 连接失败,报 "Collection setup failed",疑似 chroma-mcp 子进程死亡或未启动 | Chroma 子进程 / 数据目录初始化 |
| #1266 | Worker 的 localhost:37777 健康检查正常,但 MCP 工具全部失效 |
连接管理 / 健康探测盲区 |
手册同时声明了前置依赖:Phase 01–06 应已完成,尤其是 Phase 06 的 Windows Chroma 修复和 Phase 02 的 Chroma CPU 修复——因为部分 500 错误可能正是上游 Chroma 就绪性检查(Phase 06 中 #1225 的 readiness poll)未修复时暴露出来的下游症状。这一依赖关系提醒我们:稳定性问题必须按分诊阶段顺序收敛,后序阶段的 500 修复可能是前序修复的“自然结果”。
问题一:MCP 搜索 Worker API 500 错误(#1263)
手册给出的修复方案
手册要求首先读取搜索路由处理程序(在 src/services/worker/http/routes/ 下找 SearchRoutes)和搜索编排逻辑(SearchManager.ts),判断 500 很可能来自搜索管线中一个未被捕获的异常,然后为搜索路由添加结构化错误处理:
- 用 try/catch 包裹搜索处理器;
- 遇到 Chroma 错误时,回退到仅 SQLite 的搜索(查找现有的
chromaSync === null处理路径); - 遇到 SQLite 错误时,返回带语义的错误消息并使用 HTTP 503(而非 500);
- 以 ERROR 级别记录真实错误及完整堆栈。
手册还给出了一条判断分支:如果错误特指 Chroma collection 错误,那么 Phase 06(#1225 就绪轮询)的修复可能已解决它;如果 Chroma 客户端在查询中途断开,则应在 ChromaMcpManager.callTool() 中加入重连逻辑。
当前仓库中的落地实现
路由层的统一错误处理。 当前实现中,SearchRoutes.ts 注册的每条路由(GET /api/search、GET /api/search/observations、GET /api/search/by-file、GET /api/timeline、GET /api/context/*、POST /api/context/semantic)都不是裸写 handler,而是统一通过 this.wrapHandler(...) 包装。BaseRouteHandler.ts 中的 wrapHandler 同步/异步双路捕获异常,交给 handleError(BaseRouteHandler.ts#L84-L113):AppError 按其声明的 statusCode 响应,非 AppError 归一为 500 并记录 { error: message } 的 JSON 响应体;同时区分日志级别——4xx 客户端错误按 WARN 记录且不进错误上报通道,5xx 真正故障才走 logger.failure(含脱敏的异常上报)。这正是手册第 3、4 条要求(“有意义的错误消息 + 恰当状态码 + ERROR 级日志”)的结构性落地:错误处理不是每个路由各自打补丁,而是收敛到路由基类一处。
编排层的三条搜索路径与 FTS5 回退。 手册第 2 条要求的“Chroma 失败时回退 SQLite 搜索”,在 SearchManager.ts 的 search() 中体现为三条显式路径:
- PATH 1(仅过滤,无 query 文本):直接走 SQLite 过滤(SearchManager.ts#L495-L507);
- PATH 2(Chroma 语义搜索):
queryChroma查询取回 100 条语义匹配 → 按 90 天新鲜度窗口(SEARCH_CONSTANTS.RECENCY_WINDOW_MS)或用户dateRange过滤 → 按doc_type分桶(observation/session_summary/user_prompt)→ 回 SQLite 水合完整行(SearchManager.ts#L356-L469)。整个路径包在 try/catch 中:捕获到异常时记录chromaFailureReason(区分ChromaUnavailableError连接错误与其他错误),打 WARN 日志“falling back to FTS5 keyword search”,然后对 observations / sessions / prompts 三个集合分别调用SessionSearch的 FTS 搜索兜底(SearchManager.ts#L541-L563); - PATH 3(Chroma 未初始化,即
chromaSync为 null):直接进入 FTS5 关键词搜索;连 FTS5 失败也只做 ERROR 日志而不再向上抛——保证搜索接口尽量不 500(SearchManager.ts#L565-L583)。
策略选择本身由 SearchOrchestrator.ts 的策略模式承担:构造 ChromaSearchStrategy / SQLiteSearchStrategy / HybridSearchStrategy,Chroma 查询失败时包装为 ChromaUnavailableError 上抛给上层,且对“按平台作用域的查询在 Chroma 中零命中”的场景自动回退 SQLite——这一分支正是为平台来源(platform source)隔离上线后旧元数据不含 platform_source 字段的情况准备的。
用户可感知的降级提示。 当回退后仍然零结果时,返回文本不是干巴巴的 "No results",而是 ResultFormatter.formatChromaFailureMessage:连接类错误提示“Semantic search is offline (Chroma MCP unreachable …)”并引导运行 /api/chroma/status?deep=1 诊断;其他错误引导查看 ~/.claude-mem/logs/ 中的 CHROMA_SYNC 日志。搜索链路同时通过遥测信封(SearchTelemetryEnvelope:result_count、search_strategy: chroma|fts|filter_only、chroma_available、fallback_reason)把“这次搜索是否发生了静默降级”上报出来(SearchManager.ts#L590-L617),SearchRoutes.ts 在 /api/search 中间件里统一采集 search_performed 事件——降级不再是黑盒,而是可度量的。
问题二:Chroma “Collection setup failed”(#1261、#1232)
手册给出的修复方案
手册判断用户报的 Collection 初始化失败“很可能由 Chroma MCP 子进程死亡或未启动引起”,要求阅读 src/services/sync/ChromaMcpManager.ts,围绕 collection、setup、cacheDir 三个关键词定位错误来源,并给出有韧性的初始化四步:
- collection 不存在则创建(幂等);
- 创建失败则延迟 2 秒重试一次;
- 重试仍失败则标记 Chroma 不可用(
this.connected = false)并记录错误——SQLite 搜索保持可用; - 若根因是 Chroma 数据目录不存在,在启动 chroma-mcp 之前用
mkdirSync(path, { recursive: true })确保目录已创建。
当前仓库中的落地实现
ChromaMcpManager.ts 是目前该故障层最复杂的实现,对照手册的四条要求,当前代码的演进轨迹可以逐项印证:
数据目录递归创建(手册第 4 条)。 写入锁获取函数 acquireChromaWriterLock 在启动 chroma-mcp 之前先执行 fs.mkdirSync(normalizedDataDir, { recursive: true })(ChromaMcpManager.ts#L438),随后以 wx 排他标志写入 .claude-mem-chroma-writer.lock。锁文件携带 pid / ownerId / dataDir / acquiredAt / startToken,处理三种竞争情形:锁不存在则创建;锁属于自己(重启后的同一进程)则接管;锁属于他人但进程已死(用 PID + 启动令牌双重校验 isChromaWriterLockLive)则清理过期锁重试;锁属于活进程则拒绝启动第二个 writer 并抛 ChromaUnavailableError。这保证了同一 Chroma 数据目录全局单 writer,避免两个 worker 同时写一个 persistent 目录导致的集合损坏。
标记不可用 + 降级保留 SQLite(手册第 3 条)。 管理器持有 private connected: boolean 标志;子进程意外关闭(transport.onclose)时立即 this.connected = false、注销 supervisor 注册、记录失败时间戳并进入 10 秒重连退避(RECONNECT_BACKOFF_MS = 10_000,ChromaMcpManager.ts#L35)。上层 SearchManager 收到 ChromaUnavailableError 后即走 PATH 2 的 FTS5 回退或 PATH 3,搜索功能不中断——“SQLite 搜索保持可用”成为结构性保证而非临时补丁。
连接前的预检与预热。 每次连接先经过 prewarmChromaMcp:用 uvx --python 3.13 --with onnxruntime>=1.20 --with protobuf<7 --from chroma-mcp==0.2.6 chroma-mcp --help 跑一遍,目的是在真实 MCP 会话建立前把 uvx 环境构建完,默认超时 120 秒,可用环境变量/配置 CLAUDE_MEM_CHROMA_PREWARM_TIMEOUT_MS 调整(有效范围 1~600000 毫秒,ChromaMcpManager.ts#L26-L28、ChromaMcpManager.ts#L606-L624)。两个依赖覆盖(onnxruntime>=1.20 保证能解析 all-MiniLM-L6-v2 的 pytorch-2.0 IR 模型;protobuf<7 避免 opentelemetry 生成文件被 protobuf 7.x 拒载)以运行时 --with 形式注入,无需 fork 上游 chroma-mcp。预热失败时以 spawn 时刻捕获的进程令牌做进程树清理,并抛 ChromaUnavailableError 进入上面的降级路径。
集合层面的探测。 “Collection 是否存在/为空”不再靠猜测,probeSemanticSearch() 分三阶段(connect → list → query)给出结构化诊断:先 chroma_list_collections 数集合,再对固定集合 cm__claude-mem 发一条 query_texts: ['ping'] 探针,失败时区分“collection 缺失/为空”与一般错误并返回 stage、queryLatencyMs 等字段(ChromaMcpManager.ts#L884-L936)——这与 formatChromaFailureMessage 提示用户执行的 /api/chroma/status?deep=1(由 ChromaRoutes.ts 提供服务)正好衔接:诊断入口在路由层,探测实现在管理器层。
子进程单例不变量。 手册没有展开但源码注释反复强调的一个事实:MCP SDK 的 transport.close() 只通知直接子进程(uvx),在 Linux 上孙进程(uv、python、chroma-mcp)会重新挂到 init 下存活,反复重连会累积 20+ 个实例。因此每一条放弃 transport 的路径(重连、传输错误、连接超时、onclose、stop)都必须经过 disposeCurrentSubprocess() 做“先优雅关闭(stdin EOF → 2s → SIGTERM → 2s → SIGKILL 升级由 SDK 完成),再以捕获的 PID 补进程树清扫”。这解释了为什么手册 Phase 07 的前置条件强调 Phase 06 的 Windows Chroma 修复——跨平台子进程生命周期本身就是 #1261/#1232 类问题的温床。
问题三:Worker 看似健康、MCP 实际断连(#1266)
手册给出的修复方案
用户报告的现象是:localhost:37777 的健康检查一直正常,但 MCP 工具全部停止工作。手册的诊断判断是:健康检查只验证了 HTTP 服务,没有验证 MCP 层,连接掉线后无人察觉。修复要求两条:
- 在
/api/health端点中加入 MCP 健康验证,健康响应包含mcpConnected: true/false; - MCP 工具处理程序中,若 Worker 内部状态显示 Chroma 已断开,先尝试自动重连再返回错误。
当前仓库中的落地实现
健康探测的双层结构。 Worker 侧的 /api/health 是外部探针的入口:安装器(install.ts 中以 http://${workerUrlHost}:${workerPort}/api/health 轮询就绪)、doctor 命令(doctor.ts)、重启校验(restart-verify.ts 明确“只读取 pid 与 version 字段”,且 503 降级队列时仍返回可解析的响应体)以及守护式轮询 HealthMonitor.ts(注释即写明“/api/health 是最便宜的无扰动探针”)都依赖它。手册指出的盲区——HTTP 健康不代表 MCP/Chroma 链路健康——在源码中体现为把语义搜索链路的探测与 HTTP 存活探测解耦:ChromaMcpManager.isHealthy() 通过真实调用 chroma_list_collections(limit 1)判定(ChromaMcpManager.ts#L872-L882),深探测用 probeSemanticSearch() 三阶段探针;这些结果经 Chroma 状态路由暴露,而 /api/health 保持轻量快速,避免把 uvx 子进程冷启动(可达分钟级)拖进每个部署探针的响应时间里。这一取舍与手册“在健康响应中报告 mcpConnected 布尔值”的方向一致:关键是被检方要显式暴露 MCP 层状态,而不是让 HTTP 200 隐含 MCP 正常。
断连后的自动重连(手册第 2 条)。 callTool() 是所有 Chroma MCP 工具调用的收口:变更类工具(匹配 chroma_(add|create|delete|modify|update|upsert)_)在 local 模式下还会串行化入队并受 CLAUDE_MEM_CHROMA_MAX_PENDING_MUTATIONS(默认 5000)背压保护。核心的重连逻辑在 callToolUnqueued(ChromaMcpManager.ts#L765-L806):工具调用抛出传输错误时,打 WARN 日志“reconnecting and retrying once”,先以捕获的进程令牌杀掉将死的子进程树(避免 Linux 上的孤儿泄漏),再 ensureConnected() 重建连接并重试一次;重试仍失败才把 connected 置 false 并抛出带工具名的错误。而 ensureConnected()(ChromaMcpManager.ts#L150-L185)自身也具备防抖:距上次失败不足 10 秒直接抛“backoff (Ns remaining)”错误,并发调用共享同一个 connecting Promise,避免雪崩式重连。这就是手册设想的“Chroma 客户端查询中途断开 → 自动重连 → 仍失败才向用户返回错误”的完整闭环。
验证与回归:手册第四项任务
手册最后要求“运行测试并端到端验证搜索”,具体命令与当前仓库脚本的对应关系:
# 全量测试(package.json 中 test 脚本实际为 bun test tests)
npm test
# 构建并同步(package.json: build && sync-marketplace && restart-marketplace-worker)
npm run build-and-sync
# 端到端验证统一搜索端点(手册提示:按实际路由调整路径)
curl 'http://localhost:37777/api/search?q=test&limit=5'
需要注意两处与手册原文的出入:其一,package.json 中 test 脚本的实际执行体是 bun test tests(另有 test:search 针对 tests/worker/search/ 的专项集),手册写作时期的 npm test 表述以当前仓库脚本为准;其二,手册 curl 示例中的 q/limit 参数与当前 SearchRoutes.ts 实际注册的路由完全吻合——统一搜索就是 GET /api/search,因此该命令在当前代码上可直接执行。若搜索发生降级,返回文本本身会携带 formatChromaFailureMessage 的诊断提示,配合 /api/chroma/status?deep=1 即可判断是“连接层(chroma_connection)”还是“查询层(chroma_error)”问题——这一判别维度直接来自 SearchTelemetryEnvelope.fallback_reason 的三个枚举值:chroma_connection / chroma_error / chroma_not_initialized(SearchManager.ts#L28-L33)。
小结:搜索稳定性修复的设计模式
把 Phase 07 的三个问题放在一起看,当前代码呈现出一套一致的稳定性设计,可作为后续同类故障的排查模板:
- 错误不穿透,状态码有语义:路由基类统一 try/catch + 按
AppError映射状态码,4xx/5xx 分级记日志(BaseRouteHandler.ts); - 降级是显式路径,不是异常副产品:Chroma 语义 → FTS5 关键词的每条回退都在
SearchManager.search()中有命名的代码路径,并由遥测信封记录策略与回退原因(SearchManager.ts#L590-L617); - 子进程生命周期按不变量管理:全局单 writer 锁、spawn 时刻捕获的进程令牌、所有放弃连接的路径收口于一次树级清理,杜绝孤儿累积(ChromaMcpManager.ts);
- 不可用是正常状态:
connected = false+ 10 秒退避 + 上层 FTS 兜底,让用户在语义搜索离线时依然拿到“结果可能不完整”的透明提示,而不是 500。
如需进一步深挖,可继续阅读 SearchOrchestrator.ts 的策略组合、ChromaSearchStrategy.ts 的检索细节、docs/public/architecture/search-architecture.mdx 的搜索架构文档,以及 ChromaMcpManager.ts 后半部分的停止/资源回收实现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00