Rutter:跨 AI 工具共享决策记忆,以 Markdown 记录决策与引用笔记
Show HN: Rutter – shared decision memory across AI tools
Rutter 是一款 MIT 许可的参考实现工具,让 Claude Code、Codex、Grok 等不同 AI 工具共享同一份决策记忆:AI 会话做出决定时写入一行记录,存入用户自有笔记文件夹的带日期文件中,并附上被引用笔记的路径与内容 sha256 哈希。
在你的各种 AI 工具之间保持线索连贯。
Rutter 会记住你决定了什么、你的思路如何变化,让你能从上次停下的地方继续——即使是在另一个 AI 工具里。你的历史记录以纯 Markdown 保存,放在你自己拥有的文件夹中。
在一次工作会话过去几个月后,你想知道当时决定了什么,以及这个决定引用了哪些笔记。rutter 会保留这些。当一次 AI 会话做出某个决定时,你的客户端会就此写下一行记录。rutter 把这行记录存进你自己笔记文件夹里一个带日期的文件中,并附上每条被引用笔记的路径以及当时其内容的哈希值。之后你可以在同一个 AI 工具或你配置好的另一个工具里询问,就能拿回这个决定及其日期和所引用的笔记。
机制很朴素。记录只做追加,所以早先的决定会和后来的决定并存。每条引用都带有该行被捕获时取得的哈希值,因此一条未经编辑就被重命名的笔记会被追踪到它的新路径,而 rutter 无法定位的引用会展示给你,而不是被丢弃。
它有局限,且事先说明。只有你的客户端写下了某一行,这一行才存在。一条引用只表明该会话列出了哪些笔记,并不代表模型读过它们。一条在同一路径上被就地修改的笔记,目前不会被标记出来。rutter 是一个人的工具,采用 MIT 许可,作为参考实现发布,不承诺提供支持。它适合那些用 markdown 记笔记、并且乐于配置 AI 客户端的人。
试一试:Claude Code 快速上手只需几分钟;入门指南涵盖 Grok、Codex 和 Antigravity。本页其余部分是对这一设计的论证。
一个示例
这是一次真实运行,于 2026 年 10 月 9 日为本页记录。那两条笔记是虚构的,并在其文本中说明了这一点。下面的会话、命令和输出都是真实的,未经编辑。
一个决定,在 Claude Code 中被捕获。一个笔记文件夹里有两份笔记:Notes/reader-survey.md,一份读者调查的结果,以及 Notes/publishing-cadence.md,一份列出三个发布选项的草稿。Claude Code 中的提示词是“我正在决定是否要改变我的新闻通讯的发布节奏。阅读 Notes/reader-survey.md 和 Notes/publishing-cadence.md,从这些选项中选一个,并把它当作我的决定。”该会话选择了双周刊,并且按照 rutter 的指示要求,在回复末尾写下一行:
<!-- librarian-session {"summary":"Decided to move the newsletter from weekly to fortnightly, one long essay per issue, based on the autumn reader survey (48% want fewer, longer issues; top request was more depth per topic). Main risk: readers forgetting between issues.","refs":["Notes/reader-survey.md","Notes/publishing-cadence.md"]} -->
Stop 钩子把那行记录存进了 _librarian/sessions/2026-10-09.md,并附上每条被引用笔记在捕获时的 sha256。没有人手动输入这行记录,也没有运行命令来保存它。
一次重命名,然后是一次编辑和一次重命名。接下来,在任何 AI 会话之外,那份调查笔记被移动到 Research/reader-survey-autumn.md,内容未动,然后该文件夹被重新索引:
[rutter] identity pass: 1 dead ref(s) checked, 1 bound, 0 unresolved, 1 ledger entry appended, 2ms
接着那份选项笔记增加了一行并被重命名为 Notes/cadence-decision.md,该文件夹再次被重新索引:
[rutter] identity pass: 2 dead ref(s) checked, 1 bound, 1 unresolved, 0 ledger entries appended, 1ms
现在询问最近的工作,就会显示这个决定及其两条引用:
$ npm run recent
2026-10-09 22:16:53 [newsletter-notes] — Decided to move the newsletter from weekly to fortnightly, one long essay per issue, based on the autumn reader survey (48% want fewer, longer issues; top request was more depth per topic). Main risk: readers forgetting between issues.
refs: Notes/reader-survey.md@sha256:fa2bbeb8033a613517730bb689d49da31c9a1abf1601be48cd26dae4402cf75f (renamed to Research/reader-survey-autumn.md)
refs: Notes/publishing-cadence.md@sha256:e9b42627bcb83a2945080d28cc99ab7ce0248cad5e342fefab45f9f54b2e9dcb [UNRESOLVED -- candidates: none]
那份调查笔记字节未变,所以恰好有一条当前笔记拥有所存的哈希值,rutter 就把旧路径绑定到了新路径。那份选项笔记既被修改又被移动,所以它的旧路径已不存在,也没有任何笔记拥有它的哈希值。rutter 把这条引用显示为未解析,而不是猜测 cadence-decision.md 就是同一份笔记。若有多条笔记共享同一个哈希值,也会让一条引用保持未解析,并把它们各自列为候选。所存的那行记录本身永远不会被改写;重命名只在显示该行时才被解析。未解析的引用意味着什么,由你来判断:参见失效引用由人来判断。
在 Codex 中回忆。最后,在 Codex 中,从 notes 文件夹之外的一个目录启动,问题是“关于我简报的发布节奏,我做了什么决定,什么时候做的,那个决定引用了哪些笔记?”Codex 调用了 librarian-recent 和 librarian-positions,并回答:
Your recorded decision was to **publish every two weeks, with one long essay per issue, replacing the weekly roundup**—because readers wanted more depth and tended to skim the roundup.
It was recorded on **October 9, 2026**, with no revisions. That decision’s session cited:
- **`Notes/reader-survey.md`**, now renamed `Research/reader-survey-autumn.md`: the October 2 survey favored fewer, longer issues and deeper coverage.
- **`Notes/publishing-cadence.md`**: this reference is currently unresolved, so I couldn’t verify its contents.
A separate October 5 note, `Notes/cadence-decision.md`, says **“going fortnightly from November.”** It isn’t verified as the missing cited note.
Both readable notes are explicitly labeled fictional demo notes, so this describes the demo’s recorded decision.
使用 Claude Code 2.1.296 捕获,使用 Codex 0.162.0 回忆,基于 rutter 0.5.0。
这次运行没有展示一个边界情况:如果调查笔记在同一路径下被编辑,上面的内容不会说明这一点。rutter 存储了使比较成为可能的哈希值,但它目前不会报告同路径的更改。rutter 能确立什么、不能确立什么,在下面有详细说明。
缺失的层
一个 markdown 笔记文件夹是一个知识存储。把同一个文件夹交给两个人,他们得到的东西并不相同。每个人得出了什么结论、各自依赖了哪些笔记、其中一个人最终不再重新打开哪个问题——这些都不在存储中。从来都不在。
rutter 记录了第二件事的一部分:你的会话写下的决定,以及每个决定引用的笔记。
它给 AI 会话提供的是你如何使用一个知识体系的记录,而不是知识本身的细节。同一个存储,两个读者,两种不同的记忆。存储是客观且可共享的。使用它的记忆则两者皆非。
值得精确说明范围,因为代码的形态容易让人做出错误猜测:这与编码会话无关。一个会话可能是研究、起草、规划,或者只是围绕一个问题阅读。底层的目录是一个知识库,而不是代码库——它是针对 Obsidian 构建的,尽管并不要求使用它。灵感来自 Neal Stephenson 的《雪崩》中的 Librarian:一个跨越时间记忆并与你一同思考的伙伴——这是无状态助手无法成为的东西。名字来自帆船时代:rutter 是实际航行路线的日志,与地图相对。地图告诉你已知什么。rutter 是你对此做了什么。
这个项目在被叫做 rutter 之前叫做 librarian。工具(librarian-search 及其同类)、环境变量(LIBRARIAN_*),以及你笔记中的 _librarian/ 文件夹保留了早先的名字。它们都是 rutter 的组成部分。
本文档讲的是为什么要这样构建。如果你想运行它,请从getting-started.md开始。如果你想完整了解捕获、回忆和身份的机制,请阅读memory-of-use.md。
跨你的工具的一份记忆
大多数记忆功能都存在于单个工具内部。切换工具,你就得重新开始。
rutter 不属于任何工具。它是一个 markdown 文件文件夹加上一个 MCP(模型上下文协议)服务器。任何 MCP 客户端都可以读取记忆。写入需要一个在每轮之后运行的钩子,以及 rutter 知道如何读取的载荷。目前有四个客户端支持钩子:Claude Code、Grok、Codex 和 Antigravity 命令行工具(agy)。
在它们中任何一个里捕获的会话摘要,会立即对所有客户端可用。位置会在下一次重新索引后出现。记忆属于文件夹,也属于你。你之后添加的工具会读取相同的记录。
“共享”是指一个人的工具共享一份记忆。它不是指人与人之间共享。README 的已知限制说明了那里不在范围内的内容。README 还包含每个工具的设置以及每个工具如何捕获的表格。
工作原理
两半,且两者都不包含模型。
写入。当一个会话完成了值得记住的事情时,你的客户端会就此输出一行—— 一段简短摘要,加上它触及的路径。一个 Stop 钩子会提取最新的一行:在 Claude Code 和 Antigravity 中来自 transcript,在 Grok 和 Codex 中来自最后一条助手消息。它把这一行追加到你的笔记目录中一个按日期命名的文件里。每个路径 都随该文件字节的 sha256 一起存储。服务器读取该文件并自行计算该哈希, 而不是从客户端接受一个哈希,因此客户端无法交给它一个并不在磁盘上的文件的哈希。 客户端仍然选择要列出哪些路径。该哈希标识的是捕获时该文件的字节;rutter 不保留它们的副本, 该哈希也不表明会话读取过该文件。每条记录还会记下会话运行所在的 工作目录,以及 git remote(如果有的话)。一天的工作分散在三个任务中, 读起来依然清晰。
每个可分离的成果一行,而不是每个会话一行。一个工作会话通常会留下三到 四行。因为你的客户端在其上下文仍加载时写入,捕获不需要第二次模型 调用,也不需要网络调用。这一行只是客户端本就在写的那一轮中的额外文本。
这一行带有风格约定。以决定了什么开头。优先使用常见词,而非 会话自己的简写。目标约 40 词,最多不超过 60 词。该约定是建议性的。 服务器不会为了风格而重写一行,因为评判文句属于推理,而那里没有模型 来做这件事。它确实会把每一行规范化,使其在记录中占一行,并会截断超过 2,000 字符的行。 机制页面有详细说明。
读取。服务器通过 MCP(Model Context Protocol)暴露四个只读工具,这是 AI 客户端连接外部数据源的标准方式:
librarian-search—— 对笔记进行排序的全文搜索,每条结果都带有其路径、 frontmatter 来源信息和匹配片段。过去会话参与过的结果还会带有一条 低调的先前参与说明。该注释只是附加性的。它从不重新排序任何内容。librarian-get-note—— 按路径获取一条笔记的完整内容。librarian-recent—— “我最近在做什么?” 会话按最新优先排列,带有日期、项目 和引用。可按项目、日期窗口或数量筛选。librarian-positions—— “我对 X 怎么看,这一点变了吗?” 按精确 键获取一个主题,或通过记录立场上的自由文本查找列表,或通过立场引用的某条笔记查找。 每个答案都带有该立场形成的日期和最后修订的日期,因此客户端 会将其报告为你记录的立场,而不是它自己的结论。答案来自上一次 重建索引,因此自那以后捕获的立场会在下一次之后出现——服务器会在下次启动时 自行运行重建索引。
Stop 钩子运行在 Claude Code、Grok、Codex 和 Antigravity 中。这些工具可与任何 MCP 客户端配合使用。 服务器还会向每个连接的客户端发送自己的使用指南。Antigravity 会把该 指南保存为文件,而不是放进提示词中,因此其安装程序还会写入一个规则文件。 无需按项目进行任何配置。
记录存放在 <notes>/_librarian/ 中,位于你自己的笔记目录里。它们是纯 markdown,就放在它们所描述的笔记旁边——可以在终端里用 grep 搜索,可以提交到同一个 git 历史中。它们绝不会被写入代码仓库。SQLite FTS5 索引只是一份可丢弃的缓存:删掉它、重建索引,它仅凭笔记和那些记录就能重新构建。笔记才是真相之源;索引只是便利。
当前规范所描述的一切都已交付。即全文搜索、环境捕获和样式契约,外加工作区溯源、搜索结果上的先前交互注释、在重命名后依然有效的引用、对自身使用的埋点、位置捕获与召回,以及每条记录上标明写入它的宿主客户端的标签。唯一的存根是嵌入和语义搜索。限制条件收集在 rutter 能确立和不能确立什么 中,每个机制都在 memory-of-use.md 中有完整描述。
客户端负责思考。服务器只负责保存。
它为何不同
你的工具链几乎肯定已经在做某个版本了。Claude Code 会把会话回顾和推断出的偏好写入一个记忆文件夹,然后随时间对它们进行整合——合并重复项,丢弃看起来过时的内容。其他工具会让未使用的内容过期,或者把它保存在供应商的账户里,其可移植性完全取决于供应商能走多远。
合并是站得住脚的。它能让记忆文件夹保持小巧可读,而且会话产生的大部分内容确实就是噪音。但它在某个特定方面是有损的。一旦两条条目被合并,你就再也无法问出你在三月时实际是怎么想的——只能问出合并决定你现在怎么想。你所经历过的立场序列,恰恰是整合被设计来抹平的东西。
第二个问题才是更重要的那个。摘要本身不是记录。它是一个断言。“决定摄取路径保持同步”告诉你某个会话声称了什么。它完全没有告诉你该会话引用了哪些笔记,也没有告诉你那些文件此后是否发生了变化。文件会被重写。笔记会被重命名。摘要自始至终保持着自信的语气,没有任何东西会宣告这个缺口。
摘要加上它所引用文件的哈希才是一条记录,因为它给了你一个可以拿来比对的指纹。
那句话就是设计本身。下一节会逐一说明设计所做的每个赌注,以及那个赌注的代价。信任边界,以及 rutter 刻意不是什么,放在这些赌注之后。
赌注,以及每个赌注的代价
只追加,不重写
已存储的行永远不会被重写。新行会被追加,分组发生在你读回它们的时候。一行在写入时也不会为了样式而被重写;只应用一行规范化处理和 2,000 字符截断(详情)。一个整合式记忆文件夹会收敛到一个当前答案,而这里则保留每条已记录的行,按顺序,错误的内容也原样保留。
代价是真实的,而且已经可见。记录只会增长。在样式契约存在之前写入的条目,其密度和它们被捕获那天一模一样。没有任何东西会回头改造它们。为了看起来更整洁而重写你自己的过往记录,会是比密度更严重的失败。有些早期条目读起来像构建日志。唯一能触及它们的层面是读取时的引导:你的客户端被要求在回忆旧的高密度摘要时,用平实的语言报告它们。磁盘上的内容仍是记录;你被告知的内容才是答案。
Append-only 是 rutter 遵循的一条规则。它不是防篡改日志,记录就是普通文件,你可以编辑。
携带哈希的引用
每条引用都是一个路径,加上该行被捕获时该处内容的内容哈希。正是这一对信息让示例能够追踪一条笔记,而拒绝对另一条进行猜测。存储的哈希还为你提供了日后用来比对文件的依据。同一路径上发生变化的笔记会在记录中保留其旧哈希,而该变化尚未被报告。
相邻的方案各自做了其中一部分。架构决策记录捕获了做什么和为什么,却没有固定它们所适用的版本。事件溯源日志为事件打上时间戳,但很少携带文件级溯源信息。供应链溯源格式正确地哈希内容,但它们是给审计人员构建的,而不是为你下一次工作会话。一个近邻,Kage,确实携带这类溯源信息——但它对陈旧记忆的处理方式不同,这是下一个赌注。
代价是:哈希是关于字节的陈述,而不是关于含义的。重新格式化一条笔记,修正一个错别字,哈希就会说它变了。这个信号诚实但生硬,而且是刻意如此。更模糊的比较需要一个模型,而服务器中的模型正是这个设计所拒绝的。
展示你无法定位的内容,不要扣留答案
这是一个拥挤的领域,最近的邻居各自占据了这个立场的一半。Recall 做仅追加的会话捕获,但不哈希记忆所引用的文件。Kage 对照实时代码检查其记忆,并扣留那些已经过时的记忆。扣留是一个站得住脚的选择。rutter 做了不同的选择。它无法定位的引用仍然会被展示,标记为未解决,并附上它找到的每一个候选。对于一条缺失或变化的笔记对你所得出的结论意味着什么,你比工具更有资格判断。
同一路径上的内容变化尚未构建:rutter 今天不会标记它。设计会在那里应用同样的规则,它等待观察到的使用情况,就像本页末尾其他未构建的设计一样。
设计选择在于组合:仅追加的行不因风格而重写,引用携带内容哈希,工具无法定位的引用展示给你,而不是静默解决或丢弃。这些中的每一个都存在于别处。
死引用由人来决定
当精确哈希匹配无法解决一条引用时,如示例中被编辑的笔记那样,rutter 把决定权留给你。确认绑定是一条本地终端命令,npm run identity-confirm,绝不是 MCP 工具。这是一条明确的界线:模型绝不能决定一条死引用意味着什么。确认后的绑定此后是粘性的。假设保险库后来发生变化,使得自动匹配会指向别处。这种分歧会被呈现出来并留给你:已确认 X;哈希现在匹配 Y。
代价是摩擦。有些引用会一直未解决,直到你抽空处理它们,而且只有你能清除它们。这就是不让模型悄悄决定你的引用指向何处的预期代价。
rutter 能确立什么和不能确立什么
一条引用是某个会话记录了什么内容的证据。它不是该会话读取了什么内容的证明。
它能确立:
- 客户端列出了哪些路径;
- 捕获运行时每个文件内容的内容哈希,由服务器计算;
- 客户端写下的结论,不因风格而重写;
- 该行被捕获的时间和地点,以及在能够确立时是哪个客户端写的。
它不能确立:
- 模型读取了任何列出的文件,因为路径由客户端选择;
- 这些文件得出了该结论;
- 捕获是完整的,因为只有当客户端写入了一行且钩子发现了它时,才会存在一行;
- 文件在会话读取它和捕获之间没有发生变化,因为哈希是在回合之后计算的;
- 未变更路径上的笔记此后是否发生了变化。该报告尚未构建。
仅追加是一条应用规则,而非防篡改审计。任何对笔记文件夹有写入权限的人都可以编辑记录,且没有任何机制能检测到。捕获依赖项的完整列表见 memory-of-use.md。
它刻意不是什么
不是检索层面的尝试。 一个能力强的智能体阅读组织良好的笔记目录时已经能很好地检索,所以更好的搜索框会落败。这里的搜索只是普通管道:SQLite FTS5 关键词匹配,对词项做 AND 连接,因此 "blue man group" 会找到同时包含这三个词的笔记,而不是任意一个。语义搜索只是一个存根——embeddings.ts 是一个没有实现的端口。这些都不是这个项目存在的原因。它存在是为了一个短暂会话无法拥有的部分,即跨越时间的记忆。
不是多用户——尽管存储可以多用户。 使用记忆是个人的。存储则不必如此。多个读者可以将各自的 rutter 指向同一个知识库,各自保留自己的使用记录,同一个问题对每个人会得到不同的答案,因为差异从来不在笔记里。同一存储,两个读者,两种不同的记忆——字面意义上的。保持单用户的是记忆本身。条目不携带用户身份,因此覆盖层只有在与其所有者在一起、且未被同步到共享存储中时才有效。共享记录——权限、隐私面、谁的决策版本胜出——不在范围内。一个共享存储,N 个个人覆盖层。覆盖层是预期的单元,而不是通往更大事物的垫脚石。
不是受支持的产品。 这是一个人的工具,作为带日期的参考实现发布。没有路线图承诺,没有支持承诺,也不保证下一次提交会让你依赖的东西留在原处。值得带走的是机制。Fork 它。
而且它仍在试用中。 有状态行为位于使用门控之后。一个仅追加的本地日志记录该行为实际被触及的频率,项目已准备好得出它并不值得的结论。到目前为止,日志只记录了一个人的使用情况。第一次读取是在 2026 年 8 月,返回了通过,门控继续运行。用于追踪你的信念如何随时间变化的更大设计已部分构建:位置捕获与回忆于 2026 年 8 月发布,此前记录的摩擦要求它们。该设计的其余部分尚未构建——漂移可见性,它会告诉你某个位置所依赖的笔记自其形成以来已经改变,以及从早于捕获的记录中对位置进行可选回填。两者都等待观察到的使用情况。
试一试
如果你使用 Claude Code,安装插件后几分钟内就能开始回忆:参见快速路径。其他人:getting-started.md 有适用于 Grok、Codex 和 Antigravity 的设置路径,以及分步手动路线,每条路径都以捕获检查和回忆检查结束。
延伸阅读
getting-started.md—— 每个客户端的设置路径,从安装到首次回忆。memory-of-use.md— 捕获、召回、丰富、笔记身份、位置、信任边界和使用门控,全部机械细节。roadmap.md— 当前排序,以及刻意不构建的内容。../spec/spec.md— 可执行规范:Given-When-Then 场景、由它们推导出的需求、对每一条进行评分的测试,以及每一次修订所否决的内容。这份文档是论点;那个文件是细节。../README.md— 它今天能做什么、设置,以及一开始就说明的限制。- Recall — 在仅追加的会话捕获上最接近的邻居。如果位置的那一半正是你想要的部分,就从那里开始。
- Kage — 在内容哈希溯源上最接近的邻居。它会扣留过期的记忆。rutter 会显示一个它无法定位的引用,并且一旦构建完成,会显示一个已更改的引用。
- “你会迷失,除非你有一本 rutter。” —— 名字来源的那篇文章:荷兰东印度公司实际航行路线的航海日志,以及 Clavell 的警告:rutter 的好坏取决于写下它的领航员——这就是为什么这里的每一个引用都带有哈希。
存储记录已知之事。这个记录你对此做了什么。
那是别人无法为你保留的部分。
来源:Hacker News · ruttermemory.com