Skip to content

@agent-custody/state

Governed memory for AI agents. Not a vector store: a ledger of facts where every write cites the receipt that caused it, carries valid time and transaction time, and can be superseded or retracted without losing what the agent believed at any earlier moment.

Retrieval stays with whatever store you already use. This package owns provenance, time, and undo.

Getting started

bash
npm install @agent-custody/state           # or bun add, pnpm add

Published on npm as @agent-custody/state: compiled JavaScript with type declarations, Node 22 or later, Apache-2.0.

ts
import { Ledger } from "@agent-custody/state";

// a JSONL file, a .sqlite path, or "postgres://…" with the pg package installed; the ledger is asynchronous
const ledger = new Ledger("./state/ledger.jsonl");
const a = await ledger.assert({ subject: "acct:42", predicate: "plan", value: "pro", space: "org", actor: "agent:support", source: { receiptId } });
await ledger.assert({ subject: "acct:42", predicate: "plan", value: "enterprise", space: "org", actor: "agent:sales", supersedes: a.fact.factId });
await ledger.retract({ factId: a.fact.factId, actor: "user:admin", reason: "poisoned by a tool result" });
await ledger.asOf({ validAt: "2026-09-01T00:00:00Z", txAt: "2026-09-01T00:00:00Z" });

Six runnable examples, all executed by the test suite. 06-actions-on-beliefs.ts puts memory and a payments API behind one gateway and finds the refund in a belief's blast radius. 05-blast-radius.ts walks from a retracted belief to everything that relied on it. 04-evals.ts scores the ledger and a naive store on the same memory incidents. 03-memory-behind-the-gateway.ts runs the memory server as the gateway's upstream. 01-ledger.ts walks through a wrong write and its undo. 02-receipt-to-belief.ts runs the whole loop with the receipts package: a tool call gets a signed receipt, the receipt is verified, the belief taken from it is recorded citing the receipt, and later retracted. Run them with node examples/<file> from this directory, after bun run build at the repository root.

The memory server

The ledger as MCP tools, meant to run as the upstream of the receipts gateway. Behind the gateway, every write and read is checked by the Cedar policy and gets a signed receipt, and two things reach this server in the call's _meta that no caller can supply: the receipt id, which becomes the fact's source, and the agent from the signed delegation grant, which becomes the fact's actor. A caller's own claims about either are ignored.

bash
agent-custody-memory serve --ledger ./ledger.jsonl        # over stdio; refuses calls that did not come through the gateway

In the gateway's config, the memory server is the upstream, and the grant names the memory tools as scopes:

json
{ "upstream": { "command": "agent-custody-memory", "args": ["serve", "--ledger", "/abs/path/ledger.jsonl"] }, ... }
tooldoespolicy sees
memory.writerecords a belief in a space, optionally superseding an earlier factcontext.args.space, subject, predicate, value
memory.readthe facts believed at a moment, by space, subject, predicate, valid time, transaction timethe query
memory.confirmlifts a quarantined fact to attested; accepted only through the gatewayfactId
memory.retractundoes a belief, keeping it visible to questions about the pastfactId, reason
memory.forgeterases the value from the ledger and every store, keeping the digest; the receipt is the certificatefactId, reason
pack (CLI)one fact's history, receipts, holds, blast radius, and forget certificate as one signed artefact, verifiable offline
memory.hold, memory.releaselegal hold: while it stands the fact cannot be forgotten by request or sweepfactId, reason
memory.sweepretention: forget what was learned before an instant, in a space or all, skipping held facts, reaching every storebefore, space, reason
memory.getone fact by id in any state, for the gateway's policy lookupsfactId
memory.historyevery event that touched a factfactId

Quarantine. Every fact carries a provenance: claimed, attested, or verified. A write that came through the gateway is attested: its actor is the agent named in a human-signed grant and its receipt exists. A write that arrived any other way is claimed, and claimed facts are quarantined: memory.read leaves them out unless the caller asks for includeClaimed, and the policy can refuse that. memory.confirm, accepted only through the gateway, lifts a claimed fact to attested with its own receipt and transaction time, so "was this fact still in quarantine on Tuesday" is answerable. A tool result an SDK-only agent wrote down cannot become something the rest of the fleet believes until an attested party says so. Over stdio the server has one client; shared over HTTP, below, gateways and direct writers feed one ledger and quarantine does its job.

