Skip to main content
Glama
README.md
# Mnemochain

A memory, ledger, reflection, replay, **and citation-backed semantic
recall** layer for AI agents — built as **one data structure**, not six
separate systems. Runs entirely on your own machine. **$0 to build, $0 to
run, no server, no API key required.**

*(This project used to be two separate ones — Chronicle, the log/ledger/
replay layer, and agentmem, a hallucination-resistant memory library. They
merged, with agentmem's trust layer becoming just another view on
Chronicle's log — a genuine simplification, not just glue code, see "Why
merging simplified things" below. The merged project was renamed to
Mnemochain afterward: "Chronicle" was already taken by several other
published projects on PyPI and elsewhere, and mnemo- [memory] + chain
[the hash chain] describes what this actually is more precisely anyway.)*

## The idea

An agent's entire history is a single append-only, hash-chained log
(think "git for agent cognition"). Every other capability is just a view
on that log:

| Capability | How it's implemented |
|---|---|
| **Memory (raw)** | `entries` table in SQLite — every action/observation is a row |
| **Memory (semantic, cited)** | `semantic.SemanticView` — TF-IDF (or swap in real embeddings) recall over the log, every result carries its source; `remember()` / `recall()` |
| **Paraphrase-augmented recall** | optionally store 3-5 alternate phrasings of each fact so recall() also matches differently-worded queries — a local Ollama model if you have one, or a zero-dependency synonym swap if you don't; only the original fact is ever cited — `paraphrase.py` |
| **Hallucination checking** | `verify_claim()` — confidence tier (high/medium/low/none), not just yes/no, plus a warning if the fact is contested |
| **Contradiction detection** | two active memories that disagree get flagged automatically, even with no explicit key linking them |
| **Verifiable ledger** | each row's hash covers its content + the previous row's hash (plain SHA-256, no blockchain, no gas fees) — `ledger.verify_chain()` |
| **Reflection** | a scheduled job folds recent entries into a distilled "lesson," using a local model if you have one, or a zero-dependency rule-based summarizer if you don't — `reflect.reflect()` |
| **Skill-sharing** | lessons are just JSON — export them, commit to a git repo or drop in a shared folder, another agent imports them — `skills.py` |
| **Replay / rollback** | the log is immutable, so "rollback" is a read-only view of an earlier point, never a delete — `replay.fold()`, `replay.view_as_of()` |
| **Cost control** | a rule-based router defaults every task to the local backend; it only touches a paid API if you explicitly flip `allow_api=True` — `router.py` |

**Important naming distinction:** `ledger.verify_chain()` checks the log
hasn't been *tampered with*. `semantic.SemanticView.verify_claim()` checks
whether a *fact is actually true according to memory*. These are
deliberately different names for deliberately different kinds of trust —
don't confuse them.

## Why merging simplified things, not just combined them

agentmem used to track supersession with an explicit `superseded_by` field
that got mutated in place. Mnemochain's log is append-only — nothing can be
mutated. So "the active fact for key X" is just **the latest log entry with
that key**, derived fresh each time. No mutation tracking needed at all;
the append-only design gives you supersession for free.

It also structurally fixed a real bug: the standalone agentmem had a
cached TF-IDF vocabulary that could go stale between calls (see
`docs/ROADMAP.md` for that story). `SemanticView` caches nothing between
calls — every `recall()`/`verify_claim()` re-derives from the log — so that
whole bug class is now impossible by construction, not just patched.

## Why this stays at $0

- Storage: SQLite (stdlib, no server).
- Hashing: `hashlib.sha256` (stdlib, no blockchain fees).
- Semantic recall: TF-IDF via scikit-learn by default (one local install);
  swap in `sentence-transformers` for real embeddings, still local and free.
