文章指出,缺少跨会话上下文会让编码 Agent 重做团队早已否决的设计。其方案以追加式记录保存历史尝试及否决原因,并通过确定性查询让 Agent 在改代码前主动避坑。
“我有 99% 的把握,grep 找不到你的那次 commit,因为你拒绝的是 ‘oauth-library’,而 grep 搜索的却是 ‘auth’ rejection。既然 LLM 会凭空编造类别名称,除非采用确定性约束,否则情况只会越来越糟。”
——Hacker News 用户 0x457
坦白说,我得先交代这段话的出处,因为它并不是一句支持性评价。它来自 Contextual Commits 讨论串中一位评论者提出的对抗性反驳——总体而言,那条讨论串对结构化捕获持怀疑态度,其中也包括 selvedge 所采用的这类方案。我借用的是一种反对意见,不是宣传语。
但这条反对意见值得借鉴,因为即使是结构化捕获的批评者,也认同真正的故障模式是什么:非确定性标签。如果模型在捕获时临时编造类别名称,读取时的搜索就会漏掉它,而 grep 永远救不了你。请先记住这一点——整个版本的所有改动都由此而来。
这里有一段我反复展示给别人看的记录(完整版已经提交到 repo 中)。
几个月前,一次 Agent session 添加了 users.auth_token 列——这是一个长期有效的用户级 token,让移动应用在重启后仍能保持登录状态。两天后,团队将它 revert 了。当时记录下来的 revert 理由是:将 token 存入数据库意味着撤销 token 必须执行一次写操作,因此团队转而采用短期 JWT,并以无状态方式进行验证。决策已经做出,代价已经付过,session 到此结束。
几个月后,一次新的 session 开始了,没有任何共享上下文。用户提出:移动端用户每次重新打开应用都会退出登录——能不能让他们保持登录状态?Agent 的第一反应非常自然:给 users 添加一个持久化的 auth token 列。
这是一个看似合理的方案。Agent 自信满满地给出了它。可它径直走回了一条团队早已付出代价才否定的道路——那次事故、revert、migration,所有这些成本眼看就要再付一遍。
这正是这个产品试图解决的故障。在 AI 辅助编程中,代价高昂的故障并不是某一行糟糕的代码——linter 和测试会捕获这些问题。真正昂贵的是,Agent 自信地重新实现了一项团队早已淘汰的方案。相关知识其实存在,甚至已经有人把它写了下来,只不过它留在了一个已经结束的 chat session 里。
在 demo 中,故事走向了另一条路:Agent 在编写 migration 之前,先针对 users.auth_token 调用 prior_attempts,得到了该方案已被 revert 的 verdict 及其被拒绝的理由,于是转向另一种方案——将 refresh token 放入操作系统 keychain,提供无状态的 /auth/refresh endpoint,不再重新引入那个已被否决的列。随后,Agent 还会记录自己的决策,让下一次 session 也能继承这些信息。
之所以能够给出这样的答案,关键在于 store 的形态:仅追加的证言。reasoning 是 Agent 自己在变更发生时写下的,写作环境就是产生该变更的同一个上下文;与此同时,被拒绝的方案也会成为永久且可查询的记录。这两方面缺一不可。
那些事后解释工具会让第二个 LLM 稍后查看你的 diff;这个模型从未见过最初的 prompt,只能生成一份转述,而且每次重新运行时还可能产生不同的类别名称——这正是 0x457 所反对的问题。OpenLore 与 selvedge 一样具备零 LLM 的确定性,但它在同步后会从可查询 store 中清除已被拒绝的决策——同步后的 spec markdown 中仍会保留一条 annotation,但可查询记录已经不存在了。那些进行代码行归因的工具,没有一个能够回答 Agent 真正想问的问题:以前有人尝试过这个方案吗?结果如何?让被拒绝的路径继续保持可查询,正是 selvedge 切入问题的楔子。
v0.3.7 发布了 prior_attempts,也就是主动询问的 primitive。只要它被调用,就能正常工作。但事实证明,“只要它被调用”这句话承担了极其沉重的责任。今年,两篇彼此独立的论文对此给出了量化结果:“Delivery, Not Storage”(arXiv 2607.20972)和 PROJECTMEM(arXiv 2606.12329)都发现,pull 模式的 memory 工具无人使用——面对一个预先填充好的 store,Agent 在 114 个 turn 中主动执行 memory 操作的次数为零,而确定性注入每次都能成功送达。
零次。整整 114 个 turn。明明相关 memory 就在那里。
如果一段 memory 需要 Agent 自己想起来要去查,它大多数时候就不会被查询。selvedge 已经具备了答案中充当 gate 的那一半:当 Agent 触碰某个受监控 entity 时,会触发 PreToolUse hook。此前缺少的是:当没有任何操作需要 veto 时,也能把信息交付给 Agent。因此,v0.3.10 的核心主题就是让 memory 主动来到 Agent 面前:
在 session 开始时,由 hook 注入一份紧凑且经过相关性过滤的 digest,其中包括:已到复查时间的决策、当前 verdict 为 reverted 的 entity,以及最近的 changeset。在六月的 Agent 开始制定任何计划之前,三月的那次 revert 就已经摆在它面前——不需要调用工具,也不需要 Agent 记得主动询问。
当没有内容需要提示时,它会保持安静;其大小受 digest_max_bytes 限制;它是只读的、fail-open 的,并且和 core 中的其他部分一样采用模板生成。
上下文压缩正是 session reasoning 消亡的地方,因此 selvedge 现在会在压缩发生前触发,并列出本次 session 中已被编辑、但尚未通过 log_change 记录的受监控 entity,同时扣除已经存在于 store 中的内容。
这个 hook 被刻意设计为只提供建议:hook API 允许阻塞,但 selvedge 没有这么做。阻塞一次工具调用只会给 Agent 带来不便;阻塞上下文压缩却会让整个 session 卡死。项目中有一项测试专门断言 selvedge 永远不会这样做。
selvedge export --format markdown它会为 store 生成一份确定性、锚点稳定的 digest,按 entity 分组,并优先列出已被 revert 的决策。它的设计目标是与 .selvedge/ 一起提交,让捕获到的 intent 可以通过普通 diff 接受 review。在没有新 event 的情况下重新生成,会得到一个零行 diff。
整个过程调用 LLM 的次数为零,而且值得注意的是,它没有引入新的 MCP tool。
最后一点可以推广到整个设计:交付这一侧的全部功能,都没有改动 tool surface。MCP tool 仍然只有 8 个,schema tax 仍保持在 3705/3800 tokens。交付 memory 没有额外消耗 Agent 的哪怕一个 tool schema token。
第二个主题是:store 终于有了自己的调节旋钮。.selvedge/config.toml 现在是一等公民,支持完整的 key 集合——retention_days_events(默认:永不清理)、retention_days_tool_calls、backup_keep_last、diff_bytes、reasoning_bytes、db_size_warn_mb、stale_days、digest_max_bytes、redaction_patterns——并采用一条统一的优先级链:
CLI flag > env var > project config > global ~/.selvedge/config.toml > default
SELVEDGE_DB 是唯一的例外,在解析数据库位置时它始终优先。selvedge doctor 会逐项打印最终生效的值,以及该值来自优先级链中的哪一步,因此“为什么这里是 30?”现在只需一条命令就能得到答案。
其中有两项值得分别展开说明:
selvedge prune --include-events这是第一条能够删除已捕获 reasoning 的代码路径,因此设置了双重 gate:既要进行交互式确认,环境中又必须存在 SELVEDGE_DESTRUCTIVE=1,此外还会在 .selvedge/prune.log 中写入一条审计记录。
任何一个 gate 单独存在都不够——cron 配置中的 --yes 可以绕过交互提示,而 shell profile 中的一行配置也能让环境变量长期存在。你必须同时满足两项要求,这是有意为之。
log_change 中的 secret 形态警告它内置了一组保守的检测规则,包括带有供应商前缀的 key、PEM header、bearer token、SECRET= 赋值,以及包含 credential 的 connection string;还可以通过 redaction_patterns 扩展。
它只会警告,绝不会拒绝写入;由于过滤器过度敏感而丢失 reasoning,恰恰就是这个工具试图避免的那类损失。由于写入时检查无法看到过去已经存储的内容,doctor 现在也增加了对现有 store 的扫描功能。
event 大小限制遵循同样的理念:过大的 diff 和 reasoning 会被明确截断,并附带标记和警告,同时在 selvedge stats 中显示相应计数。
再快速说一下修复项:PreToolUse hook 的 allow 路径快了约 40%——在解释器本身耗时 14 ms 的前提下,对每次 gated call 进行 n=60 次交错测量,p50 从 33.6 ms 降至 20.1 ms;hook 自身的逻辑耗时一直都是 0.58 ms,其余时间全部来自 import 成本。SELVEDGE_HOOK_DISABLE=1 现在终于会真正 short-circuit——此前只有在所有 import 都已经完成后才检查这个变量。
log_change 在 rename 和 supersede 分支中,不会再悄无声息地丢弃 revisit_after、constraint 或 stale_when。Docker image 不会再附带我自己的数据库(没错,真的发生过)。CLI 和 MCP server 现在通过同一个共享 presenter layer 返回完全一致的结构。
此外,一轮 mutation pass 找到了大约 12 个故意设置的 guard:它们虽然会执行,却从未被断言覆盖。抽样集合的 mutation score 从 64% 提升到了 100%。测试数量从 826 增加到 984,覆盖率从 88.3% 提升到 89.0%。
pip install selvedge 时遇到了故障当时发生了两个彼此独立的问题,现在都已经解决:
mcp 2.0.0 于 2026-07-28 发布,它移除了 mcp.server.fastmcp,而 selvedge 声明的依赖是 mcp>=1.0.0,没有设置上限。因此,从 2026-07-28 到 2026-08-01,所有全新安装都会解析到 2.0.0,并在 import 阶段崩溃。v0.3.9.3 将版本固定为 mcp<2.0.0。
官方 MCP Registry 的 latest 版本解析采用类似 semver 的规则,无法将四段式 PEP 440 release 排在 0.3.9 之上。因此,?version=latest 一直返回 0.3.9,而恰恰就是这个版本会在 mcp 2.0.0 环境中于 import 阶段崩溃。三段式的 0.3.10 是长期有效的修正。截至今天,Registry 已经将 0.3.10 作为 latest 返回——这一点已经验证。
所以,如果你曾在七月下旬尝试使用 selvedge,却发现它在 import 时直接崩溃:那确实是一个真实存在过的故障窗口,现在已经关闭。执行 pip install -U selvedge 即可获得 0.3.10。
selvedge 为采用 AI 编写代码的 codebase 提供决策溯源:记录为什么这样做,以及此前尝试过并被拒绝的方案。
pip install selvedge
cd your-project
selvedge setup # detects claude code / cursor / copilot
然后完成一次 session,结束它,再开始另一次。第二次 session 启动时,会显示第一轮 session 所做决策的 digest——其中包括所有被 revert 的内容,并且会在新 Agent 再次提出它们之前出现。
repo:github.com/masondelan/selvedge
pypi:pypi.org/project/selvedge
changelog:v0.3.10 release notes
demo 记录:docs/demos/prior-attempts.md
它是开源的,采用 MIT 许可证,local-first,core 中的 LLM 调用次数为零。我最希望收到实际使用反馈的部分,是 SessionStart digest 的相关性过滤:如果它曾在 session 顶部展示任何毫无用处的内容,那就是我希望听到并处理的问题。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。