Value-level quarantine. An attested write still carries whatever value the agent chose to write. When the agent can say where a value came from, the gateway's own observation decides. A memory.write may name evidence: { fact, path }, a fact the gateway fetched itself for this call through its fact-lookup mechanism (a CRM record, say) and optionally a field in it. The memory server compares the value with the observation: equal, and the fact is written as verified, the third provenance level, where both the actor and the value are vouched for by something other than the agent; different, and the write is refused. A policy can require evidence for a space (context.args has evidence), and memory.read with requireVerified returns only verified facts. The test suite runs this with a CRM lookup behind the same gateway as the memory server.

Policy over the fact being changed. The gateway can look up the fact a write supersedes or a retraction targets before deciding, through its fact-lookup mechanism and the memory.get tool, and the policy then sees that fact's space, actor, and provenance as observed facts. This is the second half of trust tiers: a self-reported note in the org space can be superseded by anyone the grant allows, while an attested org fact cannot be displaced or retracted except by whoever the policy names. In the gateway config:

json
"facts": [
  { "name": "target", "tool": "memory.get", "args": { "factId": "$args.supersedes" }, "forTools": ["memory.write"], "optional": true },
  { "name": "target", "tool": "memory.get", "args": { "factId": "$args.factId" }, "forTools": ["memory.retract"] }
]
cedar
forbid(principal, action in [Action::"memory.write", Action::"memory.retract"], resource)
when { context.facts has target && context.facts.target.space == "org" && context.facts.target.provenance == "attested" };

The lookup for writes is optional, so a write that supersedes nothing needs no lookup; the one for retractions is required. The denial receipt records the fact the policy saw, observed by the gateway. The test suite runs exactly this configuration.

Attested executions. Started with --key memory.key, the server signs every result for the receipt the gateway is issuing. A verifier given memory.pub then reports the execution of each memory call as attested by the memory server, not only observed by the gateway; the test suite verifies a write this way.

Shared over HTTP. agent-custody-memory serve --ledger ./ledger.jsonl --http --port 8790 --token-env MEMORY_TOKEN serves the same tools over Streamable HTTP, so several gateways, one per agent host, and, with --allow-direct, SDK-only agents share one ledger. That is the deployment quarantine was built for: writes arriving through a gateway are attested, writes arriving directly are claimed and hidden until a gateway confirms them, and the ledger tells them apart. A gateway reaches it with "upstream": { "url": "http://127.0.0.1:8790/mcp", "tokenEnv": "MEMORY_TOKEN" } in its config. Bind wider than loopback only behind the token. The test suite runs exactly this: one gateway writer, one direct writer, one ledger.

Writing policies in the receipts guide has six tested policies for the memory tools, from confining an agent to its team's space to a complete support-agent policy. Trust tiers are Cedar policies over the space and, through includeClaimed, over quarantine: permit(principal, action == Action::"memory.write", resource) when { context.args.space == "team:support" }; lets this agent write team memory and nothing else. A read's receipt carries, as observed, the exact facts returned, so the ids the agent relied on are already on the record.

--allow-direct lets the server take calls without a gateway; then source.receiptId is null and actor is whatever the caller said, recorded as such. examples/03-memory-behind-the-gateway.ts runs the whole loop, including a denied write and a retraction that cites its own receipt.

Certified forget

"Certified" means the receipt certifies what was done, not that the value exists nowhere. What forget reaches is the ledger and the stores with adapters, and it reports what each of them answered. It does not reach caches, a model's context window, application logs, replicas, backups, warehouses fed by export, or any store without an adapter. On Postgres the erased row image stays in the table until the store's VACUUM FULL runs, which it does after each forget unless told not to (the ledger). Say all of that plainly to whoever is relying on it, and read the per-store status in the result before calling anything gone.

A deletion demand is different from a correction. Retract keeps the record; forget erases the value. memory.forget removes the fact's value from the ledger file itself, stops believing the fact, and removes it from every store behind the server. What it keeps in place of the value is a choice, recorded on the event as digestKind: a plain sha256 of the value, which lets the ledger prove what it erased but is guessable for short values such as an email by anyone holding the file; an hmac-sha256 under a forget key the server holds outside the file (serve --forget-key-env NAME), which proves the same to anyone shown the key and nothing to anyone else; or none, with keepDigest: false, for the case where counsel wants nothing derived from the value retained. Use the keyed form unless you have a reason not to. The result says exactly what happened: erased from the ledger, removed from which stores, still held by which, with the digest, the actor, and the reason.

