关于 server-noonien - shared memory MCP
server-noonien 是面向 AI Agent 的本地优先 MCP 记忆服务,可直接替代官方记忆知识图谱服务。它以 CRDT 合并仅追加分片,并可点对点同步多台机器,无需中央服务器。
主要功能
- 01兼容替代记忆图谱工具
- 02以 CRDT 无冲突合并分片
- 03点对点复制守护进程
- 04文件和 S3 存储后端
- 05CLI 导入、导出、查询和压缩
适用场景
- 01在多台机器间共享 Agent 记忆
- 02离线时继续使用本地记忆
- 03替换官方记忆服务
- 04导入并检查记忆图谱数据
MCP 相关
提供与官方记忆 MCP 服务相同的九种知识图谱工具及输入输出,并附加扩展工具。MCP 客户端通过 stdio 启动服务;配套守护进程或共享文件/S3 区域可在机器间复制记忆。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
9收录的工具列表 · 更新于 2 周前
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| entities | array | 是 |
详细介绍
原文在翻译后有更新,译文可能不是最新。
![]()
server-noonien
为 AI Agent 提供共享、无服务器、无冲突的记忆——支持点对点同步。
server-noonien 可直接替代官方的
@modelcontextprotocol/server-memory
知识图谱——沿用相同的实体、观察结果和关系,并且能在多台机器间收敛,无需中央服务、数据库或解决合并冲突。
它的核心功能是点对点同步:nooniend 守护进程将每个节点的分片直接复制到其他节点,因此多台机器可以共享同一份记忆,无需共享文件夹——只需通过 VPN 或 LAN 建立 IP 连通性。
目录
为什么选择 server-noonien
问题所在
如今,Agent 的记忆存储在本地,而官方记忆服务则使用单个 JSONL 文件,每次变更都会完整读取并重写该文件。只要有多台机器,这种做法就会出问题:
- 记忆只存在于一台主机上。 换台机器,Agent 就会忘记一切。
- 通过同步文件夹共享会丢失写入。 服务只会在进程内串行处理变更:两台主机各自加载完整图谱,修改自己的副本后再写回,因此后写入的一方会悄无声息地丢弃另一方的修改;同步工具还会将文件分叉为冲突副本,而不是合并内容。
- 其他方案需要服务、数据库或云端。 主流的“共享记忆”产品要么需要自行托管,要么会将你的记忆数据传出本机。
server-noonien 如何解决这个问题
server-noonien 从根本上移除了共享文件和中央服务器,同时保留了可直接替换的工具接口:
- 每个节点都有自己的记忆。 每台机器只写入自己的仅追加分片(
<node>.jsonl);分片在设计上只有一个写入者,因此不会有两台节点写入同一个文件,noonien 自身也绝不会拆分分片——共享区域的同步器仍可能拆分,而冲突副本只需作为另一个分片合并即可。 - 分片会合并,不会相互覆盖。 图是一个 CRDT——按
(HLC, node, sequence)排序的 LWW-Element-Set——其合并操作具有幂等性、交换性、结合性和收敛性。任何两台见过相同操作的机器,无论顺序如何,都会持有完全相同的图,因此不会发生“最后写入者获胜”导致的数据丢失。 - 无需共享区域,也无需中央服务器。 分片通过
nooniend守护进程在机器之间直接传输——只需通过 VPN 或 LAN 能够访问 IP——这样多台机器就能共享同一份记忆,无需共享文件夹,也无需托管服务。更喜欢使用已有的文件夹或存储桶?file和s3也能使用:传输方式由你选择,不会受到限制。 - 本地优先,适合离线使用。 每个节点都会在磁盘上读写自己的分片,并保留完整的本地副本,因此离线时也能继续工作;网络恢复后,网状网络会重新收敛。
- 可直接替换官方服务。 工具、输入和输出都与官方服务的九个工具相同,因此无需更改 Agent,即可使用
memory服务名称接入。
对比
server-noonien 与官方记忆服务及托管记忆服务的对比:
server-noonien | 官方 server-memory | 托管记忆服务 | |
|---|---|---|---|
| 多机器支持 | 支持——每个节点各有分片,共用一张图 | 不支持——只有一个本地文件 | 支持——通过服务实现 |
| 共享同步文件夹 | 安全——仅追加,不会产生冲突 | 不安全——对整个文件执行读取-修改-写入 | 不适用 |
| 是否需要服务器/数据库 | 否 | 否 | 是 |
| 是否需要云账户 | 否 | 否 | 是 |
| 是否支持离线工作 | 支持(file 后端) | 支持 | 不支持 |
| 存储 | 文件夹或兼容 S3 的存储桶,由你选择 | 一个 JSONL 文件 | 服务提供的云存储 |
| 能否直接替换官方工具 | 能——工具数量相同,均为九个 | — | 不能 |
| 迁移 | noonien import memory.jsonl | — | 导出/导入操作 |
| 模型 | 显式知识图谱 | 显式知识图谱 | 通常为向量/语义模型 |
| 许可证 | MPL-2.0 | MIT | 专有 |
托管服务是一个类别,而非某个具体产品:它涵盖为你托管记忆的服务。
server-noonien 则恰恰相反,采用本地优先的方式——存储由你掌控,而且无需账户。
file/s3 后端需要共享区域——每个节点都能访问的文件夹或对象存储。nooniend 则免去了这一要求:它会在节点之间直接复制分片。
三个命令
一个 npm 软件包 server-noonien,包含三个命令(Node.js ≥ 22)。安装一次后,三个命令都会出现在你的 PATH 中:
npm install -g server-noonien
| 命令 | 作用 | 运行方式 |
|---|---|---|
server-noonien | MCP 记忆服务 | 短时运行,通过标准输入/输出由 MCP 客户端启动 |
noonien | 维护命令行工具 | 一次性运行,手动执行 |
nooniend | 点对点复制守护进程 | 长期运行,每台机器运行一个 |
只有 server-noonien 与软件包同名,因此只有这个命令能通过软件包名称由 npx 运行。其他两个命令需使用 --package 运行,或使用上文的全局安装方式:
npx -y server-noonien # the MCP server (no argument serves over stdio)
npx -y -p server-noonien noonien help # the maintenance CLI
npx -y -p server-noonien nooniend # the replication daemon
从克隆的仓库运行——可用于开发,或运行尚未发布到 npm 的代码:
git clone https://github.com/sequico/server-noonien.git
cd server-noonien && npm install && npm run build
node dist/index.js # server-noonien — the MCP server
node dist/noonien.js # noonien — the maintenance CLI
node dist/gossip.js # nooniend — the replication daemon
server-noonien — MCP 服务
每个 MCP 客户端都会使用的入口。不带参数(或使用 serve)时,它会通过 stdio 使用 MCP,因此你无需自行运行它——客户端会启动它,并让它在会话期间保持运行:
npx -y server-noonien # the stdio command the client runs
claude mcp add memory -- npx -y server-noonien # register it with Claude Code as the `memory` server
将客户端指向它即可;它开箱即用,可在一台机器上运行,在多台机器间共享是下一步。在 OpenCode (V2) 中:
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"servers": {
"memory": {
"type": "local",
"command": ["npx", "-y", "server-noonien"],
"environment": { "NOONIEN_DIR": "~/.noonien" }
}
}
}
}
其他客户端会用各自的封装来调用相同的命令和环境变量;从克隆的仓库运行时,命令是 ["node", "/path/to/server-noonien/dist/index.js"]。由于工具接口完全相同,你可以将 memory 服务条目替换为 server-noonien,无需更改其他内容。
noonien — 维护 CLI
| 命令 | 作用 |
|---|---|
noonien serve | 通过 stdio 运行 MCP 记忆服务(与 server-noonien 相同) |
noonien import <file> | 导入官方的 server-memory JSONL 文件(写入独立的 <node>-import 分片,因此可在线运行) |
noonien export | 将合并后的知识图谱以 JSON 格式输出 |
noonien merge | 合并所有分片并报告合并后的状态 |
noonien compact | 立即维护此节点的分片:清除被覆盖的操作,并且仅在安全时物理删除其墓碑——规则与服务采用的规则相同(NOONIEN_GC;若守护进程拥有该目录,它会保留完整视图,并将分片留给守护进程处理)——若保留墓碑,则报告原因 |
noonien query <text> | 搜索实体并输出匹配的子图 |
noonien help | 列出命令;noonien --version 输出版本 |
nooniend — 守护进程
一种长时间运行的服务,每台机器运行一个,负责以点对点方式复制分片目录。不接受任何参数,完全通过环境变量配置——共享的 NOONIEN_DIR 和 NOONIEN_NODE_ID(共享变量),以及守护进程的 NOONIEND_*(守护进程变量)。设置方法请参阅点对点。
在多台机器间共享
noonien 会合并分片,但不会自行传输分片。分片如何在机器间传输由你决定:点对点(推荐的配套守护进程)或共享区域(现有文件夹或存储桶)。MCP 服务始终读写自己的本地分片;两者的区别仅在于传输方式。
点对点 — nooniend(推荐)
无需共享存储,即可在多台机器间共享同一份记忆的推荐方式。 nooniend 是一个长时间运行的守护进程,以点对点方式复制本地分片目录。它只需要 IP 可达,因此可通过任何 VPN(WireGuard、Tailscale、OpenVPN、Netbird、ZeroTier,等等)或普通局域网运行。
为每台机器分配一个唯一的节点 ID,并在每台机器上运行一个 nooniend。每个守护进程都会在本地目录中保留其他机器分片的完整副本,并与 MCP 服务使用同一目录——无需共享文件夹:
# on a tailnet
NOONIEN_DIR=~/.noonien \
NOONIEND_TAILSCALE=true \
nooniend
# or, with no self-listing underlay, seed one reachable peer
NOONIEN_DIR=~/.noonien \
NOONIEND_PEERS=ai.example:27878 \
nooniend
各节点会自动发现彼此——你无需列出机器。配置一种发现来源:NOONIEND_TAILSCALE(来自 tailscale status)、NOONIEND_DNS_SRV(_nooniend._tcp SRV 记录)或 NOONIEND_PEERS(静态种子节点;配置一个即可,之后成员关系会自行传播)。发现来源返回的是候选节点,而非成员节点:候选节点只有在响应 /info 后才会加入,因此网络上没有运行 nooniend 的机器绝不会被接纳。NOONIEND_ADVERTISE 为可选项,默认使用监听主机。
运行方式:
- 绝不为其他节点写入操作。 每个节点的 MCP 服务只写入自己的
<node>.jsonl;守护进程保留对等节点分片的副本,并且只推送自己负责写入的分片,因此始终遵守单写者规则。如果本地分片落后于对等节点的副本,它也会从副本恢复本地分片。 - 变更时推送 + 定期反熵。 本地发生变更时(
NOONIEND_PUSH)以及每隔NOONIEND_INTERVAL秒,守护进程会比较各分片的最高seq,并将增量从较新的一方同步到较落后的一方。操作具有幂等性,因此交付可以采用尽力而为的方式——无需共识,也不保证顺序,只保证最终完整。 - 支持扩展到全互联网状拓扑之外。 默认情况下,它会联系每一个对等节点;
NOONIEND_FANOUT将其限制为每轮随机抽取的子集——这不包括墓碑收集,因为本轮未抽中的可达对等节点会暂停收集,所以守护进程会拒绝这种组合;当分片数达到或超过NOONIEND_DIGEST_MIN_SHARDS时,它会通过固定大小的摘要而非完整分片列表进行协调(使用NOONIEND_CHANNELS可将成员关系迁移到独立通道)。小型网状拓扑的行为与全互联完全一致;不支持摘要的对等节点会回退到普通列表,因此混合网状拓扑不会出现故障。 - 中继。 节点会提供它持有的所有副本,因此即使某个节点离线,只要有任一对等节点持有其分片,该分片仍会持续收敛。
- 发现与成员关系。 引导适配器(Tailscale、DNS SRV、静态种子)会返回候选节点;随后,对等节点集合会自行通过 Gossip 传播。静态
node@host:port种子表示明确意图,会直接加入成员集合;裸地址(Tailscale 或 DNS-SRV 候选地址)只有在响应/info后才会加入,其 id 来自该响应——绝不来自发现源,因此不运行nooniend的设备绝不会成为成员,也不会阻碍收集。节点启动后,会在下一次探测时被接纳;发现源每隔NOONIEND_DISCOVER_INTERVAL秒重新读取一次,因此无需重启即可发现新加入的节点。存活状态根据交换结果判定——失败达到NOONIEND_SUSPECT_AFTER次后标记为可疑,达到NOONIEND_DEAD_AFTER后标记为死亡,在NOONIEND_DEAD_RETRY后重试,并在NOONIEND_MEMBERSHIP_TTL后遗忘。能够联系到本节点的对等节点会被标记为存活——在 mTLS 下,其证书会标识该节点;否则,使用其在请求中声明的 id——因此恢复的对等节点会立即重新参与,而无需等待NOONIEND_DEAD_RETRY到期。本节点尚不认识的对等节点会根据其在请求中声明的地址被接纳(与通过 Gossip 传播的条目一样,该地址不受信任,并会在下一次同步时确认),因此静态种子节点加入后,会被其联系到的种子发现,其条目随后也会传播到其他节点。节点自身的条目具有权威性;如果某个地址提供的服务属于其他节点,则会被丢弃。 - 线路协议与安全。 一个绑定到
NOONIEND_LISTEN的轻量级 HTTP/JSON 服务(完整线路协议见PROTOCOL.md);分片只接受其所属节点的操作。使用每个对等节点各自证书的 mTLS:证书的通用名称是节点 id,因此对等节点只能推送自己负责写入的分片,而读取对任何经过身份验证的对等节点开放——这正是节点能够充当中继的原因。NOONIEND_REVOKED会在所有路由上拒绝指定节点;NOONIEND_DEPARTED还会丢弃已离开节点的副本,并允许收集流程继续。 - 可观测性。 Prometheus 指标——存活状态、对等节点健康状况、稳定水位线,以及用于收集的每个对等节点离线时长(
noonien_gossip_absent_seconds)和冻结的已删除元素数量(noonien_gossip_frozen_elements)——另有每个分片的稳定水位线(所有可联系对等节点达到的最高水位线中的最小值,这是一个保守的因果稳定性界限),以及供探测和运维人员使用的状态与拓扑图路由——参见守护进程 HTTP API。
在 systemd 下,用户单元:
[Unit]
Description=nooniend — peer-to-peer shard replication
After=network-online.target
[Service]
EnvironmentFile=%h/.config/noonien/nooniend.env
ExecStart=%h/.local/bin/nooniend
Restart=on-failure
[Install]
WantedBy=default.target
守护进程 HTTP API
nooniend 在 NOONIEND_LISTEN(默认值为 0.0.0.0:27878)上提供一个简洁的 HTTP/JSON API。完整的线路契约——路由和载荷、操作格式、协议修订版本和能力、大小限制以及 mTLS 模型——在 PROTOCOL.md 中规定。
curl -s http://127.0.0.1:27878/health
curl -s http://127.0.0.1:27878/status
curl -s http://127.0.0.1:27878/peers
curl -s http://127.0.0.1:27878/graph | jq '{entities: (.entities|length), relations: (.relations|length)}'
共享区域——file 和 s3
file 和 s3 后端通过一个已存在的共享区域汇合,并在 noonien 之外进行复制——可以是每个节点都能访问的文件夹或存储桶。将每个节点指向同一个区域,各节点的分片就能到达其他节点:
NOONIEN_BACKEND=file NOONIEN_DIR=/srv/noonien-shared NOONIEN_NODE_ID=ai server-noonien
# or
NOONIEN_BACKEND=s3 NOONIEN_S3_BUCKET=my-bucket NOONIEN_NODE_ID=ai server-noonien
file——分片是NOONIEN_DIR中的*.jsonl文件;将其指向由 Syncthing、Dropbox、git 工作树或共享挂载复制的文件夹。s3——每个分片对应一个对象,发生争用时会通过条件写入重试;使用@aws-sdk/client-s3(可选依赖,仅在选用时加载)。条件写入要求存储服务在PutObject上遵循If-Match/If-None-Match(AWS S3、Cloudflare R2、MinIO 均支持);不遵循这些条件的存储服务不受支持。如果分片对象具有分段上传 ETag,则会拒绝该对象,而不会使用永远无法匹配的令牌进行保护。对象存储不支持追加,因此每次变更都会重写整个分片对象(读取-修改-写入):结果正确,但每次写入的复杂度为 O(分片大小)。
memory 后端仅在进程内保存分片——适用于测试和临时会话。变量说明请参阅配置。
共享区域无法提供安全删除。 file/s3 共享一个图,但它们无法判断某个对等节点是否持有尚未送达的写入。因此,在没有守护进程的情况下,维护操作只会修剪;只有当操作员声明目录仅由此节点写入(NOONIEN_GC)时,才会执行物理删除——参见删除与回收。收敛也交由同步器处理:noonien 既不验证,也不重传,因此延迟和冲突副本由同步器负责,而冲突副本只是另一个待合并的分片。守护进程复制的是一个目录:如果设置了 NOONIEN_BACKEND=s3(或 memory),nooniend 会拒绝运行,因为这两种情况下它要复制的是服务器根本不会使用的本地文件夹。
配置
每项设置都从环境变量读取。NOONIEN_DIR 和 NOONIEN_NODE_ID 是共享变量;MCP 服务、CLI 和守护进程除此之外各自使用不同的变量——服务使用 NOONIEN_*,守护进程使用 NOONIEND_*。
共享变量
所有命令都会使用。
| 变量 | 默认值 | 含义 |
|---|---|---|
NOONIEN_NODE_ID | 主机名 | 此节点的分片名称——MCP 服务写入以及守护进程复制时使用的标识。必须每台机器唯一,且同一时间只能由一个服务写入。 |
NOONIEN_DIR | ~/.noonien | 保存此机器分片的目录:file 后端的存储目录,也是守护进程的副本存储目录。开头的 ~ 会展开。 |
这两个变量都仅适用于各自的机器,并非共享区域:MCP 服务和守护进程在同一主机上共用同一个目录,而每台机器都保存每个分片的独立副本。使用 nooniend 进行点对点同步不需要共享文件夹或存储桶;共享区域只是 file/s3 的另一种传输方式。
节点 ID 对应一个单写者分片:同一时间只能有一个服务进程向其中追加内容。常规配置是每台机器运行一个内存服务;如果多个服务并发使用同一个存储,请为每个服务指定独立的 NOONIEN_NODE_ID。工具不会强制执行此规则:两个写入者若共用一个节点 ID,并在同一毫秒内写入,就会生成 ID 相同的操作,读取时会直接报错——但使用不同系统时钟的写入者通常会生成不同的 ID,因此分片可能悄然交错,合并时只会选取其中一个。节点 ID 必须唯一,工具无法检查这一点。
服务端变量
由 MCP 服务(以及读取和写入相同分片的 noonien CLI)使用。
| 变量 | 默认值 | 含义 |
|---|---|---|
NOONIEN_BACKEND | file | file、memory 或 s3。 |
NOONIEN_S3_BUCKET | — | 存储桶(s3 后端必需)。 |
NOONIEN_S3_PREFIX | "" | 所有分片对象所在的键前缀。 |
NOONIEN_S3_REGION | us-east-1 | 传递给 S3 客户端的区域。 |
NOONIEN_S3_ENDPOINT | — | 用于 MinIO / R2 / Contabo 等的自定义端点。 |
NOONIEN_S3_FORCE_PATH_STYLE | 设置端点时启用 | 路径样式请求。 |
NOONIEN_COMPACT_AFTER | 1000 | 追加此数量的操作后在线压缩本地分片(0 表示禁用)。 |
NOONIEN_GC | 关闭 | 不运行守护进程时删除墓碑:仅当此节点是目录的唯一写入者时执行——但没有任何目录内容能证明这一点。如果目录由守护进程管理(它会在启动时通过知识文件声明自身),此选项不起作用:由守护进程执行清理。 |
NOONIEN_GC(服务端,默认关闭)和守护进程的 NOONIEND_GC(默认开启)是不同的
开关;使用 memory 后端时没有任何共享内容,因此无论 NOONIEN_GC 如何设置,服务端都会执行清理。
S3 凭证来自标准 AWS 凭证链(AWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEY、
AWS_REGION、配置文件、实例角色等)。
本地分片会在线压缩:每追加 NOONIEN_COMPACT_AFTER 个操作后(默认 1000,0 表示禁用),
服务端会在与写入相同的变更队列中重写自己的分片。重写会针对读取到的内容执行比较并交换;并发写入——无论是追加操作,还是 nooniend 守护进程恢复分片——都会使其重新读取并重试,因此绝不会丢失内容;noonien compact 可手动执行相同的压缩。noonien import 会创建自己的 <node>-import 分片,因此节点的服务端运行期间也可以执行导入,且重复导入具有幂等性。该分片只会追加内容,因此既不需要对等节点知识,也不需要 NOONIEN_GC 保证:noonien import 会在结束时清理该分片,noonien compact 也会清理——删除被后续操作覆盖的操作,保留所有墓碑,并且仅在确有内容被删除时重写;因此,重复维护不会产生任何开销,也不会要求对等节点重新拉取分片。在 file 后端上,append 和 compact 会获取每个分片的锁,因此即使跨进程也会串行执行,而不会发生竞争——守护进程也会使用相同的锁。
在同一节点内,时间戳是单调递增的:如果墙上时钟向后跳变(NTP、恢复快照、手动执行 date),新操作会沿用前一个时间戳,而不会使用更早的时间戳,因此同一节点的较早操作绝不会胜过较晚操作。
守护进程变量
仅供守护进程使用。它至少需要 一个发现来源——NOONIEND_TAILSCALE、
NOONIEND_DNS_SRV 或 NOONIEND_PEERS——才能访问网状网络;其余均为可选项。请在 VPN 或防火墙后运行,并启用 mTLS——参见安全。
| 变量 | 默认值 | 含义 |
|---|---|---|
NOONIEND_LISTEN | 0.0.0.0:27878 | Gossip 服务绑定的 host:port。 |
NOONIEND_ADVERTISE | 监听主机(通配符地址时为主机名) | 可选。 此节点向对等节点通告的 host:port;仅当其他节点无法解析默认值时才设置。 |
NOONIEND_PEERS | — | 发现:静态种子——以逗号分隔的 node@host:port 或 host:port。设置一个即可;之后成员关系会自行传播。 |
NOONIEND_DNS_SRV | — | **发现:**其 _nooniend._tcp SRV 记录用作种子的域名。 |
NOONIEND_TAILSCALE | off | **发现:**从 tailscale status 派生候选地址(对等节点响应 /info 后即可加入)。 |
NOONIEND_DISCOVER_INTERVAL | 300 | 发现刷新间隔,单位为秒;0 表示仅在启动时读取一次来源。刷新只会添加候选节点。 |
NOONIEND_INTERVAL | 30 | 反熵周期,单位为秒。 |
NOONIEND_PUSH | on | 本地分片发生变化后立即协调。 |
NOONIEND_FANOUT | 0 | 每轮联系的对等节点数;0 表示联系所有可达对等节点。设置上限时不能启用 NOONIEND_GC:如果某个可达对等节点未在本轮抽样中,收集就会暂停,因此守护进程不允许同时使用这两项。 |
NOONIEND_DIGEST_MIN_SHARDS | 32 | 分片数达到或超过此值时,摘要将替代完整列表(0 = 只要支持就使用)。 |
NOONIEND_CHANNELS | off | 在单独的通道上交换成员关系,而不是在每次交换时都附带。 |
NOONIEND_RELAY | — | 始终抽样的节点 ID,以逗号分隔(中继节点/超级对等节点)。 |
NOONIEND_GC | on | 收集已编写的分片:清除被覆盖的操作(保留网状网络外的对等节点仍可能已折叠的操作,作为门控证据);当网状网络外所有已折叠其元素的对等节点都无法再对其提出异议时,删除墓碑(物理删除)。 |
NOONIEND_FORGET_AFTER | 15552000(180 天) | 对等节点静默超过此秒数后将被退役:其保护信息会持久化(合并后的阈值,或在其持有本节点缺少的操作时采用全局保护),并删除其实时记录,因此元数据集合所依赖的空间会保持有界。0 表示永不退役。 |
NOONIEND_SUSPECT_AFTER | 3 | 交换失败多少次后将对等节点标记为可疑。 |
NOONIEND_DEAD_AFTER | 6 | 交换失败多少次后将对等节点标记为失效。 |
NOONIEND_DEAD_RETRY | 300 | 失效的对等节点在再次探测前等待的秒数。 |
NOONIEND_MEMBERSHIP_TTL | 604800 | 失效的对等节点保留至被遗忘的秒数。 |
NOONIEND_REVOKED | — | 守护进程永不与之同步并拒绝接受的节点 ID,以逗号分隔。 |
NOONIEND_DEPARTED | — | 明确离开的节点 ID,以逗号分隔:与 NOONIEND_REVOKED 类似,但还会丢弃其副本,并且它们不再阻碍收集。 |
NOONIEND_TLS_CERT / NOONIEND_TLS_KEY | — | 使用此证书和密钥提供 TLS 服务。 |
NOONIEND_TLS_CA | — | 为对等节点证书签名的 CA,用于双向验证证书。 |
NOONIEND_TLS_REQUIRE_CLIENT | on | 要求提供客户端证书(mTLS)。 |
工作原理
每次变更都会记录为仅追加日志中的一个操作,并按节点拆分为多个分片:
create_entities/add_observations/create_relations用于添加元素;delete_entities/delete_observations/delete_relations用于写入墓碑(删除实体时,也会为其观察和关联关系写入墓碑)。
读取时会折叠所有分片:每个元素(实体、观察出现项、关系)都是由 (HLC, node, sequence) 决定的 LWW 寄存器。删除观察是内容级别的:它会移除所有早于它的出现项。只有实体存在时才会显示观察;只有关系两端都存在时才会显示关系。折叠结果只取决于操作的集合,与读取分片或行的顺序无关。操作按混合逻辑时钟排序:即使时钟存在偏差,顺序仍然有效;节点观察到一条记录后,就能写入排序在其后的更正。若观察到的时间戳超出有界时钟偏差所允许的未来范围,则会被钳制,因此错误的时钟不会把节点的时间戳拖到未来。读取是增量式的:每个分片只解码一次,并根据低开销指纹进行缓存;折叠后的图也会被记忆化,因此在内容没有变化时,读取每个分片只需进行一次指纹轮询。
以下性质均由属性测试覆盖:
- 幂等——重复应用相同操作不会带来任何变化。
- 可交换——跨分片的操作顺序无关紧要。
- 可结合——以任意分组折叠分片都会得到相同的图。
- 收敛——拥有相同操作的两个节点会生成完全相同的图。
排序是确定性的:实体按名称排序,关系按 (from, to, relationType) 排序,观察按操作顺序(HLC、node、sequence)排序。
压缩
每次变更都会让日志增长。压缩会重写此节点的分片,并仅保留每个元素中未被后续操作覆盖的操作——覆盖索引根据合并后的日志构建,因此被另一个节点的后续操作取代的操作也会被丢弃,而每个元素的获胜操作始终会保留,包括墓碑。压缩会在线运行:服务在追加 NOONIEN_COMPACT_AFTER 次后压缩自己的分片,noonien compact 也可以手动执行相同操作。何时同时收集墓碑——即进行物理删除——详见删除与回收。重写采用比较并交换方式(在 file 后端上将临时文件重命名并覆盖分片,在 s3 上则执行条件写入),因此即使中途崩溃也绝不会截断分片;如果发生并发写入(追加操作或 gossip 恢复),整个压缩过程会重新读取并重试,而不是丢失数据。
每次压缩都会写入一个 shard.compact 元数据操作(绝不会成为图元素):每次压缩都会递增其 generation,其 seq 则是分片持久化的高水位标记,因此序号不会倒退,压缩也绝不会被误认为是分片丢失。对等节点从 /shards 读取 generation,并用压缩后的分片替换过时副本,因此每个副本最终都会收敛到压缩后的内容。只有本地分片会被重写;所有其他分片均保持不变,因此压缩不会产生冲突,也不会影响其他节点。
删除与回收
删除是一个操作,而不是移除:delete_entities、delete_observations 和 delete_relations 会追加墓碑;当墓碑是某元素的最新操作时,折叠结果会隐藏该元素。正是墓碑让删除能够胜过并发写入,因此在该写入仍然存在时丢弃墓碑会使元素复活。因此,只有当没有任何节点仍可能持有墓碑时,删除才会成为物理删除。
nooniend 会在网格安全时收集它所创作的分片,按元素进行。它会先清理被后续操作覆盖、且没有任何保留的网格外节点仍可能需要的操作,然后在没有网格外节点能对某个元素提出异议时,删除仍然存在的墓碑。每个节点都必须满足两个条件:该节点的知识前沿必须是本节点现有内容的子集——如果某个节点持有本节点缺少的操作,它就可能持有该元素上的转发操作,因此会被视为阻碍因素;并且其知识前沿不得覆盖该元素的任何操作,这样只有折叠过该元素的节点才能提出异议。未曾折叠过该元素的节点仍能创建一个巧合元素(一个独立名称、对其所知实体的观察),但折叠会将其判定为并发创世,而非复活。因此,在某个节点已离线期间出现的元素可以立即收集,无需等待该节点返回;该节点确实知道的元素则会被冻结,直到它返回或被明确移除。另一个分片上的存续操作也会保留墓碑,因此折叠始终得以保留。当某个节点可能对某元素提出异议时,它已知的操作会作为证据保留下来,供下一轮再次读取;而且由创作该操作的节点来读取,而不只是删除者:如果网格外节点仍可能对某元素提出异议,本节点绝不会清理该元素上被覆盖的操作,因此即使墓碑位于不同分片上(一个节点删除另一个节点创建的元素),证据也会保留。
一句话概括:收集不需要对历史设置屏障——无论离线多久,从未折叠过某个元素的节点都无法在因果上对它提出异议,因此既不需要全局确认轮次,也不需要为数据设置计时器。它确实需要对活动节点集进行稳定轮次:每个仍可访问的保留节点都必须在该轮次中完成交换(否则该轮次会暂停,因为这样的节点可能持有本节点尚未拉取的写入),而且任何保留节点都不得持有本节点缺少的内容。这就是为什么 NOONIEND_FANOUT 和 NOONIEND_GC 不能同时启用。活性并非自动保证:既无法访问又无法验证的节点——从未联系上,或持有本节点无法读取的操作——会阻止收集,直到操作员将其移除(NOONIEND_REVOKED / NOONIEND_DEPARTED);保留期限限制的是元数据,不会丢弃任何数据,但也不会替代这一决定。
每个节点的知识前沿——已观察到它持有的每个作者分片的最大序列号——都是持久化的,与分片一同保存在 .nooniend-peers.json 中,因此离线一年的机器只会阻止收集它实际知道的元素。如果某个节点仍可被该轮次访问,但未进行交换,收集就会暂停,因为它可能持有本节点尚未拉取的写入。忘记某个节点(成员资格 TTL)不会解除阻碍——该节点可能仍在线下网格中,并持有有效的未发送写入——但 NOONIEND_REVOKED 和 NOONIEND_DEPARTED 会解除阻碍,这是操作员认定其写入可以舍弃;已离开的节点副本会被直接丢弃。被放弃节点的创作权也会从检查条件中移除,因此它不再通过仍持有其副本的节点阻止收集;已撤销节点现有的操作则会继续冻结相关元素,直到该节点被移除并丢弃这些操作。使用 NOONIEND_GC=false 可关闭守护进程的收集功能。
每个对等节点的记录都有上限。静默 NOONIEND_FORGET_AFTER 秒(默认 180 天)后,对等节点会被退役,退役在语义上不可见:它只会改变保护机制的存放位置,不会改变受保护的元素。已计入的对等节点——其前沿是本节点已有内容的子集——会合并到持久化的退役前沿中,每位作者保留一个逐元素最大值。该最大值恰好等于这些对等节点逐元素决策的并集,因此会继续为这些对等节点可能折叠的每个元素设置门控,包括退役后发生的删除,同时只需维护一张由本节点所持分片决定大小的映射,而不是每个对等节点一张映射。(过期时计算出的逐元素列表并不可靠:之后创建的墓碑不在其中。)未计入的对等节点——声称拥有本节点无法读取的操作——则会留下持久化的全面封锁:它在活动期间被门控时所依据的“它可能持有任何内容”,只需记录一次,而不必保留无法验证的映射。退役不会放弃任何内容,因此该时限可以默认启用。NOONIEND_REVOKED 和 NOONIEND_DEPARTED 仍是仅有的放弃方式——而且由于最大值无法撤销合并,本节点必须在时限到期之前将其标记为已离开,才能解除其贡献。声称包含本节点无法提供服务的作者的前沿永远不会被记录:对等节点无法凭无法证实的声明冻结网状网络。此前未能联系到的已声明节点仍会像以前一样保守地阻止操作。
未运行守护进程时,物理删除需要明确承诺。拥有该目录的守护进程(存在 .nooniend-peers.json)会持有对等节点信息并负责收集,而且它是唯一会维护该分片的写入者:服务器和 noonien compact 会保留其中的完整视图——被遮蔽的操作正是守护进程门控所读取的证据,因此不得将其清除——并将分片留给守护进程处理。没有守护进程,就没有可用于门控的信息,而且目录无法判断某个对等节点是否只是暂时缺席:离线的机器,或已配置但尚未同步的机器,都不会留下任何痕迹;收集操作会因此丢失它尚未同步的写入——这正是官方服务器执行的直接删除。因此,服务器的在线维护和 noonien compact 都只会清除已修剪的内容,直到操作员声明该目录仅由本节点写入(NOONIEN_GC=true);这一承诺才能确保操作安全。如果另一节点的分片与此声明矛盾,系统会拒绝该声明并报告。
指标
守护进程在 /metrics 提供所有指标(MCP 服务使用 stdio,没有端点)。采集和保留状态如下:
| 指标 | 含义 |
|---|---|
noonien_gossip_frozen_elements | 已删除但仍予以保留的元素,因为网格之外的节点可能仍会对其提出异议 |
noonien_gossip_absent_nodes | 上一轮未与之交换数据的保留节点 |
noonien_gossip_absent_seconds{node} | 各节点离线的时长 |
noonien_gossip_retained_peers | 其知识仍会限制采集的节点 |
noonien_gossip_retired_total | 已归入退役前沿的节点 |
noonien_gossip_retired_unaccounted_total | 已通过 blanket 退役的节点 |
noonien_gossip_retired_authors | 阈值仍保留在退役前沿中的作者 |
noonien_gossip_blankets | 当前生效的持久 blanket |
同一端点还提供传输和存活状态指标系列——noonien_gossip_rounds_total、
noonien_gossip_round_seconds、noonien_gossip_round_bytes、noonien_gossip_links、
noonien_gossip_shards、noonien_gossip_stable_shards、noonien_gossip_membership_size、
noonien_gossip_peers_ok_total、noonien_gossip_peers_failed_total、
noonien_gossip_ops_pulled_total、noonien_gossip_ops_pushed_total、
noonien_gossip_digest_exchanges_total、noonien_gossip_membership_channel_failures_total、
noonien_gossip_bytes_sent_total、noonien_gossip_bytes_received_total、
noonien_gossip_fanout、noonien_gossip_up——此外,当守护进程运行本地日志自身的维护任务时,还会提供其 noonien_mcp_* 计数器(noonien_mcp_folds_total、noonien_mcp_fold_seconds、
noonien_mcp_shards_merged、noonien_mcp_shard_stat_total、noonien_mcp_ops_decoded_total、
noonien_mcp_compacted_total、noonien_mcp_compaction_failed_total)。
工具
MCP 接口采用官方的九个工具,可直接替换:名称、输入、输出和消息均与
@modelcontextprotocol/server-memory 一致。
| 工具 | 功能 |
|---|---|
create_entities | 在知识图谱中创建多个新实体 |
create_relations | 在实体之间创建多个新关系(主动语态) |
add_observations | 向现有实体添加新观察 |
delete_entities | 删除实体及其关联关系 |
delete_observations | 删除实体中的特定观察 |
delete_relations | 删除多个关系 |
read_graph | 读取整个知识图谱 |
search_nodes | 按查询搜索节点,查询会匹配实体名称、类型和观察内容 |
open_nodes | 按名称打开特定节点 |
可直接替换的兼容性
同样是这九个工具(工具),因此无需更改 Agent,即可将其作为名为 memory 的服务使用。
整个图谱也通过 memory://knowledge-graph 资源公开。该资源为只读;客户端可以订阅它,并在任何变更导致图谱改变时接收 notifications/resources/updated 通知——在 2025 年协议中通过 resources/subscribe 实现,或在 2026-07-28 版本中通过 subscriptions/listen 流实现。服务同时支持两者:它会响应面向 2025 年客户端的 initialize 握手,以及面向 2026-07-28 客户端的 server/discover。
实体以名称为键。观察的行为与官方服务一致:如果内容已存在,add_observations 不会产生效果,但会保留单个请求内的重复内容;create_entities 会按给定内容保留重复项;删除观察时会移除该内容的所有出现项。关系采用集合语义:重复添加相同关系不会产生效果。delete_entities 还会删除实体的观察及与其关联的关系。墓碑涵盖删除节点能够看到的内容:在另一节点上并发添加的观察会作为元素保留下来,因此之后若重新创建该实体,该观察也会随之重新出现。
安全性
- 每个分片只能有一个写入者。 确保
NOONIEN_NODE_ID唯一,并且每个节点 ID 只运行一个服务。节点 ID 会被清理为有界且适合用作路径的分片文件名干,并且每个来自对等节点的节点 ID(/shards/{node}/ops路径、传播的摘要、通告的标头)都会在用于命名文件、加入对等节点集合或标记指标之前进行验证。 - 复制。
nooniend是网络服务。请在 VPN 或防火墙后运行,并启用 mTLS(配置证书后默认启用),确保只有经过身份验证的对等节点可以复制。客户端始终验证对等节点证书——使用已配置的 CA,或未配置 CA 时使用系统信任存储;绝不会使用未经验证的连接。未启用 TLS 时,守护进程信任底层网络;对等节点通告的地址仍会按普通host:port进行验证,并且只有在其主机与请求实际来源主机一致时才会采用(因此,通告的标头无法将拨号器指向其他主机)。拒绝连接(NOONIEND_REVOKED/NOONIEND_DEPARTED)需要 mTLS 提供的客户端证书——没有证书,对等节点便没有可供拒绝的身份。设置NOONIEND_TLS_REQUIRE_CLIENT=false会将 TLS 变回未经身份验证的通道(任何客户端都可以推送任何分片);守护进程会在此模式下于启动时记录警告。 - 信任模型。 mTLS 验证的是成员身份,而非单个操作。节点会提供其持有的所有分片,因此中继是受信任的:成员可以将另一节点的分片转发给第三个节点,守护进程也会从对等节点的副本恢复自己丢失的分片。仅在你信任其作为对等节点的机器上运行
nooniend。折叠操作的最后写入者胜出顺序信任操作的 HLC 时间戳,因此时钟大幅超前的成员会在其涉及的每次冲突中胜出;错误的时钟会危及正确性,而非构成权限边界。收集过程也会信任成员上报的信息:来自/shards的前沿,以及其传播的对等节点集合——证书能证明它的身份,但不能证明其上报属实——并且假定网格是节点获取信息的唯一途径:离开的节点不得继续从其他地方接收操作(不要让守护进程和共享的file/s3区域指向同一目录;连接两个网格的节点必须同时属于这两个网格,否则应将其标记为已离开)。 - 报告漏洞。 请勿公开提交问题——请参阅
SECURITY.md。
开发
git clone https://github.com/sequico/server-noonien.git
cd server-noonien
npm install
npm run check # Biome: lint + format + import order
npm run typecheck # tsc --noEmit
npm run test # Vitest, including the CRDT property tests
npm run build # tsc -> dist/
npm run gate # all of the above
有关工作流和基本规则,请参阅 CONTRIBUTING.md。
为什么叫 noonien?
这个名字是对《星际迷航:下一代》的致敬。这个项目最初打算命名为 “Datalore”——这是《星际迷航:下一代》第 1 季第 13 集的标题,该集于 1988 年 1 月 18 日首播,介绍了 Data 及其双胞胎兄弟 Lore。这两位 Soong 型机器人由 Noonien Soong 博士制造。Data 是善良的一方;Lore 则是有缺陷、情绪化且心怀恶意的原型。两者均由同一位演员 Brent Spiner 饰演。
剧集标题是两个机器人的名字合成而来,而这两个名字与知识图谱的契合度几乎过于完美:
- Data — 一个 datum:单条事实。
- Lore — 一个社群共享的 知识与传统。
因此,datalore 意味着事实加上共享的传承——这正是共享 Agent 记忆的含义。但“Datalore”也是同一软件领域中一个无关商业产品的名称,所以项目没有沿用这个名称,以避免品牌冲突,而是改名为 noonien,取自这些机器人的创造者 Noonien Soong 博士。以创造者命名,仍然保留了对 Data 和 Lore 两者的致敬。
双胞胎主题映射到这个项目的核心问题:同一张图的多个副本位于不同机器上,必须保持一致。Data 这个善良的双胞胎和 Lore 这个邪恶的双胞胎代表了两种故障模式;在 server-noonien 中,这些副本不会彼此偏离,因为合并在设计上就是无冲突的。这张图没有邪恶的双胞胎。
一个有趣的幕后故事:这集最初被提议写成 Data 与一名女性机器人的爱情故事。是 Brent Spiner 本人提出改为邪恶双胞胎的情节——因此,同时饰演这对双胞胎的演员也塑造了这集的反转。
参考资料:
- Memory Alpha — Datalore(剧集):https://memory-alpha.fandom.com/wiki/Datalore_(episode)
- Memory Alpha — Noonien Soong:https://memory-alpha.fandom.com/wiki/Noonien_Soong
- Memory Alpha — Lore:https://memory-alpha.fandom.com/wiki/Lore
本项目是独立的粉丝致敬作品。“Star Trek”、“Data”、“Lore”和“Noonien Soong”均为各自权利持有者的财产;本项目与他们无关,也未获得他们的认可。
文档
PLAN.md— 架构与设计决策。SCALING.md— 面向未来支持 1,000 / 10,000 个节点的计划。PROTOCOL.md—nooniend线路协议:传输、消息、限制与安全。docs/paper.md— 技术报告:模型、墓碑回收方面的贡献与评估;参考文献见docs/references.bib(在 1.0.0 版本发布时冻结;报告版权归 Sequi Company 所有,采用 CC BY 4.0 许可,详见NOTICE)。CONTRIBUTING.md— 如何参与贡献。AGENTS.md— 面向贡献者和 Agent 的项目规则。SECURITY.md— 安全策略及漏洞报告方式。CODE_OF_CONDUCT.md— 社区行为准则。
许可与免责声明
源代码采用 Mozilla Public License 2.0 (MPL-2.0) 许可;报告
docs/paper.md 版权归 Sequi Company 所有,采用
CC BY 4.0 许可,详见 NOTICE。
⚠️ 不提供担保和支持:本软件按“原样”提供。作者完全不提供商业支持、维护保证或正常运行时间协助。如果你提交问题,是否处理完全取决于自愿。
❤️ 支持项目(被动变现)
如果此组件为您的公司节省了数百小时的基础设施开发时间,请考虑支持本项目:
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。