mcpField
C
context-keeper

context-keeper

本地服务
github.com/jarmstrong158记忆与知识112 周前更新

关于 context-keeper

context-keeper 是面向 Claude 的离线优先项目记忆 MCP 服务,用于跨会话保存决策、流程和约束。它将内容存为带类型的可检索条目,并提供生命周期管理与质量检查。

主要功能

  1. 01记录决策、流程和约束
  2. 02按相关性或精确字段检索
  3. 03追踪替代关系与过期条目
  4. 04检查条目质量和矛盾
  5. 05导入和导出项目快照

适用场景

  1. 01在新的 Claude 会话中延续项目规则
  2. 02检索工作流程或过往设计决策
  3. 03检查过期或互相矛盾的记忆条目

MCP 相关

向 MCP 客户端提供记录、检索、更新和导出结构化项目记忆的工具。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"jarmstrong158-context-keeper": {
"command": "uvx",
"args": [
"context-keeper-mcp"
],
"env": {
"CONTEXT_KEEPER_PROJECT": "<CONTEXT_KEEPER_PROJECT>"
}
}
}
}
启动命令uvx context-keeper-mcp
需要配置的环境变量
CONTEXT_KEEPER_PROJECTAbsolute path to the project directory. If omitted, uses cwd only when it already contains a .context/ directory; otherwise tools require explicit project_dir.

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
pypicontext-keeper-mcp0.19.0

工具

14

收录的工具列表 · 更新于 2 周前

参数类型必填说明
kindenum是要记录的条目类型。decision | constraint | pipeline
namestring否pipeline:名称。
rulestring否constraint:规则,使用祈使语气。
tagsarray否用于检索的标签。
scopestring否constraint:'global' 或文件/模块路径。
stepsarray否pipeline:有序步骤。
originenum否作者(默认为 agent)。user | agent | import
reasonstring否constraint:存在原因。至少 40 个字符。
problemstring否decision:导致做出该决定的问题。至少 40 个字符。
purposestring否pipeline:存在原因。至少 40 个字符。
summarystring否decision:所做的决定(1–2 句话)。
hardnessenum否constraint:绝对要求还是建议。absolute | advisory
tradeoffsstring否decision:放弃了什么。建议填写。
related_toarray否相关条目的 ID;get_context 会遍历这些条目。
supersedesarray否decision:此决定取代的先前决定的 ID。
why_chosenstring否decision:理由。至少 60 个字符。
constraintsarray否适用的流水线规则。
enforced_bystring否约束:用于检查它的测试/命令(仅命名,绝不运行)。
project_dirstring否目标项目的绝对路径。如有需要,创建 .context/。
alternativesarray否决策:考虑过的选项及拒否原因。
what_we_triedstring否决策:之前尝试过的方法。建议填写。
when_to_invokestring否流水线:触发条件。建议填写。
retrieval_hintsarray否用于检索的 2-4 种替代表述。
constraints_createdarray否决策:由此引入的新约束。
triggering_incidentstring否约束:其背后的事件。

详细介绍

Context Keeper

xylem 技术栈的一部分。

为 Claude 提供项目记忆。记录设计决策、流水线流程和约束,让 Claude 在不同对话中保持上下文。

问题

随着对话变长,Claude 会忘记早期决策背后的“原因”。新对话则从空白开始。这会导致 Claude 做出破坏既定模式的更改——例如重写它不记得存在的流水线步骤。

解决方案

Context Keeper 为 Claude 提供 14 种工具,用于记录和检索结构化项目上下文:

工具用途
record_entry统一写入工具——通过 kind 记录决策、流水线或约束,并在服务端按类型验证字段。整合了原先的 record_decision/record_pipeline/record_constraint(为保持向后兼容,仍可通过这些名称调用)
get_context按查询、标签、范围或 ID 检索相关条目——按相关性排序,默认提取 related_to 链接
query_entries按精确结构化字段筛选(状态、来源、标签、范围、硬性程度、取代关系、日期)——结果确定、不排序;区别于 get_context 的相关性搜索
get_project_summary为对话开始时提供简要概览
update_entry按 ID 更新任意条目
deprecate_entry根据原因停用条目(可选的 merge_into 会将重复条目合并到保留条目中)
prune_stale查找近期未验证的条目
get_compaction_report检查上次压缩是否丢失了上下文
verify_quality扫描条目,检查理由是否过于简略、标签是否缺失、关联脉络是否孤立(由 PreCompact 钩子自动调用)
export_markdown从决策存储重新生成 DECISIONS.md——这是只读的派生视图
reload_constraints按需在会话中重新显示仅包含约束的内容块(刷新规则,而非完整存储)
export_snapshot将整个存储写入可提交的 .context-keeper/memory.json.gz,以便通过 git 共享项目记忆
import_snapshot将已提交的快照导入工作存储——不会破坏现有数据;首次使用且存储为空时会自动运行
mirror与可选的远程存储同步:op="pull" 将远程合并到本地(保留最新版本),op="backfill" 将本地推送到远程。未配置远程存储时不执行任何操作

所有数据都以人工可编辑的 JSON 文件形式存储在项目目录中的 .context/ 内。默认情况下零依赖,也可选用语义检索。

这与 Claude Code 内置记忆的关系

Claude Code 自带两种记忆机制:你手动编写的 CLAUDE.md 文件,以及 自动记忆,Claude 会将自由格式的笔记保存到 ~/.claude/projects/<project>/memory/。Context Keeper 并不取代其中任何一种——它的定位不同,而这些差异正是使用它的理由:

自动记忆Context Keeper
形式自由格式的 Markdown,包含索引和主题文件带有经服务端验证字段的类型化条目(决策 / 流程线 / 约束)
深度Claude 写什么就是什么由模式强制执行:problem ≥40 个字符,why_chosen ≥60 个字符,记录时会拒绝内容过于单薄的条目
生命周期编辑或删除文件supersedes(降级,但仍可检索)、deprecate、merge_into、偏移 + 过时扫描
冲突无记录时将重述与矛盾分类,并根据来源确定信任优先级
检索每个会话都会完整加载索引在令牌预算内按相关性排序;对于无法回答的查询,会发出弃答信号
覆盖范围仅限本机;明确不在多台机器间共享通过镜像跨设备共享,通过已提交的快照与团队共享

它们可以互相补充,而非彼此竞争:自动记忆擅长轻松记录偶然偏好,而 Context Keeper 用于管理你希望结构化、可查询、可执行且可移植的决策和规则。两者同时运行没有问题——启用 rules_export 后,Context Keeper 会写入工具环境自身的 .claude/rules/ 目录,而不是绕开它。

如实说明已知缺陷: 子 Agent 不会继承主对话的会话启动注入,因此子 Agent 启动时没有项目摘要。如果你的工作流依赖子 Agent,请让它们显式调用 get_project_summary——检索不可跳过这一特性仅适用于主循环。

功能一览

Context Keeper 是一个轻量、离线优先的记忆层;它的一些功能不太容易被发现,因为它们融入了现有工具,而不是作为独立功能存在。下表用记忆系统术语介绍了这些功能:

功能Context Keeper 的实现方式
程序性记忆record_entry(kind="pipeline") 会存储有序且考虑依赖关系的工作流(构建/部署/数据流),并附带 purpose + when_to_invoke,形成可重复使用的“我们如何执行 X”,而不仅仅是事实。
去重每次 record_* 都会针对存储库执行词集 Jaccard 比对;如果新条目重述了现有条目,就会返回 similar_entries,从而在录入时发现重复项。随后,deprecate_entry(merge_into=...) 会将重复项中的独有内容合并到保留项,并以非破坏性的单步操作将重复项退役。
矛盾检测同样的重叠内容会被归类为 likely_restatement 或 likely_contradiction(基于否定词/反义词的极性);如果内容发生反转,就会生成 contradiction_note,提示 Agent 解决冲突,而不是让两条仍然生效的规则互相矛盾。
质量优化verify_quality 会扫描理由单薄、缺少标签、采用旧版架构的条目,以及孤立(未链接)的关系;PreCompact 钩子会自动运行该功能,以便在上下文压缩前丰富条目内容。
取代/衰减/遗忘supersedes 会降低先前决策的优先级,但仍予以保留(可回溯的历史记录);prune_stale 会列出未经验证的条目供审查;deprecate_entry 则会将条目彻底从检索中移除。
来源 + 信任/来源归属每个条目都会记录 origin(user/agent/import);检索时会提高用户陈述条目的信任度,并在条目冲突时据此决定默认采用哪一条。
预期查询retrieval_hints 会存储未来会话可能使用的其他措辞,因此即使查询用词不同,也能在没有嵌入的情况下命中。
混合检索默认使用词法检索(标签 + 词语重叠);选择启用的嵌入余弦混合检索(semantic.enabled)会增强向量召回,并在嵌入器离线时回退到词法检索。
事实元数据查询query_entries 会基于结构化字段的精确谓词筛选条目(状态、来源、任一/全部标签、范围、硬性程度、取代关系、日期),并以 AND 组合,结果具有确定性。这是一条精确查找路径,与 get_context 的模糊相关性排序不同。
便于缓存的注入会话启动时的记忆块采用确定性排序,包含稳定前缀;每个会话唯一易变的行(质量扫描 ID)会放在最后,因此存储内容未变时,各会话注入的文本逐字节相同。
由路径触发的规则范围限定的约束会投射为 Claude Code 自带的 .claude/rules/*.md 格式,并带有 paths: 前置元数据(rules_export);这样一来,Agent 读取受覆盖文件时,工具框架就会加载相应规则,早于编辑操作,且无需钩子。对于不支持规则的客户端,scope_guard PreToolUse 钩子会覆盖写入路径。
叙述 + 聚类get_project_summary 会将超过阈值的决策按主题聚类,并生成简洁叙述;DECISIONS.md 投影则将存储内容呈现为易读的文字。
数据导出/离线/隐私.context/ 中的数据采用纯 JSON 格式,可供阅读、编辑、grep 和提交;完全离线运行,无需任何依赖,也不会有数据离开本机。
评测与基准测试(开放方法)

检索和诚实性属性是通过测量得出的,而非仅仅声称如此——测试框架位于 evals/,无需联网即可复现:

  • Token 减少率 — 会话开始时注入与倾倒完整存储内容的对比:四个真实存储的结果分别为 97.3% / 94.1% / 85.5% / 73.3%(evals/token_reduction.py)。真正有意义的属性是:随着存储增大,注入成本仍大致保持稳定。
  • 检索质量 — 在冻结的 7 个存储语料库上测试 59 个案例,每个问题都根据条目要解决的问题编写,而不是改述条目摘要。选择启用的语义混合将 recall@5 从 0.42 提升至 0.64,MRR 从 0.37 提升至 0.55(evals/run_retrieval_eval.py)。这些数值低于 2026-08-05 之前发布的数据(hit@5 为 80% → 93%),这正是重点:旧测试集的部分问题是通过改述得出的,因此查询与目标共享表面词汇,导致词汇检索率虚高。提升主要集中在大型、以散文为主的存储上;小型存储的词汇检索率已经达到 1.000。
  • 弃答 — 测量 get_context 遇到无答案查询时是否会回答“没有相关内容”,而非凭空编造;在评测集上,0.20 的相关性下限是不会导致错误弃答的最高值(evals/abstention.py)。

每个数据集、指标和注意事项都已提交到代码库——详见 evals/README.md。语料库已冻结在 evals/fixtures/corpus 下,因此任何克隆版本都能复现这些数值,回归测试也可以将其固定;如果需要查看当前情况而非可对比的结果,可以使用 --live 对真实存储运行测试。

v0.19:以替代关系作为信号,并统一范围规则

  • 存储可以说明什么是真的,却无法说明发生了什么变化。 它保存了 36 条记录,其中 36 条有效、0 条已弃用、0 条替代链接;而 dec-013 明确表示它取代了旧的仅追加同步方式,con-006 也在正文中标注了“(原为仅追加)”。deprecate_entry 从 v0.10 起就支持 superseded_by,但一次也没用过:知道答案时没有任何机制提出询问,之后也没有任何机制利用这个链接。

    现在,record_* 会指出涵盖同一主题的同类型有效记录——possible supersession of <id>: <summary>——评分依据是共有标签和整体组件范围的重叠,特意不使用 similar_entries 已经计算的文本重叠(替代项往往与被替代项几乎没有相同措辞,却恰好针对同一件事)。它不会建立链接、阻止写入或改动旧记录:启发式关联可能会悄悄降低一条仍然有效的规则的优先级。

    get_context 会在最前面添加一行简短信息,说明直接前序记录的内容及其变更原因,因为只有检索时会利用链接,建立它才有价值。只追溯一层;预算紧张时会移除这一行,并保留 predecessor_id 线索,而不是移除记录。远程 Worker 会生成完全相同的一行,因此两种传输方式不会出现差异。

    scripts/survey_supersessions.py 以只读方式提出补建链接的建议——它不会打开存储进行写入,也不会调用任何生命周期工具。

  • con-011 是错误的。 它要求所有判断“此范围是否涵盖该文件”的入口保持一致;实际上有五个入口,却有四套实现,其中两套已被证实存在分歧,而且 score_entry 仍使用规则本来要禁止的原始子字符串测试——因此,查询 scope="hooks/" 时,范围为 webhooks/ 的记录获得的加分与真正匹配的记录相同。现在 scope_rules.py 是唯一实现,它不导入任何内容,因此 PreToolUse 钩子可以在 con-010 的约束下加载它。

  • 检索现在有了固定指标。 一个包含负例和历史记录案例的 59 案例黄金集,以及一个锁定词法分支行为的回归测试。第一个锁定测试在一天内就失效了,尽管排名代码一行未改——它读取的是实时存储,所以记录三条内容就会改变测试结果。现在语料库已冻结。

  • 内部结构:server.py 从 4,270 行缩减至 3,787 行(拆分出 ranking.py、mojibake.py、quality_checks.py,并全部重新导出);verify_quality 中内联的七项检查改为使用注册表;测试套件也从一个 5,213 行的文件拆分为六个主题文件。

v0.17:弥补交付缺口

v0.16 在编辑之前将规则呈现给模型。之后对真实存储进行测量,发现了两个会相互叠加的问题,以及一个从一开始就存在的问题。

  • 被截断的条目不再无法发现。 SessionStart 钩子只会打印摘要文本,不会输出其他内容,因此因预算限制而被丢弃的条目不仅没有摘要——Agent 也无从得知它的存在,自然无法主动索取。在最大的真实存储中,有 116 行记忆悄然消失。现在,摘要末尾会列出被丢弃条目的 ID,并将其计入同一预算之内(单独的 ID 只占其所代表条目的一小部分预算),同时按稳定的存储顺序输出,以确保提示词缓存前缀的字节完全一致。summary_dropped_ids 也会以结构化形式携带相同列表。

    必要时,约束条目的最低预算仍会优先于总预算——某个真实存储中的 19 条约束就占用了 2429 个令牌,超过 2000 的预算——因为丢失规则比超出预算更糟糕。超出情况会被报告,不再悄无声息。

  • global 范围现在会被标记为质量问题。 真实项目中,47% 的约束范围为 global,这使它们无法参与偏移检查、.claude/rules/ 投影以及 scope_guard——因此,截断的摘要成了它们触达模型的唯一途径。这两个问题会相互叠加。现在,verify_quality 会标记 global_scope,但只有在能从条目的 enforced_by 或匹配真实目录的标签中确定具体路径时才会标记。对每条全局规则都笼统地建议“考虑添加范围”,只会制造噪声,让读者习惯性地忽略;而且有些规则确实应该是全局的。

  • 子 Agent 也会获得项目规则。 SessionStart 不会为子 Agent 触发,它们也不会继承父 Agent 注入的上下文,因此此前每个子 Agent 启动时都没有项目记忆——一次扇出就会产生十几个从未读过规则的贡献者。新的 subagent_start.py 钩子通过 SubagentStart 注入仅包含约束的区块。只注入约束,并且设有上限:每个启动的 Agent 都会触发一次,因此扇出会使注入量成倍增加。

    "SubagentStart": [
      { "hooks": [{ "type": "command",
                    "command": "python /path/to/context-keeper/hooks/subagent_start.py",
                    "timeout": 5 }] }
    ]
    
  • 发布不再会悄无声息地部分失败。 v0.16.0 已发布到 PyPI 和 MCP registry,随后 bundle 工作流却因清单验证失败而中止——而缺少某个资产的发布,与从未包含该资产的发布无法区分。新增的 verify-release 工作流会确认三个渠道都确实收到了该版本;清单也改为在每个 PR 上验证,而不是等到发布时才验证。

  • constraint_reinject 不再走重量级导入路径。 它的匹配器是 "",因此每次工具调用后都会运行——这是整个钩子集合中触发最频繁的钩子——而它之前为了格式化字符串列表还会导入 server。约束渲染逻辑已移至 store_paths,因此它和 subagent_start 现在都能在 Python 自身启动耗时的基础上约 5ms 内运行。仍然只有一份实现,供所有调用入口共享。

v0.16:按路径触发规则,以及编辑前的规则

此版本的两部分功能都源于阅读 Anthropic 自己的记忆文档,并思考 context-keeper 还没有利用其中哪些机制。

  • 将带范围的约束投影到 .claude/rules/*.md(可选启用)。 Claude Code 在读取匹配文件时会加载带有 paths: frontmatter 的规则文件——这比任何钩子都更早触发,而且无需接入任何钩子。rules_export.enabled 会在每次约束变更的写入路径上,将每条带范围的约束镜像到 .claude/rules/context-keeper/,每个范围对应一个文件。其契约与 DECISIONS.md 投影相同:JSON 保持为规范数据,Markdown 是派生内容,始终整体重新生成,且绝不会被解析回去。 该投影拥有的是一个子目录,而不是 .claude/rules/ 本身, 并且只清理带有自身生成标记的文件——因此,已弃用约束的规则文件会在下次写入时消失,而放在同一文件夹中的手写规则则绝不会被触碰。该标记是块级 HTML 注释,Claude Code 会在文件进入上下文前将其剥离:它以零 token 成本向工具和人工读者标识该文件。

    如果作用域无法转换为实际生效的模式,则会拒绝而非输出:glob 元字符([ ] { } * ?),因为 Claude Code 会将 [ 视为方括号表达式,而未闭合的方括号表达式不会匹配任何内容;以及双引号或控制字符,因为每个模式都会输出为双引号括起来的 YAML 标量,嵌入的引号会提前结束标量,导致整个 frontmatter 块失效——随后,运行环境不会从该文件加载任何规则,连原本有效的模式也不例外。被拒绝的作用域会返回在 skipped_scopes 中,因为悄无声息地不生效的规则文件比没有规则更糟:它看起来像是已有覆盖。

    rules_export.path 必须位于项目目录内。该目录不仅会被写入,还会被清理——每次重新生成时,其中所有带标记的 .md 文件都会成为删除候选项——因此,配置中的 ../../.. 或绝对路径会将删除操作指向用户从未与 context-keeper 关联的目录。路径必须位于项目目录内也合乎情理:Claude Code 只会发现工作树内的 .claude/rules/,因此外部目录无论如何都无法加载。越界路径会被拒绝并返回错误,而约束写入本身仍会成功——投影是派生数据,JSON 存储才是规范数据,配置错误的投影绝不能导致记录失败。

  • scope_guard 现在在 PreToolUse 下运行。 它原先在 PostToolUse 上触发,也就是说规则是在写入已经完成之后才到达——那只是审查备注,不是护栏。PreToolUse 的 additionalContext 会与工具结果一同注入,因此模型仍能采取行动时就能收到约束。 一个脚本即可支持两种接线方式:它读取 hook_event_name,并以匹配的 hookEventName 响应;如果字段缺失,则默认使用 PostToolUse,因此现有安装可原样继续工作,升级只需编辑配置,无需重写。

    可选择启用 scope_guard.confirm_absolute,将 PreToolUse 命中的绝对约束升级为 permissionDecision: "ask",暂停操作并等待用户确认,而不只是添加注释。默认关闭。坦率地说,其限制是:它会根据作用域而非违规情况升级——这里没有读取 diff,因此无法判断编辑是否真的违反规则。

  • 编辑路径现在有了不能悄悄超出的延迟预算。 改用 PreToolUse 意味着钩子会在工具执行之前运行,因此它的耗时会落在每次 Edit 和 Write 的关键路径上,而不是在操作结束后才产生。Windows 上的测量结果显示,钩子耗时约 142ms,其中约 73ms 是 import server——mirror 引入了 urllib.request → http.client → email.parser,还包括 secrets、usage、code_drift,而这个钩子根本不会用到它们。更糟的是,没有匹配任何约束的路径也要耗费同样的 142ms,而这种情况占大多数编辑。

    存储位置和原始读取现在都位于 store_paths.py 中,它只导入 json 和 os。server.py 现在从该文件导入路径解析逻辑,而不再自行定义,因此优先级顺序(环境变量 → Xylem 指针 → cwd → 向上遍历父目录)只有一个实现——第二份副本会逐渐偏离,而偏离的那份恰恰是无人执行的副本。 ~142ms → ~69ms,接近 ~62ms 的纯解释器开销下限。剩余部分是 Python 进程启动开销,shell hook 无法避免。现在,TestEditPathHookCost 会在编辑路径 hook 再次导入 server,或 store_paths 除 json/os 之外还引入其他导入时失败——这项特性已被固定,而非只是修好。按下方所示,用 "timeout": 5 配置 hook,以限制极端情况;对于卡住的 hook,没有其他机制能设定上限。

  • 超出预算的摘要现在会被截断并报告。 截断会话启动摘要时,会从末尾删掉行;预算足够小时,它会一路删到约束块,导致摘要被清空——这是 v0.9 故障(状态看似正常的存储却没有注入任何内容)的另一种表现。现在,截断会在约束块处停止;触及上限时,响应会带上 summary_truncated / summary_lines_dropped / summary_truncation_note。这些键仅在发生截断时出现,因此未截断的常见情况仍能保持字节稳定,便于提示词缓存。Anthropic 的自动记忆索引在遇到读取上限时会报错,而不是静默丢弃内容;原则相同。

  • 存储现在可以报告并修复自身的编码损坏。 con-008 修复了 v0.11 乱码的成因:Windows 上的标准输入默认使用 cp1252,因此客户端的原始 UTF-8 字节在 json.loads 运行之前就被错误解码,导致破折号以 â€" 的形式写入存储。强制传输使用 UTF-8 后,就不会再有新条目损坏。但这无法处理已写入的条目,也没人检查它们。

    这种损坏以最糟糕的方式隐而不见:文本仍然足够易读,以至于没人会重新阅读该条目,因此损坏的理由会悄然影响每次检索到它的结果。现在,verify_quality 会将其标记为 mojibake,而 repair_mojibake 处理程序可以修复它:

    context-keeper repair_mojibake '{}'                # dry run, writes nothing
    context-keeper repair_mojibake '{"apply": true}'   # repair
    

    修复过程会精确逆转损坏,并且会验证修复结果——对候选内容重新应用损坏过程后,必须逐字节还原为输入;未通过检查的内容会保持原样。对他人记录的推理进行不完整修复,比保留清晰可读的损坏更糟,因为它看起来像是已经修好。刻意不刷新 verified_at:修复编码并不意味着有人重新确认了条目仍然属实;重置陈旧度计时器会抹掉 prune_stale 和漂移检查本应发出的信号。updated_at 会更新,这样修正后的副本就会在镜像的“最新优先”合并中胜出,而不会被损坏的远程副本覆盖。

在 .context/config.json 中启用投影,并执行一次回填:

{ "rules_export": { "enabled": true } }
context-keeper export_rules '{}'

export_rules 刻意不加入 tools/list——con-004 限制了每个客户端在会话启动时都要承担的 schema 负载,而这是一次性回填。此后,写入时渲染会让目录保持最新,无需调用工具。

要么提交它,要么忽略它,但务必与存储保持一致。 生成的目录派生自 .context/。如果你的工作存储被 gitignore 忽略(默认情况),也要将 .claude/rules/context-keeper/ 加入 gitignore——否则,新克隆的仓库首次写入约束时,会从一个并不存在的存储重新生成内容,并删除所有已提交文件。如果你通过 export_snapshot 共享记忆,提交 rules 目录没有问题,还能让新队友立即获得适用范围明确的规则。

v0.15:双向镜像(本地 <-> 远程)

可选的容错镜像功能,让第二台设备(例如外出时用来记录决策的手机)既能接收桌面端的记忆,也能贡献自己的记忆。本地 .context/ JSON 存储仍然是规范数据源;context-keeper-remote Cloudflare Worker 是同步界面,绝不是事实来源。(这与提交到 Git 的 export_snapshot 机制不同——镜像是实时的、跨设备的,而 snapshot 是仓库中的版本化数据包。) 冲突解决采用按 updated_at 时间戳后写入者胜出的策略,且两个方向的规则完全相同,因此在任一设备上进行的编辑最终都会在所有设备上保持一致。任何一端都不会删除内容——弃用只会更改状态,并像其他编辑一样传播。

坦诚说明时钟偏差带来的问题。“后写入者”通过比较 ISO-8601 updated_at 字符串来确定,而这些时间戳来自执行写入的机器——本地编辑使用桌面设备的系统时间,远程编辑使用 Worker 的系统时间。若在两台机器的时钟偏差范围内(实际通常不到一秒,且发生在不同设备上)对同一条目进行两次编辑,顺序就可能错乱:时间戳较晚的副本不一定是较晚写入的,因此保护逻辑可能会选错胜出版本。没有逻辑时钟或向量时钟来打破平局——只有系统时间。影响范围有限(单次拉取只会将远程端自身的时间戳相互比较,且这些时间戳来自同一时钟,因此远程端内的顺序准确;只有在本地写入与远程写入同时修改同一 id 时,时钟偏差才会造成影响),而且绝不会悄无声息地发生:每当“最新者胜出”覆盖了内容实质不同的副本时,落败版本都会追加到 .context/.mirror_conflicts.json(见下文),以便你手动协调。

  • 镜像同步(本地 -> 远程)。 每次写入(record_entry/record_*、update_entry、deprecate_entry)后,都会通过远程服务的 upsert_entries MCP 工具推送该条目(每种类型调用一次)。upsert_entries 会保留传入的 ID,且仅当推送条目的 updated_at 更新时才会替换已有的远程副本,因此编辑或弃用条目会实际覆盖过时的远程副本,而不会被跳过。如果远程服务无法访问,该条目会加入 .context/.mirror_queue.json 队列(去重并保留最新状态),并在下次成功推送时刷新队列。推送失败绝不会阻止或导致本地写入失败。
  • 镜像拉取(远程 -> 本地)。 pull_remote 会调用远程服务的 query_entries,并按时间戳合并所有返回的行:如果本地已有同 ID 的远程条目,仅当远程条目的 updated_at 更新时才会覆盖本地副本;如果本地副本更新,则保留本地副本,并在下次写入时推送回远程服务。.context/.mirror_watermark 现在仅作记录提示:每条获取的记录都会按每个条目执行“保留最新版本”的合并,因此绝不会仅因远程条目的时间戳不晚于水位标记就将其丢弃(早期版本会在合并前按水位标记过滤,这可能因设备间时钟偏差而悄悄丢失手机记录的条目——而这正是镜像同步要解决的情况)。此功能已接入 SessionStart 钩子(因此桌面会话开始时就能看到手机记录的条目),并通过 mirror MCP 工具以 op="pull" 的形式提供。
  • 保留冲突,而不是丢弃。 任一方向覆盖实质内容不同(而非仅时间戳不同)的副本时,落败版本会追加到 .context/.mirror_conflicts.json。最后写入者胜出已决定哪个副本作为当前版本;此记录用于追踪被替换的内容。没有冲突解决界面——如需手动协调,可查看此记录。
  • 回填(mirror 配合 op="backfill")会将整个本地存储推送到远程服务(每种类型调用一次 upsert_entries),用于初始化全新的远程服务。此操作幂等:服务器端会跳过相同或更旧的重复推送。
  • 避免 ID 冲突(方案 B:随机后缀)。 两个存储各自独立生成递增 ID 会产生冲突——桌面端和 Worker 都会为不同的决策分配 dec-013,而按 (project, id) 键控的 upsert 会导致一方悄悄覆盖另一方。解决方法:启用镜像同步后,新 ID 会添加一个短随机十六进制后缀,例如 dec-013-a7f3。数字仍在开头(便于排序和搜索);旧 ID 永不重写;关闭镜像同步时,ID 仍保持原样 dec-013(只有一个写入方,不会发生冲突)。
  • 不新增依赖(仅使用标准库 urllib),代码中不存放密钥:远程 URL 将身份验证令牌作为最后一个路径段(/mcp/<token>),且仅从环境变量读取。

在运行 MCP 服务的位置设置一个环境变量即可启用:

CONTEXT_KEEPER_REMOTE_URL=https://context-keeper-remote.<acct>.workers.dev/mcp/<AUTH_TOKEN>
# CONTEXT_KEEPER_REMOTE_TIMEOUT=5    # optional per-request seconds

如果未设置 CONTEXT_KEEPER_REMOTE_URL,所有镜像路径都会静默地不执行任何操作——行为与 v0.15 之前完全相同。

**传输方式:**基于 Streamable HTTP 的无状态 JSON-RPC。每次写入都会向 /mcp/<token> URL 发送一个 POST(tools/call → upsert_entries);拉取操作(mirror 配合 op="pull")会调用远程服务的 query_entries。无需 initialize/session 握手(服务无状态);响应为单个 application/json 正文。

v0.14:去重合并(deprecate_entry(merge_into=...))

捕获时的检测已经能发现近似重复项(similar_entries 中带有 likely_restatement 关系),但要解决此类问题,仍需手动分两步操作:弃用重复项,然后对原条目执行 update_entry,补入它遗漏的内容。v0.14 将这两步合并为一个原子性、非破坏性的操作。

  • 在现有工具中加入可选参数,而不是新建工具。 deprecate_entry(id=<dupe>, reason=..., merge_into=<survivor>) 会将重复项中独有的内容并入保留条目,然后弃用重复项,并设置 superseded_by=<survivor>。如果未提供 merge_into,deprecate_entry 的行为与之前完全一致——逐字节不变。
  • 只增不减,非破坏性。 保留条目只会增加内容:列表字段(tags、retrieval_hints、related_to、constraints/constraints_created)会合并去重,空文本字段会从重复项补齐——保留条目中已有内容的字段绝不会被覆盖。重复项不会被硬删除;它会作为已弃用条目保留在磁盘上,并指向保留条目,因此合并过程完全可审计、可逆。
  • 类型相同、单次写入、先验证。 合并要求两个条目类型相同(以确保它们的架构一致);在任何写入之前,先解析并验证目标;然后在一次文件写入中修改两个条目,避免两项更新相互覆盖。无效的 merge_into(目标不存在、类型不同或指向自身)会明确报错,不会弃用任何条目。
  • 保留 Roots。 操作由用户明确发起,也可由 Agent 调用(与其他所有生命周期工具一样);不增加新依赖、不调用大模型,且结果确定。它简化了捕获循环已有规范的重述处理流程,而不是新增后台进程。
// dec-002 restates dec-001 — merge and retire it in one call
{ "id": "dec-002", "reason": "Restatement of dec-001", "merge_into": "dec-001" }
// -> dec-001 gains dec-002's unique tags/hints/related_to + any text it lacked;
//    dec-002 becomes deprecated with superseded_by = dec-001

v0.13:结构化字段查询(query_entries)

get_context 回答的是*“与我正在做的事情相关的内容有哪些?”*——它按相关性排序,融合可选语义信息,并通过弃答信号标记低相关性结果。这适合模糊召回,但不适合你已明确所需字段值的情况。query_entries 填补了这一空白:对每个条目已有的结构化字段进行确定性筛选,不排序,也不弃答。

  • 精确条件,按 AND 组合: types、status(active/superseded/deprecated)、origin(user/agent/import)、tags_any、tags_all、scope(精确匹配,区分大小写)、hardness(absolute/advisory)、supersedes/superseded_by,以及与 get_context 相同的 since/before 时间筛选条件。每个条件都严格匹配现有字段——查询要么匹配,要么不匹配。
  • 不涉及相关性,也不会编造。 结果按稳定的自然 ID 顺序返回,不包含分数,也没有 min_relevance 下限——空结果集就是实际且诚实的答案,不是弃答消息。弃答机制适用于模糊文本查询;结构化条件无需弃答。
  • 相同的数据存储,相同的预算。 它复用 get_context 使用的完全相同的数据存储读取和条目序列化路径,并将匹配结果集装入相同的令牌预算(默认为 4000,每次调用可通过 token_budget 设置),因此宽泛查询不会一次性倾倒整个数据存储——matched_entries 与 entries_returned,以及 budget_truncated 标记会告知你是否有内容因上限而被截断。
  • 增量式且独立完整。 不增加新依赖,仅使用标准库;不使用嵌入,也不调用大模型——只对磁盘上已有的 JSON 进行纯内存筛选。get_context、语义融合、评分和所有钩子均不受影响;每个现有工具的默认行为都逐字节不变。

与 get_context 有一个有意的区别:query_entries 不应用默认状态筛选,因此除非你传入 status,否则也会返回 superseded 和 deprecated 条目。get_context 始终隐藏已弃用条目;结构化工具则允许你按需查询它们。

示例:

// Absolute constraints scoped to the hooks/ directory
{ "types": ["constraints"], "hardness": "absolute", "scope": "hooks/" }

// User-stated decision that superseded dec-005
{ "origin": "user", "supersedes": "dec-005" }

// Active pipelines tagged "release"
{ "types": ["pipelines"], "status": "active", "tags_any": ["release"] }

// Everything a user asserted this month, across all types
{ "origin": "user", "since": "2026-07-01" }

v0.12:矛盾检测 + 缓存稳定的注入

  • 在捕获时区分重述与矛盾。 相似条目检查已经能发现大量重叠;现在还会对每组重叠进行分类。两种无需依赖项的信号——否定不对称(“X 是必需的”与“X 不是必需的”)和反义词极性(这里是“始终”,那里是“从不”;“启用”与“禁用”)——会将匹配项标记为 likely_restatement 或 likely_contradiction。重述会提示你合并;矛盾会提出 contradiction_note,告知 Agent 应确定哪条规则仍然有效(使用带有 superseded_by 的 deprecate_entry),而不是悄悄保留两条互相冲突的有效规则。仅供参考,而且只会检查 Jaccard 已标记为重叠的条目对——写入始终会继续。没有新增依赖项,不调用 LLM,记录时也不增加令牌数。
  • 会话开始注入保持缓存稳定。 注入的记忆块会按顺序排列,使其中较大的稳定部分——约束、决策、管线和固定的捕获指南——在存储未变化时,能在不同会话间逐字节保持一致,形成前缀;唯一易变的那一行(质量扫描标记的 ID)会放在最后。这样,记忆块就能留在模型可缓存的提示词前缀中,而不会在每个会话中都导致缓存失效。令牌数还会减少,而不是增加。

v0.11:会话中途重新注入约束(可选启用)

SessionStart 钩子会在第一轮注入一次你的约束。随着长会话中逐渐积累工具输出,这些规则会从模型的工作注意力中逐渐淡出,实际上形同衰减——模型可能仅仅因为一小时前获知的约束已被埋没,就违反该约束。v0.11 会在长会话进行中重新呈现这些约束,而不只是在会话开始时呈现。

有两种方式,都仅限约束——它们会重新注入 SessionStart 显示的完整 Absolute/Advisory 区块,不包含存储中的其他内容(无决策、无管线)。这是一次轻量级的规则刷新,而不是再次完整倾倒所有内容。

  • reload_constraints 工具——按需返回当前约束区块。始终可用;想让规则重新进入上下文时,随时调用。
  • constraint_reinject.py 钩子(PostToolUse)——可选启用,默认关闭。 启用后,它会统计每个会话中的工具调用,并通过 additionalContext 每隔 N 次调用重新注入约束区块(every_n_tools,默认值为 25)。

说实话,什么会触发它。 自动路径是 PostToolUse 钩子——这个界面会注入模型,而它的触发频率与工具输出量同步;正是工具输出量实际埋没了规则。它不是计时器:MCP 服务在上下文窗口内没有墙上时钟,因此重新注入由工具调用次数驱动,而不是由经过的秒数驱动。它也不是 PreCompact——PreCompact 的标准输出只会显示给用户,不会注入模型(压缩边界已经由 SessionStart 钩子重新覆盖,该钩子会以 compact 为来源再次触发)。

默认行为保持不变。 未配置(或设为 enabled: false)时,钩子不会执行任何操作,SessionStart 的工作方式与之前完全相同。在 .context/config.json 中启用它:

{ "constraint_reinjection": { "enabled": true, "every_n_tools": 25 } }

v0.10:弃答 + 将取代关系用作排序信号

从研究 Curion 中借鉴了两个想法,同时保持零依赖:

  • get_context 现在可以表示“没有相关内容”。 以前它总是返回得分最高的条目——但综合得分无论相关性如何,都会从时效性/状态/来源中累积约 55 分,因此即使查询中没有相关记忆,也会悄悄返回看起来很有把握的结果。测得无答案查询的编造率为 100%(evals/abstention.py)。现在,响应会包含 top_relevance;当排名最高的条目的标签/文本相关性低于 min_relevance(配置项,默认值为 0.20)时,会返回 no_confident_match: true,并附上指导,提醒 Agent 不要把这些条目当作已证实的事实。它只添加标注,不会屏蔽结果——仍会返回匹配度较低的条目,因此 retrieval_hints 和语义混合所保留的词汇不匹配召回能力不会受到影响。0.20 是在评估集上不会产生误弃答的最高阈值。
  • 将取代关系用作排序信号,而不只是过滤条件。 record_decision 接受 supersedes: [ids]:先前的决策会变为 superseded——在排序中降级,但仍可召回(例如“我们为什么从 X 改为现在这样?”);这与会将条目完全移出检索范围的 deprecate_entry 不同。prune_stale/verify_quality 会跳过已取代的条目(它们是有意保留的历史记录,不是过时工作),并在 DECISIONS.md 投影中标记为 **SUPERSEDED** by dec-NNN。

我们刻意没有采用 Curion 的 LLM 控制器架构(每次存储和召回都要调用 API)。context-keeper 仍保持零依赖,且默认离线运行。

v0.9:主题聚类、更多嵌入后端,以及测量发现的一个错误

  • 关键修复:大型存储的会话开始注入内容为空。 摘要截断循环在判断条件中检查的是原始文本,因此只要存储的摘要超过令牌预算(约 30 多个条目),就会悄悄弹出所有行,并在会话开始时注入空摘要。这个问题是在测量令牌减少量时发现的:一个包含 78 个条目的存储注入了约 0 个令牌的记忆。现在会正确截断至预算范围内。
  • 主题聚类。 当决策超过 8 条时,get_project_summary 会根据决策中出现频率最高的共同标签对其分组,而不再生成一份平铺列表——包含 59 条决策的存储现在会按十几个主题呈现。
  • 兼容 OpenAI 的嵌入。 semantic.api: "openai" 可将语义混合指向任何 /v1/embeddings 端点,例如 LM Studio、llama.cpp server 或 OpenAI 本身(api_key_env 用于指定保存密钥的环境变量名称)。默认仍使用 Ollama,并保留相同的故障安全词法回退机制。现在只有 nomic 模型会使用 nomic 任务前缀。
  • 感知可信度的冲突指导。 similar_entries 匹配结果现在会包含每个条目的 origin,指导内容也会说明优先级:用户陈述高于 Agent 推断,Agent 推断高于导入内容。
  • 令牌减少量测量(evals/token_reduction.py),针对四个真实存储运行:
存储活跃条目完整存储(令牌)会话开始时注入量减少量
balatron78~75,277~2,05797.3%
clark55~35,445~2,10294.1%
context-keeper13~5,692~82885.5%
conductor9~1,538~41173.3%

基准 = 将所有活跃条目都写入上下文;注入量 = SessionStart Hook 输出的 get_project_summary 内容。需要坦诚说明的是:摘要受预算上限约束(默认 2000 个令牌),因此大型存储的部分减少量是由设计本身决定的——真正重要的是,随着存储增长,注入成本仍保持稳定。

  • 新增六种 MCP 客户端文档(OpenCode、Copilot CLI、Antigravity、OpenClaw、Hermes、pi/oh-my-pi)——请参阅下方的 Other MCP clients。

v0.8:DECISIONS.md 投影(写入时渲染)

可选启用:将决策存储镜像到项目根目录下的人类可读文件 DECISIONS.md。在 .context/config.json 中启用:

{ "markdown_export": { "enabled": true, "path": "DECISIONS.md" } }
  • 写入时渲染。 每次会修改决策的工具调用(record_decision、update_entry、deprecate_entry)都会在写入 JSON 后、工具返回前,根据 decisions.json 重新生成整个文件——因此后续的 git commit 会在同一次提交中包含这两个文件。特意不使用 git/PostToolUse 钩子:在提交快照之后渲染会重新引入不一致。
  • JSON 仍是规范数据;Markdown 是派生且只读的。 每次都会重新生成整个文件——绝不追加、合并或反向解析。手动编辑不会保留;重新生成的投影不会产生不一致。
  • export_markdown 工具可按需重新生成(也可以指定自定义 path),因此现有仓库无需启用此标志即可补齐文件。
  • 仅使用标准库进行字符串格式化;关闭此标志时,默认行为逐字节保持不变。

此功能源于实际使用:Balatron 一直靠手动编辑镜像文件,让 DECISIONS.md 与存储保持同步,每次提交都要编辑一次。现在,这一约定已实现自动化。

v0.7:预期查询、来源可信度、时间线筛选

  • retrieval_hints(所有 record_* 工具):提供 2-4 种未来会话可能搜索的替代表述——同义词、症状描述、错误消息。系统会同时为词法和语义检索建立索引,因此即使词汇不匹配(例如“value network diverging”和“value head saturating”),也能在不使用嵌入的情况下命中。这是语义混合检索的零依赖补充。
  • origin + 可信度加权(所有 record_* 工具):条目会记录作者身份——user(明确陈述)、agent(根据会话推断)或 import(回填)。检索评分会提高用户陈述条目的可信度权重;Agent 推断的条目权重次之,导入条目再次之。v0.7 之前的条目按 agent 计分,以保留它们之间的相对顺序。
  • get_context 中的 since / before:根据每个条目的已验证时间戳或创建时间戳进行时间筛选——现在可以查询“这个月我们做了什么决定”。

v0.6:捕获时防护机制

  • 注入指定范围的约束。 新增 scope_guard.py 钩子(在 Edit|Write|NotebookEdit 的 PostToolUse 上运行):当 Agent 编辑受某个约束 scope 覆盖的文件时,该约束会立即通过 additionalContext 注入上下文。会话开始时的注入只会在第一轮向模型简要说明一次;此机制则会在规则即将发挥作用的确切时刻强制执行。每个约束在每个会话中最多触发一次。
  • 记录时显示相似条目。 record_* 现在会将新条目与存储中的条目进行比较(使用词集 Jaccard,相似度阈值可通过 similar_threshold 配置),并在现有条目高度重叠时返回 similar_entries——这样可以在捕获时发现重复陈述和矛盾,而不必依靠 MMR 在检索时缓解重复。此功能仅提供提示:写入始终会继续。

v0.5:数据完整性与检索修复

  • 原子写入。 条目文件会先写入临时文件,再通过 os.replace 替换,因此即使写入期间发生崩溃,也不会再留下被截断的 JSON 文件。
  • 损坏存储保护。 如果条目文件存在但无法解析,record_*/update_entry/deprecate_entry 现在会拒绝写入(以前,损坏的文件会被读取为空文件,下一次记录时便会悄无声息地用一条记录替换整个历史)。只读工具仍会优雅降级。
  • update_entry 强制执行架构。 结构化字段(why_chosen、problem、reason、purpose 等)在更新时也会进行最小长度校验,因此条目在记录后不会变得空洞。
  • 更好的预算打包。 get_context 会跳过超出令牌预算的条目,继续打包较小的条目,而不是在遇到第一个超大条目时停止。
  • 更新鲜的压缩报告。 SessionStart 钩子现在会自行运行快照比较(压缩后会立即以来源 compact 触发 SessionStart,早于任何 Stop),因此注入的报告不会落后一次压缩。它还会注入一行质量扫描提示,这是 verify_quality 对模型可见的入口(PreCompact 的标准输出只会显示给用户,而不会显示给模型)。
  • 语义层已包含在软件包中(semantic_index.py 之前缺失于 wheel/sdist),并加入了批量嵌入请求,每次查询还减少了一次 HTTP 往返。