Retention and legal hold. memory.sweep forgets every fact the ledger learned of before an instant, in one space or all, and removes each from every store; it is retention as a receipted call, with the receipt as the record of what was erased. Retention windows live in the server: serve --retention 'org=P365D,team:*=P90D,user:*=P30D', ISO 8601 durations by space pattern, and a sweep with no before uses them per space. To run it on a schedule without a daemon, agent-custody-memory sweep --via retention-gateway.json --reason "quarterly retention" spawns that gateway and calls memory.sweep through it, so the sweep runs as the principal named in the gateway's grant, a retention job with the single scope memory.sweep, and its receipt records when retention ran, by whom, what it erased, and what a hold kept. Any cron, systemd timer, or CI schedule drives that one command. memory.hold puts a legal hold on a fact: while it stands, neither a deletion request nor a sweep can forget it, and memory.release lifts it. Holds and releases are events with actor, reason, and receipt, so the history of a fact shows the hold as plainly as the write. agent-custody-memory sweep --ledger ... --before ... --reason ... runs retention on the ledger file alone, for ledgers with no stores behind them.

Removal is verified, not assumed. A delete by id and a search index catching up are different moments. After every retraction, forget, or sweep, the server asks each store's own search whether the fact still surfaces, a few times with backoff, and records the outcome per store in the result and so in the receipt: verified (the store's search no longer finds it), stillIndexed (it still does, after the retries), unverified (the store cannot be asked, or the adapter has no search), or failed (the removal itself failed). Mem0 and Zep both verify through their search APIs; the tests drive the real clients against indexes that lag by a configurable number of searches. A certificate that says stillIndexed is an honest certificate; run forget again later, or read it as the store's problem to fix.

The pack. agent-custody-memory pack --ledger ... --receipts ... --fact <id> --out pack.json --sign keys/pack.key gathers everything about one fact into one signed artefact: its history with the receipt that produced each event, its holds and releases, its blast radius with every downstream receipt, and, if it was forgotten, the forget certificate with what each store answered. pack --verify pack.json --key keys/pack.pub --issuer-key keys/gateway.pub --principal-key keys/principal.pub checks the pack's signature and digest, every receipt inside it against the gateway's keys, that every event's receipt is present, and that the forget receipt names this fact and records the erasure. Touch one receipt inside and the whole pack fails. It is the artefact counsel attaches to a ticket; the reviewer needs the two public keys and nothing from you.

The certificate is the receipt. Through the gateway, memory.forget is a receipted call whose result the gateway observed, so the signed, logged receipt records that the erasure happened, who asked for it, and what the stores answered. Hand that receipt to whoever demanded the deletion; anyone with the gateway's public key can verify it.

Receipts are the other place a value lives: the receipt that recorded the write carries it in its request arguments, and the receipts of reads carry it in their results, signed and in a Merkle log. The receipts package's prune command is retention for the log: it replaces older leaves with their hashes, so every root and every proof still verifies while the content is gone, and removes the pruned receipts' bundle files. Run it on the same schedule as the sweep.

Blast radius

When a belief turns out wrong, the next question is what relied on it. Two records answer it together. Every gateway receipt carries consumed: the fact ids the agent had been shown, through the gateway, before that call, which the memory server declares on each read. Every fact in the ledger carries the receipt that wrote it. Walking forward from a fact through those two links gives every later call and every belief written in those calls, transitively, and whether the root was retracted and which derived beliefs are still believed.

bash
agent-custody-memory blast --ledger ./ledger.jsonl --receipts ./receipts --fact <factId>
fact 3f2a…: acct:42 plan = "enterprise" (space team:support, by support-agent, attested)
retracted at 2026-09-07T10:12:04.118Z by support-agent: CRM sync bug: account is on the free plan
3 call(s) made after the agent was shown it:
  2026-09-07T10:12:03.902Z  memory.write       executed  receipt 7c1e…
  ...
2 belief(s) written in those calls, 2 still believed:
  acct:42 discount = "20%"  fact 9b04…  STILL BELIEVED
  acct:42 support_tier = "priority"  fact e77d…  STILL BELIEVED

With the gateway fronting several upstreams, memory and the tools the agent acts with, the radius reaches the actions too: a refund issued after the agent read a belief carries that belief's id and is listed. examples/06-actions-on-beliefs.ts shows it.

It is an upper bound by design: a call made after the agent had seen the fact is in the radius whether or not the agent used it, because no receipt can prove what a model attended to. What it never misses is the thing that matters, a downstream action or belief that did depend on the fact. examples/05-blast-radius.ts runs the whole loop.

Explain one action