- Reflection: [Ollama](https://ollama.com) running a local open-weight
  model if installed; otherwise an offline rule-based summarizer that
  ships with zero dependencies and always works.
- Paraphrase generation: same local-first pattern — Ollama if reachable,
  otherwise a zero-dependency synonym swap. Either way, a few extra
  sentences of SQLite storage per fact; nothing like the cost of training
  or fine-tuning anything.
- Sharing: a git repo or plain file copy — no hosting bill.
- MCP server: `pip install "mcp[cli]"` — one local install, still $0, no hosting.
- The only code path that can ever cost money (`backends/api.py`) is
  intentionally unimplemented and inert until you wire a key yourself.

**Packaging note:** `import mnemochain` alone stays stdlib-only — it does
NOT pull in numpy. Only `from mnemochain import semantic` (or running the
CLI's semantic commands / the MCP server) needs `pip install numpy
scikit-learn`. This was almost broken during the merge — an earlier draft
eagerly imported `semantic` from `__init__.py`, which silently forced
numpy onto every user of the base log/ledger/replay functionality. Caught
by testing the import with numpy blocked before shipping.

## Quickstart

```bash
python examples/quickstart.py          # the original log/ledger/reflect/replay demo
python examples/semantic_demo.py       # the new citation/verify/contradiction demo
```

Or via the CLI:

```bash
python -m mnemochain.cli add agent-1 action '{"task": "search papers"}'
python -m mnemochain.cli reflect agent-1
python -m mnemochain.cli verify                                    # tamper check
python -m mnemochain.cli export lessons.json
python -m mnemochain.cli import lessons.json agent-2

# semantic commands (need: pip install numpy scikit-learn):
python -m mnemochain.cli remember agent-1 "The user's name is Priya." --key user.name
python -m mnemochain.cli recall "what's the user's name?"
python -m mnemochain.cli verify-claim "The user's name is Priya."   # fact check, not tamper check
python -m mnemochain.cli contradictions

# store alternate phrasings too, to improve recall against different wording:
python -m mnemochain.cli remember agent-1 "The user moved to Munich." --key user.location --paraphrase
python -m mnemochain.cli recall "where does the user live?"
```

## Using it as an MCP server

Needs one local install with network access — still $0, no hosting:

```bash
pip install "mcp[cli]"
python -m mnemochain.mcp_server
```

Claude Desktop config (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "mnemochain": {
      "command": "python",
      "args": ["-m", "mnemochain.mcp_server"]
    }
  }
}
```

Exposes: `remember` (with an optional `paraphrase` flag), `recall`,
`verify_claim`, `list_contradictions` (semantic memory) plus `verify_chain`,
`reflect` (audit + distillation) — one server, both capabilities. Every
tool call opens its own fresh SQLite connection rather than sharing one
cached globally (see "Bugs found and fixed" below for why that matters).
Everything persists to a single SQLite file (default `./mnemochain.db`,
override with `MNEMOCHAIN_DB_PATH`) — that IS the durable store, no
separate save/load step.

`mcp_server.py` is directly tested via `MCPServer.call_tool()`/
`.list_tools()` in `tests/test_mcp_server.py` (needs `pip install
"mcp[cli]"`, skips itself gracefully otherwise) — not just the `do_*`
functions it calls. That closed a real gap: an earlier version of this
project had no network access to install `mcp` at all, so the server file
had never actually been run. The first real install surfaced two genuine
bugs on the very first try (see below) — worth stating plainly, since
"never run" and "no bugs" are different claims and it would have been easy
to conflate them.

## Run the tests

```bash
python -m unittest discover -s tests -v
```

62 tests with every optional extra installed (numpy, scikit-learn,
`mcp[cli]`) — 55 without `mcp`, which `test_mcp_server.py` detects and
skips gracefully rather than failing. The base log/ledger/reflect/replay
suite needs neither.

## Project layout

```
mnemochain/
  log.py          # append-only hash-chained log
  ledger.py       # chain verification / tamper detection
  reflect.py      # distill entries into lessons
  skills.py       # export/import lessons between agents
  replay.py       # fold/replay, checkpoints, rollback views
  router.py       # local-first cost router
  mirror.py       # JSONL durability mirror, rebuild a lost DB from it
  identity.py     # HMAC / Ed25519 signing
  embeddings.py   # pluggable embedding backends (TF-IDF now, real embeddings later)
  semantic.py     # citation-backed recall, verify_claim, contradiction detection
  paraphrase.py   # optional alternate-phrasing generation to widen recall()
  mcp_tools.py    # MCP tool logic, fully tested, zero mcp-package dependency
  mcp_server.py   # MCP protocol wiring, directly tested via call_tool()
  cli.py          # command-line interface (base commands + semantic commands)
  backends/
    rule_based.py   # zero-dependency fallback (always available)
    local.py        # Ollama backend (free, local)
    ollama_client.py # shared HTTP client for the local Ollama server
    api.py          # optional paid backend (inert by default)
tests/
  test_basic.py
  test_robustness.py
  test_semantic.py
  test_paraphrase.py
  test_mcp_server.py   # skips itself without `pip install "mcp[cli]"`
examples/
  quickstart.py
  semantic_demo.py
docs/
  ROADMAP.md          # full phase-by-phase history, what's done, what's next
```

## Robustness

Hardening passes on the base log design, plus the merge itself, each with
its own tests:

| Concern | What was added | Honest limit |
|---|---|---|
| **Concurrency** | WAL journal mode + 30s busy_timeout on every connection | Doesn't make concurrent writers coordinate — just stops them crashing on lock contention |
| **Reflection & paraphrase reliability** | `ollama_client.py` talks to Ollama's local HTTP API directly instead of shelling out to `ollama run`, with a single `BackendUnavailable` on any network/timeout/error condition | Still falls back to the rule-based summarizer / synonym swap, which is much less rich than a real model |
| **MCP server concurrency** | Every tool call opens its own SQLite connection instead of sharing one cached global | A cached connection crashed the moment two calls landed on different worker threads — see "Bugs found and fixed" below |
| **Authorship** | `identity.HmacSigner` (zero-dep) signs/verifies per agent; `verify_signatures()` checks a row's claimed author | HMAC uses one shared secret per agent — forgeable by anyone who also holds that secret. For strangers, use `identity.Ed25519Signer` (`pip install cryptography`) |
| **Scale** | `log.get_page()` for pagination; `ledger.verify_incremental()` re-walks only entries since the last checkpoint | Incremental verification **trusts prior checkpoints** — won't catch tampering behind one. Run a full `verify_chain()` periodically for a real audit |
| **Durability** | `mirror.append_mirrored()` writes each entry to a `.jsonl` alongside the DB; rebuildable from just that file | The mirror only helps if anchored somewhere an attacker doesn't also control (e.g. committed to git separately) |
| **Hard $0 ceiling** | `router.SpendLedger` enforces a real call/dollar cap, logs every attempt as a normal chain entry | Only enforced where you actually call it — nothing stops code that bypasses the router |
| **Semantic recall (merge)** | `SemanticView` re-derives everything from the log fresh each call — no cache to go stale | O(n²) contradiction scan and a full TF-IDF refit on every call — fine to a few thousand memories, a documented scale ceiling, not a silent one |

## Bugs found and fixed

Every entry here came with its own regression test, so if the mistake ever
tries to sneak back in, it gets caught immediately instead of silently
breaking things again. `docs/ROADMAP.md` has the full phase-by-phase story;
the ones below are specifically what real network access and a real
Windows + Ollama environment surfaced that a network-less sandbox couldn't:

- **`ollama run` corrupted output on Windows two independent ways.**
  `subprocess.run(text=True)` decodes stdout using the process's locale
  codepage (cp1252 on this machine), not UTF-8 — any non-cp1252 byte in a
  model's reply raised `UnicodeDecodeError` in a background thread.
  Separately, `ollama run` renders as if for an interactive terminal, so
  raw ANSI cursor-control codes (e.g. `\x1b[7D\x1b[K`) leaked into the
  captured text. Both reproduced against a real local Ollama install
  before being fixed by talking to Ollama's HTTP API directly
  (`ollama_client.py`) instead of shelling out.
- **`MemoryView.citation()` crashed on narrower Windows codepages.**
  It embedded a Unicode em-dash; encoding that on cp437 (a common `cmd.exe`
  default) raises `UnicodeEncodeError` and crashes the process outright —
  reproduced directly against the exact string, not hypothetical. Fixed by
  using a plain ASCII separator in every string that's actually printed or
  raised at runtime (citations, and two similar exception messages);
  docstrings/comments were left alone since those are never printed.
- **`mcp_server.py` had never actually been run.** An earlier version
  targeted the `mcp` package's older `FastMCP` API from memory, with no
  network access available to verify it. The first real `pip install
  "mcp[cli]"` pulled mcp 2.x, which renamed `FastMCP` to `MCPServer` —
  immediate `ModuleNotFoundError`. Fixed by migrating to the real,
  installed API and testing it directly via `MCPServer.call_tool()`.
- **The MCP server's cached connection crashed under real dispatch.**
  `MCPServer` runs synchronous tool functions on a thread pool; a
  sqlite3 connection can only be used from the thread that created it.
  The first version cached one connection in a module global, which broke
  with `SQLite objects created in a thread can only be used in that same
  thread` the moment two tool calls landed on different worker threads —
  reproduced directly in-process, then fixed by opening a fresh connection
  per call instead (see the Robustness table above).

## Contradiction detection: what it catches, what it doesn't

Flags two active memories as conflicting when they share a strong anchor
and either (a) one negates the other ("is open" vs "is not open" — catches
yes/no, done/not-done, enabled/disabled) or (b) they differ on a real value
— a number or proper noun (dates, places, names, amounts). **Known miss:**
plain lowercase antonyms with no negation and no number ("the light is
red" vs "the light is green") — closing that needs real NLP, tracked in
`docs/ROADMAP.md`, not silently pretended to be solved.

## Paraphrase generation: what it catches, what it doesn't

`SynonymSwapParaphraser` (the zero-dependency fallback) only recognizes
literal phrases in its own small built-in table — it returns nothing
rather than fabricating a variant for wording it doesn't know, same
honesty standard as the rest of this project's fallback backends.
`OllamaParaphraser` is far more general (it's a real local model) but
still, like the underlying TF-IDF matcher, works on exact tokens: it
doesn't stem, so a paraphrase using "lives" won't match a query using
"living" or "live" unless the wording is genuinely identical. Swapping in
`SentenceTransformerBackend` narrows that gap further; paraphrasing and
better embeddings are complementary, not substitutes for each other.

## Roadmap ideas

See `docs/ROADMAP.md` for the full phase-by-phase history (including
every bug found and fixed along the way — the merge-time stale-vocabulary
bug and numpy-import leak, plus the Windows/Ollama/MCP bugs above) and
what's next: LangChain/CrewAI adapters, a proper hallucination benchmark
(LOCOMO/LongMemEval), peer-to-peer lesson exchange, a static-HTML chain
browser, and real NLP for the contradiction-detection antonym gap.

## License

**AGPLv3, dual-licensed with a commercial option** — see `LICENSE` and
`COMMERCIAL.md`.

Free to use, self-host, and modify under AGPLv3, including commercially —
with AGPLv3's core condition: if you run a modified version as a network
service, you must make that modified source available to your users. A
separate commercial license (terms negotiated directly, not fixed here) is
available for organizations that specifically don't want that condition.

**Why this over plain MIT or a Business Source License:** MIT alone gives
a well-funded competitor no reason not to just take this and sell a hosted
version with nothing owed back. A Business Source License is NOT
OSI-approved open source, which weakens the "real open-source project,
real community credibility" story that's part of the plan here. AGPLv3 is
OSI-approved open source (genuine stars, genuine community trust) while
its network-use clause is exactly what creates a reason for a company that
wants to avoid that clause to pay for a commercial license instead of
just forking it.

**Said plainly: this is a business judgment call, not a legal one — get
real legal review before actually relying on the commercial side to make
sales.** No commercial contract has been drafted; `COMMERCIAL.md` explains
the model, it isn't a signable agreement.

## Author

Maintained by [Nulfied](https://github.com/Nulfied). Commercial licensing
inquiries: see `COMMERCIAL.md`, or open an issue on this repo.