v0.4:结构化理由与脉络关联

早期版本使用单个自由格式的 rationale 字段。实际上,Agent 写出的往往是一行摘要,而非完整推理,违背了设计初衷。v0.4 从三个方面解决了这个问题:

  1. 架构强制深度。 record_decision 要求提供 problem(至少 40 个字符)、why_chosen(至少 60 个字符),并接受可选字段 what_we_tried 和 tradeoffs。record_pipeline 要求提供 purpose。record_constraint 要求 reason 至少 40 个字符,并接受可选字段 triggering_incident。服务端会拒绝内容单薄的条目,并针对具体字段给出指导——走捷径不再能生成有用的条目。
  2. 通过 related_to 关联脉络。 每个条目都可以引用相关条目的 ID。默认情况下,get_context 会遍历这些关联(深度为 1),因此检索某个决策时,也会一并获取其余相关脉络。关联信息能够跨会话保留。
  3. 质量验证。 新增的 verify_quality 工具会扫描旧版条目、推理单薄、缺少标签和孤立条目(标签重叠但没有 related_to 关联)。PreCompact 钩子会自动调用该工具,并标出相关条目,以便在上下文被压缩前补充完善。

旧版条目(v0.4 之前)仍然有效——它们不会被自动拒绝,只会由 verify_quality 标记,以供选择性补充。为保持向后兼容,已弃用的 rationale 参数仍可用于 record_decision(会自动映射到 why_chosen),但仍必须提供 problem。

