A destructive merge can only be undone by keeping what went into it

OntoMem

A Python memory that merges extractions into one record per composite key, with a source ledger of pre-merge inputs so removing a document re-merges the survivors rather than leaving residue.

Carries 0 of 7 rubric mechanisms. Most systems here carry none or one (41%), and a dash means the mechanism was not found at this commit — not that the system needed it. Each mark is one LLM reviewer's reading of the code at this commit rather than a run of it — known limits.

  • Tombstone
  • Trust state
  • Bi-temporal
  • Scope enforced
  • Mutation audit
  • Human review
  • Negative evals

1. Executive Summary

OntoMem is "The Self-Consolidating Memory" — Apache-2.0, Python, version 0.6.0, 5,774 lines across 36 files, published to PyPI. Its pitch is a contrast: "[g]ive your AI agent a 'coherent' memory, not just 'fragmented' retrieval." Rather than appending extractions and ranking them later, it merges each new extraction into the existing record for a composite key — user_id + date, say — so what the store holds is one consolidated object per key rather than a pile of observations about it.

That design creates a problem, and the interesting part of this repository is its answer. A destructive merge cannot be undone from the merged result: once three documents have been folded into one record, nothing in that record says which of them contributed what, so removing one document means deleting everything it touched. The source ledger exists for exactly that, and says so:

"because merges are destructive, the only way to remove a source's contributions precisely is to re-merge the surviving sources' raw results for the affected keys."

With track_sources=True, each document's raw pre-merge extraction is kept, and remove_source(strategy="exact") recomputes every affected key from the survivors while deleting only the keys nothing else contributed to. The coarse strategy is kept and named rather than quietly avoided — strategy="touched" deletes every key the source touched, which is what a store without a ledger has to do. The cost is stated as a number, "~1.5-2x storage overhead", and bounded deliberately: only the current version per source is retained.

Two smaller mechanisms are worth lifting. The FAISS index writes an index.meta.json companion recording the embedder signature and refuses vectors from a different embedding space, which converts a silent similarity-is-nonsense failure into a refusal at load. And edit() — removing one wrong fact from a record without rewriting the rest — validates key-invariance and offers a dry run, so a semantic edit cannot quietly move the record to a different key.

What is absent is epistemic state. A merged record carries no status, no validity interval, no recorded-at, no confidence and no supersession link. The store answers with the current merged value for a key and has no vocabulary for a claim that has stopped being true, one nobody has checked, or one a person corrected. That is a coherent choice for the consolidation problem it is solving and it is the reason this report carries no capability marks.

2. Mental Model

A key is the unit, and the record under it is always current.

A merge is destructive, which is why the inputs are kept.

A rollback is a re-merge of whoever is left.

An index refuses vectors that came from a different embedder.

Diagram — extractions from several documents merge destructively into one record per composite key, and the source ledger keeps each document's raw pre-merge results so removing one re-merges the survivors rather than deleting every key it touched
Diagram source
%% caption: extractions from several documents merge destructively into one record per composite key, and the source ledger keeps each document's raw pre-merge results so removing one re-merges the survivors rather than deleting every key it touched
flowchart TB
    D1["document A"] --> EX["extraction into a caller-supplied<br/>Pydantic schema"]
    D2["document B"] --> EX
    D3["document C"] --> EX
    EX --> LEDGER[("source ledger, when track_sources=True —<br/>each source's RAW pre-merge results,<br/>current version only, ~1.5-2x storage")]
    EX --> MERGE{"merge into the record for<br/>composite key user_id + date"}
    MERGE --> STORE[("one merged record per key —<br/>no status, no validity, no confidence,<br/>no supersession link")]
    STORE --> IDX[("FAISS index + index.meta.json<br/>recording the embedder signature")]
    IDX -.->|"a vector from a different embedding space<br/>is refused at load rather than silently<br/>producing nonsense similarity"| SAFE["a silent failure made loud"]
    IDX --> SEARCH["vector search, plus O(1) secondary lookups;<br/>optional source_ids and tags filters,<br/>union semantics across contributors"]
    SEARCH -.->|"the filters are the caller's, supplied per query —<br/>no stored key is applied on the caller's behalf"| NOSCOPE["no scope-enforced mark"]
    RM["remove_source(source_id)"] --> STRAT{"strategy"}
    STRAT -->|"exact (default)"| EXACT["re-merge the affected keys from the<br/>SURVIVING sources' raw results;<br/>delete only keys nobody else contributed to"]
    STRAT -->|"touched"| COARSE["delete every key the source touched —<br/>what a store without the ledger must do"]
    LEDGER --> EXACT
    EXACT -.->|"'because merges are destructive, the only way to<br/>remove a source's contributions precisely is to<br/>re-merge the surviving sources' raw results'"| WHY["the ledger's whole reason"]
    EXACT --> STORE
    RM -.->|"nothing is keyed on what was removed, so re-adding<br/>the same document restores the same claims<br/>with no record that it was ever dropped"| NOTOMB["no tombstone mark"]
    EDIT["edit(): remove one wrong fact,<br/>keep the rest"] -.->|"key-invariance validated, dry run available,<br/>vector refreshed in place"| STORE

