首页
/ Claude-mem 分支记忆设计解析:基于 git merge-base --is-ancestor 的祖先解析工具

Claude-mem 分支记忆设计解析:基于 git merge-base --is-ancestor 的祖先解析工具

2026-09-05 17:10:43作者:贡沫苏Truman

本文解析 Claude-mem 分支记忆(Branch Memory)功能中 Phase 02 的核心设计:如何利用 git merge-base --is-ancestor 构建祖先解析工具,让已合并分支的历史观察自动可见、而并行兄弟分支的工作保持隔离。读完后你将理解该工具的函数契约(getCurrentHeadresolveAncestorCommitsresolveVisibleCommitShas)、关键的 "null 即不过滤" 约定、SQL 候选集查询,以及配套测试矩阵的设计思路。

1. 问题背景:分支记忆为何需要祖先解析

Claude-mem 会捕获 Agent 在会话中产生的观察(observations)并持久化存储。当用户在不同 Git 分支上工作时,期望的记忆可见性应当"像 Git 一样":已合并进当前分支的提交所关联的观察应当自动可见,而尚未合并的兄弟分支上的工作应当不可见

这一行为无法靠分支名字符串匹配实现(分支会重命名、会消失、存在 detached HEAD),必须依赖 Git 的历史图(DAG)本身。Phase 02 的规格文档(BRANCH-MEMORY-02.md)正是为此定义了核心工具:给定一组来自存储观察的提交 SHA,判断哪些是当前 HEAD 的祖先。该工具同时是 Phase 03(上下文构建器,context builder)和 Phase 04(搜索系统,search system)的前置依赖。

其上游是 Phase 01(BRANCH-MEMORY-01.md)建立的写入路径:为 observations 表增加 branchcommit_sha 列,并通过 hook 层(hook-command.ts)把分支元数据线程化地传入 worker 路由(SessionRoutes.ts)直至数据库。可以说:Phase 01 负责"记录观察发生在哪个提交上",Phase 02 负责"决定哪些被记录的提交对当前工作可见"。

2. 核心工具:git-ancestry 的函数契约

规格文档规定该工具位于 src/services/integrations/git-ancestry.ts(与 Phase 01 的 git-branch.ts 同一目录、复用其 spawn 模式),导出三个异步函数。

2.1 getCurrentHead:获取当前 HEAD

async function getCurrentHead(cwd: string): Promise<string | null>
  • 执行 git rev-parse HEAD,成功时返回完整的 40 字符 SHA;
  • 任何失败(非 Git 仓库、git 未安装等)返回 null,而不是抛出异常。

这个"失败返回 null"的约定是整个可见性模型的地基:它让上层能够区分"当前目录不是 Git 仓库"与"是仓库但解析失败"两种情况,并据此做出完全不同的可见性决策(见 2.3 节的 null 约定)。

2.2 resolveAncestorCommits:并发祖先判定

async function resolveAncestorCommits(
  currentHead: string,
  candidateCommitShas: string[],
  cwd: string
): Promise<string[]>

对每个候选 SHA 执行:

git merge-base --is-ancestor <candidate> <currentHead>

这条命令的语义是整个方案的关键:它以退出码而非输出表达结论——退出码 0 表示候选提交是 <currentHead> 的祖先(含自身,即 AA 的祖先),非 0 表示不是。相比解析 git log 文本,退出码判定无需解析任意输出,天然抗 locale 与格式变化。

规格文档同时规定了两条工程约束:

  1. 并发执行:使用 Promise.all 并行发起所有判定。每次 git merge-base 调用都很快且彼此独立,串行执行会把 N 次进程启动的延迟线性叠加,而并发将其压到约单次调用量级。
  2. 逐 SHA 优雅容错:某个 SHA 上的 git merge-base 失败(例如该提交已被垃圾回收而不再存在于对象库中)时,只排除该 SHA,而不是让整个批次失败。这保证了单个陈旧记录不会拖垮整条查询链路。

函数最终返回 candidateCommitShas 中确认为祖先的子集。另外,若 candidateCommitShas 为空,直接返回空数组,不发起任何 git 调用——短路判断避免了无谓的进程启动开销。

2.3 resolveVisibleCommitShas:组合函数与 null 约定

async function resolveVisibleCommitShas(
  candidateCommitShas: string[],
  cwd: string
): Promise<string[] | null>

这是给调用方(上下文构建器与搜索管理器)使用的高层入口,内部逻辑为:

  1. 通过 getCurrentHead(cwd) 获取当前 HEAD;
  2. 若为 null(当前目录不是 Git 仓库),直接返回 null
  3. 若候选集为空,返回空数组;
  4. 否则调用 resolveAncestorCommits 过滤,返回可见 SHA 列表。

这里最值得咀嚼的是 null 返回值约定,它承担了一个三态语义:

返回值 场景 调用方行为
null 不在 Git 仓库中 不过滤,显示全部观察
[](空数组) 在 Git 仓库中,但没有任何候选提交是 HEAD 的祖先 只显示当前分支自身的工作(分支相关结果为空)
string[] 在 Git 仓库中,存在可见祖先 按该列表做 IN 过滤

规格文档明确指出这一约定让调用方能够区分"非 Git 仓库(全部可见)"与"在 Git 仓库中但未找到祖先(分支工作不可见)"。若两者都返回空数组,非 Git 目录(例如用户在一个普通文件夹里使用 Agent)的记忆将被错误地清空——这是一个容易被忽略、但会直接造成"记忆丢失"假象的边界情况。

3. 数据源:候选 SHA 的 SQL 查询

祖先解析的输入来自数据库。规格文档要求在 src/services/sqlite/observations/get.ts 中新增导出函数:

getUniqueCommitShasForProject(db: Database, project: string): string[]

对应 SQL:

SELECT DISTINCT commit_sha
FROM observations
WHERE project = ?
AND commit_sha IS NOT NULL

返回扁平的提交 SHA 字符串数组。设计上有两点值得注意:

  • 按项目隔离WHERE project = ? 保证候选集只包含当前项目的观察,避免跨项目污染祖先判定;
  • DISTINCT + 空值过滤:同一提交可能关联多条观察,去重可显著压缩后续 git merge-base 调用次数;commit_sha IS NOT NULL 则把 Phase 01 迁移前写入的、尚未记录提交信息的旧观察排除在过滤链路之外。

该函数由上下文构建器与搜索管理器共同调用,作为祖先解析前的候选集来源,保证两条读取路径使用同一口径。

4. 测试设计:面向边界条件的用例矩阵

规格文档为 tests/git-ancestry.test.ts 规划了五类测试,覆盖了可见性模型的全部关键边界:

  1. getCurrentHead 正常路径:在本仓库目录下运行,应返回 40 位十六进制字符串;
  2. 祖先判定的两个基准点
    • 当前 HEAD 自身的 SHA 应被判定为自身的祖先(git merge-base --is-ancestor 对相同提交返回 0)——这保证了"当前分支最新提交上的观察立即可见";
    • git log 中取一个较旧的提交 SHA,验证它确实是 HEAD 的祖先;
  3. 不存在的 SHA:传入伪造 SHA(如 '0000000000000000000000000000000000000000'),验证它被优雅排除而非抛出异常——这正是 2.2 节逐 SHA 容错约束的回归保障;
  4. null 安全:对非 Git 目录(如 /tmp)调用 resolveVisibleCommitShas,应返回 null
  5. 空候选集:传入空数组应直接得到空数组,且不应触发任何 git 调用。

最后一步要求运行 tests/git-ancestry.test.ts 测试套件并修复所有失败,确保工具在所有边缘情况下都能干净地工作。

5. 在当前仓库快照中的落地状态

需要如实说明的是:规格文档标注的任务均已完成勾选,但在当前仓库快照中,按仓库路径 src/services/integrations/src/services/sqlite/observations/ 的实际内容核对,git-ancestry.tsgit-branch.tstests/git-ancestry.test.ts 均未出现在对应位置,全仓库范围内也未检索到 merge-baseis-ancestorresolveVisibleCommitShas 等标识符。可以推断该分支记忆工作流可能是在独立的发布分支上推进(项目规划文档中确实存在多发布分支策略的讨论,见 plans/2026-07-05-three-release-branches.md),或尚未合入当前快照。

与此同时,分支元数据线程化的部分痕迹是存在的:hook-command.tsSessionRoutes.ts 中均有 branch 相关字段的处理逻辑,从源码结构看,Phase 01 定义的 hook → worker → 数据库元数据链路在读取侧已有部分落地,而 Phase 02 的祖先解析作为其消费端,在上述文件中暂时缺席。

6. 小结:一个"退出码驱动"的可见性过滤器

把整篇规格浓缩起来,Phase 02 交付的是一个契约清晰、边界明确的可见性过滤器:

  • 输入getUniqueCommitShasForProject 按项目去重后的候选 SHA 集;
  • 判定:对每个 SHA 并发执行 git merge-base --is-ancestor <sha> <HEAD>,以退出码 0/非 0 表达祖先关系,逐 SHA 容错;
  • 输出:三态结果(null = 不过滤 / [] = 无可见分支工作 / string[] = 可见集),供 Phase 03 上下文构建器与 Phase 04 搜索系统做 IN 过滤。

其设计取舍——用 Git 历史图而非分支名做判定、用退出码而非文本解析做结论、用 null 约定表达三态、用 Promise.all 压平进程启动延迟——都是针对"记忆系统嵌入真实 Git 工作流"这一场景的针对性选择,也为后续分支记忆各阶段(Chroma 向量同步、查看器分支展示等,见 plans.maestro/playbooks 下的分支对齐文档)提供了统一的地基。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384