安装

根据客户端不同,有两种安装方式。Claude Desktop 用户可以使用一键安装包;其他用户则使用标准的 stdio 服务。

选项 A — Claude Desktop 一键安装包(.mcpb)

Context Keeper 以 MCPB 桌面扩展的形式提供: 一个 .mcpb 文件,无需修改任何配置即可安装。

  1. 从发布页面下载 context-keeper-<version>.mcpb。
  2. 双击该文件(或将其拖入 Claude Desktop → Settings → Extensions)。
  3. 出现提示时,选择一个存储目录——存放项目记忆的文件夹(系统会在其中创建一个 .context/ 子文件夹,用于存储可读的 JSON 文件)。然后启用扩展。

就是这么简单——无需使用 pip,也无需编辑 JSON。该安装包仅使用 Python 标准库,因此无需安装第三方依赖。(Claude Desktop 为 .mcpb Python 扩展提供 Python 运行时;你需要确保系统中有可用的 Python,才能启动扩展。)

该安装包通过 scripts/build-mcpb.sh 从此仓库可复现地构建,CI 会自动将其附加到每个版本的 GitHub Release 中。

选项 B — pip + stdio(Claude Code、Cursor、Codex 及任何 MCP 客户端)
pip install context-keeper-mcp
Claude Code
claude mcp add --scope user context-keeper -- python /path/to/context-keeper/server.py
Claude Desktop(手动配置)

