claude-mem 分支记忆之 Chroma 分支感知同步:BRANCH-PARITY-01 向量库分支边界补全实战
claude-mem 的「分支记忆(Branch Memory)」功能让 Agent 的记忆感知 Git 分支边界:语义检索不再跨越 Git 祖先关系返回其他分支的结果。本文以开发 Playbook BRANCH-PARITY-01-Chroma-Branch-Sync 为主体,完整还原该阶段的八个任务:合并 main 分支、为 Chroma 类型系统补齐 branch/commit_sha 元数据、改造文档格式化与同步方法、实现分支感知的向量过滤、修复 summary 表缺失列的迁移缺口,以及 19 个配套测试。读完后你能掌握「如何为一个既有向量库同步层增加一套新的过滤维度,并保持旧数据的向后兼容」这一完整的工程方案。
背景:ChromaSync 为什么需要分支边界
分支记忆功能此前已完成 6 个阶段(schema、祖先关系解析、上下文过滤、搜索过滤、边界情况、集成测试),但向量检索层 ChromaSync 始终忽略分支边界。同步到 ChromaDB 的 observation 缺少 branch 和 commit_sha 元数据,导致语义搜索无视 Git 祖先关系,返回所有分支的结果。本阶段(Phase 01)的目标有二:
- 把 main 分支的 v10.5.4–v10.5.5 区间(约 24 个提交的 bugfix)合并进 branch-memory 工作分支,保证后续开发基于最新代码;
- 让 ChromaDB 分支感知(branch-aware),使向量搜索尊重分支可见性。
在 claude-mem 中,ChromaSync 类 是 SQLite 记忆层与 ChromaDB 向量层之间的同步桥梁:每条 observation/summary 被拆成多个带元数据的 Chroma 文档写入集合(集合名形如 cm__<project>,见 集合名构造逻辑),再由 ChromaSearchStrategy 通过 where 过滤条件执行向量查询。为它增加分支维度,需要贯穿「类型定义 → 文档格式化 → 同步入口 → 过滤构造 → 回填」五个环节。
任务一:合并 main 分支
Playbook 要求在执行 git fetch origin + git merge origin/main 后,用 npm run build-and-sync 验证合并后的代码库可干净构建。实际合并中唯一的冲突出现在 SessionStore.ts 的 bulk insert:main 引入了 content_hash 字段,branch-memory 引入了 branch/commit_sha 字段,解决方案是在同一条批量插入语句中同时容纳两组字段。这一冲突本身揭示了本次功能与 main 演进的正交性——两者都在扩展同一张表的写入路径。
任务二:类型系统补齐 branch / commit_sha 字段
Playbook 指出三个接口需要更新:
src/services/sync/ChromaSync.ts中的StoredObservation接口(约 26 行处,共 16 个字段,以created_at_epoch结尾)增加branch?: string | null与commit_sha?: string | null;- 同文件中的
StoredSummary接口(约 45 行处,结构类似)增加同样两个字段; src/services/worker/search/types.ts中的ChromaMetadata接口(约 38 行处,含sqlite_id、doc_type、project等字段)增加branch?: string与commit_sha?: string。
对照当前主分支的 StoredObservation 定义 与 StoredSummary 定义 可以看到基线形态:字段列表以 prompt_number、created_at_epoch 收尾,与 Playbook 描述一致。这三个接口是数据流上的「契约」——SQLite 行 → 格式化器 → Chroma 文档 → 查询结果反解析,任何一环缺字段,元数据就会静默丢失。Playbook 记录该任务完成时出现了一些预存在的 TypeScript 报错(bun:sqlite、Component 类型),与本次改动无关。
ChromaMetadata 接口位于 搜索类型定义文件,被 ChromaSearchStrategy 的 filterByRecency() 用作查询结果元数据的类型约束。
任务三:formatObservationDocs / formatSummaryDocs 携带分支元数据
Playbook 的改法遵循既有的「可选元数据守卫模式」:在 formatObservationDocs 已有的可选元数据块(subtitle、concepts、files_read、files_modified)之后追加:
if (obs.branch) { baseMetadata.branch = obs.branch; }
if (obs.commit_sha) { baseMetadata.commit_sha = obs.commit_sha; }
formatSummaryDocs()(约 189 行处,当前为 formatSummaryDocs)对 summary 的 baseMetadata 块应用相同模式。这个「字段存在才写入」的守卫不是风格偏好,而是向后兼容的关键:Chroma 元数据是稀疏的(sparse)bag,旧文档没有 commit_sha 键,格式化器不应为它们伪造一个空键——这直接决定了后续过滤器能否用 $or 区分新旧文档。
值得注意的底层细节:addDocuments 在把批文档写入 Chroma 前,会先过滤掉 null/undefined/空字符串的元数据值(cleanMetadatas,见 元数据清洗逻辑)。也就是说格式化器产出的 branch: null 最终不会出现在 Chroma 文档上,与「omitted」等价——两条路径共同保证旧语义不变。
任务四:syncObservation / syncSummary 透传分支参数
Playbook 要求在 syncObservation(约 304 行处)与 syncSummary(约 348 行处)的 discoveryTokens 之后各增加一对可选参数 branch?: string | null、commitSha?: string | null,并写入内部构造的 StoredObservation/StoredSummary 对象,从而让格式化器把分支元数据传播到 Chroma 文档。
调用点在 ResponseProcessor.ts(约 198 行):原先 syncObservation() 传 7 个实参,Playbook 指引先弄清 storeObservationsAndMarkComplete() 是如何拿到 branch/commit_sha 的(来自 pending 消息的 branch/commit_sha 字段),再把会话上下文的 session.lastBranch 与 session.lastCommitSha 一并传给两处调用点。这里体现了 claude-mem 的数据流分层:分支信息在转录处理管道中被提取并暂存在会话对象上,响应处理阶段再分发到持久化(SQLite)与向量同步(Chroma)两条下游,保证同一事实只提取一次、两处落盘一致。
任务五:buildWhereFilter 支持分支感知过滤
这是本阶段最有工程含量的部分,对应 ChromaSearchStrategy.buildWhereFilter。Playbook 的设计要求:
- 方法签名增加可选参数
commitShas?: string[]; - 当
commitShas非空时,构造{ commit_sha: { $in: commitShas } }(Chroma 支持$in操作符); - 必须同时保留「没有
commit_sha的旧文档」(迁移前数据),可用 Chroma$or组合:{ $or: [{ commit_sha: { $in: commitShas } }, { commit_sha: { $eq: '' } }] };Playbook 特别提醒:若 Chroma 对缺失字段的$or处理不干净,退化方案是在filterByRecency()里做后置过滤(该方法本就在遍历结果),并核实$in对缺少该元数据键的文档行为; - 新过滤器通过
$and与既有的docTypeFilter、projectFilter组合。
Playbook 记录的实现选择是把 buildWhereFilter() 重构为「条件数组模式」——当前主分支代码正是这一模式的体现:先向 filters 数组按序 push doc_type 条件、project 条件($or 兼容 merged_into_project)、platform_source 条件,最后零条件返回 undefined、单条件直接返回、多条件包 $and(见 过滤组合逻辑)。分支过滤在此模式下的接入点是向数组追加一个 $or 条件即可,无需改写既有组合逻辑。同时 search() 方法负责把 StrategySearchOptions 中的 commit_sha 归一化为字符串或数组后传入过滤器构造器,消除调用方传参形态不一的问题。
从源码结构看,这一「条件数组 + 按需组合」的写法正是为 Phase 01 的扩展铺路:分支过滤作为第五个条件加入,与 doc_type/project/platform_source 正交。
任务六:ensureBackfilled 回填路径与 migration 26
Playbook 的关键洞察是:回填查询 SELECT * FROM observations 本就会返回 SQLite 中已有的 branch/commit_sha 列(这些列是此前 Phase 01–05 建 schema 时加入的),而查询结果被整体 cast 为 StoredObservation[]——类型补齐后,字段会经由 backfillObservations → formatObservationDocs() 自动流入 Chroma 元数据,回填「免费」获得分支感知。
但验证 summary 回填时发现了真正的缺口:session_summaries 表完全没有 branch/commit_sha 列。修复分三步:
- 新增 migration 26(
addSummaryBranchColumns())为session_summaries加列; - 更新
storeSummary()与storeObservations()中的 summary INSERT 语句写入 branch/commit_sha; - 此后 observation 与 summary 的回填(分别见 backfillSummaries)都能把分支元数据正确传播到 Chroma。
这一节也展示了回填的容错机制:backfillKind 的水印推进是「行原子」而非「批原子」——一行记录展开的多个 Chroma 文档必须全部落盘才会 bump 水印,任何部分写入都会把该行记入 pending 状态待下次重试,防止重启导致某行的「尾部文档」永久滞留。
任务七:19 个测试覆盖同步与过滤
Playbook 要求新建 tests/chroma-branch-sync.test.ts,风格对齐 tests/branch-memory-integration.test.ts,并对 ChromaMcpManager 做 mock/stub(测试环境中 Chroma MCP server 不可用)。最终交付两个 describe 块共 19 个用例:
- 「Chroma Branch Metadata Sync」(13 个用例):
formatObservationDocs在 branch/commit_sha 存在时写入元数据;- branch/commit_sha 为 null/undefined 时从元数据中省略(向后兼容);
syncObservation正确把 branch/commit_sha 透传到格式化文档。
- 「ChromaSearchStrategy buildWhereFilter with branch filtering」(6 个用例):
- commit_sha 数组生成正确的
$or/$and组合过滤器; - 未提供 commit_sha 时生成向后兼容过滤器;
- 单个字符串与数组两种形态的归一化。
- commit_sha 数组生成正确的
同步侧测试 mock addDocuments,过滤侧测试 mock queryChroma,使测试不依赖真实的 Chroma MCP server。
任务八:全量测试与构建验证
收尾验证记录:npm test 全量 1224 通过、3 跳过、3 失败——19 个新增 chroma-branch-sync 用例全部通过,3 个失败来自 main 分支提交中的 renderMarkdownEmptyState,与分支记忆改动无关;npm run build-and-sync 完成,全部产物编译并同步至 marketplace。
小结:为向量库加维度的通用配方
回溯 Playbook 的八项任务,它实际上示范了一套可复用的模式,把「一个新的过滤维度」安全地加进既有向量同步栈:
- 契约先行:三个接口(存储形态 × 2 + 查询元数据 × 1)同步补字段,数据流任何一环不缺位;
- 稀疏写入:格式化器用守卫模式「有则写、无则省」,写入层再清洗空值,旧文档与旧语义零扰动;
- 过滤向后兼容:新维度用
$or(新条件 OR 缺失/空值)包裹,再经条件数组以$and与既有正交条件组合; - 回填白拿:
SELECT *+ 类型 cast 让已落库列自动流经既有格式化器,唯一要补的是 schema 迁移(本例的 migration 26); - 测试双保险:格式化器(存在/缺失两态)与过滤器(有/无维度、单值/数组)各自独立 mock,不依赖真实 Chroma。
该 Playbook 所属的分支记忆系列还包括后续的 Viewer 分支展示 与 MCP 输出及验证 两个阶段,读者可结合它们了解分支维度从向量层向展示层与 MCP 工具层继续延伸的完整路径。
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