The question a security owner asks first is not about a fact but about an action: what is refund 183722, and can I trust the answer? agent-custody-memory explain --receipts receipts --receipt <id> --ledger ledger.sqlite --issuer-key keys/gateway.pub --principal-key keys/principal.pub answers it in the order it is asked:

WHO                          support-agent (attested, named in a signed grant)
WHO AUTHORIZED IT            user_456, grant signed by key bfa1c0d2e3f4, valid 2026-09-08T08:00:00.000Z to 2026-09-08T16:00:00.000Z
WHAT WAS ALLOWED             tools customer.lookup, stripe.refund
                             policy 9c1d2e3f4a5b decided allow
WHAT THE AGENT SAW           customer = {"verified":true,"plan":"pro"} (fetched by the gateway via customer.lookup)
                             1 belief(s) shown before this call:
                               acct:42 plan = "enterprise" [0b83d822]
WHAT IT DID                  stripe.refund {"customer_id":"cust_123","amount":845000} -> executed
WHY                          the policy permitted it
WHAT EVIDENCE                receipt 7f2e…, leaf 41 of a log whose head is signed by 3a9b0c1d2e3f
                             authorization committed as leaf 40, before the call was forwarded
CAN I VERIFY IT              VERIFIED, 23 checks
DID ANYTHING DEPEND ON THIS  1 belief(s) written in this call, 3 later call(s) made after seeing them, 2 belief(s) derived
WHAT NEEDS REVERSAL          2 belief(s) still believed, and 3 later call(s) to review

The first eight lines come from the receipt alone and work without a ledger; the last two are answered from the ledger, and without one they say unknown without a ledger rather than pretending nothing depended on the call. --out action.json --sign keys/pack.key writes the same answers as one signed action pack with the receipt and every downstream receipt inside it, and explain --verify action.json --key keys/pack.pub --issuer-key ... --principal-key ... checks the pack's signature and digest, the receipt, every downstream receipt, and that the written beliefs cite this receipt. Touch one receipt inside and the pack fails. The custody pack below is the same artefact seen from a fact instead of an action.

Write-through to the stores you already use

The ledger is not a retrieval store, and it does not try to be. src/stores.ts puts it under the ones teams already run: a fact written through the memory server also lands in every configured store, with its custody metadata (fact id, space, actor, provenance, receipt id), the store's own id is recorded on the fact, and a retraction reaches the store by that id. Certified forget will be built on this: a deletion is only real once it has reached the stores that serve recall.

ts
import { MemoryClient } from "mem0ai";
import { ZepClient } from "@getzep/zep-cloud";
import { Ledger, createMemoryServer, mem0Store, zepStore } from "@agent-custody/state";

const stores = [
  mem0Store(new MemoryClient({ apiKey: process.env.MEM0_API_KEY! }), { userId: "user_42" }),   // infer is off: the memory is the fact, verbatim
  zepStore(new ZepClient({ apiKey: process.env.ZEP_API_KEY! }), { userId: "user_42" }),         // or { graphId } for a shared graph
];
createMemoryServer(new Ledger("./ledger.jsonl"), { stores });

Order matters and is fixed: the ledger's checks run first, so a write it would refuse never reaches a store; the stores are written next, so their ids can be recorded; the ledger appends last. A store that refuses the write fails the write and nothing is recorded anywhere. On retraction the ledger goes first, since custody must not depend on a store being up, and a store that fails to remove is named in the error so the caller knows recall may still serve the value. The adapters are typed structurally and carry no runtime dependency on either vendor; the tests drive the real mem0ai and @getzep/zep-cloud clients against fake endpoints, offline.

Scoring memory mutations

src/evals.ts is a harness that scores a memory system on what goes wrong after writes, not on recall. A scenario is a script of writes, reads, supersessions, and retractions with the value a correct system returns at each read. The score counts stale reads (a value served after a correction was known), contradictions (two values for one subject and predicate at once), blast radius (reads that served a write the scenario marks as bad), and correct reads. Any system behind the small MemoryUnderTest interface can be scored; this ledger and a naive overwrite store ship as the two reference points, and examples/04-evals.ts prints both reports side by side.

ts
import { Ledger, ledgerUnderTest, runAll, SCENARIOS, formatReport } from "@agent-custody/state";
console.log(formatReport(await runAll(ledgerUnderTest(new Ledger("./ledger.jsonl")), SCENARIOS)));

From the shell, on a schedule:

bash
agent-custody-memory eval --baseline                                     # built-in scenarios, ledger and a naive store side by side
agent-custody-memory eval --scenarios incidents.json --sign keys/eval.key --out report.json   # your own incidents, signed report
agent-custody-memory eval --verify report.json --key keys/eval.pub       # what a reviewer runs

A scenario file is { "version": "0.1", "scenarios": [{ "name", "ops": [...] }] } with the same write, read, and retract ops as the built-ins; every read carries expect, null meaning nothing. The file is validated and a bad one names the offending op. The signed report is an in-toto statement over the scores, bound to a digest of the scenarios it ran, in the same envelope format as receipts; eval --verify checks the signature and the digest. eval exits non-zero when the ledger scores below perfect on the scenarios it was given, so a cron job that runs it fails loudly when a change regresses memory behaviour.

The ledger

src/ledger.ts keeps an append-only log of events. It holds none of them itself: every question is a query to a store, so the store decides how large a ledger can be and who shares it. A store answers four questions, everything in order (export, audits), everything about the facts matching a filter (a bitemporal read), everything that touched one fact (its history and its checks), and which spaces exist (retention), and it does three things: append, replace one assert in place (forget), and compact after erasures. Three stores ship, and the whole ledger suite runs against each of them, so they answer identically.

JSONL is the default: one event per line, readable by anyone, the file you copy for an audit. It answers from memory, which is fine to tens of thousands of events. SQLite, chosen by a path ending in .sqlite or .db, is for durability on one machine: transactional writes with write-ahead logging, an in-place forget that overwrites the erased value (secure delete, then a truncating checkpoint so nothing lingers in the write-ahead log), and every query answered by an index on fact, time, space, subject, predicate, and supersession. Node ships the SQLite module, so there is no native dependency; a ledger written by an earlier version gets the index columns filled from its JSON the first time it is opened. Measured on a million-event ledger: open in 0.2 s, a subject read in about 1 ms, a fact's history in well under a millisecond, the spaces for a sweep in 2 ms. Postgres, chosen by a postgres:// URL, is for a shared ledger: several memory servers on one table, in the database your security team has already approved, the same queries pushed down as SQL. It needs the pg package installed beside this one; ?table=custody.events names the table and ?vacuum=false skips the vacuum described below. In code, pass a PostgresStore around your own pg Pool, with whatever TLS and credentials you already use. The adapter is tested against the real Postgres engine in-process through PGlite, including that a forgotten value is absent from the database files, and checked by hand against Postgres 16.

Forget on Postgres is an UPDATE, and MVCC keeps the old row image in the table until vacuum. So after a forget or a sweep the store runs VACUUM FULL on the table, which rewrites it without the old image. That takes an exclusive lock for the rewrite; a very large ledger may set vacuum: false and run its own schedule, and the forget certificate then rests on that schedule. The write-ahead log, replicas, and backups keep their own copies for as long as their retention says; that is true of every database, and the honest reading of a forget certificate is that the live ledger no longer holds the value.

Choose JSONL for one server process and for anything an auditor should read with cat. Choose SQLite when one machine serves the ledger over HTTP, when a crash between two writes must not cost you an event, or when a deletion demand must leave no trace of the value in the file. Choose Postgres when more than one server must share the ledger, or when the ledger belongs in the database you already operate. Warehouses are not stores: Snowflake and Databricks keep deleted rows for days by default and are built for scans, not one small write per agent call. Feed them with agent-custody-memory export --ledger postgres://… --out ledger.jsonl, which writes the auditable JSONL from any store, and query the ledger where your compliance team already works.

The log holds these kinds of event.

  • assert creates a fact: subject, predicate, value, the space it lives in (a person, a team, an org), the actor that wrote it, the source receipt id if the write went through a receipts producer, and the valid-time interval. An assert may supersede an earlier fact, which ends that fact's validity where the new one begins.
  • retract says a fact should never have been believed. This is the undo. The record stays, so queries about earlier moments still see the fact, and anything the retracted fact had superseded is believed again.

Two clocks, kept apart on purpose:

question it answersset by
valid timewas this true in the world at Tthe writer, defaults to now
transaction timedid the ledger know it at Tthe ledger, never the writer

asOf({ validAt, txAt, space, subject, predicate }) returns the facts believed at a moment. Setting both times to the same instant answers "what did the agent believe on Tuesday", including beliefs later retracted. history(factId) returns every event that touched a fact.

The ledger refuses to supersede a fact that is unknown, already superseded, or retracted, and refuses a replacement whose validity starts before the fact it replaces.

Layout