相比 .mcpb 安装包,你更倾向于手动编辑配置?将以下内容添加到 claude_desktop_config.json:

{
  "mcpServers": {
    "context-keeper": {
      "command": "python",
      "args": ["/path/to/context-keeper/server.py"],
      "env": {
        "CONTEXT_KEEPER_PROJECT": "/path/to/your/project"
      }
    }
  }
}
其他 MCP 客户端(Cursor、Codex CLI、Gemini CLI、Windsurf,……)

此服务是标准的 stdio MCP 服务,因此任何支持 MCP 的客户端都可以使用它——钩子是 Claude Code 的附加功能,并非必需。将客户端的 MCP 配置指向 python /path/to/context-keeper/server.py,并设置 CONTEXT_KEEPER_PROJECT:

Cursor(~/.cursor/mcp.json 或项目级 .cursor/mcp.json)和 Windsurf(~/.codeium/windsurf/mcp_config.json)使用与 Claude Desktop 相同的配置格式:

{
  "mcpServers": {
    "context-keeper": {
      "command": "python",
      "args": ["/path/to/context-keeper/server.py"],
      "env": { "CONTEXT_KEEPER_PROJECT": "/path/to/your/project" }
    }
  }
}

OpenAI Codex CLI(~/.codex/config.toml):

