关于 EMET
EMET 是面向 AI Agent、机器人团队和聊天会话的持久共享记忆 MCP 服务,使用 MongoDB 存储。它以六类记忆层、独立的逐字记录层和写入后回读验证,区分已核实记录与未核实计算。
主要功能
- 01六类记忆分层
- 02独立保存逐字对话记录
- 03写入后回读验证
- 04本地 stdio 与远程 HTTP OAuth
- 05文档修订历史与退役标记
- 06客户端归属与审批控制
适用场景
- 01在多个 Agent 和主机间共享记忆
- 02将对话原文与摘要分开保存
- 03跟踪受控文档的修订
- 04核查记录由哪个客户端写入
MCP 相关
EMET 通过本地 stdio 或带 OAuth 的远程 HTTP 向 MCP 客户端提供记忆、对话记录和文档操作。其 Cursor 与 Grok Bot 插件连接到用户自行部署的 EMET 服务。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
29收录的工具列表 · 更新于 1 天前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| page | any | 否 | 启动信息超过一个结果时会分页提供(每页不超过 20,000 字节)。第 1 页省略此参数;除最后一页外,每页都会指出下一页应请求的页码。请在回复前使用该页码再次调用,直到某页说明它是最后一页。对于没有大小限制的宿主,`"all"` 会在一个结果中返回完整启动信息。 |
| load_id | string | 否 | 启动信息在 `startup_page.load_id` 中返回的 load_id:每个 emet_initialize 结果都包含一个(分页启动信息的每一页、指定页面为 "all" 或内容可放入单个结果时的完整启动信息,以及尚未设置的存储均包含)。后续每一页都要传入该值,以确保所有页面来自同一次启动;并将相同的 load_id 传给 emet_session_open,以便它在本次启动中检查启动页面。 |
| recent_limit | number | 否 | 返回的近期上下文条目数(默认值为 5)。 |
详细介绍
EMET
EMET 是一个 MCP(Model Context Protocol)记忆服务,为 AI Agent、机器人团队和聊天提供持久的共享记忆。
אמת — 希伯来语,意为真理。也是 持久记忆与认知层级(Enduring Memory & Epistemic Tiers)。
网站: https://dreamforgestudiollc.github.io/EMET/
另请参阅:更新日志 · 贡献指南 · 安全 · 章程 · 功能图谱 · 开发实践
功能
- 分层记忆。 六个层级——身份、语义、情景、程序、元记忆和工作记忆——保存经过整理的陈述,并按条目所属类别拆分。
- 经验证的写入。 每次写入后都会读回并进行比对,然后才报告已保存。
- 逐字记录层。 单独的层级按原样保存对话,因此摘要无法覆盖其来源记录。
- stdio 和带 OAuth 的远程 HTTP。 同一服务可在本地通过 stdio 运行,也可通过带 OAuth 的 HTTP 远程运行。传输方式会变,存储不会。
- MongoDB。 可使用 Atlas,也可使用自行运行的 MongoDB。单机试用可使用 MongoDB 6 或更高版本。只有
semantic_recall需要 Atlas。 - Golem Expertise Module(GEM)接口。 附加行为以 GEM 的形式提供,在启动时加载,并由
gem-check按照gem-library/中的 GEM 标准进行检查。GEM 的优先级低于核心底线,也低于用户当前的表述,并且不能删除受保护的底线条目。随包附带两个进行中的示例,默认关闭,需由你主动启用:Neurendipity(神经多样性互动与自闭症倦怠支持;仅提供支持,不提供咨询或治疗;EMET_ACTIVE_GEMS=neurendipity)以及 Lean Six Sigma(DMAIC 和 DMADV 指导、模板及客户端计算器;EMET_ACTIVE_GEMS=lean-six-sigma)。Fieldnote 保留在代码库中,作为运行中的 GEM 开发案例研究。它不是面向用户的第三个 GEM,且默认关闭。 - 机器人团队备份(可选)。 除非首次运行访谈中记录了肯定答复,否则默认关闭。建议选择启用。配方和恢复模板作为随包附带的 bot-team-backup GEM 提供。EMET 不会自行复制提示词或设置,也绝不会存储密钥值。
- 客户端执行 GEM。 GEM 可以声明工具。EMET 不运行该计算。EMET 验证记录,而非计算过程。客户端运行工具,写回的内容会像其他写入一样接受检查。
- 发布标记。 只有
released标记为布尔值true时,GEM 才会加载。其他任何值都会被拒绝。缺少该标记也会被拒绝,但按 ID、版本和文件摘要固定的旧版清单除外。 - 按客户端归属和审批。 每次写入都会归属到执行该操作的客户端。新客户端在获批前只能读取有限内容。审批会被记录。
- 错误代码和问题报告。 每个错误都带有稳定的
EMET-<AREA>-<NNN>代码。操作错误日志保留 10,000 条记录或 90 天的数据,并附有每日摘要。仅限所有者使用的emet_error_report会返回报告和一份电子邮件草稿,由你自行发送。请参阅报告问题。 - verify-emet 和功能图谱。 在代码检出目录中运行
npm run verify会执行scripts/verify-emet.mjs。它会在临时目录中使用内存测试数据库启动 EMET,不读取.env,并调用每个已注册的工具。若工具需要普通安装中没有的服务,则会报告为缺口。如果运行中只有缺口问题,则以状态码 3 退出;如果工具失败,则以状态码 1 退出。功能图谱见 docs/features/README.md。 - MIT。 版本 0.3.1。EMET 仍处于 0.x 阶段:存储的文档结构在 1.0 之前可能会变更。如果依赖这些结构,请固定版本。
存储只有一个所有者。其他人只能通过可选的访客(只读)和成员(读写)权限加入;这两种权限默认都关闭。
文档控制(ISO 9001 7.5.3 / ISO 13485 4.2.5)
EMET 专为符合 ISO 9001 和 ISO 13485 标准的环境中的文档控制而构建。它支持相关工作,但不持有任何认证,也不声称符合任何标准。使用 EMET 的组织仍须对自身的质量体系负责,包括验证 EMET 是否适用于预期用途。
- 设计上,任何存储中的内容都不可删除。 没有任何工具可以移除记忆条目、文档或转录内容。EMET 运行所用的数据库角色也未获授予移除记录的权限。
- 通过新增内容进行更正。 记忆条目由独立的新修订版本取代。文档会修订或退役。转录内容的分段会修订。事件永不改写。
- 退役表示已过时,而非已删除。
retire_doc会将文档标记为已过时,并保留其 id、内容、版本和摘要。read_doc仍会返回该文档,并标记为已退役,同时附上原因。 - 保留以前的版本。 每次写入文档时,系统都会归档被替换的副本;
read_doc_history可以读取任何保留的修订版本,并根据其存储的摘要进行校验。 - 整条记录替换,不做局部修改。 交接记录及任何其他记录文档都不能局部修改。系统会完整写入新修订版本,并注明其所取代的版本。
- 模板是受控文档。 基础模板随版本一同发布,且以发布的副本为准。每次基于模板的写入都会根据模板进行检查;未通过检查的写入会保留并标记为待审核,绝不会被悄然接受或丢弃。
- 测试会严格把关。 如果
src/中的任何内容从某个层、文档集合或转录集合中移除内容,write-path.test.js就会失败;如果数据库角色获授予移除权限,atlas-role.test.js就会失败。
30 秒快速开始
- 引导式设置: docs/GUIDED-SETUP.md — 从未部署过任何东西?你的助手会负责安装、配置、部署和验证。系统提示时,你只需登录并粘贴连接器 URL。
- 设置: docs/SETUP.md — 手动操作指南:MongoDB、两种传输方式和首次运行访谈。
- 部署: docs/DEPLOY.md — 使用 OAuth 的远程 HTTP,让手机、浏览器或其他供应商的 Agent 访问同一个存储。
状态:0.x。存储的文档结构尚未冻结。 在 1.0 之前,架构可能会发生 破坏性变更。如果依赖此版本,请固定版本号。
允许多个主机或机器人写入前,请设置
EMET_SOURCE_TAGS。 来源标签 防护(拒绝在主记录上使用未注册的标签,以及在其他机器人的bots/文件夹中写入) 在设置EMET_SOURCE_TAGS前处于关闭状态。未设置时,任何调用方都能在任意 标签下写入。请参阅.env.example和 docs/DEPLOY.md。
安装为 Cursor / Grok Bot 插件
EMET 提供适用于 Cursor 和 Grok Bot 的插件(本仓库中的 .cursor-plugin/plugin.json 和
mcp.json)。Cursor Marketplace 上的插件已提交,正在等待审核。此插件不会替你托管记忆:它会连接到你自己的 EMET
服务,因此你的记录会保留在你自己的 MongoDB 中。
- 远程部署 EMET:docs/DEPLOY.md。设置
PUBLIC_URL和强密码EMET_OAUTH_PASSPHRASE。检查https://<your-host>/health是否返回{"ok":true,...}。 - 安装 EMET 插件,并将变量
EMET_URL设置为https://<host>/mcp。 - 连接。OAuth 登录会打开 EMET 的密码页面;每台设备输入一次密码即可。
- 验证:使用
{"probe": true}调用emet_status(此操作仅会在_emet_verify中写入、读回并移除一行),然后使用save_to_layer保存内容,并从另一个客户端调用recall读取。
插件还附带 emet-memory 技能,用于告知 Agent 何时读取记忆、保存内容并结束会话。
跨主机共享记忆
EMET 将 Agent 记忆存储在 MongoDB(Atlas 或可自托管)中,分为六层,另设逐字记录层,并通过回读验证每次写入。它使用同一份源代码,既可作为本地 stdio 服务运行,也可作为远程 HTTP 服务运行,因此能充当桥梁:多个主机上的 AI 模型——桌面端、命令行、浏览器、手机、其他厂商的 Agent——都能读写同一份共享记忆,前一个主机留下的工作、检查结果和决策,后一个主机都能获取。这也包括 Grok Bot 团队,以及连接到同一服务的单会话 Grok 聊天:机器人保存的内容会在新的 Grok 聊天完成初始化后供其查找,反之亦然。要连接单会话聊天(Grok 项目,或任何支持远程 MCP 连接器并支持通过 OAuth 登录的聊天主机),请在其中添加已部署服务的 URL https://<your-host>/mcp 作为远程 MCP 连接器,并使用你设置的密码短语完成登录(EMET_OAUTH_PASSPHRASE;参见连接客户端)。仅支持固定标头或密钥的连接器无法使用。然后将docs/BOOTSTRAP.md 中的指令块放入项目指令:加入该指令块后,聊天最可靠地会从第一轮开始主动使用该记忆;如果没有相应字段,也可将其作为第一条消息粘贴,作为后备方案。存储只有一个所有者;其他人只能通过可选的访客(只读)和成员(读写)权限加入,且这两种权限默认均关闭。
两种访问方式
尚未初始化的会话可以读取所有内容,但只能写入新的投递项(drops/<date>-<slug>.md):其中的条目、总结和交接内容。拥有已注册来源并完成完整启动流程的会话则拥有完整访问权限。每个已初始化的会话都会优先看到尚未处理的投递项。只有在该会话启动时已列出的投递项,才会导致看板编辑请求被拒绝。每天(纽约时间)最多可创建 20 个新投递项,且共用一个配额桶:stdio 上的整个本地进程、没有已登录客户端的 HTTP 共享访客配额桶、审批时绑定的来源标签(如果有),否则为相应的 OAuth 客户端。调用方自行输入的来源不会改变配额桶。投递项永不删除。
编辑工作状态
服务器会在设置时创建一个空白看板。任何会话都不能写入看板。看板须使用 patch_doc 并附上原因进行编辑;新增行须注明其来源。一旦存在任何工作文档,就只能编辑,不能重写。如果某项更改导致文档大小低于本会话首次修改该文档时大小的一半,或低于过去 24 小时内其最大大小的一半,则会被拒绝。retire_doc 会拒绝处理工作文档。rename_doc 会生成新 ID 并将旧 ID 标记为已退役;旧文本仍可读取,且仍会显示在列表中。restore_doc 是唯一的整份文档恢复方式,适用于所有者列出的标签,且仅限恢复摘要匹配的已存储合规版本。
名称
EMET 是 אמת,即希伯来语中的 真理。它也是对服务功能的缩写: 持久记忆与认知层级。
这个名称并非装饰。在魔像传说中,人们将 emet 刻在身上,使其获得生命;擦去第一个字母,就会留下 מת——met,意为死亡。一个字符,决定了天壤之别。
因此,验证层被称为 aleph。 EMET 中的每次写入都会被读回并进行比对,然后才报告已保存。这种读回就是 aleph:没有它的记忆系统仍会返回答案,看起来也运行正常,却悄然成了一具尸体——它会信誓旦旦地提供从未真正存储过的内容。Aleph 也是第一个字母,这与协议坚持的顺序相符:重试前先验证,应用补救措施前先验证前提,信任前先读回。
同一个意象也说明了设置的作用。魔像在被刻上文字之前只是惰性的;全新的模型实例在记录写入其中之前也尚未成形。emet_initialize 就是那道刻印——它赋予模型一个身份、它所服务的人、当前状况,以及它必须遵守的准则。这里并无任何神秘之说。这只是对首次工具调用实际作用的字面描述。
缩写的两部分都不可或缺。
持久并非保守的说法,也不是与其他记忆产品竞争的一项功能。它是相对于语言模型的默认状态来衡量的:就像一块涂画板。每次会话都从零开始,也以忘掉一切告终。你得再次解释自己,再次说明正在做什么,再次推导已经做过的决定。**持久,描述的是一个了解你的助手与一个再次初次见到你的助手之间的差别。**这是它的本质,不是附加其上的营销形容词。
认知层级确保这种持久性值得信赖。第 1 层保存经过整理的主张;第 2 层保存为这些主张提供依据的逐字记录——将有依据的信念与其证据在结构上分开保存,避免任何一方悄然覆盖另一方。无法核查的持久记忆,只不过是存续更久的自信噪声。
一份记忆,适用于每台设备
这是首先值得了解的部分。
大多数 MCP 记忆服务都作为本地进程运行:桌面端通过 stdio 启动它,记忆则存放在该进程能够访问的位置。直到你拿起手机,这种方式才会遇到问题。
EMET 在同一份源代码中提供 两个入口:
src/index.js | stdio | 桌面端和 CLI,由客户端启动 |
src/http-remote.js | 流式 HTTP + OAuth | 手机、平板电脑、浏览器、其他厂商的 Agent |
它们不是两个项目。 两者都会调用 dispatch.js 中的同一个 dispatchTool,而后者会调用 tools.js 和 database.js。src/index.js 还会导入 validation.js。src/http-remote.js 不会导入 validation.js 或 content_analyzer.js;tools.js 会导入分析器。两者唯一不同的是传输方式。无需维护两个分支,也没有第二份副本逐渐过时,更不会有落后于正式版本的“移动版”。修复只需做一次,两种传输方式都会获得更新。
将 HTTP 入口部署到任何可以运行容器的主机上——项目附带了 Dockerfile,docs/DEPLOY.md 介绍了 Railway 和 Fly 的部署步骤——你拥有的每台设备都能访问同一个存储库。在笔记本电脑上记录一个决定,一小时后用手机询问它,内容就在那里。再添加一台主机,它也会加入同一份记忆,而不是另建一份。
远程端点受 OAuth 保护:设备使用你设置的口令授权一次,令牌会在重启和重新部署后保留。如果有多台主机共用一台服务器,请不要设置 EMET_SOURCE_TAG。通过 HTTP 写入文档、转录和记忆时,会使用授权时绑定的来源标签。本地 stdio 主机在写入时仍会将 EMET_SOURCE_TAGS 中的精确标签作为 source 传入(参见 .env.example)。每条记录都会标明由哪台机器生成——多台主机向同一个存储库写入时,来源信息不再是可有可无的记录工作,而是让你能够信任所读内容的关键。
单个用户的运行成本很低,只需一个小型 MongoDB 集群和一个爱好者级别的容器主机。两者都提供足以起步的免费套餐。
最佳实践
按每台主机最擅长的工作来分工。 单会话聊天(例如按照顶部说明连接的 Grok 项目聊天)只有在你向它发送消息时才会运行。它无法监视 EMET,机器人也无法向它发送消息。因此,当机器人团队需要聊天协助时:
- 机器人在 EMET 中创建一项任务,并将完成任务所需的所有内容写入文档,然后把文档的 id 告诉你。
- 你向聊天发送一行指令,指出该文档,例如:“初始化 EMET,读取 threads/open-tasks.md,完成任务,并将结果保存到 EMET。”
- 聊天将草稿保存到 EMET。
- 机器人检查草稿。
聊天适合的工作:在一次会话中完成的长篇起草和审阅。机器人适合的工作:在代码仓库、服务器上、按计划执行,或在账户中完成的工作。如果聊天使用单独的套餐,它的工作会消耗该套餐的额度,而不是机器人的额度。
启动时获取每个启动页面,然后使用该次启动的 load_id 打开会话。 首先调用 emet_initialize {} 并获取它列出的每个页面;只有在已知某台主机能够完整读取较长结果时,才请求 {"page": "all"}。将这些页面中的 load_id 传给 emet_session_open。如果打开会话时没有提供 load_id、缺少页面,或服务器没有该 load_id 的记录(例如它来自重启前,或自上次获取页面起已过 60 分钟),请求就会被拒绝,回复会说明应调用什么。页面检查会在你自己的启动过程中执行,任何被跳过的页面都会在写入任何内容前导致请求被拒绝。参见 docs/BOOTSTRAP.md 和 docs/BOTS.md。
在多个主机或机器人写入之前,先设置来源标签列表。 除非设置了 EMET_SOURCE_TAGS,否则拒绝未注册标签的保护机制不会启用;如果未设置,stdio 调用方可以使用任何标签写入。通过 HTTP 时,标签是在主机获批时绑定的标签。请在主机进行设置访谈之前,将其标签加入列表。如果多个主机写入同一个存储,标签能让你确认每条内容由谁写入。请参阅 docs/SETUP.md。
将指令块放入项目的指令中。 连接服务会让助手具备记忆能力,但不会让它使用这种能力。具有指令字段的主机是最完善的集成方式:将 docs/BOOTSTRAP.md 中的指令块粘贴到该字段中。如果没有这样的字段,就将其作为会话的第一条消息粘贴进去。放入指令块后,助手从第一轮对话开始就能更可靠地调用记忆,而不是根据推断作答却什么也不保存。
对读取操作保持长期批准,并留意那三种会替换文档的写入操作。 读取不花费任何代价,也不会造成破坏性错误,因此对大多数用户来说,为读取操作设置长期批准是合理的默认做法。追加式写入可以恢复,因为更正内容会取代被更正的内容,而不是将其覆盖。三种替换式写入会归档先前版本,但如果写入内容有误,它会悄无声息地变成另一份文档,而不会报错。不要对文件系统或 Shell 服务采用这种做法。请参阅 docs/PERMISSIONS.md。
工具列表发生变化的升级后,重新连接每个主机。 主机会缓存工具列表:部署新增或重命名的工具后,每个主机仍会提供旧的工具集,直到你在该主机上重新连接连接器。主机看到新列表之前,新工具会一直不可见;主机也可能再次请求你已经授予的批准。请参阅 docs/DEPLOY.md。
机器人首次写入前先注册,并在一次变更中设置两个列表。 一个机器人对应一个标签。在同一次部署中,将标签加入 EMET_SOURCE_TAGS,并将 bots/<tag>/HANDOFF.md 附加到 EMET_RECORD_DOCS。已注册但未列为机器人的标签仍可写入主记录。中心的标签已注册为主机,且绝不会列为机器人。请参阅 docs/BOTS.md。
信任新的部署之前,先验证写入路径。 使用 probe: true 运行 emet_status。它会写入测试文档、读回相同的值并将其删除;仅执行读取无法发现写入路径故障。缺少权限的角色会在此处明确报错,而不是等到会话进行到一半才出错。然后从一台设备保存内容,再从另一台设备取回。请参阅 docs/SETUP.md。
将通用风格保存在存储中,不要粘贴到每个项目里。 使用 write_doc 写入的文档会被每个主机读取,并且只需在一处修改;粘贴到三个项目中的文本会变成三份不一致的副本。交付偏好应放在 bootstrap/USER.md 中,而不是某个供应商自己的记忆里,因为后者无法带到下一个主机。请参阅 docs/BOOTSTRAP.md。
署名
EMET 是 Jason Glass / C.I.P.S. LLC 发布的 CASCADE Enterprise RAM 的衍生项目,原项目发布于 github.com/For-Sunny/cascade-memory-enterprise。本项目依据上游在提交 acdb5c8c99cdfc29fb44db2cd828d7752be744db(v2.2.2,2026-02-19)采用的 MIT 许可证使用并再分发。LICENSE 中保留了双方的版权声明。
链接不可用: 截至 2026-10-03,该仓库已无法访问(GitHub 返回 404),也未找到迁移后的副本、公开镜像或存档。此处保留原地址和提交号,作为本代码来源的记录。
上游代码占比:按实际比较结果计算,而非估算。 将本代码树与上述提交中的上游 v2.2.2 进行逐行比较。
方法:用可复现的方式说明:对非空行进行精确、区分大小写的比较;如果本仓库的文件中存在与上游某行完全相同的行,则该上游行计为保留。“上游有多少内容保留在此处”与“此文件有多少内容来自上游”是两个不同的问题,答案也不同,因此两者都列出。scripts/measure-upstream.mjs 会根据上方提交的克隆重新生成此表(由于上游仓库已无法访问,需要使用此前保存的副本)——每次修改 src/ 后都应运行此脚本,因为无法轻易重新计算的数字迟早会悄然过时。
| 文件 | 此处的行数 | 保留的上游行数 | 占上游比例 | 占此文件比例 |
|---|---|---|---|---|
src/content_analyzer.js | 450 | 未重新计算 | — | — |
src/validation.js | 1163 | 未重新计算 | — | — |
src/index.js | 390 | 未重新计算 | — | — |
src/tools.js | 4273 | 未重新计算 | — | — |
src/database.js | 811 | 未重新计算 | — | — |
src/http-remote.js | 269 | — | — | 上游没有对应文件 |
src/oauth-provider.js | 702 | — | — | 上游没有对应文件 |
src/named-items.js | 206 | — | — | 上游没有对应文件 |
src/security-headers.js | 26 | — | — | 上游没有对应文件 |
src/layers.js、floor.js、aleph.js、session.js、setup.js、gaps.js、provenance.js、redact.js、annotations.js、request-log.js、disciplines.js、metadata.js、env.js | 4803 | — | — | 上游没有对应文件 |
此处的行数统计于 2026-10-07(包括空行在内的所有行,末尾一个空行除外)。保留比例未重新计算:上游仓库已无法解析,也没有可用的上游提交本地副本。上次测量针对的是较早版本:内容分析器保留 394 行中的 361 行(占上游 91.6%,占该文件 93.5%);验证层保留 798 行中的 765 行(95.9%,78.2%);索引文件保留 631 行中的 541 行(85.7%,74.0%);工具文件保留 1007 行中的 798 行(79.2%,33.2%);数据库文件保留 651 行中的 353 行(54.2%,40.2%)。src/ 下的其他文件也没有上游对应文件,因此未列入此表。
上游的 server/decay.js 和 server/healthcheck.js 在此处没有对应文件,也完全没有保留。在五个有对应文件的文件中,内容分析器和验证层主要由 Jason 编写;他的 tools.js 和 index.js 中的大部分内容也保留在此处——路由模式、验证器和错误类都出自他手。下文会说明有哪些改动;没有改动的部分仍然属于他。
与上游的差异
- **存储:**从 RAM 磁盘上的 SQLite 迁移到 MongoDB。上游的
ram_disk_manager和server/decay.js时间衰减引擎未被沿用,SQLite/RAM 代码路径也已移除,而非留作闲置代码。 - **传输:**从仅支持 stdio 改为同时支持 stdio 和带 OAuth 的可流式 HTTP,二者来自同一代码树。
- **工具:**从 7 个增至 29 个。3 个沿用自上游(
recall、query_layer、save_to_layer);remember、get_stats、get_echo_stats和get_status因重复或已合并到emet_status而被移除。另有 26 个工具在上游没有对应项——文档存储(read_doc/read_doc_history/write_doc/list_docs/patch_doc/retire_doc/rename_doc/restore_doc/validate_doc/accept_nonconformance)、转录层(save_transcript/revise_transcript/query_transcripts——也会读取 2026-07 之前的交互归档)、semantic_recall、revise_memory、corpus_recall、emet_gaps、emet_floor、初始化和会话工具系列(emet_initialize、emet_status、emet_setup_complete、emet_session_open、emet_transcript_append、emet_session_close),以及两个访问入口(emet_invite_create、emet_member_access_create)。 - **写入路径:**上游会验证并插入数据。这里,每次分层记忆写入、转录写入和文档写入都会经过同一个入口:清理机密信息、确定来源标签、用清晰可见的标记填充缺失的责任追踪字段、将内容和元数据分别摘要到各自的列中、读回该行,并返回统一格式的回执。若出现第二个入口,测试就会令构建失败。参见下文的入口。
- **元数据词汇:**上游的白名单会将其不认识的字段移到
custom下。EMET 自身的字段——supersedes、superseded_by、superseded_at、corrects、corrected_by、valid_until、valid_from、routing、assertion_origin和标记布尔值——均为一等字段;仍被移入其他位置的字段会在回执中列出。 - 章程即数据:
layers.js在每个层级的说明旁记录其数值重要性区间、是否允许修订、是否必须提供来源信息、别名和预期占比;写入路径会读取这些数据,而非将其硬编码。
这里并未声称分层记忆、条目取代机制或精选/逐字存储分离的设计源自本项目。其他项目也采用了类似理念。EMET 的目标是让底层基质不必成为你需要操心的事情。
安装
从未部署过任何东西?让你的 AI 来部署:docs/GUIDED-SETUP.md。 按提示登录两项服务并粘贴一个 URL;助手会安装、配置、部署并验证其余内容。它需要一个 shell——Claude Code 和 Cowork 自带 shell;Claude Desktop 则可通过 Desktop Commander 之类的 MCP 服务获得 shell(指南中有一条命令即可完成)。
完整指南:docs/SETUP.md——Atlas、两种传输方式和首次运行访谈。远程部署:docs/DEPLOY.md。 让助手真正使用它:docs/BOOTSTRAP.md。 让机器人与你的助手并行运行——注册它们的标签、各自的交接流程和会话循环:docs/BOTS.md。有关机器人团队的设计,请参阅 docs/SUBKEYS.md。
持续聊天。 长时间运行的聊天中的机器人会逐字将聊天记录保存到 EMET,至少每晚保存一次(作为检查点),保存到开放会话或每日的
YYYY-MM-DD_chat-daily 会话中。优先使用事件触发,而非基于时间的轮询。
无法避免轮询时,使用任务允许的最长间隔。绝不要重建对话轮次。将缺失部分标记为缺失。关闭会话时要引用已保存的部分。
上下文压缩会抹去较早的对话轮次,因此几天后关闭会话时不能凭空补出聊天记录。
简短版:
npm install
cp .env.example .env # fill in MONGODB_URI and MONGODB_DB
npm start
需要 Node 22+ 和 MongoDB 连接。.env 已被 gitignore,绝不要提交它。
本地(stdio)——作为 MCP 服务条目:
{ "mcpServers": { "emet": { "command": "node", "args": ["/path/to/emet/src/index.js"] } } }
远程(HTTP)——运行 npm run start:http,或将附带的 Dockerfile 部署到任意容器托管平台。设置 PORT、PUBLIC_URL(OAuth 颁发者——必须与部署的 URL 完全一致)和
EMET_OAUTH_PASSPHRASE。然后在每个客户端中添加 https://<your-host>/mcp 作为自定义连接器。请参阅 docs/DEPLOY.md。
为每个托管端分配各自的来源标签。通过 HTTP 写入文档、聊天记录和记忆时,使用批准时绑定的标签;调用方不会在这些写入中传递 source。本地 stdio 托管端仍须将已注册的精确标签作为 source 传递。EMET_SOURCE_TAGS 用于注册标签。在多个托管端共用的服务上,不要设置 EMET_SOURCE_TAG,因为默认标签会让所有托管端看起来都一样;只有一个托管端使用该服务时才设置它(例如本地 stdio 条目)。每次写入都会记录生成内容的托管端;共用默认标签只会造成归属不明,而非实现归属追踪。
首次运行
说 “initialize”。这就是全部交互方式。
emet_initialize 是入口,也是任何人唯一需要了解的工具。在新存储中,它会返回一段简短的访谈内容——助手应该叫什么、应该如何行事、你的名字、你打算用它做什么,以及是否有多个机器人或 AI 会使用这份记忆——由你的助手以对话方式完成,然后通过 emet_setup_complete 记录答案,同时创建所有者人员条目。是否有多个机器人或 AI 会使用这份记忆是可选项;不填写则不会启用委派客户端子密钥。回答“是”时,只有在此服务读取 .env 时才会写入子密钥,并且要重启后才会生效。托管服务会拒绝此操作,并告知你需要设置的三个变量,然后重新部署。请参阅 docs/SUBKEYS.md。在已有存储中,同一调用会返回助手的身份、你的个人资料以及上次进展到的位置,因此会话一开始就已了解你。具有只读权限的 emet_initialize 不会写入任何内容;具有写入权限的调用可能会创建缺失的看板。首次设置属于所有者:带有机器人标签时,emet_setup_complete 会被拒绝;存储已完成设置时,除非所有者明确要求并传入 reissue: true,否则也会被拒绝。
要求使用设置命令本末倒置:你唯一需要它的时候是第一次会话,而那时你还没读过任何内容。因此,分支逻辑放在工具内部,你无需知道自己属于哪种情况。
连接此服务会让助手具备记忆能力,但不会让它在没有提示的情况下主动使用该能力。docs/BOOTSTRAP.md 提供了一段可粘贴到项目说明中的简短文本,并说明了在完全没有说明字段的主机上,哪些功能仍然可用。
有两种行为值得了解。设置不会在答案不完整时完成,而是拒绝替自己编造身份。如果无法访问存储,initialize 会明确说明,而不会假装记忆可用后继续操作——编造连续性比诚实承认信息缺失更糟。
此后每次会话:如何开始,如何结束
记忆的好坏,取决于对话开始和结束时的两个关键时刻。除非写入存储,否则助手学到的内容都无法留存;而写入的时机往往毫无预警——关闭标签页、达到上下文限制,或接到电话。因此,每次会话都要以一个词开始,以一个词结束。
开始:说“initialize”。 助手会调用 emet_initialize,获取自己的身份、你的身份、事情进展到哪里以及操作规范,然后从那里接续。在向你提问之前,它会根据交接文档和状态文档列出你的事项:所有尚未完成的事项、未来 7 天内有日期的事项、你负责且尚未结项的每项事项及其下一步行动,以及所有尚未决定的事情。会话开始时没有点明的事项很容易遗失,因此,只给出一个数量,或笼统地问“你想从哪里开始?”,都不算完成启动。每次开启新对话、每台设备上都要这样做——在你真正要问的问题之前,先发出这条消息。跳过这一步的会话,只能凭猜测作答,也不会保存任何内容。
结束:说“wrap up”(或说再见,或表示你要开启新会话)。随后,助手会写入四项内容——交接文档、状态文档、记录本次会话的情景条目,以及逐字记录的对话文本——并调用 emet_session_close,检查每一项是否确实写入成功,并返回 state: "complete"。**看到这个词后再关闭窗口。**如果返回其他内容,就说明有内容未能保存,助手会指出具体是哪一项。
为什么每次都要完成这两步:各层记录的是经过整理的含义,而对话文本则是逐字记录;下一次会话会读取前者,并可按时间戳查阅后者。没有按流程结束,下一次会话就会留下一个缺口——它看不到发生了什么,只知道发生过某些事,而 emet_gaps 会将其计数。没有按流程开始,助手就会变成一个拿着你的工具的陌生人。docs/BOOTSTRAP.md 中的指令块会在支持常驻指令的宿主环境中自动完成这两步;在不支持的宿主环境中,则需要你提供这两个词。
设计说明
六个层次
记忆按条目的类型而非主题划分。每条记忆都带有重要性评分,也可以选择设置有效时间范围。
| 层次 | 内容 | 重要性 | 写入时机 |
|---|---|---|---|
| identity | 助手是谁,以及它服务于谁。持久的自我定义和用户的长期要求。 | 0.9 – 1.0 | 按设计很少写入 |
| semantic | 无论何时得知都始终成立的事实。人物、系统、定义和已确立的结论。 | 0.6 – 0.9 | 确立或更正持久事实时 |
| episodic | 发生了什么,以及何时发生。会话记录、已解决的问题、长期工作进展。 | 0.4 – 0.7 | 会话结束时及达到里程碑时 |
| procedural | 如何完成某件事,以及指导未来行动的决策——包括其推理过程。 | 0.5 – 0.8 | 决策会约束后续工作时 |
| meta | 只有从交流之外才能看到的内容:记忆系统的行为方式,以及助手对待用户的方式。 | 0.6 – 0.9 | 检索失败或成功、协议偏离,以及对与用户协作方式的更正发生时 |
| working | 短期上下文、进行中的状态、留给后续接手者的备注。 | 0.3 – 0.6 | 在会话间保存状态时 |
最重要的区分是:episodic、semantic 和 procedural 都与世界有关——发生了什么、什么是真实的、如何完成某件事。Meta 与系统有关——从交流之外回看这次交流。属于 meta 的内容往往找不到其他合适的归属;也正因如此,如果没有对 meta 的定义,这类内容就会被丢弃。
每个层次从首次安装起就能发挥作用
创建六个集合,不等于拥有六个层。没有明确职责的层无法回答 “这应该放在这里吗?”,因此没有内容会被路由到那里——空集合既不会报错,也不会导致测试失败,所以它可以一直空着,无人察觉。
在这一设计的源部署中,元层近一年几乎没有使用。这个集合从第一天起就存在,工具也接受向其中写入内容;只是从来没有一个显而易见的理由让人把内容放进去。后来终于为它定义了职责,它便立刻填满了——这证明相关材料一直都存在,只是此前被丢弃了。
这个仓库中有两项内容正是因此而存在:
src/layers.js在代码中定义了全部六个层的职责,并由emet_initialize返回——无论是全新的存储,还是已有的存储,都会返回。理由与操作规范相同:文档是给人看的,而这些内容必须在每次会话开始时传达给模型。哪些内容应该放入,哪些不应该放入,何时写入,何时读取,以及该层缺失时是什么情况。assessLayerHealth()会报告已经沉寂的层。 存储无法知道本该写入什么,但它能看出某一层的内容寥寥无几,而其他层都已填充,并在下一次会话的首个载荷中指出这一点。对上述真实历史进行检查时,它会标记元层:存储中共有 10 条,所占比例为 1.8%——待该层得到实际使用后,警告便会消失。它只报告,不会阻止操作;对于尚未形成使用模式的新存储,它也会保持静默。
两个层级,刻意保持独立。 层条目是经过整理的含义;另有一个转录存储,用于保存逐字对话。层条目虽相关但内容单薄时,转录中保存着被压缩掉的深度。在写入转录时进行摘要,会把两个层级合并为一个,并破坏第二条探究路径——因此,转录路径会原样保存收到的内容。如果载荷无法传输,就把它拆分成多个部分;不要压缩。
层级 1 如何连接到层级 2——两种链接,一条优先规则。
条目会记录其来源转录片段:session_id 加上一段交互索引范围,并在保存时写入 derived_from。从其他条目整合而来的条目则会携带 {entries: [id, ...]},因为没有转录来源的条目必须能够明确说明这一点,而不是带着一个伪装成引用的、按最近时间戳猜出的来源。两者都存储为顶层索引列,而不是放在元数据子文档中,因此,反向查询——找出所有源自这份转录的条目——只需执行查询即可。
每个条目也都有时间戳,因此始终可以搜索其周边时间窗口。这两者并非冗余副本:指针精确但有条件,时间戳通用但近似——结果取决于如何选择时间窗口;当会话重叠时,准确性会下降;而当时间戳不准时,它返回的是相邻片段,而不是失败。
优先规则能避免两者冲突:只要指针存在,就优先使用指针;指针为 null 时,才回退到时间戳。 绝不取平均,绝不合并;也绝不只因为时间戳返回了结果,就优先使用时间戳。resolveProvenance() 会应用这条规则,因此调用方无需自行判断。
同时保留两者的好处,是它们能够相互校验——单独任何一者都做不到这一点。 如果某个条目的指针指向一个会话,而对应片段与该条目自身的时间戳相距甚远,那就说明出了问题:指针写错了、字段从其他条目复制而来,或条目被归到了错误的会话。crossCheckProvenance() 会报告这种不一致。能够彼此矛盾的两种独立推导结果,在工程意义上才算冗余;只有一种只能被单方面信任的链接,不算。
反向查询——我们从本次会话中获取了什么?——并不是第二种结构。
source_session_id 已建立索引,因此只是以相反方向读取同一份数据。存储反向索引会造成现有数据的镜像,而镜像正是分叉的起源。
**在指针出现前写入的条目会保留空指针,并按时间戳解析;对它们而言,这仍然是正确的——只是已被取代,而非错误。**不会为它们回填指针:要为它们凭空生成指针,只能根据时间戳推断,而这正是指针所取代的较弱方法。旧记录不会被改写成新记录的样子。另请注意,按时间戳窗口搜索无结果,并不能证明什么都没有记录——某个时段可能完全没有转录内容。
采用取代,而非修改。revise_memory 会写入一个带有 supersedes 的新条目,并在父条目上标记 superseded_by 和 superseded_at。不会编辑或删除任何内容,也不会隐藏父条目。召回结果先按查询词的匹配数量排序,再按重要性排序。所有排序方式都将当前条目置于前面,包括按时间排序。存储会先应用相同的排序,再保留时间窗口,因此更强的匹配不会被较低的重要性或较早的时间排除。单词按原样匹配,因此 $gt 就是那段文本;大小写不同的字母也会匹配,包括 École 和 école。İ 只匹配自身。全角字母只会与对应的全角字母进行折叠匹配。ẞ 和 ß 会折叠匹配。存储和页面使用相同的匹配方式。使用 order: 'latest' 时,结果按时间排序。单独的 "latest"、"newest" 或 "most recent" 与其他词一起使用时,会重新排列这些匹配结果,但不会改变匹配的行。带连字符的单词(如 latest-budget)、"not the latest" 和单独的 "latest" 都是普通关键词。
已被取代或过期的条目仍会出现在相同的结果中,并标记其结束日期及替代它的条目 ID。如果当前匹配项已填满上限,而你仍需要已结束的条目,请传入 history: true。情节条目不可变——事件不会被修订。
**部分修正采用更正,而非取代。**如果后续条目只更正了较早条目的一部分(例如某个情节中的一项),取代操作会给整个父条目(包括仍然成立的部分)加上结束日期。父条目仍可读取,并标记为已结束。改为让新条目携带 metadata.corrects: <parent id>(同一层;如果父条目不存在,会在写入任何内容前拒绝操作),并在父条目上标记:其 corrected_by 列表新增该条目的 ID,内容、摘要和有效状态保持不变,同时在同一次写入中重新验证元数据摘要。读取结果会返回 corrected_by,因此读取父条目的人会看到存在更正。corrected_by 是一个指示标记;调用方不能设置它。在此功能出现前已更正的条目(例如情节条目 ref 604 更正 ref 603)不会带有链接;修复方式是新增一个注明 corrects: 603 的更正条目,而不是编辑或回填。
**经过验证的写入——src/aleph.js。**每次写入都会读回并进行比较,之后才会报告已保存。文档写入会归档先前版本,然后确认 sha256 和版本;层写入会确认已分配 ID 对应的存储内容。这里按内容而非是否存在来检查 ID 是有意为之:ID 来自一个 counters 集合,如果从错误的集合读取计数器,计数就会重置为 1,并静默覆盖真实条目——这看起来就像保存成功一样。
验证失败会引发 AlephError,并且刻意与数据库错误区分开来。数据库错误意味着无法访问存储,写入很可能没有发生。AlephError 意味着调用已成功,但存储中的内容与预期不符。两者中这更为严重,也绝不能盲目重试,因此绝不会将其简单归结为“保存失败”。
aleph 会在两个时刻回答两个问题。写入时:记录是否准确?
上方的回读发生在任何回执之前。此后的每次读取:内容是否仍与记录时一致? 每一行都包含内容和元数据摘要、覆盖两者的 HMAC 签名(设置了 EMET_SIGNING_KEY 时;密钥保留在服务端),以及其所在层哈希链中的链接(每个条目的链摘要都涵盖其自身的内容摘要及其前一条目的内容摘要)。每次读取都会重新计算摘要并报告 integrity;emet_gaps 会遍历任意主机的整条链,并统计写入后被篡改的行(broken_chain)或从链中移除的行(chain_discontinuities)。两者的预期计数均为零。
入口
真实性、可追责性和验证应当是存储的保证属性,而不是模型可以选择不使用的选项。模型只要能被告知某个文件,就可能忽略它。因此,这项保证来自一种属性,而不是一个模块:每个存储只有一种写入方式,且其中包含检查。 六个层级使用 saveMemory,文档使用 writeDoc(patch_doc 会委托给它),逐字记录层级使用 saveTranscript 和 reviseTranscript。tests/write-path.test.js 会扫描 src/ 中所有 MongoDB 写入动词,并在发现它们位于这些函数之外时令构建失败——它曾在 setup.js 中发现并移除了第二个文档入口。
入口内部,依次执行:
- 在存储或哈希任何内容之前,秘密信息都会被屏蔽(
src/redact.js):包含秘密信息的环境变量(包括EMET_SUBKEY_PEPPER)的字面值、URI 中的凭据、Bearer 令牌、JWT,以及键名表示秘密信息的key: value对都会变成*****。屏蔽标记会有意显示出来,回执中的redacted报告本次写入实际更改的片段数量(已带有屏蔽标记的文本会保持不变,也不计入数量,因此,对文档进行读取后修改再写入时,文档原有的屏蔽标记不会计入),并且每条渲染后的日志行都会经过同一个清理器。 - 来源标签在这里确定,而不是由 HTTP 调用方决定。文档、转录和记忆写入使用审批时绑定的标签。在 stdio 上,使用调用方准确注册的标签;否则使用标记为
source_defaulted: true的环境默认值;再否则使用标记为source_missing: true的null。 - 每个缺失的问责字段都会变成可见标记,而不是直接缺失——
provenance_missing、assertion_origin_missing、importance_defaulted、source_unregistered——因为缺失无法查询,而“缺失”可以。缺失字段会被标记,不会被拒绝,但有一个例外(2026-09-29):没有来源的语义或流程类条目会被拒绝,因为无法指出言说出处的持久主张,正是这份记录旨在防止的情况。如果调用方指定会话,入口会根据会话的转录位置盖上来源标记。重要性默认为该层章程的中点;超出范围的值会保留并发出警告。入口绝不会推断主张的来源。 - 路由决策会保存在
metadata.routing中——包括该层是由调用方选择还是回退选择,以及两个路由器各自的判断——以便之后进行路由度量,而不是依赖记忆。**未标注的写入绝不会由路由器决定存放位置:**它会进入working,标记为routing.method: 'fallback',并附带回执警告。指定层是常规做法。 - 内容和元数据会分别摘要并存入各自的列(
sha256、metadata_sha256,后者基于规范化 JSON),写入报告发出前会读回并进行比对。每条读取路径——recall、query_layer、semantic_recall——都会在返回时重新计算两者,并附上integrity和metadata_integrity:值为true、false(返回该行并标记,绝不会丢弃),或对在该列存在之前写入的行标记为'unattested'。只有替代关系和更正标记是经批准的存储元数据变更,两者都会重新认证父项。 - 两种传输方式使用相同的回执结构:
layer, id, timestamp, verified, sha256, digest_stored, metadata_sha256, metadata_digest_stored, redacted, provenance, source, assertion_origin, importance, routing, warnings[],另加适用的标记。
而记录对自身缺口的如实呈现,也是一项查询:emet_gaps 会统计以下项目,并列出各项计数对应的 ID:无来源的断言、无归属的推断、按标签和会话划分的归属不明确项、自行路由的写入、未经认证的行、摘要不匹配、与转录存储不一致的指针、失效的替代关系、失效的更正链接、已过期但仍有效的行、待整合候选项,以及按来源统计的屏蔽片段。它只识别,不会填补——来源可知时,应使用真实指针调用 revise_memory 修复;来源不可知时,缺口就作为事实保留,并且从那天起不再增加计数。emet_initialize 会将这些计数与 layer_health 一并返回。
主题页面是视图,不是记录。一个领域对应一份受控文档 areas/<slug>.md,并根据 templates/AREA.md 进行检查。它会列出层、标签、关键词、文档前缀、可选的客户端列表、陈旧期限(默认 30 天)和上限。只有所有者可以创建或编辑领域。emet_page 会在每次请求时根据当前记录计算页面。每一行显示内容都标注来源:[mem:<id> · <layer> · <client> · <YYYY-MM-DD>] 或 [doc:<doc_id>@v<version>]。没有来源的行会被删除。除非设置了 include_history,否则已被取代和已退役的记录不会出现在当前章节中;若设置了该参数,它们也只会出现在「历史记录」中。页面不会存储、不会写回,也不会由 recall 返回。emet_page_lint 会标记矛盾、已被取代但仍列为当前的声明、无来源的行、重复项、失效链接和长期未经确认的状态。它不会写入任何内容。每周一运行 emet_initialize 时会记录每周计数。STATE.md 仍是手动维护的看板;areas/state.md 则是其旁生成视图的选择器。随附的示例包括 EMET、Health、Learning、Home 和 Garden。所有者使用 write_doc 将这些文件写入存储。页面仅对所有者可见。
这种方式可以保证的内容:每次写入都有证明、有归属、经过清理,并且所有缺失项都清晰可见。无法保证的内容(除非容许编造):每次写入都完整。存储不会对后者作出保证。
护栏与单轨
入口会守护每次写入。护栏与单轨守护会话写入的顺序。护栏(底线、特征、已提供的指令)划定一个领域,让 AI 在其中自由行动并作出判断。单轨(会话图 src/session-graph.js)将其从一个领域送往下一个领域,无需它自行选择:打开、工作、记录、看板、交接、关闭。车厢无法脱轨,也无法离开轨道。流程需要分岔时,交汇点处的道岔(EMET 代码中的一个小型逻辑单元,只会在满足其条件时触发,类似晶体管;不使用托管模型或 API)会将会话导向正确的护栏区域。由轨道扳动道岔,而不是由 AI 扳动。处理工作在各个站点进行,并且只有完成此前的每一站后,下一站的门才会打开。道岔已完成设计,但尚未构建。服务器会在每次受管控的写入中检查轨道,因此任何设备上的任何 AI 都遵循相同流程。无需主机钩子。任何被跳过的步骤都会遭到拒绝,并说明如何通过。完整实践、各道关卡及其明确的局限,请参阅 docs/GUARDRAILS-AND-MONORAILS.md。
工具
已注册 34 个工具,子密钥处于关闭状态。设置 EMET_SUBKEYS=1 后,还会启用另外 4 个工具(emet_subkey_request、emet_subkey_claim、emet_subkey_revoke、emet_subkey_end_session),以及 subkey 和 session_handle 参数。以下列出注册表注册的所有工具,包括这两组。
accept_nonconformance
corpus_recall
emet_client_approve
emet_error_report
emet_floor
emet_gaps
emet_initialize
emet_invite_create
emet_member_access_create
emet_page
emet_page_lint
emet_pages
emet_session_close
emet_session_open
emet_setup_complete
emet_status
emet_subkey_claim
emet_subkey_end_session
emet_subkey_request
emet_subkey_revoke
emet_transcript_append
list_docs
patch_doc
query_layer
query_transcripts
read_doc
read_doc_history
recall
rename_doc
restore_doc
retire_doc
revise_memory
revise_transcript
save_to_layer
save_transcript
semantic_recall
validate_doc
write_doc
保留修订版 — 每次写入文档都会归档先前的副本;read_doc_history 会列出某份文档保留的内容,并完整返回任何保留的修订版,同时根据其存储的摘要重新计算哈希值。无法读取回来的保留内容,只是没有凭证的承诺。
二级数据的统一读取器 — query_transcripts 会读取实时转录存储和旧版交互归档(即在转录存储出现前由早期主机写入的行),并将它们合并为同一种会话格式;每条结果都会标明其来源集合。两者之间不会迁移任何内容。
文档管控 — validate_doc 会根据管控文档的模板检查该文档;每次写入文档时,同一项检查也会在写入流程中运行,调用方无法跳过。检查不合格的记录会被标记,但不会被拒绝:记录仍会写入,带有说明失败项目的不符合项标记,并在会话开始时继续计入,直到通过后续修订更正,或使用 accept_nonconformance 进行处置。检查的内容是格式和连续性——各章节齐全、顺序正确且已填写;没有未填写的占位符;上一修订版中的每个项目都仍然存在,或附有处置说明。答案是否真实不是机器能够检查的,这套机制不会假装自己能做到。
会话开始 — emet_initialize(从这里开始):获取它列出的每个页面,或者在没有大小限制的主机上传入 page: "all";然后使用该次启动返回的 load_id 调用 emet_session_open。会话 ID 按本地时区日期生成(EMET_TIMEZONE,默认为 America/New_York);已关闭的会话永远不会重新打开,因此如果某个标识的 ID 已关闭,则会打开为 <id>-2、-3 等,结果也会说明这一点。会话完全关闭后,还恰好可以再追加一次,即关闭交互;该操作必须在关闭后的 EMET_CLOSE_GRACE_MINUTES(默认为 5,最多为 60)分钟内完成,写入会话的最后一部分,然后会话便会锁定;重试不会写入任何内容。仍允许使用 revise_transcript 进行更正,但必须保持交互编号完全不变。
会话期间 — 每次回复后调用一次 emet_transcript_append,每次只追加一轮交互。每个写入工具也都需要 session_id 和 exchange,并会返回 session_graph.next,即单轨上的下一个站点(护栏与单轨)。
会话结束 — 调用 emet_session_close。它会检查会话工作是否确实写入存储——转录已保存且完好无损、连续性文档已更新、已写入一条情景条目——并准确报告缺少哪些内容。它不会写入这些内容:只有模型掌握会话内容,而能够生成看似可信摘要的工具会制造虚假的连续性,正是这些规范旨在防止这种情况发生。emet_initialize 会在会话开始时返回相同的协议;只有这个时机足够早,才能发挥作用。
设置内部机制和诊断 — emet_status(环境、层、健康状况;probe: true 会写入并读回探测数据) · emet_setup_complete
审批
每个工具都在 src/annotations.js 的一个表格中声明 MCP 注解——readOnlyHint、destructiveHint、idempotentHint、openWorldHint。该表格也完整说明了 EMET 会对存储执行什么操作。34 个工具中有 15 个无法修改任何内容;4 个工具可以替换文档的可见内容;所有工具的 openWorldHint 均为 false。这些工具只能访问此服务自身的数据库,不会访问其他任何内容:没有文件系统,也没有 shell。登录服务会获取客户端的元数据文档,而且只会从已检查过的公共地址获取。
如果服务没有声明任何内容,就会被视为每次调用都可能执行任何操作。因此,凡是会在操作前征求确认的主机,都会在每次调用前征求确认,包括构成会话大部分内容的读取操作。这些提示让主机可以停止询问是否允许读取,而无需被告知信任写入操作。它们只是提示,不是强制措施:实际自动批准哪些操作由主机端设置决定。请参阅 docs/PERMISSIONS.md。
报告问题
emet_error_report 仅限所有者使用,且为只读工具。它会读取运行错误日志,并返回完整报告、mailto: 草稿,以及结构化邮件 {to, subject, body_text, short_id, version}。EMET 不会发送邮件,也不会为此工具建立网络连接。
请先阅读报告。草稿的收件人是 EMET_REPORT_EMAIL。未设置该变量时,收件地址为项目报告邮箱 [email protected]。将其设置为你自己的地址,即可让报告留在内部。如果将其设置为空字符串,或设置为任何非单一普通地址的值,该工具就只会返回报告,不会返回邮件。
拥有自身邮箱地址的调用助手可以将该结构化邮件传递给自己的邮件工具。调用助手只有在向用户展示报告并获得用户同意后,才能发送此报告。EMET 不会发送报告。代码表见 docs/ERROR-CODES.md,相关行为的说明见 docs/ERROR-CODES-SPEC.md。
测试
npm test
在 64 个测试套件中共有 1,852 项检查(统计截至 2026-10-07,依据 npm test 打印的 PASS 行)。不需要测试框架、数据库或凭据——每个套件都是运行在共享测试框架(tests/harness.js)上的普通 Node 脚本;运行器会发现所有 *.test.js 文件,因此在全新克隆的仓库中运行 npm test 即可。此处不记录各套件的检查数,因为这些数字很快就会过时;npm test 会打印它们。
validation.test.js 和 content_analyzer.test.js 最初是上游自己的测试套件,移植时除导入路径外未作改动;上游的每项断言仍然有效,新增内容则覆盖了 EMET 的元数据词汇和共享层列表。它们是原作者针对原作者代码编写的断言,并且一直保持通过。
其余测试覆盖此代码树。其中一些是测试夹具,而非单元测试——它们会读取源代码,并在某类代码偏移时失败:write-path.test.js 禁止在获准入口之外写入 MongoDB,要求每个入口在计算哈希前都进行清理,并要求代码树中的任何内容都不得从层、文档或转录集合中删除数据;dispatch.test.js 要求声明的工具列表与两种传输方式的分发开关包含相同的工具,并要求列表响应可以携带面向调用方的警告;annotations.test.js 要求每个工具都声明其审批提示;atlas-role.test.js 要求数据库角色没有对记录的删除权限。其余测试验证行为——包括 aleph.test.js(回读、摘要、签名、链遍历)、session-graph.test.js(monorail 的关卡和实验室结果)、receipt.test.js、provenance-stamp.test.js、session.test.js、gaps.test.js 和 redact.test.js。失败用例比成功用例更重要——验证器若放过了本应发现的问题,就会把未被发现的问题转化为令人确信的保证。layers.test.js 根据产生休眠一年的层的存储结构进行断言,因此测试针对的是它要防范的实际故障,而非假设的故障。
上游的 index.test.js、decay.test.js 和 integration.test.js 未予保留:它们针对 SQLite 后端和时间衰减引擎,而此处两者都不存在。运行这些测试只会产生与此代码无关的失败。
基准测试
EMET 的实现语言经过了评估,而非凭空假定。我们用 JavaScript、Python、Go 和 Rust 实现了相同的规范,以相同方式部署,并在 Atlas 实例上运行——在读取任何计时结果前,先通过摘要等价性检查,证明四种实现完成了相同的工作。JavaScript 获胜:无论是进程内串行开销、并发吞吐量,还是标准来源方面,它都是唯一遵循国际语言标准(ECMA-262 和 ISO/IEC 16262)的候选方案。
这次评估最有价值的成果并非语言选择,而是找到了值得优化的函数:写入路径清理器在不含秘密的文档上也几乎耗费了全部处理成本;修复这一问题所节省的成本,约为最佳可用语言变更所能节省成本的 30 倍。
方法、完整结果,以及对未测量内容的明确说明:
docs/BENCHMARKS.md。scripts/redact-bench.mjs 仅测量清理器——修改 src/redact.js 前后都运行一次。
已知不足
如实列出,因为本项目的宗旨是让它的行为可预测:
- 在 2026-09-05 之前写入的行没有内容摘要,在 2026-09-07 之前写入的行没有元数据摘要;读取时会将它们报告为
'unattested',且不会回填——今天计算出的摘要只能证明今天这份副本的情况,不能证明写入时的情况。emet_gaps会统计这一边界。 - 在来源追溯功能出现之前写入的条目会保留空指针,并按时间戳窗口解析;
emet_gaps会将它们计为来源不明。由于来源追溯一节所述的原因,不会回填这些条目。 - 有两个内容路由器沿用自上游(子字符串关键词路由器和内容分析器)。在包含 1,175 行的语料库上进行测量时,它们与存储层的吻合率都不到一半(分别为 41% 和 36%),因此都不会放置条目:未标记的写入会进入
working,并被标记,同时在routing中记录两者的判断(keyword_layer、analyzer_layer)。可以将它们重新调整到足以胜任路由工作的程度,并以这些数字为基准;目前没有此项计划。 emet_gaps中的consolidation_candidates表示标签重叠,而非语义相似度。- 单轨(会话图)会拒绝每一个被跳过的步骤(仅从 2026-10-03 起强制执行;没有报告模式或关闭开关)。其轮次覆盖范围仅与主机报告的交换次数一样可靠,而且它永远不会看到回复文本。请参阅 Guardrails & Monorails。
- 定时每周摄取任务在未开启会话的情况下写入,因此会话顺序检查会拒绝这些写入(门控
no_session),直到该任务开启会话,传入启动时的load_id并传递session_id。其旧来源标签cloud-scheduled-intake已于 2026-10-02 停用。cloud-scheduled-前缀不是已注册标签,因此此类写入会被拒绝。每周摄取任务以intake-bot身份运行。emet_gaps会继续将此前使用旧标签写入的行计为归属不明,而不会改写历史记录。 save_transcript的描述没有说明它会原样存储内容;但实现确实如此。- 尚不支持导出/导入/恢复并比较。
- 文档存储没有前缀,因此一个数据库只保存一个实例的身份信息。若要使用不同的记忆,请运行不同的数据库,而不是使用不同的前缀。
- 除 Client ID Metadata Documents 外,目前仍提供 Dynamic Client Registration。MCP 规范的弃用期限约于 2027-07 结束,计划于 2027-06-01 移除该功能。
- 除非设置时说明会有多个机器人或 AI 使用该记忆,否则委托客户端子密钥默认关闭。托管服务不会在其容器中写入 pepper。印记只能表明曾提供获批的密钥,不能说明由哪个程序持有该密钥。请参阅 docs/SUBKEYS.md。
起源
EMET 由 Dreamforge Studio LLC 创建,源于一位神经多样性用户自身的需求:拥有一个能够记住事情的 AI 助手。为此创造的词是 neurendipity,结合了 neurodivergent 和 serendipity。
它首先面向神经多样性用户设计,也适用于所有人。
许可证和访问
EMET 采用 MIT License,免费且开源(见 LICENSE)。运行它无需账户、密钥或付款。
提交历史的作者身份信息,包括一项以新增而非改写方式记录的修正:docs/AUTHORSHIP.md。
MIT。请参阅 LICENSE——任何副本都必须保留 C.I.P.S. LLC 和 Dreamforge Studio LLC 的原始声明。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。