🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

项目记忆库 shaneconner/canon

DeepSeek Harnessv0.3.1
shaneconner32 天前有提交已提供中文文档扫描:含敏感能力

Pi 编程智能体的规范项目记忆:每个资产一篇统领文章、只追加日志、胶囊式浮现。一个带脊梁的项目 wiki。

31.5/100
综合分
2
GitHub 星标
67
npm 周下载
—/5
用户评分

数据截至 2026/10/4 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add pi-canon
npm 源 · pi-canon 的发布者账号已校验与本仓库一致

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么[目录]—
dsh 原生插件 · ui
暂无时间戳·判据 →
装得上吗[实装验证]✓
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L5 · 运行时冒烟等级说明 →
数据截至 2026/10/28·判据 →
安全吗[自动扫描]—
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
数据截至 2026/11/04·判据 →
还在维护吗[仓库]—
更新放缓:最近一次提交在 32 天前
暂无时间戳·判据 →
图例:✓ 通过! 需注意— 未判定 / 无数据· 「未判定」= 本站尚无数据,不代表插件有问题
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →

可信度证据

安装兼容性检查✓ 自动检查通过
展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包pi-canon @ 0.3.1
✓Node 引擎要求 >=22.18 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/11 18:53:46

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件
查看扫描证据(共 35 处,展示前 26 条)
  • 读取本地文件
    extensions/settings.ts:75
    raw = readFileSync(path, "utf8");
  • 写入本地文件
    extensions/settings.ts:99
    mkdirSync(dirname(path), { recursive: true });
  • 写入本地文件
    extensions/settings.ts:101
    writeFileSync(temporary, `${JSON.stringify(settings, null, 2)}\n`);
  • 写入本地文件
    extensions/settings.ts:102
    renameSync(temporary, path);
  • 写入本地文件
    extensions/canon.ts:198
    appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), kind, ...data }) + "\n");
  • 读取本地文件
    extensions/lib/retrieval.ts:342
    if (existsSync(full)) return true;
  • 读取本地文件
    extensions/lib/retrieval.ts:345
    return readdirSync(dirname(full)).some(
  • 读取本地文件
    extensions/lib/store.ts:172
    if (!existsSync(file)) return undefined;
  • 读取本地文件
    extensions/lib/store.ts:173
    const { meta, extra, body } = parseFrontMatter(readFileSync(file, "utf8"));
  • 读取本地文件
    extensions/lib/store.ts:199
    if (!existsSync(dir)) return [];
  • 读取本地文件
    extensions/lib/store.ts:201
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
  • 读取本地文件
    extensions/lib/store.ts:226
    if (!existsSync(dir)) return;
  • 读取本地文件
    extensions/lib/store.ts:227
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
  • 写入本地文件
    extensions/lib/store.ts:266
    mkdirSync(dirname(file), { recursive: true });
  • 写入本地文件
    extensions/lib/store.ts:268
    writeFileSync(file, serialize(article));
  • 写入本地文件
    extensions/lib/store.ts:283
    mkdirSync(this.journalDir, { recursive: true });
  • 写入本地文件
    extensions/lib/store.ts:303
    writeFileSync(file, text, { flag: "wx" });
  • 写入本地文件
    extensions/lib/store.ts:320
    appendFileSync(this.indexFile, JSON.stringify(row) + "\n");
  • 写入本地文件
    extensions/lib/store.ts:425
    mkdirSync(this.root, { recursive: true });
  • 写入本地文件
    extensions/lib/surfacing.ts:25
    appendFileSync(file, JSON.stringify({ at: new Date().toISOString(), kind, ...data }) + "\n");
  • 读取本地文件
    extensions/lib/surfacing.ts:311
    if (existsSync(absolute) || (path.includes("/") && existsSync(dirname(absolute)))) found.add(path);
  • 读取本地文件
    extensions/lib/tool.ts:104
    if (exact && !existsSync(join(mount.dir, exact)) && mount.store.read(exact)) return exact;
  • 写入本地文件
    extensions/lib/schema.ts:88
    writeFileSync(join(root, SCHEMA_FILE), DEFAULT_FILE, { flag: "wx" });
  • 读取本地文件
    extensions/lib/schema.ts:99
    if (!existsSync(file)) return { schema: undefined, problems: [] };
  • 读取本地文件
    extensions/lib/schema.ts:102
    raw = JSON.parse(readFileSync(file, "utf8"));
  • 读取本地文件
    extensions/lib/lint.ts:237
    if (!statSync(join(dir, article.path.slice(0, cut))).isDirectory()) return undefined;
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 全包扫描 · 来源 npm 发布包

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

社区实装还没有人打卡 · 装过的第一个就是你
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 规范项目记忆
  • 每资产一篇统领文章
  • 只追加日志
v0.3.12026-10-03
· 变更内容 0.3.1 版把规范核心带到第四个 harness,重命名了陌生人会读到的标识符,并停止再附带一份宿主 UI 库的副本。 · 市场、插件、MCP 服务器和工具现在都叫 canon。在 Pi 之外,前缀指向一个读者从没听过的智能体,而工具名会随每次请求出现在窗口里,这和当初删掉引导行与工具 schema 是同样的代价。npm 包仍叫 pi-canon:九个已发布版本不可更改。钩子的自我识别守卫硬编码了旧拼写,所以光改名会让 canon 认不出自己读过的内容,悄悄破坏批量去重。 · DeepSeek Harness 通过 package.json 的 main 解析插件入口,所以适配器发布编译后的 JS,而不是 Pi 通过 jiti 加载的源码,并随已有的同步流程送达,这条流程也在供给 Claude Code 和 Codex 插件。工作区按每次调用解析,因为该宿主在一个进程里服务多个工作区。 · @earendil-works/pi-tui 是带 * 范围的 peer 依赖,不是普通依赖。作为普通依赖,安装会在 Pi 自带的那份旁边再拉一份副本,设置编辑器扩展的 Container 类渲染器根本看不到。pi-coding-agent 上的 peer 范围还排除了 1.0,等于宣称这个包不适用于它实际发布的版本。 · 第四篇论文的 DOI 加入 README 和引用文件,研究记录移到 canon-bench。 · 新增一道校验,把所有写明版本号的文件锁定到同一版本。有四个文件写明版本号,其中三个此前没有任何机制把它们和 package.json 绑在一起。 全部 175 道校验通过。
v0.3.02026-10-03
· 改了什么 0.3.0 版让文章 schema 在存储里有了归属,也让召回如实报告它漏掉了什么。 · schema 是每个存储的数据契约。首次持久化会写出 schema.json,把随附的默认值显式写明,之后编辑过的 schema.json 不会被后续持久化覆盖。必填规则会在任何内容落盘前拒绝违规写入,提示也一并拒绝;schema 格式错误时按失败放行处理,并指明问题所在,而不是让存储瘫痪。 · 完全相同的写入会报告「已是最新」,文章保持不动。真正的改动仍会落盘。增长会被点名,而不是反复唠叨;值保持不变的文章完全不会被唠叨。 · 关系规则也纳入契约,拼写错误会暴露出来,孤儿和子项都会被解析,供执行这些规则的工具使用。 · 搜索返回文章,并按地址限定范围,保证文章出现在返回窗口的某处,而不是声称返回了全部内容。 · 日志搜索需主动开启,按名称邀请,并按时刻和主题限定范围。 · 孤儿文章(资产丢失的那种)在读取和写入时都会被点名。概念级、根级和声明规则级的地址保持沉默。 · /canon-settings 把行为选项呈现为一个界面。 · 第三篇论文的 DOI 加入 README 和引用文件。 全部 174 项校验通过。
v0.2.42026-08-17
在 Pi 之外,新增共享的用户级 Codex 和 Claude Code 插件。 · Codex 在每次受支持的工具结果之后浮现。 · Claude Code 在每次 PostToolBatch 之后批量投递 capsule。 · 浮现状态限定在当前压缩周期内,不会从更早的周期补发。 · 日志条目在可用时记录 harness 和会话来源。 全部 149 项检查通过。
数据来自仓库的 GitHub Release,可能与插件实际版本不一致。

README

由 DeepSeek 最新模型翻译生成
pi-canon 为 Pi coding agent 提供规范化的项目记忆。每个资产最多只有一篇管辖文章,地址由资产自身的路径算出:src/core/config.ts 由 articles/src/core/config.md 管辖。文章之下是一份只追加的日志,每个事件一个文件。当某次工具调用碰到受管辖的资产时,那篇文章的精炼单行会不请自来地进入会话,智能体不必知道有什么东西需要查。在工具调用里识别路径是尽力而为;把路径解析成文章则不是。 只测过一种配置,其他配置同样欢迎。 pi-canon 的开发与测试只在一套配置下进行:Codex,worker 模型为 GPT 5.6,走 OpenAI 订阅。本 README 里的每个数字都测自那里。其他模型、其他服务商、按 API 计费的访问方式均未测试。如果你在别的配置下运行它,欢迎反馈,也欢迎 pull request。 把存储画成图,文章拴在它们管辖的资产上 一个示意用的存储:33 篇文章,20 条日志记录,40 个文件。圆盘是文章,圆环是日志记录,挂在各自被提炼成的那篇文章下面,圆盘下方拴着的方块是这篇文章为之命名的资产。有六篇文章不对应任何资产,无拴挂地悬着,因为自由知识在这里不是特例。选中某个节点,会打开它装着什么、它指向什么、以及什么指向它。
展开完整文档(README 剩余 1 节)
pi-canon

为 Pi coding agent 提供规范化的项目记忆。每个资产最多只有一篇管辖文章,地址由资产自身的路径算出:src/core/config.ts 由 articles/src/core/config.md 管辖。文章之下是一份只追加的日志,每个事件一个文件。当某次工具调用碰到受管辖的资产时,那篇文章的精炼单行会不请自来地进入会话,智能体不必知道有什么东西需要查。在工具调用里识别路径是尽力而为;把路径解析成文章则不是。

只测过一种配置,其他配置同样欢迎。 pi-canon 的开发与测试只在一套配置下进行:Codex,worker 模型为 GPT 5.6,走 OpenAI 订阅。本 README 里的每个数字都测自那里。其他模型、其他服务商、按 API 计费的访问方式均未测试。如果你在别的配置下运行它,欢迎反馈,也欢迎 pull request。

把存储画成图,文章拴在它们管辖的资产上

一个示意用的存储:33 篇文章,20 条日志记录,40 个文件。圆盘是文章,圆环是日志记录,挂在各自被提炼成的那篇文章下面,圆盘下方拴着的方块是这篇文章为之命名的资产。有六篇文章不对应任何资产,无拴挂地悬着,因为自由知识在这里不是特例。选中某个节点,会打开它装着什么、它指向什么、以及什么指向它。

安装

Pi

pi install npm:pi-canon

或者把本仓库 clone 到 ~/.pi/agent/extensions/。需要 Node 22.18 或更高版本,Pi 0.x 线上 0.83 或更高版本。无需配置:存储会在首次写入时创建于 <project>/.canon。这个包只 import node:fs 和 node:path,不碰别的,不发网络请求,不跑 git,在纯 node 下加载,无需构建步骤。

Codex

本仓库是一个 Codex marketplace。添加一次,然后在用户作用域安装插件:

codex plugin marketplace add shaneconner/canon
codex plugin add canon@canon

开发中的本地 checkout,把 shaneconner/canon 换成它的绝对路径即可。安装或更新后,新开一个 Codex 线程。

Claude Code

同一个仓库也是 Claude Code marketplace:

claude plugin marketplace add shaneconner/canon --scope user
claude plugin install canon@canon --scope user

同样,本地开发用绝对 checkout 路径即可。安装或更新后,新开一个 Claude Code 会话。

DeepSeek Harness

dsh plugin --profile <name> add dsh-canon

单独发布在 npm 上,名为 dsh-canon,由同一份镜像核心构建。那个构建不带 retriever,也不带设置界面。
Codex 和 Claude Code 插件启动同一个零依赖 MCP 服务器,并暴露与 Pi 相同的 canon 操作。Codex 在每次工具结果之后浮现。Claude Code 在每个并行工具批次中只去重一个 capsule 数据包,位置紧邻下一次模型请求之前,这样既避免重复的消息框架,又不拖延智能体的下一次决策。两者都会在智能体停止前给一次写后提醒。一篇文章在每个压缩周期内最多浮现一次:一次压缩开启一个新周期,而恢复同一个未压缩的会话不会开启新周期。一个会话可以包含多个压缩周期。压缩会丢弃先前的触碰状态,不重放任何内容。压缩之后,只有新的工具输入路径才能浮现该资产的精确文章或最近祖先文章;子文章和无关文章不会随行。项目里没有 .canon/articles 时,钩子不生效,也绝不会仅仅因为会话打开就创建存储。客户端询问时,请审核并批准插件钩子。通过 MCP 服务器写入的日志条目带有显式的 harness 来源,客户端暴露会话标识符时还会带上它。

默认值

安装之后、什么都没配置时,你得到的是:

- 寻址和触碰时浮现默认开启。 用工具调用触碰受治理的资产,它的文章 capsule 就会不请自来。这组搭配是测量结果一直保留的:寻址能在存储增长后存活,浮现让寻址变得可发现。在规模研究中,每个主动询问决定性地址的会话都得分,30 个没有询问的会话得分为 0,所以等待被询问的召回,恰恰会在存储超出会话已知范围时失效。
- 日志只追加、保持安静。 它从不被整体读取,也从不主动浮现;搜索是唯一能触达它的通道。把历史当作记忆,代价是每会话中位数 340,119 token,而一份提炼文档只要 21,309 token,正确性却没有提升。
- 推荐默认关闭。 retrieval: "none" 是默认值,因为只有当存储里装着地址主干触达不到的知识、声明规则不治理任何资产时,这个通道才划算,而包无法知道自己面对的是哪种存储。它还需要一个检索器,而本包刻意不替你选。用 retrieval: "lexical" 打开它,突出门控会以实测默认值 1.4 生效,并带上排空存储的防护。
- 没有任何东西迎接会话。 定位行和会话结束复查都被各自的测量结果移除了:只呈现记忆表面、背后什么都没有,把首次正确率从 32 个中的 25 个拉到 32 个中的 8 个,而复查的提醒发出了,却从未被响应。

整个选项面就是六个键:root、surface、resurface、retrieval、standout、mounts。每个都在 Options 下有文档,并附上设定其默认值的测量结果。其余一切都是刻意固定的常量。

第一篇文章

会话不会收到问候语:直到 0.2.0,每个会话开头都会有一行引导语。用惰性实现做的 2x2 实验发现,那一行和工具 schema 都有显著的负向主效应,但 19/32 对 21/32 的对比没能分辨出哪个组件的代价更大。研究 3 发现删掉那一行并没有损失收益,于是 0.2.1 把它删了。教义改由工具描述承载。第一篇文章只调用一次工具:

{ "action": "write",
"path": "src/core/config",
"capsule": "Loads layered config; env beats file; secrets never land here.",
"body": "Resolution order is defaults, then config.toml, then environment. ..." }

Wrote src/core/config.

那次写入之后,又做了一点别的工作,仓库变成:

.canon/
articles/
src/core/config.md governs the src/core/config address
lake/prices.md articles are not limited to code
journal/
2026-08-11-vendor-cap.md one file per entry, never rewritten by the tool

文章本身:

capsule: Loads layered config; env beats file; secrets never land here.
updated: 2026-08-11

Resolution order is defaults, then config.toml, then environment. ...

这就是全部存储格式。capsule 是 surfacing 发送的那一行密集文本,写入时无论智能体发来什么,都会压成单行。updated 是最后一次改动了内容的写入日期:与已存文章完全相同的写入是空操作,什么都不碰,所以重述不会刷新这个时间戳,也没有任何东西拿它和资产做比较。这两个键是 pi-canon 唯一拥有的。块里其他所有键,包括 Obsidian 属性,写入时都原样带过;只有纯 YAML 会读错的地方,才会给自有值加引号,所以整棵树始终可以手工编辑。

结果就是纯 Markdown,也是一个合法的 Obsidian vault。把它和你的仓库一起提交:git 就是历史、diff、blame 和时间机器,pi-canon 自己从不运行 git。日志条目也是普通文件。工具只负责追加;用普通文件工具读它们就行。

这套设计针对的失败

智能体把一列原始整数分格式化给人看,文件里是 225565,它写成 2,255.65。测试通过。在另一个仓库里,那个会话期间没人打开过,一个财务解析器读这个文件,把任何含逗号的行都当作损坏,于是丢掉这一行继续往下读。没有异常,没有构建失败,下游总额里少了一个数,直到有人手工对账才发现。

检索防不住这种事,因为只有某个东西想到要检索时,检索才会运行。把数字格式化得更好读,并不是一个会引出疑问的时刻。没有什么可怀疑的,也就没有什么可检索的。
项目工作中代价高昂的失败,不是智能体查了资料却得到糟糕答案的那种。而是没人知道有个问题该问。

这个场景不是战场故事。它是下面基准测试里五条链路之一,写出来是因为这正是这个包要防的失败形态。那个基准测试没有任何一条链路是搜索驱动的,所以这是设计动机,而不是与检索做的实测对比。

这套东西从哪来

pi-canon 底下的形态是 LLM wiki:一个 Markdown 文章文件夹,智能体自己写、自己重写,文章之间互相链接,事先不声明任何 schema。Andrej Karpathy 在 llm-wiki.md 里提出并推广了这个模式,针对的是在查询时重新检索原始分块的做法。它做对的大部分东西这里原样保留。纯文件,所以什么都能读,人也能在编辑器里改一篇。没有数据库,也没有索引要重建。跟仓库一起提交,所以 git 提供历史、diff 和 blame。事先也没有 schema,所以知识长成项目实际的样子。

这份自由也正是这类存储失败的地方,而且是两个具体方向,不是含糊地说。

散落。 没有任何东西把某篇文章标记为「关于某个主题的那篇」,所以找不到现有文章的智能体就再写一篇。现在有了一篇讲 vendor feed 的笔记,第二篇讲 feed 分页,第三篇讲 sync job,三篇从三个角度描述同一个约束,谁都没错。检索把三篇都找出来,智能体读排名最高的那篇,它们互相矛盾时没有任何东西来裁决。

日志漂移。 智能体在工作过程中写入的存储会塞满事件,因为工作就是由事件组成的:试过什么、什么失败了、什么修好了。当前真相最终被压在一条记录它如何变成真相的运行日志下面,本该说明规则是什么的那一页,说的是十四号那天发生了什么。

两者都不是存储失败。两种情况下,知识都在,都写下来了,就躺在文件夹里。散落是寻址失败:副本之间没有定义优先级,因为没有任何东西指定其中一篇为权威。日志漂移是可变性失败:事件历史和当前参考知识共用一页,后来的重写可能把其中一方从另一方底下改掉。

pi-canon 是这个模式上的增量,不是替代品,它加了三样东西。

一份日志,只追加,每个事件一个文件。智能体不管你想不想都会记,这股冲动得落到某个不是参考页的地方。写入时的指令是:来源到达时原样记录,包括名称和确切数字,因为文章会提炼,只有日志保留原始内容。
一条主干,也就是寻址约定。文章地址由资产算出,不用搜索,而且这套映射不需要任何配置就能成立——所以主干是约定,不是模式。这也是 RECALL 路径里什么都不搜索的原因:一次触碰已经定好了地址,就没什么可找的。search 动作为反方向存在,给想主动发问的智能体用,而且从不未经请求就运行。

浮现,推送而非拉取。检测到某次工具调用触碰了受治理的资产时,那篇文章的胶囊就为会话暂存,只要它还在上下文里,每篇文章最多一次(见 Surfacing),这样谁都不用想着去问。检测工具调用里的路径是尽力而为。路径一旦到手,解析就不是尽力而为了。

下面的评估不检验那条谱系论证:评估里没有任何一组是搜索驱动的 LLM wiki,所以这里没有任何东西能说明 pi-canon 胜过一套有纪律的 wiki。

寻址

地址就是资产路径去掉文件扩展名,而且只去一次,在边界处去。名字必须在点之前,所以 .env 保持 .env。只有最后一个斜杠之后的点才算数,所以 docs/v1.2/notes.md 规范化为 docs/v1.2/notes。而 src/core/config.test.ts 落在 src/core/config.test,与 config 并列,而不是盖在它上面。点段在根处截断,写入内部还会再检查一次包含关系,所以没有地址能逃出 articles/。

解析先试精确地址,然后一次往上走一个路径段,直到最近的已存在文章,如果走到顶还没命中就返回空。没有排序,没有打分,也没有相似度:给定一条路径,治理它的文章是树里已有内容的函数。所以不是每个文件都需要文章:src/feed 处的一篇文章,为它下面所有没有更近文章的东西作答。创建文章是少见的行为;常见的是更新那篇已经在治理的文章。

重命名就是你自己做的文件移动。pi-canon 不监视文件系统,也没有重命名动作。把文章移到新路径推导出的地址。Lint 会检查接下来写入的任何文章里的 wikilink,所以一条还指向旧地址的链接,会在那篇文章下次被写入时被点名,而不是在移动的那一刻。

不匹配任何资产的文章就是普通的自由知识。主干为项目已有的资产保证地址;它并不把存储限制在这些资产上。这个取舍值得一并说明:浮现是按资产划范围的,所以主干之外的文章要靠链接或显式读取才能到达,而不是在触碰时被推送,或者在配置了检索器时靠相关性到达。
这样的文章可以这样写,在写入时带上 scope: rule。忘记声明,并不会让一篇脱离主干的文章被排除在检索之外。声明它,能把有意归档的规则和资产已消失的文章区分开;如果之后同一地址又出现资产,这条规则仍留在检索语料里。scope: asset 则收回这个声明。

工具

一个工具,canon,五个动作:read、write、journal、map、search。

| 动作 | 参数 | 作用 |
| read | path | 返回起支配作用的文章:标题、capsule、updated、正文,以及一行日志索引。未命中时返回一句话,点明地址,并邀请你在任务结束后写入。当某个祖先节点作答时,标题读作 <ancestor> governs ,高度层级一目了然。 |
| write | path、capsule、body、scope | 创建或更新文章,然后返回 Wrote . 以及任何提示性 lint。从不拒绝。空字符串表示未改动,而不是抹除。写入内容与已存文章完全相同时,会报告为已是最新,不触碰任何东西,这样 updated 始终表示内容最后一次变更的日期。 |
| journal | body、subject、slug | 追加一条带日期的条目,作为独立文件,<date>-<slug>[-n].md。canon 永远无法改写它。正文为空时返回一句话,问发生了什么。 |
| map | path(可选前缀) | 每篇文章一行,格式为 address: capsule;存储或过滤条件为空时返回一句话。输出不设上限。 |
| search | query、journal | 按词对文章排序,返回十条结果,每条都带上限定它的范围:文章则给出地址和 capsule。日志需通过 journal: true 主动启用,因为事件是历史而非当前事实,而且在真实存储上实测,关于某事件的日志条目会挤掉承载它的文章;默认搜索从不读取条目正文,只会说明日志存在。启用后,日志条目是一等结果,由时刻和 subject 限定范围,文章保留一半窗口,较短的一方让出自己的名额。会说明上限丢掉了多少条匹配。这是唯一能触及日志内容的动作。 |

subject 是地址数组。以裸字符串传入的 subject 会被忽略,条目最终不带任何 subject。

当读取那些文章时,用 subject 地址记录的条目会以文件名一行索引的形式返回,最新三条:历史随时可取,但默认从不加载。索引只带文件名,从不带条目内容,且匹配是精确匹配,所以在 src/core/config 归档的条目,读取 src/core 时不会出现。日志始终存放在项目存储中。
写入时的 lint 是附加在响应里的提示性字符串,从不拒绝写入。因为被拦下的写入会教智能体别再写,而一句警告会教它下一步该做什么。唯一的例外是存储自己在 schema 里声明为 required 的规则,详见下文 The article schema。正文超过 8,000 字符时它会警告,超过 20,000 字符时建议改用层级结构。它会点名缺失的 capsule、超过 1,000 字符的 capsule,或写成变更日志的 capsule。地址里带有 log、journal、session、standup 或 meeting 段,或带有 ISO 日期,会被重定向到 journal。失效的 wikilink 逐行点名。资产丢失的文章在读取和写入时都会被点名:嵌套地址的父目录在磁盘上存在,却没有任何东西匹配该资产,就会触发 orphan 询问,并列出几条出路(移动文章、并入父级,或声明 scope: rule)。根级地址、已声明的规则,以及地址从未映射到文件的存储,保持沉默。

改写让正文变大时,会在自己的结果里说明:Body grew 812 -> 1304 bytes.,后面跟着提醒:文章承载当前状态,叙述性历史(旧值、状态转换)属于 journal。任何增长都会触发;创建不算增长,只写 capsule 也绝不会让已存储的正文变大。这一行是测量出来的,而测量的是方向,不是幅度。在三条双组捕获中,跨越字节完全相同的八会话谱系,工具说出这一行的组每次结束时留下的被取代值都更少:一个模型上是 51 对 96 中的 88,第二个模型上是 45 对 87,第一个模型再跑一次、并在半数谱系中颠倒组顺序时是 71 对 85。取方向,不要取幅度。第三次捕获保住了方向,却丢掉了大部分差距;两天后重跑一个匹配的未处理单元,其中位数存储移动了 39%,所以这个仪器无法可靠地测量自己的幅度。存储大小也从未承载结论:它先降了五分之一,然后几乎不动,再降了五分之二,而在配对的谱系之间,这一变化的方向在前两次捕获中与随机无法区分。96 个读者中有两个被过期值伤害,每组各一个,所以这一行并不已知能保护读者。同样的信条放进系统提示词,在 32 个中清除了 1 个被取代值,不过那一组跑在另一项单会话研究里,两者从未并排比较过。
有一行 lint 提示性质不同。写入时提供了正文,而文章原本已有正文,新正文就会和旧正文比对;如果某一行带有约束性措辞却消失了,这一行会被引用回删除它的那次写入。词汇表是固定的:must、never、always、require 的 requires 和 required 形式、do not、don't。每次写入最多点名两行,每行截到前 160 个字符,并附上说明:如果约束仍然成立,就应当保留;如果它确实变了,变化应记入 journal。引用的是前缀,不是摘要。这是提示性的:写入已经落盘,没有什么能让智能体把这一行放回去。

文章 schema

每个 store 都以文件形式携带自己的契约:store 根目录下的 schema.json,在 store 首次持久化任何内容时写入,并把随附的默认值显式展开。它是数据而非配置,所以随 store 一起走,因项目而异,任何其他读取该 store 的工具都能从同一个文件执行同一份契约。随附的默认值不要求任何东西,与提示性上限一致,所以未改动的文件不改变任何行为;它只是让契约可见、可编辑。

三个字段可以携带规则:capsule(front matter 行,注入时呈现)、title(正文开头的 # 标题;这里刻意不设单独的标题输入,所以规则检查标题唯一可能存在的位置)、body。四个规则键:required、min_chars、max_chars、hint。

执行是有意不对称的,而且这种不对称经过测量:在本项目实验室的五次写入质量模型抓取中,在工具边界执行的规则出错率为零,而交给模型判断的规则出错率约为百分之一。所以真正要紧的那类遗漏是被强制执行,而不是被提示:

- 标记为 required 的规则会拒绝违反它的写入,连同 hint 一起,磁盘上什么都不动。写入按它改动的部分来评判,文章首次创建时则按全部内容评判,所以只更新 capsule 永远不会被遗留的正文拖住。
- 其他所有违规都只警告:写入落盘,消息指出要修什么。
- 读取从不拒绝,但会报告现存问题,因为持有不合规文章的智能体才是能修复它的那个。
- 格式错误的 schema.json 会开放失败并大声报错:规则不再执行,每次写入都会说明这一点,因为所有者以为契约在执行、而一个拼写错误却把它关掉了,是最糟的状态。
- schema 声明的边界拥有自己的消息:同一方面的内置提示行保持沉默,而不是说两遍。

一个要求每篇文章都以标题开头的 store:

{
"schema_version": 1,
"article": {
"capsule": { "required": true, "max_chars": 1000, "hint": "One dense line of current truth." },
"title": { "required": true, "hint": "Start the body with a # heading naming the asset." }
}
}
删除规则即可停用它;删除文件则彻底关闭 schema 检查。编辑过的文件永远不会被覆盖。

关系规则放在同一个文件的 relations 下,因为引用图也是契约的一部分;每个工具执行它能看到的规则,所以一个文件管住所有读取该存储的工具。本包在写入边界看到文章自身的引用,并执行 refs:required 拒绝正文没有引用任何内容的写入,判定方式与字段规则相同(改变引用集的那次写入,或一次创建),min_count 在低于下限时警告,代码围栏里的示例和大小写折叠后的重复项一律不计。两条图级规则在这里解析,由读取图的工具执行,例如 canon-atlas:orphan.warn 在没有其他文章引用这篇时警告,children.listed 在文章没有引用其地址下的每个直接子项时警告。

{
"schema_version": 1,
"relations": {
"refs": { "required": true, "hint": "Name what this concerns." },
"orphan": { "warn": true },
"children": { "listed": true }
}
}

浮现

一次工具调用会为它触及的内容暂存管辖文章,但不发送任何东西。每轮结束时,所有暂存内容作为单条消息一次性发出,因为 pi 的引导队列每趟服务商往返只排空一条消息,而每次工具调用发一条消息会让每条提醒都单独占用一次模型调用。带存在标记的文章最多浮现一次,只要该标记仍在服务商收到的上下文里;一旦被折叠或压缩掉,它就会重新浮现,并在其资产下次被触及时再次搭车(见下方 resurface 选项)。发出的文本短于 24 个规范化字符时无法安全测试,保守起见会在整个会话中保持已见状态。会话之间不保留任何东西:新会话会重新浮现一切。

这一切都不由字符数决定。胶囊按 1,000 字符来写,那是写入时交给智能体的目标,不是读取时的闸门:某轮触及了其管辖资产的文章,要么整体浮现,要么不浮现。早期版本会从会话额度里扣除胶囊文本,并把超出的部分降级为裸指针。该额度在 2.0 中已移除。它是在猜测一条没人测量过的策略,而它决定的却是智能体能看多少。取而代之的是测量:每条浮现的行都记录它消耗了多少窗口,这样上下文占用事后可以对照相关性来读,而不是事先由某个常数裁定。一行不是胶囊文本的唯一剩余原因,是文章没有胶囊,此时以指针形式浮现,指明地址并告诉智能体去读它。
通过 canon 读文章,会在消息发出前撤回为它暂存的那一行,所以拉取优先于推送。读资产文件本身不会,因为读文件不等于读关于它已知的内容,而胶囊可能正好装着文件里没有的约束。只读会话静默退出。写入、编辑、补丁或识别出的变更型 shell 调用成功之后,若指名了受治理的资产,settling 会为它的文章拉出一条提醒——前提是文章没更新过,每批一次,并由下一次修改型调用重新武装。未知工具只要指名了路径,仍会浮现知识,但没有确凿的变更证据就不凭空造出更新义务。

在工具调用里找路径是尽力而为。只扫描工具调用的输入。结果从不扫描,模型的散文也不扫描。输入会扫描整段短字符串,以及磁盘上存在的、或其父目录存在的路径形 token,所以即将创建的文件仍会浮现它的治理祖先,而长字符串里带空格的路径会被漏掉。这喂给什么——从路径到治理文章的解析——是确定性的。两条主张刻意分开。

/pi-canon 打印一行状态:store 根目录、挂载数(有时)、文章数、日志条目、本会话浮现的文章,以及其中多少仍在上下文里、占多少。它只发往 UI,不给模型发任何东西,所以问它不花上下文。PI_CANON_TRACE=<file> 每次浮现决策追加一行 JSON,变量未设置时不起作用。

选项

作为包安装时,pi 加载默认导出并采用默认值。要传选项,自己写扩展文件,让它调用具名导出:

// ~/.pi/agent/extensions/my-canon.js
import { registerPiCanon } from "pi-canon"

export default function (pi) {
registerPiCanon(pi, { mounts: ["/data/lake"] })
}

六个键,其他任何键都会在注册时按名字抛错,因为其余一切都是刻意固定的常量。

四个行为键(surface、resurface、retrieval、standout)也可以来自 ~/.config/pi-canon/settings.json,/canon-settings 命令在 TUI 里编辑它:布尔值和 retrieval 周期,standout 截断值沿其格点用左右键步进、按 Enter 取精确值,每次应用的改动都立即保存,走的是注册所用的同一套校验。显式选项优先于文件。root 和 mounts 属于每项目拓扑,只留在代码里;它们在编辑器里没有行,在文件里也没有位置。

- root 决定 store 位置。绝对路径按原样使用,相对路径拼到项目 cwd。默认 <project>/.canon。
- surface: false 关闭每轮 flush 和 settle 提醒。canon 工具和 /pi-canon 仍注册且可用。
- resurface: false:一篇文章离开窗口后,无论隔了多久,每个会话最多只会重新浮现一次。默认值是 true:带存在标记的文章,只有当这个标记还在服务商收到的上下文里时,才算已读;所以一旦它被折叠或压缩掉,下次再碰到它的资产时就会重新浮现。文本经过归一化后不足 24 个字符时,没有安全的标记可用,于是保守地沿用「每会话一次」的行为。只有新的触碰才能把带标记的文章带回来,所以没有任何东西会自己重新浮现。
- retrieval:把检索语料库与智能体正在做的事做相关性排序。语料库包括每一篇脱离主干的文章,再加上任何声明了 scope: rule 的文章,这样以后有资产出现在它的地址上时,规则仍然够得着。普通的资产作用域文章不参与,因为地址主干已经能到达它们。默认值是 "none",不按相关性排序,也不浮现任何东西:只有主干,和 1.0 完全一样。"lexical" 是在标准库上实现的 BM25,没有依赖,也不用模型。任何需要模型的东西都在这里以 { name, score, index? } 的形式提供,所以这个包从不自带模型,也从不替你决定用哪个。配置了检索器之后,工具的归档规则也随之改变,因为这条建议在两个方向上都要付出知识上的代价。用默认值时,它说的是:归档在资产路径之外的知识永远不会浮现——这是事实,也正是你不该把它归档在那里的原因。有了检索器,它说的就反过来了:一条约束管着许多资产、却不拥有任何资产时,应该放在它自己的地址上,并在地址里写明规则,因为互不相关的包唯一共享的父级就是根,而根上的文章会在任何东西被触碰时浮现。

- standout is how far the best-ranked article must beat the best one that will not ride anyway, meaning the fourth, the one the three-per-message cap was already going to leave behind. A multiple, not a score: standout: 1.5 asks for the best to score half again what the first held-back rival scored. Default 1.4, an operating point priced by a 120-cell benchmark rather than picked: its n=15 comparison with the uncut channel differed by -0.07 rule facts at p=1.0 while cutting suggestions from 26 a session to 3, raised the rate at which the agent acted on one from 0.17 to 0.82, and never fired at all on a store with nothing relevant to say, 0 rankings of 139. That small observed contrast is not a general detection bound. Precision is the side to protect, though the measured argument is tokens rather than attention: a companion 124-cell study found good suggestions opened at similar rates buried among twenty-seven as one of four, so what the cutoff saves is the context it declines to spend on lines that were never going to be read. The other side has a price too, and the same benchmark paid it: a cutoff set past what a decisive ranking reaches silences the channel, and at 2.0 it delivered nothing and gave back everything the channel had won. standout: 1 is no cutoff, the measurement setting for reading your own store's trace. Below 1 throws at registration, because it asks for the best article to be worse than its rival and is what a caller writes who is still thinking in scores.

It is a ratio because a score is not the same quantity twice. lexical normalizes against a saturation ceiling computed from the query, so a score is a fraction of the best match POSSIBLE for that query rather than of the best match available in your store, and it falls as the agent says more. On a 380-article store the same article at the same relevance scored 0.68 against a short question and 0.16 with a hundred words of tool output around it. Across two benchmark runs it was worse than unstable, it inverted: keeping the answers on one corpus needed a cutoff below 0.11, and silencing the other needed one above 0.73. Dividing by another score from the same query cancels both.

The comparison is against the top of the ranking, not a quantile of it, because an agent's turn is long and touches nearly everything: 377 of 378 articles in a benchmark session, so a tenth of the way down the list is deep in the mass that shares one common word, and the ratio to it describes your corpus rather than this query. Measured there, ordinary queries reached 2.64 to 3.28 and the query that actually had something to find reached 3.10, inside that range rather than above it. In the first few ranks the same sessions separated cleanly: every ranking carrying a decisive article beat its first held-back rival by 1.68 to 1.81, and every ranking that did not stayed between 1.00 and 1.28. Still measure on your own store: set PI_CANON_TRACE and read the ranked lines, which record what each query reached and whether it passed.

A drained store does not turn the ratio into a free pass. Late in a long session, once most of what a small store had to say has been delivered, the still-eligible articles are a tail of near-zero scores, and a ratio over leftovers alone would ride junk on tiny numbers. So while the cutoff is active, the rival the best must beat is floored at the strongest already-delivered article this same query raised: what is left has to beat what the query would have re-raised if it could. A genuinely new topic clears that floor because the delivered articles score weakly on its query; leftovers do not. Replayed over a real 33-article store that the unfloored ratio had emptied entirely into the window, 33 articles down to a minimum ride score of 0.002, the floor cut the session to 15 rides with a floor of 0.075, and the strongest ride improved, because the store had not been spent on junk before the query that deserved it arrived.

The query is intent and never evidence: the user's own words out of the live context, newest first and bounded, keeping both ends of a message too long to carry whole because neither end is reliably the ask, plus this turn's tool calls by name and first argument. Tool results never reach it, and neither does the model's prose, and pi-canon's own nudges are excluded so an article cannot score highly for having been surfaced already. Relevance and transport are bounded separately: standout rules on whether the query gets an answer at all, and on top of it at most three ranked articles ride one message, best score first, with articles reached by address never counted against that, because an address is a certainty and a score is a guess. A ranked article is paid for by new intent rather than by another turn passing, so an unchanged question does not keep releasing three more until the residue runs out. The trace records the best score held back against the worst one sent. A retriever that throws costs the turn its ranking and nothing else.
- mounts lists directories outside the project that carry their own .canon beside their assets. mounts: ["/data/lake"] serves articles as lake:prices, addressable by that name or by any absolute path inside the mount. Two workspaces that mount the same directory read and write the same store, because the store lives with the assets it governs and sharing needs no protocol. A mount has no journal of its own: events are project history and every entry lands in the project store.

What the code holds, and what it asks for

An immutable journal, an addressing spine, and recall that arrives unasked could sound like a design that removed its dependency on model behavior. It did not. It moved that dependency to one side of a line and constrained the other side, and the line is short enough to state in full.

Held by the runtime:

- A journal entry is created with the exclusive-create flag, so canon never rewrites or deletes one, and a name collision increments a suffix rather than losing an entry. The files stay ordinary Markdown, so any other tool can still rewrite or delete one: append-only is a property of the tool, not of the filesystem.
- Once a path is in hand it resolves to exactly one article, walking to the nearest ancestor that has one, or to nothing at all.
- An article surfaces whole, with no character count able to truncate it or hold it back.
- An article with a presence mark surfaces at most once while that mark remains in the context the provider receives. Presence is read from that projection rather than remembered, so folding or compaction returns a marked article to surfacing; an untestably short delivery or a harness that reports no projection degrades to at most once per session.
- The Codex and Claude Code hooks cannot inspect that projection. They use an explicit compaction cycle instead: session start, context clear, and compaction discard prior touch state; resume does not. None of those events surfaces an article. A later asset touch does. Codex delivers after each tool result; Claude Code combines the current parallel batch into one packet before the next model request.
- Reading an article through the tool withdraws its staged capsule before the message goes out.
- Read-only tools can surface an article but never arm its write-after reminder. A successful modifying tool can.

Asked of the agent, and checked by nothing:

- Read the governing article before working on an asset, and update it after real changes. No write is gated on a prior read, and the settle reminder is a message rather than a gate.
- Record the source as it arrived, names and exact numbers included, because articles distill and only the journal keeps the original.
- File the entry under the right subject, and file a constraint at the asset it governs rather than the asset you happened to edit. On the default retrieval: "none", knowledge filed off the asset path never surfaces; with a retriever configured, a cross-cutting rule can instead live at its own declared address and surface by relevance.
- Open the article when a capsule or a pointer says there is one. A line in the context is not a read.
- Decide whether a dropped constraint still holds. Then follow the rule, against a live prompt asking for something else.

Nothing in the package can compel an agent to keep a line it has decided to cut.

What the package does not do, stated so nothing above reads as more than it is:

- No search that runs unasked. search is an action the agent calls; touches resolve to articles by exact address or the ancestor walk, never by ranking, and no query ever fires on the agent's behalf.
- No embeddings and no model. retrieval: "lexical" builds a BM25 index over off-spine articles plus declared rules, and ordinary asset-scoped articles are never ranked; any other ranker is a function the caller supplies.
- No filesystem watching, and no staleness detection: updated is the date of the last write and is never compared against the asset.
- No delete and no rename. Removing or moving an article is a file operation you perform.
- Articles are last write wins, with no lock, no merge, and no warning that someone else changed the file. Only journal entries get the collision retry.
- No duplicate detection. One canonical address per asset is structural, not checked.
- Nothing writes, summarizes, or compacts on its own, and nothing filters what goes in: no secrets scanning and no redaction. Every line pi-canon wrote came from an explicit tool call.
- Nothing about surfacing persists between sessions. A new session re-surfaces everything.
- Presence is tested against both the article address and the tail of what it actually placed in the provider projection. Any delivery shorter than 24 normalized characters has no safe mark and is conservatively kept present; that can be a tiny read, write, or exceptionally short surfaced line. For marked deliveries, a digest without the delivered tail counts as absent.

Evidence

This package is measured rather than asserted, and the measurements live in
their own repository: canon-bench,
the benchmark and evidence repo for this line of work.

The headline study runs five multi-session chains in a shared worktree, each
planting a constraint in one session and probing it in a later one, and scores
whether the agent still honors it. Against a no-extension floor that gets the
prior transcripts and is on record reading them, this package avoided 19 of 20
trap cells to the floor's 8, and answered recall audits at about a third of the
floor's median token cost. Recall accuracy itself was a wash across arms, and a
static doctrine file was cheaper on both metered measures while passing three
fewer trap cells. Full tables, the arms, and the limitations are in
RESULTS.md.

That study's forensic pass is what set the current research direction: of
fourteen recall misses, thirteen first went wrong at the write desk (never
captured, or captured and later overwritten) and none at retrieval. The
write-side programme that followed is in
write-desk/,
and it is where the growth line documented above comes from: two arms over
byte-identical eight-session histories, where the arm whose tool names article
growth ended with fewer superseded values standing in all three captures. The
third of those is counterbalanced and is the one to read first, because it kept
the direction and took most of the magnitude back.

Papers

Each carries its per-cell artifact trail on Zenodo. Every DOI below is a concept
DOI, so it resolves to the newest version of that paper rather than to one frozen
deposit.

- Mutable Canonical Memory over an Immutable Journal, with Recall by Surfacing, doi:10.5281/zenodo.21890647. The first campaign, and the one that asks whether the design holds up at all: one governing article per asset, an append-only journal beneath it, and recall that arrives on a touch, measured against a no-extension floor that received the prior transcripts and is on record reading them.
- Pricing Recall in Long-Term Memory for AI Agents, doi:10.5281/zenodo.21960350. Six studies on what recall costs and which parts of it earn their keep. It priced the orientation line and the tool schema (both negative, both deleted), set the standout cutoff at a measured operating point, and found the store size past which recall that waits to be asked stops working.
- The Write Desk, doi:10.5281/zenodo.22057257. The first two papers measured recall and took for granted that what the store holds is true. This one tests that and finds it does not hold: writers repeatedly left superseded values in records whose contract is to state what is true now. A condition where the tool speaks at the write boundary ended lower on that endpoint in 20 of 24 capture-lineage comparisons, tied in 2 and higher in 2, but the size did not survive a counterbalanced repeat and is withdrawn rather than qualified. It also freezes the retrieval benchmark that had been reading its corpus live, and reports the cost of two defects found in that freezing by review.
- A Durable Fit, doi:10.5281/zenodo.22087390. The lifecycle question the line had not yet asked: once a result has entered the agent's live context, how long must it stay there, and who should decide? Six preregistered studies ran the registered ladder of context-withdrawal designs to its end. A transient guidance cue could leave after its one consuming reply; task evidence under an exact-output contract could not be evicted below the task's working set by the fixed window tested; and when the model decided, it kept all 139 consulted results under a disclosed ephemeral default. The shipped durable-by-default design stands validated for this use case, and the line closes with no planned next step.

More

- The benchmark, drivers, frozen protocol, and the verifier that recomputes each paper's quantitative claims from the artifacts: canon-bench.
- canon-atlas, a viewer for a store like this one: the article graph, what each node points at, and what points back. It was built with pi-canon in mind and kept package-agnostic, so it reads any directory of structured Markdown serving as memory. It also enforces the two graph-wide relations rules this package parses but cannot see on its own, orphan.warn and children.listed.
- Interactive versions of the figures and the full measurement story, Part 1 and Part 2: shaneconner.com/projects/pi-canon.
- The first campaign's narrative version: My agents' wiki was written faster than it was read.
- The second campaign's, on pricing recall: Pricing recall in long-term memory for AI agents.
- pi-fold, a separate optional package serving the working tier. pi-canon ships the two persistent tiers of the same four-tier stack: the journal is the episodic tier, the canon the semantic tier. The two compose, neither requires the other, and neither knows what the other has spent.

MIT. In a clone of this repo, node tests/verify.mjs runs the gate suite: every invariant prints by name and the run must end all N gates green, 174 of them at this release.
预览分享卡片长什么样 ↓shaneconner/canon
分享 · DeepSeek Harness Hub
shaneconner/canon

Pi 编程智能体的规范项目记忆:每个资产一篇统领文章、只追加日志、胶囊式浮现。一个带脊梁的项目 wiki。

这个插件是你维护的吗?
作者本人可认领并核对本条目;认领通过后,详情页会显示「作者已认领」标记。
认领条目 →
回到顶部