[mcp_servers.context-keeper]
command = "python"
args = ["/path/to/context-keeper/server.py"]
env = { "CONTEXT_KEEPER_PROJECT" = "/path/to/your/project" }

Gemini CLI(~/.gemini/settings.json)使用与上方 Cursor 相同的 mcpServers JSON 格式。

GitHub Copilot CLI(~/.copilot/mcp-config.json)和 oh-my-pi(mcpServers 配置)使用带有 "type": "stdio" 的 mcpServers 格式:

{
  "mcpServers": {
    "context-keeper": {
      "type": "stdio",
      "command": "python",
      "args": ["/path/to/context-keeper/server.py"],
      "env": { "CONTEXT_KEEPER_PROJECT": "/path/to/your/project" }
    }
  }
}

OpenCode(opencode.json):

{
  "mcp": {
    "context-keeper": {
      "type": "local",
      "command": ["python", "/path/to/context-keeper/server.py"],
      "environment": { "CONTEXT_KEEPER_PROJECT": "/path/to/your/project" }
    }
  }
}

Antigravity(~/.gemini/config/mcp_config.json 或工作区 .agents/mcp_config.json)和 OpenClaw(openclaw.json)使用带有 command/args 的 mcpServers 格式,与上方的 Copilot 相同。

Hermes(~/.hermes/config.yaml):

