{"slug": "server-noonien-conflict-free-agent-memory-across-machines-no-server", "title": "Server-noonien – conflict-free agent memory across machines, no server", "summary": "Server-noonien has launched as a drop-in replacement for the official @modelcontextprotocol/server-memory knowledge graph that syncs agent memory across machines peer-to-peer via the nooniend daemon, with no central server, database, or shared folder required. The tool stores each node's memory in its own append-only shard (<node>.jsonl) and merges them as an LWW-Element-Set CRDT ordered by (HLC, node, sequence), so merges are idempotent, commutative, associative, and convergent, avoiding the last-write-wins data loss of the official server's whole-file read-modify-write JSONL approach. It exposes the same nine tools as the official server, supports file and S3-compatible backends, works offline, and is licensed MPL-2.0.", "body_md": "**Shared, serverless, conflict-free memory for AI agents — with peer-to-peer sync.**\n\n`server-noonien` is a drop-in replacement for the official\n[`@modelcontextprotocol/server-memory`](https://github.com/modelcontextprotocol/servers/tree/main/src/memory)\nknowledge graph — the same entities, observations and relations — that **converges across machines**\nwith no central server, no database and no merge conflicts.\n\nIts headline feature is **peer-to-peer sync**: the `nooniend` daemon replicates each node's shard\ndirectly to the others, so several machines share one memory with **no shared folder** — only IP\nreachability over a VPN or a LAN.\n\nAgent memory today is local, and the official memory server is a single JSONL file that every mutation reads and rewrites in full. That breaks the moment you have more than one machine:\n\n- **It lives on one host.** Switch machine and your agent has forgotten everything.\n- **Sharing it over a synced folder loses writes.** The server serializes mutations*in-process\nonly* : two hosts each load the whole graph, mutate their own copy and write it back, so the last\nwrite silently discards the other's — and the sync tool forks the file into conflict copies\ninstead of merging it.\n- **The alternatives want a server, a database or a cloud.** The mainstream \"shared memory\" products\nare something you have to host, or they ship your memory off your machines.\n\n`server-noonien` removes the shared file and the central server at the root, and keeps the drop-in\ntool surface:\n\n- **Every node keeps its own memory.** Each machine writes only its own append-only shard\n(`<node>.jsonl` ); a shard has a single writer by design, so no two nodes ever write the same file\nand noonien itself never forks one — a shared area's syncer still can, and a conflict copy is\njust another shard to fold.\n- **Shards merge, they do not overwrite.** The graph is a CRDT — an**LWW-Element-Set** ordered by`(HLC, node, sequence)` — whose merge is idempotent, commutative, associative and convergent. Any\ntwo machines that have seen the same operations hold the**identical** graph, in any order, so\nthere is no \"last write wins\" data loss.\n- **No shared area, no central server.** The shards travel**directly between machines** with the`nooniend` daemon — only IP reachability, over a VPN or a LAN — so several machines share one\nmemory with**no shared folder** and**no service to host** . Prefer a folder or a bucket you\nalready have?`file` and`s3` work too: the transport is a choice, not a lock-in.\n- **Local-first and offline-friendly.** Every node reads and writes its own shard on disk and keeps\na full local replica, so it keeps working offline; the mesh reconverges when the network returns.\n- **Drop-in for the official server.** The same nine tools, inputs and outputs, so it slots under\nthe`memory` server name with no agent changes.\n\n`server-noonien` vs the official memory server vs hosted memory services:\n\n|  | `server-noonien` | Official `server-memory` | Hosted memory services | \n|---|---|---|---|\n| Multi-machine | Yes — per-node shards, one graph | No — one local file | Yes, via the service | \n| Sharing a synced folder | Safe — append-only, conflict-free | Unsafe — whole-file read-modify-write | N/A | \n| Needs a server / database | No | No | Yes | \n| Needs a cloud account | No | No | Yes | \n| Works offline | Yes ( `file` backend) | Yes | No | \n| Storage | Folder or S3-compatible bucket, your choice | One JSONL file | Their cloud | \n| Drop-in for the official tools | Yes — same nine tools | — | No | \n| Migration | `noonien import memory.jsonl` | — | Export/import dance | \n| Model | Explicit knowledge graph | Explicit knowledge graph | Often vector/semantic | \n| License | MPL-2.0 | MIT | Proprietary | \n\n**Hosted** is a category, not a product: it covers services that host your memory for you.\n`server-noonien` is the local-first opposite — you own the storage and there is no account.\n\n**A shared area is required** for the `file`/` s3` backends — a folder or an object store every node\ncan reach. `nooniend` removes that requirement: it replicates the shards directly between nodes.\n\nOne npm package, **`server-noonien`**, ships **three commands** (Node.js **≥ 22**). Install it once\nand all three land on your `PATH`:\n\n```\nnpm install -g server-noonien\n```\n\n| Command | Role | How it runs | \n|---|---|---|\n| **`server-noonien`** | the MCP memory server | short-lived, **spawned by the MCP client** over stdio | \n| **`noonien`** | the maintenance CLI | **one-shot** , run by hand | \n| **`nooniend`** | the peer-to-peer replication daemon | **long-lived** , one per machine | \n\nOnly `server-noonien` matches the package name, so it is the only command `npx` can run by package\nname. Run the other two with `--package`, or from the global install above:\n\n```\nnpx -y server-noonien                       # the MCP server (no argument serves over stdio)\nnpx -y -p server-noonien noonien help       # the maintenance CLI\nnpx -y -p server-noonien nooniend           # the replication daemon\n```\n\nFrom a clone — for development, or to run code not yet on npm:\n\n```\ngit clone https://github.com/sequico/server-noonien.git\ncd server-noonien && npm install && npm run build\n\nnode dist/index.js      # server-noonien — the MCP server\nnode dist/noonien.js   # noonien    — the maintenance CLI\nnode dist/gossip.js     # nooniend   — the replication daemon\n```\n\nThe entry every MCP client uses. With **no argument** (or `serve`) it speaks MCP over **stdio**, so\nyou don't run it yourself — the client spawns it and it lives for the session:\n\n```\nnpx -y server-noonien                          # the stdio command the client runs\nclaude mcp add memory -- npx -y server-noonien # register it with Claude Code as the `memory` server\n```\n\nPoint your client at it; it works on one machine out of the box, and [sharing across\nmachines](#share-across-machines) is the next step. In OpenCode (V2):\n\n```\n{\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"servers\": {\n      \"memory\": {\n        \"type\": \"local\",\n        \"command\": [\"npx\", \"-y\", \"server-noonien\"],\n        \"environment\": { \"NOONIEN_DIR\": \"~/.noonien\" }\n      }\n    }\n  }\n}\n```\n\nOther clients wrap the same command and environment in their own envelope; from a clone the command\nis `[\"node\", \"/path/to/server-noonien/dist/index.js\"]`. Because the tool surface is identical, you can\nreplace the `memory` server entry with `server-noonien` and change nothing else.\n\n| Command | Does | \n|---|---|\n| `noonien serve` | run the MCP memory server over stdio (the same as `server-noonien` ) | \n| `noonien import <file>` | import an official `server-memory` JSONL file (writes its own`<node>-import` shard, so it runs online) | \n| `noonien export` | print the folded knowledge graph as JSON | \n| `noonien merge` | fold every shard and report the merged state | \n| `noonien compact` | maintain this node's shard now: prune shadowed operations and, only where it is safe, physically delete its tombstones — the same rule the server applies ( `NOONIEN_GC` ; with a daemon that owns the directory it keeps the whole view and leaves the shard to the daemon) — and report why when it keeps them | \n| `noonien query <text>` | search entities and print the matching subgraph | \n| `noonien help` | list the commands; `noonien --version` prints the version | \n\nA long-lived service, **one per machine**, that replicates the shard directory peer to peer. It\ntakes no arguments and is configured entirely through environment variables — the shared\n`NOONIEN_DIR` and `NOONIEN_NODE_ID` ([Shared variables](#shared-variables)) plus the daemon's\n`NOONIEND_*` ([Daemon variables](#daemon-variables)). See [Peer to\npeer](#peer-to-peer--nooniend-recommended) for the setup.\n\n`noonien` merges shards; it does not move them by itself. How the shards travel between machines is\nyour choice: **peer to peer** (a companion daemon — the recommended default) or a **shared area** (a\nfolder or a bucket that already exists). The MCP server always reads and writes its own local shard;\nonly the transport differs.\n\n**The recommended way to share one memory across machines with no shared storage.** `nooniend` is a\nlong-lived daemon that replicates the local shard directory **peer to peer**. It only needs IP\nreachability, so it runs over any VPN (WireGuard, Tailscale, OpenVPN, Netbird, ZeroTier, …) or a\nplain LAN.\n\nGive each machine a **unique** node id and run one `nooniend` per machine. Every daemon keeps a\nfull replica of the others' shards beside its own, in the same local directory the MCP server uses —\nno shared folder required:\n\n```\n# on a tailnet\nNOONIEN_DIR=~/.noonien \\\nNOONIEND_TAILSCALE=true \\\nnooniend\n\n# or, with no self-listing underlay, seed one reachable peer\nNOONIEN_DIR=~/.noonien \\\nNOONIEND_PEERS=ai.example:27878 \\\nnooniend\n```\n\nThe peers **find each other** — you never list the machines. Configure **one** discovery source:\n`NOONIEND_TAILSCALE` (from `tailscale status`), `NOONIEND_DNS_SRV` (the `_nooniend._tcp` SRV\nrecords), or `NOONIEND_PEERS` (a static seed; one is enough, membership then self-propagates).\nA discovery source returns **candidates**, not members: a candidate joins only once it answers\n`/info`, so a machine on the network that does not run `nooniend` is never adopted.\n`NOONIEND_ADVERTISE` is optional and defaults to the listen host.\n\nHow it behaves:\n\n- **Never authors operations.** Each node's MCP server writes only its own`<node>.jsonl` ; the\ndaemon keeps replicas of the peers' shards and only ever pushes the shard it authors, so the\nsingle-writer rule holds. It also restores the local shard from a peer's replica when the local\ncopy is behind.\n- **Push on change + periodic anti-entropy.** On a local change (`NOONIEND_PUSH` ) and every`NOONIEND_INTERVAL` seconds it compares the per-shard high-water`seq` and moves the delta in\nwhichever direction is behind. Operations are idempotent, so delivery may be best-effort — no\nconsensus, no ordering, only eventual completeness.\n- **Scales past a full mesh.** By default it contacts**every** peer;`NOONIEND_FANOUT` caps that\nto a random subset per round — which**excludes tombstone collection** , because a reachable peer\nthe round did not sample suspends it, so the daemon refuses that combination — and at or above`NOONIEND_DIGEST_MIN_SHARDS` it reconciles through a fixed-size digest instead of the full shard\nlist (with`NOONIEND_CHANNELS` moving membership onto its own channel). A small mesh keeps\nbehaving exactly like a full mesh, and a peer without the digest capability falls back to the\nplain list, so a mixed mesh never breaks.\n- **Relays.** A node serves every replica it holds, so an offline node keeps converging through any\npeer that has its shard.\n- **Discovery and membership.** Bootstrap adapters (Tailscale, DNS SRV, a static seed) return**candidates** ; the peer set then gossips itself. A static`node@host:port` seed is explicit intent\nand enters membership directly; a**bare address** (a Tailscale or DNS-SRV candidate) joins only\nonce it answers`/info` , and its id comes from that answer — never from the discovery source — so a\ndevice that does not run`nooniend` is never a member and never blocks collection. A node that\ncomes up is adopted on its next probe, and the sources are re-read every`NOONIEND_DISCOVER_INTERVAL` seconds, so a joiner is found without a restart. Liveness comes from the exchange results — suspect\nafter`NOONIEND_SUSPECT_AFTER` failures, dead after`NOONIEND_DEAD_AFTER` , retried after`NOONIEND_DEAD_RETRY` , and forgotten after`NOONIEND_MEMBERSHIP_TTL` . A peer that reaches this\nnode is marked alive — its certificate names it under mTLS, otherwise the id it announces on the\nrequest — so a peer that recovers is re-engaged at once instead of waiting out`NOONIEND_DEAD_RETRY` . A peer this node does not know yet is adopted from the address it\nannounces on the request (untrusted, like a gossiped entry, and confirmed by the next sync), so a\nstatically-seeded joiner is discovered by the seeds it reached and its entry then spreads to the\nrest. A node's own entry is authoritative; an address that serves a different node is dropped.\n- **Wire and security.** A small HTTP/JSON service bound to`NOONIEND_LISTEN` (the full wire contract\nis in[`PROTOCOL.md`](https://github.com/sequico/server-noonien/blob/main/PROTOCOL.md) ); a shard accepts only its own node's operations. mTLS with\nper-peer certificates: the certificate's**common name is the node id** , so a peer may only push\nthe shard it authors, while reads stay open to any authenticated peer — which is what lets a node\nrelay.`NOONIEND_REVOKED` refuses specific nodes on every route;`NOONIEND_DEPARTED` additionally discards a departed node's replica and lets collection proceed past it.\n- **Observability.** Prometheus metrics — liveness, peer health, the stable watermark and, for\ncollection, how long each peer has been away (`noonien_gossip_absent_seconds` ) and how many\ndeleted elements are frozen (`noonien_gossip_frozen_elements` ) — plus a per-shard stable\nwatermark (the minimum high-water mark reached on every contactable peer, a conservative\ncausal-stability bound), and status and graph routes for probes and operators — see[The daemon\nHTTP API](#the-daemon-http-api) .\n\nUnder systemd, a user unit:\n\n```\n[Unit]\nDescription=nooniend — peer-to-peer shard replication\nAfter=network-online.target\n\n[Service]\nEnvironmentFile=%h/.config/noonien/nooniend.env\nExecStart=%h/.local/bin/nooniend\nRestart=on-failure\n\n[Install]\nWantedBy=default.target\n```\n\n`nooniend` serves a small HTTP/JSON API on `NOONIEND_LISTEN` (default `0.0.0.0:27878`). The full wire\ncontract — routes and payloads, the operation format, the protocol revision and capabilities, the\nsize limits and the mTLS model — is specified in [`PROTOCOL.md`](https://github.com/sequico/server-noonien/blob/main/PROTOCOL.md).\n\n```\ncurl -s http://127.0.0.1:27878/health\ncurl -s http://127.0.0.1:27878/status\ncurl -s http://127.0.0.1:27878/peers\ncurl -s http://127.0.0.1:27878/graph | jq '{entities: (.entities|length), relations: (.relations|length)}'\n```\n\nThe `file` and `s3` backends converge through a **shared area** that already exists and is\nreplicated outside noonien — a folder or a bucket every node can reach. Point every node at the\n*same* area and each node's shard reaches the others:\n\n```\nNOONIEN_BACKEND=file NOONIEN_DIR=/srv/noonien-shared NOONIEN_NODE_ID=ai server-noonien\n# or\nNOONIEN_BACKEND=s3 NOONIEN_S3_BUCKET=my-bucket NOONIEN_NODE_ID=ai server-noonien\n```\n\n- **`file`** — shards are`*.jsonl` files in`NOONIEN_DIR` ; point it at a folder replicated by\nSyncthing, Dropbox, a git working tree or a shared mount.\n- **`s3`** — one object per shard, written with conditional writes retried on contention; uses`@aws-sdk/client-s3` (an optional dependency, loaded only when selected). Conditional writes need\na store that honours`If-Match` /`If-None-Match` on`PutObject` (AWS S3, Cloudflare R2, MinIO do);\na store that ignores them is not supported, and a shard object with a multipart ETag is refused\nrather than guarded by a token that can never match. An object store has no append, so**every\nmutation rewrites the whole shard object** (read-modify-write): correct, but O(shard size) per\nwrite.\n\nThe **`memory`** backend keeps shards in process only — for tests and throwaway sessions. See\n[Configuration](#server-variables) for the variables.\n\n**What a shared area does not give you: a safe deletion.** `file`/` s3` share a graph; they cannot\nsay whether a peer holds a write it has not delivered yet. So without a daemon the maintenance\n**prunes only**, and a physical deletion happens only where the operator declares the directory\nwritten by this node alone (`NOONIEN_GC`) — see [Deletion and\ncollection](#deletion-and-collection). Convergence is delegated to the syncer as well: noonien\nneither verifies nor retransmits, so latency and conflict copies are the syncer's business, and a\nconflict copy is just another shard to fold. And the daemon replicates a **directory**: `nooniend`\nrefuses to run with `NOONIEN_BACKEND=s3` (or `memory`), where it would replicate a local folder the\nserver never uses.\n\nEvery setting is read from the environment. `NOONIEN_DIR` and `NOONIEN_NODE_ID` are shared; the\nMCP server, the CLI and the daemon otherwise have their own variables — `NOONIEN_*` for the server,\n`NOONIEND_*` for the daemon.\n\nUsed by every command.\n\n| Variable | Default | Meaning | \n|---|---|---|\n| `NOONIEN_NODE_ID` | hostname | This node's shard name — the identity the MCP server writes under and the daemon replicates. Must be **unique per machine** and written by a**single server at a time** . | \n| `NOONIEN_DIR` | `~/.noonien` | Directory holding this machine's shards: the `file` backend's store and the daemon's replica store. A leading`~` is expanded. | \n\nThese two are **local to each machine**, not a shared area: the MCP server and the daemon share the\nsame directory *on the same host*, and every machine keeps its **own** copy of every shard.\nPeer-to-peer sync with `nooniend` needs **no shared folder or bucket**; a shared area is only the\nalternative transport for `file`/` s3`.\n\nA node id is a **single-writer shard**: exactly one server process should append to it at a time.\nThe normal setup is one memory server per machine; if you run several servers against the same store\nconcurrently, give each its own `NOONIEN_NODE_ID`. The tool does not enforce this: two writers that\nshare a node id and write in the same millisecond produce operations with the same id, which a read\n**fails loudly** on — but writers on different clocks usually land on different ids, so the shard\ncan quietly interleave and the fold picks one. A unique node id is a requirement, not something the\ntool can check.\n\nUsed by the MCP server (and the `noonien` CLI, which reads and writes the same shards).\n\n| Variable | Default | Meaning | \n|---|---|---|\n| `NOONIEN_BACKEND` | `file` | `file` ,`memory` or`s3` . | \n| `NOONIEN_S3_BUCKET` | — | Bucket (required for the s3 backend). | \n| `NOONIEN_S3_PREFIX` | `\"\"` | Key prefix all shard objects live under. | \n| `NOONIEN_S3_REGION` | `us-east-1` | Region passed to the S3 client. | \n| `NOONIEN_S3_ENDPOINT` | — | Custom endpoint for MinIO / R2 / Contabo and similar. | \n| `NOONIEN_S3_FORCE_PATH_STYLE` | on when an endpoint is set | Path-style requests. | \n| `NOONIEN_COMPACT_AFTER` | `1000` | Compact the local shard online after this many appended operations ( `0` disables). | \n| `NOONIEN_GC` | off | Delete tombstones **without a daemon** : only where this node is the directory's only writer — a promise nothing in the directory can prove. Where a daemon owns the directory (it announces itself with its knowledge file from startup) it has no effect: the daemon collects. | \n\n`NOONIEN_GC` (server, default off) and the daemon's `NOONIEND_GC` (default on) are different\nswitches; with the `memory` backend nothing is shared, so the server collects regardless of\n`NOONIEN_GC`.\n\nS3 credentials come from the standard AWS chain (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,\n`AWS_REGION`, profiles, instance roles, …).\n\nThe local shard is compacted **online**: the server rewrites its own shard after\n`NOONIEN_COMPACT_AFTER` appended operations (default `1000`, `0` disables), inside the same\nmutation queue as the writes. The rewrite is a compare-and-swap against the content it read, and a\nconcurrent write — an append, or the `nooniend` daemon recovering the shard — makes it re-read and\nretry, so nothing is ever lost; `noonien compact` runs the same compaction by hand. `noonien import` authors its own `<node>-import` shard, so it too can run while the node's server is live and\nre-importing is idempotent. That shard only ever adds, so it needs neither the peer knowledge nor\nthe `NOONIEN_GC` promise: `noonien import` prunes it on the way out, and `noonien compact` prunes\nit too — dropping the operations a later one shadowed, keeping every tombstone, and rewriting only\nwhen something was dropped, so a repeated maintenance costs nothing and no peer is asked to re-pull\nthe shard. On the `file` backend, `append` and `compact` take a per-shard lock, so they serialize\ninstead of racing even across processes — and the daemon takes the same lock.\n\nWithin a node the timestamp is **monotonic**: if the wall clock steps back (NTP, a restored\nsnapshot, a manual `date`), a new operation keeps the previous timestamp rather than an older one,\nso an earlier operation can never win over a later one of the same node.\n\nUsed by the daemon only. It needs **at least one discovery source** — `NOONIEND_TAILSCALE`,\n`NOONIEND_DNS_SRV` or `NOONIEND_PEERS` — to reach the mesh; the rest is optional. Run it behind a\nVPN or firewall and enable mTLS — see [Security](#security).\n\n| Variable | Default | Meaning | \n|---|---|---|\n| `NOONIEND_LISTEN` | `0.0.0.0:27878` | `host:port` the gossip service binds. | \n| `NOONIEND_ADVERTISE` | the listen host (hostname on a wildcard) | **Optional.**`host:port` this node advertises to peers; set it only when the default does not resolve from the others. | \n| `NOONIEND_PEERS` | — | **Discovery: static seeds** —`node@host:port` or`host:port` , comma separated. One is enough; membership then self-propagates. | \n| `NOONIEND_DNS_SRV` | — | **Discovery:** domain whose`_nooniend._tcp` SRV records are seeds. | \n| `NOONIEND_TAILSCALE` | off | **Discovery:** derive candidate addresses from`tailscale status` (a peer joins once it answers`/info` ). | \n| `NOONIEND_DISCOVER_INTERVAL` | `300` | Seconds between discovery refreshes; `0` reads the sources once at startup. A refresh only adds candidates. | \n| `NOONIEND_INTERVAL` | `30` | Anti-entropy period, seconds. | \n| `NOONIEND_PUSH` | on | Reconcile as soon as the local shard changes. | \n| `NOONIEND_FANOUT` | `0` | Peers contacted per round; `0` is every reachable peer.**A cap excludes `NOONIEND_GC`** : a reachable peer the round did not sample suspends collection, so the daemon refuses the two together. | \n| `NOONIEND_DIGEST_MIN_SHARDS` | `32` | Shard count at or above which the digest replaces the full list ( `0` = whenever supported). | \n| `NOONIEND_CHANNELS` | off | Gossip membership on its own channel instead of piggybacking it on every exchange. | \n| `NOONIEND_RELAY` | — | Comma-separated node ids always sampled (relays / super-peers). | \n| `NOONIEND_GC` | on | Collect the authored shard: prune the shadowed operations (keeping the operation a peer off the mesh could still have folded as the gate's evidence) and drop a tombstone once no peer off the mesh that folded its element could contest it — the peer's knowledge frontier is a subset of what this node holds and covers no operation of the element (physical deletion). | \n| `NOONIEND_FORGET_AFTER` | `15552000` (180 days) | Seconds of silence after which a peer is **retired** : its protection becomes durable (a merged threshold, or a blanket when it holds operations this node lacks) and its live record is dropped, so the metadata collection depends on stays bounded.`0` never retires. | \n| `NOONIEND_SUSPECT_AFTER` | `3` | Failed exchanges before a peer is marked suspect. | \n| `NOONIEND_DEAD_AFTER` | `6` | Failed exchanges before a peer is marked dead. | \n| `NOONIEND_DEAD_RETRY` | `300` | Seconds a dead peer is left alone before it is probed again. | \n| `NOONIEND_MEMBERSHIP_TTL` | `604800` | Seconds a dead peer is kept before it is forgotten. | \n| `NOONIEND_REVOKED` | — | Comma-separated node ids the daemon never syncs with and refuses to accept. | \n| `NOONIEND_DEPARTED` | — | Comma-separated node ids explicitly gone: like `NOONIEND_REVOKED` but their replica is also discarded and they stop blocking collection. | \n| `NOONIEND_TLS_CERT` /`NOONIEND_TLS_KEY` | — | Serve TLS with this certificate and key. | \n| `NOONIEND_TLS_CA` | — | CA that signs the peers' certificates, used to verify them in both directions. | \n| `NOONIEND_TLS_REQUIRE_CLIENT` | on | Require a client certificate (mTLS). | \n\nEvery mutation is recorded as an **operation** in an append-only log, split into per-node shards:\n\n- `create_entities` /`add_observations` /`create_relations` add elements;\n- `delete_entities` /`delete_observations` /`delete_relations` write tombstones (a deleted entity\nalso tombstones its observations and incident relations).\n\nA read **folds** all shards: each element (entity, observation occurrence, relation) is an LWW\nregister decided by `(HLC, node, sequence)`. A delete of an observation is content-level: it removes\nevery occurrence older than it. Observations are shown while their entity is present, relations\nwhile both endpoints are present. The fold depends only on the *set* of operations, never on the\norder shards or lines were read. Operations are ordered by a **Hybrid Logical Clock**: the order\nsurvives clock skew, and a node that has observed a claim can write a correction ordered after it.\nAn observed stamp further than a bounded skew in the future is clamped, so a wrong clock cannot drag\nthe node's timestamps into the future. Reads are **incremental**: each shard is decoded once and\ncached against a cheap fingerprint, and the folded graph is memoized, so a read with nothing changed\ncosts only a fingerprint poll per shard.\n\nConsequences, all covered by property tests:\n\n- **Idempotent** — applying the same operations twice changes nothing.\n- **Commutative** — order of operations across shards does not matter.\n- **Associative** — folding shards in any grouping yields the same graph.\n- **Convergent** — two nodes with the same operations produce the identical graph.\n\nOrdering is deterministic: entities by name, relations by `(from, to, relationType)`, observations\nin operation order (`HLC`, `node`, `sequence`).\n\nThe log grows with every mutation. Compaction rewrites this node's shard keeping, per element, only\nthe operations that no later operation shadows — the shadow index is built from the **merged** log,\nso an operation superseded by *another node's* later operation is dropped too, and the winner of\nevery element always survives, tombstones included. It runs **online**: the server compacts its own\nshard after `NOONIEN_COMPACT_AFTER` appends, and `noonien compact` does the same by hand. When\neach also collects tombstones — the physical deletion — is under [Deletion and\ncollection](#deletion-and-collection). The rewrite is a compare-and-swap (a temporary file renamed\nover the shard on the `file` backend, a conditional write on `s3`), so a crash midway can never\ntruncate it and a concurrent write — an append, or a gossip recovery — makes the whole compaction\nre-read and retry instead of losing it.\n\nEvery compaction stamps a `shard.compact` metadata operation (never a graph element): its\n`generation` is bumped each time, and its `seq` is the shard's durable high-water mark, so the\nsequence never regresses and a compaction is never mistaken for a lost shard. Peers read the\ngeneration from `/shards` and replace a stale replica with the compacted shard, so every copy\nconverges to the compacted content. Only the local shard is rewritten; every other shard is\nuntouched, so compaction is conflict-free and other nodes are unaffected.\n\nA delete is an **operation**, not a removal: `delete_entities`, `delete_observations` and\n`delete_relations` append tombstones, and the fold hides the element while a tombstone is the latest\noperation for it. The tombstone is exactly what makes the deletion beat a concurrent write, so\ndropping it while that write still exists would revive the element. A deletion is therefore only\nphysical once nobody can still hold one.\n\n`nooniend` collects the shard it authors when the mesh is safe, **per element**. It first prunes\nthe operations a later one shadows that no retained off-mesh peer could still need, then it may drop\na surviving tombstone when no peer off the mesh could contest that element. Two conditions gate it,\nper peer: the peer's **knowledge frontier** must be a subset of what this node already holds — a\npeer holding any operation this node lacks could hold a *relayed* operation on the element and is\ntreated as a blocker — and the frontier must cover no operation of the element, so only a peer that\nfolded it can contest it; a peer that never folded it can still mint a coincident element (an\nindependent name, an observation on an entity it knows), which the fold decides as a concurrent\ngenesis, not a revival. An element that appeared while a peer was already away is thus collectable\nat once, without waiting for that peer to return; an element the peer did know stays frozen until\nthe peer comes back or is explicitly departed. A surviving operation on another shard also keeps the\ntombstone, so the fold is always preserved. When a peer could contest an element, the operation it\nknew is kept as the evidence the next round reads again — and by the node that **authored** it, not\nonly by the deleter: a node never prunes a shadowed operation on an element a peer off the mesh\ncould still contest, so the proof survives even when the tombstone lives on a different shard (a\ndelete by one node of an element another created).\n\nThe claim, in one line: collection needs **no barrier over the history** — a peer that never folded\nan element cannot *causally* contest it, however long it stays away, and neither a global round of\nacknowledgements nor a timer on the data is needed. What it *does* need is a **stable round over the\nlive set**: every retained peer that is still reachable must have been exchanged with in that round\n(otherwise the round **suspends**, because such a peer may hold writes this node has not pulled),\nand no retained peer may hold anything this node lacks. That is why `NOONIEND_FANOUT` and\n`NOONIEND_GC` exclude each other. Liveness is not autonomous: a peer that is both unreachable and\nunverifiable — never reached, or holding operations this node cannot read — blocks collection until\nthe operator departs it (`NOONIEND_REVOKED` / `NOONIEND_DEPARTED`); the retention window bounds\n*metadata* and forfeits no data, but it does not remove that decision.\n\nEach peer's knowledge frontier — the greatest sequence it has been seen to hold of every author's\nshard — is **durable**, persisted beside the shards in `.nooniend-peers.json`, so a machine gone\nfor a year keeps blocking only the elements it actually knew. A peer the round could still reach but\ndid not exchange with **suspends** collection entirely, because it may hold writes this node has not\npulled. Forgetting a peer (the membership TTL) does not unblock it — it may be alive off the mesh\nwith valid unsent writes — but `NOONIEND_REVOKED` and `NOONIEND_DEPARTED` do, the operator's\ndecision that its writes are expendable; a departed peer's replica is discarded outright. A\nforfeited node's **authorship** is dropped from the gate as well, so it stops holding the collection\nback through the peers that still hold its replica; a *revoked* node's operations already here keep\ntheir elements frozen until it is departed, which discards them. Turn the daemon's collection off\nwith `NOONIEND_GC=false`.\n\nThe per-peer record is **bounded**. After `NOONIEND_FORGET_AFTER` seconds of silence (default 180\ndays) a peer is **retired**, and retirement is semantically invisible: it moves where the protection\nlives, never which elements are guarded. An **accounted** peer — one whose frontier is a subset of\nwhat this node already holds — is merged into a durable **retired frontier**, one elementwise\nmaximum per author. The maximum is exactly the union of the per-element decisions the peers would\nhave made, so it keeps gating every element those peers could have folded, **including deletions\nthat happen after the retirement**, over one map bounded by the shards this node holds rather than\none map per peer. (A per-element list computed at expiry would *not* be sound: a tombstone created\nlater is not on it.) An **unaccounted** peer — one that claims operations this node cannot read —\nleaves a durable **blanket** instead: the same \"it could hold anything\" that gated it while it was\nlive, stated once rather than carried as an unverifiable map. Nothing is forfeited by retiring,\nwhich is why the window can be on by default. `NOONIEND_REVOKED` and `NOONIEND_DEPARTED` remain\nthe only forfeiture — and since a maximum cannot be un-merged, a node must be departed **before**\nits window expires for its contribution to be lifted. A frontier that claims an author this node\ncannot serve is never recorded: a peer cannot freeze the mesh with a claim it cannot back. A claimed\nnode that was never reached keeps blocking conservatively, as before.\n\n**Without the daemon** a physical deletion needs an explicit promise. A daemon that owns the\ndirectory (`.nooniend-peers.json` present) holds the peer knowledge and collects, and it is the\nonly writer that maintains the shard: the server and `noonien compact` keep their whole view there\n— a shadowed operation is exactly the evidence the daemon's gate reads, so they must not prune it —\nand leave the shard to the daemon. With no daemon there is no knowledge to gate on, and the\ndirectory **cannot** tell whether a peer is merely absent: a machine that is offline, or configured\nbut not yet synced, leaves nothing to see, and its unsynced write would be lost by a collection —\nwhich is the immediate deletion the official server performs. So the server's online maintenance and\n`noonien compact` both prune only, until the operator declares the directory written by **this node\nalone** (`NOONIEN_GC=true`), the promise that makes it sound; a declaration contradicted by another\nnode's shard is refused and reported.\n\nThe daemon serves every metric at `/metrics` (the MCP server is stdio and has no endpoint). The\ncollection and retention state:\n\n| Metric | Meaning | \n|---|---|\n| `noonien_gossip_frozen_elements` | deleted elements kept because a peer off the mesh may still contest them | \n| `noonien_gossip_absent_nodes` | retained peers not exchanged with in the last round | \n| `noonien_gossip_absent_seconds{node}` | how long each peer has been away | \n| `noonien_gossip_retained_peers` | peers whose knowledge still gates collection | \n| `noonien_gossip_retired_total` | peers retired into the retired frontier | \n| `noonien_gossip_retired_unaccounted_total` | peers retired with a blanket | \n| `noonien_gossip_retired_authors` | authors whose thresholds survive in the retired frontier | \n| `noonien_gossip_blankets` | durable blankets in force | \n\nThe same endpoint carries the transport and liveness families — `noonien_gossip_rounds_total`,\n`noonien_gossip_round_seconds`, `noonien_gossip_round_bytes`, `noonien_gossip_links`,\n`noonien_gossip_shards`, `noonien_gossip_stable_shards`, `noonien_gossip_membership_size`,\n`noonien_gossip_peers_ok_total`, `noonien_gossip_peers_failed_total`,\n`noonien_gossip_ops_pulled_total`, `noonien_gossip_ops_pushed_total`,\n`noonien_gossip_digest_exchanges_total`, `noonien_gossip_membership_channel_failures_total`,\n`noonien_gossip_bytes_sent_total`, `noonien_gossip_bytes_received_total`,\n`noonien_gossip_fanout`, `noonien_gossip_up` — and, when the daemon runs the local log's own\nmaintenance, its `noonien_mcp_*` counters (`noonien_mcp_folds_total`, `noonien_mcp_fold_seconds`,\n`noonien_mcp_shards_merged`, `noonien_mcp_shard_stat_total`, `noonien_mcp_ops_decoded_total`,\n`noonien_mcp_compacted_total`, `noonien_mcp_compaction_failed_total`).\n\nThe MCP surface is the official nine, drop-in: names, inputs, outputs and messages match\n`@modelcontextprotocol/server-memory`.\n\n| Tool | Does | \n|---|---|\n| `create_entities` | create multiple new entities in the knowledge graph | \n| `create_relations` | create multiple new relations between entities (active voice) | \n| `add_observations` | add new observations to existing entities | \n| `delete_entities` | delete entities and their associated relations | \n| `delete_observations` | delete specific observations from entities | \n| `delete_relations` | delete multiple relations | \n| `read_graph` | read the entire knowledge graph | \n| `search_nodes` | search nodes by a query, matched against entity names, types and observation content | \n| `open_nodes` | open specific nodes by their names | \n\nThe same nine tools ([Tools](#tools)), so it slots under the `memory` server name with no agent\nchanges.\n\nThe full graph is also exposed at the `memory://knowledge-graph` resource. The resource is\nread-only; clients can subscribe to it and receive `notifications/resources/updated` whenever a\nmutation changes the graph — via `resources/subscribe` on the 2025-era protocol, or a\n`subscriptions/listen` stream on the 2026-07-28 revision. The server speaks both: it answers the\n`initialize` handshake for 2025-era clients and `server/discover` for 2026-07-28 clients.\n\nEntities are keyed by name. Observations behave like the official server: `add_observations` has no\neffect when the content is already present, but preserves duplicate contents within a single\nrequest, `create_entities` preserves duplicate contents as given, and deleting an observation\nremoves every occurrence of that content. Relations are sets: adding the same relation twice has no\neffect. `delete_entities` also removes the entity's observations and its incident relations. The\ntombstones cover what the deleting node could see: an observation added concurrently on another node\nsurvives as an element, so if the entity is later created again that observation reappears with it.\n\n- **One writer per shard.** Keep`NOONIEN_NODE_ID` unique and run one server per node id. A node id\nis sanitised to a bounded, path-safe shard-file stem, and every node id that arrives from a peer\n(a`/shards/{node}/ops` path, a gossiped summary, an announced header) is validated before it can\nname a file, enter the peer set or label a metric.\n- **Replication.**`nooniend` is a network service. Run it behind a VPN or firewall, and enable\nmTLS (on by default when certificates are configured) so only authenticated peers replicate. The\nclient**always verifies the peer certificate** — a configured CA, or the system trust store when\nnone is set; an unverified connection is never used. Without TLS the daemon trusts the underlay; a\npeer's advertised address is still validated as a plain`host:port` , and it is adopted only when\nit matches the host the request actually came from (so an announced header cannot point the dialer\nat another host). A refusal (`NOONIEND_REVOKED` /`NOONIEND_DEPARTED` ) needs the client\ncertificate that mTLS supplies — without it a peer has no identity to refuse. Setting`NOONIEND_TLS_REQUIRE_CLIENT=false` turns TLS back into an unauthenticated channel (any client\ncan push any shard); the daemon logs a warning at startup in that mode.\n- **Trust model.** mTLS authenticates**members** , not individual operations. A node serves every\nshard it holds, so relaying is trusted: a member can carry another node's shard to a third node,\nand the daemon recovers its own lost shard from a peer's replica. Run`nooniend` only on machines\nyou trust as peers. The fold's last-writer-wins order trusts the operation's HLC stamp, so a\nmember whose clock is far ahead wins every conflict on its elements; a wrong clock is a\ncorrectness hazard, not a privilege boundary. Collection also**trusts what a member reports** :\nits frontier from`/shards` and the peer set it gossips — the certificate proves*who* it is, not\nthat its reports are true — and it assumes the mesh is the only path by which a node learns: a\nnode that leaves must not keep receiving operations elsewhere (do not point a daemon and a shared`file` /`s3` area at the same directory, and a node bridging two meshes belongs to both or should\nbe departed).\n- **Reporting a vulnerability.** Do not open a public issue — see[`SECURITY.md`](https://github.com/sequico/server-noonien/blob/main/SECURITY.md) .\n\n```\ngit clone https://github.com/sequico/server-noonien.git\ncd server-noonien\nnpm install\n\nnpm run check       # Biome: lint + format + import order\nnpm run typecheck   # tsc --noEmit\nnpm run test        # Vitest, including the CRDT property tests\nnpm run build       # tsc -> dist/\nnpm run gate        # all of the above\n```\n\nSee [`CONTRIBUTING.md`](https://github.com/sequico/server-noonien/blob/main/CONTRIBUTING.md) for the workflow and the ground rules.\n\nThe name is an homage to *Star Trek: The Next Generation*. The project was first meant to be called\n**\"Datalore\"** — the title of the episode (Season 1, Episode 13, first aired 18 January 1988) that\nintroduces **Data** and his twin brother **Lore**, the two Soong-type androids built by Dr. **Noonien\nSoong**. *Data* is the good one; *Lore* is the flawed, emotional, malicious prototype. Both are\nplayed by the same actor, Brent Spiner.\n\nThe episode title is a portmanteau of the two androids, and the two names fit a knowledge graph almost too well:\n\n- **Data** — a*datum* : a single fact.\n- **Lore** — the body of*knowledge and tradition* shared by a community.\n\nSo *datalore* would have meant the facts **plus** the shared lore — exactly what a shared agent\nmemory is. But \"Datalore\" is also the name of an unrelated commercial product in the same software\nspace, so rather than carry that brand conflict the project was renamed **`noonien`**, after the\nandroids' creator, Dr. Noonien Soong. Naming it after their maker keeps the homage to **both** Data\nand Lore.\n\nThe twin motif maps onto this project's core problem — many copies of the same graph, on different\nmachines, that must stay consistent. Data's good twin and Lore's evil twin are the two failure\nmodes; in `server-noonien` the copies cannot drift apart, because the merge is conflict-free by\nconstruction. The graph has **no evil twin**.\n\nA small production gem: the episode was first pitched as a romance for Data with a female android.\nIt was **Brent Spiner himself who suggested the evil-twin plot** instead — so the actor who played\nboth twins also shaped the episode's twist.\n\nReferences:\n\n- Memory Alpha — *Datalore (episode)* :[https://memory-alpha.fandom.com/wiki/Datalore_(episode)](<https://memory-alpha.fandom.com/wiki/Datalore_(episode)>)\n- Memory Alpha — *Noonien Soong* :[https://memory-alpha.fandom.com/wiki/Noonien_Soong](https://memory-alpha.fandom.com/wiki/Noonien_Soong)\n- Memory Alpha — *Lore* :[https://memory-alpha.fandom.com/wiki/Lore](https://memory-alpha.fandom.com/wiki/Lore)\n\n*This project is an independent fan homage. \"Star Trek\", \"Data\", \"Lore\" and \"Noonien Soong\" are the\nproperty of their respective rights holders; this project is not affiliated with or endorsed by\nthem.*\n\n- [`PLAN.md`](https://github.com/sequico/server-noonien/blob/main/PLAN.md) — architecture and design decisions.\n- [`SCALING.md`](https://github.com/sequico/server-noonien/blob/main/SCALING.md) — the forward-looking plan for 1,000 / 10,000 nodes.\n- [`PROTOCOL.md`](https://github.com/sequico/server-noonien/blob/main/PROTOCOL.md) — the` nooniend` wire protocol: transport, messages, limits and\nsecurity.\n- [`docs/paper.md`](https://github.com/sequico/server-noonien/blob/main/docs/paper.md) — the technical report: the model, the tombstone-collection\ncontribution and the evaluation, with the bibliography in[`docs/references.bib`](https://github.com/sequico/server-noonien/blob/main/docs/references.bib) (frozen at release 1.0.0; the report is © Sequi\nCompany under CC BY 4.0 — see[`NOTICE`](https://github.com/sequico/server-noonien/blob/main/NOTICE) ).\n- [`CONTRIBUTING.md`](https://github.com/sequico/server-noonien/blob/main/CONTRIBUTING.md) — how to contribute.\n- [`AGENTS.md`](https://github.com/sequico/server-noonien/blob/main/AGENTS.md) — the project rules for contributors and agents.\n- [`SECURITY.md`](https://github.com/sequico/server-noonien/blob/main/SECURITY.md) — security policy and how to report a vulnerability.\n- [`CODE_OF_CONDUCT.md`](https://github.com/sequico/server-noonien/blob/main/CODE_OF_CONDUCT.md) — the community code of conduct.\n\nThe source code is licensed under the [Mozilla Public License 2.0 (MPL-2.0)](https://github.com/sequico/server-noonien/blob/main/LICENSE); the report\n[`docs/paper.md`](https://github.com/sequico/server-noonien/blob/main/docs/paper.md) is © Sequi Company, licensed under\n[CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — see [`NOTICE`](https://github.com/sequico/server-noonien/blob/main/NOTICE).\n\n**NO WARRANTY & NO SUPPORT:** This software is provided \"as is\". The author offers **absolutely\nno commercial support, no maintenance guarantees, and no uptime assistance**. If you open an issue,\nit will be addressed purely on a voluntary basis.\n\nIf this component is saving your company hundreds of hours of infrastructure development, please consider supporting the project:\n\n- **Sponsor on GitHub:** Help keep this project alive by adding your company logo to this README.[👉 Sponsor Now](https://github.com/sponsors/sequico)\n- **Back a Feature:** Want a specific bug fixed or a new protocol added? We use Polar.sh. Put a\nbounty on any GitHub Issue to incentivize its development.[👉 View Bounties](https://polar.sh)", "url": "https://wpnews.pro/news/server-noonien-conflict-free-agent-memory-across-machines-no-server", "canonical_source": "https://github.com/sequico/server-noonien", "published_at": "2026-10-15 11:40:17+00:00", "updated_at": "2026-10-15 11:49:59.413414+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "ai-infrastructure"], "entities": ["server-noonien", "nooniend", "@modelcontextprotocol/server-memory", "Model Context Protocol", "MPL-2.0"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/server-noonien-conflict-free-agent-memory-across-machines-no-server", "markdown": "https://wpnews.pro/news/server-noonien-conflict-free-agent-memory-across-machines-no-server.md", "text": "https://wpnews.pro/news/server-noonien-conflict-free-agent-memory-across-machines-no-server.txt", "jsonld": "https://wpnews.pro/news/server-noonien-conflict-free-agent-memory-across-machines-no-server.jsonld"}}