3. Architecture

Area Role
ontomem/core/omem.py The store: add, merge, search, index maintenance, rollback
ontomem/core/sources.py The ledger entry, and the argument for keeping it
ontomem/merger/classic_merger Deterministic merging
ontomem/merger/llm_merger Model-assisted merging and semantic editing
tests/ Unit, integration and fixture suites over the merge and rollback paths

4. Essential Implementation Paths

ontomem/core/sources.py:1-9 — why a consolidating store has to keep its inputs.

ontomem/core/omem.py:936-948 — the two rollback strategies, and what each costs.

ontomem/core/omem.py:1264, :1282-1299, :1354-1355 — the embedder signature written and checked.

5. Memory Data Model

One merged record per composite key, shaped by a Pydantic model the caller defines, with secondary lookups mapping custom keys to primary keys by reference rather than by copying data. When the ledger is on, each contributing document's raw extraction sits beside the merged result, one current version per source.

6. Retrieval Mechanics

Vector search over the merged records with FAISS, O(1) secondary lookups for queries by a non-primary attribute, and an optional restriction to named source ids or tags — matching a key when any of its contributing sources fits the filter, which is the right semantics for attribution and the wrong one for isolation.

7. Write Mechanics

add(items, source_id=...) extracts and merges. sync_index() patches the vector index in place rather than rebuilding it, suspended_index() batches mutations without dropping it, and upsert_source() replaces a document in one step. Merging is either deterministic or LLM-assisted, chosen by the caller.

8. Agent Integration

None beyond the library: no server, no MCP surface, no hooks. It is a component for a pipeline that already knows when to write.

9. Reliability, Safety, and Trust

The strong parts are the ledger, the embedder signature check, and the key-invariance validation on a semantic edit. The absent part is any notion of a claim's standing — the store holds the current merged value and has no way to say that a value is suspect, expired, or corrected rather than simply replaced.

10. Tests, Evals, and Benchmarks

Unit, integration and fixture suites run in CI, covering merging, index maintenance and the rollback strategies. There are no committed cases asserting that particular material must not be retrieved, which follows from there being no read path that withholds.

11. For Your Own Build

If your write path merges, keep the inputs. The moment you consolidate, you have given up the ability to remove one contributor unless you stored what each one contributed — and the day you need that is the day somebody asks you to delete a document.

Name the coarse fallback and ship it beside the precise one. strategy="touched" is what everyone else does by necessity; having both makes the cost of the precise path visible.

Write your embedder's signature next to your index. A vector store silently mixing two embedding spaces returns plausible nonsense, and a signature turns that into a refusal.

12. Open Questions

Whether a status field is wanted on a merged record. The consolidation model assumes the newest extraction is the best one, which holds for daily snapshots and does not hold for a fact somebody later disputes.

Whether the ledger should become append-only. Keeping one current version per source bounds the overhead and is what makes rollback exact; it also means the store cannot say what a key looked like before the last merge, which is a question the same users will eventually ask.

Appendix: File Index

Path What to read it for
ontomem/core/sources.py:1-9 The clearest statement of why a merging store needs a ledger
ontomem/core/omem.py:936-948 Exact rollback beside the coarse one it replaces
ontomem/core/omem.py:1282-1299 An embedder signature that makes a silent failure loud

History

2026-09-16154bf488… — first reading, at a commit dated 4 September 2026. Screened before opening, from a shallow clone: four files scanned, no auto-run surfaces, one build-time execution point, no unpinned surfaces and nothing inside the seven-day cooldown, with uv.lock present. Nothing was installed, built or run, and no embedding model was loaded.