首页
/ Claude-Mem Branch Memory Phase 01:为每条 Observation 持久化 Git 分支与 Commit 元数据的完整实现方案

Claude-Mem Branch Memory Phase 01:为每条 Observation 持久化 Git 分支与 Commit 元数据的完整实现方案

2026-09-06 11:32:11作者:庞队千Virginia

本文基于 Claude-Mem 仓库中的 Branch Memory 实施计划文档 BRANCH-MEMORY-01.md 撰写,系统讲解 Phase 01 的完整技术内容:如何通过一次数据库 schema 迁移、一个 Git 分支检测工具函数,把 branch 名与 commit SHA 沿「Hook 层 → Worker → SQLite 存储」整条 Observation 管线串起来。读完后你将掌握:Claude-Mem 的 SQLite 迁移模式如何落地新列、Hook 层如何安全调用 git 命令而不破坏 stderr 纪律,以及为什么内容去重哈希必须纳入 branch 维度——这些是后续所有分支感知检索功能的地基。

需要说明一个概念区分:这里的 “Branch Memory” 指的是给 observation(工具调用记忆)打上用户代码仓库的 git 分支元数据,与仓库文档 branches.mdx 中描述的 Claude-Mem 自身三条发布分支main / core-dev / community-edge)是两回事,不要混淆。

背景与目标:为什么 Observation 需要分支元数据

Claude-Mem 的核心工作流是:Agent 会话期间的工具调用由 Hook 捕获,经 Worker 压缩后写入 SQLite(observations 表),再在未来会话中按需注入上下文。原始计划文档开宗明义地指出 Phase 01 的目标:

By the end, every new observation will be stored with its git branch name and commit SHA. This is the critical foundation that all subsequent phases build on.

也就是说,在 Phase 01 完成之前,存储层只知道“这个项目、这个会话里发生过什么”,却不知道“它发生在哪个分支、哪个提交上”。而真实开发中,同一个项目经常同时在 main、特性分支、detached HEAD 上工作,不同分支上的代码状态、甚至同名文件的内容都完全不同。若记忆不带分支坐标,跨分支检索时就会把 A 分支的结论错误注入 B 分支的上下文。Phase 01 因此被定位为“后续所有阶段的地基”:它不引入任何新的检索能力,只保证每条新 observation 落库时自带 branchcommit_sha 两个坐标

从源码结构看,这条数据链路涉及四段代码:Hook CLI(src/cli/hook-command.tssrc/cli/handlers/observation.ts)、Worker 路由与 SessionManager(src/services/worker/http/routes/SessionRoutes.ts)、以及存储层(src/services/sqlite/observations/store.tssrc/services/sqlite/SessionStore.ts)。Phase 01 的五个任务恰好逐段覆盖这条链路。

任务一:Schema 迁移与 Observation 类型更新

迁移 24:为 observations 表增加两列

计划文档要求的新迁移方法 addObservationBranchColumns() 必须严格复刻仓库既有的迁移模式。当前代码中该模式的基准是 SessionStore.ts 中的 addSessionCustomTitleColumn()(migration 23),其结构是:

private addSessionCustomTitleColumn(): void {
  const applied = this.db.prepare('SELECT version FROM schema_versions WHERE version = ?').get(23);
  const tableInfo = this.db.query('PRAGMA table_info(sdk_sessions)').all();
  const hasColumn = tableInfo.some(col => col.name === 'custom_title');

  if (applied && hasColumn) return;

  if (!hasColumn) {
    this.db.run('ALTER TABLE sdk_sessions ADD COLUMN custom_title TEXT');
  }
  if (!applied) {
    this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)')
      .run(23, new Date().toISOString());
  }
}

可以看到既有模式的三要素:版本号查询schema_versions 表)、PRAGMA table_info 列存在性守卫(保证重复执行幂等)、先补列再记版本。计划文档要求 addObservationBranchColumns() 完全照此办理,版本号用 24,守卫对象换成 observations 表:

  • schema_versions 是否已有 version 24;
  • PRAGMA table_info(observations) 分别守卫 branchcommit_sha 两列;
  • ALTER TABLE observations ADD COLUMN branch TEXT
  • ALTER TABLE observations ADD COLUMN commit_sha TEXT
  • 最后 INSERT OR IGNORE version 24 进 schema_versions
  • 并在 runAllMigrations()最后一个位置追加 this.addObservationBranchColumns(); 调用。