mcp_servers:
  context-keeper:
    command: "python"
    args: ["/path/to/context-keeper/server.py"]
    env:
      CONTEXT_KEEPER_PROJECT: "/path/to/your/project"

如果不使用 Claude Code 钩子,你将失去自动的会话启动注入和编辑时约束保护——请在对话开始时调用 get_project_summary,并在工作过程中调用 record_*(工具说明中也会提示你这样做)。

将 CONTEXT_KEEPER_PROJECT 设置为项目根目录。如果未设置,服务会按以下顺序解析项目目录:

  1. CONTEXT_KEEPER_PROJECT 环境变量(显式选择——可信)
  2. 当前工作目录,前提是其中已包含 .context/ 目录
  3. 从当前工作目录向上遍历父目录,查找已有的 .context/(类似 Git 的发现方式——从项目的任意子目录启动服务时,也能找到项目)
  4. 否则:拒绝操作,且 record_* 会返回“项目未解析”错误

步骤 2 和 3 只会解析已包含 .context/ 的目录。服务绝不会隐式创建该目录,因此你不会因在错误位置启动服务而意外污染父目录。向任意工具显式传入 project_dir,即可强制创建新项目。

工作原理

记录上下文

做出设计决策时:

You: Let's use JSON files instead of SQLite for storage.
Claude: [calls record_entry(kind="decision") with summary, problem, why_chosen,
         alternatives, and optionally what_we_tried + tradeoffs + related_to links]

确定工作流时:

You: The deploy pipeline is: run tests, build, push to registry, deploy.
Claude: [calls record_entry(kind="pipeline") with ordered steps]

设定规则时:

You: Never run Conductor from source. Always use the exe.
Claude: [calls record_entry(kind="constraint") with rule, reason, and hardness=absolute]
检索上下文

对话开始时,SessionStart 钩子会将项目摘要(以及任何压缩差异报告)直接注入上下文——无需调用工具,因此即使任务导向的第一轮对话中,也不会跳过检索。你仍可按需调用 get_project_summary。进行修改前,Claude 会使用相关标签调用 get_context,以检查是否存在冲突。

相关性评分

无需嵌入或外部服务,Context Keeper 会通过以下方式为条目评分:

  • 标签匹配 — 查询与条目标签的重合度
  • 文本匹配 — 查询词在摘要/理由/规则文本中出现
  • 新近度 — 最近验证过的条目得分更高
  • 状态 — 优先显示有效条目,而非已被取代的条目

结果会受可配置的令牌预算限制(默认:4000 个令牌)。

Claude Code Hook 设置

Context Keeper 包含一些 hook,可在会话开始时注入项目记忆、提醒 Claude 在每次 git 提交后进行记录、在 Claude Code 压缩上下文前创建快照,并在之后检测是否有内容丢失。

将以下内容添加到 Claude Code hooks 配置(~/.claude/settings.json):

{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python /path/to/context-keeper/hooks/pre_compact.py"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python /path/to/context-keeper/hooks/post_compact.py"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python /path/to/context-keeper/hooks/session_start.py"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Edit|Write|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "python /path/to/context-keeper/hooks/scope_guard.py",
            "timeout": 5
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python /path/to/context-keeper/hooks/commit_capture_reminder.py"
          }
        ]
      },
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python /path/to/context-keeper/hooks/constraint_reinject.py"
          }
        ]
      }
    ]
  }
}

只有当 .context/config.json 中设置了 constraint_reinjection.enabled 时,constraint_reinject.py 条目才会启用(默认关闭)——添加配置不会产生影响,除非你主动启用它。它的匹配器为 ""(每次工具调用),因此每个会话的计数器会在所有活动中递增。

将 /path/to/context-keeper 替换为实际安装路径。如果项目不在当前工作目录中,请设置 CONTEXT_KEEPER_PROJECT 环境变量。

Windows 用户: 使用正斜杠(C:/Users/.../context-keeper/hooks/pre_compact.py),或在 JSON 中将反斜杠双重转义。单个反斜杠会被 shell 错误处理。

这些 hook 构成了完整的记录与检索闭环:

  • SessionStart — 导入服务器自身的处理程序,并将项目摘要(以及任何压缩差异报告和一行质量扫描提示)直接打印到 stdout,Claude Code 会在第一轮将其注入上下文。它还会在读取报告前自行运行压缩后的快照比较。压缩后,SessionStart 会立即以来源 compact 触发,早于任何 Stop 钩子,因此可以确保注入的报告保持最新。这取代了旧做法:打印一条要求调用工具的指令——由于工具会延迟提供,这种请求总会输给以任务为先的第一轮。若项目尚无 .context/,则保持静默;输出仅包含 ASCII 字符,因此不会因 Windows cp1252 stdout 而崩溃
  • PostToolUse (Bash) — 每次 Bash 工具调用后触发;当命令包含 git commit 时,会注入提醒,要求在同一工作周期内记录相应的决策/约束/注意事项。提交是最佳的记录触发时机——那正是某件事变得足够确定、值得持久保存到版本控制中的确切时刻。源于实际使用经验:在事故频发的会话中,Agent 会把记录工作攒到“之后”,结果一夜之间提交了十几个 commit,用户不得不三次要求“update context keeper”
  • PreToolUse (Edit|Write) — scope_guard.py:当 Agent 即将写入受某项约束的 scope 覆盖的文件时(例如,约束的范围限定为 hooks/,而编辑目标是 hooks/session_start.py),该约束会通过 additionalContext 在写入执行前注入。会话开始时会简要说明规则;此机制则会在编辑时刻将规则呈现在模型面前。每个会话中,每项约束仅触发一次。直到 v0.15,它都接在 PostToolUse 下——这样仍然有效(钩子会检测自身事件,并默认使用 PostToolUse),但规则是在编辑完成后才到达,因此它更像审查意见,而非护栏。可选项 scope_guard.confirm_absolute 会在命中绝对约束时升级为向用户请求确认;请注意,它在范围匹配时触发,而非在实际违规时触发
  • PostToolUse (任意工具) — constraint_reinject.py:需主动启用,默认关闭。 设置 constraint_reinjection.enabled 后,它会统计每个会话中的工具调用次数,并每隔 every_n_tools 次调用通过 additionalContext 重新注入仅含约束的区块,避免会话开始时注入的规则随着工具输出不断增加而逐渐淡出。选择 PostToolUse 是经过刻意考虑的:它是模型可见的界面(与 PreCompact 不同),且其触发频率会随工具输出量变化。这不是计时器——MCP 服务在上下文窗口中没有墙上时钟
  • PreCompact — 为所有启用的 .context/ 条目创建快照并运行质量扫描(verify_quality),将标记出的条目(推理单薄、缺少标签、脉络孤立)打印到对话记录中。注意:PreCompact 的 stdout 仅对用户可见——Claude Code 不会将其注入模型上下文,因此模型可见的质量提示改由 SessionStart 钩子提供
  • Stop — 运行与 SessionStart 相同的快照比较作为安全网,以防会话结束后没有新会话启动(具备幂等性——若快照自上次比较后未发生变化,则跳过)

这就闭合了记录循环:SessionStart 在第一轮注入检索内容,提交提醒让记录工作锚定在变更落地的时刻,PreCompact 充当压缩前的安全网,Stop 则负责完整性检查。检索不可避免;而记录现在会在恰当的时机得到提示,不再依赖 Agent 在任务中途自行决定。

