Claude-mem 分支记忆设计解析:基于 git merge-base --is-ancestor 的祖先解析工具
本文解析 Claude-mem 分支记忆(Branch Memory)功能中 Phase 02 的核心设计:如何利用 git merge-base --is-ancestor 构建祖先解析工具,让已合并分支的历史观察自动可见、而并行兄弟分支的工作保持隔离。读完后你将理解该工具的函数契约(getCurrentHead、resolveAncestorCommits、resolveVisibleCommitShas)、关键的 "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 表增加 branch 与 commit_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> 的祖先(含自身,即 A 是 A 的祖先),非 0 表示不是。相比解析 git log 文本,退出码判定无需解析任意输出,天然抗 locale 与格式变化。
规格文档同时规定了两条工程约束:
- 并发执行:使用
Promise.all并行发起所有判定。每次git merge-base调用都很快且彼此独立,串行执行会把 N 次进程启动的延迟线性叠加,而并发将其压到约单次调用量级。 - 逐 SHA 优雅容错:某个 SHA 上的
git merge-base失败(例如该提交已被垃圾回收而不再存在于对象库中)时,只排除该 SHA,而不是让整个批次失败。这保证了单个陈旧记录不会拖垮整条查询链路。
函数最终返回 candidateCommitShas 中确认为祖先的子集。另外,若 candidateCommitShas 为空,直接返回空数组,不发起任何 git 调用——短路判断避免了无谓的进程启动开销。
2.3 resolveVisibleCommitShas:组合函数与 null 约定
async function resolveVisibleCommitShas(
candidateCommitShas: string[],
cwd: string
): Promise<string[] | null>
这是给调用方(上下文构建器与搜索管理器)使用的高层入口,内部逻辑为:
- 通过
getCurrentHead(cwd)获取当前 HEAD; - 若为
null(当前目录不是 Git 仓库),直接返回null; - 若候选集为空,返回空数组;
- 否则调用
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 规划了五类测试,覆盖了可见性模型的全部关键边界:
getCurrentHead正常路径:在本仓库目录下运行,应返回 40 位十六进制字符串;- 祖先判定的两个基准点:
- 当前 HEAD 自身的 SHA 应被判定为自身的祖先(
git merge-base --is-ancestor对相同提交返回 0)——这保证了"当前分支最新提交上的观察立即可见"; - 从
git log中取一个较旧的提交 SHA,验证它确实是 HEAD 的祖先;
- 当前 HEAD 自身的 SHA 应被判定为自身的祖先(
- 不存在的 SHA:传入伪造 SHA(如
'0000000000000000000000000000000000000000'),验证它被优雅排除而非抛出异常——这正是 2.2 节逐 SHA 容错约束的回归保障; - null 安全:对非 Git 目录(如
/tmp)调用resolveVisibleCommitShas,应返回null; - 空候选集:传入空数组应直接得到空数组,且不应触发任何 git 调用。
最后一步要求运行 tests/git-ancestry.test.ts 测试套件并修复所有失败,确保工具在所有边缘情况下都能干净地工作。
5. 在当前仓库快照中的落地状态
需要如实说明的是:规格文档标注的任务均已完成勾选,但在当前仓库快照中,按仓库路径 src/services/integrations/ 与 src/services/sqlite/observations/ 的实际内容核对,git-ancestry.ts、git-branch.ts 与 tests/git-ancestry.test.ts 均未出现在对应位置,全仓库范围内也未检索到 merge-base、is-ancestor、resolveVisibleCommitShas 等标识符。可以推断该分支记忆工作流可能是在独立的发布分支上推进(项目规划文档中确实存在多发布分支策略的讨论,见 plans/2026-07-05-three-release-branches.md),或尚未合入当前快照。
与此同时,分支元数据线程化的部分痕迹是存在的:hook-command.ts 与 SessionRoutes.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 下的分支对齐文档)提供了统一的地基。
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 StartedRust0623
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