调用位置“放在最后”并非随意:从当前 SessionStore 构造函数 的迁移链可以看到,各迁移严格按顺序串行执行(ensureWorkerPortColumn → … → addObservationContentHashColumnaddSessionCustomTitleColumn → …),新迁移追加到链尾可确保它看到前面迁移产生的全部结构,也避免打乱既有版本号的语义。

一点差异说明:计划文档把迁移 runner 定位在 src/services/sqlite/migrations/runner.ts;而在当前 main 线 checkout 中,迁移链实际内聚在 SessionStore.ts 的构造函数里(src/services/sqlite/migrations/ 目录并不存在)。实现时应以当前代码的实际组织为准,把新方法挂进现有迁移链。

类型层改动:三处接口各加字段

计划文档要求在 observation 类型文件中做三处更新(文档指定路径为 src/services/sqlite/observations/types.ts;当前 checkout 中 observation 模块由 store.tsget.tsrecent.ts 等文件构成,类型定义的具体落点以实际代码为准):

类型 新增字段 说明
ObservationInput branch?: stringcommit_sha?: string 写入路径的入参,单值即可,一条 observation 只属于一个分支坐标
GetObservationsByIdsOptions branch?: string | string[]commit_sha?: string | string[] 查询选项。计划文档特别注明:数组支持是为后续阶段基于祖先关系做 IN 子句过滤预留的——例如一次传入某分支的所有祖先 commit SHA,即可圈定“该分支历史窗口内”的 observation
AllRecentObservationRow branch?: string | nullcommit_sha?: string | null 近期 observation 行的返回类型,null 表示历史数据(迁移前写入的行)无分支信息

GetObservationsByIdsOptions 中“标量或数组”的二态设计是这条链路里最值得记住的一点:写入侧永远单值,读取侧允许集合,类型在入口处就把两种查询形态都表达了出来。

任务二:Git 分支检测工具 git-branch.ts

计划文档指定新建 src/services/integrations/git-branch.ts(该集成目录当前位于 src/services/integrations/,已有 11 个集成文件,git 检测工具放这里符合既有组织方式),对外导出:

export interface BranchInfo {
  branch: string | null;
  commitSha: string | null;
}

export async function detectCurrentBranch(cwd: string): Promise<BranchInfo>

函数内部执行两条 git 命令,都带 { cwd } 选项,把命令限定在会话所在仓库目录内:

  • 分支名:git rev-parse --abbrev-ref HEAD
  • 提交 SHA:git rev-parse HEAD

计划文档对实现方式的要求是“先搜索现有代码库的 spawn 模式(Bun.spawnchild_process 用法),与项目约定保持一致”。这一点有明确的工程理由:Hook 进程的生命周期极短且 stdout/stderr 有严格纪律约束,src/cli/handlers/observation.ts 文件头部的注释即声明了这条纪律——

this handler is PURE. It returns a HookResult and MUST NOT call process.stderr.write / process.stdout.write / console.* / process.exit

四条硬性实现约束:

  1. 整体 try/catch,任何失败返回 { branch: null, commitSha: null }。这是计划文档标注为 “Critical” 的一条:非 git 仓库、未安装 git、权限异常等任何情况都只能静默降级,绝不能让分支检测把 Hook 进程打崩。Hook 层 stderr 被抑制,一个未捕获异常会直接破坏整个 observation 上报。
  2. detached HEAD 特判git rev-parse --abbrev-ref HEAD 在 detached HEAD 下会输出字面量 "HEAD"。此时把 branch 置为 null,但仍然捕获 commit SHA——detached 状态下 commit 坐标依然有效,只是没有分支名。
  3. 输出 trim:两条命令的 stdout 通常带尾部换行,必须去除空白后再赋值。
  4. cwd 参数化:不依赖调用进程自身的工作目录,由 Hook 输入的 input.cwd 传入,保证在 Claude Code 等宿主进程 cwd 与目标仓库不一致时依然检测正确。