数据存储

your-project/
  .context/
    decisions.json           # Design decisions with rationale
    pipelines.json           # Multi-step workflows
    constraints.json         # Rules and invariants
    config.json              # Token budget, stale threshold
    embeddings.json          # Semantic-retrieval vector cache, keyed by entry text (auto-generated, only when semantic enabled)
    compaction_snapshot.json  # Pre-compaction snapshot (auto-generated)
    compaction_report.json   # Post-compaction diff report (auto-generated)
    reinject_state.json      # Per-session tool counter for constraint re-injection (auto-generated)
    scope_guard_state.json   # Per-session record of already-injected scoped constraints (auto-generated)
    .mirror_queue.json       # Queued mirror-out writes pending a reachable remote (auto-generated)
    .mirror_watermark        # Newest remote timestamp already pulled (auto-generated)
    .mirror_conflicts.json   # Substance-differing versions overwritten by newest-wins (auto-generated)
    hook.log                 # Hook activity log
    mirror.log               # Mirror (local<->remote) activity log (auto-generated)
  .claude/
    rules/
      context-keeper/        # Scoped-constraint rules projection, one file per
                             # scope (only when rules_export.enabled). Derived
                             # and regenerated whole -- do not hand-edit. Files
                             # without the generated marker are left alone.

所有文件均为人类可读的 JSON。你可以直接编辑。ID 按顺序排列且易于识别:dec-001、pipe-001、con-001。

配置

创建 .context/config.json 进行自定义:

{
  "project_name": "my-project",
  "token_budget": 4000,
  "max_entry_tokens": 1000,
  "stale_threshold_days": 30,
  "min_relevance": 0.20,
  "markdown_export": {
    "enabled": false,
    "path": "DECISIONS.md"
  },
  "rules_export": {
    "enabled": false,
    "path": ".claude/rules/context-keeper"
  },
  "constraint_reinjection": {
    "enabled": false,
    "every_n_tools": 25
  },
  "scope_guard": {
    "confirm_absolute": false
  },
  "semantic": {
    "enabled": false,
    "weight": 150,
    "model": "nomic-embed-text",
    "url": "http://localhost:11434",
    "api": "ollama",
    "api_key_env": ""
  },
  "mmr": {
    "enabled": false,
    "lambda": 0.7
  }
}

mmr(可选启用,默认关闭)会按最大边际相关性重新排列排序结果:候选条目会因与已选条目的词汇相似度而受到惩罚,因此同一主题的近似重复表述不会挤占令牌预算,另一个相关主题也能获得一席之地。lambda 用于权衡相关性(1.0 = 纯相关性排序)与多样性。通过 related_to 关联的条目不受此限制——这些关联本就旨在让相关条目一同呈现。就目前的存储规模而言,影响较小(redundancy@5 已经很低);随着存储规模增长并积累被取代或重新表述的条目,它的价值会逐渐显现。

语义检索(可选启用)

默认情况下,get_context 使用纯词汇匹配(标签 + 词语重叠)为条目排序——零依赖,可离线运行。其不足在于词汇不匹配:关于“价值网络发散”的查询不会找到关于“价值头饱和”的决策,因为两者没有共同关键词。

设置 semantic.enabled: true,即可通过本地 Ollama 服务(ollama pull nomic-embed-text)将嵌入余弦相似度信号融入排序。在三个真实项目存储上进行的留出评估中,这将 hit@5 从 80% 提升到 93%,MRR 从 0.63 提升到 0.88(检索测试框架位于 evals/)。条目嵌入会按存储缓存在 .context/embeddings.json 中,并以条目文本的哈希为键,因此编辑条目后会自动重新生成嵌入。

同一余弦相似度也会用于拒答信号(top_relevance),因此仅凭含义找到的条目不会仅仅因为使用了与查询不同的词语,就被标记为 no_confident_match。它经过校准,而非直接使用原始值:嵌入余弦相似度的下限较高——即使对于存储无法回答的问题,nomic-embed 的最高余弦相似度也不会低于约 0.51。因此,将原始余弦相似度与 0.20 的拒答下限比较,会导致所有查询都高于该阈值,从而在不知不觉中禁用拒答机制。当低于 semantic.relevance_floor(默认值 0.72,略高于评估集中所有无答案查询的最高余弦相似度)时,余弦相似度不会产生贡献;达到 semantic.relevance_ceiling(默认值 0.85)时,贡献则升至 1.0。这两个值都与模型相关——使用不同的嵌入模型时,请重新校准。

此功能仅作增补,并且具有故障保护机制:如果无法访问 Ollama 或缺少模型,检索会静默回退到词汇排序。默认值仍为 enabled: false,因此开箱即用时仍保持零依赖。

任何兼容 OpenAI 的端点也都可用:设置 "api": "openai",并将 url 指向 LM Studio / llama.cpp 服务(http://localhost:1234)或 OpenAI 本身,同时将 "api_key_env" 设为保存密钥的环境变量名称。

跨项目上下文

通过传入 project_dir 查询另一个项目的上下文:

Claude: [calls get_context with project_dir="/path/to/other-project"]

也可以使用其他项目名称为条目添加标签,以便交叉引用。

命令行界面

每个工具也都可以从命令行调用,并分派到 MCP 服务使用的同一组处理程序:

context-keeper <tool> '<json-args>'

# examples
context-keeper get_project_summary '{}'
context-keeper record_entry '{"kind":"constraint","rule":"...","reason":"..."}'
context-keeper query_entries '{"kind":"decision","text":"storage","limit":5}'
context-keeper export_rules '{}'   # backfill the .claude/rules/ projection
context-keeper --help          # list tools

项目解析方式与服务相同(CONTEXT_KEEPER_PROJECT、包含 .context/ 的当前工作目录,或 JSON 中的 project_dir 键)。退出代码:成功时为 0,工具返回错误时为 1,用法错误时为 2。不带任何参数运行时,会像之前一样提供 stdio MCP 协议。

团队共享记忆(选择加入的快照)

.context/ 中的工作存储按机器隔离(通常会被 git 忽略)。若要通过 git 与团队共享项目记忆,请导出一个压缩且可提交的快照:

context-keeper export_snapshot '{}'

这会在项目旁写入 .context-keeper/memory.json.gz,并添加一行 .gitattributes,将其标记为 merge=ours,这样这个二进制文件就不会导致合并冲突。是否提交由你选择:

git add .context-keeper/memory.json.gz .gitattributes
git commit -m "Share project memory"
# one-time, per clone, for the merge=ours guard to take effect:
git config merge.ours.driver true

在快照存在但工作存储为空的新克隆中,Context Keeper 会在首次使用时(首次调用 get_project_summary/启动会话时)自动导入快照,让新队友快速了解项目情况。导入是非破坏性的——已有条目的存储绝不会被覆盖;运行 context-keeper import_snapshot '{}' 可手动触发导入。

编解码说明:快照使用标准库 gzip,而非 zstd——真正的 .zst 文件需要第三方 zstandard 软件包,这会破坏项目零依赖的保证。存储内容不变时,快照的字节内容也保持稳定,因此重新导出不会扰乱 git 历史记录。

隐私政策

Context Keeper 是一款仅限本地使用的工具。所有数据——每项决策、约束、流水线和配置文件——都以纯 JSON 格式存储在你选择的存储目录中(该目录内的 .context/ 文件夹),位于你自己的机器上。具体来说:

  • 不会向任何地方传输数据。 服务不会自行发起网络请求,也不会向作者或任何第三方发送数据。
  • 无遥测、无分析、无跟踪。 不会以任何形式报告使用情况。
  • 你拥有数据,并可随时读取/编辑/删除数据——数据只是保存在你所选文件夹中的 JSON 文件。
  • 唯一可选的网络功能完全由你主动启用,且目标地址由你指定。 如果你启用语义检索(semantic.enabled,默认关闭),服务会将条目文本发送到你配置的嵌入端点——按设计,这通常是 Ollama 或 LM Studio 等本地服务。除非你主动启用,否则该功能绝不会开启;它也只会联系你设置的 URL。如果你将其指向第三方端点,则你发送的数据将受该端点自身隐私政策约束。

由于该工具只会将数据存储在你选择的本地目录中,且不会自行传输数据,因此默认情况下没有外部服务处理你的数据。

相关项目

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。