Benchmarks
emem-membench scores a running emem responder against a
LongMemEval-S / MemoryAgentBench-style memory benchmark and emits a signed
JSON scorecard. It has two modes:
--self-test: grades an in-memory stub against a built-in synthetic fixture. No network, no responder. Exercises the scoring code in CI.--live --url <responder>: loads a dataset corpus into a running responder over its real write API, answers every query over the real read API, and computes the four axes + topline from the responder’s own output. No score is hardcoded.
Methodology
Each dataset item is {id, content, query, expected_answer} (with optional
update/update_answer and conflicts_with). The live path:
- Load (write API). Each item’s
contentis written as one signed memory file viaPOST /mcp→tools/call→memory_create(/memories/membench/<id>.md,kind: "fact"). Every create returns a server-signed receipt with afile_cid. - Pick a read path. A one-shot probe of
POST /v1/memory/searchreportsmodel_loaded. If the BGE embedder is loaded, retrieval usesmemory_search(semantic). If not (the default offline build ships no embedder weights), retrieval falls back torecall_fallback: list the loaded files viamemory_viewand rank them with a lexical token-overlap scorer. The chosen path is recorded asretrieval_path; a fallback run is never presented as a semantic-search run. - Score four axes from real retrieval output, with the standard
answer-recall criterion (the retrieved content contains the
ground-truth answer, case/whitespace-normalised, numerically canonical):
- retrieval_accuracy: query every item, grade the retrieved content.
- test_time_learning: for items with an
update, rewrite the same path (last-write-wins), re-query, require the post-update answer. - long_range_understanding: re-query the earliest-loaded third of the corpus (front of the transcript), where a recency-biased store would have dropped the needle.
- conflict_resolution: store a disagreeing companion under a second
path and require the responder to surface the disagreement. The
responder’s
/v1/memory_contradictionsscan is tried first (conflict_method: "contradiction_scan"); since that index scans EO-fact(cell, band, tslot)triples and not free-text memory files, the honest text-surface check is whether both disagreeing values were stored non-destructively and read back distinct (conflict_method: "stored_distinct_fallback").
- Topline.
longmemeval_toplineis the item-weighted fraction of all graded questions answered correctly across the four axes, the LongMemEval-S convention.
The scorecard is written to var/benchmarks/membench-live.json and printed
to stdout, with the responder’s signed receipt embedded.
Honesty: SAMPLE vs FULL
A small SAMPLE dataset (crates/emem-membench/data/sample-longmemeval.jsonl,
~15 items) is committed so --live runs end-to-end with no download. Its
score is labelled dataset_provenance: "sample" and is illustrative
only, not the published benchmark number. A user-supplied dataset is
labelled "full". Never quote a sample score as the published result.
Run --live against a local responder
export CARGO_TARGET_DIR=/path/to/shared/target # optional, to share builds
# 1. Boot a fresh in-memory responder.
cargo build -p emem-cli --bin emem-server
EMEM_DATA=:memory: EMEM_BIND=127.0.0.1:5087 EMEM_OVERTURE_SKIP_WARMUP=1 \
./target/debug/emem-server &
# 2. Score it against the committed sample (no download).
EMEM_URL=http://127.0.0.1:5087 \
cargo run -p emem-membench -- --live \
--dataset crates/emem-membench/data/sample-longmemeval.jsonl
# 3. Or score against the full public dataset (see below).
EMEM_URL=http://127.0.0.1:5087 \
cargo run -p emem-membench -- --live --dataset /path/to/longmemeval-s.jsonl
--url overrides $EMEM_URL (default http://127.0.0.1:5051). Omit
--dataset to use the committed sample.
Get the full dataset
The loader reads JSON Lines, one item per line, in the canonical emem schema with aliases for the two public corpora:
{"id":"q1","content":"...stored memory...","query":"...question...","expected_answer":"...","update":"...optional...","update_answer":"...","conflicts_with":"...optional second value..."}
Accepted aliases: id ← question_id/qid; content ←
context/text/memory; query ← question; expected_answer ←
answer/expected.
- LongMemEval (https://github.com/xiaowu0162/LongMemEval). The
LongMemEval-Ssplit ships as JSON of objects withquestion_id/question/answeroverhaystack_sessions. Flatten each item’s relevant session text intocontentand emit one JSONL line per question. Dataset: https://huggingface.co/datasets/xiaowu0162/longmemeval. - MemoryAgentBench (https://github.com/HillZhang1999/MemoryAgentBench).
Items carry
context/query/answer; map directly onto the schema above (the loader already acceptscontextandansweras aliases).
A line that fails to parse is a hard error with its line number; rows are never silently dropped (a dropped row would inflate the score).
Scorecard from the committed SAMPLE run
Produced by the run in this repo against a fresh :memory: responder
(emem-server on 127.0.0.1:5087), dataset = the committed sample,
read path = recall_fallback (the offline build ships no BGE embedder, so
retrieval is lexical, not semantic). This is a SAMPLE score: illustrative
only, not the published LongMemEval/MemoryAgentBench result.
{
"mode": "live",
"dataset_provenance": "sample",
"corpus_items": 16,
"retrieval_path": "recall_fallback",
"conflict_method": "stored_distinct_fallback",
"scorecard": {
"retrieval_accuracy": { "score": 0.6875, "items": 16, "correct": 11 },
"test_time_learning": { "score": 0.5, "items": 2, "correct": 1 },
"long_range_understanding": { "score": 0.6, "items": 5, "correct": 3 },
"conflict_resolution": { "score": 1.0, "items": 2, "correct": 2 },
"longmemeval_topline": 0.68
}
}
The full scorecard embeds the responder’s signed receipt
(primitive: "emem.memory_file", with signature, fact_cids,
responder, schema_cid) so the run is independently verifiable. The
recall_fallback retrieval and learning scores reflect a purely lexical
ranker; running against a responder with the BGE embedder loaded switches
the read path to semantic memory_search and is expected to score higher.
Measured system performance
Micro-benchmarks against the production responder at emem.dev, run on the
serving host itself over loopback (no WAN in the numbers), 2026-07-11,
binary built from commit f4946e9. Host: 30 vCPU / 216 GB RAM /
network-attached block storage, the same machine that answers public
traffic, so background load is included rather than idealised away.
Client wall-clock timing from a Python httpx client; the measurement
script method is stated per row. These are single-node numbers; no
scaling claim is made.
| Measurement | Result | Method |
|---|---|---|
| Warm recall latency | p50 2.5 ms · p95 6.1 ms · p99 9.1 ms | POST /v1/recall, one attested cell, one band, n=200 sequential |
| Warm recall with provenance filter | p50 2.4 ms · p95 4.2 ms · p99 5.3 ms | same call with deterministic: true, n=200; the filter adds no measurable overhead |
| Cold recall (auto-materialize) | 0.5 s to 1.6 s (n=3) | fresh never-attested cells (Siberia, Sahara, Australian interior), copdem30m.elevation_mean; dominated by the upstream Copernicus fetch, then signed and persisted |
| Receipt verification, server side | p50 1.0 ms · p99 4.2 ms | POST /v1/verify_receipt, n=100 |
| Receipt verification, offline | p50 0.13 ms · p99 0.17 ms | pure-Python blake3 preimage-v1 + ed25519 check, no network, n=100 |
| Token dereference | p50 1.2 ms · p99 3.0 ms | POST /v1/memory_token/resolve, n=100 |
| Sustained read throughput | 632 requests/s | 8 concurrent clients x 50 warm recalls, single node, loopback |
Not yet measured, tracked as open evaluation work: multi-node scaling, storage bytes per fact under compaction, deduplication ratio, cache-hit ratio under a realistic access mix, and a head-to-head against spatial databases and geospatial data infrastructures on the same queries. Numbers above will drift with hardware and load; re-run the method column against your own responder rather than quoting these as universal.
Failure modes, typed
Reviewers ask for failure modes; emem’s are enumerated closed sets on the wire rather than prose, so they are testable:
- Absence reasons (a missing value is a signed answer, never a bare
404):
unavailable_capability,outside_coverage,gpu_unavailable,archetype_seed_unavailable,no_auto_materializer_registered,present_only. - Change-ensemble degradation (
/v1/triple_consensuscarriesdegraded,degraded_reason, and per-encoderreason_code):gpu_sidecar_unavailable,single_vintage,outside_coverage,no_finite_overlap,recall_failed,partial_consensus_N_of_3,insufficient_encoders. A 2-of-3 result reportsdegraded: trueeven though it carries a real ensemble number. - Request errors are typed (
invalid_argument,band_not_in_registry,invalid_temporal_bound,invalid_signed_at_format, …) and teach the accepted vocabulary in the message. - Process level: a watchdog restarts the responder if the runtime stalls; receipts are content-addressed, so a restart never changes what a token resolves to.
Agreement statistics for the change ensemble
Reviewers asked for agreement statistics, so here is a first, small,
fully stated sample rather than a claim: the 15 named places used across
the site’s own demos and world presets, run through
POST /v1/triple_consensus against the production responder on
2026-07-11. The sample is site-chosen and small; it characterises the
instrument on this node, not global model behaviour.
| Outcome | Count |
|---|---|
| Computed, all three encoders | 9 |
Computed, degraded 2-of-3 (partial_consensus_2_of_3) | 5 |
| Failed before the ensemble (geocoder miss on “Sao Paulo”) | 1 |
Change claimed (all legs over the 0.15 gate) | 0 |
one_or_none (zero or one leg over the gate) | 14 of 14 computed |
Ensemble change indices (mean of per-encoder 1 - cosine between the
two latest vintages) ranged 0.047 (Borneo) to 0.579 (Interlaken). No
place cleared the all-legs rule, which is the expected null result for
stable landmarks compared year over year; the two highest means
(Interlaken 0.579, Mumbai 0.405) show single encoders firing without
corroboration, exactly the case the all-legs rule exists to hold back.
The 5 degraded runs are the honest cost of sidecar-gated encoders on a
cold vintage: the response says so in a typed degraded_reason instead
of averaging over the gap. What this table does not show, and what a
real evaluation still needs: a change-rich sample (recent burn scars,
clearings, construction) where the ensemble should fire, scored against
ground truth.