这四条约束合起来使 detectCurrentBranch 成为一个“永不抛错、永不阻塞、结果可为空”的探测函数,恰好匹配 Hook 环境的容错要求。

任务三:Hook 层 → Worker POST 体的元数据透传

分支元数据进入链路的入口在 CLI Hook 侧,共三步:

第一步,扩展 Hook 输入类型。 在代码库中找到 interface NormalizedHookInput(当前 checkout 中定义于 src/cli/types.ts),新增两个可选字段 branch?: stringcommitSha?: string

第二步,在 Hook 命令入口执行检测。src/cli/hook-command.ts 中,input.platform = platform; 赋值之后追加分支检测:

import { detectCurrentBranch } from '../services/integrations/git-branch.js';

// input.cwd 有值时才检测
if (input.cwd) {
  const { branch, commitSha } = await detectCurrentBranch(input.cwd);
  input.branch = branch;
  input.commitSha = commitSha;
}

input.cwd 为真值才调用,与工具函数“永不抛错”的约定叠加,构成双保险:cwd 缺失时根本不触发 git 子进程。

第三步,把字段写进 POST 请求体。src/cli/handlers/observation.ts 中,观察上报走 POST /api/sessions/observations。当前请求体字段为(见 dispatchToWorker):

{
  contentSessionId: input.sessionId,
  platformSource,
  tool_name: input.toolName,
  tool_input: input.toolInput,
  tool_response: input.toolResponse,
  cwd: input.cwd,
  agentId: input.agentId,
  agentType: input.agentType,
}

计划文档要求在此追加 branch: input.branch ?? nullcommit_sha: input.commitSha ?? null。注意这里统一转为 ?? null 而非直接透传 undefined:JSON 序列化会丢弃 undefined 字段,而显式 null 能让 Worker 侧和存储层区分“检测失败/非 git 环境”(null)与“未检测到字段”。

任务四:Worker 内部透传 → 数据库落库

Worker 侧的改动分路由、链路与存储三层。

路由层SessionRoutes.tshandleObservationsByClaudeId 方法中,从 req.body 取出 branchcommit_sha,随数据对象一并传给 sessionManager.queueObservation()(作为第二个参数的数据结构中新增这两个字段)。

链路层:从 sessionManager.queueObservation 一路追踪到真正调用 storeObservation() 的位置,逐个中间文件(SessionManager → Agent/observation 处理器)在对应数据结构与接口上补齐 branchcommit_sha。计划文档在此强调了一条边界:branch 与 commit_sha 是元数据,应伴随 observation 内容传递,不得进入 SDK Agent 的 prompt——它们不参与 AI 压缩/摘要的语义生成,只是随行的检索坐标。

存储层:在 src/services/sqlite/observations/store.ts 中修改 storeObservation()

  1. 函数签名在 discoveryTokens 参数之后追加 branch?: stringcommitSha?: string
  2. INSERT 的列清单从 15 列扩到 17 列,branch, commit_sha 加在 created_at_epoch 之后;
  3. VALUES 的绑定数组追加 branch ?? nullcommitSha ?? null,占位符同步增加两个 ?
  4. 最后核对占位符数量与列数严格一致——这是 SQLite 参数化查询最常见的低级错误点,计划文档特意单列了一条任务项。

内容去重哈希:为什么必须纳入 branch

同文件中还有一个容易被忽视但计划文档专门点名的小函数 computeObservationContentHash,当前实现是:

export function computeObservationContentHash(
  memorySessionId: string,
  title: string | null,
  narrative: string | null
): string {
  return createHash('sha256')
    .update([memorySessionId || '', title || '', narrative || ''].join('\x00'))
    .digest('hex')
    .slice(0, 16);
}

它用 \x00 连接 memorySessionIdtitlenarrative 后取 SHA-256 前 16 位十六进制作为 observation 的内容指纹(数据库中另有 addObservationContentHashColumn 与唯一内容哈希索引支撑去重)。计划文档要求把哈希输入改为:

(memorySessionId || '') + (branch || '') + (title || '') + (narrative || '')

branch 插入到 memorySessionId 之后。动机写在任务描述里:防止跨分支去重误杀——同一个 memory session 中,开发者切分支后产生了内容完全相同的 observation(相同的标题与叙述文本),在 30 秒去重窗口内,若哈希不含 branch,第二条会被静默丢弃,其携带的“新分支坐标”随之丢失。把 branch 编入哈希后,不同分支上的相同内容各自入库,去重语义从“内容去重”细化为“内容 + 分支坐标去重”。

这个改动同时是“便宜的”:branchnull'' + '' 不改变输入形态,历史行为在单分支场景下保持不变。

任务五:构建与验证

计划文档最后一步给出完整的验证闭环,可直接复制执行:

# 1. 编译全部 TypeScript 并同步到本地插件市场(同时重启 worker)
npm run build-and-sync

build-and-sync 是仓库既有的构建同步命令——branches.mdx 中运行非稳定分支时使用的正是它,它会构建、同步到本地 Claude Code 插件市场并重启 worker。构建中若出现 TypeScript 报错,先修复编译错误再谈验证。

# 2. 验证迁移实际生效:observations 表应出现两个新列
sqlite3 ~/.claude-mem/claude-mem.db "PRAGMA table_info(observations);"

预期输出中能看到 branchcommit_sha 两列(TEXT 类型)。最后确认 worker 能正常启动,排除回归。这三步分别验证了“编译正确”“schema 正确”“运行时正确”三个层面,对应 Phase 01 三个交付物。

当前仓库实现状态核对

为避免读者误以为本功能已在当前代码库生效,用仓库实际内容对照如下(以当前 checkout 为准):

计划项 当前 checkout 实际状态 证据路径
observations 表 branch/commit_sha 列(migration 24) 未实现:当前迁移链中 version 24 已被 addSessionPlatformSourceColumnsdk_sessions.platform_source)占用,observations 表尚无分支列 SessionStore.ts 迁移链migration 24 现占用方
git-branch.ts 检测工具 未实现src/services/integrations/ 下无该文件 src/services/integrations/
NormalizedHookInput.branch/commitSha 与 observation POST 体分支字段 未实现:POST 体当前仅含 contentSessionIdplatformSourcetool_nametool_inputtool_responsecwdagentIdagentType observation.ts
computeObservationContentHash 纳入 branch 未实现:仍为三段输入(memorySessionIdtitlenarrative store.ts

对照上表可以推断:该功能在计划文档(全部任务已勾选)与当前 main 线代码之间存在差异,实现可能位于仓库的 core-dev / community-edge 开发分支上(仓库采用 三条发布分支 模型,运行时改动不直接进 main),或者迁移版本号需要在合并时重新分配(当前 version 24 已被占用,落地时应顺延为未占用的新版本号,并同样遵循“PRAGMA 守卫 + schema_versions 记账”的幂等模式)。这也反过来印证了 Phase 01 设计成“纯增量、可独立验证”的价值:它不改动任何既有查询路径,合并冲突面小,且每一步都可用 PRAGMA table_info 这类零成本命令验收。

小结与后续阶段的接口

Phase 01 交付的是一条端到端的元数据管道:detectCurrentBranch 在 Hook 入口以“永不抛错”的方式采集坐标,NormalizedHookInput → POST 体 → queueObservationstoreObservation 逐级透传,最终两列落库,并把 branch 编入内容哈希以修正跨分支去重语义。同时它给后续阶段留好了两个明确接口:GetObservationsByIdsOptions 的数组形态字段支撑分支祖先 IN 过滤,AllRecentObservationRow 的可空返回类型区分新旧数据。对仓库维护者而言,本文的“当前状态核对”表可以直接当作合并前的 diff checklist;对读者而言,这条链路展示的“迁移幂等模式 + Hook 容错纪律 + 元数据不入 prompt”三点做法,是 Claude-Mem 这类本地记忆系统中值得复用的工程范式。

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

项目优选

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