src/ledger.ts   the fact record, the event kinds, as-of queries, supersession, retraction, forget, holds, sweeps
src/storage.ts  the store interface and its three stores: JSONL (default, auditable), SQLite (indexed, one machine), Postgres (shared); chosen by path or URL
src/server.ts   the ledger as MCP tools; source and actor taken from the gateway's _meta
src/http.ts     the memory server over Streamable HTTP with bearer auth, for a shared ledger
src/explain.ts  one action explained: the ten answers from a receipt and the ledger, and the signed action pack
src/pack.ts     the custody pack: build, sign, verify, format
src/cli.ts      agent-custody-memory serve (stdio or --http, with --retention and --forget-key-env), sweep (ledger-only or --via a gateway), eval, explain, pack, export, blast
src/blast.ts    blast radius: from receipts' consumed facts and the ledger's source receipts, forward
src/stores.ts   write-through adapters: Mem0 and Zep, and the Store interface for others
src/evals.ts    the memory-mutation harness: scenarios, scoring, report
src/evals-ledger.ts  the ledger and a naive overwrite store behind the harness interface
src/evals-file.ts    scenario files, validated; signed eval reports and their verification
src/index.ts    public surface
examples/       runnable walkthroughs, each ends with OK and is run by the test suite
test/           one test per question a platform owner asks after a memory incident
tsconfig.build.json  emits dist/ for consumers; the repo itself runs the .ts directly

Plan

Done

  • Bitemporal fact ledger with supersession, retraction, as-of and history queries, persisted as JSONL, SQLite, or Postgres behind one store interface that answers every query from an index, with export back to JSONL.
  • The memory server: the ledger as MCP tools behind the receipts gateway, with the source receipt id and the attested actor supplied by the gateway, policy over spaces, and a denial receipt for every refused write.
  • Forget digests are keyed under a server-held secret, or absent on request, so an erased value cannot be guessed back from the file.
  • Retention windows per space in the server, sweeps that default to them, and a sweep --via trigger that runs retention through a gateway as a named principal, on any timer.
  • Explain one action: from a receipt id, who, who authorized it, what was allowed, what the agent saw, what it did, why, the evidence, whether it verifies, what depended on it, and what needs reversal; the same as a signed action pack that carries every downstream receipt.
  • The custody pack: a fact's history with receipts, holds, blast radius, and forget certificate as one signed artefact, verified as a whole.
  • Removal verification: after a retraction, forget, or sweep the server asks each store's search whether the value still surfaces and records verified, stillIndexed, unverified, or failed per store, in the receipt.
  • Retention and legal hold: a receipted sweep forgets what was learned before an instant and reaches the stores; a hold refuses forget and sweep until released, as events on the fact's history.
  • Value-level quarantine: a write may cite a fact the gateway fetched itself; the value must match it and the fact is then verified, the provenance level above attested, or the write is refused.
  • Attested executions: with a key, the memory server signs its results for the gateway's receipt, so a verifier holding its public key sees memory calls as attested.
  • The memory server over HTTP: one ledger shared by several gateways and direct writers, bearer-token auth, quarantine live.
  • Certified forget: memory.forget erases a value from the ledger file keeping its digest, stops believing it, removes it from every store, and reports exactly what happened; the gateway's receipt of that call is the certificate.
  • Policy over provenance: the gateway looks up the fact a write supersedes or a retraction targets, so policy decides on its space, actor, and provenance; a claimed fact can be displaced, an attested org fact cannot.
  • Consumed facts and blast radius: the memory server declares the facts it serves, the gateway records them on every later receipt, and blast walks from a fact to every downstream call and derived belief, transitively, with its retraction status.
  • Write-through adapters for Mem0 and Zep: every write lands in the store with custody metadata, the store id is recorded on the fact, retractions reach the store, and failures are ordered so nothing is half-recorded.
  • The eval CLI: built-in or custom scenario files, the naive baseline beside the ledger, a signed report a reviewer verifies, and a non-zero exit on regression, for cron.
  • The memory-mutation eval harness: stale reads, contradictions, blast radius, and correct reads over scripted incidents, scored the same way for the ledger and for anything behind the same interface.
  • Quarantine: facts carry attested or claimed provenance; claimed facts are hidden from reads by default and a gateway-only memory.confirm lifts them, as a recorded event.

Next, in the order it pays off

  1. The hosted plane, behind early access: tenanted log, then memory, then reports and a control plane, with SSO, SCIM, residency, and SIEM export. Issue #6.
  2. Write-through adapters for Letta, LangMem, and Cognee, one per user who asks. Issue #4.